sweepUnclaimedBonus is the only value-moving path that does not set the claimsStarted finality latch, and in the riskWindowStart == 0 case it moves the entire bonus (not just dust). This will cause a total loss of the bonus portion of the bounty for the named good-faith attacker (whitehat), as any caller will sweep the bonus to recoveryAddress in the still-open window between a moderator's initial flagOutcome(SURVIVED) and their documented flagOutcome(CORRUPTED, true, attacker) correction, after which the re-flag re-snapshots a now-zero totalBonus, so bountyEntitlement collapses to principal-only and the whitehat can never recover the bonus.
the choice to exempt sweepUnclaimedBonus from the claimsStarted latch is a mistake, because it is the only path where real funds leave the pool while the moderator's re-flag window stays open — and when riskWindowStart == 0 the funds that leave are the whole bonus, not the donations/dust the exemption was designed for.
In sweepUnclaimedBonus, with riskWindowStart == 0 the bonus is unreserved, swept out, and totalBonus is zeroed, but claimsStarted is deliberately left unset:
The re-flag guard still permits the correction because claimsStarted is false:
and the re-flag re-snapshots the now-zeroed totalBonus, so the whole-pool bounty shrinks to principal only:
The enabling precondition (riskWindowStart == 0 while the registry is CORRUPTED) is itself a documented, reachable state (DESIGN.md), and flagOutcome(SURVIVED) on a CORRUPTED registry is an intended, tested action (test/unit/ConfidencePool.scope.t.sol:177).
The registry reaches CORRUPTED (e.g. …→UNDER_ATTACK→markCorrupted) while no pool interaction observes the active-risk interval, so riskWindowStart is never sealed (riskWindowStart == 0). Pool holds principal P + bonus B.
Moderator calls flagOutcome(SURVIVED, false, 0), valid, since SURVIVED accepts a CORRUPTED registry (an initial "breach was out-of-scope" judgement). claimsStarted stays false; snapshotTotalBonus = B.
Any address (e.g. the sponsor, who controls recoveryAddress and can front-run) calls sweepUnclaimedBonus(). Because riskWindowStart == 0, only principal is reserved, so the full B is transferred to recoveryAddress and totalBonus is zeroed. claimsStarted remains false.
Moderator corrects the scope judgement (the flow DESIGN.md §4 exists for) and calls flagOutcome(CORRUPTED, true, attacker). The guard passes; re-snapshot captures totalBonus == 0, so bountyEntitlement = P.
Whitehat (attacker) calls claimAttackerBounty() and receives only P. B is permanently stranded at recoveryAddress.
Impact:
Impact 1
The named good-faith attacker (whitehat) suffers an approximate loss of B — the entire bonus pool (100% of the bonus portion of their intended P + B bounty). The sponsor's recoveryAddress gains B, a party designed to receive nothing on a good-faith CORRUPTED outcome. The loss is permanent and unrecoverable.
Drop into test/unit/PoCAudit.t.sol, extending the repo's BaseConfidencePoolTest:
(Recommended) In sweepUnclaimedBonus, set claimsStarted = true when it actually sweeps bonus (the riskWindowStart == 0 || totalEligibleStake == 0 branch). This closes the re-flag window precisely when real value has left, without regressing the 1-wei-donation griefing concern the exemption was added for (a donation does not enter that branch with riskWindowStart != 0 and stakers present).
Reserve snapshotTotalBonus - claimedBonus unconditionally (drop the riskWindowStart != 0 guard at line 485) while a CORRUPTED re-flag is still reachable, so the bonus can't be swept before finality.
Block a good-faith CORRUPTED re-flag once any bonus has been swept.
Impact – Medium Up to the entire bonus pool can be routed to the wrong party for good, with no on-chain way to unwind it, and in a stakerless pool the effect is total since bountyEntitlement computes to zero and claimAttackerBounty reverts outright. This impact is definitely not High: the pool stays solvent throughout with no principal ever at risk, and the money ends up at the sponsor's own recoveryAddress, which in most pools means a sponsor recovering a bonus they funded themselves. The whitehat's claim on that bonus also only exists because the moderator changed their mind after the fact. Likelihood – Low Four separate things have to coincide: nobody touches the pool for the whole active-risk window (or there are no stakers to begin with), the moderator flags SURVIVED and later reverses to CORRUPTED, that reversal is good-faith with a named attacker, and a sweep lands between the two flags. The first cuts against staker self-interest, since skipping the poke costs them their entire bonus share, and the second asks the moderator to overturn a scope judgement, which is a bigger deal than the typo fix DESIGN.md #4 offers the window for. The sweep itself I'd treat as near-certain once the rest holds, given it's permissionless and the sponsor has an obvious reason to make the call, but assembling the first three in one pool lifecycle is where this stays rare.
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.