Chainlink Reference Guide (Hardhat Version)

Note: Use chainlink-hardhat-test as a reference repository while following this guide. There are additonal resources at the bottom of the page incase you get stuck somewhere. More content will be added as I get a chance to test it.

Intro to Hardhat

What is Hardhat?

Hardhat is a development environment to compile, deploy, test, and debug your Ethereum software. Hardhat comes built-in with Hardhat Network, a local Ethereum network designed for development. Its functionality focuses around Solidity debugging, featuring stack traces, console.log() and explicit error messages when transactions fail.

Read more at https://hardhat.org/getting-started/.

Why use Hardhat over Remix?

For professional development, we must write tests to check the code and scripts that help deploy and run the code. Hardhat gives us these capabilities out of the box. Additonally, working with Remix on large projects and downloading the repos is a complex process.

Installing Hardhat

Note: The following steps are only for projects that do not have native hardhat support. Project repos like https://www.create-web3.xyz/ come with Hardhat out of the box.

To install the hardhat package in your project, run the following in your terminal:

#For an npm project:
npm install --save-dev hardhat

#Or for a yarn project:
yarn add --dev hardhat

Note: If you are running a mono repo setup, create a folder named hardhat and run the command above in that folder.

Now run the following in the terminal for setting up a barebones hardhat project and select the basic project option. Select yes for adding the .gitignore and additional dependencies:

npx hardhat
#or
yarn hardhat

We can re-run the command above to see the tasks Hardhat is capable of performing.

If you see a hardhat message similar to the following, copy it and run it in the terminal. You may have to run the command in batches if your terminal does not paste the full list of dependencies in one go:

#You need to install these dependencies to run the sample project:
yarn add --dev "hardhat@^2.9.3" "@nomiclabs/hardhat-waffle@^2.0.0" "ethereum-waffle@^3.0.0" 
"chai@^4.2.0" "@nomiclabs/hardhat-ethers@^2.0.0" "ethers@^5.0.0" "@chainlink/contract"

Out of the box, Hardhat gives us a bunch of files and folders of which the following are most important:

  • contracts : All your smart contracts go here.
  • scripts : Scripts to run and interact with your contract are housed here. Sometimes this folder is named deploy in custom projects or a separate folder named deploy is created to work with the hardhat-deploy package.
  • test : We must write robust tests to thoroughly check our contracts. And you guessed it! Those tests go here.
  • hardhat.config.json : Cornerstone for hardhat. For running any scripts, this config is important. It defines things like what network to deploy contract and in what configuration.

Working with Hardhat

The contract

Hardhat comes with a standard Greeter.sol contract. It has a single variable to store the value of the greeting. There are also two functions to call the greeting or change the value of the greeting. We will work with this to explore the basic use of Hardhat. For syntax highlighting in VSCode, you can use the following solidity extension.

Here's the sample code to work with. If your sample code is different, just copy this for trial:

//Read more about License at: 
//https://docs.soliditylang.org/en/v0.6.8/layout-of-source-files.html#spdx-license-identifier
//SPDX-License-Identifier: MIT
//working with solidity version 0.8.4 and above
pragma solidity ^0.8.4;

//importing this allows us to console log stuff in solidity
import "hardhat/console.sol";

//create contract with name Greeter
contract Greeter {

    //create a private variable of type string and name greeting
    string private greeting;

    //constructor is a function that runs the first time a contract is deployed
    //we take a greeting as an argument
    constructor(string memory _greeting) {
        console.log("Deploying a Greeter with greeting:", _greeting);
        //assign value of argument to variable named greeting
        greeting = _greeting;
    }

    //function to call the value of variable greeting
    function greet() public view returns (string memory) {
        return greeting;
    }

    //function to change value of variable greeting
    //takes new greeting value as argument
    function setGreeting(string memory _greeting) public {
        console.log("Changing greeting from '%s' to '%s'", greeting, _greeting);
        greeting = _greeting;
    }
}

The following command is used to check if our contracts compile successfully. Run to check if Greeter.sol compiles successfully.

npx hardhat compile
#or
yarn hardhat compile

Hardhat will add an artifacts folder in the project. All the compiled code lives in this folder.

Working with Hardhat locally

In order to run scripts locally, Hardhat gives us the ability to set up a local blockchain along with test accounts. To set up the local blockchain run the following in ther terminal:

npx hardhat node
#or
yarn hardhat node

The Script

The general format for standalone (that are run individually) scripts is:

  • import and require statements
  • the main function
  • invoking the main function

Now let's see what the scripts/sample-script.js is doing:

const hre = require("hardhat");

async function main() {
  // Hardhat always runs the compile task when running scripts with its command
  // line interface.
  //
  // If this script is run directly using `node` you may want to call compile
  // manually to make sure everything is compiled
  // await hre.run('compile');

  // We get the contract we want to deploy
  // We wait for the tranaction request for deploying the contract
  const Greeter = await hre.ethers.getContractFactory("Greeter");
  const greeter = await Greeter.deploy("Hello, Hardhat!");

  //we wait for the contract to be deployed
  await greeter.deployed();

  //returns address of the deployed contract
  console.log("Greeter deployed to:", greeter.address);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
//Invoking the main function
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

Now without closing the hardhat node terminal, we open a new terminal window to run our scripts. Run the script as follows:

To set up the local blockchain run the following in ther terminal:

npx hardhat run scripts/sample-script.js
#or
yarn hardhat run scripts/sample-script.js

Testing with Hardhat

Coming soon.


Oracles

Blockchains are deterministic and cannot connect to the outside world natively in order to maintain transparency and decentralisation. They need to have oracles (middleware) to communicate data and computations to and from the rest of the world. Since decentralisation is at the core of blockchains, having centralised oracles destroys the purpose.

Chainlink is a decetralised oracle.

Decentralised Oracle Networks (DONs)

DONs can be built with as many or few nodes as needed and every aspect can be customised for the data being tracked. They can have their own system of consensus as well. For example, nodes in a DON may have different prices for ETH/ USD that is aggregated. The aggregation is system of consensus.

Building a DON from scratch is a lot of work. Chainlink provides us access to tried and tested DONs.

Chainlink Data Feeds, a type of Chainlink DONs can be viewed at https://data.chain.link/.


Hybrid Smart Contracts

They combine on-chain (code) and off-chain (data and computation) systems. For example, Decentralised Finance (DeFi) takes traditional financial systems like trading and makes it into Decentralised Apps. For this, the smart contracts must fetch exchange rates from the real world.

Chainlink has a bunch of Hybrid Reference Smart Contracts for interacting with the real world. We shall go over them with examples in the following sections.


Chainlink VRF (Verifiable Random Function)

Randomness is difficult to achieve in computers as they are deterministic systems. It is even more difficult in blockchains because they are deterministic and transparent. Random numbers can be got externally but we must trust the source and the source could be a single point of failure.

A request to the Chainlink VRF returns a random number from an off-chain chainlink oracle along with a cryptographic proof so that we can prove and verify that the number we have is actually random.

Request and Receive Cycle

The request and receive is a two transaction cycle. In one transaction we make a request to a Chainlink Oracle and in the second transaction the oracles responds with the requested data. In order to make a request, the smart contract must be funded with LINK tokens, which are ERC20 tokens built on the ERC677 Standard and are native to the Chainlink Oracles.

Like we need ETH to transact with the Ethereum Blockchain, we need LINK to request data/ interact with Chainlink Oracles. This is known as the Oracle cost which is similar to transaction cost. This cost depends on the oracle itself as individual Chainlink node operators can set the price to whatever they want.

Example: Getting a Random Number using Chainlink VRF

We will deploy this contract and fetch data from it on the localhost or the rinkeby testnet.

Note: Please make sure you havesome Rinkeby testnet ETH and LINK to work with.

If you need some, head over to https://faucets.chain.link/rinkeby.

Chainlink has released VRF v2 and to work with it we must familiarise ourselves with Subscriptions. It is a way of pre-paying for VRF v2 requests so we don't need to fund our contract with LINK each time we request a random number. This also reduces the overall gas cost paid over multiple requests. Read more at https://docs.chain.link/docs/chainlink-vrf/#subscriptions.

Now let's got to the Subscription Manager to create a subscription to fund our contract.

  • Create a new subsciption account and add funds to it as directed.
  • Click the add consumer button to view Subscription ID. We will need this to deploy our contract and get a random number.

Working with a VRF v2 compatible contract

We will be working with the sample contract RandomNumberConsumableV2.sol provided by Chainlink. The aim is to get provably random values from a Chainlink oracle.

Create a new file in your contracts folder named RandomNumberConsumableV2.sol and paste the following code in it. Comments have been added in the code to identify what block of code performs what task.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

//importing supporting contracts for RandomNumberConsumerV2
import "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";

//inheriting code from VRFConsumerBase import
contract RandomNumberConsumerV2 is VRFConsumerBaseV2 {

  //Interface for VRF Coordinator that verifies the numbers returned are actually random
  VRFCoordinatorV2Interface immutable COORDINATOR;

  //Interface for the Link Token
  LinkTokenInterface immutable LINKTOKEN;

  //Your subscription ID from earlier (Hint: Subscription Manager)
  uint64 immutable s_subscriptionId;

  //keyHash used to determine which Chainlink Oracle to use to get a random number
  bytes32 immutable s_keyHash;

  // Depends on the number of requested values that you want sent to the
  // fulfillRandomWords() function. Storing each word costs about 20,000 gas,
  // so 100,000 is a safe default for this example contract. Test and adjust
  // this limit based on the network that you select, the size of the request,
  // and the processing of the callback request in the fulfillRandomWords()
  // function.
  uint32 immutable s_callbackGasLimit = 100000;

  // The default is 3, but you can set this higher.
  uint16 immutable s_requestConfirmations = 3;

  // For this example, retrieve 2 random values in one request.
  // Cannot exceed VRFCoordinatorV2.MAX_NUM_WORDS.
  uint32 immutable s_numWords = 2;

  //Array for storing the received random values
  uint256[] public s_randomWords;

  //request Id for requesting random values
  //request Id ensures you get your random values and not someone else's
  uint256 public s_requestId;

  //address variable
  address s_owner;

  //event to return the array of random words
  event ReturnedRandomness(uint256[] randomWords);

  //constructor is function called when smart contract is deployed
  //we must pass the VRFConsumerBase constructor since we are inheriting that contract also
  constructor(
    uint64 subscriptionId,
    address vrfCoordinator,
    address link,
    bytes32 keyHash
  ) VRFConsumerBaseV2(vrfCoordinator) {
    COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
    LINKTOKEN = LinkTokenInterface(link);
    s_keyHash = keyHash;
    s_owner = msg.sender;
    s_subscriptionId = subscriptionId;
  }

  //function to request the random values
  //onlyOwner modifier ensures that only contract owner can call this
  function requestRandomWords() external onlyOwner {
    //Will revert if subscription is not set and funded
    //Please make sure you have funds in subscription
    s_requestId = COORDINATOR.requestRandomWords(
      s_keyHash,
      s_subscriptionId,
      s_requestConfirmations,
      s_callbackGasLimit,
      s_numWords
    );
  }


  //Callback function used by VRF Coordinator
  function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override {
    s_randomWords = randomWords;
    emit ReturnedRandomness(randomWords);
  }

  //checks if contract owner is the one calling a transaction
  modifier onlyOwner() {
    require(msg.sender == s_owner);
    _;
  }
}

Since we are importing contracts from chainlink, make sure you run the following in the terminal to have access to the @chainlink/contracts package.

npx i @chainlink/contracts
#or
yarn add @chainlink/contracts

Deploying the contract

We will deploy this slightly differently from the one before. But it's gonna be cool!

Let's first get the hardhat.config.js setup to run our script smoothly. For that we require some more dependencies for this.

npx i hardhat-deploy dotenv @chainlink/token
#or
yarn add hardhat-deploy dotenv @chainlink/token

Make sure your hardhat.config.js file looks like this. We are configuring it to deploy our contracts on the local network and rinkeby testnet at the moment:

const { version } = require("chai");

require("@nomiclabs/hardhat-waffle")
require("@nomiclabs/hardhat-ethers")
// enables use of yarn hardhat deploy
require("hardhat-deploy");
// allows us to run custom tasks
require("./tasks");
// enables reading environment variables
require("dotenv").config();

const defaultNetwork = "hardhat";

/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
  solidity: {
    compilers: [
      {
        version: "0.4.24",
      },
      {
        version: "0.8.7",
      }
    ],
  },
  defaultNetwork,
  //configuring network chain Ids for the deploy script
  networks: {
    hardhat: {
      chainId: 31337
    },
    rinkeby: {
      chainId: 4,
      // rpc node url (store as environment variable)
      // i like to use alchemy
      url: process.env.RINKEBY_RPC_URL,
      // wallet private key (store as environment variable)
      accounts: [process.env.PRIVATE_KEY],
      // gas override to prevent gas  estimation error
      gas: 2100000, 
      gasPrice: 8000000000,
    },
  },
  // when we get fake accounts, deployer is always at index 0
  namedAccounts: {
    deployer: {
      default: 0,
    },
  },
};

Create a .env file in the main folder and add the RINKEBY_RPC_URL, wallet (use test wallet that have been configured with the Subscription Manager) PRIVATE_KEY and VRF_SUBSCRIPTION_ID (from earlier) in it.

As said earlier, we will configure it for the Hardhat local network and the Rinkeby network.

For the hardhat network we will use a process known as mocking where we create a fake chainlink vrf node. For that we need to create a new folder named test in the contracts folder and create two new files named VRFCoordinatorV2Mock.sol and LinkToken.sol in it. We are basically getting fake VRFCoordinator and LinkToken addresses for the local network

Copy the following code in the VRFCoordinatorV2Mock.sol file:

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

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

Add this to the LinkToken.sol file:

// SPDX-License-Identifier: MIT
pragma solidity ^0.4.24;

import "@chainlink/token/contracts/v0.4/LinkToken.sol";

In the package.json file, redefine the value of "@nomiclabs/hardhat-ethers" as:

"@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers@^0.3.0-beta.10",

Great!! Now let's write some scripts to deploy our contract.

First we must create a folder named deploy in the main directory and create a file name 00-deploy-mocks.js in it. This file will be the script to deploy the mock VRFCoordinatorV2Mock.sol and LinkToken.sol.

Add the following code to it:

const { network } = require("hardhat")

const BASE_FEE = "250000000000000000000"
const GAS_PRICE_LINK = 1e9 // link per gas

// getNamedAccounts, deployments are props from hardhat-deploy library
// give you utilities to define user accounts and deploy contracts easily
module.exports = async({ getNamedAccounts, deployments }) => {

    const { deploy, log, get } = deployments
    // we set the deployer in hardhart.config.js earlier
    const { deployer } = await getNamedAccounts()
    // we get chainId based on the network we deploy our contracts on
    const chainId = network.config.chainId

    // chainId for localhost
    // runs only if we deploy to localhost
    if (chainId == 31337) {

        // deploy mock link token
        await deploy("LinkToken", { from: deployer, log: true })

        // deploy mock VRF Coordinator
        await deploy("VRFCoordinatorV2Mock", {
            from: deployer,
            log: true,
            args: [BASE_FEE, GAS_PRICE_LINK],
        })
    }
}

module.exports.tags = ["all", "mocks"]

Moving on to the script to deploy the actual contract. Create a file named 01-random-number-consumer.js in the deploy folder and paste the following code:

const { network } = require("hardhat")

module.exports = async function (hre) {
    const { getNamedAccounts, deployments } = hre
    const { deploy, log, get } = deployments
    const { deployer } = await getNamedAccounts()
    const chainId = network.config.chainId

    let vrfCoordinatorAddress
    let subscriptionId

    const FUND_AMOUNT = "10000000000000000000"

    // if we are working with local networks we wont have 
    // the vrfCoordinator and linktoken
    // we need to configure it for the local network separately in that case
    // runs if we deploy to local host
    if ( chainId === 31337 ) {
        log("if")
        // make a fake chainlink vrf node
        // mocking
        const vrfCoordinatorV2Mock = await hre.ethers.getContract("VRFCoordinatorV2Mock")
        vrfCoordinatorAddress = vrfCoordinatorV2Mock.address
        log(vrfCoordinatorAddress)

        const linkTokenMock = await hre.ethers.getContract("LinkToken")
        linkTokenAddress = linkTokenMock.address
        log(linkTokenAddress)

        // create subscription to fund our contract request
        const tx = await vrfCoordinatorV2Mock.createSubscription()
        const txReceipt = await tx.wait(1)
        //we get subscription id
        subscriptionId = txReceipt.events[0].args.subId
        // // now we fund it with fake tokens for mocking
        await vrfCoordinatorV2Mock.fundSubscription(subscriptionId, FUND_AMOUNT)
    
    // else statement runs if we deploy to rinkeby
    } else {
        log("else")
        // use real chainlink vrf node
        vrfCoordinatorAddress = "0x6168499c0cFfCaCD319c818142124B7A15E857ab",
        subscriptionId =process.env.VRF_SUBSCRIPTION_ID,
        linkTokenAddress = "0x01BE23585060835E02B77ef475b0Cc51aA1e0709"

    }

    args = [
        subscriptionId,
        vrfCoordinatorAddress,
        linkTokenAddress,
        // this is the keyHash
        // keyHash is same for localhost and Rinkeby
        "0xd89b2bf150e3b9e13446986e571fb9cab24b13cea0a43ea20a6049a85cc807cc",
        
    ]

    // deploying the RandomNumberConsumerV2 contract
    const randomNumberConsumerV2 = await deploy("RandomNumberConsumerV2", {
        from: deployer,
        log: true,
        args: args,
    })

    // this log returns the next steps
    log("Then run RandomNumberConsumer contract with the following command")
    const networkName = network.name == "hardhat" ? "localhost" : network.name
    log(
        // yarn hardhat request-random-number is a task
        // read the next section named "tasks" before running the task
        `yarn hardhat request-random-number --contract ${randomNumberConsumerV2.address} --network ${networkName}`
    )
    log("----------------------------------------------------")
    
}

module.exports.tags =["all"]

If you're working with the localhost, run this before running the next command:

npx hardhat node
#or
yarn hardhat node

Note: However, localhost is unable to return requested random value for me yet. Running on Rinkeby network is advised.

Now run the following to deploy your scripts:

npx hardhat deploy
#or
yarn hardhat deploy

Once our contract is deployed, copy the contract address, head over to the Subscription Manager, select Add consumer and paste wallet address. This will create a connection between your Subscription and deployed contract to fund your request easliy and automatically.

Creating and Running Tasks

Tasks are just async function that can automate somethings for you. You have been using tasks without even knowing. yarn hardhat deploy or yarn hardhat node are some default tasks.

Let's get some tasks. Create a folder named tasks in the root directory and copy the files and folders from the tasks folder of this chainlink-hardhat-test repo.

Now you will be able to run tasks. Follow the suggested instructions from the console log after deploying your contracts as shown in the previous section. Another console log will show you how to read the requested random values, once you request them.


Additional Resources for Reference