Rock Paper Scissors

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

Arbitrary Game ID in TimeoutJoin Function

Summary

The timeoutJoin function in the RockPaperScissors contract can be called with any arbitrary game ID, including non-existent ones. This leads to phantom event emissions that can disrupt off-chain services and potentially cause confusion in the game state tracking.

Vulnerability Details

The timeoutJoin function does not validate if the provided game ID actually exists before processing the timeout:

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);
}

When accessing a non-existent game through the games mapping:

  1. The returned Game struct contains default values (0 for uint, address(0) for address)

  2. These default values pass all the require checks:

    • game.state == GameState.Created (true because GameState.Created = 0)

    • block.timestamp > game.joinDeadline (true because joinDeadline = 0)

    • game.playerB == address(0) (true because playerB = address(0))

As demonstrated by the test:

function testTimeoutJoin() public {
game.timeoutJoin(1423040); // Random non-existent game ID
// Function executes successfully and emits event
}

Impact

  1. Event Spam:

    • The function emits GameCancelled events for non-existent games

    • This can pollute event logs and confuse off-chain services

    • Indexers and UIs relying on these events may show incorrect game states

  2. Resource Waste:

    • Each phantom cancellation consumes gas

    • Creates unnecessary blockchain bloat

    • Increases costs for indexing services

  3. User Experience:

    • Players might see cancelled games that never existed

    • Could lead to confusion about game state and history

    • Potential for griefing by spamming phantom game cancellations

Proof of Concept

function testTimeoutJoin() public {
game.timeoutJoin(1423040); // Random game ID
// Test passes, event is emitted for non-existent game
}

Output from test:

[PASS] testTimeoutJoin() (gas: 38215)
Traces:
[38215] RockPaperScissorsTest::testTimeoutJoin()
├─ [32907] RockPaperScissors::timeoutJoin(1423040)
│ ├─ emit GameCancelled(gameId: 1423040)
│ └─ ← [Return]

Tools Used

  • Manual code review

  • Foundry testing framework

Recommendations

  1. Add explicit game existence validation:

function timeoutJoin(uint256 _gameId) external {
require(_gameId < nextGameId, "Game does not exist");
Game storage game = games[_gameId];
require(game.playerA != address(0), "Game does not exist");
require(game.state == GameState.Created, "Game must be in created state");
// ... rest of the function
}
  1. Implement a game registry to track valid games:

mapping(uint256 => bool) public gameExists;
function createGame(...) {
// ... existing game creation logic
gameExists[nextGameId] = true;
}
function timeoutJoin(uint256 _gameId) external {
require(gameExists[_gameId], "Game does not exist");
// ... rest of the function
}

While not directly leading to fund loss, this vulnerability can disrupt system operation and create confusion in game state tracking, off-chain services

Updates

Appeal created

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

Support

FAQs

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