SPL Token CPIs
anchor-spl exposes typed CPI functions for SPL Token: transfer_checked, mint_to, burn, approve, and more. They wrap account metas and instruction data for the token program.
Search across all documentation pages
anchor-spl exposes typed CPI functions for SPL Token: transfer_checked, mint_to, burn, approve, and more. They wrap account metas and instruction data for the token program.
anchor_spl::token::{transfer, Transfer} is deprecated. It omits the mint and decimals, so the token program cannot verify you are moving the asset you think you are. Use transfer_checked / TransferChecked instead, and reach for anchor_spl::token_interface when you need Token-2022 as well as classic Token.
use anchor_spl::token_interface::{self, Burn, MintTo, TransferChecked};
token_interface::mint_to(cpi_ctx, amount)?;
token_interface::transfer_checked(cpi_ctx, amount, decimals)?;
token_interface::burn(cpi_ctx, amount)?;When to reach for this: Your program moves SPL tokens without reimplementing token instruction layouts.
use anchor_lang::prelude::*;
use anchor_spl::token_interface::{
self, Mint, TokenAccount, TokenInterface, TransferChecked,
};
#[derive(Accounts)]
pub struct Deposit<'info> {
#[account(mut)]
pub user: Signer<'info>,
pub mint: InterfaceAccount<'info, Mint>,
#[account(mut, token::mint = mint)]
pub user_ata: InterfaceAccount<'info, TokenAccount>,
#[account(mut, token::mint = mint)]
pub vault_ata: InterfaceAccount<'info, TokenAccount>,
pub token_program: Interface<'info, TokenInterface>,
}
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
token_interface::transfer_checked(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
TransferChecked {
from: ctx.accounts.user_ata.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.vault_ata.to_account_info(),
authority: ctx.accounts.user.to_account_info(),
},
),
amount,
ctx.accounts.mint.decimals,
)?;
Ok(())
}What this demonstrates:
transfer_checked carries the mint and decimals so a mint swap cannot slip throughtoken::mint = mint pins both ATAs to the same mintInterface + InterfaceAccount accept classic Token and Token-2022use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};
pub mint: InterfaceAccount<'info, Mint>,
pub vault_ata: InterfaceAccount<'info, TokenAccount>,
pub token_program: Interface<'info, TokenInterface>,Use when supporting both classic Token and Token-2022 programs. Keep the whole set consistent: InterfaceAccount must be paired with the token_interface Mint / TokenAccount types. Mixing anchor_spl::token::Mint into an InterfaceAccount does not compile.
token::transfer - No mint or decimals check. Fix: Use transfer_checked / TransferChecked.transfer_checked fails the tx. Fix: Pin both ATAs with token::mint = mint and pass that mint's decimals.anchor_spl::token::Mint with InterfaceAccount - Type error. Fix: Take both from token_interface.| Alternative | Use When | Don't Use When |
|---|---|---|
| Raw token instruction CPI | Custom layouts | Standard SPL flows |
| Client-side token transfers only | User signs token ix | Program custody |
Custom codes in IDL plus #[msg] strings.
emit_cpi! costs more CU but improves capture reliability.
Avoid except behind feature flags.
From IDL via Anchor or Codama codegen.
Yes, #[event] types are listed.
Yes with @ MyError::Variant.
Each log line and formatted string.
anchor test expecting error codes.
Yes, but not an API contract; use events.
See Related for spl cpi.
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 16, 2026