Road to Web 3 Week 5 — Chainlink NFT Part 2 of 2

mitey.titey
10 min readFeb 10

--

In the last post I covered how we create an NFT contract that could dynamically be updated by a Chainlink oracle.

For the final steps of this lesson we will:

  • Update our contract to use a random number generator for the token selection (0–2) in our Bull/Bear image arrays
  • Deploy the contract to the Goerli test network using the BTC/USD Chainlink oracle.
  • Register our contract using test LINK tokens
  • Verify the NFT images changing on test Opensea

Applying the Random Number Generator

Generating a random number in a trustless way is actually quite more complicated than I expected.

To accomplish this we first deploy a mock coordinator contract similar to the mock price feed. This allows us to test our contract before deploying to Goerli.

The mock coordinator can be imported from the Chainlink repo. The code for the contract is very simple.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@chainlink/contracts/src/v0.8/mocks/VRFCoordinatorV2Mock.sol";

To deploy the contract we must pass 2 arguments, _baseFee and _gasPriceLink. We were instructed to use 100000000000000000 and 1000000000 respectively.

Next, we need to create a subscription, and fund the subscription with LINK tokens. The createSubscription does not require any parameters. You will need to note the subId from the resulting log to pass as a parameter in the fundSubscription function in addition to the amount of LINK (1000000000000000000 or 10 LINK). Once our mock random number generator is deployed and funded it is time to move to the next step.

Updating the Bull Bear contract

The next step in the process is to update the Bull Bear contract to interact with the random number generator.

The first step is to import the needed libraries from Chainlink.

//These are the randomness imports
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";

The next step is to make the contract a VRFConsumerBaseV2 and add some new variables that will store information about our random number generator and some values it will need to function properly.

//make KeeperCompatibleInterface and VRFConsumerBaseV2
contract BullBear is ERC721, ERC721Enumerable, ERC721URIStorage, KeeperCompatibleInterface, Ownable, VRFConsumerBaseV2 {

// VRF variables
VRFCoordinatorV2Interface public COORDINATOR;
uint256[] public s_randomWords;
uint256 public s_requestId;
uint32 public callbackGasLimit = 500000; // set higher as fulfillRandomWords is doing a LOT of heavy lifting.
uint64 public s_subscriptionId;
bytes32 keyhash = 0xd89b2bf150e3b9e13446986e571fb9cab24b13cea0a43ea20a6049a85cc807cc; // keyhash, see for Rinkeby https://docs.chain.link/docs/vrf-contracts/#rinkeby-testnet

enum MarketTrend{BULL, BEAR} // Create Enum
MarketTrend public currentMarketTrend = MarketTrend.BULL;

Next, we need to update the constructor to define the random number coordinator. We need to add the COORDINATOR as a parameter, VRFConsumerBaseV2(_vrfCoordinator)and pass that value to our variable, COORDINATOR.

 constructor(uint updateInterval, address _pricefeed, address _vrfCoordinator) ERC721("Bull&Bear", "BBTK") VRFConsumerBaseV2(_vrfCoordinator) {
// Set the keeper update interval
interval = updateInterval;
lastTimeStamp = block.timestamp; // seconds since unix epoch

// set the price feed address to
// BTC/USD Price Feed Contract Address on Goerli: https://goerli.etherscan.io/address/0xA39434A63A52E749F02807ae27335515BA4b07F7
// or the MockPriceFeed Contract
pricefeed = AggregatorV3Interface(_pricefeed); // To pass in the mock

//Set up Randomness
COORDINATOR = VRFCoordinatorV2Interface(_vrfCoordinator);

// set the price for the chosen currency pair.
currentPrice = getLatestPrice();
}

The next step is to add a new function, requestRandomnessForNFTUris, to our contract that will replace setTokenUris. This step does not change the token URIs, but instead looks into our mock coordinator contract for a subscription, requests random words, and returns a requestID that we will use later to request our random words and set the token URIs.

function requestRandomnessForNFTUris() internal {
require(s_subscriptionId != 0, "Subscription ID not set");

// Will revert if subscription is not set and funded.
s_requestId = COORDINATOR.requestRandomWords(
keyhash,
s_subscriptionId, // See https://vrf.chain.link/
3, //minimum confirmations before response
callbackGasLimit,
1 // `numWords` : number of random values we want. Max number for rinkeby is 500 (https://docs.chain.link/docs/vrf-contracts/#rinkeby-testnet)
);

console.log("Request ID: ", s_requestId);

// requestId looks like uint256: 80023009725525451140349768621743705773526822376835636211719588211198618496446
}

The next new function is called fulfillRandomWords which is called externally from our mock coordinator, and takes the parameter of the requestID and the corresponding randomWords. This function DOES change our tokenURIs, but instead of defaulting to 0, it uses the random number. To get our random number to be between 0 and 2 we take the modulus (%) between the random word and the length of our corresponding array of images.

 // This is the callback that the VRF coordinator sends the 
// random values to.
function fulfillRandomWords(
uint256, /* requestId */
uint256[] memory randomWords
) internal override {
s_randomWords = randomWords;
// randomWords looks like this uint256: 68187645017388103597074813724954069904348581739269924188458647203960383435815

console.log("...Fulfilling random Words");

string[] memory urisForTrend = currentMarketTrend == MarketTrend.BULL ? bullUrisIpfs : bearUrisIpfs;
uint256 idx = randomWords[0] % urisForTrend.length; // use modulo to choose a random index.


for (uint i = 0; i < _tokenIdCounter.current() ; i++) {
_setTokenURI(i, urisForTrend[idx]);
}

string memory trend = currentMarketTrend == MarketTrend.BULL ? "bullish" : "bearish";

emit TokensUpdated(trend);
}

The next step in modifying our contract is to head back the performUpkeep function, comment out the call to setTokenURIs and replace with the call to requestRandomnessForNFTUris.

The final step is to add three helper functions to support the random number coordinator.

    // For VRF Subscription Manager
function setSubscriptionId(uint64 _id) public onlyOwner {
s_subscriptionId = _id;
}


function setCallbackGasLimit(uint32 maxGas) public onlyOwner {
callbackGasLimit = maxGas;
}

function setVrfCoodinator(address _address) public onlyOwner {
COORDINATOR = VRFCoordinatorV2Interface(_address);
}

Before we can deploy our Bull Bear contract we must deploy the mock price feed so that we can pass the corresponding address as a parameter to our contract.

To deploy the mock price feed, we need to pass the number of decimals (8), and an initial price (3034715771688).

Now with our mock coordinator address, mock price feed address, and interval(10) parameters, we can deploy our Bull Bear contract.

After we deploy all of the contacts, we must add the Bull Bear contract as a Consumer of our random number coordinator. To accomplish this, we head back to the Mock Coordinator contract and run the addConsumer function with the subId (1) from createSubscriptionand Bull Bear contract address as the parameters.

The final step is to set the subscription ID in the Bull Bear contract using the setSubscription function with the paramter of subID equal to 1.

Set subscription ID in Bull Bear contract

With all of the contracts deployed, our subscription created and funded, and our NFT contract set as the consumer, we can finally test the contract function. 🤓

Testing the contract

To test the contract our first step is to use the safeMint function in the Bull Bear contract to mint a new NFT.

Mint token in Bull Bear

The next step is to check the tokenUri and verify that it is showing the Bull[0] image.

Check token uri in Bull Bear

Next we move to our price feed contract and using the updateAnswer function to change the price. I choose to lower the value to make sure it flips over to a Bear image.

Change answer in price feed

After update the answer we run the performUpkeep function in the Bull Bear contract by passing an empty array [] as the parameter. This will pass the new answer to our contract and set the marketTrend equal to Bear.

Calculate the market trend in Bull Bear

Next, we must note the request ID (1) from logs from the previous step.

To change the token URI’s we must pass go to the mock Ccoordinator contract and run the fulfillRandomWords function passing our request ID and Bull Bear contract address as the parameters. In this case a random number will be used to select our image from the Bear image array.

After this step, we then can verify the tokenURI has changed and that a random number was selected. In this case the image was changed to simple_bear.json which is the third item in our Bear array. 🎉

Verify that token URI has changed to random Bear 🎉

Deploying to Goerli

The good thing about deploying to Goerli is that we no longer need to mess with our mock contracts. While good for testing and understanding they add a lot of needed steps that on Goerli will be taken care of for us automatically.

Before we can deploy our Bull Bear contract to the Goerli test network we must get the addresses of the price feed and random number coordinator. We also must update the keyhash variable value to match the Goerli network.

For the price feed I used ETH/USD — 0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e

For the random number coordinator I used — 0x2Ca8E0C643bDe4C2E08ab1fA0da3401AdAD7734D

For the interval I increased to 300 to prevent uneeded upkeep.

For the keyHash I used — 0x79d3d8832d904592c0bf9818b621522c988bb8b0c05cdc3b15aea1b6e8db0c15.

Before we deploy we must make sure we get test ETH and LINK from the faucets to deploy our contracts and to fund our price feed and random number generator.

Are you ready? Let’s deploy the Bull Bear contract. 🚀

Next we must register our upkeep using our newly deployed contract.

I first needed to connect my wallet. Then I needed to input my contract address ABI which I copied from Remix

After hitting Next I was directed to select my upkeep function and input the parameters that needed passed. I selected performUpkeep and passed 0x as the parameter which represents an empty array (thanks chatGPT). There were a few more things to enter related to how often my upkeep needed to occur and my project name, but after approving two transactions my upkeep was registered. 🎉

The next step was to create my subscription on the random generator and fund it with LINK. To create my subcription I went to https://vrf.chain.link/ and selected Create Subscription. The subscription is associated with my wallet address so I must connect my wallet and approve a transaction. After created I must note the Subscription ID so I can set it in my contract.

Next, I need to add LINK funds to the subscription. I realized the value of 5 was not enough in later steps so I suggest to fund it with at least 20 LINK tokens.

Finally, I need to add my contract address as a Consumer for the subscription.

Consumer added https://vrf.chain.link/goerli/9484

Now I must set the subscription ID in my Bull Bear contract equal to 9484. I will do this on Remix.

Now that my price feed and random number generator are successfully connected and funded it is time to mint an NFT using the safeMint function. We can verify the image using testnets.opensea.io. Step one is successful.🎉 We see the gamer Bull.

Now we must wait for the Chainlink pricefeed and random number generator to do their magic. The price feed will trigger the request for a random number based on the marketTrend.

The random number contract will callback the contract with the request ID and update the token URIs based on the marketTrend.

We can also monitor the currentPrice in the contract to verify our price feed is working and check our upkeep on Chainlink to see when the next update will trigger.

I was not successful on the first try so I had to deploy another contract 😒. My mistake was using the wrong keyHash value which I easily identified by looking at the activity on my random number generator. The interface told me directly that the keyHash was incorrect. Here is the new contract for future reference.

After the needed redeployment, there we have it, the Beanie Bear. 🎉

This was very challenging but rewarding lesson. I learned a ton about how Chainlink works in additon to a ton of other tidbits about Solidity and IPFS.

Here is the link to the Alchemy docs https://docs.alchemy.com/docs/connect-apis-to-your-smart-contracts-using-chainlink

Here is a link to my final contracts. https://github.com/mtitus6/web3-Projects/tree/main/Dynamic%20NFT%20Chainlink

--

--