Rock Paper Scissors

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

Player B Overwrite in Game Joining

Summary

In the RockPaperScissors contract, a critical vulnerability exists in the game-joining logic where multiple users can join the same game as Player B by calling joinGameWithEth or joinGameWithToken. This causes the playerB field in the game struct to be overwritten each time, effectively allowing only the last joiner to participate. All previous joiners who sent funds or tokens lose their assets, leading to fund loss and potential exploitation.

Vulnerability Details

The following functions are vulnerable:

  • joinGameWithEth(uint256 _gameId)

  • joinGameWithToken(uint256 _gameId)

In both functions, there is no check to ensure that playerB is not already set. Therefore, every call overwrites the existing playerB value.

require(game.playerA != msg.sender, "Cannot join your own game");
// MISSING: require(game.playerB == address(0), "Game already has a second player");
game.playerB = msg.sender;

This omission allows any user to continuously call the function and overwrite the previous playerB, causing earlier participants to lose their ETH or tokens without any chance to participate in the game.

Impact

  • Loss of funds: Users sending ETH to join a game will lose their ETH if another user joins afterward.

  • Denial of participation: Only the last user to call the join function will actually be recognized as Player B.

  • Potential for griefing: An attacker can spam join a game to prevent legitimate players from joining, without any cost to themselves.

  • Game logic inconsistency: The contract incorrectly allows multiple joiners, violating the "two-player only" nature of Rock-Paper-Scissors.

Tools Used

  • Manual code review

  • Solidity knowledge

  • Remix IDE for scenario testing (optional)

Recommendations

Add a check to prevent overwriting playerB after it has been set:

require(game.playerB == address(0), "Game already has a second player");

Place this check before setting game.playerB in both joinGameWithEth and joinGameWithToken.

Optionally, consider locking the GameState to prevent any further joins once a player has been accepted:

game.state = GameState.Committed;

Additionally, a refund mechanism could be added for edge cases to further protect users in case of failed joins.

Updates

Appeal created

m3dython Lead Judge about 2 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Absence of State Change on Join Allows Player B Hijacking

Game state remains Created after a player joins

Support

FAQs

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