Normally closePot() should run exactly once per contest: skim the manager cut, distribute the remainder to claimants, and then be finished.
The issue: closePot() derives managerCut and claimantCut from remainingRewards but never zeroes it, and there is no closed guard. Both entry gates — the 90-day timestamp check and remainingRewards > 0 — stay true after a successful close, so the owner can call closeContest() again and the entire payout re-executes.
Likelihood:
Requires the owner (a trusted role) to call closeContest() more than once — e.g. an accidental double-submit, a retry after a transaction that appeared to fail, or buggy keeper/automation.
There is no attacker path; the trigger is bounded to the owner role. This is why the finding is Medium rather than High.
Impact:
Each extra close re-pays the manager cut and re-pays every claimant from whatever token balance the pot still holds, over-distributing the contest's funds.
Repeated closes drain the pot's remaining balance (including any remainder stranded by the denominator bug) until a transfer reverts — a real misallocation/loss of the contest's funds.
This Foundry test passes against the in-scope code. Ten players are owed 100 (total 1000); only p1 claims (remaining 900); the owner closes twice; the manager is paid the cut both times:
Zero the accounting at the end of closePot() (or add a one-shot closed flag) so the gates cannot pass a second time:
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.