MyCut

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

Unchecked ERC20 transfer results allow silent payment failures

Root + Impact

Description

  • Normal behavior: funding and payouts should only update state when token transfers succeed.

  • Issue: `transferFrom and transfer` return values are ignored, allowing failures to go unnoticed and state to advance.

function fundContest(uint256 index) public onlyOwner {
...
@> token.transferFrom(msg.sender, address(pot), totalRewards);
}
function closePot() external onlyOwner {
...
@> i_token.transfer(msg.sender, managerCut);
...
}
function _transferReward(address player, uint256 reward) internal {
@> i_token.transfer(player, reward);
}

Risk

Likelihood:

  • A token returns `false` instead of reverting on failure.

  • Transfers fail due to balance/allowance constraints while state updates proceed.

Impact:

  • Funding/claims appear successful but no tokens move.

  • State records claims as paid, leaving funds stuck or unclaimable.

Proof of Concept

contract FalseReturnERC20 is IERC20 {
string public name = "FalseToken";
string public symbol = "FALSE";
uint8 public decimals = 18;
uint256 public override totalSupply;
mapping(address => uint256) private balances;
mapping(address => mapping(address => uint256)) private allowances;
function mint(address to, uint256 amount) external {
balances[to] += amount;
totalSupply += amount;
emit Transfer(address(0), to, amount);
}
function balanceOf(address account) external view override returns (uint256) {
return balances[account];
}
function allowance(address owner, address spender) external view override returns (uint256) {
return allowances[owner][spender];
}
function approve(address spender, uint256 amount) external override returns (bool) {
allowances[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transfer(address, uint256) external pure override returns (bool) {
return false;
}
function transferFrom(address, address, uint256) external pure override returns (bool) {
return false;
}
}
function testUncheckedTransferReturnValues() public {
address owner = makeAddr("owner");
address p1 = makeAddr("p1");
ContestManager manager = _deployManager(owner);
FalseReturnERC20 token = new FalseReturnERC20();
token.mint(owner, 100);
vm.prank(owner);
token.approve(address(manager), 100);
address[] memory players = new address[](1);
players[0] = p1;
uint256[] memory rewards = new uint256[](1);
rewards[0] = 100;
vm.startPrank(owner);
address potAddr = manager.createContest(players, rewards, IERC20(token), 100);
manager.fundContest(0);
vm.stopPrank();
assertEq(token.balanceOf(potAddr), 0);
uint256 beforeBalance = token.balanceOf(p1);
vm.prank(p1);
Pot(potAddr).claimCut();
uint256 afterBalance = token.balanceOf(p1);
assertEq(afterBalance, beforeBalance);
assertEq(Pot(potAddr).checkCut(p1), 0);
}

Recommended Mitigation

- token.transferFrom(msg.sender, address(pot), totalRewards);
+ SafeERC20.safeTransferFrom(token, msg.sender, address(pot), totalRewards);
- i_token.transfer(msg.sender, managerCut);
+ SafeERC20.safeTransfer(i_token, msg.sender, managerCut);
- i_token.transfer(player, reward);
+ SafeERC20.safeTransfer(i_token, player, reward);
Updates

Lead Judging Commences

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