(Medium) Arbitrary Reveal Timeout Griefing - Allows a player to lock opponent's funds/tokens for an arbitrary duration.
When creating a game, Player A specifies the _timeoutInterval, which is the duration allowed for players to reveal their moves after both have committed. The contract enforces a minimum of 5 minutes but no maximum.
A malicious Player A can set this interval to an extremely large value (e.g., several years).
If Player B joins, both commit, and Player B fails to reveal their move, Player A (or anyone) cannot call timeoutReveal to resolve the game and potentially reclaim funds/tokens until this excessively long timeoutInterval has fully elapsed.
This griefs Player B by locking their funds/tokens for an arbitrary period dictated by Player A.
The createGameWithEth
and createGameWithToken
functions accept _timeoutInterval
as an argument from the game creator (Player A).
This value is stored in the Game struct and used to calculate the revealDeadline
.
The revealMove
function requires block.timestamp <= game.revealDeadline
, and timeoutReveal
requires block.timestamp > game.revealDeadline
.
The only check on _timeoutInterval
is require(_timeoutInterval >= 5 minutes
, "Timeout must be at least 5 minutes");.There is no upper bound validation.
If Player A creates a game with a very large _timeoutInterval, and the game reaches the Committed state where both players have committed but Player B fails to reveal:
Player A cannot reveal for B.
Player A cannot call timeoutReveal until block.timestamp exceeds the very large revealDeadline (block.timestamp + very_large_timeout).
Player B's ETH (in ETH games) or Token (in Token games) remains locked in the contract until Player A (or someone else) can successfully call timeoutReveal after the long timeout period.
Player B is grieved as their funds are inaccessible for an unreasonable duration.
Code Location:
Prerequisites:
Player A (attacker) wants to lock up Player B's funds/tokens.
Steps:
Player A calls createGameWithEth (or createGameWithToken), providing a valid _totalTurns and an extremely large value for _timeoutInterval (e.g., 100 years). Player A sends the required bet amount or transfers the token.
Player B joins the game by calling joinGameWithEth (or joinGameWithToken), sending the required bet amount or transferring the token. Player B's funds/tokens are now held by the contract.
Player A and Player B both call commitMove for the first turn. The game state transitions to Committed, and the revealDeadline is set to block.timestamp + extremely_large_timeoutInterval.
Player A calls revealMove.
Player B fails to call revealMove for the current turn.
Player A (or anyone attempting to resolve the game) tries to call timeoutReveal. This call will revert with "Reveal phase not timed out yet" because block.timestamp is not greater than the revealDeadline (which is set far in the future).
Player B's funds/tokens (and potentially Player A's, although A could win by timeout eventually) are now locked in the contract until the revealDeadline passes.
The vulnerability doesn't allow direct theft of funds, but it enables griefing and denial of access to funds/tokens for the opposing player (and potentially the creator if the opponent acts maliciously) for an arbitrarily long, attacker-defined duration. This makes the game impractical or unusable if a malicious player decides to lock up deposits.
Manual + AI Review
AI Assistance in understanding impact and probable exploitation scenarios.
Recommendations Implement an upper bound for the _timeoutInterval parameter in createGameWithEth and createGameWithToken. The maximum value should be reasonable for a game (e.g., a few days or weeks, not years).
Attack allows a player to reveal their move for the next turn before the opponent commits
Attack allows a player to reveal their move for the next turn before the opponent commits
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.