A Confidence Pool lets stakers back the claim that the in-scope contracts survive the agreement term. If the contracts are corrupted during the term, the pool is supposed to sweep its entire balance (stake + bonus) to the recovery address and the stakers forfeit; if the contracts survive, stakers get their principal back plus a share of the bonus. claimExpired() is the permissionless resolver: once block.timestamp >= expiry it reads the live registry state and latches finality with claimsStarted = true.
When the registry is still in an active-risk state (UNDER_ATTACK / PROMOTION_REQUESTED) at the first post-expiry claimExpired() call and only latches CORRUPTED shortly after expiry, the call falls into the else branch, resolves the pool to EXPIRED, and latches finality. From that point the moderator can never flag CORRUPTED — it is barred by OutcomeAlreadySet — and could not have flagged it earlier either, because flagOutcome(CORRUPTED) requires the registry to already read CORRUPTED, which by assumption only happens after expiry. The grace-period revert that protects a pre-expiry corruption never fires here, because it is gated on the registry already reading CORRUPTED. A permissionless caller therefore forecloses a genuine in-scope breach in a window where the moderator is structurally unable to act.
Likelihood:
Occurs whenever the in-scope contracts are breached around the term boundary and the on-chain CORRUPTEDtransition latches shortly after expiry, while claimExpired() is already callable — the registry is still UNDER_ATTACK / PROMOTION_REQUESTED at the first post-expiry call, so the grace revert cannot fire and the call resolves EXPIRED.
Occurs whenever a remaining staker (or a staker-colluding attacker) calls claimExpired() in that window to lock EXPIRED before the registry reads CORRUPTED; they are directly incentivised to do so because winning the race returns their own principal plus the bonus they would otherwise forfeit, and a colluding attacker can deliberately arrange the on-chain CORRUPTED confirmation to land just after expiry.
Impact:
The recovery address (and bonus sponsor) receives nothing on a confirmed in-scope breach; the full pool — every staker's principal plus the bonus — is refunded to the stakers who bet on survival and lost.
Defeats the core economic guarantee of the mechanism: stakers are supposed to lose when the in-scope contracts are corrupted, and here they are paid in full instead.
Place in the project's test/ directory and run:
Result:
The corrupted pool paid the stakers in full and swept nothing to the recovery address.
Extend the grace-period protection that already exists for a pre-expiry CORRUPTED so it also covers a breach confirmed shortly after expiry. When the registry is still in an active-risk state at the first post-expiry call and a risk window was observed, defer resolution for a bounded moderator window instead of immediately latching EXPIRED — mirroring AgreementCorruptedAwaitingModerator. Adapt the state list and revert name to the actual source.
This preserves the intended "survived the term → EXPIRED" payout for pools that reach expiry in normal attackable operation, while closing the window in which a permissionless caller can foreclose a real, imminent in-scope CORRUPTEDoutcome the moderator has had no opportunity to flag.
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.