Root + Impact
Description
After closePot distributes funds, remainingRewards is never reset to zero. While onlyOwner limits who can call it, if closePot is ever called a second time (possible if the contract receives more tokens), the stale remainingRewards value could trigger another distribution even though the original pot was already closed.
function closePot() external onlyOwner {
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);
}
}
}
Risk
Likelihood:
Requires owner to call closePot twice, low probability
But if contract receives additional tokens, second call would distribute incorrectly
Impact:
Proof of Concept
The following test demonstrates that after closePot distributes funds, remainingRewards remains non-zero.
If the contract receives additional tokens and closePot is called again, it triggers another full distribution
using the stale remainingRewards value:
function testStaleRemainingRewardsAfterClose() public mintAndApproveTokens {
vm.startPrank(user);
contest = ContestManager(conMan).createContest(players, rewards, IERC20(weth), 4);
ContestManager(conMan).fundContest(0);
vm.stopPrank();
vm.prank(player1);
Pot(contest).claimCut();
vm.warp(91 days);
vm.startPrank(user);
ContestManager(conMan).closeContest(contest);
vm.stopPrank();
uint256 remaining = Pot(contest).getRemainingRewards();
assertGt(remaining, 0);
ERC20Mock(address(weth)).mint(contest, 100);
vm.warp(block.timestamp + 1 days);
vm.startPrank(user);
ContestManager(conMan).closeContest(contest);
vm.stopPrank();
}
Recommended Mitigation
function closePot() external onlyOwner {
if (remainingRewards > 0) {
uint256 managerCut = remainingRewards / managerCutPercent;
+ remainingRewards = 0;
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);
}
}
}