Creating a Lottery with Hardhat and Chainlink
Jonathan Burmester
Posted on January 4, 2022
The code repo is here. In this tutorial, I will be covering only the smart contract. But the repo also has a frontend powered by Vite (React), Ethers.js, and Tailwind CSS to interact with the smart contract, and a simple backend because I wanted to keep the data off-chain, so I'm using WebSockets to listen to the contract events to sync data into a Postgres database.
A couple of months ago, I started my journey into web3 by learning Solidity and haven't stopped experimenting with it since then. One of the topics that caught my attention very early in this stage was randomness.
Randomness generation requires a source of entropy, which is typically related to hardware-related events such as keypress, free memory, mouse movements, or unpredictable physical variables. In Solidity, you could rely on the block variables as the source of entropy, but you need to be careful because miners can manipulate those values.
Fortunately, oracles are here to help us. These are entities that connect the blockchain with external data. The oracle will help us get the random number from outside the network in this tutorial. Some of the most established oracle solutions are Chainlink and Provable (Formerly Oraclize).
I think you already have an idea which one we will be using. Right?
Chainlink provides different alternatives to get off-chain data. To solve the random generation problem, it offers a Verifiable Random Function (VRF) to obtain random numbers from outside the blockchain.
What we will build
We will be building a lottery smart contract with the following features:
- It will have an admin.
- The admin will be the only one who will create lotteries.
- A lottery will have a ticket price, an end date, and a single winner.
- Players will be able to participate by paying the ticket price.
- Players will be able to participate any number of times.
- Each participation increases the likelihood of winning.
- The winner will be declared after the end of the lottery by taking a random address from the participation list.
Prerequisites
Setup
We will start by creating a folder and initializing the project.
mkdir cryptoLottery
cd cryptoLottery
npm init -y
Next, install hardhat using yarn or npm.
yarn add hardhat --dev
Now, we will initialize hardhat. I will be using the advanced project because it installs some linters that I found helpful, but feel free to use the simple project to avoid unnecessary package installation.
npx hardhat
888 888 888 888 888
888 888 888 888 888
888 888 888 888 888
8888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888
888 888 "88b 888P" d88" 888 888 "88b "88b 888
888 888 .d888888 888 888 888 888 888 .d888888 888
888 888 888 888 888 Y88b 888 888 888 888 888 Y88b.
888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888
Welcome to Hardhat v2.8.0
? What do you want to do? …
Create a basic sample project
▸ Create an advanced sample project
Create an advanced sample project that uses TypeScript
Create an empty hardhat.config.js
Quit
Next, we will install hardhat-deploy to improve our deployment experience and fund-link plugin to help us fund our contract with LINK. Hardhat-deploy will allow us to deploy on different networks and keep track of them. Also, it helps to re-use our deployments during tests.
yarn add hardhat-deploy @appliedblockchain/chainlink-plugins-fund-link --dev
Now, change your hardhat.config.js
to look like this:
require("dotenv").config();
require("@nomiclabs/hardhat-etherscan");
require("@nomiclabs/hardhat-waffle");
// Advance project aditional libraries
require("hardhat-gas-reporter");
require("solidity-coverage");
//////////////////////////////////////
require("hardhat-deploy");
require("@appliedblockchain/chainlink-plugins-fund-link");
module.exports = {
networks: {
rinkeby: {
url: process.env.RINKEBY_URL || "",
chainId: 4,
accounts: [
process.env.PRIVATE_KEY_DEPLOYER,
process.env.PRIVATE_KEY_USER_2,
process.env.PRIVATE_KEY_USER_3,
].filter((x) => x !== undefined),
},
},
gasReporter: {
enabled: process.env.REPORT_GAS !== undefined,
currency: "USD",
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY,
},
namedAccounts: {
deployer: {
default: 0,
4: 0,
},
user2: {
default: 1,
4: 1,
},
user3: {
default: 2,
4: 2,
},
},
solidity: {
compilers: [
{
version: "0.8.7",
},
{
version: "0.6.6",
},
{
version: "0.4.24",
},
],
},
mocha: {
timeout: 10000000,
},
};
Some of the changes to this file:
- Plugins
hardhat-deploy
and@appliedblockchain/chainlink-plugins-fund-link
imported at the beginning of the file. - Rinkeby network configured in the networks list.
- NamedAccounts is a feature from hardhat-deploy that allows us to name the accounts.
- Additional compiler versions configured. This is required to compile the contracts we will use from Chainlink.
Finally, create a .env
file with the following values:
RINKEBY_URL=https://eth-rinkeby.alchemyapi.io/v2/<key>
PRIVATE_KEY_DEPLOYER=<private_key_1>
PRIVATE_KEY_USER_2=<private_key_2>
PRIVATE_KEY_USER_3=<private_key_3>
Coding the Contract
Hardhat sample project generates some files we won't use, so let's delete them.
rm contracts/Greeter.sol
rm scripts/deploy.js
rm test/sample-test.js
Next, we create our smart contract in the contracts folder.
touch contracts/Lottery.sol
pragma solidity ^0.8.0;
contract LotteryGame {
}
Chainlink VRF prerequisites
Before getting into the details of our contract, these are the prerequisites we need to meet for Chainlink VRF to work.
- Our contract should inherit
VRFBaseConsumer
from the Chainlink contracts package. - Call
requestRandomness
in our contract to request the random number. - Define the
fulfillRandomness
function, which is the function that will receive the random number and do something with it. - Define the following parameters in our contract:
- LINK Token: Link token address on the network you are deploying your contract to.
- VRF Coordinator address: address of the VRF Coordinator on the network you are deploying your contract to.
- Key Hash: public key used to generate randomness.
- Fee: LINK fee required on the network to fulfill the VRF request.
Warning: without LINK in our contract we won't be able to request a random number.
We will adapt our contract to meet all these requirements later.
Libraries
Besides using Chainlink, we will also use some libraries from Openzeppelin:
Counters
to obtain an incremental integer that will be used as an id for our lotteries.
SafeMath
to avoid overflow in math operations.
Let's install both of them:
yarn add @chainlink/contracts @openzeppelin/contracts
And import them into our contract.
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";
Our contract must inherit from VRFConsumerBase
. This new contract will provide us with methods to request the random number and to receive the result.
contract LotteryGame is VRFConsumerBase {
using Counters for Counters.Counter;
using SafeMath for uint256;
....
}
Structs
Next, we define our Lottery struct according to the definitions from above:
struct Lottery {
uint256 lotteryId;
address[] participants;
uint256 ticketPrice;
uint256 prize;
address winner;
bool isFinished;
uint256 endDate;
}
Then, we add some variables to help us with lotteries management.
Counters.Counter private lotteryId;
mapping(uint256 => Lottery) private lotteries;
mapping(bytes32 => uint256) private lotteryRandomnessRequest;
mapping(uint256 => mapping(address => uint256)) ppplayer; //participations per player
mapping(uint256 => uint256) playersCount;
bytes32 private keyHash;
uint256 private fee;
address private admin;
▸ lotteryId
will be an incremental uint we will use to identify our lotteries.
▸ lotteries
will store all of our lotteries.
▸ lotteryRandomnessRequest
will let us know how the request for a random number and a lottery are related. More on this later.
▸ ppplayer
will store the number of times an address have participated in a lottery.
▸ playersCount
will keep the number of unique addresses participating in a lottery.
▸ keyHash
and fee
are parameters required by Chainlink VRF.
▸ admin
will be the deployer's address and will be the only address capable of creating lotteries and declaring winners.
Events
We also define some events:
event RandomnessRequested(bytes32,uint256);
event WinnerDeclared(bytes32,uint256,address);
event PrizeIncreased(uint256,uint256);
event LotteryCreated(uint256,uint256,uint256,uint256);
Functions
Following VRF prerequisites, our contract constructor will take all the VRF required parameters as arguments and supply the first two to the VRFConsumerBase constructor. Also, we will set the admin of our contract.
constructor(address vrfCoordinator, address link, bytes32 _keyhash, uint256 _fee)
VRFConsumerBase(vrfCoordinator, link)
{
keyHash = _keyhash;
fee = _fee;
admin = msg.sender;
}
We also define a modifier to limit some methods to be called from admin only.
modifier onlyAdmin {
require(msg.sender == admin, "Only admin can call this function");
_;
}
Then, we define a function to create lotteries:
function createLottery(uint256 _ticketPrice,uint256 _seconds) payable public onlyAdmin {
require(_ticketPrice > 0, "Ticket price must be greater than 0");
require(_seconds > 0, "Lottery time must be greater than 0");
Lottery memory lottery = Lottery({
lotteryId: lotteryId.current(),
participants: new address[](0),
prize: 0,
ticketPrice: _ticketPrice,
winner: address(0),
isFinished: false,
endDate: block.timestamp + _seconds * 1 seconds
});
lotteries[lotteryId.current()] = lottery;
lotteryId.increment();
emit LotteryCreated(lottery.lotteryId,lottery.ticketPrice,lottery.prize,lottery.endDate);
}
As you can see, we are validating that the ticketPrice
and seconds
are greater than 0. Then, we create a new instance of a lottery and put it into our mapping using the current value of our lotteryId
as key. Next, we increment the lotteryId
for the next lottery and emit an event to notify the creation of a lottery.
Also, note that I'm using
block.timestamp
as a reference to establish the end of the lottery, but depending on your contract, you should consider usingblock.number
and block times to calculate time. This is a good lecture if you are interested in this topic.
Next, we implement the participate function:
function participate(uint256 _lotteryId) public payable {
Lottery storage lottery = lotteries[_lotteryId];
require(block.timestamp < lottery.endDate,"Lottery participation is closed");
require(lottery.ticketPrice == msg.value, "Value must be equal to ticket price");
lottery.participants.push(msg.sender);
lottery.prize += msg.value;
uint256 uniqueP = participations[_lotteryId][msg.sender];
if(uniqueP == 0) {
uniqueParticipations[_lotteryId]++;
}
participations[_lotteryId][msg.sender]++;
emit PrizeIncreased(lottery.lotteryId, lottery.prize);
}
This function will let players participate in the lottery by sending some ETH equals to the ticketPrice
when the lottery has not ended. We also keep track of players with multiple participations to avoid requesting a random number when we only have a single player.
Next, is the declareWinner function:
function declareWinner(uint256 _lotteryId) public onlyAdmin {
Lottery storage lottery = lotteries[_lotteryId];
require(block.timestamp > lottery.endDate,"Lottery is still active");
require(!lottery.isFinished,"Lottery has already declared a winner");
if(uniqueParticipations[_lotteryId] == 1) {
require(lottery.participants[0] != address(0), "There has been no participation in this lottery");
lottery.winner = lottery.participants[0];
lottery.isFinished = true;
lottery.winner.call{value: lottery.prize }("");
require(success, "Transfer failed");
} else {
require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK");
bytes32 requestId = requestRandomness(keyHash, fee);
lotteryRandomnessRequest[requestId] = _lotteryId;
emit RandomnessRequested(requestId,_lotteryId);
}
}
This function verifies that all the requirements have been met before declaring a winner. If we have a single player, we transfer the "prize" back to that player. Otherwise, we validate if the contract has enough LINK to request the random number to the oracle. If there is enough LINK, we call the requestRandomness
function with keyHash
and fee
as the arguments. Then, we map the requestId
to the lotteryId
because when the random number comes back within the fulfillRandomness
function it will only have the requestId and the random number, so without the mapping we won't be able to determine the winner of the right lottery.
Almost done, we define the fulfillRandomness function:
function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
uint256 _lotteryId = lotteryRandomnessRequest[requestId];
Lottery storage lottery = lotteries[_lotteryId];
uint256 winner = randomness.mod(lottery.participants.length);
lottery.isFinished = true;
lottery.winner = lottery.participants[winner];
delete lotteryRandomnessRequest[requestId];
delete uniqueParticipations[_lotteryId];
lottery.winner.call{value: lottery.prize }("");
emit WinnerDeclared(requestId,lottery.lotteryId,lottery.winner);
}
Notice that we override the function's implementation to declare the winner with the random number. Then we are using mod (modulo) from the SafeMath library to get a number between 0 and the length of participations - 1. Finally, we pick the winner using the new number and declare the winner. Also, we save some gas by deleting some of the mapping values :).
Great! We now have our contract completed, but how do we deploy it? how do we test it if we don't have LINK in our local environment and can't interact with the VRFCoordinator?
Deployment
At the beginning of this tutorial, we installed hardhat-deploy plugin to help us with the deployment. Before using it, let's compare it with the default deployment method from Hardhat. Hardhat deployments require you to create a script within the scripts folder. The script looks like this:
const Greeter = await hre.ethers.getContractFactory("Greeter");
const greeter = await Greeter.deploy("Hello, Hardhat!");
await greeter.deployed();
And you run it with:
npx hardhat run scripts/deploy.js
On the other hand, hardhat-deploy will read our deployment scripts from the deploy folder.
hardhat-deploy extends Hardhat Runtime Environment (HRE) by including 4 new fields:
getNamedAccounts: hardhat-deploy plugin extends Hardhat config to associate names to addresses and have them configured per chain. This function returns those names and addresses.
getUnnamedAccounts: similar to getNamedAccounts but returns accounts with no name.
deployments: contains functions to access deployments.
getChainId: fetch the current chain id.
The previous script would be written like this:
module.exports = async ({ getNamedAccounts, deployments, getChainId }) => {
const { deploy, log } = deployments;
const { deployer } = await getNamedAccounts();
log("Deploying Greeter");
await deploy("Greeter", {
from: deployer,
log: true,
args: ["Hello, Hardhat!"],
});
}
};
module.exports.tags = ["main"];
We are including tags
, a list of string values that can be referenced to execute either a single script or a group of them. Moreover, it allows us to establish dependencies between deployments which could be handy when writing complex deployment procedures.
Then, you can deploy it with:
npx hardhat deploy
Ok, let's see this in action. First, create the deploy folder at the root of our hardhat project.
mkdir deploy
To test the Chainlink VRF, we need to deploy the LINK token in our HardHat environment and mock the VRFCoordinador
to simulate the random number request. Fortunately, the Chainlink contracts package already comes with the contracts we need. We will create new contracts and import the Chainlink contracts to avoid copying all the code into our project.
Let's create the new contracts:
mkdir contracts/external
touch contracts/external/LinkToken.sol
touch contracts/external/VRFCoordinatorMock.sol
LinkToken.sol
pragma solidity ^0.4.0;
import "@chainlink/contracts/src/v0.4/LinkToken.sol";
VRFCoordinatorMock.sol
pragma solidity ^0.6.0;
import "@chainlink/contracts/src/v0.6/tests/VRFCoordinatorMock.sol";
Now, we code our deployment script
touch contracts/00_Deploy_External.js
00_Deploy_External.js
module.exports = async ({ getNamedAccounts, deployments, getChainId }) => {
const { deploy, log } = deployments;
const { deployer } = await getNamedAccounts();
const chainId = await getChainId();
if (chainId == 31337) {
log("Local Network Detected, Deploying external contracts");
const linkToken = await deploy("LinkToken", { from: deployer, log: true });
await deploy("VRFCoordinatorMock", {
from: deployer,
log: true,
args: [linkToken.address],
});
}
};
module.exports.tags = ["all", "mocks", "main"];
This deployment script will be executed when any of the tags
are referenced in the deploy command or in tests. But, it will deploy the LinkToken
and VRFCoordinatorMock
contracts only when the chainId is 31337, which is the chain Id of our HardHat environment node. There's no need to deploy it on the others supported chains because the contracts are already there.
Do you remember that our contract constructor has four input parameters required by ChainLink? Where do we get those values from? the answer is here
To include these values in our project, we create a config file:
mkdir config
touch config/chainlink.config.js
chainlink.config.js
const config = {
// Hardhat local network
// Mock Data (it won't work)
31337: {
name: "hardhat",
keyHash:
"0x6c3699283bda56ad74f6b855546325b68d482e983852a7a82979cc4807b641f4",
fee: "0.1",
fundAmount: "10000000000000000000",
},
// Rinkeby
4: {
name: "rinkeby",
linkToken: "0x01BE23585060835E02B77ef475b0Cc51aA1e0709",
vrfCoordinator: "0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B",
keyHash:
"0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311",
fee: "0.1",
fundAmount: "2000000000000000000",
},
};
module.exports = {
config
};
touch contracts/01_Deploy_Lottery.js
01_Deploy_Lottery.js
const { config } = require("../config/link.config");
module.exports = async ({ getNamedAccounts, deployments, getChainId, ethers }) => {
const { deploy, get, log } = deployments;
const { deployer } = await getNamedAccounts();
const chainId = await getChainId();
let linkToken;
let linkTokenAddress;
let VRFCoordinatorMock;
let vrfCoordinatorAddress;
let additionalMessage = "";
if (chainId == 31337) {
linkToken = await get("LinkToken");
VRFCoordinatorMock = await get("VRFCoordinatorMock");
linkTokenAddress = linkToken.address;
vrfCoordinatorAddress = VRFCoordinatorMock.address;
additionalMessage =
" --linkaddress " +
linkTokenAddress +
" --fundadmount " +
config[chainId].fundAmount;
} else {
linkTokenAddress = config[chainId].linkToken;
vrfCoordinatorAddress = config[chainId].vrfCoordinator;
}
const keyHash = config[chainId].keyHash;
const fee = config[chainId].fee;
const lottery = await deploy("LotteryGame", {
from: deployer,
args: [
vrfCoordinatorAddress,
linkTokenAddress,
keyHash,
ethers.utils.parseUnits(fee, 18),
],
log: true,
});
log("Run the following command to fund contract with LINK:");
log(
"npx hardhat fund-link --contract " +
lottery.address +
" --network " +
config[chainId].name +
additionalMessage
);
log("----------------------------------------------------");
};
module.exports.tags = ["all", "main"];
If we are in our local environment (chain id 31337), the script will get the address of our local LinkToken and VRFCoordinator contracts and use them to deploy our Lottery contract. On the other hand, if we deploy to non-local networks, we will use the addresses from the config file.
Also, we log a message to remind the user to run the fund-link command to send LINK to our Lottery contract.
npx hardhat deploy
Local Network Detected, Deploying Mocks
deploying "LinkToken" (tx: 0x596222d755670dab88fa5005d6e6f25097b05ccef8810d13ec5b6c8adb02e05d)...:
deployed at 0x5FbDB2315678afecb367f032d93F642f64180aa3 with 1279067 gas
deploying "VRFCoordinatorMock" (tx: 0xa7a3d8b4f67818b87d496938f4075c3c46f04d392e7ae2758b4c72807e215d3f)...:
deployed at 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 with 363662 gas
deploying "LotteryGame" (tx: 0x92d0807c1a47825c8197d00ec44425c23e4499b84bbd91d82308e03363659e37)...:
deployed at 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
.........
Great! We have deployed all the contracts into our local environments. Now, let's focus on testing.
Testing
You can find all the tests in the repository. Here I will only focus on VRF testing.
Let's start by creating our unit test file:
touch test/lottery_unit_test.js
lottery_unit_test.js
const { expect } = require("chai");
const { ethers, getChainId, deployments } = require("hardhat");
const { config, autoFundCheck } = require("../config/link.config");
describe("LotteryGame Unit Tests", () => {
let LotteryGame;
let lotteryGame;
let LinkToken;
let linkToken;
let chainId;
let deployer;
let user2;
let user3;
const DAY = 3600 * 24;
before(async () => {
[deployer, user2, user3] = await ethers.getSigners();
chainId = await getChainId();
await deployments.fixture(["main"]);
LinkToken = await deployments.get("LinkToken");
linkToken = await ethers.getContractAt("LinkToken", LinkToken.address);
LotteryGame = await deployments.get("LotteryGame");
lotteryGame = await ethers.getContractAt(
"LotteryGame",
LotteryGame.address
);
});
it("Should Declare a winner", async () => {
const networkName = config[chainId].name;
const additionalMessage = " --linkaddress " + linkToken.address;
if (
await autoFundCheck(
lotteryGame.address,
networkName,
linkToken.address,
additionalMessage
)
) {
await hre.run("fund-link", {
contract: lotteryGame.address,
linkaddress: linkToken.address,
});
}
await lotteryGame.createLottery(ethers.utils.parseEther("0.0005"), DAY * 1);
await lotteryGame.connect(user2).participate(0, {
value: ethers.utils.parseEther("0.0005"),
});
await lotteryGame.connect(user3).participate(0, {
value: ethers.utils.parseEther("0.0005"),
});
await ethers.provider.send("evm_increaseTime", [3600 * 24 * 2]);
const tx = await lotteryGame.declareWinner(0);
const txReceipt = await tx.wait();
const requestId = txReceipt.events[2].topics[1];
// eslint-disable-next-line no-unused-expressions
expect(requestId).to.be.not.null;
});
});
We start the unit test script by deploying our contracts with await deployments.fixture(["main"])
. In this case, we are deploying all the scripts that include the tag "main". Then, we get the reference to our Lottery
Contract and the LinkToken
.
Next, we define the test itself and start by running the fund-link command to fund our Lottery contract with LINK using hre.run
. Then, we create a lottery and impersonate some users to participate in it. Since we only can declare a winner after the lottery has ended, we will jump forward in time with await ethers.provider.send("evm_increaseTime", [3600 * 24 * 2])
. Finally, we declare the winner and since we can't expect a random number, the only way we have to validate that the random number was requested is to read events from the transaction receipt.
The transaction receipt from const txReceipt = await tx.wait();
contains an event array with all the events emitted by our contract method call. Each event will have an array of topics up to four elements where the first element will be the hash obtained from the function signature and the rest will be indexed arguments. You can read more about this here.
There are four events that will be emitted with our method call:
- events[0]: Transfer(address,address,uint256) from ERCBasic.sol
- events[1]: Transfer(address,address,uint256,bytes) from ERC677 called from ERC677Token.sol
- events[2]: RandomnessRequest(address,bytes32,uint256) from VRFCoordinatorMock.sol
- events[3]: WinnerDeclared from Lottery.sol
So, if the random number was successfully requested, a RandomnessRequest event should be emitted with the second indexed argument not equal to 0.
const requestId = txReceipt.events[2].topics[1];
expect(requestId).to.be.not.null;
If you wonder were ERCBasic, ERC677Token contracts are, you can find them inside the @chainlink/contracts package.
Next, we run our unit test script:
npx hardhat test ./test/lottery_unit_test.js
The integration script is similar to the previous one. But we need to fund our contract with LINK from the network we will deploy our contract to. We can use Chainlink faucets to fund our wallets with LINK and then run fund-link command to send LINK to our contract.
Warning: It is essential to select the correct network when requesting LINK. Otherwise, you won't be able to send LINK into your contract.
Then, we deploy our contract to a testnet:
touch test/lottery_integration_test.js
lottery_integration_test.js
describe("Lottery Integration Tests", () => {
let LotteryGame;
let lotteryGame;
let chainId;
let deployer;
let user2;
let user3;
before(async () => {
LotteryGame = await deployments.get("LotteryGame");
lotteryGame = await ethers.getContractAt(
"LotteryGame",
LotteryGame.address
);
[deployer, user2, user3] = await ethers.getSigners();
});
it("Should receive the random number from the Oracle", async () => {
const createLotteryTx = await lotteryGame.createLottery(120, {
value: ethers.utils.parseEther("0.0005"),
});
await createLotteryTx.wait();
console.log("Lottery created", new Date());
const length = await lotteryGame.getLotteryCount();
const lengthNumber = length.toNumber() - 1;
console.log("Current lottery id", lengthNumber);
const lottery = await lotteryGame.getLottery(lengthNumber);
console.log("EndDate: ", new Date(lottery.endDate * 1000));
const participateTx = await lotteryGame
.connect(user2)
.participate(lengthNumber, {
value: ethers.utils.parseEther("0.0005"),
});
console.log(participateTx);
await participateTx.wait();
console.log("User2 participated", new Date());
const lottery2 = await lotteryGame.getLottery(lengthNumber);
console.log(lottery2);
await new Promise((resolve) => setTimeout(resolve, 120000));
const winnerTx = await lotteryGame.declareWinner(lengthNumber);
await winnerTx.wait();
// Give the oracle some minutes to update the random number
await new Promise((resolve) => setTimeout(resolve, 180000));
const lotteryAfter = await lotteryGame.getLottery(lengthNumber);
console.log(lotteryAfter);
expect(lotteryAfter.winner).to.not.be.eq(
"0x0000000000000000000000000000000000000000"
);
});
});
Finally, we run our integration test script:
npx hardhat test ./test/lottery_unit_test.js
The result should be:
✓ Should receive the random number from the Oracle
1 passing (6m)
Conclusion
This ended up being a more extensive tutorial than I expected. When I started playing with Chainlink VRF I was worried about testing and even read VRFConsumerBase contract and its dependencies to understand how it worked.
I encourage you to read about hardhat-deploy plugin. It has many other features we haven't covered here that could be helpful in your future projects.
In future posts, I'll be writing about smart contracts upgradability patterns and IPFS. If you have some questions about this tutorial or suggestions for future posts, leave some comments below.
Posted on January 4, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.