Summary
When the closePot
function is called, there could still be remaining funds left in the Pot
contract that cannot be recovered, leaving it stuck in the contract forever.
Vulnerability Details
The vulnerability lies in line 54 and line 57 of the Pot
contract.
Due to integer division precision loss, the code in the mentioned lines would round down to the nearest integer, resulting in remaining funds left in the Pot
contract.
Proof of Concept
Working Test Case
function testRemainingFundsAfterClosePot() public mintAndApproveTokens {
address player3 = makeAddr("player3");
players = [player1, player2, player3];
rewards = [32, 30, 38];
totalRewards = 100;
uint256 claimantsLength = 0;
vm.startPrank(user);
contest = ContestManager(conMan).createContest(players, rewards, IERC20(ERC20Mock(weth)), totalRewards);
ContestManager(conMan).fundContest(0);
vm.stopPrank();
vm.startPrank(player1);
Pot(contest).claimCut();
claimantsLength++;
vm.stopPrank();
vm.startPrank(player2);
Pot(contest).claimCut();
claimantsLength++;
vm.stopPrank();
vm.warp(91 days);
vm.startPrank(user);
ContestManager(conMan).closeContest(contest);
vm.stopPrank();
uint256 unclaimedRewards = totalRewards - rewards[0] - rewards[1];
uint256 managerCut = unclaimedRewards / 10;
uint256 remainingFunds =
unclaimedRewards - managerCut - ((unclaimedRewards - managerCut) / players.length) * claimantsLength;
assertEq(ERC20Mock(weth).balanceOf(address(Pot(contest))), remainingFunds);
}
Impact
The funds remaining in the contract are permenantly stuck in the contract and there are no ways to recover it.
Tools Used
Foundry, manual review
Recommended Mitigation
To mitigate this vulnerability, all remaining balance of i_token
after the closePot
function is called should be transferred to an EOA or a contract address where funds can be withdrawn safely. In this example, we treat destination
as an EOA set up by the owner of the contest.
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);
}
+ if (i_token.balanceOf(address(this)) != 0) {
+ i_token.transfer(destination, i_token.balanceOf(address(this)));
+ }
+ remainingRewards = 0;
}
}