Normal behavior: sweepUnclaimedBonus() is the documented cleanup path that returns any pool balance above the remaining stakers' real entitlement — including k=2 rounding dust — to the recovery address.
Specific issue: Its reserve is computed from the gross global remainder snapshotTotalBonus - claimedBonus rather than the sum of the remaining stakers' floor-rounded shares, and each claim only subtracts the floored amount it pays, so the reserve stays inflated by the accumulated rounding residue while any staker is unclaimed. A final staker whose own share rounds to zero can hold the whole residue hostage: the sweep reverts NothingToSweep until that staker claims, even though they can never receive any of the reserved amount. On a low-decimal allowlisted token the residue is whole tokens, not negligible wei.
Likelihood:
WHEN a pool uses a factory-allowlisted standard low-decimal ERC20 and an equal-score cohort shares a bonus small enough that per-user shares floor down materially (in the extreme, bonus B < staker count N, so every share is zero). The compatibility rules exclude fee-on-transfer and rebasing tokens but set no minimum decimals.
WHEN all but one staker claim and the final zero-share staker simply does not claim (there is no bonus-claim deadline), the rounding residue stays reserved and sweepUnclaimedBonus reverts NothingToSweep indefinitely.
Impact:
Bonus dust owed to the recovery address is denied, and the documented cleanup path is bricked for an unbounded time controlled by a single non-claimer. The pinned amount is bounded by the number of claimants in raw token units, which on a low-decimal token is whole tokens.
No staker principal is stolen or frozen and the attacker gains nothing directly, so this is Low, not Medium/High — but it is a real accounting defect fully inside src/ConfidencePool.sol.
Uses a plain standard ERC20 whose only customization is decimals() == 0 (transfer accounting unchanged). Save as test/unit/RoundingReserveOverReservation.t.sol and run forge test --match-contract RoundingReserveOverReservationTest -vv.
Passing result: the sweep reverts NothingToSweep while a final zero-share staker holds out, releasing the 9 pinned units only after that staker claims.
Track a running upper bound of the bonus weight still attributable to unclaimed users and reserve floor(snapshotTotalBonus * remainingWeight / snapshotGlobalScore) (and the analogous remaining-stake bound in the amount-weighted fallback). Alternatively, add a bounded bonus-claim deadline after which unclaimed positions keep principal but forfeit bonus, enforced identically by the claim and sweep paths.
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.