MyCut

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

Incorrect reward distribution in closePot() allows duplicate payments to claimants

Description

The closePot() function distributes remaining rewards to all claimants without tracking who has already claimed during the normal period. This causes users who claimed early to receive duplicate payments, while users who waited receive less than their fair share.

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);
// Does not check if claimants already received rewards
uint256 claimantCut = (remainingRewards - managerCut) / i_players.length;
for (uint256 i = 0; i < claimants.length; i++) {
_transferReward(claimants[i], claimantCut);
}
}
}

The function calculates claimantCut by dividing remaining rewards among all players, then pays this amount to every claimant—including those who already claimed. Additionally, it fails to update remainingRewards after transferring the manager cut, violating the solvency invariant.

Risk

Severity: Critical

Users who claim early receive their original share plus an additional payment from closePot(), while users who wait receive only the reduced closePot() amount. In a scenario with 1,000 tokens and 4 players where 2 claim early (250 each), closePot() calculates 112.5 per claimant from the remaining 450. Early claimers receive 362.5 total (45% extra), while late claimers receive only 112.5 (55% less).

The failure to update remainingRewards creates an accounting discrepancy where pot.balance < remainingRewards, breaking protocol invariants and potentially causing other operations to fail.

Proof of concept

The following trace demonstrates the solvency invariant violation:

BEFORE closePot():
├─ Pot balance: 1000000000000000000000 [1e21]
├─ remainingRewards: 1000000000000000000000 [1e21]
└─ Balance >= remainingRewards ✓
AFTER closePot():
├─ MockERC20::transfer(ContestManager, 100000000000000000000 [1e20])
├─ Pot balance: 900000000000000000000 [9e20]
├─ remainingRewards: 1000000000000000000000 [1e21] (unchanged)
└─ FAILED: Balance < remainingRewards (900 < 1000)

Recommended Mitigation

Track which users have claimed and only distribute to unclaimed addresses:

mapping(address => bool) public hasClaimed;
function claim() external {
// ... existing claim logic ...
hasClaimed[msg.sender] = true;
}
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);
+ remainingRewards -= managerCut;
// Count unclaimed users
uint256 unclaimedCount = 0;
for (uint256 i = 0; i < claimants.length; i++) {
if (!hasClaimed[claimants[i]]) unclaimedCount++;
}
if (unclaimedCount > 0) {
uint256 claimantCut = remainingRewards / unclaimedCount;
for (uint256 i = 0; i < claimants.length; i++) {
if (!hasClaimed[claimants[i]]) {
_transferReward(claimants[i], claimantCut);
}
}
}
}
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 6 days 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!