pokeRiskWindow / claimExpired seal riskWindowStart after expiry, flipping the destination of the whole bonus pool between the stakers and the sponsor-controlled recoveryAddress.
When a pool reaches expiry, its bonus is split among stakers only if a risk window was observed (riskWindowStart != 0). If none was ever observed, the bonus is instead swept to recoveryAddress — the documented "no observable risk" rule (DESIGN §5). The k=2 reward math anchors on riskWindowStart/riskWindowEnd, which are sealed the first time a pool interaction reads the registry in the matching state.
Neither pokeRiskWindow() nor claimExpired()'s _observePoolState() is gated on block.timestamp < expiry. A pool that reaches expiry with riskWindowStart == 0 can still have riskWindowStart sealed (capped to expiry) by the first post-expiry caller that catches an active-risk state. That seal flips _bonusShare from the riskWindowStart == 0 → pay-zero → sweep-to-recovery branch to the globalScore == 0 → amount-weighted → pay-stakers branch. The destination of the entire bonus pool therefore depends on who triggers settlement first, not on what actually happened — falsifying DESIGN §7's claim that the timing residual has "no third-party loss."
Likelihood:
Triggers whenever a pool reaches expiry with riskWindowStart == 0 — a developer-anticipated state (the code has a dedicated riskWindowStart == 0 path) that arises naturally in low-activity pools: staking clusters pre-attack, withdrawal attempts revert and roll back their observation during an attack, and pokeRiskWindow pays its caller nothing.
Realized whenever the registry is in (or transiently enters) an active-risk state at or after expiry and a self-interested staker calls pokeRiskWindow/claimExpired before the pool settles from a benign state — a permissionless race the staker (who gains the bonus) and the sponsor (who gains the sweep) are on opposite sides of.
Impact:
The entire bonus pool moves between two distinct parties — the staker set and the sponsor-controlled recoveryAddress — decided by settlement-trigger timing rather than actual borne risk.
Stakers who bore real in-term risk can be denied the bonus (swept to the sponsor), or the sponsor can be denied bonus they would receive under the documented no-observed-risk rule.
Both tests start from the identical state (UNDER_ATTACK at expiry, riskWindowStart == 0); only the post-expiry trigger differs, yet the whole bonus lands in different hands.
Do not open the risk window once the term is over — the "no observable risk" rule should hold after expiry:
This one-line guard also fixes the principal-sweep variant (companion finding). Sealing already-nonzero windows is unaffected.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.