# Tick and tier boundary off-by-one

Boundary rank or tick derived from post-mutation counts or cached price, landing one position off

_Area: DeFi accounting and math bugs. Seen in 3 disclosed reports; latest 2025-05-21. Updated 2026-10-24._

## How it works
A boundary (a tier rank, a tick, a range edge) is computed from a value that no longer describes the state the boundary should reflect. Either the count was already mutated, or the cached tick lags the true price. The code assumes the number it reads still marks the correct edge. When the mutation does not touch the bounded quantity, or the cache is only refreshed on crossing, the recomputed boundary lands one position off: the wrong staker is demoted, the wrong range is set, or an ordered range collapses.

## Where it shows up
- Boundary recomputed on removal from post-removal counts (`new_t1 + new_t2`) [E1](https://bugbountylibrary.com/r/sherlock%3A2025-05-layeredge%3AH-1.md)
- Update logic that processes only the old boundary rank and skips the new one in a branch [E3](https://bugbountylibrary.com/r/sherlock%3A2025-05-layeredge%3AH-2.md)
- Ranges derived from a cached `slot0` tick instead of `sqrtPriceX96` [E2](https://bugbountylibrary.com/r/sherlock%3A2025-02-yieldoor%3AH-2.md)
- Sibling functions where one corrects negative modulo and the other does not [E4]

## How to review
State the invariant per boundary: the boundary rank must equal the actual last position of the lower tier after the mutation, and the range must satisfy `lower < upper` with ticks aligned to spacing. Ask whether the removed/added entity was in the tier being bounded; if not, the boundary must come from the unchanged side [E1](https://bugbountylibrary.com/r/sherlock%3A2025-05-layeredge%3AH-1.md). Check every branch of the update function, including the "count unchanged" branch, and confirm both old and new boundary ranks are processed [E3](https://bugbountylibrary.com/r/sherlock%3A2025-05-layeredge%3AH-2.md). For price-derived ranges, recompute the tick from `sqrtPriceX96` via TickMath and compare to the cached tick [E2](https://bugbountylibrary.com/r/sherlock%3A2025-02-yieldoor%3AH-2.md). For tick math, test negative inputs against the modulo correction used in the sibling function [E4].

## Variants seen
- Removal path: T3 exit leaves T1/T2 counts unchanged, boundary off by one, wrong staker demoted [E1](https://bugbountylibrary.com/r/sherlock%3A2025-05-layeredge%3AH-1.md)
- Insert path: new boundary rank never processed, promotion missed [E3](https://bugbountylibrary.com/r/sherlock%3A2025-05-layeredge%3AH-2.md)
- Cached tick off by one at a price boundary skews the range [E2](https://bugbountylibrary.com/r/sherlock%3A2025-02-yieldoor%3AH-2.md)
- Unpatched negative modulo makes `tickLower == tickUpper` [E4]

## Impact and severity
Overpaid APY and wrong demotions [E1](https://bugbountylibrary.com/r/sherlock%3A2025-05-layeredge%3AH-1.md), underpaid interest [E3](https://bugbountylibrary.com/r/sherlock%3A2025-05-layeredge%3AH-2.md), misallocated liquidity and lost fees [E2](https://bugbountylibrary.com/r/sherlock%3A2025-02-yieldoor%3AH-2.md), and division-by-zero reverts blocking rebalance [E4]. Severity rises when the boundary drives payouts or liquidity allocation and the error persists silently across many operations. E1 and E3 are separate reports against the same contract.

## Evidence
- E1: https://bugbountylibrary.com/r/sherlock%3A2025-05-layeredge%3AH-1.md
- E2: https://bugbountylibrary.com/r/sherlock%3A2025-02-yieldoor%3AH-2.md
- E3: https://bugbountylibrary.com/r/sherlock%3A2025-05-layeredge%3AH-2.md
