Normally, closing a pot is a terminal operation: the manager takes the cut, claimants get their shares, and the pot is finalized.
closePot writes no state: there is no closed flag, and remainingRewards is not zeroed, so the 90-day guard is the only check — which remains satisfied forever after the first close. Each subsequent call repeats the exact same transfers while the actual pot balance keeps shrinking (ledger/balance desync).
Likelihood:
The owner simply calls closeContest repeatedly in separate transactions — nothing in the code prevents the 2nd..Nth call after the 90-day mark.
A single close only drains managerCut + claimants*claimantCut; in the verified low-claim-rate setup (10 players, 1 claimant) each round drains ~11% of remainingRewards, so five consecutive rounds succeed before the 6th reverts on insufficient balance.
Impact:
Verified: with 10 players x 100 (total 1000) and a single claimant, 5 close rounds transfer 450 to the ContestManager (frozen per V1), 905 to the claimant (905 - 500 entitlement = 405 taken from the 9 non-claiming players' shares), leaving 45 stranded — while remainingRewards still reads 900 on-chain.
The pot's accounting desynchronizes from its balance, breaking every downstream expectation and making audits/monitoring meaningless.
PoC explanation: The test drives closePot through five successful rounds on a single, normally-funded pot. Setup: 10 players at 100 each (total 1000); only player 0x1000 claims, leaving remainingRewards = 900. Step: the admin calls closeContest five times in separate calls — nothing stops calls 2..5 because no closed flag exists and remainingRewards was never zeroed. Each round re-pays managerCut = 90 (to the frozen CM, per V1) and claimantCut = (900 - 90) / 10 = 81 to the single claimant, so five rounds extract 5 × 90 = 450 for the CM and 5 × 81 = 405 extra for the claimant (405 taken directly from the 9 non-claiming players' shares). Assertions after 5 rounds: CM balance = 450, claimant balance = 100 + 81×5 = 505, the pot retains 45 of real tokens, yet getRemainingRewards() STILL reads 900 — the ledger never moved while the balance drained (full ledger/balance desync). The 6th call finally reverts — with a raw ERC20InsufficientBalance, not a domain error, confirming no idempotency guard of any kind exists. Run with: forge test --match-test test_POC4_ClosePotRepeatable -vv (PASS; all five rounds and all balance assertions hold).
⚠ Combination note: the s_closed flag must be shared with claimCut (V5). Closing out the ledger while leaving playersToRewards untouched lets post-close claims succeed whenever the pot holds an excess balance (e.g. double funding, V6). Also note: fixing V6 (double funding) does NOT fix this issue — the PoC above uses a single normal funding.
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.