# Checkpoint and epoch boundary off-by-one

Write and read paths using different boundary predicates, so one record is double-counted, dropped, or misattributed

_Area: DeFi accounting and math bugs. Seen in 47 disclosed reports; latest 2026-04-27. Updated 2026-10-23._

## How it works
A protocol writes a record at a boundary (timestamp, block, rank, epoch end) using one predicate, then reads it back using a different one. The write side excludes a slot the read side includes, or the two sides round or order the same value differently. The broken assumption is that "the boundary" means one thing on both sides. When it does not, an entry is counted twice, dropped, or attributed to the wrong owner, and balances drift from reality. The core pair to name explicitly: the predicate that creates the record versus the predicate that authorizes a read of it.

## Where it shows up
- Batch builders that decrement an index (`latestEligibleIndex--`) while the claim path accepts `<=` the same timestamp [E1](https://bugbountylibrary.com/r/sherlock%3A2025-07-mellow-flexible-vaults%3AH-2.md)
- Epoch-end helpers returning the first timestamp of the next epoch, combined with inclusive lookups (`ts <= t`) [E6](https://bugbountylibrary.com/r/c4%3A2026-03-intuition%3AM-02.md)
- Checkpoint writers that append per call without batching by timestamp, leaving duplicate timestamps for binary search [E2](https://bugbountylibrary.com/r/c4%3A2022-09-nouns-builder%3AH-03.md)
- Epoch writers that increment the epoch even when the new point shares the previous timestamp [E8](https://bugbountylibrary.com/r/c4%3A2025-10-hybra-finance%3AM-08.md)
- Boundary-rank updates that process only the old boundary in the count-unchanged branch, leaving the new boundary unprocessed [E3](https://bugbountylibrary.com/r/sherlock%3A2025-05-layeredge%3AH-2.md)
- Checkpoint loops whose break condition (`t > rounded_timestamp`) records the still-open week before it ends [E4](https://bugbountylibrary.com/r/sherlock%3A2024-06-velocimeter%3AH-9.md)
- Time-indexed bitmaps where the event time equals block time and the branch skips initialization [E7](https://bugbountylibrary.com/r/c4%3A2025-11-ekubo%3AM-02.md)
- Bucketing that truncates timestamps to epoch start, so an instant of balance counts as a full epoch [E5](https://bugbountylibrary.com/r/c4%3A2023-08-verwa%3AH-01.md)

## How to review
The invariant: the predicate that authorizes a read must be identical to the predicate that created the record. Check operator pairs directly: `<` vs `<=`, `--` vs `<=`, `upperLookup` vs `lowerLookup`. For each boundary confirm (a) batch/claim, snapshot/claim, and epoch-end/lookup pairs use the same operator and boundary value; (b) multiple writes in one block or timestamp are batched to the latest value so lookups are unambiguous [E2](https://bugbountylibrary.com/r/c4%3A2022-09-nouns-builder%3AH-03.md)[E8](https://bugbountylibrary.com/r/c4%3A2025-10-hybra-finance%3AM-08.md); (c) both old and new boundary ranks are processed in every branch, including count-unchanged [E3](https://bugbountylibrary.com/r/sherlock%3A2025-05-layeredge%3AH-2.md); (d) checkpoint loops do not record an epoch that has not yet closed [E4](https://bugbountylibrary.com/r/sherlock%3A2024-06-velocimeter%3AH-9.md); (e) the `==` case of a time branch is handled, not silently falling into the else [E7](https://bugbountylibrary.com/r/c4%3A2025-11-ekubo%3AM-02.md); (f) bucketed accounting does not treat an instant of balance as a full epoch [E5](https://bugbountylibrary.com/r/c4%3A2023-08-verwa%3AH-01.md). Compare single-call and multi-call paths for identical end state.

## Variants seen
- Claim predicate broader than batch predicate, letting a later user drain an earlier batch [E1](https://bugbountylibrary.com/r/sherlock%3A2025-07-mellow-flexible-vaults%3AH-2.md)
- Inclusive epoch-end lookup retroactively qualifying next-epoch locks [E6](https://bugbountylibrary.com/r/c4%3A2026-03-intuition%3AM-02.md)
- Duplicate same-timestamp checkpoints steering binary search [E2](https://bugbountylibrary.com/r/c4%3A2022-09-nouns-builder%3AH-03.md)
- Same-timestamp lock writes creating a new epoch that hides the added amount [E8](https://bugbountylibrary.com/r/c4%3A2025-10-hybra-finance%3AM-08.md)
- New boundary rank left unprocessed when tier counts are unchanged [E3](https://bugbountylibrary.com/r/sherlock%3A2025-05-layeredge%3AH-2.md)
- Open-week checkpoint recorded before the week ends, excluding later locks [E4](https://bugbountylibrary.com/r/sherlock%3A2024-06-velocimeter%3AH-9.md)
- Boundary-equal branch skipping bitmap registration, exposing historical rewards [E7](https://bugbountylibrary.com/r/c4%3A2025-11-ekubo%3AM-02.md)
- Week-start truncation letting a flash deposit capture a full epoch of rewards [E5](https://bugbountylibrary.com/r/c4%3A2023-08-verwa%3AH-01.md)

## Impact and severity
Outcomes: users locked out of shares while another drains a batch [E1](https://bugbountylibrary.com/r/sherlock%3A2025-07-mellow-flexible-vaults%3AH-2.md); wrong governance vote weight [E2](https://bugbountylibrary.com/r/c4%3A2022-09-nouns-builder%3AH-03.md); underpaid interest from a missed tier promotion [E3](https://bugbountylibrary.com/r/sherlock%3A2025-05-layeredge%3AH-2.md); a lock capturing nearly all future distribution rewards [E4](https://bugbountylibrary.com/r/sherlock%3A2024-06-velocimeter%3AH-9.md); a flash deposit earning a full week of rewards [E5](https://bugbountylibrary.com/r/c4%3A2023-08-verwa%3AH-01.md); retroactive emissions [E6](https://bugbountylibrary.com/r/c4%3A2026-03-intuition%3AM-02.md); drained shared reward pool [E7](https://bugbountylibrary.com/r/c4%3A2025-11-ekubo%3AM-02.md); understated voting power and rewards [E8](https://bugbountylibrary.com/r/c4%3A2025-10-hybra-finance%3AM-08.md). Severity is highest when the boundary is attacker-steerable (spam checkpoints [E2](https://bugbountylibrary.com/r/c4%3A2022-09-nouns-builder%3AH-03.md), boundary-equal events [E7](https://bugbountylibrary.com/r/c4%3A2025-11-ekubo%3AM-02.md), flash deposits [E5](https://bugbountylibrary.com/r/c4%3A2023-08-verwa%3AH-01.md)) and funds move directly on the misread value; accounting-only drift is lower.

## Evidence
- E1: https://bugbountylibrary.com/r/sherlock%3A2025-07-mellow-flexible-vaults%3AH-2.md
- E2: https://bugbountylibrary.com/r/c4%3A2022-09-nouns-builder%3AH-03.md
- E3: https://bugbountylibrary.com/r/sherlock%3A2025-05-layeredge%3AH-2.md
- E4: https://bugbountylibrary.com/r/sherlock%3A2024-06-velocimeter%3AH-9.md
- E5: https://bugbountylibrary.com/r/c4%3A2023-08-verwa%3AH-01.md
- E6: https://bugbountylibrary.com/r/c4%3A2026-03-intuition%3AM-02.md
- E7: https://bugbountylibrary.com/r/c4%3A2025-11-ekubo%3AM-02.md
- E8: https://bugbountylibrary.com/r/c4%3A2025-10-hybra-finance%3AM-08.md
