Normally, sweepUnclaimedBonus() is a permissionless hygiene function: once a pool resolves SURVIVED or EXPIRED, anyone can sweep bonus that no staker is owed (because no risk window was ever observed, or because no stakers remain) to recoveryAddress. Separately, flagOutcome() may be re-flagged by the moderator any number of times before the first claim, so they can correct a mistaken outcome or attacker before any participant locks in the wrong distribution (docs/DESIGN.md §4). For good-faith CORRUPTED, bountyEntitlement is meant to equal the entire pool — snapshotTotalStaked + snapshotTotalBonus — so the named whitehat can claim it all via claimAttackerBounty() (docs/DESIGN.md §12).
The specific issue is that sweepUnclaimedBonus() deliberately never sets claimsStarted, so it remains callable inside the moderator's still-open pre-claim re-flag window. If the moderator's first flag is SURVIVED while riskWindowStart == 0 (an accepted, documented possibility — docs/DESIGN.md §5/§6), anyone can sweep the entire live totalBonus to recoveryAddress before the moderator corrects the flag to good-faith CORRUPTED with a named attacker. The correction still succeeds, but bountyEntitlement is recomputed from the now-drained live totalBonus, not from any value captured at the first flag — so the named attacker permanently receives less than "the entire pool" the design guarantees them, and recoveryAddress (sponsor-controlled, docs/DESIGN.md §10) keeps the difference.
Likelihood:
This occurs whenever the registry transitions from pre-attack staging directly to a terminal state without any pool interaction observing it in an active-risk state, which leaves riskWindowStart == 0 — a scenario docs/DESIGN.md §5/§6 explicitly documents as reachable and accepted, not a purely theoretical edge case.
This occurs whenever the moderator exercises the documented re-flag mechanism (docs/DESIGN.md §4) to correct an interim outcome, because sweepUnclaimedBonus() is permissionless and the sponsor — who directly controls recoveryAddress — has a standing, zero-cost financial incentive to call it in the gap between the moderator's interim flag and their correction.
Impact:
The named good-faith attacker permanently receives less than the "entire pool" bounty the design guarantees them, shorted by exactly the diverted bonus amount, with no revert or on-chain signal marking the shortfall — it is only visible by comparing bountyEntitlement against snapshotTotalStaked + BONUS off-chain.
recoveryAddress (sponsor-controlled) permanently retains value the design intends for the whitehat attacker, at zero additional cost to the sponsor beyond one permissionless transaction.
Two Foundry tests: the exploit, and a control proving the whitehat receives the full pool (STAKE + BONUS) when the intervening sweep does not occur — isolating sweepUnclaimedBonus() as the sole cause of the shortfall. The real BattleChain Safe Harbor interfaces live in an out-of-scope git submodule not included in the contest download; this PoC mocks them with matching call signatures, consistent with docs/DESIGN.md §11's own framing of the registry as a trusted, out-of-adversarial-model dependency.
Test output:
Close the re-flag window whenever sweepUnclaimedBonus() sweeps real, snapshot-tied bonus value (the riskWindowStart == 0 / totalEligibleStake == 0 branch) — the same branch that already decrements the live totalBonus. Leave claimsStarted untouched for the pure donation/dust-excess case, preserving the original 1-wei-donation protection.
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.