Loop Gas Exhaustion
Detects loops without proper iteration bounds where the count can be influenced by external input, causing DoS via block gas limit exhaustion.
Loop Gas Exhaustion
Overview
The loop gas exhaustion detector identifies unbounded loops in externally callable functions where the iteration count depends on user-controlled calldata, external contract return values, or mutable storage state. When an attacker can influence the loop bound, they can force the function to consume more gas than the block gas limit allows, making it permanently uncallable for those inputs.
This is classified as SWC-128 (DoS With Block Gas Limit) and affects batch processing, airdrop distribution, and any function that iterates over a dynamic collection.
Why This Is an Issue
Each loop iteration that performs storage writes (~20,000 gas), external calls (~25,000 gas), or hash operations consumes significant gas. A loop bounded by array.length from calldata allows an attacker to submit an array of arbitrary size. For a loop body that costs 45,000 gas per iteration (one SSTORE + one CALL), only ~650 iterations exhaust the 30M block gas limit.
The consequences include:
- Permanently locked funds if a withdrawal function iterates over depositors.
- Blocked protocol operations if governance or epoch functions iterate over a growing list.
- Griefing attacks where a cheap operation (adding entries to a list) makes an expensive operation (iterating the list) uncallable.
How to Resolve
// Before: Vulnerable -- loop bound from calldata
function distributeTokens(address[] calldata recipients, uint256[] calldata amounts) external {
for (uint256 i = 0; i < recipients.length; i++) {
token.transfer(recipients[i], amounts[i]);
}
}
// After: Fixed -- maximum iteration limit
uint256 constant MAX_BATCH_SIZE = 100;
function distributeTokens(address[] calldata recipients, uint256[] calldata amounts) external {
require(recipients.length <= MAX_BATCH_SIZE, "Batch too large");
require(recipients.length == amounts.length, "Length mismatch");
for (uint256 i = 0; i < recipients.length; i++) {
token.transfer(recipients[i], amounts[i]);
}
}
Examples
Vulnerable
function processAll() external {
uint256 count = staking.getStakerCount(); // External call -- unbounded
for (uint256 i = 0; i < count; i++) {
address staker = staking.getStaker(i);
_distribute(staker); // SSTORE + CALL per iteration
}
}
Fixed (Pagination)
uint256 constant MAX_PER_BATCH = 50;
function processBatch(uint256 startIndex, uint256 count) external {
require(count <= MAX_PER_BATCH, "Batch too large");
uint256 total = staking.getStakerCount();
uint256 end = startIndex + count;
if (end > total) end = total;
for (uint256 i = startIndex; i < end; i++) {
address staker = staking.getStaker(i);
_distribute(staker);
}
}
Sample Sigvex Output
[HIGH] loop-gas-exhaustion
Loop without maximum iteration guard in function 'distributeTokens'
Location: distributeTokens @ block 2, instruction 0
Confidence: 0.85
Risk Factors:
- Loop bound from user-controlled calldata
- No constant maximum iteration limit
- 1 external call(s) per iteration
- 1 storage write(s) per iteration
- Function is publicly accessible
Gas Analysis:
- Estimated gas per iteration: 45900 gas
Detection Methodology
- Loop identification: Detects back-edges in control flow (conditional or unconditional jumps to earlier blocks).
- Bound analysis: Checks whether the loop has a constant upper bound (a comparison against a compile-time constant). Loops without constant bounds are flagged.
- Dynamic bound source: Traces loop bounds to calldata (
CALLDATALOAD), external calls (CALL/STATICCALL), or storage (SLOAD) to classify the input source. - Gas estimation: Counts expensive operations per iteration (SSTORE, SLOAD, CALL, KECCAK256) and estimates gas consumption.
- Confidence scoring: Higher confidence for calldata-bounded, public, unbounded loops with expensive operations. Lower confidence for constant-bounded or access-controlled functions.
Limitations
False positives: Loops bounded by a constant defined in a different contract or inherited from a base class may not be resolved. Governance and batch token contracts intentionally use unbounded loops with external gas accounting. False negatives: Loops where the bound is computed through a chain of function calls or complex arithmetic may not have the bound fully resolved.
Related Detectors
- DoS — detects general denial of service patterns
- DoS with Failed Call — detects unchecked calls in loops
- Controlled Array Length — detects unbounded storage array iteration
- Memory Expansion DoS — detects unbounded memory allocation