MyCut

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

ERC-777 token callback during claimCut enables cross-function reentrancy

Root + Impact

Description

  • claimCut() follows the Checks-Effects-Interactions pattern for its own state (zeroing playersToRewards before transfer), which prevents classic single-function reentrancy. However, if the token is an ERC-777 (which is backward-compatible with ERC-20), the transfer call triggers a tokensReceived hook on the recipient.

  • During this callback, remainingRewards has been decremented and claimants has been updated, but other players' rewards are still intact. A malicious contract could exploit cross-function interactions during this window.

// Pot.sol::claimCut()
function claimCut() public {
address player = msg.sender;
uint256 reward = playersToRewards[player];
if (reward <= 0) {
revert Pot__RewardNotFound();
}
playersToRewards[player] = 0;
remainingRewards -= reward;
claimants.push(player);
@> _transferReward(player, reward); // ERC-777 tokensReceived callback here
}

Risk

Likelihood:

  • Requires the reward token to be ERC-777 compatible (e.g., imBTC). ERC-777 tokens are less common but are backward-compatible with ERC-20, so they could be passed as the token parameter.

  • The claimCut itself is safe against re-entry (reward already zeroed), but cross-function reentrancy into closePot or view functions reading remainingRewards during the callback window could be exploited.

Impact:

  • Limited practical exploitation since closePot requires onlyOwner and the claimer is unlikely to be the owner. The primary risk is read-only reentrancy where external contracts reading getRemainingRewards() during the callback get stale data.

  • If the protocol is integrated with other DeFi protocols that query Pot state, the inconsistent state during callback could be leveraged.

Proof of Concept

The following describes the theoretical attack path. If the reward token is ERC-777 compatible, the tokensReceived hook fires during _transferReward. During this callback window, the caller could attempt to re-enter other Pot functions. While claimCut itself is safe (reward already zeroed), cross-function re-entry into closePot or read-only reentrancy via getRemainingRewards() could yield incorrect results for integrated protocols.

// Theoretical: if player is also the owner (ContestManager)
// During claimCut's token transfer callback, re-enter closePot()
// remainingRewards has been decremented but closePot doesn't check a 'closed' flag

Recommended Mitigation

Inherit OpenZeppelin's ReentrancyGuard and apply the nonReentrant modifier to both claimCut() and closePot(). This prevents all reentrancy variants (single-function, cross-function, and read-only) by locking the contract during execution of any guarded function.

+ import {ReentrancyGuard} from "lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol";
- contract Pot is Ownable(msg.sender) {
+ contract Pot is Ownable(msg.sender), ReentrancyGuard {
- function claimCut() public {
+ function claimCut() public nonReentrant {
- function closePot() external onlyOwner {
+ function closePot() external onlyOwner nonReentrant {
Updates

Lead Judging Commences

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