sweepUnclaimedBonus() can transfer tracked bonus to recoveryAddress without setting the value-movement finality latch. A later permitted correction from SURVIVED to good-faith CORRUPTED therefore snapshots the reduced totalBonus and awards the named attacker less than the pool held when the initial outcome was flagged.
Before the first claim, the moderator may re-run flagOutcome() to correct an erroneous outcome or attacker address. This correction mechanism relies on claimsStarted to prove that no distribution-changing value movement has occurred. Every successful claim path that transfers pool value sets the latch and makes a later re-flag revert.
sweepUnclaimedBonus() is the exception. Under SURVIVED or EXPIRED, it transfers the live balance above the staker reserve to recoveryAddress, updates totalBonus when the sweep removes accounted bonus, but intentionally leaves claimsStarted false. Consequently, moving the tracked bonus does not close the correction window.
This becomes exploitable when the pool did not locally observe an active-risk state before the registry reached CORRUPTED. In that case, riskWindowStart == 0, so the bonus is not reserved for surviving stakers. The moderator can initially flag SURVIVED, which is valid even on a CORRUPTED registry because the moderator may judge the breach to be outside the pool's scope. Before the moderator corrects that decision to an in-scope, good-faith CORRUPTED outcome, any account can call sweepUnclaimedBonus().
The sweep reserves only outstanding principal, transfers all bonus to recoveryAddress, and decrements totalBonus to zero. Because claimsStarted remains false, the corrective re-flag succeeds and takes a fresh snapshot from that zeroed value. bountyEntitlement becomes principal alone instead of the pre-sweep principal-plus-bonus balance. The named whitehat can never recover the diverted bonus.
The accounting remains solvent: the attacker receives the remaining principal and recovery receives the bonus. The defect is the distribution. For principal P = 100 and bonus B = 50, a direct good-faith CORRUPTED flag awards the attacker 150; inserting the permissionless sweep between flags instead awards recovery 50 and the attacker only 100.
The fee-on-transfer scenario grouped into the canonical finding is a separate token-compatibility subcase. This PoC uses the repository's standard token and demonstrates that no nonstandard transfer behavior is required.
Likelihood:
The pool must have no persisted riskWindowStart, which requires the registry to pass through active risk without a successful pool observation before reaching CORRUPTED, or the pool to have no remaining eligible stakers.
The moderator must first flag SURVIVED and later correct it to good-faith CORRUPTED before any claim. This is the narrow correction sequence the pre-claim re-flag feature intentionally supports.
Once that sequence exists, exploitation is permissionless and deterministic. A caller can insert the sweep between the moderator's separate flag transactions; the caller does not need to control the recovery address or receive the proceeds directly.
Impact:
The named good-faith attacker loses the entire tracked bonus that was swept between flags. Bonus can comprise a substantial portion of the pool, so the shortfall can be material even though principal remains payable.
Funds are irreversibly redirected to the sponsor-selected recoveryAddress. Re-snapshotting the reduced accounting prevents insolvency but also makes the corrected bounty permanently smaller.
Create test/audit/CP009BonusSweepUnderfundsBounty.t.sol with the following contents:
Run the PoC from the repository root:
Execute forge test --offline --match-path test/audit/CP009BonusSweepUnderfundsBounty.t.sol -vv.
Confirm that test_BonusSweepBeforeCorrectiveReflagUnderfundsAttackerBounty() passes and the suite reports one test passed with zero failures.
Treat a sweep of tracked bonus as a distribution-finalizing value movement, while preserving the intended ability to sweep pure direct-transfer donations without closing the moderator's correction window. Compute the accounted portion of the sweep separately and set claimsStarted only when that portion is nonzero.
With this change, the moderator must correct the outcome before tracked bonus is released. A failed token transfer reverts both the accounting update and the latch, preserving atomicity. Add regression tests showing that a tracked-bonus sweep blocks re-flagging, while sweeping only an unaccounted donation leaves re-flagging available.
If correction must remain possible after bonus becomes sweepable, introduce an explicit finalization step or bounded correction period and prohibit tracked-bonus sweeps until it closes. Merely retaining the old bonus in bountyEntitlement after transferring it out would create an unfunded claim and is not a valid fix.
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.