Rock Paper Scissors

First Flight #38
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: high
Invalid

Missing Commit Deadline Enables Permanent Game Stalling (DoS)

Summary

The RockPaperScissors::commitMove function in the RockPaperScissors contract does not enforce any deadline for the commit phase. Once a game is created and joined, if one player never calls commitMove, the game remains stuck in the Created (or Committed) state indefinitely. This creates a denial-of-service (DoS) scenario where ETH or tokens become locked and no further progress—turn resolution, tie handling, or cancellation—can occur.


Vulnerability Details

The issue centers on the absence of a commit-phase timeout. After both players have joined:

function commitMove(uint256 _gameId, bytes32 _commitHash) external {
Game storage game = games[_gameId];
require(
msg.sender == game.playerA || msg.sender == game.playerB,
"Not a player in this game"
);
require(
game.state == GameState.Created || game.state == GameState.Committed,
"Game not in commit phase"
);
@audit-issue No commit-phase timeout enforced; allows game to stall forever
@> // Missing: require(block.timestamp <= game.commitDeadline, "Commit phase timed out");
// First commit of the game
if (
game.currentTurn == 1 &&
game.state == GameState.Created &&
game.commitA == bytes32(0) &&
game.commitB == bytes32(0)
) {
require(game.playerB != address(0), "Waiting for player B to join");
game.state = GameState.Committed;
@audit-issue Missing initialization of commitDeadline on first commit
@> // Missing: game.commitDeadline = block.timestamp + game.timeoutInterval;
}
if (msg.sender == game.playerA) {
require(game.commitA == bytes32(0), "Already committed");
game.commitA = _commitHash;
} else {
require(game.commitB == bytes32(0), "Already committed");
game.commitB = _commitHash;
}
emit MoveCommitted(_gameId, msg.sender, game.currentTurn);
if (game.commitA != bytes32(0) && game.commitB != bytes32(0)) {
@audit-issue Missing initialization of commitDeadline on subsequent commits
@> // Missing (if not set above): game.commitDeadline = block.timestamp + game.timeoutInterval;
game.revealDeadline = block.timestamp + game.timeoutInterval;
}
}

Issues Identified

  1. No Commit-Phase Deadline

    • The contract never records a timestamp (commitDeadline) once both players are expected to commit.

    • Without such a deadline, a malicious or negligent player can simply refuse to call commitMove, halting all subsequent game logic.

  2. Locked Funds and State

    • ETH or tokens staked in createGameWithEth/createGameWithToken remain in the contract permanently.

    • Neither _cancelGame, _finishGame, nor _handleTie can be triggered, since they all require moving past the commit/reveal cycles.

  3. Permanent Game Stalling

    • Future turns cannot begin because currentTurn never advances.

    • No mechanism exists for the honest player to claim victory or refunds when the opponent fails to commit.


Impact

Severe Consequences

  • Denial of Service (DoS): Any player can indefinitely block the entire game by not calling commitMove.

  • Locked Capital: Both players’ ETH or tokens become irretrievably locked in the contract.

  • User Frustration & Loss: Honest participants lose their stakes and cannot recover funds, undermining trust.

  • Operational Halt: No administrative or user-level function can progress or cancel the game once stalled.


Tools Used

  • Manual code review


Recommendations

  1. Add commitDeadline Field to the Game Struct

    • Update the Game struct to include a new commitDeadline field, which will be used to enforce a time limit for submitting committed moves.

    struct Game {
    ...
    uint256 commitDeadline; // ⬅️ New field to track commit deadline
    ...
    }
  2. Introduce a Commit Deadline

    • On the first commit or when both players have joined, set:

      game.commitDeadline = block.timestamp + game.timeoutInterval;
    • Add a commitDeadline field to Game.

  3. Enforce Deadline in commitMove

    require(block.timestamp <= game.commitDeadline, "Commit phase timed out");
  4. Add a timeoutCommit Function

    function timeoutCommit(uint256 _gameId) external {
    Game storage game = games[_gameId];
    require(block.timestamp > game.commitDeadline, "Commit deadline not reached");
    bool aCommitted = game.commitA != bytes32(0);
    bool bCommitted = game.commitB != bytes32(0);
    if (aCommitted && !bCommitted) {
    _finishGame(_gameId, game.playerA);
    } else if (!aCommitted && bCommitted) {
    _finishGame(_gameId, game.playerB);
    } else {
    _cancelGame(_gameId);
    }
    }
  5. Adjust State Transitions

    • Ensure state reflects entry into a commit phase and transitions properly on timeout.

    • Use Checks–Effects–Interactions and pull‑payment patterns for safety.

By enforcing and handling commit-phase timeouts, the contract can guarantee that games either progress to reveal, award a timely victory, or refund both players, eliminating permanent stalls.

Updates

Appeal created

m3dython Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
m3dython Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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