The RockPaperScissors contract fails to reset the revealDeadline
variable between game rounds, creating a critical vulnerability in the core gameplay mechanism. This flaw allows a malicious player to gain an unfair advantage by revealing their move before their opponent can commit, rendering the opponent unable to participate in the round, and then exploiting the timeout mechanism to automatically win the game and claim the entire prize pool.
After a round completes, the _determineWinner
function resets several game state variables but crucially fails to reset the revealDeadline
:
This vulnerability can be exploited through three critical flaws working together:
In the revealMove
function, there is no check to ensure both players have committed before allowing reveals:
In the commitMove
function, once a player has revealed, the other player is blocked from committing:
The timeoutReveal
function automatically declares a winner if only one player has revealed:
This vulnerability has severe consequences for the game's integrity:
Complete Breakdown of Game Fairness: A malicious player can bypass the core commit-reveal mechanism intended to prevent players from seeing each other's moves before committing.
Automatic Win Exploitation: Attackers can guarantee wins without actual gameplay by blocking their opponents from participating.
Financial Loss: Victims lose their bet amounts to attackers who exploit this vulnerability.
Unwinnable Scenario: The victim player is placed in a position where they cannot defend against the attack by any means.
Protocol Trust Degradation: This exploitable vulnerability undermines trust in the protocol and renders the game unplayable.
Manual review
Foundry
The following steps demonstrate how this vulnerability can be exploited:
Complete the first round of the game normally (both players commit and reveal)
When the next round begins, Player A quickly commits their move
Player A immediately reveals their move (which is allowed due to the lingering revealDeadline
)
Player B attempts to commit but is blocked by the check require(game.moveA == Move.None && game.moveB == Move.None, "Moves already committed for this turn");
Player A waits for the reveal deadline to pass
Player A calls timeoutReveal()
which automatically declares them the winner, since they have revealed and Player B could not
Below is a Foundry test that proves this vulnerability exists:
To fix this vulnerability, implement all of the following changes:
Reset the revealDeadline
between rounds
Ensure both players have committed before allowing reveals
Allow commits even if one player has already revealed
Consider adding a commit phase timeout as well, to prevent game stalling if a player never commits.
timeoutReveal function incorrectly allows execution and game cancellation even when only one player has committed
timeoutReveal function incorrectly allows execution and game cancellation even when only one player has committed
Attack allows a player to reveal their move for the next turn before the opponent commits
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.