sweepUnclaimedBonus can move the entire bonus pot out of the pool while the moderator's outcome-correction window is still open. When a pool reaches a terminal CORRUPTED registry state without ever having observed an active-risk window (riskWindowStart == 0), the bonus is treated as owed to no staker and is fully sweepable to recoveryAddress by any unprivileged caller. Because this sweep deliberately does not set the claimsStarted finality latch, the moderator can subsequently correct the outcome to good-faith CORRUPTED — but the corrected snapshot captures a bonus pot of zero. The named attacker's bounty is silently reduced by the swept amount, which becomes permanently stranded at recoveryAddress with no code path to recover it into the bounty.
Two behaviours that are each individually correct combine to produce the flaw:
The correction window must stay open until genuine reliance. flagOutcome permits re-flagging until the first claim sets claimsStarted (ConfidencePool.sol:327). This is a deliberate typo-correction affordance — e.g. to fix a misjudged scope or a mistyped attacker address before anyone acts on the verdict.
sweepUnclaimedBonus intentionally does not latch finality. It omits claimsStarted = true (ConfidencePool.sol:503-505) so that a 1-wei donation cannot be used to grief the moderator's correction window shut. Sweeping excess to recoveryAddress is not considered staker reliance on the verdict.
The gap appears when value is moved between two snapshots. In the riskWindowStart == 0 branch, sweepUnclaimedBonus reserves only principal and treats the whole bonus as unowed (ConfidencePool.sol:482-501), sweeping it out and zeroing totalBonus — all without closing the correction window. A later good-faith CORRUPTED re-flag re-snapshots totalBonus (now 0) and sets bountyEntitlement = snapshotTotalStaked + snapshotTotalBonus = P + 0 (ConfidencePool.sol:362). The bonus that should have been part of the attacker's bounty is gone.
This is a localized violation of the contract's own "snapshot, then distribute" invariant: value distributed between two snapshots leaks out of the second one.
Attack sequence (P = 100, B = 50)
Alice stakes P = 100; Carol contributes bonus B = 50. Pool balance = 150, riskWindowStart == 0.
Registry reaches terminal CORRUPTED with no active-risk state ever observed by this pool.
Moderator flags SURVIVED (an out-of-scope-breach judgement). Snapshot: snapshotTotalBonus = 50, claimsStarted == false.
Any unprivileged caller invokes sweepUnclaimedBonus(). Because riskWindowStart == 0, all 50 is swept to recoveryAddress; totalBonus → 0; claimsStarted stays false.
New evidence: the breach was in scope. Moderator correctly re-flags CORRUPTED, good-faith, attacker = A. Re-snapshot captures totalBonus = 0, so bountyEntitlement = 100.
Attacker A claims and receives 100 only. The 50 is permanently stranded at recoveryAddress.
Likelihood:
Low. No unprivileged party profits — the swept funds land at recoveryAddress, a legitimate destination, so the trigger is unrewarded. Concrete harm materialises only if the moderator later reverses their own earlier judgement (SURVIVED → good-faith CORRUPTED), a two-step correction sequence. The permissionless sweep can, however, be executed by anyone the instant the SURVIVED flag lands, so an adversary aware of a pending correction could deliberately foreclose it. It requires no special privilege — only timing.
Impact:
Medium. The named good-faith attacker (a whitehat who performed a legitimate in-scope breach) is underpaid their bounty by the full bonus amount. The shortfall is permanently stranded at recoveryAddress with no on-chain path to redirect it to the attacker. This undermines the credibility of the bounty guarantee that the confidence-pool product depends on.
Bounded in scope:
Only the bonus pot is affected — staker principal is never at risk.
Reachable only when riskWindowStart == 0 (no active-risk window ever observed) and the first resolution was a moderator SURVIVED/CORRUPTED flag (not a mechanical EXPIRED, which reserves differently).
Copy the test into test/audit/economic_reflag_sweep.t.sol
Run forge test --match-path test/audit/economic_reflag_sweep.t.sol -vv
Reserve the tracked bonus whenever the outcome is still legally re-flaggable, not only when a risk window was observed. This keeps the bonus fenced off until the correction window has genuinely closed, so a later good-faith correction can still fund the attacker in full:
This preserves both existing design goals:
The 1-wei griefing defense is retained — sweepUnclaimedBonus still does not set claimsStarted.
Genuine excess (dust, post-resolution donations) remains sweepable, since reserved never exceeds legitimate liabilities.
After the correction window closes (claimsStarted == true) and if still no risk window applies, the now-unowed bonus becomes sweepable as before.
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.