MyCut

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

M-03 Unchecked ERC20 Transfer Return Value

Root + Impact:

Description:

The _transferReward() function uses IERC20.transfer() but does not check the return value. Standard ERC20 transfer() returns a boolean indicating success or failure. If the transfer fails silently (returns false), the contract state is still updated (in claimCut()), but the user receives nothing.

While some tokens revert on failure, the ERC20 standard specifies that transfer() should return false on failure. Not checking the return value means the contract cannot handle failed transfers properly.

@> function _transferReward(address player, uint256 reward) internal {
@> i_token.transfer(player, reward);
}

This is called from claimCut() which updates state before the transfer (good CEI pattern), but if the transfer fails, the user's state is already updated (playersToRewards[player] = 0, remainingRewards -= reward, claimants.push(player)), and they cannot claim again.

Risk:

Likelihood: Medium - Depends on the token used; some tokens may fail transfers without reverting.

Impact:

  • Users may lose their rewards if transfer fails silently

  • State is updated but no tokens are sent

  • Users cannot reclaim lost rewards (state already updated)

Proof of Concept:

This POC demonstrates that when transfer() fails silently (returns false), the player's state is already updated (playersToRewards[player] = 0), but they receive no tokens and cannot claim again.

function testExploitVulnerability() public {
// Make transfers fail
token.setFailTransfers(true);
// Player claims their cut
vm.prank(player1);
pot.claimCut();
// Check that player's reward is now 0 (state updated)
uint256 playerRewardAfter = pot.checkCut(player1);
console.log("Player reward after claimCut (state):", playerRewardAfter);
assertEq(playerRewardAfter, 0); // State was updated
// But the transfer failed silently!
uint256 playerTokenBalance = token.balanceOf(player1);
console.log("Player token balance after claimCut:", playerTokenBalance);
assertEq(playerTokenBalance, 0); // Player got nothing!
console.log("BUG: Player state updated but received no tokens!");
}

Recommended Mitigation:

Using OpenZeppelin's SafeERC20 library ensures that failed transfers revert the transaction, preventing state updates when tokens are not actually sent.

+ import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
+ import {SafeERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
contract Pot is Ownable(msg.sender) {
+ using SafeERC20 for IERC20;
// ... existing code ...
function _transferReward(address player, uint256 reward) internal {
- i_token.transfer(player, reward);
+ i_token.safeTransfer(player, reward);
}
}
Updates

Lead Judging Commences

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