Puppy Raffle

AI First Flight #1
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: medium
Likelihood: high
Invalid

M-5: Off-by-one in rarity comparison gives 4% legendary chance instead of 5%

Description

Severity: Medium

The rarity distribution in selectWinner() at PuppyRaffle.sol:141-148 uses <= comparisons that produce incorrect probabilities:

uint256 rarity = uint256(keccak256(abi.encodePacked(msg.sender, block.difficulty))) % 100;
if (rarity <= COMMON_RARITY) { // COMMON_RARITY = 70
tokenIdToRarity[tokenId] = COMMON_RARITY;
} else if (rarity <= COMMON_RARITY + RARE_RARITY) { // 70 + 25 = 95
tokenIdToRarity[tokenId] = RARE_RARITY;
} else {
tokenIdToRarity[tokenId] = LEGENDARY_RARITY;
}

The rarity variable ranges from 0 to 99 (100 possible values). With <=:

  • rarity <= 70 matches values 0-70: 71 values (71%, not 70%)

  • rarity <= 95 matches values 71-95: 25 values (25%, correct)

  • rarity > 95 matches values 96-99: 4 values (4%, not 5%)

The stated constants suggest 70%/25%/5% distribution, but the actual distribution is 71%/25%/4%. Legendary NFTs are 20% rarer than intended.

Proof of Concept

function testRarityDistribution() public {
uint256 commonCount;
uint256 rareCount;
uint256 legendaryCount;
// Simulate 100 rarity values (0-99)
for (uint256 rarity = 0; rarity < 100; rarity++) {
if (rarity <= 70) {
commonCount++;
} else if (rarity <= 95) {
rareCount++;
} else {
legendaryCount++;
}
}
// Actual: 71/25/4 — not 70/25/5 as constants suggest
assertEq(commonCount, 71); // should be 70
assertEq(rareCount, 25); // correct
assertEq(legendaryCount, 4); // should be 5
}

Risk

  • Impact: Medium — participants are misled about legendary NFT odds. Legendary NFTs are 20% rarer than the stated 5% chance, affecting the value proposition for raffle entrants.

  • Likelihood: High — the off-by-one applies to every single raffle round.

Recommended Mitigation

Change <= to < at PuppyRaffle.sol:141-143 to match the stated distribution:

if (rarity < COMMON_RARITY) {
tokenIdToRarity[tokenId] = COMMON_RARITY;
} else if (rarity < COMMON_RARITY + RARE_RARITY) {
tokenIdToRarity[tokenId] = RARE_RARITY;
} else {
tokenIdToRarity[tokenId] = LEGENDARY_RARITY;
}

This produces the correct 70/25/5 distribution matching the declared constants.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 4 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!