The RockPaperScissors.sol
contract provides a cancelGame
function intended for the game creator (Player A) to cancel a game and retrieve their stake. However, this function only validates that the game.state
is GameState.Created
and that the caller is game.playerA
. Crucially, the functions used by the second player to join (joinGameWithEth
, joinGameWithToken
) set game.playerB
but do not transition the game.state
away from Created
. This discrepancy allows Player A to successfully call cancelGame
even after Player B has already joined the game and committed their stake (ETH or Token). While the underlying _cancelGame
function correctly refunds both players in this scenario, the ability for Player A to unilaterally cancel a game after Player B has joined constitutes a griefing vector, enabling unfair game termination.
cancelGame
Function Logic: This function allows cancellation based on two checks:
require(game.state == GameState.Created, "Game must be in created state");
require(msg.sender == game.playerA, "Only creator can cancel");
joinGame...
Function Behavior: The joinGameWithEth
and joinGameWithToken
functions successfully register Player B by setting game.playerB = msg.sender
and taking their stake. However, they do not modify game.state
, leaving it as GameState.Created
.
Missing Check in cancelGame
: The cancelGame
function lacks a check to verify if Player B has already joined (e.g., require(game.playerB == address(0), "Cannot cancel after player B joined");
).
Exploitation: Because the game.state
remains Created
even after Player B joins, Player A can still satisfy the conditions of the cancelGame
function and successfully execute it.
Outcome: The _cancelGame
internal function is called. It sets the state to Cancelled
and correctly refunds both Player A and Player B (since game.playerB != address(0)
).
The provided test case below demonstrates this vulnerability
This test proves that Player A can invoke the cancellation logic after Player B has already joined and staked, exploiting the insufficient state check in cancelGame
.
Griefing: Allows Player A to unfairly terminate a game after Player B has shown commitment by joining and staking. Player A might do this if they dislike the opponent, want to avoid playing, or simply to be disruptive.
Wasted Resources for Player B: Player B spends gas fees to approve token transfers (if applicable) and to call the joinGame...
function, only to have their efforts nullified by Player A's cancellation. This wastes Player B's time and funds (gas).
Violation of Implicit Agreement: Once Player B joins, there's an implicit agreement for the game to proceed. Allowing Player A to unilaterally cancel at this stage breaks this agreement.
User Trust Erosion: This capability undermines trust in the fairness and reliability of the game platform, as players cannot be certain a game will proceed even after successfully joining.
(Note: This vulnerability does not lead to direct loss of staked ETH or Tokens for Player B, as _cancelGame
handles the refund correctly. The impact is primarily related to griefing, wasted resources, and fairness.)
Manual Code Review
Foundry/Forge (for Test Execution and PoC verification)
To prevent Player A from cancelling after Player B has joined, the cancelGame
function needs an additional check to ensure Player B has not yet joined.
Add Player B Occupancy Check: Modify the cancelGame
function to include a check for game.playerB
.
This additional requirement ensures that cancelGame
can only be called by Player A before any opponent has successfully joined, aligning the function with its likely intended purpose and preventing the griefing vector.
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.