Rock Paper Scissors

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

Unlimited Game Creation and Cancellation (Storage Griefing)

Summary

Location: cancelGame(), createGameWithEth(), createGameWithToken()

Description:

The RockPaperScissors contract allows playerA to create games and later cancel them via cancelGame. There's no restriction on how many games can be created. Since the _gameId is a sequential uint256, an attacker can pre-fill a large number of game IDs with cheap games (using minimum ETH/token) and then cancel them, effectively reserving/bloating valuable game IDs and increasing lookup/storage costs.

function cancelGame(uint256 _gameId) external {
Game storage game = games[_gameId];
require(game.state == GameState.Created, "Game must be in created state");
require(msg.sender == game.playerA, "Only creator can cancel");
_cancelGame(_gameId);
}
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 gameId = gameCounter++;
Game storage game = games[gameId];
game.playerA = msg.sender;
game.bet = msg.value;
game.timeoutInterval = _timeoutInterval;
game.creationTime = block.timestamp;
game.joinDeadline = block.timestamp + joinTimeout;
game.totalTurns = _totalTurns;
game.currentTurn = 1;
game.state = GameState.Created;
emit GameCreated(gameId, msg.sender, msg.value, _totalTurns);
return gameId;
}
/**
* @notice Create a new game with winning token
* @param _totalTurns Number of turns for the game (must be odd)
* @param _timeoutInterval Seconds allowed for reveal phase
*/
function createGameWithToken(uint256 _totalTurns, uint256 _timeoutInterval) external returns (uint256) {
require(winningToken.balanceOf(msg.sender) >= 1, "Must have winning token");
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");
// Transfer token to contract
winningToken.transferFrom(msg.sender, address(this), 1);
uint256 gameId = gameCounter++;
Game storage game = games[gameId];
game.playerA = msg.sender;
game.bet = 0; // Zero ether bet because using token
game.timeoutInterval = _timeoutInterval;
game.creationTime = block.timestamp;
game.joinDeadline = block.timestamp + joinTimeout;
game.totalTurns = _totalTurns;
game.currentTurn = 1;
game.state = GameState.Created;
emit GameCreated(gameId, msg.sender, 0, _totalTurns);
return gameId;
}

Impact:

Storage Griefing / Denial of Access to Legitimate Game IDs

a. Storage griefing.

b. Game ID space bloating.

c. Potential gas cost increase for legitimate players.

d. Difficulties in game discovery/tracking.

Recommendation:

  1. Impose a cap on the number of pending or cancelled games per user.

  2. Consider using a mapping indexed by (playerA, nonce) instead of a global incrementor.

  3. Implement cleanup logic for stale/cancelled games.

Tools Used

Manual review

Updates

Appeal created

m3dython Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Informational

Code suggestions or observations that do not pose a direct security risk.

m3dython Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Informational

Code suggestions or observations that do not pose a direct security risk.

Support

FAQs

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