Rock Paper Scissors

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

Tokens transferred to contract are permanently lost while new tokens are minted, creating token inflation

Summary

In token-based games, the RockPaperScissors contract collects WinningTokens from players through transferFrom but never returns these tokens back to players. Instead, the contract mints new tokens when refunding players or awarding winners. This permanently locks the original tokens in the contract, causing players to lose their tokens and artificially inflating the token supply.

Vulnerability Details

When players join a token-based game, they transfer tokens to the contract:

// Transfer token to contract
winningToken.transferFrom(msg.sender, address(this), 1);

However, when refunding players or awarding winners, the contract mints new tokens instead of transferring the collected ones:

// Return tokens 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 creates two problems:

  1. The original tokens transferred to the contract are never returned and remain locked

  2. New tokens are minted, leading to token inflation

As shown in the POC:

  • PlayerA and PlayerB each transfer 1 token to the contract

  • When the game is cancelled, 2 new tokens are minted (1 for each player)

  • The original 2 tokens remain locked in the contract

  • The total token supply increases by 2

Every time a token-based game is played, tokens accumulate in the contract with no mechanism to retrieve them, and the total supply increases.

Impact

The impact of this vulnerability is significant:

  1. Players permanently lose their original tokens when participating in token-based games

  2. The token supply continually inflates as more games are played, potentially devaluing the token

  3. The contract accumulates tokens that cannot be recovered

  4. The token economics are fundamentally unsound and unsustainable

This undermines the token economy and creates a hidden cost for players using token-based games.

Tools Used

  • Manual code review

  • Foundry for POC validation

POC

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol";
import {console} from "forge-std/console.sol";
import {RockPaperScissors} from "../../src/RockPaperScissors.sol";
import {WinningToken} from "../../src/WinningToken.sol";
contract TokenLossPOC is Test {
RockPaperScissors public rps;
WinningToken public token;
address public admin = address(0x1);
address public playerA = address(0x2);
address public playerB = address(0x3);
uint256 public totalTurns = 3; // Odd number as required
uint256 public timeoutInterval = 5 minutes;
function setUp() public {
// Setup accounts
vm.startPrank(admin);
// Deploy RockPaperScissors contract
rps = new RockPaperScissors();
// Get the token address created by the RPS contract
token = rps.winningToken();
vm.stopPrank();
// Fund accounts
vm.deal(playerA, 1 ether);
vm.deal(playerB, 1 ether);
// Mint some initial tokens to players for testing
vm.startPrank(address(rps));
token.mint(playerA, 2);
token.mint(playerB, 2);
vm.stopPrank();
}
function testTokenLoss() public {
// Initial token balances
console.log("Initial contract token balance:", token.balanceOf(address(rps)));
console.log("Initial playerA token balance:", token.balanceOf(playerA));
console.log("Initial playerB token balance:", token.balanceOf(playerB));
// Step 1: PlayerA creates a game with token
vm.startPrank(playerA);
token.approve(address(rps), 1);
uint256 gameId = rps.createGameWithToken(totalTurns, timeoutInterval);
vm.stopPrank();
console.log("Game created by playerA with token");
// Check token balances after playerA joins
console.log("Contract token balance after playerA joins:", token.balanceOf(address(rps)));
console.log("PlayerA token balance after joining:", token.balanceOf(playerA));
// Step 2: PlayerB joins the game with token
vm.startPrank(playerB);
token.approve(address(rps), 1);
rps.joinGameWithToken(gameId);
vm.stopPrank();
console.log("PlayerB joined the game with token");
// Check token balances after playerB joins
console.log("Contract token balance after playerB joins:", token.balanceOf(address(rps)));
console.log("PlayerB token balance after joining:", token.balanceOf(playerB));
// Step 3: Let's cancel the game and see what happens to the tokens
vm.startPrank(playerA);
rps.cancelGame(gameId);
vm.stopPrank();
console.log("Game cancelled by playerA");
// Check token balances after game cancellation
console.log("Contract token balance after cancellation:", token.balanceOf(address(rps)));
console.log("PlayerA token balance after cancellation:", token.balanceOf(playerA));
console.log("PlayerB token balance after cancellation:", token.balanceOf(playerB));
// Verify the tokens transferred to the contract are still there (lost)
// and new tokens were minted to players
assert(token.balanceOf(address(rps)) == 2); // 2 tokens still locked in contract
assert(token.balanceOf(playerA) == 2); // PlayerA got a token back via minting
assert(token.balanceOf(playerB) == 2); // PlayerB got a token back via minting
console.log("VULNERABILITY CONFIRMED: Original tokens remain locked in contract while new tokens were minted");
}
}

Recommendations

Instead of minting new tokens, the contract should transfer the existing tokens back to players:

// For cancellations:
if (game.bet == 0) {
if (game.playerA != address(0)) {
winningToken.transfer(game.playerA, 1);
}
if (game.playerB != address(0)) {
winningToken.transfer(game.playerB, 1);
}
}
// For game completion:
if (game.bet == 0) {
// Transfer both tokens to the winner
winningToken.transfer(_winner, 2);
}

If additional tokens should be awarded to winners, that should be a separate design decision clearly stated in the documentation.

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.