Skip to main content
Sigvex

Stale Account Data Remediation

How to fix stale account data usage after CPI calls.

Stale Account Data Remediation

Overview

Related Detector: Stale Account Data After CPI

Stale account data vulnerabilities occur when programs cache account data before a CPI and use the stale values after the CPI returns. The fix is to always reload account data after any CPI that could modify the relevant accounts.

Before (Vulnerable)

let balance = get_balance(vault)?;
invoke(&cpi_ix, accounts)?;
// Using stale balance
if balance >= withdrawal_amount { /* ... */ }

After (Fixed)

invoke(&cpi_ix, accounts)?;
// Re-read fresh data after CPI
let balance = get_balance(vault)?;
if balance >= withdrawal_amount { /* ... */ }

Alternative Mitigations

1. Anchor reload pattern

// After CPI, reload the account to get fresh data
ctx.accounts.vault.reload()?;
let fresh_amount = ctx.accounts.vault.amount;

2. Checks-effects-interactions ordering

Perform all reads and checks before CPI, eliminating the need for post-CPI reads:

// CHECKS: read and validate before CPI
let balance = get_balance(vault)?;
require!(balance >= amount, InsufficientFunds);

// EFFECTS: update state before CPI
set_balance(vault, balance - amount)?;

// INTERACTIONS: CPI last
invoke(&transfer_ix, accounts)?;

3. Post-CPI assertion pattern

Re-read and assert expected state after CPI:

let balance_before = get_balance(vault)?;
invoke(&deposit_ix, accounts)?;

// Assert the expected post-CPI state
let balance_after = get_balance(vault)?;
require!(
    balance_after == balance_before + deposit_amount,
    ErrorCode::UnexpectedBalance
);

Common Mistakes

Mistake 1: Caching in a local variable and forgetting to refresh

let cached = vault.data.borrow()[0..8]; // Cached before CPI
invoke(&ix, accounts)?;
// cached is stale -- must re-borrow vault.data

Mistake 2: Reading lamports before CPI

let lamports = vault.lamports(); // Stale after CPI
invoke(&transfer_ix, accounts)?;
if lamports > 0 { /* wrong -- CPI may have drained lamports */ }

Lamports are also account data that can change during CPI.

Mistake 3: Only reloading some accounts

ctx.accounts.vault.reload()?; // Reloaded
// But also need to reload other accounts modified by CPI
// ctx.accounts.user_token.reload()?; // Missing!

References