Rock Paper Scissors

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

Game Cancellation and Token Minting Inconsistency

Summary

When both players commit but neither reveals their moves during a token game, the game is cancelled via _cancelGame() which mints new tokens for both players instead of returning the originally staked tokens. This creates an economic vulnerability where players can effectively generate tokens from nothing.

Vulnerability Details

In the timeoutReveal function, when neither player reveals their move:

else if (!playerARevealed && !playerBRevealed) {
// Neither player revealed, cancel the game and refund
_cancelGame(_gameId);
}

This calls _cancelGame, which handles token refunds as follows:

// 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);
}
}

The issue is that instead of transferring the originally staked tokens back to players, the contract mints new tokens, effectively doubling the total token supply used in the game.

Impact

This vulnerability can be systematically exploited to:

  1. Inflate Token Supply: Players can create and join token games, commit moves, then deliberately let the reveal timeout expire to generate new tokens.

  2. Mint Tokens Without Risk: Players can commit to multiple games, never revealing their moves, and collect newly minted tokens without ever risking their original stake.

  3. Devalue Existing Tokens: Over time, this inflation reduces the value of legitimately earned tokens, undermining the tokenomics.

##PoC

// 1. Player A creates a token game
playerA has 1 token initially
uint256 gameId = game.createGameWithToken(3, 300);
// PlayerA now has 0 tokens, contract has 1 token
// 2. Player B joins the game
playerB has 1 token initially
game.joinGameWithToken(gameId);
// PlayerB now has 0 tokens, contract has 2 tokens
// 3. Both players commit moves
bytes32 commitHashA = keccak256(abi.encodePacked(uint8(1), bytes32("salt")));
bytes32 commitHashB = keccak256(abi.encodePacked(uint8(2), bytes32("salt2")));
game.commitMove(gameId, commitHashA); // Player A commits
game.commitMove(gameId, commitHashB); // Player B commits
// 4. Neither player reveals, timeout expires
// Fast forward time
vm.warp(block.timestamp + 301);
// 5. Either player calls timeout
game.timeoutReveal(gameId);
// 6. Result:
// - Both players receive 1 newly minted token each
// - The original 2 tokens remain locked in the contract
// - Total supply increased by 2 tokens

Tools Used

Manual code review

Recommendations

Modify the _cancelGame function to return the original tokens rather than minting new ones:

// Return tokens for token games
if (game.bet == 0) {
if (game.playerA != address(0)) {
// Transfer original token back instead of minting
winningToken.transfer(game.playerA, 1);
}
if (game.playerB != address(0)) {
// Transfer original token back instead of minting
winningToken.transfer(game.playerB, 1);
}
}
Updates

Appeal created

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