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