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.
Recommended Fix
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 ®istry.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: u8parameter through recursive helpers and reject inputs beyondMAX_DEPTH; better, rewrite as an iterative loop with an explicit stack of bounded size. - Request a higher limit for known-heavy paths. The
ComputeBudgetprogram’sSetComputeUnitLimitraises 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).