The RockPaperScissors contract is vulnerable to a cross-function reentrancy attack through the cancelGame
function. This vulnerability allows a malicious player (Player A) to potentially steal funds from other players (Player Bs) by exploiting the lack of reentrancy protection and the nested if structure when ETH is refunded.
The vulnerability exists in the interaction between the cancelGame
function and the internal _cancelGame
function it calls:
In cancelGame
(lines 319-326), the function only checks:
If the game is in the Created
state
If the caller is the game creator (Player A)
In _cancelGame
(lines 548-574), the contract:
Sets the game state to Cancelled
(line 551)
Refunds ETH to Player A using a low-level call
(line 556)
Then refunds ETH to Player B if they exist (line 560)
The nested if statement in _cancelGame
is particularly problematic:
This sequential execution allows Player A to receive their refund first and potentially reenter before Player B gets their refund.
The critical issue is that when a reentrant call is made to cancelGame
for another game, it only checks if that game is in the Created
state, not whether it's already been cancelled in the same transaction.
Attacker (Player A) creates multiple games with ETH bets
Other players (Player Bs) join these games with matching bets
Before any moves are committed, Attacker calls cancelGame()
on Game #1
When receiving the ETH refund, Attacker reenters and calls cancelGame()
on Games #2, #3, etc.
Due to the nested if structure, the Attacker can potentially manipulate the call sequence to steal Player B's refunds
This vulnerability has a high severity impact as it allows a malicious player to:
Steal ETH from other players who have joined their games
Drain the contract of funds meant for legitimate refunds
Manipulate game cancellations to their advantage
The financial impact is directly proportional to the number and size of active games with ETH bets. In a production environment with multiple games and significant betting amounts, this could lead to substantial financial losses for other players.
Manual code review
To fix this vulnerability, implement the following changes:
Add a reentrancy guard using OpenZeppelin's ReentrancyGuard:
Implement proper state validation in the _cancelGame
function:
Restructure the nested if statements to avoid sequential execution:
Consider using a pull-over-push pattern for refunds:
Store refund amounts in a mapping
Let players withdraw their funds with a separate function
This eliminates reentrancy concerns entirely
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.