SPL Token in Programs
On-chain programs interact with SPL Token via cross-program invocations (CPIs). Anchor 0.32.1 anchor-spl wraps account validation, deserialization, and instruction builders for production-grade token logic.
Search across all documentation pages
On-chain programs interact with SPL Token via cross-program invocations (CPIs). Anchor 0.32.1 anchor-spl wraps account validation, deserialization, and instruction builders for production-grade token logic.
use anchor_spl::token::{self, Mint, Token, TokenAccount, TransferChecked};
pub mint: Account<'info, Mint>,
#[account(mut, token::mint = mint, token::authority = authority)]
pub from: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
// ...
token::transfer_checked(cpi_ctx, amount, decimals)?;When to reach for this:
use anchor_lang::prelude::*;
use anchor_spl::associated_token::AssociatedToken;
use anchor_spl::token::{self, Mint, Token, TokenAccount, TransferChecked};
#[derive(Accounts)]
pub struct Deposit<'info> {
pub mint: Account<'info, Mint>,
#[account(
mut,
associated_token::mint = mint,
associated_token::authority = user,
associated_token::token_program = token_program,
)]
pub user_ata: Account<'info, TokenAccount>,
#[account(
mut,
token::mint = mint,
token::authority = vault_authority,
)]
pub vault_ata: Account<'info, TokenAccount>,
pub user: Signer<'info>,
/// CHECK: PDA authority over vault ATA
pub vault_authority: UncheckedAccount<'info>,
pub token_program: Program<'info, Token>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub system_program: Program<'info, System>,
}
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
require!(amount > 0, ErrorCode::ZeroAmount);
let decimals = ctx.accounts.mint.decimals;
let cpi = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
TransferChecked {
mint: ctx.accounts.mint.to_account_info(),
from: ctx.accounts.user_ata.to_account_info(),
to: ctx.accounts.vault_ata.to_account_info(),
authority: ctx.accounts.user.to_account_info(),
},
);
token::transfer_checked(cpi, amount, decimals)?;
Ok(())
}
#[error_code]
pub enum ErrorCode {
ZeroAmount,
}What this demonstrates:
Account<'info, TokenAccount> validates owner is the classic Token program - it rejects Token-2022 accountstoken::mint and associated_token constraints reduce manual pubkey checkstransfer_checked includes decimals for safer asset handling; the unchecked Transfer is deprecated and Token-2022 rejects it on transfer-fee mintsTokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DAanchor_spl::token_interface supports Token-2022 with TokenInterface type| Instruction | Signer | Validates |
|---|---|---|
transfer_checked (prefer; transfer is deprecated) | Source authority | Sufficient balance, mint, decimals |
mint_to | Mint authority | Supply increase |
burn | Account owner | Balance decrease |
approve | Owner | Delegate ceiling |
use anchor_lang::prelude::*;
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};
// Never mix the two module trees. `anchor_spl::token::{Mint, TokenAccount}` are
// classic-Token-only and their owner check rejects Token-2022 accounts; the
// token_interface types dispatch over both programs.
#[derive(Accounts)]
pub struct EitherProgram<'info> {
pub mint: InterfaceAccount<'info, Mint>,
#[account(mut, token::mint = mint)]
pub from: InterfaceAccount<'info, TokenAccount>,
pub token_program: Interface<'info, TokenInterface>,
}token::mint = mint constraint on all token accounts.seeds + bump constraints on authority PDA.Program<'info, Token> pins Tokenkeg..., and Account<'info, TokenAccount> rejects Token-2022-owned accounts, so there is no "just pass the Token-2022 program account" path. Fix: switch the whole account struct to anchor_spl::token_interface (InterfaceAccount + Interface<TokenInterface>), or in native Rust use spl_token_2022::instruction::* - spl_token::instruction::* calls check_program_account and returns IncorrectProgramId for any id but Tokenkeg....anchor_spl::token and anchor_spl::token_interface types - the classic types' owner check rejects extension mints. Fix: pick one module tree per account struct.with_signer seeds. Fix: CpiContext::new_with_signer with vault seeds.ReentrancyNotAllowed. Fix: budget CU and CPI depth; see transfer hook docs.
| Alternative | Use When | Don't Use When |
|---|---|---|
Raw invoke + spl_token crate | Non-Anchor program | Anchor project with anchor-spl available |
token_interface | Multi-program (Token + Token-2022) | Classic Token only forever |
| Client-only token moves | User signs all transfers | Custodial vault logic |
Anchor crate wrapping SPL program CPI helpers and account types for Token, ATA, metadata, and more.
Strongly recommended in Anchor - deserializes and checks Token program ownership.
CpiContext::new_with_signer with seeds matching vault authority PDA.
Prefer transfer_checked in programs - includes decimals guard.
Yes. Batch multiple token instructions in one transaction from client; program can loop CPIs within CU budget.
Switch to token_interface types and pass actual mint owner program in constraints.
init or init_if_needed on ATA with payer signer - often protocol treasury.
Yes via CloseAccount CPI when balance is zero and close authority signs.
LiteSVM 0.6.x or Surfpool 0.12.0 local validator with Token program deployed by default.
Legacy SPL multisig still exists - most apps use PDA vaults instead.
Required in account list when instruction creates ATAs via CPI.
Token program errors return as program errors - map to custom #[error_code] for UX.
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, anchor-lang 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, Surfpool 0.12.0, and LiteSVM 0.6.x.
Reviewed by Chris St. John·Last updated Jul 19, 2026