closePot is not terminal + repeated payouts and stale remaining-rewards accountingClosing a pot should be a one-time event. The remaining pool is split once, 10% to the manager and 90% to the in-time claimants. After that, remainingRewards and the views getRemainingRewards and getContestRemainingRewards should show that nothing is left to distribute.
closePot reads remainingRewards but never zeroes or decrements it. The only writes are in the constructor (Pot.sol:27) and in claimCut (Pot.sol:44). It also records no closed state, and ContestManager::closeContest forwards to it without tracking anything.
Each later closeContest on the same pot therefore passes the 90-day check and the remainingRewards > 0 check again. It pays the same stale managerCut and claimantCut for as long as the Pot's balance covers a full round.
Likelihood:
It needs the trusted owner to call closeContest more than once on the same pot. Causes include a retried transaction, a keeper or script that re-runs, or an admin misled by getContestRemainingRewards, which still reports the pre-close amount.
Funds move only while the Pot still holds residue after the first close. Residue remains after any close where some players did not claim (because of the separate bonus-divisor issue) and after any close with zero claimants.
Impact:
Each repeat close sends another manager cut into ContestManager, which has no withdraw path, and another bonus to the existing claimants. In the PoC there are three players at 100 and only p1 claims:
Close 1 pays 20 + 60, leaving 120 in the Pot.
Close 2 pays 20 + 60 again, leaving 40.
Close 3 reverts atomically with ERC20InsufficientBalance(pot, 20, 60).
p2's post-close claimCut succeeds after one close but reverts after two (ERC20InsufficientBalance(pot, 40, 100)).
getRemainingRewards and getContestRemainingRewards keep returning 200 after every close, which misleads off-chain consumers. Claimants are not overpaid compared with the README (p1 ends at 220 against the 280 intended). The harm is mis-routed residue and wrong accounting, not theft.
The test deploys ContestManager and the repo's test/ERC20Mock.sol. All three tests pass:
test_baseline_allClaim_repeatCloseIsNoOp (control): when everyone claims, remainingRewards is 0 and two closes move nothing.
test_repeatClose_repaysStaleCuts_staleViews_starvesLateClaimer: the scenario above. Each close is asserted to pay ContestManager 20 and p1 60. Both views stay at 200. The third close reverts atomically, p2's claim reverts, and p1's 220 is asserted to be below the README entitlement of 280.
test_singleClose_lateClaimStillPossible (contrast): after a single close, p2's claim of 100 still succeeds, so the second close is what starves p2.
Save the file as test/PoC_MC-04.t.sol and run from the repository root:
Make the close terminal: set a closed flag and zero remainingRewards before paying out. Combine this with the divisor fix from the bonus-distribution issue.
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.