Rock Paper Scissors

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

Attacker can copy the `_commitHash` in `commitMove`, making the game continuously end up in a tie.

Summary

Because the _commitHash itself is not user-bound (i.e. the content of the hash is not bound to a specific user), an attacker can copy the hash that a player submitted using commitMove from the mempool, and re-submit the same commit hash, continuously making the turn, and eventually the game, into a tie.

Vulnerability Details

The commit hash only contains the move and _salt as shown below:

bytes32 commit = keccak256(abi.encodePacked(move, _salt));

A malicious attacker can see this hash inside the mempool when a user calls commitMove, and commit the exact same has as his/her commit hash. Because revealMove does not check if the hash is derived from a certain player and only checks the move and the _salt, if only the attacker figures out the _salt, he will be able to successfully reveal his move and turn the game(or turn) into a tie.

For example,

  1. PlayerA commit a hashed move using commitMove. The _commitHash used in this process is generated by PlayerA's move and _salt.

  2. Attacker sees this _commitHash in the mempool, and commit a hashed move using the same _commitHash by calling commitMove.

  3. PlayerA calls revealMove and reveals his move.

  4. Attacker sees the transaction when PlayerA is calling revealMove, and obtains the _salt used when playerA hashed his move.

  5. Attacker calls revealMove and revelas his move.

  6. The turn ends up in a tie.

This process could happen for every turn, eventually making the whole game into a tie. Even though _totalTurns is an odd number, all the turns will end up in a tie and will make the whole game end up in a tie.

Impact

The game will always end in a tie. This is a severe disruption of functionality, making the game unplayable.

Tools Used

VSCode

Recommendations

When committing a hashed move in commitMove, use a player-bound _commitHash, which can be later checked in revealMove.

// use a '_commitHash' that includes an information about the player(user) making the commit
function commitMove(uint256 _gameId, bytes32 _commitHash) external {

In revealMove, add the caller as part of the hash,

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));
+ bytes32 commit = keccak256(abi.encodePacked(move, msg.sender, _salt));

This way the _commitHash is bound to a specific player(i.e. it is unique), and prevents others from using the same commit hash.

Updates

Appeal created

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