Skip to main content
Sigvex

Stale Account Data After CPI

Detects use of cached account data after CPI without reload, leading to stale reads.

Stale Account Data After CPI

Overview

Remediation Guide: How to Fix Stale Account Data

The stale account data detector identifies cases where account data is read before a CPI call and then used after the CPI returns without reloading. The called program may modify the account data during the CPI, making previously cached values stale. Using stale data leads to incorrect state transitions, authorization bypasses, or fund loss.

Why This Is an Issue

In Solana, account data is shared across programs during CPI. When Program A reads a balance, calls Program B via CPI, and Program B modifies the balance, Program A’s cached value is now stale. If Program A uses the stale balance for subsequent calculations (e.g., “balance is sufficient for withdrawal”), the result is incorrect. Attackers exploit this pattern to drain funds or bypass invariant checks.

CWE mapping: CWE-362 (Concurrent Execution Using Shared Resource with Improper Synchronization).

How to Resolve

Native Solana

pub fn process(accounts: &[AccountInfo]) -> ProgramResult {
    let vault = &accounts[0];

    // Read data before CPI
    let balance_before = read_balance(vault)?;

    // CPI that may modify the vault
    invoke(&transfer_ix, accounts)?;

    // FIXED: Re-read data after CPI
    let balance_after = read_balance(vault)?;

    // Use fresh data
    if balance_after < MIN_BALANCE {
        return Err(ProgramError::InsufficientFunds);
    }
    Ok(())
}

Anchor

pub fn process(ctx: Context<Process>) -> Result<()> {
    // CPI that may modify the account
    let cpi_ctx = CpiContext::new(/* ... */);
    token::transfer(cpi_ctx, amount)?;

    // FIXED: Reload account after CPI
    ctx.accounts.vault.reload()?;

    // Use fresh data
    require!(ctx.accounts.vault.amount >= min_balance, ErrorCode::InsufficientFunds);
    Ok(())
}

Examples

Vulnerable Code

pub fn compound(accounts: &[AccountInfo]) -> ProgramResult {
    let vault = &accounts[0];
    let cached_balance = {
        let data = vault.data.borrow();
        u64::from_le_bytes(data[0..8].try_into().unwrap())
    };

    // CPI modifies vault balance
    invoke(&yield_ix, accounts)?;

    // VULNERABLE: using cached_balance which is now stale
    let new_shares = cached_balance / SHARE_PRICE;
    update_shares(accounts, new_shares)?;
    Ok(())
}

Fixed Code

pub fn compound(accounts: &[AccountInfo]) -> ProgramResult {
    let vault = &accounts[0];

    invoke(&yield_ix, accounts)?;

    // FIXED: re-read fresh data after CPI
    let fresh_balance = {
        let data = vault.data.borrow();
        u64::from_le_bytes(data[0..8].try_into().unwrap())
    };

    let new_shares = fresh_balance / SHARE_PRICE;
    update_shares(accounts, new_shares)?;
    Ok(())
}

Sample Sigvex Output

{
  "detector_id": "stale-account-data",
  "severity": "high",
  "confidence": 0.80,
  "description": "Account data for 'var_0' was cached before CPI at stmt 1 and is being used at stmt 3 without reloading.",
  "location": { "function": "compound", "block": 0, "stmt": 3 }
}

Detection Methodology

  1. Account read tracking: Records which accounts have their data read (AccountData, AccountLamports) and at which statement.
  2. CPI boundary detection: Identifies InvokeCpi statements that divide the function into pre-CPI and post-CPI regions.
  3. Intra-block analysis: Within a single block, detects when an account is read before a CPI and the same account is read again after the CPI.
  4. Cross-block analysis: Tracks account reads across blocks to detect stale data patterns spanning multiple basic blocks.

Limitations

  • The detector conservatively assumes that any CPI can modify any account. In practice, only writable accounts passed to the CPI can be modified.
  • Explicit reload operations (like Anchor’s .reload()) at the HIR level may not be distinguishable from regular data reads.
  • Indirect account data access through helper functions is not tracked.

References