Skip to main content
Sigvex

Compute Budget Accounting Remediation

How to make compute costs predictable by bounding loops, batching with cursors, and checking remaining compute units before expensive operations.

Compute Budget Accounting Remediation

Overview

Related Detector: Compute Budget Accounting

Compute cost that scales with data size eventually collides with Solana’s fixed per-transaction budget. The failure arrives late — in production, on the first large account — and an instruction that can no longer complete under any budget is effectively bricked until a program upgrade. The fix is to make every instruction’s worst-case cost a design-time constant: bound loops, split unbounded work into resumable batches, and cap recursion depth.

Before (Vulnerable)

pub fn distribute(ctx: Context<Distribute>) -> Result<()> {
    // Cost grows with holder count; will eventually exceed
    // the budget and the instruction becomes unusable
    for holder in ctx.accounts.registry.holders.iter() {
        pay_out(holder)?;
    }
    Ok(())
}

After (Fixed)

pub fn distribute(ctx: Context<Distribute>, batch: u16) -> Result<()> {
    let registry = &mut ctx.accounts.registry;
    let start = registry.cursor as usize;
    let end = start
        .saturating_add(batch.min(MAX_BATCH) as usize)
        .min(registry.holders.len());

    for holder in &registry.holders[start..end] {
        pay_out(holder)?;
    }

    registry.cursor = end as u64;      // resumable: next call continues here
    if end == registry.holders.len() {
        registry.cursor = 0;
        registry.round += 1;
    }
    Ok(())
}

The cursor turns one unbounded instruction into a series of constant-cost ones. MAX_BATCH is chosen so that MAX_BATCH * cost_per_item fits the budget with margin; anyone can crank the remaining batches.

Alternative Mitigations

  • Runtime budget gate. Where per-item cost varies, stop cleanly instead of aborting: if sol_remaining_compute_units() < UNITS_PER_ITEM { break; } and persist progress before returning.
  • Depth-limited recursion. Thread a depth: u8 parameter through recursive helpers and reject inputs beyond MAX_DEPTH; better, rewrite as an iterative loop with an explicit stack of bounded size.
  • Request a higher limit for known-heavy paths. The ComputeBudget program’s SetComputeUnitLimit raises the ceiling up to the network maximum — useful headroom, but it changes the constant, not the asymptotics. Batching is still required for unbounded data.

Common Mistakes

Mistake: Bounding the Loop but Not the Data

for holder in registry.holders.iter().take(1000) { ... }  // silently skips the rest

take(N) without a cursor drops items instead of deferring them. Pair every cap with persisted progress.

Mistake: Checking the Budget After the Expensive Call

invoke(&heavy_cpi, &accounts)?;                       // may abort here
if sol_remaining_compute_units() < RESERVE { ... }    // too late

Check before spending, and keep a reserve for the instruction’s own epilogue (serialization, event emission).

References