Normal behavior. A good-faith CORRUPTED resolution entitles the named whitehat to the whole pool — staked principal plus the entire bonus. flagOutcome sets bountyEntitlement = snapshotTotalStaked + snapshotTotalBonus, which docs/DESIGN.md §12 states as the protocol's own rule: "the entire pool is the named attacker's bounty."
Issue. sweepUnclaimedBonus moves accounted bonus out of the pool but never sets claimsStarted, so it stays callable during the moderator's pre-claim re-flag window. When a pool is resolved SURVIVED with no observed risk window (riskWindowStart == 0), the bonus is unreserved, sent to recoveryAddress, and removed from totalBonus. A later good-faith CORRUPTED re-flag re-snapshots the now-reduced totalBonus, so the whitehat bounty excludes the swept bonus. The bonus is permanently redirected from the whitehat to the sponsor.
The code's own comment at L.503-505 states the invariant this breaks: "Genuine reliance only comes from claim entrypoints." A sweep is not a claim entrypoint, yet it irreversibly transfers real accounted bonus to a third party — creating exactly the reliance the comment assumes cannot arise here.
The sequence is not an unconsidered edge case, and it is not a documented one either:
The code anticipates it. L.496-497 drops the swept amount from totalBonus "keeping the accounting honest for any later re-snapshot." The author explicitly contemplated a re-flag after a sweep — and verified the arithmetic, but not the whitehat's entitlement.
sweepUnclaimedBonus appears nowhere in docs/DESIGN.md. The document devotes whole sections to justifying accepted trade-offs (§4 re-flag finality, §5 no-risk-window race, §6 scope-blind backstop). No such rationale exists for this path.
§5's bonus rule does not survive a CORRUPTED outcome. §5 says that with riskWindowStart == 0 the bonus sweeps to recoveryAddress — but that describes a final SURVIVED/EXPIRED resolution. Under CORRUPTED, §5 states the moderator sweeps "the pool whole" and §12 assigns the entire pool to the whitehat. Path A of the PoC proves this on-chain: with the identical riskWindowStart == 0, a direct CORRUPTED flag yields bountyEntitlement == 150, bonus included.
The re-flag is the window's documented purpose. §4 provides it precisely so the moderator can correct an outcome/attacker before value is locked in.
Likelihood:
The registry reaches CORRUPTED while the pool never observed an active-risk state, so riskWindowStart == 0 (the protocol's documented "no observed risk" case).
The moderator resolves SURVIVED first (breach judged out of scope) and later corrects to good-faith CORRUPTED through the re-flag window §4 provides for this exact purpose.
Any address calls sweepUnclaimedBonus() between the two flags — the function is permissionless.
Impact:
The good-faith whitehat receives principal only; 100% of the bonus is redirected to the sponsor's recoveryAddress (asserted in the PoC).
The loss equals the entire bonus, so it scales with how bonus-funded the pool is: in a bonus-dominant pool the whitehat loses nearly the whole intended bounty.
Two identical pools. The only difference: pool B has one permissionless sweepUnclaimedBonus() inserted during the re-flag window. Same stake (100), same bonus (50), same CORRUPTED registry, same named whitehat.
Place in test/unit/ and run:
forge test --match-test test_MAIN_sweepDivertsWhitehatBounty -vv
Output:
Boundary check (proves the exact precondition): with an observed risk window the bonus is reserved and sweepUnclaimedBonus() reverts NothingToSweep, so the issue does not apply.
Make a bonus sweep a finality event, but only when it actually removes accounted bonus — this preserves the exact property L.503-505 protects, since a dust/donation sweep (accountedSwept == 0) still cannot latch the flag or grief the re-flag window:
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.