Rock Paper Scissors

First Flight #38
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: high
Invalid

Reentrancy Refund Drain Vulnerability in RockPaperScissors.sol::cancelGame

Summary

The cancelGame function is vulnerable to a reentrancy attack, allowing a malicious playerA to repeatedly call cancelGame during ETH refunds in _cancelGame. Since the game state remains GameState.Created after playerB joins, playerA can drain the contract's ETH, including playerB's bet, before playerB receives a refund.

Vulnerability Details

The cancelGame function calls _cancelGame, which transfers ETH to playerA via .call. Without a reentrancy guard, a malicious playerA can reenter cancelGame during the transfer. The state remains GameState.Created after playerB joins, allowing playerA to cancel the game and exploit reentrancy to siphon multiple refunds, including playerB's funds.

Vulnerable Code:

function _cancelGame(uint256 _gameId) internal {
Game storage game = games[_gameId];
game.state = GameState.Cancelled;
if (game.bet > 0) {
(bool successA,) = game.playerA.call{value: game.bet}("");
require(successA, "Transfer to player A failed");
if (game.playerB != address(0)) {
(bool successB,) = game.playerB.call{value: game.bet}("");
require(successB, "Transfer to player B failed");
}
}
// ... rest of function ...
}

Attack Scenario:

  1. Malicious playerA creates a game with an ETH bet.

  2. playerB joins, state stays GameState.Created.

  3. playerA calls cancelGame, triggering _cancelGame.

  4. During playerA's ETH refund, their receive function reenters cancelGame, draining additional ETH, including playerB's bet.

  5. playerB receives no refund as the contract’s funds are depleted.

Impact

  • Financial Loss: playerA can steal playerB's bet and other contract funds via multiple refunds.

  • Reputation Damage: Exploits undermine contract trust.

Tools Used

  • Manual code review

  • Foundry for exploit simulation

  • Custom ReentrancyAttack contract

Recommendations

  1. Add Reentrancy Guard:
    Use OpenZeppelin’s ReentrancyGuard with nonReentrant on cancelGame.

    import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
    contract RockPaperScissors is ReentrancyGuard {
    function cancelGame(uint256 _gameId) external nonReentrant {
    // ... existing code ...
    }
    }
Updates

Appeal created

m3dython Lead Judge about 2 months ago
Submission Judgement Published
Invalidated
Reason: Too generic
eightysix Submitter
about 2 months ago
m3dython Lead Judge
about 2 months ago
m3dython Lead Judge about 2 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.