MyCut

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

No protocol profit and it may be economically impractical to close a contest

Impact: High
Likelihood: High

Root + Impact

Description

  • By design, a ContestManager.sol contract receives 10% reward from pot's remaining rewards after the owner closed a contest. Before that during 90 days the participants are allowed to claim their reward. A contract will receive nothing if all participants decided to claim their reward or if the remaining reward is too small to cover even gas expenses.

function closePot() external onlyOwner {
if (block.timestamp - i_deployedAt < 90 days) {
revert Pot__StillOpenForClaim();
}
if (remainingRewards > 0) {
@> uint256 managerCut = remainingRewards / managerCutPercent;
@> i_token.transfer(msg.sender, managerCut);
uint256 claimantCut = (remainingRewards - managerCut) / i_players.length;
for (uint256 i = 0; i < claimants.length; i++) {
_transferReward(claimants[i], claimantCut);
}
}
}

Risk

Likelihood:

  • The issue occurs when all players claimed their rewards before the contest is closed or when the manager's cut, which is 10% from remaining rewards, is too low to cover even gas expenses.

Impact:

  • The purpose of the protocol is to have profit for players and an owner. The current design allows financial loss or absence of any profit for an owner.

Proof of Concept

Pleade, add the following tests to TestMyCut.t.sol:

  1. test_economicallyImpracticalToCloseContest_whenAllPlayersClaimed shows that when all players claimed their reward, there is nothing left for managerCut.

  2. test_economicallyImpracticalToCloseContest_whenRewardIsLessThanGasUsed shows that managerCut is too small even to cover gas usage for closeContest() transaction.

function test_economicallyImpracticalToCloseContest_whenAllPlayersClaimed() public mintAndApproveTokens {
vm.startPrank(user);
rewards = [500, 500];
totalRewards = 1000;
contest = ContestManager(conMan).createContest(players, rewards, IERC20(ERC20Mock(weth)), totalRewards);
ContestManager(conMan).fundContest(0);
vm.stopPrank();
// all players claimed
uint256 playersAmount = players.length;
for(uint256 i = 0; i < playersAmount; i++){
vm.startPrank(players[i]);
Pot(contest).claimCut();
vm.stopPrank();
}
uint256 conManBalanceBeforeCloseContest = ERC20Mock(weth).balanceOf(conMan);
console.log("conManBalanceBeforeCloseContest ", conManBalanceBeforeCloseContest.toString());
vm.warp(91 days);
vm.startPrank(user);
ContestManager(conMan).closeContest(contest);
vm.stopPrank();
uint256 conManBalanceAfterCloseContest = ERC20Mock(weth).balanceOf(conMan);
console.log("conManBalanceAfterCloseContest ", conManBalanceAfterCloseContest.toString());
assertEq(conManBalanceAfterCloseContest - conManBalanceBeforeCloseContest, 0);
}
function test_economicallyImpracticalToCloseContest_whenRewardIsLessThanGasUsed() public mintAndApproveTokens {
vm.startPrank(user);
rewards = [500, 500];
totalRewards = 1000;
contest = ContestManager(conMan).createContest(players, rewards, IERC20(ERC20Mock(weth)), totalRewards);
ContestManager(conMan).fundContest(0);
vm.stopPrank();
// one player claimed
vm.startPrank(player1);
Pot(contest).claimCut(); // one reward is left -> is 10% is more than gas used?
vm.stopPrank();
vm.warp(91 days);
uint256 conManBalanceBeforeCloseContest = ERC20Mock(weth).balanceOf(conMan);
//gas
uint256 gasStart = gasleft();
vm.txGasPrice(960000000);//0.96 gwei per unit of gas
vm.prank(user);
ContestManager(conMan).closeContest(contest);
uint256 gasEnd = gasleft();
uint256 gasUsed = (gasStart - gasEnd) * tx.gasprice;
console.log("gas units used ", (gasStart - gasEnd).toString());
console.log("gasUsed ", gasUsed.toString());
uint256 comManBalanceAfterCloseContest = ERC20Mock(weth).balanceOf(conMan);
uint256 managerCut = comManBalanceAfterCloseContest - conManBalanceBeforeCloseContest;
console.log("managerCut ", managerCut.toString());
assertGt(gasUsed, managerCut);
}

Recommended Mitigation

  1. It may be acceptable to allow not all players to claim, so at least 1 reward of one player stays in the protocol.

  2. Specify the minimum reward to cover at least gas expenses by 10% from it (at least 0.0005 ether)

+ uint256 public constant MINIMUM_REWARD_AMOUNT = 0.0005 ether;
+ error Pot__RewardIsLessThanMininumAllowed();
+ error Pot__CannotClaimTheLastReward();
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);
for (uint256 i = 0; i < i_players.length; i++) {
+ require(i_rewards[i] >= MINIMUM_REWARD_AMOUNT, Pot__RewardIsLessThanMininumAllowed());
playersToRewards[i_players[i]] = i_rewards[i];
}
}
function claimCut() public {
address player = msg.sender;
uint256 reward = playersToRewards[player];
if (reward <= 0) {
revert Pot__RewardNotFound();
}
playersToRewards[player] = 0;
+ require((remainingRewards - reward) > 0, Pot__CannotClaimTheLastReward());
remainingRewards -= reward;
claimants.push(player);
_transferReward(player, reward);
}
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!