Root + Impact:
ContestManager.createContest() does not validate that the players array is non-empty before deploying a new Pot.
function createContest(
address[] memory players,
uint256[] memory rewards,
IERC20 token,
uint256 totalRewards
)
public
onlyOwner
returns (address)
{
Pot pot = new Pot(players, rewards, token, totalRewards);
contests.push(address(pot));
contestToTotalRewards[address(pot)] = totalRewards;
return address(pot);
}
Later, Pot.closePot() divides by i_players.length.
uint256 claimantCut = (remainingRewards - managerCut) / i_players.length;
If i_players.length == 0, closing the pot reverts due to division by zero. The funded rewards become stuck because closePot() cannot complete.
Description:
The protocol allows the owner/admin to create multiple reward pots. Each pot should contain a list of authorized claimants and their rewards.
However, there is no validation that at least one claimant exists. If a pot is created with an empty players array and later funded, no user can claim from it. After 90 days, the owner attempts to close the pot, but closePot() tries to divide by i_players.length.
Since the length is zero, the transaction reverts every time.
Because there is no emergency withdrawal or alternate close path, the funded tokens remain locked in the Pot.
Risk:
Likelihood is low because the owner/admin is trusted. This would most likely happen due to a configuration mistake or bad off-chain input.
Impact is medium if it occurs, because the pot cannot be closed and the funded ERC20 rewards are stuck permanently.
Proof of Concept:
Setup:
players.length = 0
rewards.length = 0
totalRewards = 10 ether
Flow:
1. Owner creates a contest with no players.
2. Owner funds the contest with 10 ether worth of ERC20 tokens.
3. 90 days pass.
4. Owner calls closeContest().
5. Pot.closePot() calculates claimantCut by dividing by i_players.length.
6. Since i_players.length is 0, the call reverts.
7. The pot cannot be closed and the tokens remain stuck.
Foundry-style test:
function testClosePotRevertsWithZeroPlayers() public {
address[] memory players = new address[](0);
uint256[] memory rewards = new uint256[](0);
uint256 totalRewards = 10 ether;
address contest = contestManager.createContest(
players,
rewards,
IERC20(weth),
totalRewards
);
ERC20Mock(weth).approve(address(contestManager), totalRewards);
contestManager.fundContest(0);
vm.warp(block.timestamp + 91 days);
vm.expectRevert();
contestManager.closeContest(contest);
assertEq(ERC20Mock(weth).balanceOf(contest), totalRewards);
}
Recommended Mitigation:
Validate input when creating a contest. A pot should not be created with an empty player list.
+ error ContestManager__InvalidPlayersLength();
+ error ContestManager__InvalidRewardsLength();
function createContest(
address[] memory players,
uint256[] memory rewards,
IERC20 token,
uint256 totalRewards
)
public
onlyOwner
returns (address)
{
+ if (players.length == 0) revert ContestManager__InvalidPlayersLength();
+ if (players.length != rewards.length) revert ContestManager__InvalidRewardsLength();
Pot pot = new Pot(players, rewards, token, totalRewards);
contests.push(address(pot));
contestToTotalRewards[address(pot)] = totalRewards;
return address(pot);
}
For extra safety, the same invariant can also be enforced in the Pot constructor, so direct deployments of Pot cannot create invalid pots either.