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.
Recipe
use anchor_spl::token::{self, Token, TokenAccount, Transfer};
#[account(mut)]
pub from: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
// ...
token::transfer(cpi_ctx, amount)?;When to reach for this:
- Escrow, vault, or payment programs moving SPL tokens
- Validating user ATAs in DeFi instructions
- Minting reward tokens from a PDA mint authority
- Enforcing mint match and signer rules on token CPIs
Working Example
use anchor_lang::prelude::*;
use anchor_spl::associated_token::AssociatedToken;
use anchor_spl::token::{self, Mint, Token, TokenAccount, Transfer, TransferChecked};
#[derive(Accounts)]
pub struct Deposit<'info> {
pub mint: Account<'info, Mint>,
#[account(
mut,
associated_token::mint = mint,
associated_token::authority = user,
)]
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 Token programtoken::mintandassociated_tokenconstraints reduce manual pubkey checkstransfer_checkedincludes decimals for safer asset handling
Deep Dive
How It Works
- SPL Token program ID:
TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA - CPI passes serialized instruction + account metas to Token program
- Signer requirements: owner for debit, mint authority for mint_to, etc.
anchor_spl::token_interfacesupports Token-2022 withTokenInterfacetype
Common CPI Instructions
| Instruction | Signer | Validates |
|---|---|---|
transfer / transfer_checked | Source authority | Sufficient balance |
mint_to | Mint authority | Supply increase |
burn | Account owner | Balance decrease |
approve | Owner | Delegate ceiling |
Rust Notes
use anchor_spl::token_interface::{
TokenAccount as InterfaceTokenAccount,
TokenInterface,
transfer_checked as interface_transfer_checked,
};
// Use TokenInterface + InterfaceTokenAccount for Token-2022 mintsGotchas
- Missing mint equality check - vault ATA wrong asset. Fix:
token::mint = mintconstraint on all token accounts. - UncheckedAccount as vault authority - no PDA seed validation. Fix:
seeds+bumpconstraints on authority PDA. - Wrong token_program account - CPI to classic Token for Token-2022 mint fails. Fix: use
token_interfaceor pass correct program ID. - PDA cannot sign transfer - missing
with_signerseeds. Fix:CpiContext::new_with_signerwith vault seeds. - Reentrancy via token CPI - external hook programs (Token-2022) can call back. Fix: guard state; see transfer hook docs.
Alternatives
| 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 |
FAQs
What is anchor-spl?
Anchor crate wrapping SPL program CPI helpers and account types for Token, ATA, metadata, and more.
Must I use Account<TokenAccount>?
Strongly recommended in Anchor - deserializes and checks Token program ownership.
How do PDAs sign token CPIs?
CpiContext::new_with_signer with seeds matching vault authority PDA.
transfer vs transfer_checked?
Prefer transfer_checked in programs - includes decimals guard.
Can one instruction CPI multiple transfers?
Yes. Batch multiple token instructions in one transaction from client; program can loop CPIs within CU budget.
How do I support Token-2022?
Switch to token_interface types and pass actual mint owner program in constraints.
Who pays ATA creation in programs?
init or init_if_needed on ATA with payer signer - often protocol treasury.
Can I close token accounts in programs?
Yes via CloseAccount CPI when balance is zero and close authority signs.
How to test token CPIs?
LiteSVM 0.6.x or Surfpool 0.12.0 local validator with Token program deployed by default.
What about multisig token accounts?
Legacy SPL multisig still exists - most apps use PDA vaults instead.
Do I need associated_token_program account?
Required in account list when instruction creates ATAs via CPI.
How are errors mapped?
Token program errors return as program errors - map to custom #[error_code] for UX.
Related
- Associated Token Accounts (ATAs) - ATA constraints
- Transfers & Delegates - delegate flows
- Cross-Program Invocations (CPI) - CPI fundamentals
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.