MyCut

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

Duplicate player addresses in constructor overwrite rewards causing fund loss

[H-4] Duplicate player addresses in constructor overwrite rewards causing fund loss

Description: The Pot constructor iterates through the i_players array and maps each address to their reward amount in playersToRewards. However, the constructor does not check for duplicate addresses. When a player address appears multiple times in the array, the mapping overwrites previous values, causing players to lose their rightful rewards and leaving funds permanently stuck in the contract.

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;
// WARNING: NO CHECK FOR DUPLICATES
for (uint256 i = 0; i < i_players.length; i++) {
playersToRewards[i_players[i]] = i_rewards[i]; // Overwrites if duplicate
}
}

Impact:

  • HIGH severity - Direct financial loss for users

  • Players lose rightful rewards (only receive last occurrence)

  • Funds permanently stuck in contract

  • totalRewards accounting breaks

  • remainingRewards never reaches zero even if all claim

  • Manager receives less than expected cut

Example scenario:

players = [Alice, Bob, Alice]
rewards =
Loop iterations:
i=0: playersToRewards[Alice] = 100 [OK]
i=1: playersToRewards[Bob] = 200 [OK]
i=2: playersToRewards[Alice] = 50 WARNING: OVERWRITES 100!
Result:
- Alice can only claim 50 (lost 100!)
- Bob can claim 200
- Total claimable: 250
- Contract expects: 350
- 100 tokens stuck forever

Proof of Concept:

function test_Duplicate_Address_Overwrites_Reward() public {
address[] memory duplicatePlayers = new address[](3);
duplicatePlayers = player1;
duplicatePlayers = player2; [ppl-ai-file-upload.s3.amazonaws](https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/images/50523984/449e3f4d-9fad-462b-8c42-b08784ab76b6/image.jpg)
duplicatePlayers = player1; // Duplicate!
uint256[] memory duplicateRewards = new uint256[](3);
duplicateRewards = 100; // player1 first reward
duplicateRewards = 200; // player2 reward [ppl-ai-file-upload.s3.amazonaws](https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/images/50523984/449e3f4d-9fad-462b-8c42-b08784ab76b6/image.jpg)
duplicateRewards = 50; // player1 second reward (overwrites first)
vm.startPrank(owner);
address pot = manager.createContest(duplicatePlayers, duplicateRewards, token, 350);
manager.fundContest(0);
vm.stopPrank();
// Check what player1 can claim
uint256 player1Reward = Pot(pot).checkCut(player1);
assertEq(player1Reward, 50); // Only last entry, not 100!
uint256 player2Reward = Pot(pot).checkCut(player2);
assertEq(player2Reward, 200);
// Player1 should get 150 (100+50) but only gets 50
// 100 tokens permanently stuck in contract
vm.prank(player1);
Pot(pot).claimCut();
vm.prank(player2);
Pot(pot).claimCut();
// Check remaining - should be 0 but is 100
uint256 remaining = Pot(pot).getRemainingRewards();
assertEq(remaining, 100); // 100 tokens stuck forever
}

Recommended Mitigation:

+ error Pot__DuplicatePlayer();
+ error Pot__ArrayLengthMismatch();
constructor(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards) {
+ require(players.length == rewards.length, "Array length mismatch");
+
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++) {
+ // Check for duplicate addresses
+ if (playersToRewards[i_players[i]] != 0) {
+ revert Pot__DuplicatePlayer();
+ }
+ // Also check for zero address
+ require(i_players[i] != address(0), "Invalid player address");
+ require(i_rewards[i] > 0, "Reward must be greater than zero");
playersToRewards[i_players[i]] = i_rewards[i];
}
}

Alternative: Use a mapping to track seen addresses:

constructor(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards) {
require(players.length == rewards.length, "Array length mismatch");
+ mapping(address => bool) memory seenPlayers;
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++) {
+ require(!seenPlayers[i_players[i]], "Duplicate player address");
+ require(i_players[i] != address(0), "Zero address not allowed");
+ require(i_rewards[i] > 0, "Zero reward not allowed");
+
+ seenPlayers[i_players[i]] = true;
playersToRewards[i_players[i]] = i_rewards[i];
}
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 10 days 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!