Affected: ConfidencePool.sol:222-227 · ConfidencePool.sol:727-733 · ConfidencePool.sol:784-799
stake() and contributeBonus() observe registry state before enforcing their deposit gate. _observePoolState() writes riskWindowStart and scopeLocked as one-way transitions, then _assertDepositsAllowed() reverts on PROMOTION_REQUESTED, PRODUCTION, or CORRUPTED. EVM atomicity rolls the writes back with the revert, so the pool never persists the first observation.
A permissionless caller (Bob) can therefore attempt stake(1 wei) during PROMOTION_REQUESTED and knowingly force the revert; the latches that _observePoolState() would have sealed are lost. Combined with a later legitimate DAO setAttackRegistry migration that reports NOT_DEPLOYED for the agreement, the withdraw() gate re-opens for previously staked capital — even though active risk was already publicly observable on-chain.
Same root cause as M-3, distinct entrypoint: here the failed sealing is triggered by an unprivileged Bob rather than by the observation ordering of a low-traffic pool.
Likelihood: Requires two events — permissionless reverting deposit (or bonus) call during active risk, plus a later routine DAO registry migration (DESIGN.md §11 treats migration as expected).
Impact: Existing staker exits principal after active-risk was on-chain. Bounded per-staker; aggregate up to totalEligibleStake. No attacker capture — this is a leak vs. the intended commitment invariant, not a direct value transfer to the attacker.
Actor: Bob (revert trigger) is fully permissionless. The migration is the DAO's documented onlyOwner operation.
When a deposit is blocked, persist the transition instead of reverting the whole transaction. A seal-only-return pattern preserves the one-way observation without accepting the deposit:
Apply the same rule to contributeBonus(). Alternatively, harden the withdraw gate as recommended in M-3 (|| riskWindowEnd != 0) plus store scopeLocked durably via a dedicated sealObservation() no-op path.
Save as test/ObserverRevertRollback.t.sol and run forge test --match-contract ObserverRevertRollbackTest -vv.
Impact - Low The staker keeps their principal, so what is lost is the premium they were accruing while locked, and it ends up at the sponsor's recoveryAddress. DESIGN.md #9 justifies the whole no-observed-risk rule by pairing two events, saying a staker forfeits the exit exactly when the premium starts. One call does the first and undoes the second. Likelihood - Low Any staker can cure this for free with pokeRiskWindow, which succeeds in the same state the withdrawal failed in. A permanent loss therefore needs the whole cohort to miss that step, and the trap is that the obvious move is the one that fails. A short active-risk interval can also close before anyone gets a block in which to poke. DESIGN.md #6 explains an unsealed window as nobody having interacted with the pool, which is exactly what did not happen here.
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.