MyCut

AI First Flight #8
Beginner FriendlyFoundry
EXP
View results
Submission Details
Severity: high
Valid

`Pot` constructor overwrites rewards for duplicate players and doesn't validate the input arrays, so rewards are lost and remain stuck in the Pot

Root + Impact

Description

  • Each entry in players should be credited with the matching rewards entry. A player who won more than one prize should be able to claim the total.

  • The constructor uses = instead of +=, so a player's later entry overwrites the earlier one. There is also no check that players.length == rewards.length or that the rewards add up to totalRewards. The overwritten amount can never be claimed, and duplicates also inflate i_players.length, which is used as the divisor in closePot.

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];
}
}

Risk

Likelihood:

  • When a contest has one address winning several prizes (e.g. several placements or categories), which is common when rewards are listed per prize.

  • When the off-chain list of players/rewards is built by a script, since the contract accepts any input without checks.

Impact:

  • The player loses every reward except the last one listed for them (50 tokens in the PoC).

  • The lost amount stays in remainingRewards as if unclaimed and ends up stranded in the Pot or redistributed to other players, even though everyone claimed.

Proof of Concept

player1 wins 50 + 100 and player2 wins 100 (total 250, fully funded). player1 can only claim 100, and 50 is left as "remaining" even after every player has claimed.

function testDuplicatePlayerRewardOverwritten() public mintAndApproveTokens {
address[] memory ps = new address[](3);
ps[0] = player1; ps[1] = player1; ps[2] = player2; // player1 won 2 prizes
uint256[] memory rs = new uint256[](3);
rs[0] = 50e18; rs[1] = 100e18; rs[2] = 100e18;
vm.startPrank(user);
address pot = ContestManager(conMan).createContest(ps, rs, IERC20(address(weth)), 250e18);
ContestManager(conMan).fundContest(0);
vm.stopPrank();
assertEq(Pot(pot).checkCut(player1), 100e18); // should be 150e18
vm.prank(player1);
Pot(pot).claimCut();
vm.prank(player2);
Pot(pot).claimCut();
assertEq(weth.balanceOf(player1), 100e18); // lost 50e18
assertEq(Pot(pot).getRemainingRewards(), 50e18); // "unclaimed" although everyone claimed
}

Recommended Mitigation

+ error Pot__LengthMismatch();
+ error Pot__RewardsMismatch();
constructor(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards) {
+ if (players.length != rewards.length) revert Pot__LengthMismatch();
i_players = players;
i_rewards = rewards;
i_token = token;
i_totalRewards = totalRewards;
remainingRewards = totalRewards;
i_deployedAt = block.timestamp;
+ uint256 sum;
for (uint256 i = 0; i < i_players.length; i++) {
- playersToRewards[i_players[i]] = i_rewards[i];
+ playersToRewards[i_players[i]] += i_rewards[i];
+ sum += i_rewards[i];
}
+ if (sum != totalRewards) revert Pot__RewardsMismatch();
}

Also count unique claimants (not i_players.length) when splitting the leftover (see S1).

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 4 hours ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-03] [M1] `Pot::constructor` Overwrites Rewards for Duplicate Players, Leading to Incorrect Distribution

## Description The `for` loop inside the `Pot::constructor` override the `playersToRewards[i_players[i]]` with new reward `i_rewards[i]`.So if a player's address appears multiple times, the reward is overwritten rather than accumulated. This results in the player receiving only the reward from the last occurrence of their address in the array, ignoring prior rewards. ## Vulnerability Details **Proof of Concept:** 1. Suppose i_players contains \[0x123, 0x456, 0x123] and i_rewards contains \[100, 200, 300]. 2. The playersToRewards mapping will be updated as follows during construction: - For address 0x123 at index 0, reward is set to 300. - For address 0x456 at index 1, reward is set to 200. - For address 0x123 at index 2, reward is updated to 100. 3. As a result, the final reward for address 0x123 in playersToRewards will be 100, not 400 (300+100).This leads to incorrect and lower reward distributions. **Proof of Code (PoC):** place the following in the `TestMyCut.t.sol::TestMyCut` ```Solidity address player3 = makeAddr("player3"); address player4 = makeAddr("player4"); address player5 = makeAddr("player5"); address[] sixPlayersWithDuplicateOneAddress = [player1, player2, player3, player4, player1, player5]; uint256[] rewardForSixPlayers = [2, 3, 4, 5, 6, 7]; uint256 totalRewardForSixPlayers = 27; // 2+3+4+5+6+7 function test_ConstructorFailsInCorrectlyAssigningReward() public mintAndApproveTokens { for (uint256 i = 0; i < sixPlayersWithDuplicateOneAddress.length; i++) { console.log("Player: %s reward: %d", sixPlayersWithDuplicateOneAddress[i], rewardForSixPlayers[i]); } /** * player1 has two occurance in sixPlayersWithDuplicateOneAddress ( at index 0 and 4) * So it's expected reward should be 2+6 = 8 */ vm.startPrank(user); contest = ContestManager(conMan).createContest(sixPlayersWithDuplicateOneAddress, rewardForSixPlayers, IERC20(ERC20Mock(weth)), totalRewardForSixPlayers); ContestManager(conMan).fundContest(0); vm.stopPrank(); uint256 expectedRewardForPlayer1 = rewardForSixPlayers[0] + rewardForSixPlayers[4]; uint256 assignedRewardForPlaye1 = Pot(contest).checkCut(player1); console.log("Expected Reward For Player1: %d", expectedRewardForPlayer1); console.log("Assigned Reward For Player1: %d", assignedRewardForPlaye1); assert(assignedRewardForPlaye1 < expectedRewardForPlayer1); } ``` ## Impact The overall integrity of the reward distribution process is compromised. Players with multiple entries in the i_players\[] array will only receive the reward from their last occurrence in the array, leading to incorrect and lower reward distributions. ## Recommendations **Recommended Mitigation:** Aggregate the rewards for each player inside the constructor to ensure duplicate addresses accumulate rewards instead of overwriting them.This can be achieved by using the += operator in the loop that assigns rewards to players. ```diff for (uint256 i = 0; i < i_players.length; i++) { - playersToRewards[i_players[i]] = i_rewards[i]; + playersToRewards[i_players[i]] += i_rewards[i]; } ```

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!