MyCut

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

H-05: Constructor Missing Token Transfer - Pots Deploy with Zero Balance

Root + Impact

Root Cause

In Pot.sol line 55, the token transfer to fund the pot at deployment is commented out:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol";
contract Pot is Ownable(msg.sender) {
// ... state variables ...
constructor(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards) {
i_players = players;
i_rewards = rewards;
i_token = token;
i_totalRewards = totalRewards;
remainingRewards = totalRewards;
i_deployedAt = block.timestamp;
// @> ROOT CAUSE: This line is commented out - pot never receives tokens at deployment
// i_token.transfer(address(this), i_totalRewards);
for (uint256 i = 0; i < i_players.length; i++) {
playersToRewards[i_players[i]] = i_rewards[i];
}
}
// ... rest of contract
}

Impact

Impact Assessment

Dimension Assessment
Fund Loss None directly (tokens stay with owner)
Functionality Complete failure - no claims possible
User Experience All claims revert with transfer error
Protocol State Broken - pots appear funded but aren't

Description

The Pot contract constructor has the token transfer line commented out, preventing the contract from receiving its initial funding at deployment time.

// Line 55 in Pot.sol
// i_token.transfer(address(this), i_totalRewards); // COMMENTED OUT!

This means every deployed Pot starts with zero token balance, making all claimCut() calls fail unless the owner manually calls fundContest() via ContestManager.

Risk

Likelihood:

  • Reason 1 // Describe WHEN this will occur (avoid using "if" statements)

  • Reason 2

Impact:

  • Every deployed Pot starts with 0 token balance despite remainingRewards being set to totalRewards

  • All claimCut() calls revert because _transferReward() calls i_token.transfer() on empty contract

  • Protocol is completely non-functional without manual fundContest() call

  • Owner must remember to call fundContest() for every pot - easy to miss

  • Creates confusing UX where contract state says "300 tokens available" but balance is 0

Proof of Concept

Test: test_POC_ConstructorMissingTokenTransfer()

function test_POC_ConstructorMissingTokenTransfer() public {
address[] memory players = new address[](2);
players[0] = player1;
players[1] = player2;
uint256[] memory rewards = new uint256[](2);
rewards[0] = 100;
rewards[1] = 200;
vm.startPrank(user);
potAddr = conMan.createContest(players, rewards, IERC20(weth), 300);
pot = Pot(potAddr);
vm.stopPrank();
// Pot has 0 balance - constructor didn't pull tokens!
assertEq(weth.balanceOf(potAddr), 0);
// Player tries to claim but pot has no funds
vm.startPrank(player1);
vm.expectRevert(); // Will revert on transfer
pot.claimCut();
vm.stopPrank();
}

Result: ✅ PASSED - Confirms pot balance = 0, claim reverts

Execution Trace

[PASS] test_POC_ConstructorMissingTokenTransfer() (gas: 985846)

Recommended Mitigation

The constructor has been updated to replace a direct transfer call with transferFrom.

Original issue: Using transfer(address(this), totalRewards) assumed the Pot contract itself already held the required tokens or that the tokens were sent beforehand, which is error-prone and could lead to deployment succeeding without the contract actually being funded.

Mitigation: The transferFrom(msg.sender, address(this), totalRewards) call pulls the exact reward tokens directly from the caller (msg.sender) at deployment time. This guarantees that the Pot contract is fully funded immediately upon creation, provided that the caller (e.g., ContestManager) has previously approved the Pot contract to spend the required token amount. This approach enforces a secure, atomic funding mechanism and prevents deployment with insufficient or missing funds.

// Pot.sol constructor
constructor(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards) {
i_players = players;
i_rewards = rewards;
i_token = token;
i_totalRewards = totalRewards;
remainingRewards = totalRewards;
i_deployedAt = block.timestamp;
- // i_token.transfer(address(this), i_totalRewards);
+ i_token.transferFrom(msg.sender, address(this), i_totalRewards);
for (uint256 i = 0; i < i_players.length; i++) {
playersToRewards[i_players[i]] = i_rewards[i];
}
}

Note: Caller (ContestManager.createContest) must approve the Pot contract for totalRewards before deployment, or use transferFrom from the owner.

Updates

Lead Judging Commences

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