Skip to main content
Sigvex

CPI Account List Mismatch Remediation

How to fix CPI calls with unvalidated account lists.

CPI Account List Mismatch Remediation

Overview

Related Detector: CPI Account List Mismatch

CPI account list mismatch vulnerabilities allow attackers to manipulate the accounts passed to cross-program invocations. The fix is to validate account count, properties, and keys before every CPI call.

Before (Vulnerable)

pub fn transfer(accounts: &[AccountInfo]) -> ProgramResult {
    let ix = spl_token::instruction::transfer(/* ... */);
    invoke(&ix, accounts)?; // Account list not validated
    Ok(())
}

After (Fixed)

pub fn transfer(accounts: &[AccountInfo]) -> ProgramResult {
    // Validate exact account count
    if accounts.len() != 5 {
        return Err(ProgramError::NotEnoughAccountKeys);
    }
    // Validate critical accounts
    if !accounts[2].is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }
    if !accounts[0].is_writable || !accounts[1].is_writable {
        return Err(ProgramError::InvalidAccountData);
    }

    let ix = spl_token::instruction::transfer(/* ... */);
    invoke(&ix, accounts)?;
    Ok(())
}

Alternative Mitigations

1. Anchor typed accounts

#[derive(Accounts)]
pub struct Transfer<'info> {
    #[account(mut)]
    pub source: Account<'info, TokenAccount>,
    #[account(mut)]
    pub destination: Account<'info, TokenAccount>,
    pub authority: Signer<'info>,
    pub token_program: Program<'info, Token>,
}

2. Named account extraction with explicit indices

pub fn process(accounts: &[AccountInfo]) -> ProgramResult {
    let source = accounts.get(0).ok_or(ProgramError::NotEnoughAccountKeys)?;
    let dest = accounts.get(1).ok_or(ProgramError::NotEnoughAccountKeys)?;
    let authority = accounts.get(2).ok_or(ProgramError::NotEnoughAccountKeys)?;

    if !authority.is_signer { return Err(ProgramError::MissingRequiredSignature); }
    // Build account list explicitly
    let cpi_accounts = &[source.clone(), dest.clone(), authority.clone()];
    invoke(&ix, cpi_accounts)?;
    Ok(())
}

Common Mistakes

Mistake 1: Only checking account count, not properties

// INSUFFICIENT: count is right but accounts could be wrong
if accounts.len() == 5 {
    invoke(&ix, accounts)?; // No signer/writable checks
}

Mistake 2: Passing remaining_accounts without filtering

// WRONG: remaining_accounts may contain unexpected extra accounts
let all: Vec<_> = ctx.remaining_accounts.iter().collect();
invoke(&ix, &all)?;

Always validate remaining accounts before including them in CPI calls.

References