SPL Token Account Validation
Detects missing validation in SPL token operations including ownership, mint, and authority checks.
SPL Token Account Validation
Overview
Remediation Guide: How to Fix SPL Token Account Validation Issues
The SPL token account validation detector identifies Solana programs that perform SPL token operations (transfer, mint, burn) via CPI without proper validation of the accounts involved. SPL token operations require multiple validations: token account ownership (owned by SPL Token program), mint validation (correct token type), authority validation (signer check), and associated token account verification. Missing any of these checks can allow attackers to pass fake accounts and steal funds.
Sigvex performs a two-pass analysis: first collecting all validation checks (owner, signer, key) per account variable, then identifying SPL token CPI calls and verifying that all accounts involved have been appropriately validated. CWE mapping: CWE-345 (Insufficient Verification of Data Authenticity).
Why This Is an Issue
SPL token operations involve multiple accounts that each require specific validation:
- Source/destination accounts: Must be owned by the SPL Token program. Without this check, an attacker passes a fake account that claims to have any balance.
- Mint account: Must match the expected token type. Without this check, an attacker passes a worthless mint account, exchanging cheap tokens for valuable ones.
- Authority account: Must be a signer and must be the designated authority for the token account. Without this check, unauthorized parties can initiate transfers.
- Associated Token Account (ATA): Must be derived from the correct wallet and mint. Without this check, funds can be redirected to the wrong wallet.
How to Resolve
Native Rust
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey};
pub fn secure_transfer(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let source = &accounts[0];
let destination = &accounts[1];
let authority = &accounts[2];
let mint = &accounts[3];
let token_program = &accounts[4];
// Validate token program
if token_program.key != &spl_token::id() {
return Err(ProgramError::IncorrectProgramId);
}
// Validate token account ownership
if source.owner != &spl_token::id() || destination.owner != &spl_token::id() {
return Err(ProgramError::IllegalOwner);
}
// Validate authority is signer
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// Validate mint matches expected token
let source_data = spl_token::state::Account::unpack(&source.data.borrow())?;
if source_data.mint != *mint.key {
return Err(ProgramError::InvalidArgument);
}
solana_program::program::invoke(
&spl_token::instruction::transfer(
token_program.key, source.key, destination.key,
authority.key, &[], amount,
)?,
accounts,
)?;
Ok(())
}
Anchor
#[derive(Accounts)]
pub struct SecureTransfer<'info> {
#[account(mut, token::mint = mint, token::authority = authority)]
pub source: Account<'info, TokenAccount>,
#[account(mut, token::mint = mint)]
pub destination: Account<'info, TokenAccount>,
pub authority: Signer<'info>,
pub mint: Account<'info, Mint>,
pub token_program: Program<'info, Token>,
}
Examples
Vulnerable Code
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
pub fn swap(accounts: &[AccountInfo], amount_a: u64, amount_b: u64) -> ProgramResult {
let token_a_source = &accounts[0]; // No ownership check
let token_b_dest = &accounts[1]; // No mint check
let authority = &accounts[2]; // No signer check
let token_program = &accounts[3];
// Transfer A without validation
solana_program::program::invoke(
&spl_token::instruction::transfer(
token_program.key, token_a_source.key, &accounts[4].key,
authority.key, &[], amount_a,
)?,
&[token_a_source.clone(), accounts[4].clone(), authority.clone()],
)?;
// Transfer B without validation
solana_program::program::invoke(
&spl_token::instruction::transfer(
token_program.key, &accounts[5].key, token_b_dest.key,
authority.key, &[], amount_b,
)?,
&[accounts[5].clone(), token_b_dest.clone(), authority.clone()],
)?;
Ok(())
}
Fixed Code
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
pub fn swap(accounts: &[AccountInfo], amount_a: u64, amount_b: u64) -> ProgramResult {
let token_a_source = &accounts[0];
let token_b_dest = &accounts[1];
let authority = &accounts[2];
let token_program = &accounts[3];
// Validate all accounts
if token_a_source.owner != &spl_token::id() || token_b_dest.owner != &spl_token::id() {
return Err(ProgramError::IllegalOwner);
}
if token_program.key != &spl_token::id() {
return Err(ProgramError::IncorrectProgramId);
}
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// Validate mints match expected tokens
let source_data = spl_token::state::Account::unpack(&token_a_source.data.borrow())?;
if source_data.mint != EXPECTED_MINT_A {
return Err(ProgramError::InvalidArgument);
}
// Safe to proceed with transfers
// ...
Ok(())
}
Sample Sigvex Output
{
"detector_id": "spl-token-account-validation",
"severity": "high",
"confidence": 0.82,
"description": "SPL token CPI at block 0 stmt 4 uses accounts without proper validation. Missing checks: owner validation for token accounts, signer validation for authority.",
"location": { "function": "swap", "offset": 4 }
}
Detection Methodology
The detector performs a two-pass analysis:
- Validation collection: Scans all basic blocks for
CheckOwner,CheckSigner, andCheckKeystatements, building per-account validation sets. - SPL operation detection: Identifies CPI calls to SPL token operations (by program ID or instruction data heuristics), then checks that all accounts involved have the required validations (owner check for token accounts, signer check for authorities).
Context modifiers:
- Anchor programs: confidence reduced (Anchor’s typed accounts enforce validation)
- Admin/initialization functions: confidence reduced by 40%
Limitations
False positives:
- Programs that use a wrapper library for SPL operations which handles validation internally may be flagged because the validation is not visible at the CPI call site.
- Token accounts validated through ATA derivation (which implicitly proves ownership and mint) may be flagged.
False negatives:
- SPL token operations invoked through indirect CPI (passing instruction data as a variable) may not be recognized as token operations.
- Token-2022 extensions that add additional validation requirements are not specifically tracked.
Related Detectors
- Token Account Ownership — general CPI account ownership validation
- Missing Owner Check — general missing owner validation
- Missing Signer Check — missing authority signer validation