Rock Paper Scissors

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

Locked tokens in Rock Paper Scissors game contract with continuous minting

Summary

The RockPaperScissors.sol contract has a design flaw in its token-based game logic. When players participate in token games, they transfer tokens to the contract, but these tokens remain permanently locked in the contract as the game mints new tokens for winners instead of returning the original ones. This creates unbounded token supply inflation and resource inefficiency.

Vulnerability Details

In token-based games, the contract collects tokens from both players but never returns these original tokens to circulation:

// From createGameWithToken()
winningToken.transferFrom(msg.sender, address(this), 1);
// From joinGameWithToken()
winningToken.transferFrom(msg.sender, address(this), 1);

When games end (win, tie, or cancel), instead of returning the collected tokens, the contract mints new ones:

// From _finishGame() for token games
if (game.bet == 0) {
// Mint a winning token
winningToken.mint(_winner, 2);
}
// From _handleTie() for token games
if (game.bet == 0) {
winningToken.mint(game.playerA, 1);
winningToken.mint(game.playerB, 1);
}
// From _cancelGame() for token games
if (game.bet == 0) {
if (game.playerA != address(0)) {
winningToken.mint(game.playerA, 1);
}
if (game.playerB != address(0)) {
winningToken.mint(game.playerB, 1);
}
}

This pattern causes tokens to accumulate indefinitely in the contract while simultaneously increasing total token supply, as verified by test results. The following test proves that. You can paste it in the RockPaperScissorsTest.t.sol contract.

function testGameTokenBalance() public {
// First create a game with token
vm.startPrank(playerA);
token.approve(address(game), 1);
gameId = game.createGameWithToken(TOTAL_TURNS, TIMEOUT);
vm.stopPrank();
// Now join the game with token
vm.startPrank(playerB);
token.approve(address(game), 1);
vm.expectEmit(true, true, false, true);
emit PlayerJoined(gameId, playerB);
game.joinGameWithToken(gameId);
vm.stopPrank();
playTurn(gameId, RockPaperScissors.Move.Rock, RockPaperScissors.Move.Paper);
playTurn(gameId, RockPaperScissors.Move.Scissors, RockPaperScissors.Move.Rock);
playTurn(gameId, RockPaperScissors.Move.Paper, RockPaperScissors.Move.Scissors);
// playerB wins
// Verify game state
(,,,,,,,,,,,,,,, RockPaperScissors.GameState state) = game.games(gameId);
assertEq(uint256(state), uint256(RockPaperScissors.GameState.Finished));
// Verify player B received a winner token
assertEq(token.balanceOf(playerB), 11);
console.log("Balance of playerB after the win: ", token.balanceOf(playerB)); // returns 11
assertEq(token.balanceOf(address(game)), 2);
console.log("Balance of contract: ", token.balanceOf(address(game))); // returns 2
}

Impact

The current implementation has several negative consequences:

  1. Token supply inflation: As more games are played, the total supply continuously increases without bounds, diluting the token value.

  2. Resource Inefficiency: Original tokens remain locked in the contract.

Tools Used

-Manual analysis

Recommendations

Update the token management logic to transfer existing tokens rather than minting new ones.

Updates

Appeal created

m3dython Lead Judge 4 months 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

m3dython Lead Judge 4 months 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.