Skip to main content

Overview

This guide covers the fundamental token operations you’ll need in Anchor programs:
  • Creating token accounts
  • Minting new tokens
  • Transferring tokens between accounts
  • Using Program Derived Addresses (PDAs) as authorities
All examples use the token_interface module, which works with both the Token Program and Token-2022 Program.

Creating Token Accounts

Before tokens can be held, you must create a token account. Anchor provides two approaches: Associated Token Accounts (ATAs) are the standard way to create token accounts for users:
use anchor_lang::prelude::*;
use anchor_spl::associated_token::AssociatedToken;
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};

#[derive(Accounts)]
pub struct CreateTokenAccount<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(
        init,
        payer = payer,
        associated_token::mint = mint,
        associated_token::authority = payer,
        associated_token::token_program = token_program,
    )]
    pub token_account: InterfaceAccount<'info, TokenAccount>,
    pub mint: InterfaceAccount<'info, Mint>,
    pub token_program: Interface<'info, TokenInterface>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
}
Key points:
  • Uses associated_token constraints
  • Address is deterministically derived from owner + mint
  • Requires the AssociatedToken program
  • Use init_if_needed to handle existing accounts (requires init-if-needed feature)

Custom Token Accounts with PDAs

For program-controlled accounts, use custom PDAs:
#[derive(Accounts)]
pub struct CreateVault<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(
        init,
        payer = payer,
        token::mint = mint,
        token::authority = vault, // PDA is its own authority
        token::token_program = token_program,
        seeds = [b"vault", mint.key().as_ref()],
        bump
    )]
    pub vault: InterfaceAccount<'info, TokenAccount>,
    pub mint: InterfaceAccount<'info, Mint>,
    pub token_program: Interface<'info, TokenInterface>,
    pub system_program: Program<'info, System>,
}
Key points:
  • Uses token constraints instead of associated_token
  • Requires seeds and bump for PDA derivation
  • Can set the PDA as its own authority for program-controlled transfers

Minting Tokens

Minting creates new token supply. Only the mint authority can mint tokens.

Basic Minting

use anchor_spl::token_interface::{self, Mint, MintTo, TokenAccount, TokenInterface};

pub fn mint_tokens(
    ctx: Context<MintTokens>,
    amount: u64
) -> Result<()> {
    let cpi_accounts = MintTo {
        mint: ctx.accounts.mint.to_account_info(),
        to: ctx.accounts.token_account.to_account_info(),
        authority: ctx.accounts.mint_authority.to_account_info(),
    };
    
    let cpi_ctx = CpiContext::new(
        ctx.accounts.token_program.to_account_info(),
        cpi_accounts
    );
    
    token_interface::mint_to(cpi_ctx, amount)?;
    Ok(())
}

#[derive(Accounts)]
pub struct MintTokens<'info> {
    #[account(mut)]
    pub mint_authority: Signer<'info>,
    #[account(mut)]
    pub mint: InterfaceAccount<'info, Mint>,
    #[account(mut)]
    pub token_account: InterfaceAccount<'info, TokenAccount>,
    pub token_program: Interface<'info, TokenInterface>,
}
Important: The amount should account for decimals. If the mint has 6 decimals, minting 1,000,000 base units equals 1 token.

Minting with PDA Authority

When the mint authority is a PDA, your program must sign with the PDA’s seeds:
pub fn mint_tokens(
    ctx: Context<MintTokens>,
    amount: u64
) -> Result<()> {
    // Get the seeds for the PDA
    let seeds = &[
        b"mint".as_ref(),
        &[ctx.bumps.mint]
    ];
    let signer_seeds = &[&seeds[..]];
    
    let cpi_accounts = MintTo {
        mint: ctx.accounts.mint.to_account_info(),
        to: ctx.accounts.token_account.to_account_info(),
        authority: ctx.accounts.mint.to_account_info(), // PDA signs
    };
    
    let cpi_ctx = CpiContext::new(
        ctx.accounts.token_program.to_account_info(),
        cpi_accounts
    ).with_signer(signer_seeds); // Add signer seeds
    
    token_interface::mint_to(cpi_ctx, amount)?;
    Ok(())
}

#[derive(Accounts)]
pub struct MintTokens<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(
        mut,
        seeds = [b"mint"],
        bump,
        // Mint authority is the PDA itself
    )]
    pub mint: InterfaceAccount<'info, Mint>,
    #[account(mut)]
    pub token_account: InterfaceAccount<'info, TokenAccount>,
    pub token_program: Interface<'info, TokenInterface>,
}
Key points:
  • Use ctx.bumps.<account_name> to access the bump seed
  • Call .with_signer() on the CPI context with the seeds
  • The PDA must be set as the mint authority when creating the mint

Transferring Tokens

Transfers move tokens from one token account to another. Use transfer_checked for safety.

Basic Transfer

use anchor_spl::token_interface::{self, TransferChecked, Mint, TokenAccount, TokenInterface};

pub fn transfer_tokens(
    ctx: Context<TransferTokens>,
    amount: u64
) -> Result<()> {
    let cpi_accounts = TransferChecked {
        from: ctx.accounts.from.to_account_info(),
        mint: ctx.accounts.mint.to_account_info(),
        to: ctx.accounts.to.to_account_info(),
        authority: ctx.accounts.authority.to_account_info(),
    };
    
    let cpi_ctx = CpiContext::new(
        ctx.accounts.token_program.to_account_info(),
        cpi_accounts
    );
    
    let decimals = ctx.accounts.mint.decimals;
    token_interface::transfer_checked(cpi_ctx, amount, decimals)?;
    Ok(())
}

#[derive(Accounts)]
pub struct TransferTokens<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,
    pub mint: InterfaceAccount<'info, Mint>,
    #[account(mut)]
    pub from: InterfaceAccount<'info, TokenAccount>,
    #[account(mut)]
    pub to: InterfaceAccount<'info, TokenAccount>,
    pub token_program: Interface<'info, TokenInterface>,
}
Key points:
  • transfer_checked verifies the mint and decimals for safety
  • The authority must own the source token account
  • Both token accounts must be for the same mint

Transfer from Program-Controlled Account

When transferring from a PDA-owned account, use PDA signing:
pub fn transfer_from_vault(
    ctx: Context<TransferFromVault>,
    amount: u64
) -> Result<()> {
    // PDA seeds for the vault
    let seeds = &[
        b"vault".as_ref(),
        ctx.accounts.mint.key().as_ref(),
        &[ctx.bumps.vault]
    ];
    let signer_seeds = &[&seeds[..]];
    
    let cpi_accounts = TransferChecked {
        from: ctx.accounts.vault.to_account_info(),
        mint: ctx.accounts.mint.to_account_info(),
        to: ctx.accounts.destination.to_account_info(),
        authority: ctx.accounts.vault.to_account_info(), // PDA signs
    };
    
    let cpi_ctx = CpiContext::new(
        ctx.accounts.token_program.to_account_info(),
        cpi_accounts
    ).with_signer(signer_seeds);
    
    let decimals = ctx.accounts.mint.decimals;
    token_interface::transfer_checked(cpi_ctx, amount, decimals)?;
    Ok(())
}

#[derive(Accounts)]
pub struct TransferFromVault<'info> {
    pub mint: InterfaceAccount<'info, Mint>,
    #[account(
        mut,
        seeds = [b"vault", mint.key().as_ref()],
        bump,
    )]
    pub vault: InterfaceAccount<'info, TokenAccount>,
    #[account(mut)]
    pub destination: InterfaceAccount<'info, TokenAccount>,
    pub token_program: Interface<'info, TokenInterface>,
}

Complete Example

Here’s a complete example showing mint creation, token account creation, minting, and transferring:
lib.rs
use anchor_lang::prelude::*;
use anchor_spl::{
    associated_token::AssociatedToken,
    token_interface::{self, Mint, MintTo, TokenAccount, TokenInterface, TransferChecked},
};

declare_id!("YourProgramIDHere");

#[program]
pub mod token_basics {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        msg!("Mint created: {}", ctx.accounts.mint.key());
        Ok(())
    }

    pub fn mint_tokens(ctx: Context<MintTokens>, amount: u64) -> Result<()> {
        let seeds = &[b"mint".as_ref(), &[ctx.bumps.mint]];
        let signer_seeds = &[&seeds[..]];

        let cpi_accounts = MintTo {
            mint: ctx.accounts.mint.to_account_info(),
            to: ctx.accounts.token_account.to_account_info(),
            authority: ctx.accounts.mint.to_account_info(),
        };

        let cpi_ctx = CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            cpi_accounts,
        )
        .with_signer(signer_seeds);

        token_interface::mint_to(cpi_ctx, amount)?;
        Ok(())
    }

    pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64) -> Result<()> {
        let cpi_accounts = TransferChecked {
            from: ctx.accounts.from.to_account_info(),
            mint: ctx.accounts.mint.to_account_info(),
            to: ctx.accounts.to.to_account_info(),
            authority: ctx.accounts.authority.to_account_info(),
        };

        let cpi_ctx = CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            cpi_accounts,
        );

        token_interface::transfer_checked(cpi_ctx, amount, ctx.accounts.mint.decimals)?;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(
        init,
        payer = payer,
        mint::decimals = 6,
        mint::authority = mint,
        seeds = [b"mint"],
        bump
    )]
    pub mint: InterfaceAccount<'info, Mint>,
    pub token_program: Interface<'info, TokenInterface>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct MintTokens<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(
        mut,
        seeds = [b"mint"],
        bump
    )]
    pub mint: InterfaceAccount<'info, Mint>,
    #[account(
        init_if_needed,
        payer = payer,
        associated_token::mint = mint,
        associated_token::authority = payer,
        associated_token::token_program = token_program,
    )]
    pub token_account: InterfaceAccount<'info, TokenAccount>,
    pub token_program: Interface<'info, TokenInterface>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct TransferTokens<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,
    pub mint: InterfaceAccount<'info, Mint>,
    #[account(mut)]
    pub from: InterfaceAccount<'info, TokenAccount>,
    #[account(mut)]
    pub to: InterfaceAccount<'info, TokenAccount>,
    pub token_program: Interface<'info, TokenInterface>,
}

Common Patterns

Checking Token Balances

let balance = ctx.accounts.token_account.amount;
require!(balance >= amount, ErrorCode::InsufficientBalance);

Validating Mint and Owner

#[account(
    constraint = token_account.mint == mint.key() @ ErrorCode::InvalidMint,
    constraint = token_account.owner == authority.key() @ ErrorCode::InvalidOwner,
)]
pub token_account: InterfaceAccount<'info, TokenAccount>,

Burning Tokens

use anchor_spl::token_interface::{self, Burn};

let cpi_accounts = Burn {
    mint: ctx.accounts.mint.to_account_info(),
    from: ctx.accounts.token_account.to_account_info(),
    authority: ctx.accounts.authority.to_account_info(),
};

let cpi_ctx = CpiContext::new(
    ctx.accounts.token_program.to_account_info(),
    cpi_accounts
);

token_interface::burn(cpi_ctx, amount)?;

Best Practices

  1. Use transfer_checked over transfer - It validates mint and decimals
  2. Use init_if_needed for ATAs - Handles existing accounts gracefully (requires feature flag)
  3. Store bump seeds - Use ctx.bumps.<account> instead of recomputing
  4. Use InterfaceAccount and Interface - Works with both token programs
  5. Validate token account relationships - Check mint and owner match expectations
  6. Handle decimals correctly - Remember amounts are in base units

Next Steps

Build docs developers (and LLMs) love