Rock Paper Scissors

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

Lack of Player Address Binding in Move Commitments Allows Replay and Cheating in RockPaperScissors Contract

Summary

The RockPaperScissors smart contract fails to bind player addresses to their committed moves, allowing malicious actors to reuse or intercept another player's commitment. This vulnerability undermines the fairness of the game and opens the door for replay attacks or unauthorized move revelations.

Vulnerability Details

The commit-reveal mechanism implemented in the RockPaperScissors contract allows players to commit to their moves by hashing the move and a secret. However, the contract does not include the player's address in the commitment hash, which leads to the following issues:

  1. Replay Attack:
    A malicious player (e.g., Player B) can copy a previously observed commitment from the other player (Player A) and use it as their own, effectively mirroring moves without knowing the secret.

  2. Reveal Phase Exploitation: If Player A later reveals their move and secret, Player B can use the same values to reveal too, and pass the hash check. Since there’s no per-player validation of who originally committed the move, B’s reveal will be accepted.

  3. Game Integrity Compromise:
    This allows Player B to always mirror Player A’s commitment, guaranteeing at least a draw or even a win if they selectively reveal. It also opens the door to front-running, collusion, or scripted manipulation in a competitive setting.


Impact

The vulnerability allows a malicious player to replicate a legitimate player's commitment and reveal actions, because the contract does not bind moves to player addresses. This breaks the integrity and fairness of the game, enabling:

  • Commitment spoofing (e.g., B copies A's move).

  • Forced draws or avoided losses through selective reveals.

  • Unfair wins, leading to loss of rewards or funds.

Tools Used

  • Manual Code Review

  • Foundry (Test framework)

  • Custom test cases simulating malicious reveal and replay scenarios

function testGameFairNess() public {
gameId = createAndJoinGame();
// Player A commits
uint8 moveA = 1; // Rock
bytes32 saltA = keccak256(abi.encodePacked("salt for player A"));
bytes32 commitA = keccak256(abi.encodePacked(moveA, saltA));
vm.prank(playerA);
game.commitMove(gameId, commitA);
// Player B can wait Player A to commit and commit the same
// hash
bytes32 commitB = commitA ;
vm.prank(playerB);
game.commitMove(gameId, commitB);
// A reveals first
vm.prank(playerA);
game.revealMove(gameId, moveA, saltA);
// B now copies move/salt of player A and reveals
vm.prank(playerB);
game.revealMove(gameId, moveA, saltA);
// we will have a tie and no one will win
}

Recommendations

  1. Bind Commitments to Player Address:

    Change the commitment scheme to include the player’s address when generating the hash, e.g.:

bytes32 commitment = keccak256(abi.encodePacked(move, secret, msg.sender);
  1. Update Reveal Logic Accordingly:
    Ensure that during the reveal phase, the hash is recomputed using msg.sender, the revealed move, and the secret, and that it matches the stored commitment.

  2. Test for Malicious Behavior:
    Add unit tests to ensure that a player cannot reuse another’s commitment or spoof reveals.

Updates

Appeal created

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

Lack of Salt Uniqueness Enforcement

The contract does not enforce salt uniqueness

Support

FAQs

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