Pot::closePot has no "already closed" flag and never decrements remainingRewards inside its payout loop. Once the 90-day window has passed, the function can be called repeatedly. Each call passes the time check again, sees remainingRewards > 0 again, and re-executes both the manager-cut transfer and the full claimant payout loop, draining the Pot beyond the intended single distribution.
A correct close-out is a one-time operation: take the manager cut once, distribute the residue once, mark the Pot closed. closePot does neither the "once" nor the "mark closed" part. remainingRewards is the only state that could gate re-entry into the distribution, and it is never updated during close, so its value is identical before and after a close.
The owner (or anyone who can trigger closeContest) can call close multiple times after the 90-day boundary. Each repeat sends another managerCutPercent cut to the manager and pays claimants[] their claimantCut a second, third, ... time, transferring out funds that were never theirs to distribute again. The accounting view getRemainingRewards() also reports a stale, pre-close figure forever, misleading any integrator.
src/Pot.sol::closePot (L49-62):
function closePot() external onlyOwner {
if (block.timestamp - i_deployedAt < 90 days) revert Pot__StillOpenForClaim();
if (remainingRewards > 0) {
uint256 managerCut = remainingRewards / managerCutPercent;
i_token.transfer(msg.sender, managerCut);
uint256 claimantCut = (remainingRewards - managerCut) / i_players.length;
for (uint256 i = 0; i < claimants.length; i++) {
_transferReward(claimants[i], claimantCut);
}
}
}
There is no closed boolean and remainingRewards is never set to 0. The guard if (remainingRewards > 0) is the only re-entry gate, and it is never falsified by a close.
Double (or N-times) manager cut: first close sends 70e18; a second close sends another 70e18 (70e18 -> 140e18 in the PoC), and so on per call.
Repeated claimant payouts drain residue/leftover balance.
Stale accounting: getRemainingRewards() never reflects the distribution.
test_HH2_reclose_double_manager_cut (close, then close again after the window). Real forge output:
[PASS] test_HH2_reclose_double_manager_cut()
Assertions: manager balance after second close == 2x the single-cut amount (70e18 -> 140e18); Pot balance decreases on each re-call with no state preventing it.
Add a closed flag and set it at the start of closePot:
bool private closed;
...
if (closed) revert Pot__AlreadyClosed();
closed = true;
Also zero remainingRewards after distribution so the accounting view is correct and the residue cannot be re-distributed.
Foundry (forge), manual review.
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.