# Stale or colliding checkpoint reads

Checkpoint or snapshot written and read in the same block, or a stale cached value reused in share math

_Area: DeFi accounting and math bugs. Seen in 4 disclosed reports; latest 2026-09-18. Updated 2026-11-02._

## How it works
Accounting code often stores a value once (a checkpoint, snapshot, delta or epoch) and later reconstructs state from it. The assumption that breaks is that the stored value is the *complete* record of what happened since the last read. When several state changes land between two reads, or a write path forgets to update the field that feeds the math, the reader sees a value that no longer matches reality. Share, debt or reward math then runs on a stale or collapsed number and drifts from true balances.

## Where it shows up
- AMM-integrated margin/leverage paths that write some deltas but not the reserve-feeding delta (e.g. `pairDelta`) [E1](https://bugbountylibrary.com/r/dhl%3A20260918-likwid.md)
- `Checkpoints`-style libraries where `getAtBlock` returns the first checkpoint of a block, not the final state [E2](https://bugbountylibrary.com/r/sherlock%3A2023-02-telcoin%3AM-4.md)
- Guards like `checkpointProtection` applied to exits but not entries [E2](https://bugbountylibrary.com/r/sherlock%3A2023-02-telcoin%3AM-4.md)
- Reward/epoch functions that early-return on `totalShares == 0`, leaving an epoch index unadvanced [E3](https://bugbountylibrary.com/r/c4%3A2022-12-tigris%3AM-16.md)
- Per-block snapshot mappings (`_redemptions[block.number]`) overwritten by a second call in the same block [E4](https://bugbountylibrary.com/r/cantina%3Aalchemix_may2025%3A3.2.16.md)
- `_sync()`-style functions that read only the *last* stored snapshot [E4](https://bugbountylibrary.com/r/cantina%3Aalchemix_may2025%3A3.2.16.md)

## How to review
For every value read in share/debt/reward math, find every writer and ask: can two writes happen between reads, and does the reader see the final one?
- The invariant: the value consumed by share math must equal the true cumulative state at read time, not the first or last partial write.
- Diff sibling code paths (leverage>0 vs leverage==0) for missing delta/reserve write-backs [E1](https://bugbountylibrary.com/r/dhl%3A20260918-likwid.md).
- For checkpoint libraries, confirm every state-mutating path is guarded, not just exits [E2](https://bugbountylibrary.com/r/sherlock%3A2023-02-telcoin%3AM-4.md).
- For early-returns, check whether the skipped index is consumed elsewhere as a time anchor [E3](https://bugbountylibrary.com/r/c4%3A2022-12-tigris%3AM-16.md).
- For sync functions, confirm per-event deltas are preserved, not collapsed into one snapshot [E4](https://bugbountylibrary.com/r/cantina%3Aalchemix_may2025%3A3.2.16.md).

## How finders spotted it
- Compared `_executeAddCollateralAndBorrow` against `_executeAddLeverage` and noticed the missing `pairDelta` assignment, then traced that `pairDelta` is the only field feeding `pairReserves` [E1](https://bugbountylibrary.com/r/dhl%3A20260918-likwid.md)
- Noticed `checkpointProtection` on exit functions but not `_stake()`, and reasoned `Checkpoints` returns the first checkpoint of a block [E2](https://bugbountylibrary.com/r/sherlock%3A2023-02-telcoin%3AM-4.md)
- Read `distribute()`'s early-return guard and traced how `createLock()` consumes `epoch` with no other update path [E3](https://bugbountylibrary.com/r/c4%3A2022-12-tigris%3AM-16.md)
- Read `redeem()` setting `lastRedemptionBlock` and `_redemptions[block.number]`, then traced `_sync()` using only the last snapshot [E4](https://bugbountylibrary.com/r/cantina%3Aalchemix_may2025%3A3.2.16.md)

## Variants seen
- Missing write-back of a reserve/delta field, freezing the quote [E1](https://bugbountylibrary.com/r/dhl%3A20260918-likwid.md)
- Same-block multi-write where the reader takes the first checkpoint [E2](https://bugbountylibrary.com/r/sherlock%3A2023-02-telcoin%3AM-4.md)
- Early-return skipping an epoch advance, later read as a stale anchor [E3](https://bugbountylibrary.com/r/c4%3A2022-12-tigris%3AM-16.md)
- Per-block snapshot overwrite collapsing intermediate events [E4](https://bugbountylibrary.com/r/cantina%3Aalchemix_may2025%3A3.2.16.md)

## Impact and severity
Stale reads fed directly into borrow quotes and share math. In E1 the frozen quote let the same static borrow be repeated, draining 74.31 BNB, capped only by a 20%-of-reserves limit [E1](https://bugbountylibrary.com/r/dhl%3A20260918-likwid.md). E2 understated historical balances and biased downstream vault decisions [E2](https://bugbountylibrary.com/r/sherlock%3A2023-02-telcoin%3AM-4.md); E3 inflated reward shares for later bonds [E3](https://bugbountylibrary.com/r/c4%3A2022-12-tigris%3AM-16.md); E4 miscomputed account debt and earmarked amounts [E4](https://bugbountylibrary.com/r/cantina%3Aalchemix_may2025%3A3.2.16.md). Impact scales with how many times the stale value can be re-read before a real update lands.

## Evidence
- E1: https://bugbountylibrary.com/r/dhl%3A20260918-likwid.md
- E2: https://bugbountylibrary.com/r/sherlock%3A2023-02-telcoin%3AM-4.md
- E3: https://bugbountylibrary.com/r/c4%3A2022-12-tigris%3AM-16.md
- E4: https://bugbountylibrary.com/r/cantina%3Aalchemix_may2025%3A3.2.16.md
