Rock Paper Scissors

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

Missing Validation in joinGameWithEth Allows Free Participation in Token-Based Games

Summary

The RockPaperScissors contract contains a critical validation flaw in the joinGameWithEth() function. The function fails to validate whether a game requires an ETH bet or a token bet. This allows a malicious player to join token-based games through the ETH betting function without providing the required token to be deposited, essentially participating for free while still being eligible to win the game.

Scenario:

PlayerA creates a game based on Token bet calling `createGameWithToken()`.
PlayerB joins that gameId by calling `joinGameWtihEth()` without paying.
PlayerB joins for free and becomes eligible to win.

Vulnerability Details

The contract implements two separate functions for joining games: joinGameWithEth() for ETH-based games and joinGameWithToken() for token-based games. However, the joinGameWithEth() function only checks that the sent ETH value matches the game's bet amount without verifying whether the game was intended to be played with tokens or Eth to begin with.

In token-based games, the bet value is set to 0 ETH, but the participant is expected to provide a token. A malicious player can exploit this by calling joinGameWithEth() with 0 ETH for a token-based game, bypassing the token requirement entirely. It is worth noting that on the other side the joinGameWithToken() function is validated to be only used for token-based games. This is already implemented with the check require(game.bet == 0, "This game requires ETH bet");.

// Proof of Concept
function testJoinGameWithTokenByCallingJoinGameWithEth() public {
// First create a game with token
vm.startPrank(playerA);
token.approve(address(game), 1);
gameId = game.createGameWithToken(TOTAL_TURNS, TIMEOUT);
vm.stopPrank();
// Now join the game with token
vm.startPrank(playerB);
vm.expectEmit(true, true, false, true);
emit PlayerJoined(gameId, playerB);
game.joinGameWithEth(gameId);
vm.stopPrank();
// Verify attacker maintained his balance while succeeding to join the game
assertEq(token.balanceOf(playerB), 10);
assertEq(token.balanceOf(address(game)), 1);
// Verify game state
(address storedPlayerA, address storedPlayerB,,,,,,,,,,,,,, RockPaperScissors.GameState state) =
game.games(gameId);
assertEq(storedPlayerA, playerA);
assertEq(storedPlayerB, playerB);
assertEq(uint256(state), uint256(RockPaperScissors.GameState.Created));
}

Impact

  • Attackers can join token-based games without providing the required token stake.

  • This creates an unfair advantage as they risk nothing while legitimate players commit their tokens.

  • If the attacker wins, they collect the opponent's tokens for free.

  • This fundamentally breaks the game's economic model and trust assumptions.

  • Scope: All token-based games in the system are affected by this vulnerability.

Tools Used

  • Foundry

Recommendations

  • Add a validation check in the joinGameWithEth() function to ensure it can only be used for ETH-based games:

  • Consider introducing a specific game type flag in the Game struct to explicitly differentiate between ETH-based and token-based games, making the validation explicit in code.

Updates

Appeal created

m3dython Lead Judge 2 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

m3dython Lead Judge 2 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.