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

The wrong divisor is being used to select the rarity, resulting in the tokenURI returning the NFT regardless of its rarity chances. This leads to unworthy NFTs being assigned to the winner.

Summary

The fulfillRandomWords internal function incorrectly uses the divisor number 3 to calculate NFT rarities. Additionally, the tokenURI external function fails to utilize rarity chances to return a legitimate NFT. This behavior deviates from the expected functionality of the Snek Raffle.

vulnerability Details

Checkout the Line 154 and Line 166...

.
.
.
@internal
def fulfillRandomWords(request_id: uint256, random_words: uint256[MAX_ARRAY_SIZE]):
index_of_winner: uint256 = random_words[0] % len(self.players)
recent_winner: address = self.players[index_of_winner]
self.recent_winner = recent_winner
self.players = []
self.raffle_state = RaffleState.OPEN
self.last_timestamp = block.timestamp
rarity: uint256 = random_words[0] % 3
--------------------------------------------^
self.tokenIdToRarity[ERC721._total_supply()] = rarity
log WinnerPicked(recent_winner)
ERC721._mint(recent_winner, ERC721._total_supply())
send(recent_winner, self.balance)
#####################
# View Functions #
#####################
@external
@view
def tokenURI(token_id: uint256) -> String[53]:
rarity: uint256 = self.tokenIdToRarity[token_id]
------------^
return self.rarityToTokenURI[rarity]
.
.
.

Impact

Winners will find themselves with incorrect and illegitimate NFTs assigned to their owned token IDs, which could be perceived as advantageous by some winners but disadvantageous by others. However, the Snek Raffle is intended to assign NFTs with valid rarity chances to the selected winners.

PoC: Wrongly Calc NFT
  • Go to tests/snek_raffle_test.py and put the following test into that file...

def test_fulfill_random_words_picks_a_winner_resets_and_sends_money_but_assing_an_invalid_wrongly_calc_nft(
raffle_boa_entered, vrf_coordinator_boa, entrance_fee
):
additional_entrants = 10
for i in range(additional_entrants):
player = boa.env.generate_address(i)
boa.env.set_balance(player, STARTING_BALANCE)
with boa.env.prank(player):
raffle_boa_entered.enter_raffle(value=entrance_fee)
starting_balance = boa.env.get_balance(USER)
boa.env.time_travel(seconds=INTERVAL + 1)
raffle_boa_entered.request_raffle_winner()
# Normally we need to get the requestID, but our mock ignores that
vrf_coordinator_boa.fulfillRandomWords(0, raffle_boa_entered.address)
recent_winner = raffle_boa_entered.get_recent_winner()
winner_balance = boa.env.get_balance(recent_winner)
prize = entrance_fee * (additional_entrants + 1)
print("\nWrongly calculated NFT: ", raffle_boa_entered.tokenURI(0))
assert recent_winner == USER
assert winner_balance == starting_balance + prize
  • save the file, and open the terminal and execute the following command (Make sure venv for vyper is activated)

pytest -v tests/snek_raffle_test.py::test_fulfill_random_words_picks_a_winner_resets_and_sends_money_but_assing_an_invalid_wrongly_calc_nft -s
  • See the logs...

==================================================================== test session starts =====================================================================
platform linux -- Python 3.10.12, pytest-8.0.2, pluggy-1.4.0 -- /home/theirrationalone/vyperenv/bin/python
cachedir: .pytest_cache
hypothesis profile 'default' -> database=DirectoryBasedExampleDatabase(PosixPath('/home/theirrationalone/first-flights/2024-03-snek-raffle/.hypothesis/examples'))
rootdir: /home/theirrationalone/first-flights/2024-03-snek-raffle
plugins: titanoboa-0.1.8, cov-4.1.0, hypothesis-6.98.17
collected 1 item
tests/snek_raffle_test.py::test_fulfill_random_words_picks_a_winner_resets_and_sends_money_but_assing_an_invalid_wrongly_calc_nft
Wrongly calculated NFT: ipfs://QmRujARrkux8nsUG8BzXJa8TiDyz5sDJnVKDqrk3LLsKLX
PASSED
===================================================================== 1 passed in 1.55s ======================================================================

Tools Used

Manual Review, Pytest

Recommendations

  • Update the contracts/snek_raffle.vy file like below...

.
.
.
@internal
def fulfillRandomWords(request_id: uint256, random_words: uint256[MAX_ARRAY_SIZE]):
index_of_winner: uint256 = random_words[0] % len(self.players)
recent_winner: address = self.players[index_of_winner]
self.recent_winner = recent_winner
self.players = []
self.raffle_state = RaffleState.OPEN
self.last_timestamp = block.timestamp
- rarity: uint256 = random_words[0] % 3
+ rarity: uint256 = random_words[0] % (COMMON_RARITY + RARE_RARITY + LEGEND_RARITY)
self.tokenIdToRarity[ERC721._total_supply()] = rarity
log WinnerPicked(recent_winner)
ERC721._mint(recent_winner, ERC721._total_supply())
send(recent_winner, self.balance)
#####################
# View Functions #
#####################
@external
@view
def tokenURI(token_id: uint256) -> String[53]:
rarity: uint256 = self.tokenIdToRarity[token_id]
+ selectedURI: String[53] = self.rarityToTokenURI[COMMON]
+ if rarity > COMMON:
+ selectedURI = self.rarityToTokenURI[RARE]
+ if rarity > COMMON_RARITY + RARE_RARITY:
+ selectedURI = self.rarityToTokenURI[LEGEND]
- return self.rarityToTokenURI[rarity]
+ return selectedURI
.
.
.

Now if we re-execute the PoC Test then we will get a true legit assigned NFT...

==================================================================== test session starts =====================================================================
platform linux -- Python 3.10.12, pytest-8.0.2, pluggy-1.4.0 -- /home/theirrationalone/vyperenv/bin/python
cachedir: .pytest_cache
hypothesis profile 'default' -> database=DirectoryBasedExampleDatabase(PosixPath('/home/theirrationalone/first-flights/2024-03-snek-raffle/.hypothesis/examples'))
rootdir: /home/theirrationalone/first-flights/2024-03-snek-raffle
plugins: titanoboa-0.1.8, cov-4.1.0, hypothesis-6.98.17
collected 1 item
tests/snek_raffle_test.py::test_fulfill_random_words_picks_a_winner_resets_and_sends_money_but_assing_an_invalid_wrongly_calc_nft
<!-- 👇 Now here we have a Valid tokenURI -->
Wrongly calculated NFT: ipfs://QmZit9nbdhJsRTt3JBQN458dfZ1i6LR3iPGxGQwq34Li4a
PASSED
===================================================================== 1 passed in 1.57s ======================================================================
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.