When a pool resolves SURVIVED or EXPIRED, sweepUnclaimedBonus returns the bonus that is no longer owed to any staker back to the sponsor recoveryAddress. Separately, flagOutcome lets the trusted moderator correct a SURVIVED outcome to good-faith CORRUPTED while claimsStarted == false, paying the whitehat a bounty snapshotted from totalBonus. Each behavior is intended on its own.
The problem is that the two were never reconciled. sweepUnclaimedBonus is permissionless and its only state guard is outcome == SURVIVED || EXPIRED. A moderator-flagged SURVIVED on a CORRUPTED-registry pool is explicitly valid and does NOT latch claimsStarted, so the moderator's re-flag window is still open. In the riskWindowStart == 0 branch the sweep zeroes the LIVE totalBonus and deliberately leaves claimsStarted false. A subsequent, supported SURVIVED to good-faith CORRUPTED re-flag then re-snapshots snapshotTotalBonus from the already-drained totalBonus, so bountyEntitlement covers principal only and the whole bonus is permanently diverted to recoveryAddress.
Medium
The test drives the REAL ConfidencePool through the project's own BaseConfidencePoolTest harness (test/unit/NemesisHunt.t.sol): a pool reaches terminal CORRUPTED with riskWindowStart == 0 (nobody observes it during UNDER_ATTACK), the moderator flags SURVIVED, an attacker sweeps the bonus, and the moderator then issues the supported good-faith CORRUPTED correction. A paired control test (testDirectGoodFaithCorruptedPaysWholePool) shows the whitehat is owed the whole 150e18 pool when no sweep intervenes.
Output (forge test --match-contract NemesisHunt -vv, forge 1.7.1):
The control proves 150e18 is owed; the exploit proves the interleaved sweep leaves the whitehat with 100e18 and diverts 50e18 to the sponsor.
Do not let a permissionless sweep drain the bonus while the moderator re-flag window is still open. Gate the bonus-draining path on the outcome being final, so the value the CORRUPTED payout re-snapshots cannot be shrunk out from under it.
Alternatively, capture snapshotTotalBonus once at the first flagOutcome and have the CORRUPTED re-flag reuse that frozen value instead of re-reading a totalBonus that a later sweep can mutate (combined with reserving the bonus so the sweep cannot remove funds a re-flag would still owe).
Likelihood:
Any address can call sweepUnclaimedBonus the moment the moderator flags SURVIVED, because the function carries no access-control modifier (ConfidencePool.sol:490). Flagging SURVIVED on a CORRUPTED-registry pool is a documented, supported moderator action (ConfidencePool.sol:347).
The riskWindowStart == 0 branch is reached whenever no entrypoint observed the pool during its active-risk (UNDER_ATTACK / PROMOTION_REQUESTED) phase, since the window is sealed lazily. That is the ordinary state for a pool that takes no traffic during the risk window.
Impact:
The entire unclaimed bonus is transferred to recoveryAddress and removed from the live totalBonus (ConfidencePool.sol:516, :522). The subsequent good-faith CORRUPTED re-flag re-snapshots the bounty from the drained totalBonus (:367, :372), so the whitehat receives principal only. In the PoC the whitehat is paid 100e18 against 150e18 owed, with 50e18 of bonus permanently diverted to the sponsor.
The live totalBonus / snapshot accounting is left inconsistent: a value the CORRUPTED payout depends on is mutable by a permissionless call that runs before the payout is snapshotted.
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.