Summary
Improper calculation of the claimantCut results in the tokens being stuck in the contract,claimantCut which is used to distribute the unclaimed tokens between the players is calculated wrongly.
Vulnerability Details
The claimantCut variable in the closePot is used to distribute the unclaimed amount but instead of using claimants length in the calculation, the contract uses player length which in turn leads to reward tokens getting stuck in the contract itself.
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);
}
}
}
Proof Of Concept
Steps to recreate:
Create a contest with two players and give 500 rewards for each player.
Fund the contest and claim the reward for one player.
End the contest,now the contract will send 50 tokens from the unclaimed tokens to the manager.
Remaining 450 tokens will be split into two as per the calculation and one part of the split will be send to the player1 as he is a claimant.
Remaining half which equals to 225 tokens will be stuck in the contract itself.
function testCalculationOfClaimantCut() public mintAndApproveTokens {
vm.startPrank(user);
rewards = [500, 500];
totalRewards = 1000;
contest = ContestManager(conMan).createContest(players1, rewards, IERC20(ERC20Mock(weth)), totalRewards);
ContestManager(conMan).fundContest(0);
vm.stopPrank();
vm.startPrank(player1);
Pot(contest).claimCut();
vm.stopPrank();
vm.warp(91 days);
vm.startPrank(user);
ContestManager(conMan).closeContest(contest);
vm.stopPrank();
assertEq(ERC20Mock(weth).balanceOf(contest),225);
}
Tools Used
-> Manual Review
-> Foundry
Recommendations
instead of using player length for catculating claimantCut use Claimant length like shown in the following code:
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) / claimants.length;
for (uint256 i = 0; i < claimants.length; i++) {
_transferReward(claimants[i], claimantCut);
}
}
}