Rock Paper Scissors

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

Griefing and Prolonged Fund Locking via Unbounded Reveal Timeout Interval

Summary

The RockPaperScissors.sol contract allows the game creator (Player A) to specify a _timeoutInterval parameter during game creation (createGameWithEth, createGameWithToken). This interval determines the duration players have to reveal their moves after both have committed. While the contract enforces a minimum value for this interval (5 minutes), it crucially lacks validation for a maximum value. This allows Player A to set an arbitrarily long timeout period (e.g., weeks, months, or years). If Player B joins such a game without verifying the interval, and a situation arises where the timeoutReveal function is needed (e.g., one player becomes unresponsive after committing), the game resolution and associated fund release will be delayed until the extremely distant revealDeadline passes. This enables griefing by Player A and leads to impractically long fund locking periods for both players.

Vulnerability Details

  1. Affected Functions: createGameWithEth, createGameWithToken.

  2. Input Parameter: _timeoutInterval (uint256), representing the duration in seconds for the reveal phase.

  3. Existing Validation: The functions correctly check for a minimum duration:

    require(_timeoutInterval >= 5 minutes, "Timeout must be at least 5 minutes");
  4. Missing Validation: There is no corresponding check to enforce a maximum reasonable value for _timeoutInterval. Player A can supply any value greater than or equal to 5 minutes.

  5. Deadline Calculation: The revealDeadline is calculated within the commitMove function after both players have committed:

    game.revealDeadline = block.timestamp + game.timeoutInterval;

    If game.timeoutInterval (set during creation by Player A) is excessively large, game.revealDeadline will be set far into the future.

  6. Timeout Function Dependency: The timeoutReveal function, which handles scenarios where one or both players fail to reveal, requires the deadline to have passed:

    require(block.timestamp > game.revealDeadline, "Reveal phase not timed out yet");
  7. Exploitation: If game.revealDeadline is set extremely far in the future due to a large _timeoutInterval, the timeoutReveal function becomes unusable until that distant point in time. Any situation requiring a timeout resolution (e.g., Player B refuses to reveal a losing move) results in the game state remaining Committed and funds remaining locked.

Proof Of Concept (Conceptual)

  1. Game Creation: Malicious Player A calls createGameWithEth with _timeoutInterval = 31536000 (approximately 1 year) and a standard BET_AMOUNT.

  2. Game Joining: Unsuspecting Player B finds the game and calls joinGameWithEth with the matching BET_AMOUNT, without checking the game's specific timeoutInterval parameter (which is publicly readable from the games mapping).

  3. Commit Phase: Both Player A and Player B successfully call commitMove. The revealDeadline for the game is now set to block.timestamp + 1 year.

  4. Reveal Phase & Malice: Player A reveals their move. Player B observes Player A's move, realizes they will lose the turn (or the game), and decides not to call revealMove.

  5. Timeout Attempt: Player A waits a reasonable time (e.g., 1 hour) and attempts to resolve the game by calling timeoutReveal.

  6. Result: The timeoutReveal call reverts with the message "Reveal phase not timed out yet" because block.timestamp is far less than the revealDeadline (1 year in the future).

  7. Outcome: The game remains stuck in the Committed state. Both Player A's and Player B's BET_AMOUNT remain locked in the contract. Player A cannot claim their win, and Player B cannot get a refund (nor can the game be cancelled via this path) until the 1-year deadline passes. Player A has successfully griefed Player B (and themselves) by locking funds for an unreasonable duration.

Impact

  • Griefing: Allows Player A to impose extremely long fund-locking conditions on Player B if a timeout scenario occurs. Player A can potentially force such a scenario by refusing to reveal themselves if they are losing later in a multi-turn game, although this also locks their own funds.

  • Prolonged Fund Locking: Staked ETH or Tokens for both players can be locked within the contract for impractically long periods (potentially years) if a timeout resolution is required.

  • Poor User Experience: Player B may unknowingly join a game with unreasonable timeout terms, leading to significant frustration and locked assets if their opponent becomes unresponsive or malicious.

  • Reduced Game Liveness: Games requiring timeout resolution become effectively stalled and unusable for the duration of the excessively long timeout interval.

Tools Used

  • Manual Code Review

Recommendations

Enforce a reasonable upper limit on the _timeoutInterval parameter during game creation to prevent abuse.

  1. Add Maximum Timeout Validation: Modify createGameWithEth and createGameWithToken to include a require statement checking for a maximum allowed timeout. The specific maximum should be chosen based on reasonable gameplay expectations (e.g., 1 week, 30 days).

    // Example for createGameWithEth
    function createGameWithEth(uint256 _totalTurns, uint256 _timeoutInterval) external payable returns (uint256) {
    require(msg.value >= minBet, "Bet amount too small");
    require(_totalTurns > 0, "Must have at least one turn");
    require(_totalTurns % 2 == 1, "Total turns must be odd");
    require(_timeoutInterval >= 5 minutes, "Timeout must be at least 5 minutes");
    + uint256 constant MAX_REVEAL_TIMEOUT = 7 days; // Example: Set a max of 7 days
    + require(_timeoutInterval <= MAX_REVEAL_TIMEOUT, "Timeout interval too long");
    // ... rest of function ...
    }
    // Similar check needed in createGameWithToken
  2. Client-Side Validation/Display: User interfaces interacting with the contract should clearly display the timeoutInterval for a game before Player B joins, allowing them to make an informed decision about the game's terms.

Implementing Recommendation 1 (adding the require check for a maximum timeout) is crucial to prevent the griefing and prolonged fund locking vulnerability.

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

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.