The Pot constructor's token-funding line is commented out, so a freshly created Pot holds zero tokens. Funding happens only if ContestManager.fundContest is called separately, and nothing enforces that it ever is. If a contest is created but not funded (or funded late), every claimCut call reverts on the token transfer, permanently denying all players their rewards.
createContest and fundContest are two independent owner actions. createContest deploys a Pot and records its total, but moves no tokens. The Pot's own constructor would have pulled funds, but that line is disabled. So the funded state of a Pot is entirely dependent on a second call that the protocol does not require or verify.
A Pot that is created and opened for claims while unfunded will revert on every claimCut (the internal i_token.transfer fails on a zero balance). Players believe they have rewards (playersToRewards is populated) but can never withdraw them. There is no recovery: claims are bricked until/unless the owner funds, and nothing guarantees that.
src/Pot.sol constructor (L30) - funding disabled:
// i_token.transfer(address(this), i_totalRewards);
src/ContestManager.sol createContest (L16+) deploys but does not fund:
Pot pot = new Pot(players, rewards, token, totalRewards);
contests.push(address(pot));
contestToTotalRewards[address(pot)] = totalRewards;
Funding is a separate, unenforced call - ContestManager.fundContest(index). Nothing ties claim-availability to funded state.
claimCut -> _transferReward -> i_token.transfer(player, reward) reverts when the Pot's balance is zero.
Full denial of service on all claims for any unfunded Pot.
Players' rewards are recorded but unwithdrawable.
test_HH5_unfunded_pot_dos_claims (create without funding, attempt claim). Real forge output:
[PASS] test_HH5_unfunded_pot_dos_claims()
Assertions: claimCut reverts for a valid player on an unfunded Pot; the Pot's token balance is 0.
Fund atomically at creation so a Pot cannot exist in an unfunded-but-open state - either re-enable a pull in the constructor (with the token approval flow) or have createContest perform the transferFrom in the same transaction. At minimum, gate claim availability on a funded check.
Note: this finding's precondition is owner negligence (the owner not funding), so the contest creator is trusted-but-bounded and a judge may tier the likelihood lower.
Foundry (forge), manual review.
````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); } ``` ````
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.