Normal behavior: flagOutcome gives the moderator a pre-claim correction window — the outcome can be re-flagged any time before the first claim (claimsStarted == false), so a mistaken initial flag can be fixed before funds move to the wrong party.
The issue: sweepUnclaimedBonus() is permissionless and callable the moment outcome is SURVIVED, and it deliberately never sets claimsStarted. When riskWindowStart == 0 — reachable via AttackRegistry.goToProduction(), a normal, unrestricted path that skips the attack phase entirely — the function treats the whole bonus as unowed and sweeps 100% of it to recoveryAddress immediately. If the moderator then corrects the outcome to good-faith CORRUPTED and names a whitehat attacker, bountyEntitlement is recomputed from snapshotTotalBonus, which is now 0 because the bonus already left the contract. The attacker's bounty is silently short-changed by the swept amount with no revert and no recovery path.
Likelihood:
The moderator flags SURVIVED while riskWindowStart == 0, which occurs whenever the sponsor calls the permissionless, precondition-free AttackRegistry.goToProduction() — a documented normal usage path, not a rare edge case.
Any address calls sweepUnclaimedBonus() in the same window before the moderator corrects a wrong flag, since nothing gates timing between flagOutcome and sweepUnclaimedBonus, and the call requires no special permission or coordination.
Impact:
A legitimate whitehat attacker's bounty is silently reduced by the full bonus amount, with the shortfall permanently redirected to recoveryAddress instead of the party who found the real corruption.
No revert or event signals the shortfall — the loss is only detectable by comparing expected vs. actual payout after the fact, with no on-chain path to recover the difference.
Save the following as a single file, e.g. test/poc/SweepBeforeCorrection.t.sol — it is fully self-contained (mocks included in the same file, no other test files required) and only depends on the repo's own src/ contracts and its existing lib/ dependencies (OpenZeppelin + the battlechain-safe-harbor-contracts submodule).
How to run:
Result: passes. Trace confirms 10e18 swept to recovery before correction, and the attacker receives exactly 100e18 (principal only) instead of the full 110e18 (principal + bonus) entitlement.
Note on inlined struct import path: Account, Chain, BountyTerms, AgreementDetails, and Contact are declared in lib/battlechain-safe-harbor-contracts/src/types/AgreementTypes.sol. If your project's remapping for @battlechain/ differs, adjust that single import line to match your remappings.txt.
This closes the gap without reintroducing the griefing vector the original design avoided: claimsStarted is only ever set by genuine value-movement claims (claimSurvived, claimExpired, claimCorrupted, claimAttackerBounty), so a 1-wei donation still cannot trigger it. The tradeoff is that sweepUnclaimedBonus now requires at least one legitimate claim to have already occurred — which matches the intended sequence anyway, since bonus sweeping is meant to run after stakers have had the chance to claim against a finalized outcome, not before the correction window has even closed.
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.