Rock Paper Scissors

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

Funds/Token Lockup Risk Due to Unresponsive Opponent After Move Commit

Description: When a player joins a game and commits their move, the game state transitions to Committed, but the revealDeadline remains unset (defaulting to 0) until the opponent also commits their move. If the opponent becomes unresponsive and does not commit, the game remains in the Committed state indefinitely with no way to proceed or exit. This results in both players’ funds/tokens being locked in the contract.

Why existing timeout/cancel mechanisms don't work:

RockPaperScissors::timeoutReveal cannot be triggered because revealDeadline == 0, making the requirement require(block.timestamp > game.revealDeadline) always fail.

RockPaperScissors::cancelGame cannot be used because:

The game is no longer in the Created state.

Only the creator (playerA) is allowed to cancel, and they can’t once the state has moved to Committed.

Impact: Players may permanently lose access to their funds/tokens if their opponent abandons the game after one move is committed. There is currently no way to recover from this state.

Proof of Concept: N/A

Recommended Mitigation: Introduce a new field (e.g., commitDeadline) that sets a timeout after the first player commits a move. If the second player fails to commit within this timeframe, the first player should be able to cancel the game and recover their funds. This ensures fairness and prevents griefing through inactivity.

// Game structure
struct Game {
address playerA; // Creator of the game
address playerB; // Second player to join
uint256 bet; // Amount of ETH bet
uint256 timeoutInterval; // Time allowed for reveal
++ uint256 commitDeadline; // Deadline for committing a move
uint256 revealDeadline; // Deadline for revealing moves
uint256 creationTime; // When the game was created
uint256 joinDeadline; // Deadline for someone to join the game
uint256 totalTurns; // Total number of turns in the game
uint256 currentTurn; // Current turn number
bytes32 commitA; // Hashed move from player A
bytes32 commitB; // Hashed move from player B
Move moveA; // Revealed move from player A
Move moveB; // Revealed move from player B
uint8 scoreA; // Score for player A
uint8 scoreB; // Score for player B
GameState state; // Current state of the game
}
// commitMove function
if (game.commitA != bytes32(0) && game.commitB != bytes32(0)) {
++ game.commitDeadline = 0; // Reset the deadline once both parties have committed
game.revealDeadline = block.timestamp + game.timeoutInterval;
}
++ else {
++ game.commitDeadline = block.timestamp + 1 days; // or whatever duration that the opponent should respond by
++ }
// additional function for either player to cancel the game after one side has committed
function timeoutCommit(uint256 _gameId) external {
Game storage game = games[_gameId];
require(game.state == GameState.Committed, "Game must be in created state");
require(msg.sender == game.playerA || msg.sender == game.playerB, "Only players can cancel");
require(game.commitDeadline != 0, "Commit deadline must be set!");
require(block.timestamp > game.commitDeadline, "Commit deadline not reached yet");
_cancelGame(_gameId);
}
Updates

Appeal created

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