MyCut

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

No SafeERC20 usage causes silent failures and reverts with non-standard tokens like USDT

Root + Impact

Description

  • The protocol uses IERC20.transfer() and IERC20.transferFrom() directly without OpenZeppelin's SafeERC20 wrapper throughout all token operations.

  • Tokens like USDT that do not return a boolean value will cause the ABI decoder to revert, making the entire protocol incompatible. Tokens that return false on failure instead of reverting will be silently ignored — state updates proceed but tokens never move, leading to accounting insolvency.

// Pot.sol::_transferReward()
function _transferReward(address player, uint256 reward) internal {
@> i_token.transfer(player, reward); // no return value check, no SafeERC20
}
// ContestManager.sol::fundContest()
function fundContest(uint256 index) public onlyOwner {
Pot pot = Pot(contests[index]);
IERC20 token = pot.getToken();
uint256 totalRewards = contestToTotalRewards[address(pot)];
if (token.balanceOf(msg.sender) < totalRewards) {
revert ContestManager__InsufficientFunds();
}
@> token.transferFrom(msg.sender, address(pot), totalRewards); // no return value check
}

Risk

Likelihood:

  • Any contest created with USDT (the most widely used stablecoin) as the reward token will fail completely because USDT's transfer() does not return a boolean, causing Solidity's ABI decoder to revert.

  • Tokens that return false on failure (instead of reverting) will silently pass, causing the protocol to believe funds were sent when they were not.

Impact:

  • With USDT or similar non-standard tokens: all claimCut(), closePot(), and fundContest() calls revert — the entire protocol is bricked for that token.

  • With tokens returning false on failure: claimCut() zeroes a player's reward mapping and decrements remainingRewards even though no tokens moved. The player loses their reward permanently.

  • fundContest() marks the Pot as funded with totalRewards but the Pot receives nothing, making all subsequent claims fail.

Proof of Concept

The following test shows that when the Pot uses a non-standard token like USDT (which does not return a boolean from transfer()), the ABI decoder fails and all claim operations revert, making the protocol completely unusable for that token.

// Using a token like USDT that doesn't return bool from transfer:
function testUSDTTransferReverts() public {
// USDT's transfer signature: transfer(address,uint256) returns nothing
// Solidity expects: transfer(address,uint256) returns (bool)
// ABI decode fails -> revert
// Deploy contest with USDT as reward token
address[] memory players = new address[](1);
players[0] = address(0x1);
uint256[] memory rewards = new uint256[](1);
rewards[0] = 100e6; // 100 USDT
Pot pot = new Pot(players, rewards, IERC20(USDT_ADDRESS), 100e6);
// Fund the pot with actual USDT
// ...
// Player tries to claim
vm.prank(address(0x1));
vm.expectRevert(); // ABI decode failure
pot.claimCut();
}

Recommended Mitigation

Import OpenZeppelin's SafeERC20 library in both Pot.sol and ContestManager.sol, and replace all transfer()/transferFrom() calls with safeTransfer()/safeTransferFrom(). This handles tokens with missing return values (USDT) and tokens that return false on failure by reverting the transaction.

+ import {SafeERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
contract Pot is Ownable(msg.sender) {
+ using SafeERC20 for IERC20;
// ...
function _transferReward(address player, uint256 reward) internal {
- i_token.transfer(player, reward);
+ i_token.safeTransfer(player, reward);
}
}
```diff
+ import {SafeERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
contract ContestManager is Ownable {
+ using SafeERC20 for IERC20;
function fundContest(uint256 index) public onlyOwner {
// ...
- token.transferFrom(msg.sender, address(pot), totalRewards);
+ token.safeTransferFrom(msg.sender, address(pot), totalRewards);
}
}
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!