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

`snek_raffle::fulfillRandomWords` Distributes Common/Rare/Legend Rarities Equally, Causing a Mismatch with Intended Probabilities

Summary

snek_raffle::fulfillRandomWords allocates NFT rarities incorrectly. Instead of distributing rarities according to the specified chances (70% Common, 25% Rare, 5% Legendary), the contract equally distributes rarities.

Vulnerability Details

snek_raffle::fulfillRandomWords is the function that - among other things - is supposed to allocate NFT rarites according to the specified probabilites (70% Common, 25% Rare, 5% Legendary). However, in the code rarity is assigned as random_words[0] % 3, which incorrectly maps the random number directly to one of the three rarities without considering the specified probabilities.

Proof of Code

The following test focus analyzes the distribution of rarities after a significant number of entries have been processed, to statistically demonstrate that the distribution does not align with the intended 70% Common, 25% Rare, and 5% Legendary probabilities.

import boa
import pytest
from collections import defaultdict
# Assuming these are defined in your smart contract or test setup
COMMON = 0
RARE = 1
LEGEND = 2
NUM_SIMULATIONS = 1000 # Number of times to simulate raffle entry for statistical significance
@pytest.fixture
def setup_raffle_with_entries(raffle_boa_entered, vrf_coordinator_boa, entrance_fee):
# Simulate multiple entries to the raffle
for _ in range(NUM_SIMULATIONS):
player = boa.env.generate_address("additional_player")
boa.env.set_balance(player, STARTING_BALANCE)
with boa.env.prank(player):
raffle_boa_entered.enter_raffle(value=entrance_fee)
# Advance time to allow for raffle winner request
boa.env.time_travel(seconds=INTERVAL + 1)
raffle_boa_entered.request_raffle_winner()
# Mock fulfillment from VRF Coordinator (skipping requestId handling for simplicity)
# This part should ideally be looped to simulate multiple raffle cycles
# However, for a single cycle, this demonstrates the principle
vrf_coordinator_boa.fulfillRandomWords(0, raffle_boa_entered.address)
return raffle_boa_entered
def test_rarity_distribution_accuracy(setup_raffle_with_entries):
raffle_boa = setup_raffle_with_entries
rarity_counts = defaultdict(int)
# Assuming a function exists to fetch the rarity of each NFT by index,
# and a total_supply function exists to know how many NFTs to loop through.
# These functionalities depend on the implementation details of your smart contract.
for token_id in range(1, raffle_boa.total_supply() + 1): # Assuming token IDs start at 1
rarity = raffle_boa.tokenIdToRarity(token_id)
rarity_counts[rarity] += 1
total_nfts = raffle_boa.total_supply()
common_percentage = (rarity_counts[COMMON] / total_nfts) * 100
rare_percentage = (rarity_counts[RARE] / total_nfts) * 100
legend_percentage = (rarity_counts[LEGEND] / total_nfts) * 100
# Asserting the distribution matches expected probabilities
# Note: These assertions may need a margin of error due to the statistical nature of probability
assert common_percentage >= 65 and common_percentage <= 75, f"Common rarity distribution off: {common_percentage}%"
assert rare_percentage >= 20 and rare_percentage <= 30, f"Rare rarity distribution off: {rare_percentage}%"
assert legend_percentage >= 4 and legend_percentage <= 6, f"Legend rarity distribution off: {legend_percentage}%"

Impact

"Common", "Rare", and "Legend" designations will be meaningless. In a larger collection, 33.33..% of the items will be "Common", 33.33..% "Rare", and 33.33..% "Legend".

Tools Used

ChatGPT

Recommendations

Update the rarity assignment logic to reflect the intended probabilities. Use ranges derived from the random number to allocate rarities according to the specified distribution probabilities:

def fulfillRandomWords(request_id: uint256, random_words: uint256[MAX_ARRAY_SIZE]):
...
- rarity: uint256 = random_words[0] % 3
+ rarity_score: uint256 = random_words[0] % 100 # Map the random number to a 0-99 range
+ if rarity_score < 70:
+ rarity = COMMON # 70% chance
+ elif rarity_score < 95:
+ rarity = RARE # 25% chance
+ else:
+ rarity = LEGEND # 5% chance
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.