flagOutcome() deliberately allows a moderator to re-flag a pool's outcome before any claim has occurred (if (outcome != UNRESOLVED && claimsStarted) revert OutcomeAlreadySet();, line 327), so a mistaken or premature resolution can be corrected. The problem is that claimsStarted is latched to true by any of 7 separate call sites across ConfidencePool.sol, and none of them are synchronized with a pending flagOutcome correction. We demonstrate three concrete instantiations of this same root cause, in increasing order of how little precondition they require:
Grace-boundary race (no moderator mistake required at all): at exactly expiry + MODERATOR_CORRUPTED_GRACE (180 days after expiry), the mechanical, scope-blind auto-resolution inside claimExpired() can be triggered by any address the instant the grace period ends. If the moderator is in the middle of submitting a fully correct flagOutcome(SURVIVED, ...) — because the underlying breach was out of the pool's scope — and loses the race, their correction reverts, the pool is permanently force-resolved to bad-faith CORRUPTED, and the entire pool (100% of staked + bonus capital) is swept to recoveryAddress via claimCorrupted(), with zero repayment to any staker who should have received their principal and bonus back in full.
Outcome-type instantiation: a normal staker front-runs the moderator's correction to claimSurvived(), permanently locking the pool into SURVIVED even after real, in-scope corruption is discovered — the entire attacker-bounty / recovery mechanism becomes unreachable.
Attacker-address instantiation: a wrongly-named attacker front-runs the moderator's correction to claimAttackerBounty(), permanently keeping a CORRUPTED bounty they were never supposed to receive.
Moderators should always be able to correct an incorrect or premature pool resolution until payout finality begins. Claims should only finalize the outcome after this correction window has safely ended.
Issue
Any claim path can independently set claimsStarted, permanently preventing further moderator corrections. Since claim execution is not synchronized with flagOutcome(), a competing transaction can finalize an outdated outcome before the moderator's correction is processed, resulting in irreversible fund distribution under the wrong resolution.
The developers' own comment acknowledges the exact class of problem this report describes — they explicitly reasoned about "the moderator can't override mechanical bad-faith CORRUPTED" and added the claimsStarted = true lock specifically to enforce it. What the comment does not account for is the reverse direction: the moderator may be trying to correct the pool away from mechanical CORRUPTED (because the breach was out of scope, or otherwise mistaken), and claimExpired()'s auto-resolution — callable by anyone, requiring no stake, no special role, and no mistake by the moderator — can win that race and permanently foreclose the correction just as easily in this direction. The same unsynchronized-latch pattern additionally makes claimSurvived, claimAttackerBounty, claimCorrupted, and sweepUnclaimedCorrupted (7 call sites in total) able to close the window prematurely.
The protocol explicitly allows moderator re-flagging before the first claim so that an incorrect or premature outcome/attacker determination can be safely corrected. Therefore, before payout finality, the moderator should always be able to replace an incorrect determination with the correct one — including overriding the mechanical, scope-blind auto-CORRUPTED resolution when the true resolution should be SURVIVED.
Violation: any address able to trigger a claim/auto-resolution path under the currently observable state — a normal staker, the currently-named attacker, or literally anyone once expiry + MODERATOR_CORRUPTED_GRACE has elapsed — can permanently finalize that state before the moderator's pending, and potentially entirely correct, flagOutcome call can take effect.
The issue is not that transactions can be reordered — that is expected on any EVM chain. The issue is that the protocol explicitly exposes a correction mechanism intended to recover from exactly this class of mistaken/premature resolution, yet several independent functions can permanently invalidate that correction before it takes effect — including, in the grace-boundary instantiation, a function (claimExpired) that requires no wrongdoing, no named party, and no stake at all to call. Transaction ordering merely exposes the missing synchronization between flagOutcome's re-flag path (which assumes it has until the first claim/auto-resolution to correct itself) and the fact that nothing in the contract actually reserves that window for the moderator.
Likelihood:
• The protocol explicitly supports moderator re-flags, meaning this correction flow is an intended and reachable part of normal operation.
• The grace-boundary is deterministic (expiry + MODERATOR_CORRUPTED_GRACE) and publicly observable, allowing any external account to race the moderator using only normal protocol interactions.
• No privileged access, malicious registry behavior, or protocol assumption violation is required; the exploit relies solely on transaction ordering during the intended correction window.
Impact:
• The protocol's correction guarantee becomes unenforceable in its most consequential case. The grace-boundary race requires no mistake by the moderator whatsoever — only that their entirely correct
flagOutcome(SURVIVED, ...)transaction does not land before someone else'sclaimExpired()call at the exact moment the 180-day grace period elapses. Losing that race permanently and irreversibly force-resolves the pool to bad-faith CORRUPTED.
100% of the pool's staked + bonus capital is misdirected. In the bad-faith auto-CORRUPTED branch,
claimCorrupted()sweeps the entire pool balance torecoveryAddress— not a bounty, not a partial amount, the whole pool. In the PoC this is 190 tokens (150 staked + 40 bonus); in production it scales with whatever a pool's total staked + bonus capital is at expiry.Every staker in the pool is affected, not just one. Unlike the attacker-address instantiation (which misdirects a bounty), the grace-boundary and outcome-type instantiations affect the entire pool's principal and bonus distribution — every staker who would have received their stake back under a correct SURVIVED resolution instead receives nothing.
No on-chain recovery exists once
claimsStartedis set in any instantiation. Neither the moderator nor the protocol has any path to correct the outcome orattackerdesignation, or to recover funds already swept/paid under the superseded determination.
The PoC uses only normal protocol interactions (stake, contributeBonus, flagOutcome, claimExpired, and claimCorrupted) without modifying any protocol assumptions or external dependencies. No mock behavior is altered beyond what the real AttackRegistry and SafeHarborRegistry would produce for a pool resolving to CORRUPTED. The demonstrated race relies solely on ordinary transaction ordering around the publicly known expiry and MODERATOR_CORRUPTED_GRACE values, making the scenario reproducible without requiring privileged access or any moderator mistake.
[Confirmed] — all four tests pass: the grace-boundary race, the outcome-type exploit, the attacker-address exploit, and the isolating control test. The control test demonstrates that the moderator's correction succeeds when no competing transaction races it, confirming that the exploit is caused specifically by the unsynchronized claimsStarted latch rather than any unrelated restriction in the re-flag path.
The protocol should preserve the intended moderator correction window. Claims should not permanently finalize pool state while moderator corrections are still permitted. One possible approach is introducing a short correction phase or decoupling moderator correction rights from claimsStarted.
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.