Summary
When a game is created using RockPaperScissors::createGameWithToken()
, players can call RockPaperScissors::joinGameWithEth()
with no value to join the game for free.
Vulnerability Details
When a user calls RockPaperScissors::createGameWithToken()
, it sets game.bet = 0
.
function createGameWithToken(uint256 _totalTurns, uint256 _timeoutInterval) external returns (uint256) {
require(winningToken.balanceOf(msg.sender) >= 1, "Must have winning token");
require(_totalTurns > 0, "Must have at least one turn");
require(_totalTurns % 2 == 1, "Total turns must be odd");
require(_timeoutInterval >= 5 minutes, "Timeout must be at least 5 minutes");
winningToken.transferFrom(msg.sender, address(this), 1);
uint256 gameId = gameCounter++;
Game storage game = games[gameId];
game.playerA = msg.sender;
@> game.bet = 0;
game.timeoutInterval = _timeoutInterval;
game.creationTime = block.timestamp;
game.joinDeadline = block.timestamp + joinTimeout;
game.totalTurns = _totalTurns;
game.currentTurn = 1;
game.state = GameState.Created;
emit GameCreated(gameId, msg.sender, 0, _totalTurns);
return gameId;
}
This means that another user can call RockPaperScissors::joinGameWithEth()
with {value: 0}
and join the game for free.
Add the following code to the test suite:
function testPlayerBCanJoinTokenGameForFree() public {
vm.startPrank(playerA);
token.approve(address(game), 1);
gameId = game.createGameWithToken(TOTAL_TURNS, TIMEOUT);
vm.stopPrank();
vm.startPrank(playerB);
console2.log("playerB token balance before joining: ", token.balanceOf(playerB));
console2.log("playerB ETH balance before joining: ", playerB.balance);
vm.expectEmit();
emit PlayerJoined(gameId, address(playerB));
game.joinGameWithEth{value: 0}(gameId);
console2.log("playerB token balance after joining: ", token.balanceOf(playerB));
console2.log("playerB ETH balance before joining: ", playerB.balance);
}
The output is:
Ran 1 test for test/RockPaperScissorsTest.t.sol:RockPaperScissorsTest
[PASS] testPlayerBCanJoinTokenGameForFree() (gas: 244672)
Logs:
playerB token balance before joining: 10
playerB ETH balance before joining: 10000000000000000000
playerB token balance after joining: 10
playerB ETH balance before joining: 10000000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 11.40ms (1.86ms CPU time)
Impact
Anyone can join games created with RockPaperScissors::createGameWithToken
for free and play with zero risk, effectively gaming the protocol.
Tools Used
Manual review
Recommendations
Require game.bet > 0
when calling 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, "Bet amount cannot be zero");
require(msg.value == game.bet, "Bet amount must match creator's bet");
game.playerB = msg.sender;
emit PlayerJoined(_gameId, msg.sender);
}