Rock Paper Scissors

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

joinGameWithEth() & joinGameWithToken() allow joining multiple times resulting in loss of funds

Summary

RockPaperScissors.sol::joinGameWithEth() & RockPaperScissors.sol::joinGameWithToken() allow joining multiple times as Player B, resulting in the loss of the participation funds for the past Player(s) B.

Vulnerability Details

joinGameWithEth() & joinGameWithToken() allow joining multiple times as Player B, the problem is that Player B has to send ETH or Tokens to be able to join.
When another player joins, now the former one is erased he no longer is the "Player B", instead a new player is taking his place. But his participation in the game
(ETH or Tokens) is still locked in the contract with no way to recover the funds.

https://github.com/CodeHawks-Contests/2025-04-rock-paper-scissors/blob/main/src/RockPaperScissors.sol#L154

function joinGameWithEth(uint256 _gameId) external payable {
Game storage game = games[_gameId];
require(game.state == GameState.Created, "Game not open to join");
require(game.playerA != msg.sender, "Cannot join your own game");
require(block.timestamp <= game.joinDeadline, "Join deadline passed");
require(msg.value == game.bet, "Bet amount must match creator's bet");
game.playerB = msg.sender;
emit PlayerJoined(_gameId, msg.sender);
}

https://github.com/CodeHawks-Contests/2025-04-rock-paper-scissors/blob/main/src/RockPaperScissors.sol#L170

function joinGameWithToken(uint256 _gameId) external {
Game storage game = games[_gameId];
require(game.state == GameState.Created, "Game not open to join");
require(game.playerA != msg.sender, "Cannot join your own game");
require(block.timestamp <= game.joinDeadline, "Join deadline passed");
require(game.bet == 0, "This game requires ETH bet");
require(winningToken.balanceOf(msg.sender) >= 1, "Must have winning token");
// Transfer token to contract
winningToken.transferFrom(msg.sender, address(this), 1);
game.playerB = msg.sender;
emit PlayerJoined(_gameId, msg.sender);
}

Impact

Loss of participation funds for player B if someone else joins after him. No way to recover the funds stuck in the contract.

Tools Used

Github, manual review.

Recommendations

When the second player joins (Player B), the contract should not allow another player to take his place, it should check if there is already a Player B : require (game.playerB != address(0));

Or it should refund the former one before accepting another player to take his place.
The first solution seems fairer.

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.