Rock Paper Scissors

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

Permanent Locking of Assets Possible if Neither Player Triggers Reveal Timeout

Summary

The RockPaperScissors contract includes a timeoutReveal function intended to resolve games where one or both players fail to reveal their moves before the deadline. However, this function requires an explicit call from one of the involved players after the deadline has passed. If both players commit their moves, the reveal deadline passes, and neither player subsequently calls timeoutReveal, the game becomes permanently stuck in the Committed state. There is no other mechanism within the contract for anyone (including the players or admin) to transition the game out of this state, leading to the permanent locking of the ETH or tokens wagered in that specific game instance.

Vulnerability Details

  1. Commit Phase Completed:

    • Both players successfully commit their moves using commitMove.

    • The game state becomes GameState.Committed.

    • game.revealDeadline is set.

  2. Deadline Passes:

    • Time progresses and block.timestamp > game.revealDeadline.

  3. Player Inactivity:

    • Neither playerA nor playerB calls revealMove.

    • game.moveA and game.moveB remain Move.None.

  4. Reveal Blocked:

    • Any attempt to call revealMove after the deadline fails due to:

      require(block.timestamp <= game.revealDeadline, "Reveal phase timed out");
  5. Turn Advancement Blocked:

    • _determineWinner() is never called, because it is only invoked from revealMove when both players have revealed.

  6. No timeoutReveal Called:

    • The contract provides timeoutReveal which could resolve the issue by calling _cancelGame.

    • However, if neither player calls it (e.g., due to forgotten game or cost etc), the game is permanently stuck.

  7. No Alternative Resolution:

    • cancelGame and timeoutJoin are limited to GameState.Created.

    • _finishGame is unreachable.

    • _cancelGame is only called via the other blocked paths.

Impact

  • Game Incompletion: Affected game can never reach a final state (Finished or Cancelled).

  • Locked Funds: ETH or tokens staked in the game remain indefinitely locked.

Tools Used

  • Manual code review

Recommendations

Implement a mechanism to ensure that games stuck in the Committed state post-deadline can eventually be resolved, even without player intervention via timeoutReveal.

Consider adding a public function (e.g., forceCancelStuckGame) callable by anyone. This function should check for the specific stuck condition:

  • games[_gameId].state == GameState.Committed

  • block.timestamp > games[_gameId].revealDeadline

  • games[_gameId].moveA == Move.None

  • games[_gameId].moveB == Move.None

  • block.timestamp > games[_gameId].revealDeadline + 24 hours

If these conditions are met, the function should execute the logic currently present in _cancelGame(_gameId): set the state to Cancelled and refund the assets.

To prevent this function from being called prematurely or maliciously right after the deadline, consider adding an additional delay requirement (e.g., block.timestamp > games[_gameId].revealDeadline + 24 hours) before this public cancellation function can be successfully invoked. This ensures players have ample time to call timeoutReveal themselves if they wish, but provides a fallback for abandoned games.

Updates

Appeal created

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

Player B cannot cancel a game if Player A becomes unresponsive after Player B joins

Protocol does not provide a way for Player B to exit a game and reclaim their stake if Player A stops participating

Support

FAQs

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