Skip to main content
Sigvex

Unchecked CPI Return Value

Detects CPI calls without return value validation, allowing silent failures.

Unchecked CPI Return Value

Overview

Remediation Guide: How to Fix Unchecked CPI Return

The unchecked CPI return detector identifies cross-program invocations where the return value is not checked for errors. When a CPI call fails but the result is ignored, the calling program continues execution as if the call succeeded, leading to inconsistent state, loss of funds, or broken protocol invariants.

Why This Is an Issue

In Solana, invoke() and invoke_signed() return ProgramResult. If the called program returns an error but the calling program does not propagate or check it, execution continues with stale or incorrect assumptions. For example, a token transfer CPI that silently fails means the program believes funds moved when they did not, potentially allowing double-spending or state corruption.

CWE mapping: CWE-252 (Unchecked Return Value).

How to Resolve

Native Solana

// Always use the ? operator to propagate CPI errors
pub fn transfer(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    let ix = spl_token::instruction::transfer(/* ... */)?;
    invoke(&ix, accounts)?; // ? propagates any error
    Ok(())
}

Anchor

// Anchor CPI helpers return Result and should use ?
pub fn transfer(ctx: Context<Transfer>, amount: u64) -> Result<()> {
    let cpi_ctx = CpiContext::new(/* ... */);
    token::transfer(cpi_ctx, amount)?; // ? propagates errors
    Ok(())
}

Examples

Vulnerable Code

pub fn process(accounts: &[AccountInfo]) -> ProgramResult {
    let ix = build_transfer_ix(accounts)?;

    // VULNERABLE: return value discarded
    let _ = invoke(&ix, accounts);

    // Program continues as if transfer succeeded
    update_balance(accounts, new_balance)?;
    Ok(())
}

Fixed Code

pub fn process(accounts: &[AccountInfo]) -> ProgramResult {
    let ix = build_transfer_ix(accounts)?;

    // FIXED: propagate errors via ?
    invoke(&ix, accounts)?;

    update_balance(accounts, new_balance)?;
    Ok(())
}

Sample Sigvex Output

{
  "detector_id": "unchecked-cpi-return",
  "severity": "high",
  "confidence": 0.70,
  "description": "CPI return value is not checked for errors. The invoked program could fail silently, leading to inconsistent state or loss of funds.",
  "location": { "function": "process", "block": 0, "stmt": 1 }
}

Detection Methodology

  1. CPI identification: Scans for InvokeCpi statements (direct CPI calls without result capture) and CPI expressions in assignments.
  2. Result tracking: Collects variables used in comparisons, checks, returns, and branches to build a set of “checked” variables.
  3. Unchecked detection: Flags direct CPI statements (always unchecked by nature), CPI expressions without result capture, and CPI results assigned to variables that are never checked.
  4. Context adjustment: Confidence is reduced for Anchor programs (which idiomatically use ?) and read-only functions.

Limitations

  • The detector may flag CPI calls in generated code or low-level wrappers that handle errors through non-standard patterns.
  • CPI error handling via custom error mapping functions may not be recognized.
  • At the HIR level, InvokeCpi statements appear as unchecked by default; the actual Rust ? propagation may not be visible.

References