SPL Token Account Validation Remediation
How to fix missing validation in SPL token operations.
Remediating SPL Token Account Validation Issues
Overview
Related Detector: SPL Token Account Validation
SPL token operations require comprehensive validation of all accounts: ownership by the SPL Token program, correct mint, authorized signer, and proper ATA derivation. The fix is to validate each account according to its role before invoking any token CPI.
Recommended Fix
Before (Vulnerable)
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
pub fn deposit(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let user_token = &accounts[0];
let pool_token = &accounts[1];
let authority = &accounts[2];
// No validation before CPI
solana_program::program::invoke(
&spl_token::instruction::transfer(
&spl_token::id(), user_token.key, pool_token.key,
authority.key, &[], amount,
)?,
&[user_token.clone(), pool_token.clone(), authority.clone()],
)?;
Ok(())
}
After (Fixed)
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
pub fn deposit(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let user_token = &accounts[0];
let pool_token = &accounts[1];
let authority = &accounts[2];
// Validate ownership
if user_token.owner != &spl_token::id() || pool_token.owner != &spl_token::id() {
return Err(ProgramError::IllegalOwner);
}
// Validate signer
if !authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// Validate mint match
let user_data = spl_token::state::Account::unpack(&user_token.data.borrow())?;
let pool_data = spl_token::state::Account::unpack(&pool_token.data.borrow())?;
if user_data.mint != pool_data.mint {
return Err(ProgramError::InvalidArgument);
}
solana_program::program::invoke(
&spl_token::instruction::transfer(
&spl_token::id(), user_token.key, pool_token.key,
authority.key, &[], amount,
)?,
&[user_token.clone(), pool_token.clone(), authority.clone()],
)?;
Ok(())
}
Alternative Mitigations
- Anchor typed accounts with token constraints: Anchor’s
token::mintandtoken::authorityconstraints validate ownership, mint, and authority in one declaration.
#[derive(Accounts)]
pub struct Deposit<'info> {
#[account(mut, token::mint = pool_mint, token::authority = authority)]
pub user_token: Account<'info, TokenAccount>,
#[account(mut, token::mint = pool_mint)]
pub pool_token: Account<'info, TokenAccount>,
pub authority: Signer<'info>,
pub pool_mint: Account<'info, Mint>,
pub token_program: Program<'info, Token>,
}
- ATA validation: For user-facing accounts, validate they are the canonical Associated Token Account for the user and mint.
let expected_ata = spl_associated_token_account::get_associated_token_address(
authority.key, &expected_mint,
);
if user_token.key != &expected_ata {
return Err(ProgramError::InvalidArgument);
}
- Comprehensive validation function: Create a reusable validation function that checks ownership, mint, and authority for each token account.
Common Mistakes
- Validating only one side of a transfer: Both source and destination token accounts must be validated. A fake destination can redirect funds.
- Checking mint on source but not destination: Ensures you send the right token but does not prevent receiving into a wrong-mint account.
- Forgetting to validate the token program itself: The token program account must be validated (
key == spl_token::id()) to prevent a fake token program from approving invalid operations. - Not checking authority matches token account authority: Even with a signer check, the signer must be the designated authority for the specific token account being operated on.
- Ignoring Token-2022 extensions: Transfer hooks, non-transferable tokens, and other extensions may require additional validation beyond basic ownership and mint checks.