Rock Paper Scissors

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

Inadequate Game Cancellation Rights for Player B

Summary

The RockPaperScissors smart contract contains a vulnerability in its game cancellation functionality. Currently, only the game creator (Player A) can cancel a game that is still in the created state. Player B, despite having joined and staked funds or tokens, lacks the ability to cancel the game before moves are committed, creating an unfair power imbalance in the contract's permission structure.

Vulnerability Details

The vulnerability exists in the cancelGame function (lines 310-318):

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

This function explicitly checks that the message sender is game.playerA, preventing Player B from canceling the game even if they've joined but no moves have been committed. This creates an asymmetric power dynamic where Player B has no exit option until the game progresses to the next phase or reaches a timeout.
The issue is particularly concerning because:

  • Player B has already committed funds (ETH) or tokens to the game

  • The game is still in an early state (no moves committed)

  • Player B is forced to wait for the timeout or for Player A to take action

Impact

While it doesn't result in direct loss of funds (as Player B can eventually recover their stake through timeout mechanisms), it creates an unfair user experience and potentially locks Player B's funds or tokens for extended periods. Player B must wait for the game to timeout or for Player A to voluntarily cancel the game, with no ability to exit independently.
This could lead to:

  • Temporary fund/token lockup for Player B

  • Poor user experience and lack of control

  • Potential game abandonment by Player A, forcing Player B to wait for timeout periods

Tools Used

  • Manual code review

Recommendations

Implement a modified cancelGame function that permits both players to cancel the game while in the created state, but with different behavior based on which player initiates the cancellation:

  • The deadline is increased if playerB canceled so that in situations where playerB canceled close to the deadline, the creator does not have to create a new game. and since playerA can cancel anytime, this does not create any issue.

/**
* @notice Cancel game based on who initiated the cancellation
* @param _gameId ID of the game
*/
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 || msg.sender == game.playerB, "Must be a player in this game");
require(game.commitA == bytes32(0) && game.commitB == bytes32(0), "Moves already committed");
if (msg.sender == game.playerB) {
// Player B canceling - refund only Player B and extend deadline for Player A
game.playerB = address(0);
// Refund Player B's ETH
if (game.bet > 0) {
(bool success,) = msg.sender.call{value: game.bet}("");
require(success, "Transfer to player B failed");
}
// Return tokens for token games
if (game.bet == 0) {
winningToken.mint(msg.sender, 1);
}
// Extend join deadline for Player A
game.joinDeadline = block.timestamp + joinTimeout;
emit PlayerLeft(_gameId, msg.sender);
} else {
// Player A canceling - full cancellation
_cancelGame(_gameId);
}
}
Updates

Appeal created

m3dython Lead Judge 7 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.