Compute Budget Accounting
Overview
The compute budget accounting detector finds code that spends compute units without accounting for them: loops with no apparent iteration bound, expensive operations (CPIs, hashing, deserialization) inside loops that never check remaining budget, and recursive call patterns without depth tracking. Where DoS-focused detectors look for attacker-triggerable exhaustion, this detector targets unintentional budget mismanagement — reliability bugs where a program works on small inputs and fails unpredictably on large ones.
The failure mode is worse than a clean revert suggests. A transaction that exhausts its budget aborts at whatever instruction it had reached, and while the runtime rolls the transaction back atomically, protocols that spread one logical operation across multiple transactions can be left with the earlier transactions committed and the later ones impossible to land.
For remediation guidance, see Compute Budget Accounting Remediation.
Why This Is an Issue
Solana meters every transaction in compute units, and the ceiling is fixed before execution starts. A loop whose iteration count scales with account data grows quietly in cost as the protocol accrues users: a distribution loop over 10 holders fits comfortably; the same loop over 4,000 holders can never complete under any requestable budget. At that point the instruction is permanently unusable — funds or state behind it are stuck — and the fix requires a program upgrade, not a parameter change.
How to Resolve
Bound every loop explicitly, and check the remaining budget before each expensive iteration.
// Before: unbounded — cost scales with external data
for holder in holders.iter() {
distribute(holder)?;
}
// After: bounded batch with an explicit cursor for continuation
let end = state.cursor.saturating_add(MAX_PER_TX).min(holders.len());
for holder in &holders[state.cursor..end] {
distribute(holder)?;
}
state.cursor = end;
For loops whose per-iteration cost varies, gate on the runtime’s own meter:
for item in queue.iter() {
if sol_remaining_compute_units() < UNITS_PER_ITEM {
break; // graceful stop; resume in the next transaction
}
process(item)?;
}
Examples
Vulnerable Code
// Recursion with no depth limit: each level costs call overhead
// plus the body, and stack depth is also finite
fn settle_tree(node: &Node) -> Result<u64> {
let mut total = node.value;
for child in &node.children {
total += settle_tree(child)?;
}
Ok(total)
}
Fixed Code
fn settle_tree(node: &Node, depth: u8) -> Result<u64> {
require!(depth < MAX_DEPTH, ErrorCode::TreeTooDeep);
let mut total = node.value;
for child in &node.children {
total += settle_tree(child, depth + 1)?;
}
Ok(total)
}
Sample Sigvex Output
{
"detector": "compute-budget-accounting",
"severity": "medium",
"confidence": 0.8,
"message": "Loop contains expensive operations (cpi, hash) but does not check remaining compute units",
"location": { "function": "process_batch", "block": 3, "statement": 0 }
}
Detection Methodology
The detector builds the function’s control-flow graph and identifies loops via back edges. For each loop it checks three properties: whether the iteration count has a recognizable bound (constant limit, bounded counter comparison), whether the body contains expensive operations (CPI, syscalls, heavy arithmetic), and whether any budget query guards the body. Unbounded complex loops are reported at low severity with 0.50 confidence; expensive operations without a budget check raise a medium-severity finding at 0.80 confidence. Separately, self-referential call patterns are flagged as unbounded recursion at 0.70 confidence. Findings inside functions that look like admin or initialization entry points have their confidence reduced, since those run rarely and with controlled inputs.
Limitations
- Iteration bounds established outside the analyzed function (e.g., validated by the caller) are not visible, producing false positives on internally-safe loops — this is why the unbounded-loop finding carries reduced confidence.
- Budget checks performed through custom wrappers rather than recognizable syscall patterns may be missed, flagging loops that are actually guarded.
- The detector reasons per function; recursion through mutual calls across functions is not tracked.
Related Detectors
- DoS Compute Exhaustion — attacker-triggerable compute exhaustion
- Compute Exhaustion — resource-focused compute budget analysis
- CPI in Loop DoS — cross-program invocations inside loops