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.
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/.
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.
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.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.
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 general format for standalone (that are run individually) scripts is:
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
Coming soon.
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.
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/.
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.
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.
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.
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.
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
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.
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.