Summary
When a pot is created, the totalRewards is set, this parameter doesn't have any requirement, therefor it can be less than the actual total rewards amount in rewards[] or even 0, and in order to claim their prizes, the players use Pot::claimCut which substract the amount claimed to remainingRewardsthat in on itself is set as equal to totalRewards when the pot is created, If the said variable is less than the amount claimed, the function will revert beacuse of underflow, and thus the player will not be able to claim his cut.
Vulnerability Details
First in ContestManager::createContest the total rewards is set with no check for zero amount in place, nor check if the said value is equivalent to the actual total rewards (as shown in th code snipet below), as such, it is possible to make a mistake by accident or on purpuse by malicious contest manager.
function createContest(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards)
public
onlyOwner
returns (address)
{
Pot pot = new Pot(players, rewards, token, totalRewards);
contests.push(address(pot));
contestToTotalRewards[address(pot)] = totalRewards;
return address(pot);
}
When the pot is created, remainingRewards is set to total rewards in Pot::constructor.
constructor(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards) {
i_players = players;
i_rewards = rewards;
i_token = token;
i_totalRewards = totalRewards;
@> remainingRewards = totalRewards;
i_deployedAt = block.timestamp;
for (uint256 i = 0; i < i_players.length; i++) {
playersToRewards[i_players[i]] = i_rewards[i];
}
}
The issue becomes evident when a player tries to claim his cut, as shown below, the amount the player is claimming, is subtracted from the remainingRewards variable, as susch, if reward > remainingRewards the function will revert due to arithmetic underflow, making it imposible for a player to get his reward.
function claimCut() public {
address player = msg.sender;
uint256 reward = playersToRewards[player];
if (reward <= 0) {
revert Pot__RewardNotFound();
}
playersToRewards[player] = 0;
@> remainingRewards -= reward;
claimants.push(player);
_transferReward(player, reward);
}
You may corroborate the problem by adding the following codes in TestMyCut.sol, in this codes, we test two posible sitations, the first one where totalRewards = 0 making every claim imposible; an a second one, where totalRewards != 0 but is still less than the actual total amount to be claimed, meaning the first claim will succeed but the following ones will not.
function testCreatePotTotalRewardZero() public mintAndApproveTokens {
vm.startPrank(user);
contest = ContestManager(conMan).createContest(players, rewards, IERC20(ERC20Mock(weth)), 0);
ContestManager(conMan).fundContest(0);
vm.stopPrank();
vm.startPrank(player1);
vm.expectRevert();
Pot(contest).claimCut();
vm.stopPrank();
}
function testCreatePotTotalRewardLessThanRealAmount() public mintAndApproveTokens {
vm.startPrank(user);
contest = ContestManager(conMan).createContest(players, rewards, IERC20(ERC20Mock(weth)), 3);
ContestManager(conMan).fundContest(0);
vm.stopPrank();
vm.startPrank(player1);
Pot(contest).claimCut();
vm.stopPrank();
vm.startPrank(player2);
vm.expectRevert();
Pot(contest).claimCut();
vm.stopPrank();
}
Impact
Player would be unable to claim their prizes if it is greater than the totalRewards if t was set incorrectly.
Tools Used
Foundry and Manual review
Recommendations
The simplest solution would be that the totalRewards variable is set as the addition of all values within the rewards array using a for loop as follows:
- function createContest(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards)
+ function createContest(address[] memory players, uint256[] memory rewards, IERC20 token)
public
onlyOwner
returns (address)
{
+ uint256 totalRewards = 0
+ for (uint256 i = 0; i < claimants.length; i++){
+ totalRewards = totalRewards + rewards[i];
+ }
+ if (totalRewards == 0){
+ revert;
+ }
Pot pot = new Pot(players, rewards, token, totalRewards);
contests.push(address(pot));
contestToTotalRewards[address(pot)] = totalRewards;
return address(pot);
}