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 10 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
10 months ago
m3dython Lead Judge
10 months ago
m3dython Lead Judge 10 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.

Give us feedback!