MyCut

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

Unchecked ERC20 transfer in fundContest allows contests to appear funded when they're not

Root + Impact

Description

  • Describe the normal behavior in one or more sentences

  • The normal behavior should be: funding either succeeds (tokens transferred) or reverts.

  • Explain the specific issue or problem in one or more sentences

  • The ContestManager.fundContest() function calls token.transferFrom() without checking the return value.

// Root cause in the codebase with @> marks to highlight the relevant section
// ContestManager.sol - Lines 28-38
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); // @audit Return value ignored
}

Risk

Likelihood:

  • Reason 1

    • Given USDT's popularity and the balance-only check, this is likely to occur.

    • Using USDT (very common stablecoin)

    • Manager's allowance is insufficient (balance check doesn't verify approval)


Impact:

  • Impact 1

    Protocol appears broken when claims don't work

  • No on-chain indication that funding failed

Proof of Concept

// Test: USDT with insufficient allowance
function testInsufficientAllowanceCreatesUnfundedContest() public {
// Setup contest
address[] memory players = new address[](2);
players[0] = alice;
players[1] = bob;
uint256[] memory rewards = new uint256[](2);
rewards[0] = 5000;
rewards[1] = 5000;
// Manager creates contest for 10,000 USDT
vm.startPrank(owner);
address pot = contestManager.createContest(
players,
rewards,
usdt, // Using USDT
10000
);
// Manager has 100,000 USDT (balance check passes)
assertEq(usdt.balanceOf(owner), 100000);
// But manager only approved 1,000 USDT (MISTAKE)
usdt.approve(address(contestManager), 1000);
// Try to fund contest
contestManager.fundContest(0); // Succeeds! (No revert)
vm.stopPrank();
// Check: Pot received NO tokens
assertEq(usdt.balanceOf(pot), 0);
// But contract thinks it's funded
assertEq(contestManager.getContestRemainingRewards(pot), 10000);
// Alice tries to claim
vm.prank(alice);
Pot(pot).claimCut(); // "Succeeds" but Alice receives nothing
// Alice got 0 USDT
assertEq(usdt.balanceOf(alice), 0);
// Alice can't claim again (marked as complete)
vm.prank(alice);
vm.expectRevert(Pot__RewardNotFound.selector);
Pot(pot).claimCut();
// Result: Contest appears funded but is actually empty
// All players lose their rewards
}

Recommended Mitigation

- remove this code// ContestManager.sol
pragma solidity ^0.8.20;
import {Pot} from "./Pot.sol";
import {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol";
import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
+ 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 {
Pot pot = Pot(contests[index]);
IERC20 token = pot.getToken();
uint256 totalRewards = contestToTotalRewards[address(pot)];
- if (token.balanceOf(msg.sender) < totalRewards) {
- revert ContestManager__InsufficientFunds();
- }
+ // Check both balance AND allowance
+ require(
+ token.balanceOf(msg.sender) >= totalRewards,
+ "Insufficient balance"
+ );
+ require(
+ token.allowance(msg.sender, address(this)) >= totalRewards,
+ "Insufficient allowance"
+ );
- token.transferFrom(msg.sender, address(pot), totalRewards);
+ // Use SafeERC20 (reverts on false return)
+ token.safeTransferFrom(msg.sender, address(pot), totalRewards);
+
+ // Verify pot actually received the funds
+ require(
+ token.balanceOf(address(pot)) >= totalRewards,
+ "Pot funding verification failed"
+ );
+
+ emit ContestFunded(address(pot), totalRewards);
}
}
+ add this code
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!