Rock Paper Scissors

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

Improper Validation in `RockPaperScissors::timeoutReveal` Allows Premature Game Forfeits

Description: he RockPaperScissors::timeoutReveal function is intended to handle situations where one or both players fail to reveal their committed moves after the revealDeadline. However, there is no check to ensure that game.revealDeadline has been properly set before using it in the statement:

require(block.timestamp > game.revealDeadline, "Reveal phase not timed out yet");

If game.revealDeadline is still at its default value of 0 (e.g., when only one player has committed a move), this require statement will always pass, as block.timestamp will always be greater than zero. This allows any player to prematurely invoke timeoutReveal immediately after committing a move—before the opponent even has a chance to respond.

Impact: A malicious player could join multiple games, commit a move, and instantly call timeoutReveal to disrupt the game flow. While this may not result in financial loss, it can prevent legitimate gameplay, effectively acting as a denial-of-service attack against the contract’s normal operations.

Proof of Concept: Please refer to [H-1], where this exploit was also demonstrated

Recommended Mitigation: Add a validation check to ensure game.revealDeadline has been explicitly set before performing the timeout logic. This prevents the function from being abused before both players have committed their moves and the reveal phase has officially started.

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.revealDeadline != 0, "Reveal Deadline not set yet");
require(block.timestamp > game.revealDeadline, "Reveal phase not timed out yet");
// If player calling timeout has revealed but opponent hasn't, they win
bool playerARevealed = game.moveA != Move.None;
bool playerBRevealed = game.moveB != Move.None;
if (msg.sender == game.playerA && playerARevealed && !playerBRevealed) {
// Player A wins by timeout
_finishGame(_gameId, game.playerA);
} else if (msg.sender == game.playerB && playerBRevealed && !playerARevealed) {
// Player B wins by timeout
_finishGame(_gameId, game.playerB);
} else if (!playerARevealed && !playerBRevealed) {
// Neither player revealed, cancel the game and refund
_cancelGame(_gameId);
} else {
revert("Invalid timeout claim");
}
}
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.Committed || (game.revealDeadline != 0 && block.timestamp <= game.revealDeadline)) {
return (false, address(0));
}
bool playerARevealed = game.moveA != Move.None;
bool playerBRevealed = game.moveB != Move.None;
if (playerARevealed && !playerBRevealed) {
return (true, game.playerA);
} else if (!playerARevealed && playerBRevealed) {
return (true, game.playerB);
} else if (!playerARevealed && !playerBRevealed) {
return (true, address(0)); // Both forfeit
}
return (false, address(0));
}
Updates

Appeal created

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

Invalid TimeoutReveal Logic Error

timeoutReveal function incorrectly allows execution and game cancellation even when only one player has committed

Support

FAQs

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