Detects loop gas inefficiencies in Solidity contracts: uncached array length in loop conditions, storage variable reads inside loop bodies, unprotected loop counters without unchecked, post-increment usage, do-while opportunities, and short-circuit ordering. Use when writing or reviewing for/while loops in Foundry-based Solidity projects. Covers LO-001 through LO-006: length caching, body storage caching, unchecked counters, pre-increment, do-while patterns, and boolean short-circuit evaluation.
Find and fix gas inefficiencies in Solidity loop constructs. Every per-iteration cost multiplies linearly — 100 iterations × 100 gas = 10,000 gas. Loop bodies are the highest-leverage location for gas optimization in Solidity contracts.
for or while loop in a Solidity contractforge test --gas-report).length on a memory array is an MLOAD — already cheap; no caching needed.length if the loop body calls push() or pop()| Claim | Why it is wrong | Correct action |
|---|---|---|
| "The loop is small" | Per-iteration costs multiply linearly — 100 iterations × 100 gas = 10,000 gas | Apply all loop optimizations regardless of loop size |
| "The compiler handles caching" | Solidity does not cache storage reads across iterations; each loop iteration pays SLOAD cost | Explicitly cache all loop-invariant storage reads before the loop |
| "It already has unchecked" | unchecked on the counter does not cache the length or body storage reads | Apply LO-001 + LO-002 + LO-003 together; they are independent savings |
# Confirm Foundry is available
forge --version
# Run gas report to find expensive loop functions
forge test --gas-report
# Trace SLOAD count in a specific loop function
forge test --match-test <testName> -vvvv | grep SLOAD
| Loop Pattern | Gas Impact | Fix | Example Saving (100 iters) |
|---|---|---|---|
i < arr.length in condition | Medium — 97 gas/iter (warm SLOAD) | Cache to uint256 len = arr.length before loop | 9,603 gas |
| Storage var read in body | High — 97–2,097 gas/iter per var | Cache to local var before loop | 9,700–209,700 gas |
i++ without unchecked | Medium — ~30 gas/iter | unchecked { ++i; } | ~3,000 gas |
i++ vs ++i | Low — ~5 gas/iter | Use ++i inside unchecked block | ~500 gas |
for when body always runs | Low — ~13 gas one-time | do { ... } while (...) | 13 gas flat |
Expensive condition first in && | Variable | Reorder: cheapest condition first | Up to 2,100 gas on each short-circuit |
Priority order: LO-002 (body caching) > LO-001 (length caching) > LO-003 (unchecked) > LO-004 (pre-increment) > LO-005 (do-while) > LO-006 (short-circuit)
Step 1 — Identify all loops. Read the contract and list every for and while loop by
function name and line number. Note the loop bound expression for each.
Step 2 — Check loop bounds (is length cached?). For each loop, inspect the condition
expression. If it references storageArray.length directly (not via a local uint256), flag as
LO-001. Check that the flagged array's length is not modified inside the loop body before caching.
Step 3 — Check loop body (are storage reads inside?). For each loop body, identify every state variable reference. If a state variable is read inside the body and its value does not change between iterations (i.e., it is loop-invariant), flag as LO-002. Cache it before the loop.
Step 4 — Check counter increment (unchecked? pre-increment?). For each loop counter,
verify: (a) the increment is wrapped in unchecked {} (LO-003); (b) ++i is used rather than
i++ inside the unchecked block (LO-004). If the loop body provably runs at least once, consider
converting to do-while (LO-005). If any if/require in or around the loop uses && with an
expensive left-hand condition, flag as LO-006.
Only read these files when explicitly needed — do not load all three by default:
| File | Read only when… |
|---|---|
resources/PATTERNS.md | Encountering an LO-004/LO-005/LO-006 edge case not covered by the Quick Reference above |
resources/CHECKLIST.md | Producing a formal /decipher-gas-optimizoor:analyze report and confirming completeness |
resources/EXAMPLE_FINDING.md | Generating a report and needing the exact format for a multi-pattern loop finding |
docs/evm-gas-reference.md | You need authoritative SLOAD/MLOAD costs or unchecked arithmetic savings to back a gas estimate |
For each finding, produce:
[SEVERITY] <pattern title> — <brief description>
File: <path>, line <N>
Loop: <exact loop signature>
Estimated saving: <formula> (e.g., ~97 gas × N iterations)
Before:
<solidity code showing the anti-pattern>
After:
<solidity code showing the fix>
Concrete example finding:
[HIGH] Storage variable read inside loop — not cached
File: src/RewardDistributor.sol, line 45
Loop: for (uint256 i = 0; i < recipients.length; i++)
Estimated saving: ~97 gas × N iterations (warm SLOAD → MLOAD per iteration) + 97 gas × N for
uncached length + ~30 gas × N for checked counter. For 50 recipients: ~11,200 gas per call.
Before:
function distribute() external {
for (uint256 i = 0; i < recipients.length; i++) {
uint256 reward = balances[recipients[i]] * rewardRate; // rewardRate: SLOAD each iter
_pay(recipients[i], reward);
}
}
After:
function distribute() external {
uint256 _rewardRate = rewardRate; // SLOAD once before loop
uint256 len = recipients.length; // SLOAD once before loop
for (uint256 i = 0; i < len;) {
uint256 reward = balances[recipients[i]] * _rewardRate; // MLOAD: 3 gas
_pay(recipients[i], reward);
unchecked { ++i; }
}
}
Gas breakdown for 50 recipients: