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.
Impact - Low The depositor loses the exit the moment they enter, and if the agreement later settles CORRUPTED their principal is swept along with everyone else's. What they read beforehand said the opposite: DESIGN.md #3 tells stakers to check the live registry state, and after a rewind that state reports pre-attack while the latch behind it is already sealed. They still earn an ordinary late-entrant bonus, so what they lose is the exit and the exposure that comes with it. Likelihood - Low Reaching the split needs the attack-registry pointer migrated while a pool is mid-risk, in the window before the fresh registry has re-registered the agreement. Nothing else is privileged, and once the live state reads pre-attack any ordinary depositor walks in through public calls. Cutting the other way, riskWindowStart is a public variable, so anyone who checks the pool instead of the registry is safe.
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.