[H-02] A Portion of Funds is Lost Because the ClaimantCut Calculations Are Incorrect
Summary
The calculations for distributing rewards among claimants in the Pot::closePot
function are incorrect. Instead of dividing the remaining rewards (after subtracting the manager's cut) by the number of claimants, the function divides by i_players.length
, which causes some funds to be left unclaimed in the contract.
Vulnerability Details
In the Pot::closePot
function, the managerCut
is correctly calculated as one-tenth of the remaining rewards. However, the claimantCut
calculation incorrectly uses i_players.length
instead of claimants.length
. This discrepancy results in some rewards being left in the contract, as the actual number of claimants is not correctly accounted for.
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);
}
}
}
Impact
A portion of the rewards will remain unclaimed in the contract, leading to a permanent loss of funds.
Proof of Concept
Add this to the following test suite to reproduce the issue:
function test_my_wrongClaimantCutCalculations()
public
mintAndApproveTokens
{
address[] memory players_ = new address[](15);
uint256[] memory rewards_ = new uint256[](15);
for (uint256 i; i < 15; i++) {
players_[i] = address(uint160(i + 10));
rewards_[i] = 1 ether;
}
vm.startPrank(user);
contest = ContestManager(conMan).createContest(
players_,
rewards_,
IERC20(ERC20Mock(weth)),
15 ether
);
ContestManager(conMan).fundContest(0);
vm.stopPrank();
for (uint256 i; i < 11; i++) {
vm.prank(players_[i]);
Pot(contest).claimCut();
}
vm.warp(91 days);
vm.startPrank(user);
ContestManager(conMan).closeContest(contest);
vm.stopPrank();
assertEq(weth.balanceOf(conMan), (1000e18 - 15e18) + 0.4 ether);
assertEq(weth.balanceOf(players_[1]), 1.24 ether);
assertEq(weth.balanceOf(contest), 0.96 ether);
}
Tools Used
Manual Review
Recommendations
Change the division in the claimantCut calculation from i_players.length
to claimants.length
to ensure that the rewards are distributed correctly among the actual number of claimants.
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;
+ claimants.length;
for (uint256 i = 0; i < claimants.length; i++) {
_transferReward(claimants[i], claimantCut);
}
}
}