MyCut

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

Incorrect handling of duplicate addresses in Pot constructor

Description

  • Each unique player address should map to exactly one reward amount. The constructor should initialize playersToRewards so that every player can claim the reward specified for them, and the sum of allocated rewards should match (or be bounded by) totalRewards.

  • The constructor writes playersToRewards[i_players[i]] = i_rewards[i] for each index, without validating that i_players contains unique addresses. If a player address appears multiple times, the mapping entry is silently overwritten by the last occurrence. Earlier rewards are lost, causing misallocation and accounting drift. This also interacts badly with post‑close redistribution: duplicates inflate i_players.length even though only distinct claimants can receive payouts, further skewing calculations.

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;
// @> No duplicate detection:
// @> Later entries overwrite prior entries for the same address.
for (uint256 i = 0; i < i_players.length; i++) {
playersToRewards[i_players[i]] = i_rewards[i];
}
}

Risk

Likelihood: High

  • Contest setups and data imports frequently include human or tooling errors, making duplicate entries a routine possibility during configuration.

  • There is no validation path in the current constructor, so any duplicate will be accepted and applied.

Impact: High

  • Silent underpayment: A player listed multiple times will only be able to claim the last specified reward; earlier allocations vanish.

  • Broken accounting: The sum of assigned rewards can diverge from totalRewards, leaving excess funds in the pot and causing incorrect redistribution and remainingRewards reporting.

Proof of Concept

  • Copy the code below to TestMyCut.t.sol.

  • Run command forge test --mt testPotConstructorOverwritesDuplicateAddresses -vv.

function testPotConstructorOverwritesDuplicateAddresses() public mintAndApproveTokens {
// Setup duplicate address in players array
rewards = [1000, 1];
players = [player1, player1];
totalRewards = 1001;
uint256 player1ExpectedReward = rewards[0] + rewards[1];
emit log_named_uint("Player 1 expected reward", player1ExpectedReward);
// Create and fund the contest
vm.startPrank(user);
contest = ContestManager(conMan).createContest(players, rewards, IERC20(ERC20Mock(weth)), totalRewards);
ContestManager(conMan).fundContest(0);
vm.stopPrank();
// Check assigned reward for player1
uint256 player1AssignedReward = Pot(contest).checkCut(player1);
emit log_named_uint("Player 1 assigned reward", player1AssignedReward);
assert(player1AssignedReward < player1ExpectedReward);
}

Output:

[⠊] Compiling...
No files changed, compilation skipped
Ran 1 test for test/TestMyCut.t.sol:TestMyCut
[PASS] testPotConstructorOverwritesDuplicateAddresses() (gas: 1034257)
Logs:
User Address: 0x6CA6d1e2D5347Bfab1d91e883F1915560e09129D
Contest Manager Address 1: 0x7BD1119CEC127eeCDBa5DCA7d1Bd59986f6d7353
Minting tokens to: 0x6CA6d1e2D5347Bfab1d91e883F1915560e09129D
Approved tokens to: 0x7BD1119CEC127eeCDBa5DCA7d1Bd59986f6d7353
Player 1 expected reward: 1001
Player 1 assigned reward: 1
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.98ms (561.80µs CPU time)
Ran 1 test suite in 14.59ms (2.98ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

  • Explicitly aggregate duplicates.

  • Chage the code in the Pot constructor accordingly:

- for (uint256 i = 0; i < players.length; i++) {
- playersToRewards[players[i]] = rewards[i];
- }
+ for (uint256 i = 0; i < players.length; i++) {
+ playersToRewards[players[i]] += rewards[i]; // accumulate
+ }
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 6 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!