Remediating Off-By-One Errors
How to fix off-by-one errors in loop boundaries by using correct comparison operators and avoiding length-1 patterns.
Remediating Off-By-One Errors
Overview
Related Detector: Off-By-One
Off-by-one errors in loop boundaries cause one element too many or too few to be processed. The fix depends on the specific pattern: replace length - 1 with length for full iteration, replace <= with < for array access, and simplify negated conditions to direct comparisons.
Recommended Fix
Use Standard Loop Idiom
// BEFORE: Skips last element
for (uint256 i = 0; i < array.length - 1; i++) {
process(array[i]);
}
// AFTER: Processes all elements
for (uint256 i = 0; i < array.length; i++) {
process(array[i]);
}
Fix Inclusive Upper Bound
// BEFORE: Out-of-bounds on last iteration
for (uint256 i = 0; i <= array.length; i++) {
process(array[i]); // Reverts when i == array.length
}
// AFTER: Correct exclusive upper bound
for (uint256 i = 0; i < array.length; i++) {
process(array[i]);
}
Alternative Mitigations
Explicit Bounds Checking
When accessing an array by computed index, validate the index explicitly:
function getElement(uint256[] storage arr, uint256 index) internal view returns (uint256) {
require(index < arr.length, "Index out of bounds");
return arr[index];
}
Use Solidity 0.8+ for Underflow Protection
In Solidity 0.8+, array.length - 1 reverts with a panic when array.length == 0, preventing the underflow that creates unbounded loops in older versions. However, the off-by-one logic error (skipping the last element) still exists — the compiler only catches the zero-length edge case.
// Solidity 0.8+: safe from underflow, but still skips last element
for (uint256 i = 0; i < array.length - 1; i++) {
process(array[i]); // Last element still skipped
}
Common Mistakes
Mistake: Applying length-1 for “All Pairs” Without Comment
// Is this intentional (comparing adjacent pairs) or a bug?
for (uint256 i = 0; i < array.length - 1; i++) {
require(array[i] <= array[i + 1], "Not sorted");
}
If length - 1 is intentional (e.g., comparing adjacent pairs), add a comment. Without documentation, this pattern is indistinguishable from a bug during review.
Mistake: Off-by-One in Reverse Loops
// WRONG: skips index 0
for (uint256 i = array.length - 1; i > 0; i--) {
process(array[i]); // array[0] never processed
}
// CORRECT: processes all elements including index 0
for (uint256 i = array.length; i > 0; i--) {
process(array[i - 1]);
}
Mistake: Mixing 0-Based and 1-Based Indexing
// WRONG: roundId starts at 1, but loop starts at 0
for (uint256 i = 0; i < totalRounds; i++) {
_processRound(i); // Round 0 does not exist
}
// CORRECT: match the indexing scheme
for (uint256 i = 1; i <= totalRounds; i++) {
_processRound(i);
}