Skip to main content
Sigvex

CPI in Loop DOS Remediation

How to fix CPI calls inside loops that can exhaust compute budget.

CPI in Loop DOS Remediation

Overview

Related Detector: CPI in Loop DOS

CPI-in-loop vulnerabilities cause denial of service by exhausting Solana’s per-transaction compute budget. The fix is to move CPI calls outside loops, batch operations into single calls, or enforce strict iteration bounds.

Before (Vulnerable)

pub fn process_all(accounts: &[AccountInfo], items: &[Item]) -> ProgramResult {
    for item in items {
        let ix = build_transfer_ix(item);
        invoke(&ix, accounts)?; // Unbounded CPI in loop
    }
    Ok(())
}

After (Fixed)

const MAX_ITEMS: usize = 5;

pub fn process_all(accounts: &[AccountInfo], items: &[Item]) -> ProgramResult {
    if items.len() > MAX_ITEMS {
        return Err(ProgramError::InvalidArgument);
    }

    // Collect data in loop, single CPI after
    let mut total_amount: u64 = 0;
    for item in items {
        total_amount = total_amount.checked_add(item.amount)
            .ok_or(ProgramError::ArithmeticOverflow)?;
    }

    let ix = build_batch_transfer_ix(total_amount);
    invoke(&ix, accounts)?;
    Ok(())
}

Alternative Mitigations

1. Pagination across transactions

Split large operations across multiple transactions:

pub fn process_page(
    accounts: &[AccountInfo],
    start_index: u32,
    page_size: u32,
) -> ProgramResult {
    let end = start_index.checked_add(page_size.min(MAX_PAGE_SIZE))
        .ok_or(ProgramError::ArithmeticOverflow)?;

    for i in start_index..end {
        let ix = build_ix(i);
        invoke(&ix, accounts)?;
    }
    Ok(())
}

2. Compute budget monitoring

Check remaining compute units inside the loop and break early:

for item in items.iter().take(MAX_ITEMS) {
    let remaining = sol_remaining_compute_units();
    if remaining < MIN_CU_FOR_CPI {
        break; // Exit before exhausting budget
    }
    invoke(&build_ix(item), accounts)?;
}

3. Off-chain aggregation

Move iteration off-chain and submit aggregated results:

// Off-chain: compute merkle root of all transfers
// On-chain: verify merkle proof and execute single batch CPI
pub fn execute_batch(accounts: &[AccountInfo], proof: &MerkleProof) -> ProgramResult {
    verify_proof(proof)?;
    invoke(&batch_ix, accounts)?; // Single CPI
    Ok(())
}

Common Mistakes

Mistake 1: Unbounded user-supplied arrays

// WRONG: no size limit on recipients
pub fn airdrop(accounts: &[AccountInfo], recipients: Vec<Pubkey>) -> ProgramResult {
    for r in recipients { /* CPI per recipient */ }
}

Always validate collection sizes before iteration.

Mistake 2: Relying on compute budget extension alone

// INSUFFICIENT: extending budget delays but doesn't prevent the attack
// Attacker can still submit arrays larger than extended budget allows

Budget extensions are a supplement, not a replacement for bounded loops.

References