Files
gh-tenequm-claude-plugins-s…/skills/solana-development/references/tokens-validation.md
2025-11-30 09:01:25 +08:00

5.6 KiB

SPL Token Program - Validation Patterns

Validation patterns for SPL Token accounts including ownership verification, mint validation, ATA address derivation checks, and balance verification. Covers both Anchor constraint-based and Native Rust manual validation approaches.

For related topics, see:

Table of Contents

  1. Validate Token Account Ownership and Mint
  2. Validate ATA Address
  3. Check Token Balance

Validate Token Account Ownership and Mint

Using Anchor

use anchor_spl::token_interface::{TokenAccount, Mint};

#[derive(Accounts)]
pub struct ValidateTokenAccount<'info> {
    #[account(
        constraint = token_account.owner == owner.key() @ ErrorCode::InvalidOwner,
        constraint = token_account.mint == mint.key() @ ErrorCode::InvalidMint,
    )]
    pub token_account: InterfaceAccount<'info, TokenAccount>,

    pub mint: InterfaceAccount<'info, Mint>,

    /// CHECK: Any account
    pub owner: UncheckedAccount<'info>,
}

pub fn validate_token_account(ctx: Context<ValidateTokenAccount>) -> Result<()> {
    // Validation is automatic via constraints

    // Additional checks if needed
    require!(
        ctx.accounts.token_account.amount >= 100,
        ErrorCode::InsufficientBalance
    );

    Ok(())
}

Using Native Rust

use spl_token::state::Account as TokenAccount;
use solana_program::{
    account_info::AccountInfo,
    entrypoint::ProgramResult,
    msg,
    program_error::ProgramError,
    program_pack::Pack,
    pubkey::Pubkey,
};

pub fn validate_token_account(
    token_account_info: &AccountInfo,
    expected_owner: &Pubkey,
    expected_mint: &Pubkey,
) -> ProgramResult {
    // 1. Verify owned by Token Program
    if token_account_info.owner != &spl_token::ID {
        msg!("Account not owned by Token Program");
        return Err(ProgramError::IllegalOwner);
    }

    // 2. Deserialize token account
    let token_account = TokenAccount::unpack(&token_account_info.data.borrow())?;

    // 3. Verify owner
    if token_account.owner != *expected_owner {
        msg!("Token account owner mismatch");
        return Err(ProgramError::IllegalOwner);
    }

    // 4. Verify mint
    if token_account.mint != *expected_mint {
        msg!("Token account mint mismatch");
        return Err(ProgramError::InvalidAccountData);
    }

    // 5. Verify not frozen
    if token_account.state != spl_token::state::AccountState::Initialized {
        msg!("Token account is frozen or uninitialized");
        return Err(ProgramError::InvalidAccountData);
    }

    Ok(())
}

Validate ATA Address

Using Anchor

use anchor_spl::associated_token::AssociatedToken;
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};

#[derive(Accounts)]
pub struct ValidateATA<'info> {
    #[account(
        associated_token::mint = mint,
        associated_token::authority = owner,
        associated_token::token_program = token_program,
    )]
    pub ata: InterfaceAccount<'info, TokenAccount>,

    pub mint: InterfaceAccount<'info, Mint>,

    /// CHECK: Any account
    pub owner: UncheckedAccount<'info>,

    pub token_program: Interface<'info, TokenInterface>,
}

pub fn validate_ata(ctx: Context<ValidateATA>) -> Result<()> {
    // ATA address is automatically validated by Anchor constraints
    Ok(())
}

Using Native Rust

use spl_associated_token_account::get_associated_token_address;
use solana_program::{
    account_info::AccountInfo,
    entrypoint::ProgramResult,
    msg,
    program_error::ProgramError,
    pubkey::Pubkey,
};

pub fn validate_ata(
    ata_info: &AccountInfo,
    wallet: &Pubkey,
    mint: &Pubkey,
) -> ProgramResult {
    // Derive expected ATA address
    let expected_ata = get_associated_token_address(wallet, mint);

    // Validate match
    if expected_ata != *ata_info.key {
        msg!("Invalid ATA address");
        return Err(ProgramError::InvalidAccountData);
    }

    Ok(())
}

Check Token Balance

Using Anchor

use anchor_spl::token_interface::TokenAccount;

pub fn check_balance(
    ctx: Context<SomeContext>,
    minimum_amount: u64
) -> Result<()> {
    let token_account = &ctx.accounts.token_account;

    require!(
        token_account.amount >= minimum_amount,
        ErrorCode::InsufficientBalance
    );

    Ok(())
}

Using Native Rust

use spl_token::state::Account as TokenAccount;
use solana_program::{
    account_info::AccountInfo,
    entrypoint::ProgramResult,
    msg,
    program_error::ProgramError,
    program_pack::Pack,
};

pub fn check_token_balance(
    token_account_info: &AccountInfo,
    minimum_amount: u64,
) -> ProgramResult {
    let token_account = TokenAccount::unpack(&token_account_info.data.borrow())?;

    if token_account.amount < minimum_amount {
        msg!("Insufficient token balance: {} < {}", token_account.amount, minimum_amount);
        return Err(ProgramError::InsufficientFunds);
    }

    Ok(())
}

Next Steps

  • Token-2022: See tokens-2022.md for Token Extensions Program features
  • Patterns & Security: See tokens-patterns.md for common patterns and comprehensive security best practices