riskWindowStart and riskWindowEnd are independent one-way latches: the
first records an observed active-risk state, the second a observed terminal
state. withdraw()'s gate checks only riskWindowStart and the live
registry state — it never checks riskWindowEnd.
If the pool's first observation lands directly on a terminal state
(PRODUCTION/CORRUPTED) with no prior active-risk observation,
riskWindowEnd latches while riskWindowStart stays 0. If the registry then
reports/rewinds to NOT_DEPLOYED, NEW_DEPLOYMENT, or ATTACK_REQUESTED,
withdraw() passes — even though a terminal state was already recorded and
the pool should resolve via claimExpired/flagOutcome instead.
Likelihood:
Occurs whenever the pool's first observed state is already terminal —
possible whenever no one interacts with the pool during the active-risk
interval, an acknowledged scenario per §5/§6.
Permissionless: pokeRiskWindow() is the documented way this latch gets
set, so it happens through normal expected usage.
Impact:
Lets a staker withdraw principal after a terminal state (including
CORRUPTED) is already recorded, bypassing the claim paths meant to govern
post-resolution behavior.
Defeats the exact escape §9 says withdraw()'s lock prevents: exiting
after observing an attack, ahead of flagOutcome.
Skews remaining accounting for stakers who don't exit early.
Shows a terminal observation latching riskWindowEnd, then withdraw()
succeeding on a later call because it never checks that latch.
Alternative: treat riskWindowStart != 0 || riskWindowEnd != 0 as one
combined observation latch everywhere withdraw()-eligibility is checked.
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.