MyCut

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

Silent Failure in _transferReward Wipes User Claims

Description

The _transferReward function uses the standard ERC20 transfer method without checking its boolean return value. According to the protocol's scope, it supports all standard ERC20 tokens. Some ERC20 tokens (e.g., ZRX) do not revert the transaction if the sender has an insufficient balance or if the transfer fails; instead, they simply return false.

In Pot.sol, the claimCut function optimistically updates the user's state by zeroing out their reward balance (playersToRewards[player] = 0;) before making the external call. If the external call to transfer returns false, the transaction will not revert. Consequently, the user's state is completely wiped, but no tokens are actually transferred to their wallet.

Risk

Users will permanently lose their allocated rewards. Since their internal balance is set to 0 and they are added to the claimants array, it becomes impossible for them to retry the claim, resulting in a permanent loss of funds.

Proof of Concept

The following Foundry test demonstrates how a token that silently fails will successfully wipe the user's state without transferring any actual value.

Solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test, console} from "forge-std/Test.sol";
import {Pot} from "../src/Pot.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
// Mock token that fails silently
contract SilentFailToken is ERC20 {
constructor() ERC20("Silent", "SLT") {}
function transfer(address to, uint256 amount) public override returns (bool) {
return false; // Silently fails instead of reverting
}
}
contract PotTest is Test {
Pot pot;
SilentFailToken token;
address player1 = address(0x1);
function setUp() public {
token = new SilentFailToken();
address[] memory players = new address[](1);
players[0] = player1;
uint256[] memory rewards = new uint256[](1);
rewards[0] = 100 ether;
pot = new Pot(players, rewards, token, 100 ether);
}
function test_SilentFailureWipesReward() public {
vm.startPrank(player1);
pot.claimCut();
vm.stopPrank();
// The transaction succeeds, and the contract state is wiped
uint256 remainingPlayerReward = pot.checkCut(player1);
assertEq(remainingPlayerReward, 0);
// However, the player received 0 tokens due to the silent failure
assertEq(token.balanceOf(player1), 0);
}
}

Recommended Mitigation

Utilize OpenZeppelin’s SafeERC20 wrapper to ensure that boolean return values are strictly checked and that the transaction reverts upon failure.

Solidity

import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
using SafeERC20 for IERC20;
function _transferReward(address player, uint256 reward) internal {
// Replace standard transfer with safeTransfer
i_token.safeTransfer(player, reward);
}
Updates

Lead Judging Commences

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