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

The `Chainlink VRF` is looking for non-existing function selector in `snek_raffle` contract

Summary

The function selector that the Chainlink VRF is looking for in snek_raffle contract doesn't exist.

Vulnerability Details

The protocol snek_raffle uses Chainlink VRF to generate a random number for picking a winner in the raffle. Therefore, the rawFulfillRandomWords and fulfillRandomWords functions are used.

@external
@> def rawFulfillRandomWords(requestId: uint256, randomWords: uint256[MAX_ARRAY_SIZE]):
"""The function the VRF Coordinator calls back to to provide the random words."""
assert msg.sender == VRF_COORDINATOR.address, ERROR_NOT_COORDINATOR
self.fulfillRandomWords(requestId, randomWords)
@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)

In the example of implementing the subscription method in the chainlink documentation is said that "we need to use ``DynArray`` so our function selector is the same as the one Chainlink VRF is looking for":

@internal
def fulfillRandomWords(request_id: uint256, _random_words: DynArray[uint256, MAX_ARRAY_SIZE]):
self.random_words = [_random_words[0]]
log ReturnedRandomness(self.random_words)
# In solidity, this is the equivalent of inheriting the VRFConsumerBaseV2
# Vyper doesn't have inheritance, so we just add the function here
@> # Also, we need to use `DynArray` so our function selector is the same as the one Chainlink VRF is looking for:
# Function Signature: rawFulfillRandomWords(uint256,uint256[])
# Function selector: 0x1fe543e3
@external
def rawFulfillRandomWords(requestId: uint256, randomWords: DynArray[uint256, MAX_ARRAY_SIZE]):
assert msg.sender == self.vrf_coordinator.address, "Only coordinator can fulfill!"
self.fulfillRandomWords(requestId, randomWords)

But in the snek_raffle::rawFulfillRandomWords and snek_raffle::fulfillRandomWords the DynArray is not used. That means the function selector for rawFulfillRandomWords(requestId: uint256, randomWords: uint256[MAX_ARRAY_SIZE]) function will be not the same as the one Chainlink VRF is looking for.
The function selector that Chainlink VRF is looking for: 0x1fe543e3. The function selector of the snek_raffle::rawFulfillRandomWords is 0xc2bef140 (b'\xc2\xbef\x14'). In Vyper you can retrieve the function selector of a function using the method: method_id, then you should convert the return value (Bytes[4]) to hexadecimal.

This difference is because the function signature of rawFulfillRandomWords(requestId: uint256, randomWords: uint256[MAX_ARRAY_SIZE]) is rawFulfillRandomWords(uint256, uint256[MAX_ARRAY_SIZE]), but the function signature of rawFulfillRandomWords(requestId: uint256, randomWords: DynArray[uint256, MAX_ARRAY_SIZE]) is rawFulfillRandomWords(uint256, uint256[]]).

Impact

If the function selector does not match the one that the Chainlink VRF Coordinator is expecting, the VRF Coordinator will not be able to successfully call the rawFulfillRandomWords function in the snek_raffle contract. This is because the VRF Coordinator uses the function selector to identify and call the correct function in the snek_raffle contract to deliver the random number.

When the VRF Coordinator is ready to return the random number to snek_raffle contract, it constructs a transaction to call the rawFulfillRandomWords function. It does this by including the function selector in the transaction data along with the encoded parameters. The Ethereum Virtual Machine (EVM) uses the function selector to determine which function to execute. It does this by matching the selector against the function selectors derived from the contract's code. If there is no function in the contract with the matching selector, the EVM will not find a function to execute, and the transaction will fail. This means the snek_raffle will not receive the random number and the winner of the raffle can not be picked. That means also the new raffle will be not opened and the functionality of the protocol will be broken.

Tools Used

Manual Review

Recommendations

Use DynArray as the second parameter of the snek_raffle::rawFulfillRandomWords and snek_raffle::fulfillRandomWords functions so the function selector will be the same as the one Chainlink VRF is looking for:

@external
+ def rawFulfillRandomWords(requestId: uint256, randomWords: DynArray[uint256, MAX_ARRAY_SIZE]):
- def rawFulfillRandomWords(requestId: uint256, randomWords: uint256[MAX_ARRAY_SIZE]):
"""The function the VRF Coordinator calls back to to provide the random words."""
assert msg.sender == VRF_COORDINATOR.address, ERROR_NOT_COORDINATOR
self.fulfillRandomWords(requestId, randomWords)
@internal
+ def fulfillRandomWords(request_id: uint256, random_words: DynArray[uint256, MAX_ARRAY_SIZE]):
- 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)
Updates

Lead Judging Commences

inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Incorrect Function Selector for Chainlink VRF

Support

FAQs

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