Unpredictable randomness to NFT minting with Chainlink VRF
Table Of Contents
- Introduction
- Implementation
- Getting randomness from Chainlink
- Find a random token Id
- Writing the Mint Function - Writing Tests
- Mocking Chainlink VRF
- Test Randomness Request & Recieve cycle
- Staging Test
Introduction
Blockchains like Ethereum and Bitcoin are transparent and deterministic. This means generating tamper-proof unpredictable randomness is somewhat of a challenging task. Yet it remains a crucial aspect in many blockchain applications, such as determining governance roles in DAOs, giveaways, play-to-earn games, generating random NFT traits, distributing assets fairly, and many more.
One simple way to achieve this would be to generate randomness using a globally available variable like block.difficulty
or block.timestamp
as a source of entropy. But this way of generating randomness is never a good idea if you’re building something serious. Why? Because these can be manipulated by the miners.
So, the solution would be to use an oracle network that can generate randomness off-chain but provide an on-chain cryptographic proof. And Chainlink VRF ( Verifiable Random Function ) does exactly this. So I’d like to walk you through a simple implementation of VRF in an NFT minting contract to fairly distribute rare NFTs among the minters. Most NFT artwork projects generate NFT traits via off-chain computation and store NFT images in a distributed storage like IPFS. However, this poses a risk of minters sniping rare NFTs thus hindering the fair distribution of tokens. So to solve this problem, we can write a smart contract that will enable minters to get a random token ID using Chainlink VRF.
New to trading? Try crypto trading bots or copy trading on best crypto exchanges
I’ll be writing the minting contract and tests using hardhat and typescript. Also, test the smart contract using chainlink mocks. Here’s a link to the GitHub repository containing the contracts, tests, and deploy scripts.
Implementation
Getting Randomness from chainlink
Minting a random token Id first requires a random value from chainlink. So, let’s take a quick look at how to do that. There are two contracts that’ll be important in getting randomness. The first one is VRFCoordinatorV2
. As the name suggests, this contract is in charge of coordinating the whole requesting and receiving randomness process. You can read more information about the Request and Recieve Data cycle here. And the other one is VRFConsumerBaseV2.sol
. To request a random value from chainlink, your contract (randomness-consuming contract) must inherit this contract and implement the fulfillRandomWords
function. VRFConsumerBaseV2’s constructor takes the vrfCoordinator address as an argument. You can find the supported networks and VRFCoordinator addresses here.
Here, I’ve implemented the fulfillRandomWords
function defined in VRFConsumerBaseV2. This is the VRF callback function, which is called when the randomness request gets fulfilled. This can be only called by the VRFCoordinatorV2
contract pre-defined in the consumer contract. As seen here, if any other contract tries to call this it gets reverted with OnlyCoordinatorCanFulfill
error.
To send the randomness request, the consumer contract should call reqeustRandomWords
function in VRFCoordinatorV2
with the below arguments.
- keyHash : used to indicate the gas price limit for the randomness request. Find the available key hashes here.
- subscriptionId : You need to create a subscription and fund it with some testnet LINK tokens to get a subscription Id.
- requestConfirmations : The number of block confirmations the VRF service will wait to respond. Max and Min confirmations can be found here.
- callbackGasLimit : VRFCoordinator contract will use this amount of gas when calling the callback function(
rawFulfillRandomWords
) in vrfConsumerBaseV2 contract. (seecallWithExactGas
)
VRFCoordinatorV2.sol#L567
bytes memory resp = abi.encodeWithSelector(v.rawFulfillRandomWords.selector, requestId, randomWords)VRFCoordinatorV2.sol#L575
bool success = callWithExactGas(rc.callbackGasLimit, rc.sender,resp);
- numWords: The number of random numbers to request. Max values can be found here.
You can also direct fund LINK tokens to request randomness instead of managing subscriptions. You can read more about it here.
Find a Random Token Id
The random number we’re getting from chainlink is a 32 bytes Integer. The max token supply of this particular contract is 3333. Starting token index is 0. So to get a random token id, we can divide the 32 bytes random integer by 3333 and then get the remainder. uint16 randomId = randomInt % 3333
So then to avoid collisions between random token ids, we need to check if that id has been already minted or not.
The above function will get a random token id, and if the id is already minted, it will look for the next available token Id. You can see I’ve added a require statement to check if the randomness request has been fulfilled. Take a look at the code here.
Writing the Mint Function
I’ve used OpenZeppelin’s ERC721 Enumerable extension here. The _safeMint
function takes two arguments. The address that the NFT should be transferred to and the token id. As you can see, all left to do now is pass the generated random token id to the mint function.
Writing Tests
Mocking Chainlink VRFCoordinatorV2
As I previously said, all the randomness requests are coordinated by the VRFCoordinator contract. There are already deployed coordinator contracts on the Ethereum mainnet and testnets. But I wanted to test this whole process in an isolated local environment. So I deployed this mock contract in the chainlink GitHub repo to mimic the coordinator.
You can find the deploy scripts written using the hardhat-deploy plugin here. These scripts also handle the creation and funding of VRF subscriptions in mock testing.
The mock contract uses the request-id returned from requestRandomWords
function as a seed to generate a fake random number.
VRFCoordinatorV2Mock.sol#L113for (uint256 i = 0; i < req.numWords; i++)
{
_words[i] = uint256(keccak256(abi.encode(_requestId, i)));
}
To mimic Coordinator’s behavior, we have to call the fulfillRandomWords
function in our test scripts.
Test Randomness Request & Recieve cycle
Here I’ve tested if the expected outcomes are returned in the randomness request & response cycle. You can find this and other test contracts in the Github repo.
Staging Test
Use the below test block when testing in a test network like Goerli instead of localhost.
When testing in a testnet like Goerli, we don’t know exactly when will the randomness request will get fulfilled. So we can’t expect our contract to emit the RandomnessRequestFulfilled
event instantly like in the local environment. So I’ve created a promise to resolve when the event gets emitted and the expected outcomes returned.
Join Coinmonks Telegram Channel and Youtube Channel learn about crypto trading and investing