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.
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.