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