The functions responsible for distributing ETH prizes (_finishGame, _handleTie) and refunding bets (_cancelGame) perform external calls (.call{value: ...}) before all effects of the function are completed (specifically,
before minting WinningToken in _finishGame and _cancelGame, and between player payouts in _handleTie).
If the recipient is a malicious contract, it can re-enter the RockPaperScissors contract during the ETH transfer, potentially interacting with other functions or games before the initial function fully completes, violating the Checks-Effects-Interactions pattern.
When the game sends ETH to a winner or refunds players, if that player is a malicious smart contract, it can run its own code before the game contract finishes its current task (like minting a winner token). This malicious code could call back into the game contract and try to perform actions it shouldn't be able to, like interfering with another game it's playing, because the first operation isn't fully complete yet.
Technical Root Cause: The vulnerability stems from violating the Checks-Effects-Interactions (CEI) pattern. The external interaction (ETH transfer via .call) happens before all internal state changes (effects, like winningToken.mint) are finalized. The low-level .call function forwards all available gas by default, allowing the recipient contract ample gas to execute potentially complex logic and re-enter the calling contract. While the game.state is updated before the call, preventing simple replay attacks on the same game, reentrancy allows interaction with other parts of the contract state during the vulnerable window.
The issue exists because external calls are made before the state is fully settled according to the function's logic.
Location 1: src/RockPaperScissors.sol, Function _finishGame
https://github.com/CodeHawks-Contests/2025-04-rock-paper-scissors/blob/main/src/RockPaperScissors.sol#L472-L505
Location 2: src/RockPaperScissors.sol, Function: _handleTie
https://github.com/CodeHawks-Contests/2025-04-rock-paper-scissors/blob/main/src/RockPaperScissors.sol#L511-L541
Location 3: src/RockPaperScissors.sol, Function: _cancelGame
https://github.com/CodeHawks-Contests/2025-04-rock-paper-scissors/blob/main/src/RockPaperScissors.sol#L547-L573
Interfere with other games: If the attacker is participating in multiple games, they could re-enter during the payout of Game 1 and call functions like commitMove or revealMove for Game 2, potentially exploiting timing or state inconsistencies.
Exploit future vulnerabilities: While no direct fund theft from the current game payout seems possible due to the prior state change, the reentrancy opens the door to exploiting other potential bugs (e.g., if an admin function had a flaw reachable during reentrancy).
Cause unexpected state changes: The reentrancy could lead to transient states where contract invariants are violated before the initial function call completes, although the specific impact depends heavily on the reentrant call target. The immediate impact isn't direct theft of the current prize pool, but the potential for state corruption or exploitation of other interactions elevates the severity. Business impact includes potential loss of user funds in edge cases, damage to game integrity, and reputational harm.
Attacker deploys a malicious contract (AttackerContract).
Attacker uses AttackerContract address to create or join a RockPaperScissors ETH game (Game ID X). Let's assume AttackerContract is Player A.
Attacker plays the game normally against Player B (can be another address controlled by the attacker or a legitimate player).
Attacker ensures AttackerContract wins Game X.
When Player B makes the final reveal triggering _finishGame, the RockPaperScissors contract calls _finishGame(X, address(AttackerContract)).
Inside _finishGame, the line _winner.call{value: prize}("") executes, sending ETH to AttackerContract.
AttackerContract's receive() or fallback function is triggered.
Inside the receive() function, AttackerContract immediately calls back into the RockPaperScissors contract (re-entrancy). For example, it could call commitMove for another game (Game ID Y) it is participating in.
The commitMove call executes before the winningToken.mint(_winner, 1) line in the original _finishGame call for Game X.
The execution flow returns to _finishGame, which then attempts to mint the token.
Manual Code
AI for repport ehancement
Apply Checks-Effects-Interactions Pattern: Modify _finishGame, _handleTie, and _cancelGame to perform all state changes (Effects), including token minting, before sending ETH (Interaction).
Use Reentrancy Guard: Implement OpenZeppelin's ReentrancyGuard modifier on all non-view/pure external and public functions that modify state or interact with other contracts, especially the payout/refund functions (_finishGame, _handleTie, _cancelGame, timeoutReveal, cancelGame, timeoutJoin). This provides a robust general protection against reentrancy.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.