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.
Appeals are being carefully reviewed by our judges.
The contest is complete and the rewards are being distributed.