src/ConfidencePool.sol — sweepUnclaimedBonus moves real value (the bonus) out of the pool but intentionally does NOT latch claimsStarted (to avoid a 1-wei donation blocking the §4 re-flag window). The moderator's re-flag window therefore stays open AFTER the bonus has physically left the pool. A re-flag that changes the outcome then computes its distribution against snapshotTotalBonus, which the sweep has already zeroed — producing a payout short by the entire bonus.
When riskWindowStart == 0 (no observed risk — e.g. a direct-to-CORRUPTED or direct-to-PRODUCTION agreement), the bonus is unreserved and fully sweepable under a SURVIVED/EXPIRED flag. Sequence: moderator flags SURVIVED -> anyone sweeps the bonus to recoveryAddress -> moderator re-flags good-faith CORRUPTED (a legitimate §4 correction) -> the named whitehat's bountyEntitlement is computed as snapshotTotalStaked + snapshotTotalBonus, but snapshotTotalBonus is now 0. The whitehat receives only principal; the entire bonus is stuck at recoveryAddress.
Impact: the whitehat — who under good-faith CORRUPTED is entitled to the WHOLE pool — is underpaid by the full bonus, which is misdirected to the sponsor's recoveryAddress. Direct loss of funds for the legitimate CORRUPTED beneficiary; the §4 correction mechanism is not safe against an interleaved sweep.
sweepUnclaimedBonus does not latch finality:
@> amount is the FULL bonus when riskWindowStart == 0, not a 1-wei donation. It leaves the pool irreversibly, yet the re-flag window stays open. The same function also does totalBonus -= amount in this case, so the live totalBonus is now 0.
re-flag recomputes the snapshot from the emptied totalBonus:
@> bountyEntitlement is missing the bonus, so claimAttackerBounty pays the whitehat principal only.
Sequence:
riskWindowStart == 0 (agreement reaches a terminal state without an observed risk window).
Moderator flags SURVIVED (breach judged out-of-scope — §8 allows SURVIVED on a CORRUPTED registry).
sweepUnclaimedBonus() (permissionless) sends the full bonus to recoveryAddress; does not latch claimsStarted.
Moderator re-flags good-faith CORRUPTED (new info: the breach was in-scope). Allowed because claimsStarted == false (§4 window open).
bountyEntitlement = snapshotTotalStaked + 0. Whitehat claims and receives principal only; bonus stuck at recoveryAddress.
Likelihood: Low — requires riskWindowStart == 0, a SURVIVED->CORRUPTED moderator correction, and a sweep interleaved between the two flags. The sweep is permissionless and a sponsor who controls recoveryAddress is incentivized to call it.
Impact: High — direct loss of funds: the whitehat is underpaid by the entire bonus, which is misdirected to the sponsor.
forge test --match-contract SweepThenReflagUnderpaysAttackerTest -vv passes against the in-scope contracts:
Any of: (a) latch claimsStarted = true in sweepUnclaimedBonus when it moves non-dust value — distinguish a real bonus sweep from a 1-wei donation-dust sweep and only skip the latch for dust — closing the re-flag window once real value has left; (b) block sweepUnclaimedBonus while the outcome is still re-flaggable (claimsStarted == false), reserving the bonus until the outcome is final; (c) on re-flag, derive the new snapshotTotalBonus from accounting that includes already-swept amounts, so a prior sweep cannot silently shrink the new outcome's entitlement.
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.