The RockPaperScissors.sol
contract contains a state management flaw affecting multi-turn games. After a turn successfully completes (both players reveal on time) and it's not the final turn, the _determineWinner
function resets game variables (moveA
, moveB
, commitA
, commitB
) and sets the game.state
back to Committed
in preparation for the next turn. However, it fails to invalidate or clear the revealDeadline
associated with the just completed turn. If the revealDeadline
for the completed turn passes after this state reset, the timeoutReveal
function can be called. It incorrectly interprets the state (Committed
, deadline passed
, moves == None
) as a scenario where neither player revealed for the previous turn, leading it to erroneously call _cancelGame
. This allows a player to unfairly trigger a full game cancellation and refund after a turn has legitimately concluded.
Turn Completion (Non-Final): In a multi-turn game, when both players successfully call revealMove
for turn N
, the _determineWinner
function is triggered.
State Reset for Next Turn: Inside _determineWinner
, if game.currentTurn < game.totalTurns
, the following state changes occur to prepare for turn N+1
:
game.currentTurn
is incremented.
game.commitA
and game.commitB
are reset to bytes32(0)
.
game.moveA
and game.moveB
are reset to Move.None
.
game.state
is set back to GameState.Committed
.
Persistent Deadline: Crucially, the game.revealDeadline
which was set when both players committed for turn N
is not reset or cleared during this state transition.
Timeout Window: A window opens after the state reset for turn N+1
but before the next turn significantly progresses. During this window, the revealDeadline
for turn N
may pass (block.timestamp > game.revealDeadline
).
timeoutReveal
Called: If timeoutReveal(_gameId)
is called during this window:
The check require(game.state == GameState.Committed, ...)
passes because the state was reset for turn N+1
.
The check require(block.timestamp > game.revealDeadline, ...)
passes because the deadline for turn N
has expired.
The status checks bool playerARevealed = game.moveA != Move.None;
and bool playerBRevealed = game.moveB != Move.None;
both evaluate to false
because the moves were reset to Move.None
by _determineWinner
.
Consequently, the condition else if (!playerARevealed && !playerBRevealed)
becomes true.
Incorrect Action: timeoutReveal
incorrectly concludes that neither player revealed for turn N
and calls the internal function _cancelGame(_gameId)
.
The provided test case below demonstrates the vulnerability
Unfair Game Termination: Allows a player (potentially one who is losing or anticipates losing) to force the cancellation of a game after a turn has been legitimately won or lost.
Denial of Service (Game Progression): Prevents the game from reaching its natural conclusion based on the agreed number of turns and player skill/luck.
Griefing: Enables malicious players to disrupt ongoing multi-turn games and force refunds, wasting opponents' time and potentially gas fees.
User Trust Erosion: Players expect games to proceed according to the rules; unexpected cancellations after successful turns undermine confidence in the platform's fairness and reliability. (Note: This specific bug leads to refunds, not direct fund theft, but disrupts the game).
Manual Code Review
Foundry/Forge (for Test Execution and PoC verification)
The root cause is the failure to invalidate the previous turn's revealDeadline
when resetting the state for the next turn.
Clear revealDeadline
on Turn Progression: Modify the _determineWinner
function. Within the if (game.currentTurn < game.totalTurns)
block, after resetting other variables, explicitly reset the deadline:
This ensures that once a turn is processed, its associated deadline can no longer trigger the timeoutReveal
logic.
Add Deadline Check in timeoutReveal
(Optional Robustness): As an additional safeguard, consider adding a check in timeoutReveal
to ensure the deadline being checked is non-zero. This prevents potential issues if the deadline was somehow already zero.
Implementing Recommendation 1 is essential to fix the vulnerability. Recommendation 2 adds an extra layer of defense.
timeoutReveal function incorrectly allows execution and game cancellation even when only one player has committed
timeoutReveal function incorrectly allows execution and game cancellation even when only one player has committed
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.