Puppy Raffle

AI First Flight #1
Beginner FriendlyFoundrySolidityNFT
EXP
Submission Details
Impact: low
Likelihood: high

Rarity distribution is 71/25/4, not the intended 70/25/5

Author Revealed upon completion

Root + Impact

Description

rarity = hash % 100 gives values 0..99, but the branches use <=, so the boundary values fall into the wrong bucket: rarity 70 (the 71st outcome) is still COMMON, and legendary only covers 96..99 (4 outcomes). The real distribution is 71/25/4, not the intended 70/25/5.

@> if (rarity <= COMMON_RARITY) { // 0..70 = 71 outcomes (71%)
tokenIdToRarity[tokenId] = COMMON_RARITY;
@> } else if (rarity <= COMMON_RARITY + RARE_RARITY) { // 71..95 = 25 outcomes (25%)
tokenIdToRarity[tokenId] = RARE_RARITY;
} else { // 96..99 = 4 outcomes (4%)
tokenIdToRarity[tokenId] = LEGENDARY_RARITY;
}

Risk

Likelihood: High — the wrong split applies to every single mint; it is deterministic, not probabilistic.

Impact: Low — legendary puppies are 20% rarer than advertised (4% instead of 5%) and common puppies are 71% instead of 70%. No funds are at risk, but the documented 70/25/5 split is wrong.

Proof of Concept

The tests brute-force a caller whose rarity hash lands exactly on the boundary values and show where they map: rarity 70 → COMMON (proving common covers 71 outcomes), rarity 96 → LEGENDARY (proving legendary covers only 4 outcomes, 96..99).

function test_L1_Rarity70IsCommonNotRare() public {
address caller70;
for (uint256 i = 0; i < 10000; i++) {
address c = address(uint160(0x200000 + i));
uint256 r = uint256(keccak256(abi.encodePacked(c, block.difficulty))) % 100;
if (r == 70) { caller70 = c; break; }
}
require(caller70 != address(0), "caller not found");
_enterPlayers(4, 100);
vm.warp(block.timestamp + duration + 1);
vm.prank(caller70);
raffle.selectWinner();
// rarity value 70 is the 71st outcome (0..70) -> COMMON, so common is 71% not 70%
assertEq(raffle.tokenIdToRarity(0), 70, "rarity 70 maps to COMMON");
}

Recommended Mitigation

Use < instead of <= so the buckets are exactly 0..69 (70), 70..94 (25), and 95..99 (5).

- if (rarity <= COMMON_RARITY) {
+ if (rarity < COMMON_RARITY) {
- } else if (rarity <= COMMON_RARITY + RARE_RARITY) {
+ } else if (rarity < COMMON_RARITY + RARE_RARITY) {

Support

FAQs

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

Give us feedback!