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.
Commit Phase Completed:
Both players successfully commit their moves using commitMove
.
The game state becomes GameState.Committed
.
game.revealDeadline
is set.
Deadline Passes:
Time progresses and block.timestamp > game.revealDeadline
.
Player Inactivity:
Neither playerA
nor playerB
calls revealMove
.
game.moveA
and game.moveB
remain Move.None
.
Reveal Blocked:
Any attempt to call revealMove
after the deadline fails due to:
Turn Advancement Blocked:
_determineWinner()
is never called, because it is only invoked from revealMove
when both players have revealed.
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.
No Alternative Resolution:
cancelGame
and timeoutJoin
are limited to GameState.Created
.
_finishGame
is unreachable.
_cancelGame
is only called via the other blocked paths.
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.
Manual code review
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.
Protocol does not provide a way for Player B to exit a game and reclaim their stake if Player A stops participating
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.