Skip to main content
Sigvex

Unchecked CPI Return Remediation

How to fix CPI calls that ignore return values, preventing silent failures.

Unchecked CPI Return Remediation

Overview

Related Detector: Unchecked CPI Return Value

Unchecked CPI return values allow called programs to fail silently while the calling program continues execution with incorrect assumptions. The fix is to always propagate or explicitly check CPI return values.

Before (Vulnerable)

let _ = invoke(&transfer_ix, accounts); // Error discarded
update_state(accounts)?; // Proceeds even if transfer failed

After (Fixed)

invoke(&transfer_ix, accounts)?; // Error propagated
update_state(accounts)?; // Only runs if transfer succeeded

Alternative Mitigations

1. Explicit error handling

When you need custom error logic rather than simple propagation:

match invoke(&transfer_ix, accounts) {
    Ok(()) => {
        update_state(accounts)?;
    }
    Err(e) => {
        msg!("Transfer failed: {:?}", e);
        rollback_state(accounts)?;
        return Err(e);
    }
}

2. Conditional CPI with fallback

if let Err(e) = invoke(&primary_ix, accounts) {
    msg!("Primary CPI failed, attempting fallback");
    invoke(&fallback_ix, accounts)?;
}

3. Anchor pattern

the account-validation framework’s typed CPI helpers return Result and the ? operator is the standard pattern:

token::transfer(cpi_ctx, amount)?;

Common Mistakes

Mistake 1: Using let _ = to suppress the warning

// WRONG: silences the compiler warning but ignores the error
let _ = invoke(&ix, accounts);

Mistake 2: Checking but not returning the error

// WRONG: logs the error but continues execution
if invoke(&ix, accounts).is_err() {
    msg!("CPI failed");
    // Missing: return Err(...);
}
// Execution continues with incorrect state

Mistake 3: Wrapping in ok() or unwrap_or_default()

// WRONG: converts error to success, hiding the failure
invoke(&ix, accounts).ok();
invoke(&ix, accounts).unwrap_or_default();

References