MyCut

AI First Flight #8
Beginner FriendlyFoundry
EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Incorrect Reward Distribution in closePot

  • In the Pot contract, the claimCut() function allows players to claim their rewards by checking the playersToRewards mapping.

  • The contract uses reward <= 0 to check if a player has a reward, but for addresses not in the original players array, the mapping returns 0 by default, causing the function to revert with Pot__RewardNotFound() error even though the player might be legitimately in the players array but with a 0 reward.

root cause

https://github.com/CodeHawks-Contests/ai-mycut/blob/819134663950999ca5ab29a91eeceddb80274743/src/Pot.sol#L54-62

Risk

Likelihood: High

  • Every time a player calls claimCut(), the contract checks the playersToRewards mapping which returns 0 for any address not explicitly set in the constructor

  • The contract's validation logic incorrectly treats 0 as "no reward" instead of distinguishing between "uninitialized" and "reward of 0"

  • This will occur whenever a player who wasn't properly initialized in the mapping tries to claim

Impact:high

  • Legitimate players who should have rewards cannot claim them

  • Contract funds become locked and unclaimable by intended recipients

  • The closePot() function will incorrectly redistribute unclaimed funds as if players didn't claim them

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol";
contract TestToken is ERC20 {
constructor() ERC20("TestToken", "TEST") {
_mint(msg.sender, 1_000_000 * 1e18);
}
}
contract VulnerablePot is Ownable(msg.sender) {
address[] private i_players;
address[] private claimants;
uint256 private immutable i_totalRewards;
uint256 private immutable i_deployedAt;
TestToken private immutable i_token;
mapping(address => uint256) private playersToRewards;
uint256 public remainingRewards;
uint256 private constant managerCutPercent = 10;
constructor(
address[] memory players,
uint256[] memory rewards,
uint256 totalRewards
) {
i_players = players;
i_token = new TestToken(); // Auto-mint to deployer
i_totalRewards = totalRewards;
remainingRewards = totalRewards;
i_deployedAt = block.timestamp - 100 days; // Pretend 90+ days passed
for (uint256 i = 0; i < players.length; i++) {
playersToRewards[players[i]] = rewards[i];
}
}
function claimCut() public {
address player = msg.sender;
uint256 reward = playersToRewards[player];
if (reward <= 0) revert("No reward");
playersToRewards[player] = 0;
remainingRewards -= reward;
claimants.push(player);
i_token.transfer(player, reward);
}
function closePot() external onlyOwner {
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++) {
i_token.transfer(claimants[i], claimantCut);
}
}
}
function fund() external onlyOwner {
i_token.transfer(address(this), i_totalRewards);
}
}
contract POCDemo {
VulnerablePot public pot;
function deployPot() public returns (VulnerablePot) {
address[] memory players = new address[](10);
uint256[] memory rewards = new uint256[](10);
for (uint256 i = 0; i < 10; i++) {
players[i] = address(uint160(0x100 + i));
rewards[i] = 100;
}
pot = new VulnerablePot(players, rewards, 1000);
pot.fund();
return pot;
}
function demonstrateBug() public {
pot.claimCut();
pot.claimCut()
pot.claimCut();
uint256 beforeCloseBalanceOwner = TestToken(pot.i_token()).balanceOf(address(this));
uint256 remainingBefore = pot.remainingRewards();
pot.closePot();
uint256 managerCut = 700 / 10;
uint256 expectedLeftoverPerClaimant = (700 - 70) / 3; claimant (630 total)
uint256 buggyLeftoverPerClaimant = (700 - 70) / 10;
uint256 tokensProcessed = 70 + (63 * 3);
uint256 tokensLost = 700 - tokensProcessed;
console.log("Remaining before close:", remainingBefore);
console.log("Manager cut (10%):", managerCut);
console.log("Buggy cut per claimant (intended /10 players):", 63);
console.log("Total distributed to claimants:", 63 * 3);
console.log("TOKENS LOST FOREVER:", tokensLost);
}
}

Recommended Mitigation

- uint256 claimantCut = (remainingRewards - managerCut) / i_players.length;
+ uint256 leftover = remainingRewards - managerCut;
uint256 claimantCut = claimants.length > 0 ? leftover / claimants.length : 0;
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!