The PuppyRaffle::selectWinner function uses an incorrect rarity distribution algorithm. The code intends to distribute rarities as 70% Common, 25% Rare, and 5% Legendary, but due to an off-by-one error in the conditional logic, the actual distribution is 71% Common, 25% Rare, and only 4% Legendary.
Expected behavior: NFT rarities should be distributed exactly as documented: 70% Common, 25% Rare, 5% Legendary.
Actual behavior: The rarity distribution is skewed. Common NFTs appear 71% of the time (not 70%), and Legendary NFTs only appear 4% of the time (not 5%). This means users have a 20% lower chance of receiving a Legendary NFT than advertised.
The issue is that rarity % 100 produces values from 0-99 (100 possible values), but the first condition uses <= which includes 0, creating 71 values for Common (0 through 70 inclusive) instead of 70. This leaves only 4 values (96-99) for Legendary instead of 5.
Likelihood:
High - Occurs on every winner selection
Affects 100% of NFT mints
Not random or probabilistic - always happens
Impact:
Violates documented rarity distribution
Users receive 20% fewer Legendary NFTs than expected (4% vs 5%)
If marketed as "5% legendary drop rate", this is false advertising
Unfair to users who participate expecting correct odds
Could lead to reputation damage if discovered post-launch
Add to test/PuppyRaffleTest.t.sol:
Run: forge test --match-test test_rarityDistribution -vv --offline
Output:
The test proves that across all 100 possible random values (0-99), only 4 result in Legendary (96, 97, 98, 99), not 5 as intended.
Change the first condition from <= to < to ensure exactly 70 values for Common:
This gives the correct distribution: 70% Common (0-69), 25% Rare (70-94), 5% Legendary (95-99).
## Description The drop chances defined in the state variables section for the COMMON and LEGENDARY are misleading. ## Vulnerability Details The 3 rarity scores are defined as follows: ``` uint256 public constant COMMON_RARITY = 70; uint256 public constant RARE_RARITY = 25; uint256 public constant LEGENDARY_RARITY = 5; ``` This implies that out of a really big number of NFT's, 70% should be of common rarity, 25% should be of rare rarity and the last 5% should be legendary. The `selectWinners` function doesn't implement these numbers. ``` uint256 rarity = uint256(keccak256(abi.encodePacked(msg.sender, block.difficulty))) % 100; if (rarity <= COMMON_RARITY) { tokenIdToRarity[tokenId] = COMMON_RARITY; } else if (rarity <= COMMON_RARITY + RARE_RARITY) { tokenIdToRarity[tokenId] = RARE_RARITY; } else { tokenIdToRarity[tokenId] = LEGENDARY_RARITY; } ``` The `rarity` variable in the code above has a possible range of values within [0;99] (inclusive) This means that `rarity <= COMMON_RARITY` condition will apply for the interval [0:70], the `rarity <= COMMON_RARITY + RARE_RARITY` condition will apply for the [71:95] rarity and the rest of the interval [96:99] will be of `LEGENDARY_RARITY` The [0:70] interval contains 71 numbers `(70 - 0 + 1)` The [71:95] interval contains 25 numbers `(95 - 71 + 1)` The [96:99] interval contains 4 numbers `(99 - 96 + 1)` This means there is a 71% chance someone draws a COMMON NFT, 25% for a RARE NFT and 4% for a LEGENDARY NFT. ## Impact Depending on the info presented, the raffle participants might be lied with respect to the chances they have to draw a legendary NFT. ## Recommendations Drop the `=` sign from both conditions: ```diff -- if (rarity <= COMMON_RARITY) { ++ if (rarity < COMMON_RARITY) { tokenIdToRarity[tokenId] = COMMON_RARITY; -- } else if (rarity <= COMMON_RARITY + RARE_RARITY) { ++ } else if (rarity < COMMON_RARITY + RARE_RARITY) { tokenIdToRarity[tokenId] = RARE_RARITY; } else { tokenIdToRarity[tokenId] = LEGENDARY_RARITY; } ```
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.