claimsStarted is the value-movement finality latch. the moderator may re-flag a pool's outcome up until the first participant acts on it, so the outcome and named attacker stay adjustable across the entire pre-claim window (flagOutcome re-flag gate, ConfidencePool.sol#L322-L327):
every value-moving exit latches claimsStarted the moment value leaves, which freezes the outcome, because a corrective re-flag re-snapshots the accounting inputs and would otherwise hand out funds the pool no longer holds. the docs state this invariant directly (DESIGN.md#L75-L90): "Once value has left the contract, a corrective re-flag cannot be honored without breaking balance accounting", and "claimsStarted is a value-movement finality latch".
sweepUnclaimedBonus is the one value-moving exit that deliberately does not latch (ConfidencePool.sol#L474-L509). its comment justifies that by asserting only dust and donations ever leave through it: "Genuine reliance only comes from claim entrypoints". that assertion breaks in the riskWindowStart == 0 mode, which the same function handles two lines earlier. with riskWindowStart == 0, _bonusShare pays every staker zero, so the reserve withholds nothing for the bonus:
the entire genuine bonus B moves to recoveryAddress and live totalBonus is zeroed, yet claimsStarted stays false. real value has left the pool and the re-flag window is still open over a pool already drained of its bonus.
a subsequent re-flag to good-faith CORRUPTED then re-snapshots the depleted totalBonus (ConfidencePool.sol#L354-L362):
the named whitehat's entitlement is now S, short by the whole bonus B that already sits at recoveryAddress. claimAttackerBounty pays min(remaining, freeBalance) == S (ConfidencePool.sol#L432-L442), and contributeBonus is blocked once outcome != UNRESOLVED, so the depletion is permanent. there is no path to restore totalBonus, and even a corrective bad-faith re-flag cannot pull B back out of recoveryAddress.
this is exactly the balance-accounting break section 4 states cannot happen: the latch exists so that "value has left" implies "outcome frozen", and the bonus sweep is the single exit that severs the implication. the docs are on this side of the argument. the one no-risk-window race they already accept (DESIGN.md#L93-L127) is a different mechanism, a staker calling claimExpired to foreclose CORRUPTED with EXPIRED, which turns on claimExpired latching finality. this path turns on sweepUnclaimedBonus not latching it, and it strands the bonus under a corrected CORRUPTED outcome the docs never discuss. the same section only describes the SURVIVED sweep that sends the bonus to recoveryAddress at riskWindowStart == 0; it never addresses the corrected CORRUPTED outcome, where that same bonus is owed to the attacker but has already left the pool.
Likelihood:
the registry reaches terminal CORRUPTED with, in the design's own words, "no pool interaction observed the registry in an active-risk state before it reached a terminal one", leaving riskWindowStart == 0.
the moderator resolves an initial out-of-scope reading as SURVIVED and later corrects it to good-faith CORRUPTED, the exact pre-claim correction the re-flag window is built to allow. SURVIVED is accepted on a terminal-CORRUPTED registry, so both flags are valid.
the permissionless bonus sweep runs between the two flags, ahead of any claim latching finality. the sequence needs no malicious actor: sweeping unclaimed bonus is the routine post-SURVIVED cleanup any keeper performs. and the sponsor who controls recoveryAddress is directly paid by that sweep, so a motivated party exists to guarantee the timing.
Impact:
the entire sponsor bonus B, which the good-faith CORRUPTED path routes to the named whitehat as their bounty, is permanently captured by recoveryAddress instead. the whitehat receives S where a direct CORRUPTED resolution of the identical state pays S + B.
the shortfall is unrecoverable. contributeBonus is gated on UNRESOLVED so totalBonus can never be topped back up, and the moderator has no corrective flag that pulls B back out of recoveryAddress once the sweep has fired.
the finality invariant the claimsStarted latch enforces is broken: value has left the pool while the outcome was left unfrozen, the exact balance-accounting break the design states is impossible.
add test/poc/SweepReflagAttackerShortfall.t.sol. it deploys a pool through the in-repo BaseConfidencePoolTest helper and the existing registry/agreement mocks, no fork. the test walks the SURVIVED -> sweep -> good-faith CORRUPTED sequence and asserts the named whitehat receives only S while the whole bonus B sits at recoveryAddress.
run:
output:
the test shows the named whitehat receiving 100_000e18 (principal only) instead of the 140_000e18 a good-faith CORRUPTED resolution entitles them to, with the 40_000e18 bonus permanently held by recoveryAddress. the shortfall is exactly the swept bonus B.
treat the bonus sweep as a finality event on the branch where it moves genuine bonus rather than dust. the 1-wei-donation anti-grief concern the current comment cites only applies to the reserved case, where the swept amount is pure excess. in the riskWindowStart == 0 || totalEligibleStake == 0 branch the swept amount is the real bonus and must freeze the outcome:
alternatively, gate flagOutcome's re-flag on a dedicated bonusSwept latch set whenever a non-dust bonus sweep occurs, so a correction cannot re-snapshot bountyEntitlement / corruptedReserve after the bonus has already been moved out.
both options bring the bonus sweep in line with the one-way finality the design already relies on: the riskWindowStart != 0 latch that keeps a benign upstream rewind from re-opening withdraw (DESIGN.md#L289-L290), the expiryLocked latch on expiry, and claimsStarted on every claim path. value that has left the pool should not be re-openable by a later re-flag, which is the invariant the design already upholds for every other latched position.
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.