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.
View preliminary resultsAppeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.