MyCut

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

Claims Remain Possible After Pot Closure, Causing Permanent Fund Lock.

Root + Impact

Description

The protocol specification implies the following lifecycle:
Created
Funded
90-day claim period
Closed
No more claims
However, https://github.com/CodeHawks-Contests/ai-mycut/blob/819134663950999ca5ab29a91eeceddb80274743/src/Pot.sol#L37 has no check preventing claims after the pot has been finalized.
function (https://github.com/CodeHawks-Contests/ai-mycut/blob/819134663950999ca5ab29a91eeceddb80274743/src/Pot.sol#L37) public contains no validation such as potClosed == false
Since redistribution has already occurred, remainingRewards accounting no longer reflects reality, causing tokens to become permanently stranded inside the contract.
// Root cause in the codebase with @> marks to highlight the relevant section

Risk

Likelihood:

Highly likely, as it breaks the intended protocol lifecycle.

Impact:

reward accounting becomes inconsistent
users receive rewards after redistribution has completed
remaining reward calculations become invalid
tokens become permanently trapped
the contest never truly reaches a final immutable state

Proof of Concept

Observed during testing:
Owner creates contest.
Users partially claim.
Owner waits 90 days.
Owner calls closeContest().
Redistribution executes.
Another eligible player successfully calls
claimCut()
function test_claimAfterPotClosed() public {
address contests;
vm.startPrank(owner);
contests = _contestManager.createContest(players, rewards, IERC20(ERC20Mock(weth)), 1000);
_contestManager.fundContest(0);
vm.stopPrank();
uint256 player6Balb4 = weth.balanceOf(player6);
console.log("Player 6 Balance before Claim:", player6Balb4);
//player 6 is claims 100
vm.startPrank(player6);
Pot(contests).claimCut();
vm.stopPrank();
uint256 player6Bal = weth.balanceOf(player6);
console.log("Player 6 Balance After Claim:", player6Bal);
vm.warp(100 days);
vm.startPrank(owner);
_contestManager.closeContest(contests);
vm.stopPrank();
uint256 player4BalB4 = weth.balanceOf(player4);
console.log("Player 4 Balance Before Claim:", player4BalB4);
// Player 6 gets a share of the pot after closePot()
uint256 player6BalAF = weth.balanceOf(player6);
console.log("Player 6 Balance After Pot Closed:", player6BalAF);
//player 4 can stil claim after pot closed
vm.startPrank(player4);
Pot(contests).claimCut();
vm.stopPrank();
uint256 player4Bal = weth.balanceOf(player4);
console.log("Player 4 Balance After Claim:", player4Bal);
assertGt(player4Bal,player4BalB4);
console::log("Player 4 Balance Before Claim:", 0) [staticcall]
├─ [0] console::log("Player 6 Balance After Pot Closed:", 181) [staticcall]
│ └─ ← [Stop]
├─ [0] VM::startPrank(player4: [0x4AB770c39b683c352367fC63C468F18b201d4bd0])
│ └─ ← [Return]
├─ [48123] Pot::claimCut()
│ ├─ [23750] ERC20Mock::transfer(player4: [0x4AB770c39b683c352367fC63C468F18b201d4bd0], 100)
│ │ ├─ emit Transfer(from: Pot: [0x104fBc016F4bb334D775a19E8A6510109AC63E00], to: player4: [0x4AB770c39b683c352367fC63C468F18b201d4bd0], value: 100)
│ │ └─ ← [Return] true
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [938] ERC20Mock::balanceOf(player4: [0x4AB770c39b683c352367fC63C468F18b201d4bd0]) [staticcall]
│ └─ ← [Return] 100
├─ [0] console::log("Player 4 Balance After Claim:", 100)
}
```

Recommended Mitigation

Track closure status and Reject claims after closure.
bool public closed; + add this code
if (closed)
revert PotClosed(); + add this code
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 1 day 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!