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