Skip to main content
Sigvex

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.

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);
}

References