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.
Recommended Fix
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.