Manipulando tokens em Solana com Rust e Anchor

Web3 e Blockchain

Manipulando tokens em Solana com Rust e Anchor

Luiz Duarte
Escrito por Luiz Duarte em 08/06/2026
Junte-se a mais de 34 mil devs

Entre para minha lista e receba conteúdos exclusivos e com prioridade

Recentemente escrevi um tutorial aqui no blog ensinando como criar um novo token na blockchain Solana utilizando a Solana Program Library (SPL), que é a forma oficial de fazer isso. No tutorial de hoje quero ensinar como você usa tokens já existentes na criação de protocolos DeFi neste mesmo ecossistema, usando programação Rust com o framework Anchor.

Para que você consiga aproveitar e entender este tutorial, é necessário que você já entenda os fundamentos de programação DeFi em Solana, além de entender os fundamentos do funcionamento de tokens nesta mesma rede.

Vamos lá!

#1 – Estrutura geral do protocolo

Pegando como base o tutorial anterior de programação DeFi e adaptando-o para que use um spl-token ao invés de SOL, podemos iniciar o desenvolvimento mantendo algumas estruturas básicas que nos servem também na nova configuração. Para uma explicação detalhada delas, procure o tutorial anterior.

Nosso protocolo fará as mesmas duas operações de depósito e de saque, mas agora de spl-token, padrão 2022 (mais recente). Para isso, vamos usar a biblioteca anchor-spl que nos fornece algumas dependências que vão facilitar muito o nosso desenvolvimento. Importe-as no topo do arquivo lib.rs.

Mas para que ela esteja disponível, você deve ir no Cargo.toml mais próximo (que fica na pasta do programa) e ajustar as configurações abaixo, sendo que as demais configurações devem permanecer inalteradas:

E no Cargo.toml mais distante, que fica na raiz do projeto, eu precisei fazer esse ajuste:

Agora falando dos unit tests, vamos precisar instalar a biblioteca @solana/spl-token via NPM.

E depois carregar ela e a assert no módulo de testes.

Já nas variáveis globais dos testes, teremos algumas bem conhecidas e outras específicas dessa bateria de testes.

A saber:

  • provider: comunicação com a blockchain de testes;
  • user: carteira que vai rodar os testes;
  • program: o programa a ser tetado (TokenProtocolAnchor neste caso);
  • vaultPda: o PDA do vault que usaremos nos testes;
  • mint: o endereço do spl-token que vamos usar nos testes;
  • userTokenAccount: a token account do user que vamos usar nos testes;

Algumas dessas variáveis, bem como outros preparativos, nós faremos na função before, que executará antes do primeiro teste.

Começamos a before calculando o endereço PDA do vault e guardando na respectiva variável.

Depois, criamos um novo spl-token mint account para os testes, a fim de simular um token real. A createMint espera a conexão com a blockchain, a carteira que vai pagar pelo rent, a pubkey que vai ser a authority de emissão da nova moeda, de freeze (null), o número de casas decimais, mas dois parâmetros opcionais e por último o program id a ser utilizado na sua criação, sendo que aqui estamos trabalhando com o padrão mais recente que é o spl-token-2022.

Na sequência, criamos o ATA para o usuário de testes com a função createAssociatedTokenAccount, que espera a conexão, o pagador do rent, o token mint account, o usuário dono dessa account e por último o program id a ser usado.

Por fim, usamos a getAssociatedTokenAddress para calcular o ATA do nosso usuário dos testes e com essa informação mintamos algumas moedas para que ele tenha saldo nos testes (10 tokens em lamports).

Com isso fizemos os preparativos iniciais.

Clique para saber mais
Clique para saber mais

#2 – Depósito de SPL-Token

Para conseguirmos fazer o depósito de um spl-token, precisamos primeiro criar o contexto para o mesmo, como abaixo.

Com os campos sendo:

  • vault: controle do nosso protocolo sobre qual é o saldo depositado de cada usuário. Conta com init_if_needed para ser criado se ainda não existir (que deve estar habilitado no Cargo.toml) e usa como seed a pubkey do usuário para garantir que cada um tenha apenas um vault;
  • mint: a Mint Token Account, que representa a moeda que estamos trabalhando com a constraint de que o programa que a criou deve ser o mesmo que está sendo passado mais embaixo. Repare também que uso o tipo InterfaceAccount ao invés de Account, pois é para spl-token-2022;
  • user_token_account: a Token Account que vai depositar, que deve ser a ATA do usuário e ter sido criada com o programa de token presente no contexto;
  • vault_token_account: a Token Account do vault, que irá receber o depósito e, mais tarde, irá fazer as transferências de saque. Essa conta é criada automaticamente se necessário, é única por usuário (usa a pubkey na seed) e tem como constraints adicionais que o token mint dessa nova account seja o mesmo do campo mint do contexto, que o authority dessa nova account seja o vault e que o program seja o mesmo sendo passado;
  • user: usuário que vai fazer/assinar o depósito;
  • token_program: o programa SPL-Token que vai fazer a transferência de depósito, sendo que aqui estamos usando o tipo TokenInterface pois estamos usando spl-token-2022;
  • system_program: o programa de sistema que vai criar as accounts;
  • rent: propriedade exigida internamente pela spl-token para conseguir pagar pela token account criada (se houver);

Atenção aqui à dinâmica entre user_token_account e vault_token_account. No depósito, o saldo sai da user_token… e vai para vault_token…, enquanto que no saque é o contrário.

Agora vamos à função de depósito, que vai usar esse contexto, como abaixo:

Começamos testando se a quantia é válida com a macro require! e jogando o custom error se necessário. O mesmo para a verificação de saldo na token account do usuário.

Depois, como a transferência ocorre com nosso programa chamando o programa spl-token, temos de fazer uma Cross-Program Invocation (CPI). Iniciamos a mesma configurando um objeto TransferChecked onde informamos a account de from (user), to (vault), o token (mint) e a authority que vai assinar a transferência (user).

Na sequência carregamos um novo CpiContext com o token program e cpi_accounts e mandamos realizar a transferência com ele.

Terminada a transferência, é hora de atualizar o vault, para controle próprio do nosso programa, o que fazemos setando o owner correto do mesmo e o novo saldo, incrementado de maneira segura, com checked_add e unwrap. Ao término da atualização dos controles, emitimos o evento de depósito.

#3 – Testes de Depósito

Agora que temos tudo programado para realizar depósitos, vamos voltar ao nosso arquivo de testes. Vamos começar criando uma função que faz somente o depósito em si, que vamos usar em vários testes.

Essa função espera a quantidade a ser depositada e a primeira coisa que faz é calcular o PDA do token account do vault que irá receber o depósito, lembrando que é apenas um vault e um vault token account por usuário.

Aí chamamos a função deposit informando a amount e passando em accounts o vault PDA, o mint token account, o user token account, o vault token account, o usuário que está depositando, o id do programa spl-token, o id do programa system e a sysvar de rent para uso interno do spl-token.

Agora vamos escrever um primeiro cenário de depósito bem sucedido, onde o usuário deposita 1 token em nosso protocolo.

Começamos definindo a quantidade, na sequência chamamos a função de depósito e por fim verificamos o vault para ver se o saldo foi atualizado corretamente.

Agora vamos esrever outro teste, um com cenário de falha no depósito.

Aqui não tem nada de especial, é o mesmo teste que já tínhamos do protocolo anterior.

Roadmap Web3
Roadmap Web3

#4 – Saque de SPL-Token

Para conseguirmos fazer o saque de um spl-token, precisamos primeiro criar o contexto para o mesmo, como abaixo.

Com os campos sendo:

  • vault: controle do nosso protocolo sobre qual é o saldo depositado de cada usuário. Aqui ele espera que já tenha sido criado, através da chamada de depósito provavelmente, apenas usando seeds e bump como constraints, além de uma constraint específica para garantir que somente o owner do vault consiga chamar a função;
  • mint: a Mint Token Account, que representa a moeda que estamos trabalhando. Ela usa o tipo InterfaceAccount pois usa o padrão spl-token-2022 e aliás isso é reforçado através de constraint;
  • vault_token_account: a Token Account do vault, que irá transferir o saque para o usuário. Essa conta já foi criada e usa as constraints para garantir que somente o owner faça o saque e somente da moeda (mint) e programa corretos;
  • user_token_account: a Token Account do usuário que vai sacar;
  • user: usuário que vai fazer/assinar o saque;
  • token_program: o programa SPL-Token que vai fazer a transferência de saque, no padrão 2022 (TokenInterface);

Note que diferente do context de depósito, aqui não precisamos do system program pois não há criação de novas contas e também não precisamos da informação de rent pelo mesmo motivo.

Agora vamos à função de saque, que vai usar esse contexto, como abaixo:

Começamos testando se a quantia é válida com a macro require! e jogando o custom error se necessário, incluindo um segundo teste para ver se o vault tem saldo (balance) suficiente também.

Na sequência temos de fazer algo ligeiramente complexo que é gerar as signer seeds do vault PDA. Isso porque PDAs não podem assinar transações por padrão, pois eles não possuem chaves privadas. Assim, para que o próprio protocolo possa fazer a chamada em nome do vault PDA ele precisa ter as seeds dele, incluindo o bump. Isso é feito derivando seu endereço novamente e depois regerando os bytes de suas seeds + bump.

Antes de fazer a transferência em si, atualizamos o balance do vault, seguindo o padrão Checks-Effects-Interactions.

Como a transferência ocorre com nosso programa chamando o programa spl-token, temos de fazer uma Cross-Program Invocation (CPI). Iniciamos a mesma configurando um objeto TransferChecked onde informamos a account de from (vault), token (mint), to (user) e a authority que vai “assinar” a transferência (vault).

Na sequência carregamos um novo CpiContext com o token program e cpi_accounts e mandamos realizar a transferência com ele assinando a mesma com signer_seeds.

Terminada a transferência, o vault já foi atualizado lá no início da função, então apenas emitimos o evento de saque.

Curso Beholder
Curso Beholder

#5 – Testes de Saque

Agora que temos tudo programado para realizar saques, vamos voltar ao nosso arquivo de testes. Nossos testes de saque vão precisar usar a função de depósito, já que infelizmente os testes acabam compartilhando a mesma infraestrutura e estado dos programas e accounts. Vamos escrever um primeiro cenário de saque bem sucedido, onde o usuário saca meio token em nosso protocolo.

Logo após a variável de quantidade e o depósito inicial seream inicializados, nós derivamos o PDA do token account do vault e pegamos o saldo inicial usando a função getTokenAccountBalance que já vem pronta na @solana/web3.js especialmente para lidar com spl-tokens (injetada no objeto connection do Anchor).

A chamada em si para o saque dispensa grandes explicações, apenas atenção à passagem correta dos parâmetros, que são o vault PDA, o mint token account, o vault token account (PDA também), o user token account, o signer (user) e o programa spl-token (2022).

Após o saque, pega-se uma nova amostra do saldo e verifica-se se atualizou corretamente.

Agora vamos escrever outro cenário para cobrir a possibilidade de não haver saldo suficiente para o saque.

Aqui não tem novidades na programação em si, apenas na lógica do teste.

E por fim, vamos escrever também um unit test para cobrir o cenário de uma tentativa de saque de moedas que não são suas.

Aqui a única diferença é a geração de uma nova wallet account apenas para ser usada na assinatura da transação, a fim de forçar que seja alguém diferente tentando fazer o saque das moedas do usuário padrão dos testes (que é o real dono delas).

Agora se você rodar um anchor build, deve ter o seguinte resultado.

Espero que tenha gostado de mais esse tutorial.

Até a próxima!

Curso Web23
Curso Web23
TAGS:

Olá, tudo bem?

O que você achou deste conteúdo? Conte nos comentários.

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *