23 KiB
23 KiB
SPL Token Program - Operations
Complete guide to SPL Token operations including creating mints, minting tokens, transferring (with transfer_checked), burning, and closing token accounts. Shows both Anchor and Native Rust implementations side-by-side.
For related topics, see:
- tokens-overview.md - Token fundamentals and account structures
- tokens-validation.md - Account validation patterns
- tokens-2022.md - Token Extensions Program features
- tokens-patterns.md - Common patterns and security
Table of Contents
Creating Tokens
Initialize a New Mint
Using Anchor
use anchor_spl::token_interface::{Mint, TokenInterface};
#[derive(Accounts)]
pub struct CreateMint<'info> {
#[account(
init,
payer = payer,
mint::decimals = 9,
mint::authority = mint_authority,
mint::freeze_authority = freeze_authority,
mint::token_program = token_program,
)]
pub mint: InterfaceAccount<'info, Mint>,
/// CHECK: Can be any account
pub mint_authority: UncheckedAccount<'info>,
/// CHECK: Can be any account (optional)
pub freeze_authority: UncheckedAccount<'info>,
#[account(mut)]
pub payer: Signer<'info>,
pub token_program: Interface<'info, TokenInterface>,
pub system_program: Program<'info, System>,
}
pub fn create_mint(ctx: Context<CreateMint>) -> Result<()> {
// Mint is automatically created and initialized by Anchor constraints
msg!("Mint created: {}", ctx.accounts.mint.key());
Ok(())
}
Key Anchor Constraints:
init- Creates and initializes the accountmint::decimals- Number of decimal placesmint::authority- Who can mint tokensmint::freeze_authority- Who can freeze token accounts (optional)mint::token_program- Which token program to use
Using Native Rust
use spl_token::instruction::initialize_mint;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program::invoke,
rent::Rent,
system_instruction,
sysvar::Sysvar,
};
pub fn create_mint(
payer: &AccountInfo,
mint_account: &AccountInfo,
mint_authority: &Pubkey,
freeze_authority: Option<&Pubkey>,
decimals: u8,
system_program: &AccountInfo,
token_program: &AccountInfo,
rent_sysvar: &AccountInfo,
) -> ProgramResult {
// Mint account size
let mint_size = 82;
// Calculate rent
let rent = Rent::get()?;
let rent_lamports = rent.minimum_balance(mint_size);
// Create mint account via System Program
invoke(
&system_instruction::create_account(
payer.key,
mint_account.key,
rent_lamports,
mint_size as u64,
&spl_token::ID,
),
&[payer.clone(), mint_account.clone(), system_program.clone()],
)?;
// Initialize mint
invoke(
&initialize_mint(
token_program.key,
mint_account.key,
mint_authority,
freeze_authority,
decimals,
)?,
&[
mint_account.clone(),
rent_sysvar.clone(),
token_program.clone(),
],
)?;
Ok(())
}
Initialize a Token Account (Non-ATA)
Using Anchor
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};
#[derive(Accounts)]
pub struct CreateTokenAccount<'info> {
#[account(
init,
payer = payer,
token::mint = mint,
token::authority = owner,
token::token_program = token_program,
)]
pub token_account: InterfaceAccount<'info, TokenAccount>,
pub mint: InterfaceAccount<'info, Mint>,
/// CHECK: Can be any account
pub owner: UncheckedAccount<'info>,
#[account(mut)]
pub payer: Signer<'info>,
pub token_program: Interface<'info, TokenInterface>,
pub system_program: Program<'info, System>,
}
pub fn create_token_account(ctx: Context<CreateTokenAccount>) -> Result<()> {
// Token account is automatically created and initialized
Ok(())
}
Using Native Rust
use spl_token::instruction::initialize_account3;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program::invoke,
rent::Rent,
system_instruction,
sysvar::Sysvar,
};
pub fn create_token_account(
payer: &AccountInfo,
token_account: &AccountInfo,
mint: &AccountInfo,
owner: &Pubkey,
system_program: &AccountInfo,
token_program: &AccountInfo,
) -> ProgramResult {
// Token account size
let token_account_size = 165;
// Calculate rent
let rent = Rent::get()?;
let rent_lamports = rent.minimum_balance(token_account_size);
// Create token account
invoke(
&system_instruction::create_account(
payer.key,
token_account.key,
rent_lamports,
token_account_size as u64,
&spl_token::ID,
),
&[payer.clone(), token_account.clone(), system_program.clone()],
)?;
// Initialize token account
invoke(
&initialize_account3(
token_program.key,
token_account.key,
mint.key,
owner,
)?,
&[token_account.clone(), mint.clone(), token_program.clone()],
)?;
Ok(())
}
Minting Tokens
Basic Minting (User Authority)
Using Anchor
use anchor_spl::token_interface::{self, Mint, MintTo, TokenAccount, TokenInterface};
#[derive(Accounts)]
pub struct MintTokens<'info> {
#[account(mut)]
pub mint: InterfaceAccount<'info, Mint>,
#[account(mut)]
pub token_account: InterfaceAccount<'info, TokenAccount>,
pub mint_authority: Signer<'info>,
pub token_program: Interface<'info, 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_program = ctx.accounts.token_program.to_account_info();
let cpi_context = CpiContext::new(cpi_program, cpi_accounts);
token_interface::mint_to(cpi_context, amount)?;
Ok(())
}
Using Native Rust
use spl_token::instruction::mint_to;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program::invoke,
program_error::ProgramError,
};
pub fn mint_tokens(
mint: &AccountInfo,
destination: &AccountInfo,
mint_authority: &AccountInfo,
amount: u64,
token_program: &AccountInfo,
) -> ProgramResult {
// Mint authority must be a signer
if !mint_authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
invoke(
&mint_to(
token_program.key,
mint.key,
destination.key,
mint_authority.key,
&[], // No multisig signers
amount,
)?,
&[
mint.clone(),
destination.clone(),
mint_authority.clone(),
token_program.clone(),
],
)?;
Ok(())
}
Minting with PDA Authority
Using Anchor
use anchor_spl::token_interface::{self, Mint, MintTo, TokenAccount, TokenInterface};
#[derive(Accounts)]
pub struct MintWithPDA<'info> {
#[account(
mut,
mint::authority = mint_authority,
)]
pub mint: InterfaceAccount<'info, Mint>,
#[account(mut)]
pub token_account: InterfaceAccount<'info, TokenAccount>,
#[account(
seeds = [b"mint-authority"],
bump,
)]
/// CHECK: PDA signer
pub mint_authority: UncheckedAccount<'info>,
pub token_program: Interface<'info, TokenInterface>,
}
pub fn mint_with_pda(ctx: Context<MintWithPDA>, amount: u64) -> Result<()> {
let seeds = &[
b"mint-authority",
&[ctx.bumps.mint_authority],
];
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_authority.to_account_info(),
};
let cpi_context = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts
).with_signer(signer_seeds);
token_interface::mint_to(cpi_context, amount)?;
Ok(())
}
Using Native Rust
use spl_token::instruction::mint_to;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program::invoke_signed,
program_error::ProgramError,
pubkey::Pubkey,
};
pub fn mint_tokens_from_pda(
program_id: &Pubkey,
mint: &AccountInfo,
destination: &AccountInfo,
mint_authority_pda: &AccountInfo,
token_program: &AccountInfo,
amount: u64,
pda_seeds: &[&[u8]],
bump: u8,
) -> ProgramResult {
// Validate PDA
let (expected_pda, _) = Pubkey::find_program_address(pda_seeds, program_id);
if expected_pda != *mint_authority_pda.key {
return Err(ProgramError::InvalidSeeds);
}
// Prepare signer seeds
let mut full_seeds = pda_seeds.to_vec();
full_seeds.push(&[bump]);
let signer_seeds: &[&[&[u8]]] = &[&full_seeds];
invoke_signed(
&mint_to(
token_program.key,
mint.key,
destination.key,
mint_authority_pda.key,
&[],
amount,
)?,
&[
mint.clone(),
destination.clone(),
mint_authority_pda.clone(),
token_program.clone(),
],
signer_seeds,
)?;
Ok(())
}
Transferring Tokens
Basic Transfer
Using Anchor
use anchor_spl::token_interface::{self, TokenAccount, TokenInterface, Transfer};
#[derive(Accounts)]
pub struct TransferTokens<'info> {
#[account(mut)]
pub from: InterfaceAccount<'info, TokenAccount>,
#[account(mut)]
pub to: InterfaceAccount<'info, TokenAccount>,
pub authority: Signer<'info>,
pub token_program: Interface<'info, TokenInterface>,
}
pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64) -> Result<()> {
let cpi_accounts = Transfer {
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
};
let cpi_context = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts
);
token_interface::transfer(cpi_context, amount)?;
Ok(())
}
Using Native Rust
use spl_token::instruction::transfer;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program::invoke,
program_error::ProgramError,
};
pub fn transfer_tokens(
source: &AccountInfo,
destination: &AccountInfo,
authority: &AccountInfo,
amount: u64,
token_program: &AccountInfo,
) -> ProgramResult {
// Authority must be a signer
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
invoke(
&transfer(
token_program.key,
source.key,
destination.key,
authority.key,
&[], // No multisig signers
amount,
)?,
&[
source.clone(),
destination.clone(),
authority.clone(),
token_program.clone(),
],
)?;
Ok(())
}
Transfer with Checks (Recommended)
Using Anchor
use anchor_spl::token_interface::{self, Mint, TokenAccount, TokenInterface, TransferChecked};
#[derive(Accounts)]
pub struct TransferTokensChecked<'info> {
#[account(mut)]
pub from: InterfaceAccount<'info, TokenAccount>,
#[account(mut)]
pub to: InterfaceAccount<'info, TokenAccount>,
pub mint: InterfaceAccount<'info, Mint>,
pub authority: Signer<'info>,
pub token_program: Interface<'info, TokenInterface>,
}
pub fn transfer_tokens_checked(
ctx: Context<TransferTokensChecked>,
amount: u64
) -> Result<()> {
token_interface::transfer_checked(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
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(),
},
),
amount,
ctx.accounts.mint.decimals,
)?;
Ok(())
}
Using Native Rust
use spl_token::instruction::transfer_checked;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program::invoke,
program_error::ProgramError,
};
pub fn transfer_tokens_checked(
source: &AccountInfo,
mint: &AccountInfo,
destination: &AccountInfo,
authority: &AccountInfo,
amount: u64,
decimals: u8,
token_program: &AccountInfo,
) -> ProgramResult {
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
invoke(
&transfer_checked(
token_program.key,
source.key,
mint.key,
destination.key,
authority.key,
&[],
amount,
decimals,
)?,
&[
source.clone(),
mint.clone(),
destination.clone(),
authority.clone(),
token_program.clone(),
],
)?;
Ok(())
}
Transfer with PDA Signer
Using Anchor
use anchor_spl::token_interface::{self, TokenAccount, TokenInterface, Transfer};
#[derive(Accounts)]
pub struct TransferWithPDA<'info> {
#[account(
mut,
token::authority = authority,
)]
pub from: InterfaceAccount<'info, TokenAccount>,
#[account(mut)]
pub to: InterfaceAccount<'info, TokenAccount>,
#[account(
seeds = [b"authority"],
bump,
)]
/// CHECK: PDA signer
pub authority: UncheckedAccount<'info>,
pub token_program: Interface<'info, TokenInterface>,
}
pub fn transfer_with_pda(ctx: Context<TransferWithPDA>, amount: u64) -> Result<()> {
let seeds = &[
b"authority",
&[ctx.bumps.authority],
];
let signer_seeds = &[&seeds[..]];
let cpi_accounts = Transfer {
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
};
let cpi_context = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts
).with_signer(signer_seeds);
token_interface::transfer(cpi_context, amount)?;
Ok(())
}
Using Native Rust
use spl_token::instruction::transfer;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program::invoke_signed,
program_error::ProgramError,
pubkey::Pubkey,
};
pub fn transfer_tokens_from_pda(
program_id: &Pubkey,
source: &AccountInfo,
destination: &AccountInfo,
authority_pda: &AccountInfo,
token_program: &AccountInfo,
amount: u64,
pda_seeds: &[&[u8]],
bump: u8,
) -> ProgramResult {
let (expected_pda, _) = Pubkey::find_program_address(pda_seeds, program_id);
if expected_pda != *authority_pda.key {
return Err(ProgramError::InvalidSeeds);
}
let mut full_seeds = pda_seeds.to_vec();
full_seeds.push(&[bump]);
let signer_seeds: &[&[&[u8]]] = &[&full_seeds];
invoke_signed(
&transfer(
token_program.key,
source.key,
destination.key,
authority_pda.key,
&[],
amount,
)?,
&[
source.clone(),
destination.clone(),
authority_pda.clone(),
token_program.clone(),
],
signer_seeds,
)?;
Ok(())
}
Burning Tokens
Basic Burn
Using Anchor
use anchor_spl::token_interface::{self, Burn, Mint, TokenAccount, TokenInterface};
#[derive(Accounts)]
pub struct BurnTokens<'info> {
#[account(mut)]
pub mint: InterfaceAccount<'info, Mint>,
#[account(mut)]
pub token_account: InterfaceAccount<'info, TokenAccount>,
pub authority: Signer<'info>,
pub token_program: Interface<'info, TokenInterface>,
}
pub fn burn_tokens(ctx: Context<BurnTokens>, amount: u64) -> Result<()> {
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_context = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts
);
token_interface::burn(cpi_context, amount)?;
Ok(())
}
Using Native Rust
use spl_token::instruction::burn;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program::invoke,
program_error::ProgramError,
};
pub fn burn_tokens(
token_account: &AccountInfo,
mint: &AccountInfo,
authority: &AccountInfo,
amount: u64,
token_program: &AccountInfo,
) -> ProgramResult {
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
invoke(
&burn(
token_program.key,
token_account.key,
mint.key,
authority.key,
&[],
amount,
)?,
&[
token_account.clone(),
mint.clone(),
authority.clone(),
token_program.clone(),
],
)?;
Ok(())
}
Burn with PDA Authority
Using Anchor
use anchor_spl::token_interface::{self, Burn, Mint, TokenAccount, TokenInterface};
#[derive(Accounts)]
pub struct BurnWithPDA<'info> {
#[account(mut)]
pub mint: InterfaceAccount<'info, Mint>,
#[account(
mut,
token::authority = authority,
)]
pub token_account: InterfaceAccount<'info, TokenAccount>,
#[account(
seeds = [b"burn-authority"],
bump,
)]
/// CHECK: PDA signer
pub authority: UncheckedAccount<'info>,
pub token_program: Interface<'info, TokenInterface>,
}
pub fn burn_with_pda(ctx: Context<BurnWithPDA>, amount: u64) -> Result<()> {
let seeds = &[
b"burn-authority",
&[ctx.bumps.authority],
];
let signer_seeds = &[&seeds[..]];
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_context = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts
).with_signer(signer_seeds);
token_interface::burn(cpi_context, amount)?;
Ok(())
}
Using Native Rust
pub fn burn_tokens_from_pda(
program_id: &Pubkey,
token_account: &AccountInfo,
mint: &AccountInfo,
authority_pda: &AccountInfo,
token_program: &AccountInfo,
amount: u64,
pda_seeds: &[&[u8]],
bump: u8,
) -> ProgramResult {
let (expected_pda, _) = Pubkey::find_program_address(pda_seeds, program_id);
if expected_pda != *authority_pda.key {
return Err(ProgramError::InvalidSeeds);
}
let mut full_seeds = pda_seeds.to_vec();
full_seeds.push(&[bump]);
let signer_seeds: &[&[&[u8]]] = &[&full_seeds];
invoke_signed(
&burn(
token_program.key,
token_account.key,
mint.key,
authority_pda.key,
&[],
amount,
)?,
&[
token_account.clone(),
mint.clone(),
authority_pda.clone(),
token_program.clone(),
],
signer_seeds,
)?;
Ok(())
}
Closing Token Accounts
Close Token Account
Using Anchor
use anchor_spl::token_interface::{self, CloseAccount, TokenAccount, TokenInterface};
#[derive(Accounts)]
pub struct CloseTokenAccount<'info> {
#[account(mut)]
pub token_account: InterfaceAccount<'info, TokenAccount>,
#[account(mut)]
pub destination: SystemAccount<'info>,
pub authority: Signer<'info>,
pub token_program: Interface<'info, TokenInterface>,
}
pub fn close_token_account(ctx: Context<CloseTokenAccount>) -> Result<()> {
let cpi_accounts = CloseAccount {
account: ctx.accounts.token_account.to_account_info(),
destination: ctx.accounts.destination.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
};
let cpi_context = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts
);
token_interface::close_account(cpi_context)?;
Ok(())
}
Using Anchor Constraints (Simplified):
#[derive(Accounts)]
pub struct CloseTokenAccount<'info> {
#[account(
mut,
close = destination,
token::authority = authority,
)]
pub token_account: InterfaceAccount<'info, TokenAccount>,
#[account(mut)]
pub destination: SystemAccount<'info>,
pub authority: Signer<'info>,
}
pub fn close_token_account(ctx: Context<CloseTokenAccount>) -> Result<()> {
// Account is automatically closed by Anchor constraints
Ok(())
}
Using Native Rust
use spl_token::instruction::close_account;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
program::invoke,
program_error::ProgramError,
};
pub fn close_token_account(
token_account: &AccountInfo,
destination: &AccountInfo,
authority: &AccountInfo,
token_program: &AccountInfo,
) -> ProgramResult {
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
invoke(
&close_account(
token_program.key,
token_account.key,
destination.key,
authority.key,
&[],
)?,
&[
token_account.clone(),
destination.clone(),
authority.clone(),
token_program.clone(),
],
)?;
Ok(())
}
Next Steps
- Validation: See tokens-validation.md for account validation patterns
- Token-2022: See tokens-2022.md for Token Extensions Program features
- Patterns & Security: See tokens-patterns.md for common patterns and security best practices