Beginner FriendlyFoundryNFT
100 EXP
View results
Submission Details
Severity: medium
Valid

Broken Rarity Logic

Summary

The Snek Raffle project incorporates a random selection mechanism utilizing Chainlink VRF to determine the winners of a snek NFT with distinct rarities: Brown Snek, Jungle Snek, and Cosmic Snek. As per the project's specifications, each NFT has been assigned specific probabilities of winning: Brown Snek (70%), Jungle Snek (25%), and Cosmic Snek (5%). However, during the security review, a significant vulnerability was identified pertaining to the disparity between the expected probabilities and the observed outcomes.

It was observed that the probabilities of winning each NFT is consistently distributed evenly, with each NFT having approximately a 33.33% chance of being awarded. This discrepancy between the expected probabilities and the observed outcomes indicates a fundamental flaw in the implementation of the random selection mechanism.

Vulnerability Details

Notice that the rarity calculated in the fulfillRandomWords() function is using the retrieved random number modulo 3 to determine the rarity. This is making the chance of winning each NFT even (so 33.33% each type):

@internal
def fulfillRandomWords(request_id: uint256, random_words: uint256[MAX_ARRAY_SIZE]):
index_of_winner: uint256 = random_words[0] % len(self.players)
recent_winner: address = self.players[index_of_winner]
self.recent_winner = recent_winner
self.players = []
self.raffle_state = RaffleState.OPEN
self.last_timestamp = block.timestamp
@> rarity: uint256 = random_words[0] % 3
@> self.tokenIdToRarity[ERC721._total_supply()] = rarity
...

Impact

The inconsistency between expected and observed probabilities erodes the trust of participants in the fairness and integrity of the Snek Raffle project. This could lead to a loss of confidence in the project's legitimacy and deter participation.

Tools Used

Manual analysis.

Recommendations

Review the rarity logic implementation to ensure alignment with the specified percentages. Utilizing the random number generated by Chainlink VRF is fine, but the subsequent calculation of rarity should accurately reflect the intended probabilities.

A proposed approach would be to perform the following changes to the fulfillRandomWords() internal function:

...
-rarity: uint256 = random_words[0] % 3
+rarity: uint256 = random_words[0] % 100
+if rarity < COMMON_RARITY:
+ self.tokenIdToRarity[ERC721._total_supply()] = COMMON
+elif rarity < COMMON_RARITY + RARE_RARITY:
+ self.tokenIdToRarity[ERC721._total_supply()] = RARE
+else:
+ self.tokenIdToRarity[ERC721._total_supply()] = LEGEND
-self.tokenIdToRarity[ERC721._total_supply()] = rarity
...
Updates

Lead Judging Commences

inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Rarity is 1/3 instead of what the docs say

Support

FAQs

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