Rock Paper Scissors

First Flight #38
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: low
Valid

Unrestricted Token Minting in `RockPaperScissors.sol`

Summary

I see that there's some functions that mint new tokens to the winner instead of sending it from the the RockPaperScissors contract which is the wallet, and already player pays the token for creating game or joining the game.

Vulnerability Details

  1. At _finishGame() in https://github.com/CodeHawks-Contests/2025-04-rock-paper-scissors/blob/25cf9f29c3accd96a532e416eee6198808ba5271/src/RockPaperScissors.sol#L472
    the game winner are payed by minting new tokens instead of sending from the stored token that they payed.

  2. At _handleTie() in https://github.com/CodeHawks-Contests/2025-04-rock-paper-scissors/blob/25cf9f29c3accd96a532e416eee6198808ba5271/src/RockPaperScissors.sol#L511 players are refunded by minting new tokens instead of sendback from the tokens they send and stored from the admin wallet/contract

  3. In _cancelGame() at https://github.com/CodeHawks-Contests/2025-04-rock-paper-scissors/blob/25cf9f29c3accd96a532e416eee6198808ba5271/src/RockPaperScissors.sol#L547 the players are payed back by minting new tokens instead of sending from what they have send to play the game

POC

Add the following test code in RockPaperScissorsTest.t.sol

// Test if the new token was minted and instead of sending new token
function testTokenMintedInsteadOfSend() public {
uint256 adminBalanceBefore = token.balanceOf(address(game)); // admin balance before players create and joins the game
uint256 playerABalanceBefore = token.balanceOf(address(playerA)); // playerA balance before
uint256 playerBBalanceBefore = token.balanceOf(address(playerB)); // playerB balance before
gameId = createAndJoinTokenGame();
// First turn: A=Paper, B=Rock (A wins)
playTurn(gameId, RockPaperScissors.Move.Paper, RockPaperScissors.Move.Rock);
// Second turn: A=Rock, B=Scissors (A wins)
playTurn(gameId, RockPaperScissors.Move.Rock, RockPaperScissors.Move.Scissors);
// Third turn: A=Paper, B=Scissors (B wins, but A still has more points)
playTurn(gameId, RockPaperScissors.Move.Paper, RockPaperScissors.Move.Scissors);
uint256 adminBalanceAfter = token.balanceOf(address(game)); //token balance of admin after the end of the game
uint256 playerABalanceAfter = token.balanceOf(address(playerA)); // playerA balance after
uint256 playerBBalanceAfter = token.balanceOf(address(playerB)); // playerB balance after
// Print the balances in console
console.log("admin balance before");
console.log(adminBalanceBefore);
console.log("Player A balance before");
console.log(playerABalanceBefore);
console.log("Player B balance before");
console.log(playerBBalanceBefore);
console.log("admin balance after");
console.log(adminBalanceAfter);
console.log("Player A balance after");
console.log(playerABalanceAfter);
console.log("Player B balance after");
console.log(playerBBalanceAfter);
assertEq(adminBalanceBefore, 0);
assertEq(adminBalanceAfter, 2);
}

and Run the test with foundry using

forge test --mt testTokenMintedInsteadOfSend

I used console.log() to print balances of admin and players before the game and after the game,
You will see that admin balance before the game is 0 and 2 after the game while the winner has been paid from nowhere ( mint)

Impact

The game can mint unlimited tokens, devaluing them.

Tools Used

Manual review

Recommendations

Pay players by sending from what they have payed before playing the game ( from admin wallet )

Updates

Appeal created

m3dython Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

Minting Instead of Transferring Staked Tokens

Mints new tokens upon game completion or cancellation for token-based games

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.