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
- CPI identification: Scans for
InvokeCpistatements (direct CPI calls without result capture) and CPI expressions in assignments. - Result tracking: Collects variables used in comparisons, checks, returns, and branches to build a set of “checked” variables.
- 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.
- 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,
InvokeCpistatements appear as unchecked by default; the actual Rust?propagation may not be visible.
Related Detectors
- CPI Return Forgery — detects forged CPI return values
- CPI Program Validation — validates CPI target programs