# Off-by-one settlement boundary bugs

Mismatched comparison operators on expiry, period, or loop bounds that shift which record settles

_Area: DeFi accounting and math bugs. Seen in 4 disclosed reports; latest 2024-12-09. Updated 2026-10-29._

## How it works
A state machine decides *when* something settles using a comparison on a height, timestamp, or index. A second piece of code — an event scheduler, a guard in another function, or a loop bound — encodes the same boundary with a different operator (`<` vs `<=`, `< 255` vs `<= 255`). At the exact boundary value the two disagree, so a record is treated as both active and expired, or is silently skipped. The invariant that breaks: every consumer of a boundary must agree on whether the boundary value is inside or outside the window.

## Where it shows up
- Status predicates (`GetStatus`, `isExpired`, `isLastPaymentCycle`) compared against heights/times computed elsewhere.
- Event scheduling heights (e.g. `EndHeight - UnbondingTime`) versus the predicate's inequality [E1](https://bugbountylibrary.com/r/sherlock%3A2024-12-babylon%3AH-2.md).
- Multiple functions sharing one period constant but using `<`, `<=`, `>` inconsistently [E3](https://bugbountylibrary.com/r/sherlock%3A2023-04-ajna%3AH-4.md).
- Fixed-size bitmap/array loops with 1-based indexing and a hardcoded upper bound [E2](https://bugbountylibrary.com/r/sherlock%3A2024-08-morphl2%3AM-3.md).
- Amortization/installment logic assuming duration is an exact multiple of the cycle [E4](https://bugbountylibrary.com/r/sherlock%3A2023-03-teller%3AM-11.md).

## How to review
- For each boundary, list every comparison touching it and confirm they classify the boundary value identically. The property: at the boundary value exactly one path may act.
- Check the scheduler's computed height against the predicate's operator; strict vs inclusive must match [E1](https://bugbountylibrary.com/r/sherlock%3A2024-12-babylon%3AH-2.md).
- For bitmap loops, confirm the bound covers the maximum index (`<= max`, not `< max`) [E2](https://bugbountylibrary.com/r/sherlock%3A2024-08-morphl2%3AM-3.md).
- For time windows, diff operators across all guards on the same period [E3](https://bugbountylibrary.com/r/sherlock%3A2023-04-ajna%3AH-4.md).
- For installments, test durations that are not multiples of the cycle and verify the split sums correctly [E4](https://bugbountylibrary.com/r/sherlock%3A2023-03-teller%3AM-11.md).

## Variants seen
- Strict `>` in status vs event firing at the boundary block, allowing double-processing [E1](https://bugbountylibrary.com/r/sherlock%3A2024-12-babylon%3AH-2.md).
- Loop bound excluding the last index, substituting `address(0)` [E2](https://bugbountylibrary.com/r/sherlock%3A2024-08-morphl2%3AM-3.md).
- Settlement permitted at exactly the period end while another guard treats it as still open [E3](https://bugbountylibrary.com/r/sherlock%3A2023-04-ajna%3AH-4.md).
- Last-cycle flag set one period early for irregular durations [E4](https://bugbountylibrary.com/r/sherlock%3A2023-03-teller%3AM-11.md).

## Impact and severity
Consequences ranged from chain-halting panic via double-decrement of bonded totals [E1](https://bugbountylibrary.com/r/sherlock%3A2024-12-babylon%3AH-2.md), to slashed funds misdirected and a staker escaping removal [E2](https://bugbountylibrary.com/r/sherlock%3A2024-08-morphl2%3AM-3.md), to bad debt shifted onto other depositors [E3](https://bugbountylibrary.com/r/sherlock%3A2023-04-ajna%3AH-4.md), to wrongful liquidation from an inflated final installment [E4](https://bugbountylibrary.com/r/sherlock%3A2023-03-teller%3AM-11.md). Severity rises when the boundary is reachable atomically in one transaction or when the misclassified record controls fund movement.

## Evidence
- E1: https://bugbountylibrary.com/r/sherlock%3A2024-12-babylon%3AH-2.md
- E2: https://bugbountylibrary.com/r/sherlock%3A2024-08-morphl2%3AM-3.md
- E3: https://bugbountylibrary.com/r/sherlock%3A2023-04-ajna%3AH-4.md
- E4: https://bugbountylibrary.com/r/sherlock%3A2023-03-teller%3AM-11.md
