CPI in Loop Denial of Service
Detects CPI calls inside loops that can cause compute exhaustion and denial of service.
CPI in Loop Denial of Service
Overview
Remediation Guide: How to Fix CPI in Loop DOS
The CPI-in-loop detector identifies cross-program invocations placed inside loops, which can exhaust Solana’s 200,000 compute unit per-transaction budget. Each CPI call consumes a minimum of 5,000 compute units of overhead alone. Placing CPI calls inside unbounded or large loops creates a denial-of-service vector where an attacker forces high iteration counts, causing transactions to fail, potentially locking funds or breaking protocol functionality.
Why This Is an Issue
Solana transactions have a hard compute budget limit (200,000 CU by default, extendable to 1,400,000 CU). Each CPI call has significant fixed overhead. When CPI calls are placed inside loops that iterate over user-provided data, an attacker can submit a large array that causes the transaction to exceed its compute budget and fail. This can be exploited to prevent legitimate operations, lock funds in vaults, or break protocol invariants that depend on successful execution.
CWE mapping: CWE-400 (Uncontrolled Resource Consumption).
How to Resolve
Native Solana
// BAD: CPI in loop
for account in user_accounts.iter() {
invoke(&transfer_ix(account), accounts)?; // DOS risk!
}
// GOOD: Batch processing with single CPI
let batch_data: Vec<_> = user_accounts.iter().map(|a| prepare(a)).collect();
invoke(&batch_transfer_ix(&batch_data), accounts)?;
Anchor
// GOOD: Bounded iteration with compute check
pub fn process_batch(ctx: Context<BatchProcess>, items: Vec<Item>) -> Result<()> {
require!(items.len() <= MAX_BATCH_SIZE, ErrorCode::BatchTooLarge);
for item in items.iter() {
// Process locally without CPI
update_state(&mut ctx.accounts.state, item)?;
}
// Single CPI after loop
let cpi_ctx = CpiContext::new(/* ... */);
token::transfer(cpi_ctx, total_amount)?;
Ok(())
}
Examples
Vulnerable Code
pub fn distribute_rewards(accounts: &[AccountInfo], recipients: &[Pubkey]) -> ProgramResult {
for recipient in recipients { // Unbounded iteration
let ix = spl_token::instruction::transfer(/* ... */);
invoke(&ix, accounts)?; // Each iteration burns 5000+ CU
}
Ok(())
}
Fixed Code
const MAX_RECIPIENTS: usize = 10;
pub fn distribute_rewards(accounts: &[AccountInfo], recipients: &[Pubkey]) -> ProgramResult {
if recipients.len() > MAX_RECIPIENTS {
return Err(ProgramError::InvalidArgument);
}
for recipient in recipients {
let ix = spl_token::instruction::transfer(/* ... */);
invoke(&ix, accounts)?;
}
Ok(())
}
Sample Sigvex Output
{
"detector_id": "cpi-in-loop-dos",
"severity": "high",
"confidence": 0.80,
"description": "Cross-program invocation detected inside a loop (block 2 contains 1 CPI call(s)). Each CPI call consumes significant compute units.",
"location": { "function": "distribute_rewards", "block": 2, "stmt": 3 }
}
Detection Methodology
- Loop identification: Identifies blocks with conditional branches and comparison operations that indicate loop headers.
- CPI scanning: Checks each loop block for
InvokeCpistatements or CPI-containing expressions. - Excessive CPI detection: Also flags blocks with more than 8 CPI calls even outside loops, as these can approach budget limits.
- Context adjustment: Confidence is reduced for Anchor programs (CpiContext does not prevent the pattern) and admin functions.
Limitations
- Loop detection is heuristic-based; complex loop patterns (while-let, iterators) may not be recognized at the HIR level.
- Loops with known small bounds (e.g., iterating over a fixed-size array of 3 elements) may be flagged as false positives.
- The detector does not account for compute budget extensions via
ComputeBudgetInstruction::set_compute_unit_limit.
Related Detectors
- CPI Cycle — detects circular CPI patterns
- CPI Account List Mismatch — validates CPI account lists