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.
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}%"
"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".
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
...