Rock Paper Scissors

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

Opponents could join for free games created with tokens

Summary

Opponents could join for free games created with tokens

Vulnerability Details

The following function RockPaperScissors::joinGameWithEth() implements the conditions required a potential opponent to join games, created with ETH bets. Though missing a critical check for msg.value amount could drive to incompatible with the protocol flow behavior - join for free games, created with tokens. When a player creates a game with tokens we have zero amount of ETH (game.bet = 0). So if the opponent or malicious user just calls the function RockPaperScissors::joinGameWithEth() with no ETH, it will fullfil the requirements and pass the validation, successfully entering the game. The only important requirement for the opponent is to have at least 1 winner token.

function joinGameWithEth(uint256 _gameId) external payable {
Game storage game = games[_gameId];
require(game.state == GameState.Created, "Game not open to join");
require(game.playerA != msg.sender, "Cannot join your own game");
require(block.timestamp <= game.joinDeadline, "Join deadline passed");
// missing check if ETH bet > 0 allows users with tokens to join the game for free
require(msg.value == game.bet, "Bet amount must match creator's bet");
game.playerB = msg.sender;
emit PlayerJoined(_gameId, msg.sender);
}

Impact

Мissing required check leads to:

  • incompatible with protocol flow behavior

  • join for free games created with ETH bets

  • not charged even with tokens

  • potencially win more tokens

Tools Used

Manual review
Foundry

PoC

Add the following test to RockPaperScissors.t.sol.

function testJoinForFreeGameCreatedWithToken() public {
// player A creates a game with token
vm.startPrank(playerA);
token.approve(address(game), 1);
gameId = game.createGameWithToken(TOTAL_TURNS, TIMEOUT);
vm.stopPrank();
uint256 initBalancePlayerB = token.balanceOf(playerB);
// player B joins the game
vm.startPrank(playerB);
vm.expectEmit(true, true, false, true);
emit PlayerJoined(gameId, playerB);
uint256 ZERO_AMOUNT = 0 ether;
// not sending any ETH
game.joinGameWithEth{value: ZERO_AMOUNT}(gameId);
vm.stopPrank();
uint256 finalBalancePlayerB = token.balanceOf(playerB);
// verify no tokens spent
assertEq(initBalancePlayerB, finalBalancePlayerB);
}

Recommendations

Add the following check to RockPaperScissors::joinGameWithEth()

function joinGameWithEth(uint256 _gameId) external payable {
Game storage game = games[_gameId];
require(game.state == GameState.Created, "Game not open to join");
require(game.playerA != msg.sender, "Cannot join your own game");
require(block.timestamp <= game.joinDeadline, "Join deadline passed");
+ require(game.bet > 0, "This game requries positive ETH bet");
require(msg.value == game.bet, "Bet amount must match creator's bet");
game.playerB = msg.sender;
emit PlayerJoined(_gameId, msg.sender);
}
Updates

Appeal created

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

Game Staking Inconsistency

joinGameWithEth function lacks a check to verify the game was created with ETH

Support

FAQs

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