Rock Paper Scissors

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

Reveal Phase Manipulation Due to Missing Deadline Reset

Summary

The RockPaperScissors contract contains a flaw where, after the first round, the revealDeadline is not reset when players commit their moves in subsequent rounds. This allows a malicious player to commit their move, immediately reveal it (since revealDeadline is stale from the first round), and then use timeoutReveal to unfairly win the round before the opponent has a chance to commit or reveal their move.

Vulnerability Details

In the commitMove function, the revealDeadline is only set if both players' commitments are submitted and it's the first round:

if (game.commitA != bytes32(0) && game.commitB != bytes32(0)) {
game.revealDeadline = block.timestamp + game.timeoutInterval;
}

However, in rounds beyond the first, if a player commits first and then calls revealMove, the stale revealDeadline (from round 1) might already have passed. This allows them to:

  1. Commit their move.

  2. Immediately call revealMove.

  3. Wait a moment.

  4. Call timeoutReveal, claiming the opponent didn't reveal on time — even though the opponent never got a chance to commit in that round.

This leads to a forced win that breaks the fairness of the game.

Impact

  • Unfair advantage: A malicious player can abuse the outdated revealDeadline to force wins.

  • Race condition: It creates a timing race where the first committer has the power to lock out the opponent.

  • Game logic flaw: The expected order of commit → commit → reveal → reveal is violated.

  • Undermines fairness: A core value of Rock-Paper-Scissors is simultaneous reveal, which this bug breaks.

Tools Used

  • Manual code review

  • Logic tracing of commitMove, revealMove, and timeoutReveal

  • Inference of state transitions across multiple rounds

Recommendations

Update the commitMove function to reset revealDeadline on every round, once both players have committed:

if (game.commitA != bytes32(0) && game.commitB != bytes32(0)) {
game.revealDeadline = block.timestamp + game.timeoutInterval;
}

Ensure this block always runs, even after the first round. You might also refactor the condition logic to explicitly handle revealDeadline reset in every round.

Additionally, consider adding a state check in revealMove to prevent revealing before both commits are received:

require(game.commitA != bytes32(0) && game.commitB != bytes32(0), "Both players must commit before reveal");

This ensures the reveal phase begins only when both commitments are present, maintaining fair play.

Updates

Appeal created

m3dython Lead Judge 2 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Invalid TimeoutReveal Logic Error

timeoutReveal function incorrectly allows execution and game cancellation even when only one player has committed

vtim99077 Submitter
about 2 months ago
m3dython Lead Judge
about 2 months ago
m3dython Lead Judge about 2 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Game State Manipulation Preventing Opponent Commit

Attack allows a player to reveal their move for the next turn before the opponent commits

Support

FAQs

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