After a pool resolves SURVIVED or EXPIRED, sweepUnclaimedBonus() lets anyone recover bonus tokens nobody is owed — when riskWindowStart == 0, no staker earned any bonus, so the entire totalBonus is fair game to sweep to recoveryAddress. This function deliberately does not set claimsStarted, specifically so a 1-wei donation can't be used to grief-close the moderator's correction window.
That same omission means sweepUnclaimedBonus() can move the entire live bonus pool — not just dust — while flagOutcome's re-flag window is still open, since that window's only gate is claimsStarted. A moderator's provisional SURVIVED flag (legal even when the registry reads CORRUPTED, as an out-of-scope judgment call) can be followed by a full bonus sweep, and then corrected to good-faith CORRUPTED — but the correction recomputes bountyEntitlement from the now-drained live totalBonus, permanently shorting the whitehat named in the correction.
Likelihood:
A pool's risk window stays unobserved (riskWindowStart == 0) whenever the registry transitions to a terminal state without any pool interaction happening during UNDER_ATTACK/PROMOTION_REQUESTED — a normal outcome for a pool nobody happens to poke during that window, not a contrived edge case, and one the codebase's own design doc already acknowledges as reachable.
A moderator facing a CORRUPTED registry read makes a provisional out-of-scope SURVIVED judgment, a permissionless caller sweeps the bonus in the same block or the next one, and the moderator then corrects to good-faith CORRUPTED once the in-scope determination is confirmed — this sequence follows the exact "fix a mistaken flag before anyone claims" workflow flagOutcome's re-flag window is built for, so the drain rides along an intended, everyday correction path rather than requiring an unusual sequence.
Impact:
The named good-faith whitehat's bounty is permanently short by the entire swept bonus amount — not dust, the full totalBonus at sweep time — with the difference stuck at recoveryAddress instead of reaching the attacker docs/DESIGN.md says is owed "the entire pool."
The shortfall is irreversible once claimAttackerBounty() pays out against the reduced bountyEntitlement; there is no later mechanism to top up the attacker's payment from the swept funds.
Run: forge test --match-path "SweepBonusReflagDrain.t.sol" -vvvv → PASS (gas: 591171)
Setting claimsStarted = true inside sweepUnclaimedBonus closes the re-flag window the moment value actually leaves the pool, mirroring every other payout/sweep function in the contract (claimSurvived, claimCorrupted, claimAttackerBounty, sweepUnclaimedCorrupted all already do this) — sweepUnclaimedBonus was the one exception.
Trade-off to weigh: the existing code comment explains this was deliberately omitted so a griefer couldn't donate 1 wei and immediately lock the moderator's correction window. This fix reintroduces that exact griefing vector — anyone can now force-close the re-flag window early with a trivial donation-plus-sweep. A more targeted fix would gate the lock on the swept amount being non-dust (e.g. only when totalEligibleStake != 0 or amount exceeds a minimum), preserving the original anti-griefing intent while still closing the drain for any economically meaningful sweep.
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.