Rock Paper Scissors

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

Incomplete and incorrect game state management, absence of `Reveal` state

Summary

Incomplete and incorrect game state management, absence of Reveal state

Vulnerability Details

The game state management in contract RockPaperScissors lacks of GameState.Revealed state usage, leading to inccorect handling of entire reveal phase and mixing it with commit phase. Hereby protocol behavior becomes unstable, opening the door for a critical vulnerabilities all over the game contract implementation. Affected are almost all ot the core functions in the smart contract:

  • joinGameWithEth

  • joinGameWithToken

  • commitMove

  • revealMove

  • timeoutReveal

  • canTimeoutReveal

Impact

  • mixing commit and reveal phase

  • incorrect or missing game state transitions (both commit and reveal state)

  • incorrect and incompleted function validations

  • incorrect or irrelevant error messages

Tools Used

Manual review
Foundry

Recommendations

Implement game state management using separate reveal phase and avoid mixing it with the commit phase. Review and update all state transitions, introducing the missed state GameState.Revealed, which will make the entire contract logic more robust, consistend and will make it less error prone, closing the door for most of the critical vulnerabilites available now.

function joinGameWithEth(uint256 _gameId) external payable {
// ...
game.playerB = msg.sender;
+ game.state = GameState.Committed;
emit PlayerJoined(_gameId, msg.sender);
}
function joinGameWithToken(uint256 _gameId) external {
// ...
game.playerB = msg.sender;
+ game.state = GameState.Committed;
emit PlayerJoined(_gameId, msg.sender);
}
function commitMove(uint256 _gameId, bytes32 _commitHash) 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.Created || game.state == GameState.Committed, "Game not in commit phase");
+ require(game.state == GameState.Committed, "Game not in commit phase");
+ // most of the following code could be removed, since state transition to state Committed is moved in correcponding joinGameWith*** functions
- if (game.currentTurn == 1 && game.commitA == bytes32(0) && game.commitB == bytes32(0)) {
- // First turn, first commits
- require(game.playerB != address(0), "Waiting for player B to join");
- game.state = GameState.Committed;
- } else {
- // Later turns or second player committing
- require(game.state == GameState.Committed, "Not in commit phase");
- require(game.moveA == Move.None && game.moveB == Move.None, "Moves already committed for this turn");
- }
// ...
emit MoveCommitted(_gameId, msg.sender, game.currentTurn);
// If both players have committed, set the reveal deadline
if (game.commitA != bytes32(0) && game.commitB != bytes32(0)) {
game.revealDeadline = block.timestamp + game.timeoutInterval;
+ game.state = GameState.Revealed;
}
}
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(game.state == GameState.Revealed, "Game not in reveal phase");
require(block.timestamp <= game.revealDeadline, "Reveal phase timed out");
require(_move >= 1 && _move <= 3, "Invalid move");
// ...
}
function timeoutReveal(uint256 _gameId) 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(game.state == GameState.Revealed, "Game not in reveal phase");
require(block.timestamp > game.revealDeadline, "Reveal phase not timed out yet");
// ...
}
function canTimeoutReveal(uint256 _gameId) external view returns (bool canTimeout, address winnerIfTimeout) {
Game storage game = games[_gameId];
- if (game.state != GameState.Committed || block.timestamp <= game.revealDeadline) {
+ if (game.state != GameState.Revealed || block.timestamp <= game.revealDeadline) {
return (false, address(0));
}
// ...
}
Updates

Appeal created

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

Game State Manipulation Preventing Opponent Commit

Attack allows a player to reveal their move for the next turn before the opponent commits

Support

FAQs

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