src/ConfidencePool.sol — the deposit gate and the withdraw gate are asymmetric with respect to the one-way riskWindowStart latch.
withdraw() is disabled whenever riskWindowStart != 0 (the "risk has materialized" latch), specifically so a benign upstream registry rewind cannot re-open exits (DESIGN.md §11).
stake() (_assertDepositsAllowed) gates ONLY on the live registry state — it never checks riskWindowStart. It stays OPEN in NOT_DEPLOYED / NEW_DEPLOYMENT / ATTACK_REQUESTED even after the latch has sealed.
After the registry rewinds from an active-risk state back to a pre-attack state, the pool advertises NEW_DEPLOYMENT (deposits open, apparently pre-attack) while withdraw() is already permanently disabled. A new staker who reads the live registry state — exactly what DESIGN.md §3 instructs ("A staker who does not want this can read the live registry state before staking") — deposits believing exit is open, and is immediately trapped in the resolution path with no exit.
Impact: the staker loses the exit option the instant they deposit, without risk having visibly materialized in the state they observed. If the agreement later resolves CORRUPTED in-scope, their principal is swept to recoveryAddress. The §11 anti-rewind protection is incomplete: it blocks the exit but not the entrance.
withdraw gate — enforces the latch:
deposit gate — ignores the latch:
@> _assertDepositsAllowed receives only the live state; it has no access to and never checks riskWindowStart. NEW_DEPLOYMENT passes even when the latch is already sealed.
Sequence:
Registry reaches UNDER_ATTACK; any interaction (or pokeRiskWindow) seals riskWindowStart != 0.
Registry rewinds to NEW_DEPLOYMENT (§11 acknowledges benign rewinds happen).
Bob reads the live state — NEW_DEPLOYMENT, "pre-attack, exit open" per §3 — and stakes. _assertDepositsAllowed(NEW_DEPLOYMENT) passes.
Bob calls withdraw() → reverts WithdrawsDisabled (gated on the sealed latch). Bob is trapped.
This is distinct from §3 (intentional UNDER_ATTACK deposits): there the live state Bob reads IS UNDER_ATTACK, so his self-lock is informed and consensual. Here the live state reads pre-attack, so the lock is invisible at deposit time.
Likelihood: Low — requires a registry rewind from an active-risk state back to a pre-attack state.
Impact: Medium — a staker is trapped into the resolution path against a state that advertises exit is open; principal is at risk (CORRUPTED sweep) with no exit.
forge test --match-contract StakeTrappedAfterRewindTest -vv passes against the in-scope contracts:
Make the deposit gate consistent with the withdraw latch WITHOUT breaking the intentional UNDER_ATTACK deposits of §3: reject stake() / contributeBonus() when riskWindowStart != 0 AND the live state is a pre-attack state (NOT_DEPLOYED / NEW_DEPLOYMENT / ATTACK_REQUESTED) — i.e. a detected rewind whose advertised state no longer matches the sealed latch. Deposits during a genuine UNDER_ATTACK (where the live state honestly reflects the risk) stay allowed. Concretely, pass riskWindowStart into _assertDepositsAllowed and add: if the latch is sealed but the live state reads pre-attack, revert StakingClosed.
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.