Rock Paper Scissors

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

Token Supply Inflation Due to Improper Minting in Prize Distribution

Summary

The protocol mints new tokens to reward winners in token-based games instead of transferring the originally deposited tokens. This leads to token supply inflation and locks deposited tokens in the contract indefinitely.

Vulnerability Details

Affected Functions:

  1. _finishGame()

  2. _handleTie()

  3. _cancelGame()

Root Cause:

  • In token-based games (bet == 0), players deposit 1 RPSW token each (total 2 tokens held by the contract).

  • The current implementation mints new tokens to reward winners/players instead of transferring the deposited tokens:

    // _finishGame():
    winningToken.mint(_winner, 2); // ❌ Mints new tokens
    // _handleTie()/_cancelGame():
    winningToken.mint(player, 1); // ❌ Mints new refund tokens
  • This results in:

    1. Locked tokens: Original 2 tokens remain stuck in the contract.

    2. Supply inflation: New tokens are minted for every game, violating fixed-supply ERC20 semantics.

Impact

  • Critical economic flaw: Uncontrolled token supply growth.

  • Deposited tokens become permanently locked in the contract.

  • Winners receive newly minted tokens instead of the actual staked tokens, breaking game fairness.

Tools Used

  • Manual code review

  • Foundry test simulations (token balance tracking)

Recommendations

Modified Functions:

  1. _finishGame():

function _finishGame(uint256 _gameId, address _winner) internal {
// ... existing code ...
// Handle token prizes - transfer deposited tokens
if (game.bet == 0) {
// Transfer both players' tokens to winner
winningToken.transfer(_winner, 2); // ✅ Changed from mint()
} else {
// Keep ETH prize logic unchanged
winningToken.mint(_winner, 1);
}
// ... rest of the code ...
}
  1. _handleTie():

function _handleTie(uint256 _gameId) internal {
// ... existing code ...
// Return tokens for token games
if (game.bet == 0) {
winningToken.transfer(game.playerA, 1); // ✅ Changed from mint()
winningToken.transfer(game.playerB, 1); // ✅
}
// ... rest of the code ...
}
  1. _cancelGame():

function _cancelGame(uint256 _gameId) internal {
// ... existing code ...
// Return tokens for token games
if (game.bet == 0) {
if (game.playerA != address(0)) {
winningToken.transfer(game.playerA, 1); // ✅ Changed from mint()
}
if (game.playerB != address(0)) {
winningToken.transfer(game.playerB, 1); // ✅
}
}
// ... rest of the code ...
}

Key Changes:

  • Replaced all winningToken.mint() calls with winningToken.transfer() for token-based games.

  • Ensures the contract transfers deposited tokens instead of creating new ones.

  • Maintains ETH prize logic (minting 1 token for ETH winners) as originally designed.

Additional Safeguards:

  1. Add balance check before transfers:

require(
winningToken.balanceOf(address(this)) >= amount,
"Insufficient contract token balance"
);
  1. Update token approval logic in createGameWithToken()/joinGameWithToken() to ensure proper allowance.

Updates

Appeal created

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