Create your own
Lesson illustration

Token Transfer Logic

Hello! Welcome back to our module on working with Solana tokens.

In our last few lessons, we've successfully brought a new token into existence. We've learned to create its blueprint (the mint), give users a place to store it (the Associated Token Account), and even generate new supply through minting (mint_to).

Now, we'll complete the token's core functionality by enabling it to move between users. The goal for this lesson is to implement the program logic to transfer tokens between two token accounts. This is one of the most fundamental operations in any blockchain application involving assets, from simple payments to complex DeFi trades.

Just like minting, this will be achieved with a Cross-Program Invocation (CPI), but this time we'll be calling the transfer_checked instruction on the SPL Token Program.

How Token Transfers Work on Solana

Transferring tokens involves moving a certain amount from a source token account to a destination token account. For this to be valid, both accounts must belong to the same mint.

Critically, a transfer can only be authorized by the owner (or authority) of the source token account. This is the security model that prevents anyone from spending your tokens. In our programs, this authority can be either:

  1. A user who signs the transaction (Signer).
  2. A Program Derived Address (PDA) controlled by our program.

We will cover both of these essential patterns today.

The transfer_checked Instruction

The SPL Token Program provides an instruction called transfer_checked. The _checked suffix is important: it means the token program will perform extra validation to ensure the mint account provided in the instruction matches the mint of both the source and destination token accounts. It also uses the mint's decimals property to interpret the transfer amount correctly. This is the safest and recommended way to perform transfers.

Let's dive into the official Anchor documentation, which provides a clear guide on this topic.

Transfer Tokens

The following guide from the Anchor documentation explains how to perform token transfers. We'll focus on the user-initiated transfer first.

Please read from the beginning of the document down to the end of the first code example under the 'Transfer Tokens via CPI' heading. Focus on understanding the purpose of transfer_checked, the accounts required by the TransferTokens struct, and the steps to build and execute the CPI call.

Deconstructing a User-Initiated Transfer

Let's break down the code from the resource you just read. This first example covers the most common scenario: a user transferring their own tokens to someone else.

1. The TransferTokens Accounts Struct

The #[derive(Accounts)] struct defines and validates all the accounts our instruction needs.

#[derive(Accounts)]
pub struct TransferTokens<'info> {
    // The user authorizing the transfer. Must be the owner of the sender_token_account.
    #[account(mut)]
    pub signer: Signer<'info>,

    // The mint of the token being transferred.
    #[account(mut)]
    pub mint: InterfaceAccount<'info, Mint>,

    // The source token account. Its balance will decrease.
    #[account(mut)]
    pub sender_token_account: InterfaceAccount<'info, TokenAccount>,

    // The destination token account. Its balance will increase.
    #[account(mut)]
    pub recipient_token_account: InterfaceAccount<'info, TokenAccount>,
    
    // The SPL Token Program we will be invoking.
    pub token_program: Interface<'info, TokenInterface>,
}
  • signer: This is the user's wallet account. Anchor's Signer type ensures this account has signed the transaction. This signer must be the authority of the sender_token_account.
  • sender_token_account & recipient_token_account: These are the source and destination for the tokens. Both are marked mut because their balances will change.
  • mint: This account is needed for the transfer_checked instruction to verify the token type and its decimals.
  • token_program: We pass in the token program itself to perform the CPI. The TokenInterface type is a modern Anchor feature that allows your program to seamlessly interact with both the standard SPL Token program and the newer Token-2022 program.

2. The Instruction Logic

The function body constructs and executes the CPI.

pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64) -> Result<()> {
    // 1. Get the mint's decimals for the transfer amount.
    let decimals = ctx.accounts.mint.decimals;

    // 2. Define the accounts for the CPI.
    let cpi_accounts = TransferChecked {
        mint: ctx.accounts.mint.to_account_info(),
        from: ctx.accounts.sender_token_account.to_account_info(),
        to: ctx.accounts.recipient_token_account.to_account_info(),
        authority: ctx.accounts.signer.to_account_info(),
    };
    
    // 3. Get the token program to invoke.
    let cpi_program = ctx.accounts.token_program.to_account_info();

    // 4. Create the CpiContext.
    let cpi_context = CpiContext::new(cpi_program, cpi_accounts);

    // 5. Execute the CPI to the `transfer_checked` instruction.
    token_interface::transfer_checked(cpi_context, amount, decimals)?;

    Ok(())
}

This is a classic CPI pattern in Anchor:

  1. Prepare CPI Accounts: We populate the TransferChecked struct, which is a helper from anchor_spl that mirrors the accounts required by the SPL Token Program's instruction. The authority here is the signer who owns the from account.
  2. Create CPI Context: We use CpiContext::new because the authority (signer) is signing the transaction directly. There's no PDA involved yet.
  3. Invoke: We call token_interface::transfer_checked, passing the context, the raw token amount (e.g., 1000000 for 1 token with 6 decimals), and the decimals for validation.
Test your understanding!

In the TransferTokens Accounts struct, imagine a scenario where the sender_token_account is owned by Alice, but the signer who submitted the transaction is Bob. What would happen when the token_interface::transfer_checked function is called?

Show answer

The transaction would fail with an authority error inside the CPI. The SPL Token Program receives the transfer_checked instruction and sees that the authority provided (Bob's public key) does not match the owner field stored in the sender_token_account's data (Alice's public key). Therefore, the token program rejects the transfer, and the entire transaction fails.

Program-Initiated Transfers (PDA as Authority)

What if you want your program to control a token vault or an escrow account? In this case, the program itself needs to authorize transfers. We achieve this by making a PDA the authority of the token account. This pattern is identical to how we used a PDA as a mint_authority in the previous lesson.

Let's return to the Anchor documentation to see how this works.

Transfer Tokens

Now, let's look at the second pattern, where the program transfers tokens from an account it controls via a PDA.

Read the section titled 'Transfer Tokens with PDA token owner via CPI'. Notice how the authority of the token account is defined as a PDA and, crucially, how the CpiContext is constructed using with_signer to execute the CPI.

The key difference is in the CPI call. Instead of CpiContext::new, we use CpiContext::new(...).with_signer(...).

// ... inside the instruction logic ...

// 1. Prepare the PDA seeds
let signer_seeds: &[&[&[u8]]] = &[&[b"token", &[ctx.bumps.sender_token_account]]];

// 2. Create the CpiContext, "signing" with the PDA's seeds
let cpi_context = CpiContext::new(cpi_program, cpi_accounts)
    .with_signer(signer_seeds);

// 3. Execute the CPI
token_interface::transfer_checked(cpi_context, amount, decimals)?;

By providing the seeds and bump via .with_signer(), our program can generate the required signature for the PDA-owned account, thus authorizing the transfer. This powerful pattern is the foundation for automated token mechanics in Solana programs.

Testing a Token Transfer

After writing your on-chain program, you'll want to test it. This is typically done using TypeScript or JavaScript. The process involves setting up the scenario (creating accounts, minting tokens) and then calling your program's instruction.

The QuickNode guide, "How to Transfer SOL and SPL Tokens Using Anchor," contains a great example of a test script for a transfer function. Let's look at the structure of that test.

The core logic of the test script does the following:

  1. Setup Accounts: Creates a mint and the from and to Associated Token Accounts (ATAs).
  2. Mint Initial Tokens: Mints some tokens to the fromAta so there is a balance to transfer.
  3. Call the Program: Invokes the transferSplTokens instruction, passing the required accounts.
  4. Verify the Result: Fetches the balance of the toAta after the transaction and asserts that it equals the transferred amount.

Here's a condensed snippet from resource LINK showing the invocation part:

// Abridged example from the QuickNode guide

// Amount to transfer
const transferAmount = new BN(500);

// Build and send the transaction
await pg.program.methods
    .transferSplTokens(transferAmount)
    .accounts({
        from: fromKp.publicKey, // The authority
        fromAta: fromAta,       // Source ATA
        toAta: toAta,           // Destination ATA
        tokenProgram: TOKEN_PROGRAM_ID,
    })
    .rpc(); // simplified from the guide for clarity

// Check the balance of the recipient's account
const toTokenAccount = await pg.connection.getTokenAccountBalance(toAta);

// Assert that the balance is correct
assert.strictEqual(toTokenAccount.value.uiAmount, transferAmount.toNumber());

This demonstrates the full loop: writing Rust code for the on-chain logic and then using a client-side script to interact with and verify it. This is a workflow you'll use constantly in Solana development.

Conclusion

You have now mastered the complete basic lifecycle of an SPL Token: creation, minting, and transferring. These are the fundamental building blocks for nearly any application involving digital assets on Solana.

Here are the key takeaways:

  • Token transfers are performed via a CPI to the transfer_checked instruction on the SPL Token Program.
  • The TransferChecked struct in anchor_spl is used to build the context for the CPI call, requiring the mint, from (source), to (destination), and authority accounts.
  • For transfers authorized by a user, the user is the Signer, and you use CpiContext::new.
  • For transfers authorized by your program, a PDA owns the source token account, and you must use CpiContext::new(...).with_signer(...), passing the PDA's seeds.

In our next lesson, we will build upon this foundation to explore a special kind of token: the Non-Fungible Token (NFT). We will learn about the Metaplex Token Metadata program and see how it's used to attach metadata (like a name, symbol, and image URI) to a token mint, effectively turning it into an NFT.

Can't find a good explanation? Sign up and we'll make it for you

Sign up