Skip to main content
Sigvex

CPI Cycle Remediation

How to fix circular CPI patterns that cause infinite loops or depth limit failures.

CPI Cycle Remediation

Overview

Related Detector: CPI Cycle Detection

CPI cycle vulnerabilities arise when programs can invoke themselves or form circular call chains, exceeding Solana’s CPI depth limit of 4 and causing transaction failures. The fix is to validate CPI targets are not self-referential and to minimize CPI chain depth.

Before (Vulnerable)

pub fn proxy(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
    let target = &accounts[2]; // Could be self
    invoke(&build_ix(target.key, data), accounts)?;
    Ok(())
}

After (Fixed)

pub fn proxy(accounts: &[AccountInfo], program_id: &Pubkey, data: &[u8]) -> ProgramResult {
    let target = &accounts[2];

    // Prevent self-invocation
    if target.key == program_id {
        return Err(ProgramError::InvalidArgument);
    }

    invoke(&build_ix(target.key, data), accounts)?;
    Ok(())
}

Alternative Mitigations

1. Whitelist allowed CPI targets

const ALLOWED_TARGETS: &[Pubkey] = &[TOKEN_PROGRAM_ID, SYSTEM_PROGRAM_ID];

pub fn safe_proxy(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
    let target = &accounts[2];
    if !ALLOWED_TARGETS.contains(target.key) {
        return Err(ProgramError::IncorrectProgramId);
    }
    invoke(&build_ix(target.key, data), accounts)?;
    Ok(())
}

2. Reentrancy guard via state flag

pub fn guarded_call(accounts: &[AccountInfo]) -> ProgramResult {
    let state = &accounts[0];
    let mut data = state.data.borrow_mut();

    // Check and set reentrancy guard
    if data[0] != 0 { return Err(ProgramError::InvalidAccountData); }
    data[0] = 1; // Lock

    invoke(&ix, accounts)?;

    data[0] = 0; // Unlock
    Ok(())
}

3. Flatten CPI chains

Refactor deep CPI chains into flat structures where the top-level program orchestrates all calls directly rather than nesting them.

Common Mistakes

Mistake 1: Only checking program name, not key

// WRONG: string comparison is not reliable
if target.key.to_string() != "MyProgram" { /* ... */ }

Always compare Pubkey values directly.

Mistake 2: Assuming Solana prevents all cycles

Solana enforces a depth limit of 4 but does not prevent cycles within that limit. Programs A and B can still call each other if the total depth stays under 4.

References