Rock Paper Scissors

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

Non-Existent Game Cancellation Vulnerability Due to Uninitialized State Check

Summary

A critical vulnerability in the timeoutJoin function of the Rock-Paper-Scissors game smart contract leads to DOS attack. This vulnerability allows cancellation of non-existent games, potentially leading to a Denial of Service (DoS) attack against the platform. The issue stems from insufficient validation of game existence before performing state transitions.

Vulnerability Details

The timeoutJoin function is designed to allow cancellation of games that have reached their join deadline without player B joining. However, the function only validates that the game is in the Created state without verifying if the game actually exists.

Since GameState.Created has a value of 0 (the default value for enums in Solidity), calling timeoutJoin on a non-existent game passes the state check, as the default values for non-existent mapping entries in Solidity are zero.

function timeoutJoin(uint256 _gameId) external {
Game storage game = games[_gameId];
require(game.state == GameState.Created, "Game must be in created state");
require(block.timestamp > game.joinDeadline, "Join deadline not reached yet");
require(game.playerB == address(0), "Someone has already joined the game");
_cancelGame(_gameId);
}

The test provided demonstrates that calling timeoutJoin on a non-existent game ID (20) successfully transitions the game state to Cancelled (state 4) without any errors.

function testTimeoutJoinBug() public {
vm.prank(playerB);
game.timeoutJoin(20);
(
,
,
,
,
uint256 revealDeadline,
uint256 creationTime,
uint256 joinDeadline,
uint256 totalTurns,
uint256 currentTurn,
,
,
RockPaperScissors.Move moveA,
RockPaperScissors.Move moveB,
uint8 scoreA,
uint8 scoreB,
RockPaperScissors.GameState state
) = game.games(20);
console.log("state:-----------", uint256(state)); // returns 4 which cancelled
}

Output

[PASS] testTimeoutJoinBug() (gas: 63901)
Logs:
state:----------- 4
Traces:
[63901] RockPaperScissorsTest::testTimeoutJoinBug()
├─ [0] VM::prank(playerB: [0x3d3D63BabfeD85B3e08dE2d4A6c25b0d80cf77f1])
│ └─ ← [Return]
├─ [32907] RockPaperScissors::timeoutJoin(20)
│ ├─ emit GameCancelled(gameId: 20)
│ └─ ← [Return]
├─ [16060] RockPaperScissors::games(20) [staticcall]
│ └─ ← [Return] 0x0000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000, 0, 0, 0, 0, 0, 0, 0, 0x0000000000000000000000000000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000000000000000000000000000, 0, 0, 0, 0, 4
├─ [0] console::log("state:-----------", 4) [staticcall]
│ └─ ← [Stop]
└─ ← [Return]

The test shows that an attacker can call timeoutJoin with any arbitrary game ID, causing the function to emit a GameCancelled event and mark the game as Cancelled without any cost or restrictions.

Impact

This vulnerability can be exploited to:

  1. Cancel games that haven't been created yet, effectively reserving game IDs in a Cancelled state

  2. Execute a DoS attack by cancelling a large batch of sequential game IDs, preventing legitimate users from creating games with those IDs

  3. Potentially disrupt platform operation by continuously cancelling future game IDs

Tools Used

  • Foundry framework for testing and verification

  • Manual code review

Recommendations

  1. Restructure enum to avoid using meaningful states as zero values: The current enum design uses Created as the first value (index 0), which is problematic because it's indistinguishable from an uninitialized state. Redesign the enum to include an explicit "Uninitialized" or "NonExistent" state as the first value:

enum GameState {
NonExistent, // Default state (0) for non-existing games
Created, // Now value 1
Committed, // Now value 2
Revealed, // Now value 3
Finished, // Now value 4
Cancelled // Now value 5
}
  1. Add existence validation: Implement a check to verify that the game has been properly initialized before allowing cancellation:

function timeoutJoin(uint256 _gameId) external {
Game storage game = games[_gameId];
// Verify the game exists and has been properly initialized
require(game.creationTime != 0, "Game does not exist");
require(game.state == GameState.Created, "Game must be in created state");
require(block.timestamp > game.joinDeadline, "Join deadline not reached yet");
require(game.playerB == address(0), "Someone has already joined the game");
_cancelGame(_gameId);
}
  1. Implement game tracking: Keep track of valid game IDs using an array or a separate mapping to verify game existence:

mapping(uint256 => bool) public gameExists;
function createGame(...) external {
// Existing game creation logic
gameExists[gameId] = true;
}
function timeoutJoin(uint256 _gameId) external {
require(gameExists[_gameId], "Game does not exist");
// Existing function logic
}
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
0xblackadam Submitter
4 months ago
m3dython Lead Judge
4 months ago
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.