Back-End

10 jun, 2026

Manipulando tokens em Solana com Rust e Anchor

Publicidade

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.



#[account]
pub struct Vault {
    pub owner: Pubkey,
    pub balance: u64,
}


#[error_code]
pub enum VaultError {
    #[msg("Amount must be greater than zero")]
    InvalidAmount,


    #[msg("Insufficient balance")]
    InsufficientBalance,
}


#[event]
pub struct DepositEvent {
    pub user: Pubkey,
    pub amount: u64,
    pub new_balance: u64,
}


#[event]
pub struct WithdrawEvent {
    pub user: Pubkey,
    pub amount: u64,
    pub new_balance: u64,
}

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.

#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.

#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.

#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!