Normal behavior: riskWindowStart is documented as a one-way latch — once any
pool interaction observes an active-risk registry state (UNDER_ATTACK /
PROMOTION_REQUESTED), withdrawals are meant to be permanently disabled, even
if the registry later rewinds to a withdraw-allowed state.
The issue: when withdraw() itself is the first call to observe an
active-risk state, _observePoolState() sets riskWindowStart as a side
effect, and withdraw() then immediately reverts because riskWindowStart != 0.
Since a revert unwinds every storage write made earlier in the same
transaction, the SSTORE that latched riskWindowStart is rolled back along
with the revert. The latch is never actually persisted. If the registry
subsequently rewinds to a withdraw-allowed state, a staker can call
withdraw() again and succeed, even though the pool previously observed
active risk.
The latch's entire purpose is to survive a registry rewind. Because the write
and the revert happen in the same call, the one case where the latch is
supposed to matter most — a staker trying to withdraw right as risk
materializes — is exactly the case where it fails to persist.
Likelihood:
Triggers whenever a staker calls withdraw() at the moment the registry has
already moved to UNDER_ATTACK or PROMOTION_REQUESTED but no earlier pool
interaction (stake, poke, etc.) latched the state first — plausible any time
a staker reacts to a live attack by trying to exit rather than by staking.
Requires no privileged role or special timing beyond a registry rewind,
which the design doc itself treats as a real event the latch exists to
guard against (§9, §11).
Compounds with pokeRiskWindow() being separately callable — but if no one
pokes before the affected withdraw() call, the window for the bypass stays
open until someone does.
Impact:
Defeats the one-way latch's stated purpose (§9): "an upstream registry
rewind cannot re-open" withdrawals — here it can.
Lets a staker exit with full principal after risk has genuinely
materialized, which is the exact scenario withdraw()'s lock from
UNDER_ATTACK onward (§3, §9) is designed to prevent.
Undermines the accounting other stakers and the moderator rely on: a
withdrawal that should have been permanently foreclosed instead succeeds on
a later call, changing totalEligibleStake after risk was observed.
This sequence shows a staker's own withdraw() call losing the very latch
that should have blocked them. The call observes active risk, reverts on that
same observation, and the failed latch write lets a later call (after a
registry rewind) succeed instead of permanently reverting.
Alternative: keep withdraw() from mutating state itself — require the
active-risk observation to be latched by a prior, independent call (e.g.
pokeRiskWindow(), which already exists and succeeds/persists on its own),
and have withdraw() only read riskWindowStart and the live state to
decide eligibility, reverting freely since it no longer risks unwinding a
latch it just set.
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.