Tokens and CPI
SPL token account types, constraints, CPI helpers, and Token-2022 interfaces in anchor-spl-v2.
SPL token programs own token mints and token accounts. Your program usually does not edit those accounts directly. It validates the accounts it was given, then calls the Token Program through CPI.
anchor-spl-v2 provides zero-copy SPL account types, namespaced constraints, CPI helpers, associated token address utilities, and Token-2022 interface account support.
The token surface is built like the rest of Anchor. Account wrappers implement shared traits, constraints are downstream AccountConstraint markers, and CPI helpers use CpiHandle<'a> so token instructions can run through the cheaper CPI path.
If you are new to SPL tokens, start with the basic pages in order. Create a mint first, create a token account for that mint, mint tokens into it, then transfer tokens between token accounts.
SPL token basics
Create mints, create token accounts, mint tokens, and transfer tokens with Anchor SPL wrappers.
Token-2022 and extensions
Accept Token or Token-2022 accounts with InterfaceAccount<T> and parse TLV extensions.
Core modules#
The SPL token basics pages cover the legacy Token Program path. Reach for Token-2022 and extensions only when an instruction must accept Token-2022 accounts or parse extension state.
SPL token basics
Common SPL token workflows with anchor-spl-v2.
This section covers the common SPL token operations from an Anchor program. The examples focus on the legacy Token Program path because that surface is the most complete today. Token-2022 interface accounts and extension readers are covered separately.
A mint defines a token. A token account holds a balance for one mint and one authority. Minting creates new units of a token, and transferring moves existing units between token accounts. Anchor checks the mint, authority, and writable flags before your handler invokes the Token Program.
The SPL surface mirrors Anchor’s normal account model:
Account<Mint>zero-copies SPL mint dataAccount<TokenAccount>zero-copies SPL token account datamint::*andtoken::*constraints are downstreamAccountConstraintmarkers- CPI helpers come from
anchor_spl_v2::token::cpi - CPI account structs are built from
cpi_handle()andcpi_handle_mut()
Create mint
Initialize an SPL mint with mint::decimals and mint::authority.
Create token account
Initialize a token account with token::mint and token::authority.
Mint tokens
Call the token program through token::cpi::mint_to.
Transfer tokens
Transfer with transfer or transfer_checked.
Common workflow#
Most token programs follow this order:
- Create a mint with
mint::decimalsandmint::authority. - Create token accounts with
token::mintandtoken::authority. - Mint tokens by invoking
token::cpi::mint_toormint_to_checked. - Transfer tokens by invoking
token::cpi::transfer_checked.
For program-controlled tokens, make the mint authority or token account authority a PDA and pass signer seeds through CpiContext::with_signer().
Create a token mint
Create and initialize SPL token mint accounts with namespaced mint constraints.
A mint account is the Token Program account that represents a token on Solana. The mint address is the token’s unique identifier, and the account stores global token state such as supply, decimals, mint authority, and optional freeze authority.
Anchor models the legacy SPL Token mint as Account<Mint> from anchor_spl_v2::mint. The wrapper is zero-copy over the SPL Token mint layout. It does not add an Anchor discriminator because the account is owned and initialized by the Token Program.
Mint account data#
The base mint layout is 82 bytes:
pub struct Mint { mint_authority_flag: [u8; 4], mint_authority: Address, supply: [u8; 8], decimals: u8, is_initialized: u8, freeze_authority_flag: [u8; 4], freeze_authority: Address,}The fields are private in anchor-spl-v2. Read them through accessors such as supply(), decimals(), is_initialized(), mint_authority(), and freeze_authority().
Usage#
The usual import shape is:
use { anchor_lang_v2::prelude::*, anchor_spl_v2::mint::{self, Mint},};The account constraints handle the full setup. They create the account, allocate the Token Program mint size, transfer ownership to the Token Program, and invoke InitializeMint2:
#[derive(Accounts)]pub struct CreateMint { #[account(mut)] pub payer: Signer, pub authority: UncheckedAccount, #[account( init, payer = payer, mint::decimals = 6, mint::authority = authority, )] pub mint: Account<Mint>, pub token_program: Program<Token>, pub system_program: Program<System>,}The handler does not need to call the token program manually. Initialization happens in the generated account-validation path before the handler runs:
pub fn create_mint(_ctx: &mut Context<CreateMint>) -> Result<()> { Ok(())}Account constraints#
Use these constraints together for a new legacy SPL Token mint:
After initialization, the same namespaced constraints validate existing mints:
#[derive(Accounts)]pub struct UseMint { pub expected_authority: UncheckedAccount, #[account( mint::decimals = 6, mint::authority = expected_authority, )] pub mint: Account<Mint>,}Address choices#
Use a normal account address when a client should generate the mint keypair and pass it to the instruction:
#[derive(Accounts)]pub struct CreateMint { #[account(mut)] pub payer: Signer, pub authority: UncheckedAccount, #[account( init, payer = payer, mint::decimals = 6, mint::authority = authority, mint::freeze_authority = authority, )] pub mint: Account<Mint>, pub token_program: Program<Token>, pub system_program: Program<System>,}The client or Rust test must include the mint keypair as a signer because the address is not a PDA.
Use seeds and bump when the program should own a deterministic mint address. For program-controlled minting, a separate authority PDA keeps later CPI code simple:
#[derive(Accounts)]pub struct CreateMint { #[account(mut)] pub payer: Signer, #[account(seeds = [b"mint-authority"], bump)] pub mint_authority: UncheckedAccount, #[account( init, payer = payer, seeds = [b"mint"], bump, mint::decimals = 6, mint::authority = mint_authority, mint::freeze_authority = mint_authority, )] pub mint: Account<Mint>, pub token_program: Program<Token>, pub system_program: Program<System>,}Later CPI calls can sign with the mint_authority seeds to mint tokens. A design can use the same PDA as both the mint address and the mint authority, but a separate authority PDA avoids duplicate-account examples and works cleanly with CPI handles.
Note (Token-2022 and interface accounts)
Account<Mint> is the legacy Token Program wrapper and validates exact legacy mint size. Use InterfaceAccount<Mint> from anchor_spl_v2::token_interface when a read or validation path should accept either Token or Token-2022-owned mints. Token-2022 extension parsing is covered in Token-2022 and extensions.
Create a token account
Create and initialize SPL token accounts with token constraints and ATA address checks.
A token account stores one authority’s balance for one mint. Each token account records a mint, an authority, an amount, and optional delegate, wrapped-SOL, and close-authority state.
Anchor models the legacy SPL Token account as Account<TokenAccount> from anchor_spl_v2::token. Like Account<Mint>, it is zero-copy over the Token Program layout and starts at byte offset 0 instead of using an Anchor discriminator.
Token account data#
The base token account layout is 165 bytes:
pub struct TokenAccount { mint: Address, owner: Address, amount: [u8; 8], delegate_flag: [u8; 4], delegate: Address, state: u8, is_native_flag: [u8; 4], native_amount: [u8; 8], delegated_amount: [u8; 8], close_authority_flag: [u8; 4], close_authority: Address,}The fields are private. Read them with accessors such as mint(), owner(), amount(), delegate(), delegated_amount(), is_initialized(), is_frozen(), and close_authority().
Notation (Two meanings of "owner")
A token account’s owner() is the token authority that can transfer or burn tokens. It is not the Solana account owner. The Solana account owner is the program that owns the data, which is the Token Program for Account<TokenAccount>.
Account kinds#
There are three common ways to choose the token-account address:
Anchor has high-level init support for keypair accounts and your program’s PDAs through token::* constraints. For associated token accounts, the current surface exposes address derivation and validation helpers. High-level associated_token::* init constraints are still in progress.
Imports#
Import the mint and token account wrappers together:
use { anchor_lang_v2::prelude::*, anchor_spl_v2::{ mint::Mint, token::{self, TokenAccount}, },};Account patterns#
Use token::mint and token::authority constraints to initialize a token account:
#[derive(Accounts)]pub struct CreateTokenAccount { #[account(mut)] pub payer: Signer, pub mint: Account<Mint>, pub authority: UncheckedAccount, #[account( init, payer = payer, token::mint = mint, token::authority = authority, )] pub token_account: Account<TokenAccount>, pub token_program: Program<Token>, pub system_program: Program<System>,}The generated validation path creates the account with the Token Program as owner and invokes InitializeAccount3 before entering the handler. The handler can stay empty when all it needs to do is create the account:
pub fn create_token_account(_ctx: &mut Context<CreateTokenAccount>) -> Result<()> { Ok(())}An associated token account is a token account at the canonical PDA derived from the wallet, token program, and mint under the Associated Token Program.
Anchor currently exposes get_associated_token_address for deriving and checking that address:
use anchor_spl_v2::associated_token::get_associated_token_address;
#[derive(Accounts)]pub struct UseAssociatedTokenAccount { pub authority: Signer, pub mint: Account<Mint>, #[account( mut, token::mint = mint, token::authority = authority, address = get_associated_token_address( authority.address(), mint.account().address(), &Token::id(), ), )] pub token_account: Account<TokenAccount>,}Use this when the associated token account is created elsewhere, or when your instruction only needs to verify that the supplied account is the canonical ATA.
Note (Creating ATAs during alpha)
The ATA address belongs to the Associated Token Program’s PDA space, so a normal init constraint from your program cannot sign for that address. Until the associated_token::* init constraints are available, create ATAs outside this instruction or through a lower-level CPI to the Associated Token Program, then validate the resulting address with get_associated_token_address.
A token account can also be a PDA controlled by your program:
#[derive(Accounts)]pub struct CreateVaultTokenAccount { #[account(mut)] pub payer: Signer, pub mint: Account<Mint>, #[account(seeds = [b"vault-authority", mint.address().as_ref()], bump)] pub vault_authority: UncheckedAccount, #[account( init, payer = payer, seeds = [b"vault-token", mint.address().as_ref()], bump, token::mint = mint, token::authority = vault_authority, )] pub token_account: Account<TokenAccount>, pub token_program: Program<Token>, pub system_program: Program<System>,}Later CPI calls can transfer or burn from the token account by signing with the vault_authority PDA seeds. A program can also use the same PDA for the token account address and its authority, but a separate authority PDA is easier to read and avoids duplicate-account edge cases in examples.
Interface accounts#
Account<TokenAccount> accepts legacy Token Program accounts with the exact 165-byte base layout. Use InterfaceAccount<TokenAccount> when a read or validation path should accept Token-2022 accounts with TLV extension data after the base state:
use anchor_spl_v2::token_interface::InterfaceAccount;
#[derive(Accounts)]pub struct UseEitherTokenProgram { pub mint: InterfaceAccount<Mint>, #[account( token::mint = mint, token::authority = authority, )] pub token_account: InterfaceAccount<TokenAccount>, pub authority: Signer,}For high-level initialization through an interface account, include token::token_program = token_program and use Program<Token> for the current legacy Token Program path. Token-2022 account creation and extension setup should use lower-level Token-2022 APIs, then validate the resulting account through InterfaceAccount<TokenAccount>.
Account constraints#
Use these constraints together for a new legacy SPL Token account:
After initialization, the same constraints validate existing accounts:
#[derive(Accounts)]pub struct UseTokenAccount { pub mint: Account<Mint>, pub authority: Signer, #[account( mut, token::mint = mint, token::authority = authority, )] pub token_account: Account<TokenAccount>,}Use init_if_needed only when the instruction is designed to be idempotent. The existing-account branch still checks token::mint and token::authority, so a caller cannot pass a token account for a different mint or authority.
Client and test checks#
In Rust tests, derive the expected ATA or PDA explicitly and pass that address to the generated instruction helper. For PDA token accounts, keep the seed list in one place in the program and the test. Array-form seeds also help IDL clients understand the derivation.
Mint tokens
Mint SPL tokens through CPI helpers and PDA signer seeds.
Minting creates new units of a token by invoking the Token Program’s MintTo instruction. The mint authority stored on the mint account must authorize the CPI, and the destination must be a token account for that mint.
Minting uses typed CPI account structs. The account values passed into CPI structs are CpiHandle<'a> values from cpi_handle() or cpi_handle_mut(). The helpers come from anchor_spl_v2::token::cpi.
How minting works#
The token program checks three things before increasing supply:
amount is always in base units. For a mint with 6 decimals, 1_000_000 represents one whole token.
Imports#
Import the CPI module with the mint and token account wrappers:
use { anchor_lang_v2::prelude::*, anchor_spl_v2::{ mint::Mint, token::{self, cpi as token_cpi, TokenAccount}, },};CPI patterns#
At minimum, the instruction needs a writable mint, a writable destination token account, the mint authority, and the token program:
#[derive(Accounts)]pub struct MintTokens { #[account(mut, mint::authority = authority)] pub mint: Account<Mint>, #[account(mut, token::mint = mint)] pub token_account: Account<TokenAccount>, pub authority: Signer, pub token_program: Program<Token>,}The handler builds token_cpi::accounts::MintTo and invokes the helper:
pub fn mint_tokens(ctx: &mut Context<MintTokens>, amount: u64) -> Result<()> { let accounts = token_cpi::accounts::MintTo { mint: ctx.accounts.mint.cpi_handle_mut(), to: ctx.accounts.token_account.cpi_handle_mut(), authority: ctx.accounts.authority.cpi_handle(), }; let cpi = CpiContext::new(ctx.accounts.token_program.address(), accounts); token_cpi::mint_to(cpi, amount)}The mint::authority = authority constraint is not required for correctness because the Token Program checks it during CPI. It is still useful because the Anchor validation path fails earlier and returns a clearer account-validation error.
When the mint authority is a PDA, the program authorizes the CPI by reconstructing the PDA seeds and attaching them with with_signer().
The account validation describes the mint and the authority PDA separately:
#[derive(Accounts)]pub struct MintTokens { #[account(mut, mint::authority = mint_authority)] pub mint: Account<Mint>, #[account(mut, token::mint = mint)] pub token_account: Account<TokenAccount>, #[account(seeds = [b"mint-authority"], bump)] pub mint_authority: UncheckedAccount, pub token_program: Program<Token>,}The CPI passes the writable mint, writable destination, and read-only authority as separate handles:
pub fn mint_tokens(ctx: &mut Context<MintTokens>, amount: u64) -> Result<()> { let signer_seeds: &[&[&[u8]]] = &[&[b"mint-authority", &[ctx.bumps.mint_authority]]]; let accounts = token_cpi::accounts::MintTo { mint: ctx.accounts.mint.cpi_handle_mut(), to: ctx.accounts.token_account.cpi_handle_mut(), authority: ctx.accounts.mint_authority.cpi_handle(), }; let cpi = CpiContext::new(ctx.accounts.token_program.address(), accounts) .with_signer(signer_seeds); token_cpi::mint_to(cpi, amount)}This is the standard pattern for program-controlled supply. A design can use the mint PDA itself as the mint authority, but a separate authority PDA is easier to explain and avoids borrowing the same account as both writable mint and authority in examples.
Checked minting#
Use mint_to_checked when the caller should also provide the expected decimals:
pub fn mint_tokens_checked( ctx: &mut Context<MintTokens>, amount: u64, decimals: u8,) -> Result<()> { let accounts = token_cpi::accounts::MintToChecked { mint: ctx.accounts.mint.cpi_handle_mut(), to: ctx.accounts.token_account.cpi_handle_mut(), authority: ctx.accounts.authority.cpi_handle(), }; let cpi = CpiContext::new(ctx.accounts.token_program.address(), accounts); token_cpi::mint_to_checked(cpi, amount, decimals)}This is a good public-instruction default because it forces the caller and program to agree on the mint’s decimal configuration.
Destination checks#
Validate the destination account before the CPI:
#[derive(Accounts)]pub struct MintToVault { pub authority: Signer, #[account(mut, mint::authority = authority)] pub mint: Account<Mint>, #[account( mut, token::mint = mint, token::authority = vault_authority, )] pub vault: Account<TokenAccount>, pub vault_authority: UncheckedAccount, pub token_program: Program<Token>,}token::mint = mint prevents minting into a token account for a different mint. token::authority = vault_authority is useful when the destination must be a specific vault or program-controlled token account.
Client and test checks#
Rust tests should verify both supply and destination balance after the instruction. The SPL wrappers expose accessors for this:
let mint = svm.get_account(&mint_address).expect("mint");let mint_state: &anchor_spl_v2::mint::Mint = bytemuck::from_bytes(&mint.data);assert_eq!(mint_state.supply(), amount);For Token-2022 or interface-account flows, use InterfaceAccount<Mint> and InterfaceAccount<TokenAccount> in the program accounts, then choose the token program explicitly with mint::token_program or token::token_program. The legacy token_cpi::mint_to helper shown here targets the legacy Token Program surface.
Note (Why cpi_handle_mut() matters)
CPI helpers use CpiHandle<'a> instead of raw account views. A writable token or mint account should be passed with cpi_handle_mut(), while read-only authority accounts use cpi_handle().
Transfer tokens
Transfer SPL tokens between token accounts through CPI helpers.
Transferring tokens moves units between two token accounts for the same mint. The source token account’s authority must authorize the CPI, and both token accounts must be writable because their balances change.
Anchor exposes transfer helpers under anchor_spl_v2::token::cpi. Prefer transfer_checked for user-facing transfers because it includes the mint account and verifies the mint decimals.
Imports#
Import the token CPI module and account wrappers:
use { anchor_lang_v2::prelude::*, anchor_spl_v2::{ mint::Mint, token::{self, cpi as token_cpi, TokenAccount}, },};Transfer patterns#
The account struct should validate that the source and destination accounts use the expected mint, and that the source account belongs to the signer:
#[derive(Accounts)]pub struct TransferTokens { pub mint: Account<Mint>, #[account( mut, token::mint = mint, token::authority = authority, )] pub from: Account<TokenAccount>, #[account(mut, token::mint = mint)] pub to: Account<TokenAccount>, pub authority: Signer, pub token_program: Program<Token>,}Build a TransferChecked CPI account struct and pass it through CpiContext:
pub fn transfer_tokens( ctx: &mut Context<TransferTokens>, amount: u64, decimals: u8,) -> Result<()> { let accounts = token_cpi::accounts::TransferChecked { from: ctx.accounts.from.cpi_handle_mut(), mint: ctx.accounts.mint.cpi_handle(), to: ctx.accounts.to.cpi_handle_mut(), authority: ctx.accounts.authority.cpi_handle(), }; let cpi = CpiContext::new(ctx.accounts.token_program.address(), accounts); token_cpi::transfer_checked(cpi, amount, decimals)}amount is in base units. If the mint has 6 decimals, 1_000_000 is one whole token. The decimals argument must match the mint account.
The plain transfer helper omits the mint and decimals check:
pub fn transfer(ctx: &mut Context<TransferTokens>, amount: u64) -> Result<()> { let accounts = token_cpi::accounts::Transfer { from: ctx.accounts.from.cpi_handle_mut(), to: ctx.accounts.to.cpi_handle_mut(), authority: ctx.accounts.authority.cpi_handle(), }; let cpi = CpiContext::new(ctx.accounts.token_program.address(), accounts); token_cpi::transfer(cpi, amount)}Use it only when another part of the instruction already guarantees the mint and decimals relationship you need.
PDA authority#
When a program controls the source token account, set the token account authority to a PDA and sign the CPI with that PDA’s seeds:
#[derive(Accounts)]pub struct TransferFromVault { pub mint: Account<Mint>, #[account(seeds = [b"vault-authority", mint.address().as_ref()], bump)] pub vault_authority: UncheckedAccount, #[account( mut, token::mint = mint, token::authority = vault_authority, )] pub vault: Account<TokenAccount>, #[account(mut, token::mint = mint)] pub recipient: Account<TokenAccount>, pub token_program: Program<Token>,}
pub fn transfer_from_vault( ctx: &mut Context<TransferFromVault>, amount: u64, decimals: u8,) -> Result<()> { let signer_seeds: &[&[&[u8]]] = &[&[ b"vault-authority", ctx.accounts.mint.address().as_ref(), &[ctx.bumps.vault_authority], ]]; let accounts = token_cpi::accounts::TransferChecked { from: ctx.accounts.vault.cpi_handle_mut(), mint: ctx.accounts.mint.cpi_handle(), to: ctx.accounts.recipient.cpi_handle_mut(), authority: ctx.accounts.vault_authority.cpi_handle(), }; let cpi = CpiContext::new(ctx.accounts.token_program.address(), accounts) .with_signer(signer_seeds); token_cpi::transfer_checked(cpi, amount, decimals)}The authority PDA is separate from the token account in this example, which keeps the account list easy to reason about. A program can use the same PDA as both the token account address and authority when it has a specific reason to do so.
Common validation checks#
Use constraints to reject mismatched accounts before the CPI runs:
For associated token accounts, derive the expected address with get_associated_token_address and use address = ... or constraint = ....
Token-2022 and extensions
Accept Token-2022 accounts, read TLV extensions, and use anchor-spl-v2 interface accounts.
Token-2022 uses the same base mint and token-account layouts as the legacy Token Program, then appends extension data in a TLV section. A mint or token account can opt into features like transfer fees, metadata pointers, permanent delegates, and transfer hooks.
Anchor handles Token-2022 in two layers:
InterfaceAccount<T>accepts accounts owned by either the legacy Token Program or Token-2022.anchor_spl_v2::extensionsparses supported fixed-size TLV extension structs from the account tail.
Warning (Alpha surface)
The Token-2022 surface is read and validation focused. Not every Token-2022 initialization or extension instruction has a high-level helper yet. Use the lower-level token program CPI APIs when an extension instruction is not wrapped.
Interface accounts#
Use InterfaceAccount<Mint> or InterfaceAccount<TokenAccount> when an instruction should accept either Token or Token-2022-owned accounts:
use { anchor_lang_v2::prelude::*, anchor_spl_v2::{ mint::Mint, token::TokenAccount, token_interface::InterfaceAccount, },};
#[derive(Accounts)]pub struct ReadAnyTokenAccount { pub mint: InterfaceAccount<Mint>, #[account(token::mint = mint)] pub token_account: InterfaceAccount<TokenAccount>,}InterfaceAccount<T> is a transparent wrapper over the same base SPL types. It relaxes ownership validation to accept both token program IDs, and it allows Token-2022 accounts to be larger than the base layout because extension data follows the base state.
Constraints#
The usual namespaced constraints work on interface accounts:
For a public instruction that accepts either token program, include the token program account in the account list and validate ownership with token::token_program or mint::token_program. If the token program field is unchecked, also constrain it to one of the two known token program IDs before using it for initialization or CPI:
#[derive(Accounts)]pub struct ReadToken2022Account { #[account( constraint = *token_program.address() == Token::id() || *token_program.address() == Token2022::id() )] pub token_program: UncheckedAccount, pub mint: InterfaceAccount<Mint>, #[account( token::mint = mint, token::token_program = token_program, )] pub token_account: InterfaceAccount<TokenAccount>,}Use a concrete Program<Token> or Program<Token2022> field when an instruction should accept only one token program.
Interface init#
The current high-level interface init path is for the legacy Token Program. Useful when initializing a base SPL mint or token account while keeping the account type as InterfaceAccount<T> for later read and validation paths:
#[derive(Accounts)]pub struct InitInterfaceMint { #[account(mut)] pub payer: Signer, pub authority: UncheckedAccount, pub token_program: Program<Token>, #[account( init, payer = payer, mint::decimals = 6, mint::authority = authority, mint::token_program = token_program, )] pub mint: InterfaceAccount<Mint>, pub system_program: Program<System>,}For Token-2022 account creation or extension selection, use lower-level Token-2022 APIs, then validate or read the resulting account with InterfaceAccount<T>. Most Token-2022 extensions must be selected before the base account is initialized.
Extension data#
Token-2022 stores extension data after the base mint or token-account layout. anchor_spl_v2::extensions reads supported TLV entries by type:
use anchor_spl_v2::extensions::{self, TransferFeeConfig};
pub fn read_fee_config( ctx: &mut Context<ReadFeeConfig>, expected_bps: u16,) -> Result<()> { let ext: &TransferFeeConfig = extensions::get_mint_extension(ctx.accounts.mint.account())?; require_eq!(ext.newer_transfer_fee.basis_points(), expected_bps); Ok(())}
#[derive(Accounts)]pub struct ReadFeeConfig { pub mint: InterfaceAccount<Mint>,}Use get_mint_extension::<T>() for mint extensions and get_token_account_extension::<T>() for token-account extensions.
Supported extension structs#
The current helpers include fixed-size structs for common extensions:
Variable-length extension data and extension initialization flows are still settling. Keep Token-2022 programs close to the source APIs, and add focused tests for every extension path.