(HIGH) Selective Revealing / Front-running -Allows a player to gain an unfair advantage by reacting to opponent's reveal.
A player can observe their opponent's move reveal transaction in the mempool before it is mined.
By learning the opponent's move, the player can then decide whether to reveal their own committed move (if it results in a win or tie for that turn) or refrain from revealing, forcing a game timeout.
This allows the player to avoid revealing a losing move, thereby avoiding a turn loss and potentially influencing the final game outcome or forcing a cancellation, gaining an unfair advantage based on off-chain information and transaction ordering.
The contract uses a commit-reveal scheme where players first submit a hash of their move and a salt (commitMove), and later reveal the actual move and salt (revealMove)
. The revealMove
function checks that the revealed move/salt combination matches the committed hash.
Once both players have revealed for a turn, the winner of the turn is determined and scores are updated (_determineWinner)
.
If a player fails to reveal their move within the revealDeadline, the opponent (or anyone) can call timeoutReveal to end the entire game (not just the turn) and claim the prize or force cancellation, depending on who did/didn't reveal.
The vulnerability arises because one player (e.g., Player A) might submit their revealMove transaction, which enters the public mempool. The opponent (Player B) can monitor the mempool, identify A's transaction, extract the revealed moveA and saltA, and verify them against game.commitA. Knowing moveA, Player B can determine the outcome of the current turn if they were to reveal their own committed move (moveB).
The problematic sequence is:
Both players commit valid moves (commitA, commitB). revealDeadline is set.
Player A calls revealMove(gameId, moveA, saltA). Transaction TxA enters the mempool.
Player B monitors the mempool, reads moveA from TxA.
Player B knows their committed move moveB. B compares moveB and moveA to determine the turn outcome.
Exploitation: If moveB would lose to moveA, Player B does not submit their own revealMove transaction (TxB), or cancels any pending TxB. Player B waits for the revealDeadline to pass. Then Player A (who did reveal) can call timeoutReveal. According to the logic in timeoutReveal (Lines 269-284), if Player A revealed (playerARevealed true) and Player B did not (playerBRevealed false), Player A wins the entire game.
If moveB would win or tie against moveA, Player B submits their revealMove transaction (TxB), potentially with higher gas to front-run TxA if desired, or just lets TxA confirm and then submits TxB. The turn plays out normally according to the game rules.
Prerequisites:
A game has been created and joined by Player A and Player B.
Both Player A and Player B have committed their moves for the current turn.
The game is in the Committed state and block.timestamp <= game.revealDeadline.
steps:
Player A prepares and sends a revealMove transaction (TxA). This transaction enters the public mempool.
Player B (the attacker) monitors the mempool. They intercept TxA and extract the revealed move and salt for Player A.
Player B verifies that the revealed move/salt matches Player A's commitment hash (game.commitA) to confirm its validity.
Player B determines their own committed move (moveB) and simulates the outcome of the turn if moveB were played against moveA.
If moveB wins or ties against moveA, Player B proceeds as normal and submits their own revealMove transaction (TxB) for the current turn.
If moveB loses against moveA, Player B does not submit their revealMove transaction (TxB), or cancels any pending TxB.
Player B waits for the revealDeadline to pass.
After the revealDeadline passes, Player A (or anyone) calls timeoutReveal(_gameId).
Since Player A revealed (playerARevealed is true) and Player B did not reveal (playerBRevealed is false), the condition msg.sender == game.playerA && playerARevealed && !playerBRevealed in timeoutReveal evaluates to true (if A calls it), or the equivalent for B (if B calls it). Assuming A calls it, Player A is declared the winner of the entire game. Player B successfully avoided revealing a losing move, forcing a timeout instead of a normal turn resolution.
The impact is that Player B avoids a turn loss by strategically failing to reveal, forcing a game timeout win for Player A instead. While Player B loses the game and their bet/token, they might prefer this outcome over potentially losing subsequent turns and the game later, especially if they are already behind on points. This breaks the fairness premise that both players commit and reveal their moves blind to the opponent's move for that turn.
High. This vulnerability allows a player to cheat by adapting their strategy based on the opponent's revealed move for the current turn. While they cannot change their committed move, they can strategically choose not to reveal a losing move, forcing a game-ending timeout and potentially changing the overall outcome compared to a fair reveal process. This undermines the core fairness of the commit-reveal mechanism.
Manual code review
Use AI to examine impact and write report
Preventing selective revealing entirely in a commit-reveal scheme on a public blockchain is challenging due to transaction ordering. Potential mitigations include:
Using a trusted relayer or sequencer: Not feasible for a public decentralized game.
Batching reveals: Requires both players' reveals to be included in the same transaction or block. Hard to enforce and adds complexity/cost.
Economic Incentives/Penalties: Implement a mechanism where failing to reveal after committing incurs a greater penalty than revealing a losing move and losing the turn. Currently, not revealing results in losing the entire game via timeout, which is already a significant penalty (loss of bet/token). However, if the game is multi-turn, losing one turn might be better than losing the whole game later.
Adjusting Timeout Outcome: Modify timeoutReveal so that if a player reveals and the opponent doesn't, the revealing player doesn't win the entire game immediately, but perhaps wins the current turn by default, and the game continues. The opponent who failed to reveal could face a penalty (e.g., loss of their bet for that turn if multi-bet, or points penalty). This makes selective revealing less of an "escape losing the game" strategy and more of a "forfeit the current turn" strategy, which is a less significant advantage. However, this game is structured as winner-takes-all after total turns or timeout, making per-turn outcomes less relevant for final payout on timeout.
Given the game's structure, modifying the timeoutReveal logic to potentially not end the entire game on a single turn timeout might be the most impactful change to reduce the value of selective revealing, but this would require a significant re-architecture of the timeout mechanism in _determineWinner and timeoutReveal.
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.