The RockPaperScissors.sol contract allows a game to be cancelled almost immediately if one player commits their move but the opponent fails to commit theirs. This occurs because the revealDeadline is only set within the commitMove function after both players have submitted their commitments. If only one player commits, the revealDeadline remains at its default value of 0. The timeoutReveal function checks if block.timestamp > game.revealDeadline. Since block.timestamp is always greater than 0, this check passes instantly when revealDeadline is 0. Consequently, either player can call timeoutReveal immediately after the first player commits (and before the second player commits), triggering the logic path for "neither player revealed" (as the reveal phase never truly started), which results in _cancelGame being called. While this correctly refunds stakes, it allows a player (or even the first player) to force an immediate cancellation, potentially constituting a minor griefing or Denial of Service vector against completing the game.
revealDeadline Initialization: The revealDeadline field in the Game struct defaults to 0.
commitMove Logic: The revealDeadline is only calculated and set if both game.commitA and game.commitB are non-zero:
If only Player A commits, game.commitB remains 0, and game.revealDeadline stays 0.
timeoutReveal Logic: This function contains the check:
Exploitation Scenario:
Player A creates and Player B joins a game.
Player A calls commitMove. game.commitA is set, game.state becomes Committed, but game.revealDeadline remains 0.
Player B (or Player A) immediately calls timeoutReveal.
The check block.timestamp > game.revealDeadline (i.e., block.timestamp > 0) passes instantly.
Since neither game.moveA nor game.moveB has been set (the reveal phase didn't start), the condition !playerARevealed && !playerBRevealed becomes true.
timeoutReveal calls _cancelGame(_gameId).
The test case below demonstrates the behavior
Minor Griefing / Denial of Service: Allows either player (most likely Player B, but Player A could also change their mind) to force an immediate cancellation after Player A has committed, preventing the game from proceeding. This wastes Player A's gas spent on game creation and committing.
User Experience: Can be confusing or frustrating if games are frequently cancelled immediately after the first commit.
Manual Code Review
Foundry/Forge (for Test Execution and PoC verification)
The current behavior, while perhaps slightly unintuitive, ensures games don't get stuck indefinitely if a player fails to commit. However, the immediacy of the cancellation via timeoutReveal might be considered undesirable. A more robust approach would involve a dedicated timeout for the commit phase itself.
Introduce a Commit Timeout (Recommended):
Add a new state variable to the Game struct, e.g., commitDeadline (uint256).
When Player A commits (and Player B has joined), set commitDeadline = block.timestamp + commitTimeoutDuration; (where commitTimeoutDuration is a new configurable or constant value, e.g., 1 hour).
Modify commitMove for Player B to require block.timestamp <= game.commitDeadline.
Create a new function timeoutCommit(uint256 _gameId):
Requires game.state == Committed.
Requires game.commitB == bytes32(0) (only B hasn't committed).
Requires block.timestamp > game.commitDeadline.
If conditions met, calls _cancelGame(_gameId).
Modify timeoutReveal: Add require(game.revealDeadline != 0, "Reveal deadline not set"); to ensure it only triggers after the reveal phase has actually begun (i.e., both committed).
Accept Current Behavior (Less Ideal): Acknowledge that the current mechanism, while allowing immediate cancellation via timeoutReveal if the second player doesn't commit, does resolve the stuck state and refunds players. Document this behavior clearly.
Implementing Recommendation 1 provides a more structured game flow with distinct timeouts for committing and revealing, preventing the immediate cancellation vector while still handling unresponsive players.
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.