Rock Paper Scissors

First Flight #38
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: medium
Invalid

`RockPaperScissors` game is susceptible to Sybil attacks which inflates the circulating supply of `WinningToken`, decrease the value of `WinningToken`

Summary

Any malicious player can create multiple addresses to play the RockPaperScissors game with themselves. For ETH games, RockPaperScissors::_finishGame rewards winning player with ETH bet of both players (minus protocol fees) as well as a WinningToken. Hence, malicious player can create multiple addresses to play ETH games with themselves, thus farming WinningTokens, leading to an inflated circulating supply of WinningToken and decreasing its value.

RockPaperScissors::_finishGame#L501

function _finishGame(uint256 _gameId, address _winner) internal {
Game storage game = games[_gameId];
game.state = GameState.Finished;
uint256 prize = 0;
// Handle ETH prizes
if (game.bet > 0) {
// Calculate total pot and fee
uint256 totalPot = game.bet * 2;
uint256 fee = (totalPot * PROTOCOL_FEE_PERCENT) / 100;
prize = totalPot - fee;
// Accumulate fees for admin to withdraw later
accumulatedFees += fee;
emit FeeCollected(_gameId, fee);
// Send prize to winner
(bool success,) = _winner.call{value: prize}("");
require(success, "Transfer failed");
}
// Handle token prizes - winner gets both tokens
if (game.bet == 0) {
// Mint a winning token
winningToken.mint(_winner, 2);
} else {
// Mint a winning token for ETH games too
@> winningToken.mint(_winner, 1);
}
emit GameFinished(_gameId, _winner, prize);
}

Vulnerability Details

The minimum cost to execute this attack is:

minCostPerWinningToken = minBet * 2 sybil players * (100 - PROTOCOL_FEE_PERCENT) / 100
minCostPerWinningToken = 0.01 ETH * 2 * (100 - 10) / 100
minCostPerWinningToken = 0.18 ETH

Impact

Impact: High, inflated circulating supply of WinningToken decreases its value
Likelihood: Low, the relatively costly farming of WinningToken through this method (0.18 ETH per WinningToken) deters this attack
Severity: Medium

Tools Used

Manual review

Recommendations

Do not give out WinningTokens as extra rewards in ETH games. If there is a need to distribute WinningTokens into the market consider, using liquidity pools

function _finishGame(uint256 _gameId, address _winner) internal {
Game storage game = games[_gameId];
game.state = GameState.Finished;
uint256 prize = 0;
// Handle ETH prizes
if (game.bet > 0) {
// Calculate total pot and fee
uint256 totalPot = game.bet * 2;
uint256 fee = (totalPot * PROTOCOL_FEE_PERCENT) / 100;
prize = totalPot - fee;
// Accumulate fees for admin to withdraw later
accumulatedFees += fee;
emit FeeCollected(_gameId, fee);
// Send prize to winner
(bool success,) = _winner.call{value: prize}("");
require(success, "Transfer failed");
}
// Handle token prizes - winner gets both tokens
if (game.bet == 0) {
// Mint a winning token
winningToken.mint(_winner, 2);
- } else {
- // Mint a winning token for ETH games too
- winningToken.mint(_winner, 1);
- }
emit GameFinished(_gameId, _winner, prize);
}
Updates

Appeal created

m3dython Lead Judge about 2 months ago
Submission Judgement Published
Invalidated
Reason: Design choice
m3dython Lead Judge about 2 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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