# Epoch and window eligibility mismatch

Eligibility or cooldown measured by epoch while state, weights or rewards use time, or vice versa

_Area: DeFi accounting and math bugs. Seen in 4 disclosed reports; latest 2024-08-06. Updated 2026-10-24._

## How it works
A protocol keeps two clocks: an epoch/cycle counter and wall-clock time (or absolute week rounding). Eligibility, cooldowns, weights or reward windows are computed on one clock while the state they gate is recorded on the other. When the two drift — a cycle longer than a week, an epoch counter that lags `block.timestamp`, a weight zeroed at a different boundary than the one being claimed — the check passes or fails at the wrong moment. The broken assumption is that "epoch N" and "the time window for epoch N" always coincide.

## Where it shows up
- Cooldown or exercise check uses `block.timestamp` while reward accounting uses an epoch number [E4](https://bugbountylibrary.com/r/c4%3A2024-02-tapioca%3AM-14.md).
- Epoch length is variable (e.g. `>= 7 days` between rollovers) but another module rounds to fixed absolute weeks [E1](https://bugbountylibrary.com/r/sherlock%3A2023-11-convergence%3AM-4.md).
- A removal/zeroing writes a weight at `nextTime` while the claim loop still includes the removal epoch [E2](https://bugbountylibrary.com/r/c4%3A2024-05-olas%3AM-09.md).
- Eligibility checked against the *current* epoch rather than the position's recorded lock window [E3](https://bugbountylibrary.com/r/c4%3A2024-02-tapioca%3AM-03.md).
- Reward loops bounded by `lastClaimedEpoch` or `epochAfterRemoved` that include an epoch whose snapshot is already zeroed [E2](https://bugbountylibrary.com/r/c4%3A2024-05-olas%3AM-09.md).

## How to review
The invariant: for every position, the epoch used to test eligibility must be the same epoch whose snapshot (weight, lock end, denominator) is read, and that snapshot must not be mutated by the action being gated. Concretely: diff the unlock/eligibility computation of each parallel clock with concrete timestamps; confirm the epoch counter cannot lag the timestamp when rewards are computed; confirm a zeroing write lands strictly after the last epoch it should affect; confirm exercise eligibility reads the position's stored lock window, not the live epoch.

## Variants seen
- Cycle counter vs absolute-week rounding drift, making locks unwithdrawable [E1](https://bugbountylibrary.com/r/sherlock%3A2023-11-convergence%3AM-4.md).
- Weight zeroed before the epoch it was meant to still cover [E2](https://bugbountylibrary.com/r/c4%3A2024-05-olas%3AM-09.md).
- Eligibility by current epoch instead of recorded lock window, allowing double exercise [E3](https://bugbountylibrary.com/r/c4%3A2024-02-tapioca%3AM-03.md).
- Timestamp cooldown with epoch-indexed reward denominator, allowing early exercise against a stale denominator [E4](https://bugbountylibrary.com/r/c4%3A2024-02-tapioca%3AM-14.md).

## Why fixes fail
A prior mitigation for the epoch-boundary double-exercise was lost in a later refactor, and the bug reappeared in the audited codebase [E3](https://bugbountylibrary.com/r/c4%3A2024-02-tapioca%3AM-03.md). Timing fixes are easy to drop when code is restructured.

## Impact and severity
Yield and voting power DoS for users [E1](https://bugbountylibrary.com/r/sherlock%3A2023-11-convergence%3AM-4.md); lost staking incentives for a removed nominee [E2](https://bugbountylibrary.com/r/c4%3A2024-05-olas%3AM-09.md); double payouts for one lock epoch [E3](https://bugbountylibrary.com/r/c4%3A2024-02-tapioca%3AM-03.md); early exercise draining same-epoch rewards [E4](https://bugbountylibrary.com/r/c4%3A2024-02-tapioca%3AM-14.md). Severity rises when the drift is permanent or when rewards are paid from a shared pool.

## Evidence
- E1: https://bugbountylibrary.com/r/sherlock%3A2023-11-convergence%3AM-4.md
- E2: https://bugbountylibrary.com/r/c4%3A2024-05-olas%3AM-09.md
- E3: https://bugbountylibrary.com/r/c4%3A2024-02-tapioca%3AM-03.md
- E4: https://bugbountylibrary.com/r/c4%3A2024-02-tapioca%3AM-14.md
