MyCut

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

`ContestManager::fundContest` assumes the Pot receives exactly `totalRewards`, so with a fee-on-transfer token the last claimer cannot claim

Root + Impact

Description

  • fundContest sends totalRewards to the Pot, and the Pot promises exactly that amount to its players.

  • With a fee-on-transfer token the Pot receives less than totalRewards, but remainingRewards is still initialised to totalRewards. Claims succeed until the balance runs out, and then the last claimer's transfer reverts.

function fundContest(uint256 index) public onlyOwner {
...
@> token.transferFrom(msg.sender, address(pot), totalRewards); // Pot receives less
}
constructor(...) {
...
@> remainingRewards = totalRewards; // assumes the full amount

Risk

Likelihood:

  • When the contest uses a token that takes a fee on transfer (e.g. STA, PAXG, or USDT if its fee is ever enabled). The README restricts compatibility to standard ERC20 tokens, so this is unlikely.

Impact:

  • The last player(s) to claim cannot receive their reward: the transfer reverts for lack of balance.

Proof of Concept

The token burns 1% on transfer. The Pot receives 99 instead of 100, and after player1 takes 65 only 34 are left for player2's 35.

contract FeeOnTransferToken is ERC20 {
constructor() ERC20("Fee", "FEE") {}
function mint(address to, uint256 amount) external { _mint(to, amount); }
function _update(address from, address to, uint256 value) internal override {
if (from != address(0) && to != address(0)) {
uint256 fee = value / 100;
super._update(from, address(0), fee);
value -= fee;
}
super._update(from, to, value);
}
}
function testFeeOnTransferBlocksLastClaim() public {
FeeOnTransferToken token = new FeeOnTransferToken();
token.mint(admin, 100);
address[] memory players = new address[](2);
players[0] = player1; players[1] = player2;
uint256[] memory rewards = new uint256[](2);
rewards[0] = 65; rewards[1] = 35;
vm.startPrank(admin);
token.approve(address(conMan), 100);
Pot pot = Pot(conMan.createContest(players, rewards, IERC20(address(token)), 100));
conMan.fundContest(0);
vm.stopPrank();
assertEq(token.balanceOf(address(pot)), 99);
vm.prank(player1);
pot.claimCut();
vm.prank(player2);
vm.expectRevert(); // 34 < 35
pot.claimCut();
}

Recommended Mitigation

Check the amount the Pot actually received, and revert if it is short.

+ uint256 balanceBefore = token.balanceOf(address(pot));
token.transferFrom(msg.sender, address(pot), totalRewards);
+ if (token.balanceOf(address(pot)) - balanceBefore != totalRewards) {
+ revert ContestManager__InsufficientFunds();
+ }
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!