MyCut

AI First Flight #8
Beginner FriendlyFoundry
EXP
View results
Submission Details
Severity: medium
Valid

90-day timer starts at deployment — users lose claim window if funding delayed

Title: 90-day timer starts at deployment — users lose claim window if funding delayed
Impact: Medium. Delayed funding shortens effective claim period below advertised 90 days.
Likelihood: Medium. Requires contest creation and funding to occur in separate transactions.
Reference Files: repos/src/Pot.sol:28,37-47, repos/src/ContestManager.sol:16-38

Description

The Pot constructor captures block.timestamp as i_deployedAt and the 90-day claim window is computed from this value in closePot(). However, contest funding happens in a separate fundContest() transaction — tokens are not transferred during construction. If the owner delays funding for 30 days, users lose 30 days of their advertised 90-day claim period. Additionally, claimCut() has no check that the Pot has actually been funded — users attempting to claim before funding receive a generic ERC20 revert with no clear error message.

// Constructor: timer starts immediately, but no tokens transferred
constructor(...) {
i_deployedAt = block.timestamp; // timer starts NOW
// i_token.transfer(address(this), i_totalRewards); // COMMENTED OUT
}
// Funding: separate transaction, may happen much later
function fundContest(uint256 index) public onlyOwner {
token.transferFrom(msg.sender, address(pot), totalRewards);
}

The contest advertises a 90-day claim window, but the effective window is 90 days - (funding delay) — a shorter period that violates the protocol's stated guarantee.

Risk

Impact: Medium. If funding is delayed by 45 days, users have only 45 days to claim instead of 90. Users who attempt to claim during the first 45 days (before funding) see confusing reverts. Those who wait until after funding may miss the shortened window entirely.
Likelihood: Medium. createContest and fundContest are separate onlyOwner functions — the owner may create multiple contests before funding them, or funding may require governance approval causing delays. The protocol provides no guarantee that funding happens immediately.
With a 30-day funding delay, users lose one-third of their promised claim window, and early claim attempts fail with an opaque ERC20 revert.

Proof of Concept

function testTimerStarsBeforeFunding() public {
vm.prank(owner);
address pot = contestManager.createContest(players, rewards, token, 100);
// funding NOT called immediately
vm.warp(block.timestamp + 45 days); // 45 days pass without funding
vm.prank(owner); contestManager.fundContest(0); // fund late
vm.warp(block.timestamp + 46 days); // 91 total days since deployment
vm.prank(owner); contestManager.closeContest(pot); // succeeds — but users had only 1 day!
}

The PoC shows that with 45 days of funding delay, users effectively had 45 days less than the advertised 90-day window to claim their rewards.

Recommended Mitigation

Start the 90-day timer when fundContest is called, not at deployment. Add an isFunded flag to the Pot, set it during funding, and require it in claimCut() to give a clear error when claiming before funds arrive.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 1 day ago
Submission Judgement Published
Validated
Assigned finding tags:

[M-02] **[L-1] users can invoke `claimCut` prior to the contest being funded**

````markdown **Description:** It is possible that once the contest has been created, it is not necessarily funded at the same time, these are separate operations, which may result in users attempting to invoke `claimCut`, however there would be no funds and we would most likely get a `ERC20InsufficientBalance` error. Users have most probably assumed that at the time of claiming their cut that the contest is funded. The more insidious issue lies in the fact that the timer of 90 days begins when the Pot contract is constructed not when it's funded, hence if the contract is not funded at the time of creation, users will not be entitled to the whole 90 day duration claim period. **Impact:** Bad UX, as users would be able to attempt claim their cut but this would result in a reversion. **Proof of Concept:** The below test can be added to `TestMyCut.t.sol:TestMyCut` contracts test suite. **Recommended Mitigation:** We must ensure the contest is funded at the time it is created. Otherwise we should state a clearer error message. In the event where we want to give the users a more gracious error message, we could add the following changes which leverages a boolean to track if the Pot has been funded: ```diff contract Pot is Ownable(msg.sender) { /** Existing Code... */ + boolean private s_isFunded; // Ensure this is updated correctly when the contract is funded. function claimCut() public { + if (!s_isFunded) { + revert Pot__InsufficientFunds(); + } address player = msg.sender; uint256 reward = playersToRewards[player]; if (reward <= 0) { revert Pot__RewardNotFound(); } playersToRewards[player] = 0; remainingRewards -= reward; claimants.push(player); _transferReward(player, reward); } } ``` In the scenario where we want to ensure the contest is funded at the time of being created employ the following code. ```diff function createContest(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards) public onlyOwner returns (address) { // Create a new Pot contract Pot pot = new Pot(players, rewards, token, totalRewards); contests.push(address(pot)); contestToTotalRewards[address(pot)] = totalRewards; + fundContest(contests.length - 1); return address(pot); } - function fundContest(uint256 index) public onlyOwner { + function fundContest(uint256 index) internal 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); } ``` ````

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!