Rock Paper Scissors

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

Funds are locked in a game if one of players never commits his move

Summary

Funds can be locked forever in a game in a situation when one of players never commits his move due to either malicious behaviour or life-related event.

Vulnerability Details

There might be a situation when both players entered the game and then one of the players commits its move as a hashed value and waits for the other player. However, the second player never commits their move due to being malicious or because of some life-related events which prevent them from submitting the move. As a result, the first player who submitted a move wants to exit the game and return their locked funds, but they are stuck in the game because the cancelGame function cannot be called (the game state is not Created anymore), and timeoutReveal function cannot be called either.

Impact

Users' funds are locked in the game without possibility to retrieve them.

Tools Used

  • Manual code review

  • Foundry

PoC

The following proof of code represents a situation where Player A creates game with ETH bet. and waits for another player to join.
When Player B joined the game, the Player A decides to make first move and commits move.

When 2 days passed and no there no move from the Player B, Player A decides to cancel the game and retrieve his funds, but cannot do so.

The following test_fundsStuckWhenOnePlayerDoesntCommitMove test function can be placed in the RockPaperScissorsTest.t.sol file:

function test_fundsStuckWhenOnePlayerDoesntCommitMove() public {
// First we create a game. It doesn't matter if bet on ETH on game tokens
vm.prank(playerA);
gameId = game.createGameWithEth{value: BET_AMOUNT}(1, TIMEOUT);
// After creation another player decides to join the game
vm.prank(playerB);
game.joinGameWithEth{value: BET_AMOUNT}(gameId);
// Player A decides to make a first move and creates commit
bytes32 saltA = keccak256(abi.encodePacked("salt for player A"));
bytes32 commitA = keccak256(abi.encodePacked(uint8(RockPaperScissors.Move.Rock), saltA));
// Player A commits move and waits for his opponent
vm.prank(playerA);
game.commitMove(gameId, commitA);
// For some reason Player B doesn't commit his move for 2 days. It can be either mallicios behavior or some other incident that prevented player from committing move
vm.warp(block.timestamp + 2 days);
// Player A decides that he doesn't want to wait any longer and wants to get his bet back
// He decides to try available function, but none of them work
// cancelGame function will fail because game is not in the Created state anymore and the timeoutReveal will fail because reveal state hasn't started yet
vm.prank(playerA);
game.cancelGame(gameId);
// Verify game state
(, , , , , , , , , , , , , , , RockPaperScissors.GameState state1) = game.games(gameId);
assertEq(uint256(state1), uint256(RockPaperScissors.GameState.Cancelled));
}

Recommendations

Add commit phase timeout with a new `commitDeadline` and timeout logic:

struct Game {
...
+ uint256 commitDeadline;
}
function commitMove(...) public {
...
if (game.commitA != bytes32(0) && game.commitB != bytes32(0)) {
game.revealDeadline = block.timestamp + game.timeoutInterval;
+ } else if (game.commitA != bytes32(0) || game.commitB != bytes32(0)) {
+ game.commitDeadline = block.timestamp + commitTimeoutInterval;
}
}
function timeoutCommit(uint256 gameId) external {
Game storage game = games[gameId];
+ require(block.timestamp > game.commitDeadline, "Commit timeout not reached");
_cancelGame(gameId);
}
Updates

Appeal created

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

Player B cannot cancel a game if Player A becomes unresponsive after Player B joins

Protocol does not provide a way for Player B to exit a game and reclaim their stake if Player A stops participating

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

Player B cannot cancel a game if Player A becomes unresponsive after Player B joins

Protocol does not provide a way for Player B to exit a game and reclaim their stake if Player A stops participating

Support

FAQs

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