Hussain'z
Posted on March 28, 2022
Few months ago i started reading about web3 technology and fell in love with ethereum and smart contracts. I decided to learn solidity to write smart contracts.After couple of weeks the first full fledged smart contract which i came across was Lottery Smart Contract which was just a basic smart contract which used to allow anyone to join the lottery by depositing some eth and the lottery Manager(owner of the lottery smart contract) would pick a random winner.But the approach which it took to pick the winner was not actually a true random winner.
function random() private view returns(uint){
return uint(keccak256(abi.encode(block.difficulty,block.timestamp, players)));
}
Problem with the above snippet is that relying on the block variables for the source of entropy(randomness) is not the safe approach, As miners can easily manipulate those values.
We will look at how we can solve this issue by generating the random number off-chain using chailink oracle VRF v2 .
Oracle
Blockchain oracles are entities that connect blockchains to external systems, thereby enabling smart contracts to execute based upon inputs and outputs from the real world.
Tech Stack
- Solidity With Hardhat Framework
Prequisites
- NodeJS
- Fair understanding of how solidity works
- Love for web3 β€οΈ
Let's Start
Create a new directory i'll call it eth-lottery why not it sounds cool π.
npm init -y // inside your eth-lottery directory
Next, Let's Install Hardhat
npm install --save-dev hardhat
Next we initialize hardhat project.
> 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.9.2
? 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
Once the hardhat project is initialized, Next we install hardhat-deploy which will make our life easier with deployments and testsπ
npm install hardhat-deploy
Next, Lets configure our accounts, Hardhat comes with a cool feature NamedAccounts which allows us to access our account with name, rather than accessing by specific index account[0]
Next, In your .env add your account private key and alchemy / infura url which we will be later required for deployements.
For now ignore the VRF_SUBSCRIPTION_ID
we will come to that later
RINKEBY_URL=https://eth-rinkeby.alchemyapi.io/v2/<YOUR ALCHEMY KEY>
PRIVATE_KEY=0xabc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc1
PRIVATE_KEY_USER_2=0xabc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc1
PRIVATE_KEY_USER_3=0xabc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc1
VRF_SUBSCRIPTION_ID=000
Next, update your network section in hardhat.config.js
.
networks: {
ganache:{
url: "http://127.0.0.1:8545",
chainId:1337
},
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,
},
}
Next, Lets install the required libraries.
npm install @chainlink/contracts @openzeppelin/contracts
We will be using chainlink/contracts to implement the vrf and from openzeppeline i'll be using a contract call Counters
which helps to get a counter that can be incremented and decremented we will be using it as our lotteryID.
Next, Create two files Lottery.sol
& LotteryData.sol
in contracts folder. It Should look like this.
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
import "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "./LotteryData.sol";
contract Lottery is VRFConsumerBaseV2{
VRFCoordinatorV2Interface COORDINATOR;
LinkTokenInterface LINKTOKEN;
LotteryData LOTTERY_DATA;
using Counters for Counters.Counter;
using SafeMath for uint256;
Counters.Counter private lotteryId;
uint public totalAllowedPlayers = 10;
address public lotteryManager;
mapping(uint256 => uint256) private lotteryRandomnessRequest;
bytes32 private keyHash;
uint64 immutable s_subscriptionId;
uint16 immutable requestConfirmations = 3;
uint32 immutable callbackGasLimit = 100000;
uint256 public s_requestId;
event RandomnessRequested(uint256,uint256);
//To emit data which will contain the requestId-from chainlink vrf, lotteryId, winnder address
event WinnerDeclared(uint256 ,uint256,address);
//To emit data which will contain the lotteryId, address of new-player & new Price Pool
event NewLotteryPlayer(uint256, address, uint256);
//To emit data which will contain the id of newly created lottery
event LotteryCreated(uint256);
//custom Errors
error invalidValue();
error invalidFee();
error lotteryNotActive();
error lotteryFull();
error lotteryEnded();
error playersNotFound();
error onlyLotteryManagerAllowed();
constructor(
bytes32 _keyHash,
uint64 subscriptionId,
address _vrfCoordinator,
address _link,
address _lotteryData
) VRFConsumerBaseV2(_vrfCoordinator){
lotteryId.increment();
lotteryManager = msg.sender;
COORDINATOR = VRFCoordinatorV2Interface(_vrfCoordinator);
LINKTOKEN = LinkTokenInterface(_link);
s_subscriptionId = subscriptionId;
keyHash = _keyHash;
LOTTERY_DATA = LotteryData(_lotteryData);
}
modifier onlyLotteryManager {
if(msg.sender != lotteryManager) revert onlyLotteryManagerAllowed();
_;
}
function getAllLotteryIds() public view returns(uint256[] memory){
return LOTTERY_DATA.getAllLotteryIds();
}
function startLottery() public payable onlyLotteryManager {
LOTTERY_DATA.addLotteryData(lotteryId.current());
lotteryId.increment();
emit LotteryCreated(lotteryId.current());
}
function enterLottery(uint256 _lotteryId) public payable {
(uint256 lId,
uint256 ticketPrice,
uint256 prizePool,
address[] memory players,
address winner,
bool isFinished) = LOTTERY_DATA.getLottery(_lotteryId);
if(isFinished) revert lotteryNotActive();
if(players.length > totalAllowedPlayers) revert lotteryFull();
if(msg.value < ticketPrice) revert invalidFee();
uint256 updatedPricePool = prizePool + msg.value;
LOTTERY_DATA.addPlayerToLottery(_lotteryId, updatedPricePool, msg.sender);
emit NewLotteryPlayer(_lotteryId, msg.sender, updatedPricePool);
}
function pickWinner(uint256 _lotteryId) public onlyLotteryManager {
if(LOTTERY_DATA.isLotteryFinished(_lotteryId)) revert lotteryEnded();
address[] memory p = LOTTERY_DATA.getLotteryPlayers(_lotteryId);
if(p.length == 1) {
if(p[0] == address(0)) revert playersNotFound();
//require(p[0] != address(0), "no_players_found");
LOTTERY_DATA.setWinnerForLottery(_lotteryId, 0);
payable(p[0]).transfer(address(this).balance);
emit WinnerDeclared(0,_lotteryId,p[0]);
} else {
//LINK is from VRFConsumerBase
s_requestId = COORDINATOR.requestRandomWords(
keyHash,
s_subscriptionId,
requestConfirmations,
callbackGasLimit,
1 // number of random numbers
);
lotteryRandomnessRequest[s_requestId] = _lotteryId;
emit RandomnessRequested(s_requestId,_lotteryId);
}
}
function fulfillRandomWords(uint256 requestId, uint256[] memory randomness) internal override {
uint256 _lotteryId = lotteryRandomnessRequest[requestId];
address[] memory allPlayers = LOTTERY_DATA.getLotteryPlayers(_lotteryId);
uint256 winnerIndex = randomness[0].mod(allPlayers.length);
LOTTERY_DATA.setWinnerForLottery(_lotteryId, winnerIndex);
delete lotteryRandomnessRequest[requestId];
payable(allPlayers[winnerIndex]).transfer(address(this).balance);
emit WinnerDeclared(requestId,_lotteryId,allPlayers[winnerIndex]);
}
function getLotteryDetails(uint256 _lotteryId) public view returns(
uint256,
uint256,
uint256 ,
address[] memory,
address ,
bool
){
return LOTTERY_DATA.getLottery(_lotteryId);
}
}
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;
contract LotteryData {
struct LotteryInfo{
uint256 lotteryId;
uint256 ticketPrice;
uint256 prizePool;
address[] players;
address winner;
bool isFinished;
}
mapping(uint256 => LotteryInfo) public lotteries;
uint256[] public allLotteries;
uint public lotteryTicketPrice = 0.5 ether;
address private manager;
bool private isLotteryContractSet;
address private lotteryContract;
constructor(){
manager = msg.sender;
}
error lotteryNotFound();
error onlyLotteryManagerAllowed();
error actionNotAllowed();
modifier onlyManager(){
if(msg.sender != manager) revert onlyLotteryManagerAllowed();
_;
}
modifier onlyLoterryContract(){
if(!isLotteryContractSet) revert actionNotAllowed();
if(msg.sender != lotteryContract) revert onlyLotteryManagerAllowed();
_;
}
function updateLotteryContract(address _lotteryContract) external onlyManager{
isLotteryContractSet = true;
lotteryContract = _lotteryContract;
}
function getAllLotteryIds() external view returns(uint256[] memory){
return allLotteries;
}
function addLotteryData(uint256 _lotteryId) external onlyLoterryContract{
LotteryInfo memory lottery = LotteryInfo({
lotteryId: _lotteryId,
ticketPrice: lotteryTicketPrice,
prizePool: 0,
players: new address[](0),
winner: address(0),
isFinished: false
});
lotteries[_lotteryId] = lottery;
allLotteries.push(_lotteryId);
}
function addPlayerToLottery(uint256 _lotteryId, uint256 _updatedPricePool, address _player) external onlyLoterryContract{
LotteryInfo storage lottery = lotteries[_lotteryId];
if(lottery.lotteryId == 0){
revert lotteryNotFound();
}
lottery.players.push(_player);
lottery.prizePool = _updatedPricePool;
}
function getLotteryPlayers(uint256 _lotteryId) public view returns(address[] memory) {
LotteryInfo memory tmpLottery = lotteries[_lotteryId];
if(tmpLottery.lotteryId == 0){
revert lotteryNotFound();
}
return tmpLottery.players;
}
function isLotteryFinished(uint256 _lotteryId) public view returns(bool){
LotteryInfo memory tmpLottery = lotteries[_lotteryId];
if(tmpLottery.lotteryId == 0){
revert lotteryNotFound();
}
return tmpLottery.isFinished;
}
function getLotteryPlayerLength(uint256 _lotteryId) public view returns(uint256){
LotteryInfo memory tmpLottery = lotteries[_lotteryId];
if(tmpLottery.lotteryId == 0){
revert lotteryNotFound();
}
return tmpLottery.players.length;
}
function getLottery(uint256 _lotteryId) external view returns(
uint256,
uint256,
uint256 ,
address[] memory,
address ,
bool
){
LotteryInfo memory tmpLottery = lotteries[_lotteryId];
if(tmpLottery.lotteryId == 0){
revert lotteryNotFound();
}
return (
tmpLottery.lotteryId,
tmpLottery.ticketPrice,
tmpLottery.prizePool,
tmpLottery.players,
tmpLottery.winner,
tmpLottery.isFinished
);
}
function setWinnerForLottery(uint256 _lotteryId, uint256 _winnerIndex) external onlyLoterryContract {
LotteryInfo storage lottery = lotteries[_lotteryId];
if(lottery.lotteryId == 0){
revert lotteryNotFound();
}
lottery.isFinished = true;
lottery.winner = lottery.players[_winnerIndex];
}
}
Here we have two smart-contracts Lottery
& LotteryData
.I took this approach so that the LotteryData which holds the overall lotteries and other info can be managed separately.
Let's Quickly Go through LotteryData.sol
first we have a struct mapping. which holds the info to our specific lottery which maps a (uniqueLotteryId => lottery info)
struct LotteryInfo{
uint256 lotteryId;
uint256 ticketPrice;
uint256 prizePool;
address[] players;
address winner;
bool isFinished;
}
mapping(uint256 => LotteryInfo) public lotteries;
Next we define some custom errors, i'll be using custom errors rather than require
as it saves up on some gas.
error lotteryNotFound();
error onlyLotteryManagerAllowed();
error actionNotAllowed();
Next, we have a function called updateLotteryContract
which can only be called by the owner of this[LotteryData.sol
] contract which sets the Lottery.sol
address to the variable
address private lotteryContract
so LotteryData.sol
can be only accessed and modified by the address set for lotteryContract.
Next we have other functions which i won't be explaining in depth.Which are just to maintain each lottery data which will be modified from our Lottery.sol
.
Next, Let's Take a look at our Lottery.sol
First we import our required contracts
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
import "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "./LotteryData.sol";
Next, We Inherit from VRFConsumerBaseV2
contract Lottery is VRFConsumerBaseV2
As we inherit from VRFConsumerBaseV2 we need to also implement the VRFConsumerBaseV2 constructor which expects _vrfCoordinator
constructor(
bytes32 _keyHash,
uint64 subscriptionId,
address _vrfCoordinator,
address _link,
address _lotteryData
) VRFConsumerBaseV2(_vrfCoordinator){
lotteryId.increment();
lotteryManager = msg.sender;
COORDINATOR = VRFCoordinatorV2Interface(_vrfCoordinator);
LINKTOKEN = LinkTokenInterface(_link);
s_subscriptionId = subscriptionId;
keyHash = _keyHash;
LOTTERY_DATA = LotteryData(_lotteryData);
}
VRF Coordinator is a contract that is deployed to a blockchain that will check the randomness of each random number returned from a random node.
With VRFv2 we need VRF Subscription which can be created from
https://vrf.chain.link/ then add the newly generated subscription id to your .env
. Now in order to generate a random number using vrf we need make a request to vrf coordinator.If you take a look at the pickWinner
function
function pickWinner(uint256 _lotteryId) public onlyLotteryManager {
if(LOTTERY_DATA.isLotteryFinished(_lotteryId)) revert lotteryEnded();
address[] memory p = LOTTERY_DATA.getLotteryPlayers(_lotteryId);
if(p.length == 1) {
if(p[0] == address(0)) revert playersNotFound();
//require(p[0] != address(0), "no_players_found");
LOTTERY_DATA.setWinnerForLottery(_lotteryId, 0);
payable(p[0]).transfer(address(this).balance);
emit WinnerDeclared(0,_lotteryId,p[0]);
} else {
//LINK is from VRFConsumerBase
s_requestId = COORDINATOR.requestRandomWords(
keyHash,
s_subscriptionId,
requestConfirmations,
callbackGasLimit,
1 // number of random numbers
);
lotteryRandomnessRequest[s_requestId] = _lotteryId;
emit RandomnessRequested(s_requestId,_lotteryId);
}
}
when the lottery manager calls pickWinner()
we first check if there is just one player in lottery if yes ? we consider the only player player[0] as the winner. Else we request a random number from COORDINATOR which has a function requestRandomWords
which expects keyHash, s_subscriptionId ,requestConfirmations,callbackGasLimit, and number of random numbers required from oracle.
You can find the keyHash from here
In previous version of vrf we had to fund the LINK token address before requesting random number. But with VRFv2 we just add our contract in our case Lottery.sol
as the consumer in subscription manager and maintain sufficient amount of LINK to request random numbers.
Next, Once we call the requestRandomWords
from COORDINATOR. To get the random number we need to implement fulfillRandomWords()
in our Lottery.sol
which is then called by the coordinator which passes the random numbers to our lottery contract.
function fulfillRandomWords(uint256 requestId, uint256[] memory randomness) internal override {
uint256 _lotteryId = lotteryRandomnessRequest[requestId];
address[] memory allPlayers = LOTTERY_DATA.getLotteryPlayers(_lotteryId);
uint256 winnerIndex = randomness[0].mod(allPlayers.length);
LOTTERY_DATA.setWinnerForLottery(_lotteryId, winnerIndex);
delete lotteryRandomnessRequest[requestId];
payable(allPlayers[winnerIndex]).transfer(address(this).balance);
emit WinnerDeclared(requestId,_lotteryId,allPlayers[winnerIndex]);
}
First argument is the requestId
which we get when we called requestRandomWords
second argument is randomness array of uint256[] random numbers
in our case we requested just 1
random number so we access it as randomness[0]
.
Let's take it for a spin with hardhat.
i'll quickly go through the deploy scripts which i have written and also the mocks which can be used for testing.
First lets create new directory called config inside that create chainlink.config.js
const config = {
// Hardhat local network
// Mock Data (it won't work)
31337: {
name: "hardhat",
keyHash:
"0x7c3699283bda56ad74f6b855546325b68d482e983852a7a82979cc4807b641f3",
fee: "0.1",
fundAmount: "10000000000000000000",
},
//ganache
1337: {
name: "ganache",
keyHash:
"0x6c3699283bda56ad74f6b855546325b68d482e983852a7a82979cc4807b641f4",
fee: "0.1",
fundAmount: "10000000000000000000",
},
// Rinkeby
4: {
name: "rinkeby",
linkToken: "0x01BE23585060835E02B77ef475b0Cc51aA1e0709",
vrfCoordinator: "0x6168499c0cFfCaCD319c818142124B7A15E857ab",
keyHash:
"0xd89b2bf150e3b9e13446986e571fb9cab24b13cea0a43ea20a6049a85cc807cc",
fee: "0.25",
fundAmount: "2000000000000000000",
},
};
const developmentChains = ["hardhat", "localhost"]
const VERIFICATION_BLOCK_CONFIRMATIONS = 6
module.exports = {
developmentChains,
VERIFICATION_BLOCK_CONFIRMATIONS,
config,
};
Create two files 00_Deploy_Mocks.js
& 01_Deploy_Lottery.js
//00_Deploy_Mocks.js
const POINT_ONE_LINK = "100000000000000000"
module.exports = async ({getNamedAccounts, deployments, getChainId, network}) => {
const {deploy, log} = deployments;
const {deployer} = await getNamedAccounts();
const chainId = await getChainId();
log(chainId);
log(await getNamedAccounts());
if (chainId == 31337 || chainId == 1337) {
log("Local network detected! Deploying mocks...")
const linkToken = await deploy("LinkToken", { from: deployer, log: true })
await deploy("VRFCoordinatorV2Mock", {
from: deployer,
log: true,
args: [
POINT_ONE_LINK,
1e9, // 0.000000001 LINK per gas
],
})
await deploy("MockOracle", {
from: deployer,
log: true,
args: [linkToken.address],
})
log("Mocks Deployed!");
}
};
module.exports.tags = ["all", "mocks", "main"]
Over here we deploy a mock version of VRFCoordinator
contract called VRFCoordinatorV2Mock
to simulate the Oracle. We call it MockOracle.
//01_Deploy_Lottery.js
const { config,developmentChains,VERIFICATION_BLOCK_CONFIRMATIONS } = require("../config/chainlink.config");
const { network } = require("hardhat")
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 vrfCoordinatorAddress;
let subscriptionId
if (chainId == 31337 || chainId == 1337) {
linkToken = await get("LinkToken");
//log(ethers)
VRFCoordinatorV2Mock = await get("VRFCoordinatorV2Mock")
linkTokenAddress = linkToken.address;
vrfCoordinatorAddress = VRFCoordinatorV2Mock.address;
const vrfM = await ethers.getContractAt(
"VRFCoordinatorV2Mock", vrfCoordinatorAddress, await ethers.getSigner()
);
const fundAmount = config[chainId]["fundAmount"]
const transaction = await vrfM.createSubscription()
const transactionReceipt = await transaction.wait(1)
subscriptionId = ethers.BigNumber.from(transactionReceipt.events[0].topics[1])
await vrfM.fundSubscription(subscriptionId, fundAmount)
} else {
subscriptionId = process.env.VRF_SUBSCRIPTION_ID
linkTokenAddress = config[chainId]["linkToken"]
vrfCoordinatorAddress = config[chainId]["vrfCoordinator"]
}
const keyHash = config[chainId].keyHash;
const waitBlockConfirmations = developmentChains.includes(network.name)
? 1
: VERIFICATION_BLOCK_CONFIRMATIONS
const lotteryData = await deploy("LotteryData",{
from:deployer,
log: true
});
const lottery = await deploy("Lottery", {
from: deployer,
args: [
keyHash,
subscriptionId,
vrfCoordinatorAddress,
linkTokenAddress,
lotteryData.address
],
log: true,
waitConfirmations: waitBlockConfirmations,
});
const lData = await ethers.getContractAt(
"LotteryData", lotteryData.address, await ethers.getSigner()
);
await lData.updateLotteryContract(lottery.address);
log("----------------------------------------------------");
log("VRF subscriptionId " + subscriptionId);
log("Lottery Data Deployed On " + lotteryData.address + " network " + network.name);
log("Lottery Deployed On " + lottery.address + " network " + network.name);
log("----------------------------------------------------");
};
module.exports.tags = ["all", "main"];
In order to test the oracle locally with mocks we need to fake bunch of flows one is the subscription. which can be done using Mock Contracts.If you take look at VRFCoordinatorV2Mock Contract it has bunch of handy functions which we can use to mock the working of oracle vrf coordinator we will use two of those initially
createSubscription
& fundSubscription()
There you go we have our own mock vrf which we can use locally for testing.
Next, We deploy our Lottery.sol
& LotteryData.sol
Contracts.
const keyHash = config[chainId].keyHash;
const waitBlockConfirmations = developmentChains.includes(network.name)
? 1
: VERIFICATION_BLOCK_CONFIRMATIONS
const lotteryData = await deploy("LotteryData",{
from:deployer,
log: true
});
const lottery = await deploy("Lottery", {
from: deployer,
args: [
keyHash,
subscriptionId,
vrfCoordinatorAddress,
linkTokenAddress,
lotteryData.address
],
log: true,
waitConfirmations: waitBlockConfirmations,
});
const lData = await ethers.getContractAt(
"LotteryData", lotteryData.address, await ethers.getSigner()
);
await lData.updateLotteryContract(lottery.address);
First i deploy our LotteryData
contract then the Lottery
Contract with the same deployer account.Once we deploy it we call the updateLotteryContract()
function from LotteryData.sol
Contract to set the lotteryContract address as the deployed Lottery
contract address, You can consider LotteryData
contract as the storage for your Lottery
contract.Then we export our deployments as tags
.
Tags in hardhat represent what the deploy script acts on. In general it will be a single string value, the name of the contract it deploys or modifies.
Let's Deploy π
> npx hardhat deploy
Local network detected! Deploying mocks...
deploying "LinkToken" (tx: 0x596222d755670dab88fa5005d6e6f25097b05ccef8810d13ec5b6c8adb02e05d)...: deployed at 0x5FbDB2315678afecb367f032d93F642f64180aa3 with 1279067 gas
deploying "VRFCoordinatorV2Mock" (tx: 0xec41fc616bb633e16f7b7adbe9174b7155b95768d1e4499311eeccb4c3960e7e)...: deployed at 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 with 1803090 gas
deploying "MockOracle" (tx: 0x6de3453525ba02d0a91fa291924fcd8ac1d638925399254285738b29b0d643c0)...: deployed at 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 with 1131081 gas
Mocks Deployed!
deploying "LotteryData" (tx: 0xd7f72658787d802e4cf8be6ad3069fded940ba0c624b3fda7120bf981b072774)...: deployed at 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707 with 1310966 gas
deploying "Lottery" (tx: 0xa0b3e0de0ebf2e114d69e4894cb032a08f0c3877741d0f637ab7ba58c855ba9c)...: deployed at 0x0165878A594ca255338adfa4d48449f69242Eb8F with 1746568 gas
----------------------------------------------------
VRF subscriptionId 1
Lottery Data Deployed On 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707 network hardhat
Lottery Deployed On 0x0165878A594ca255338adfa4d48449f69242Eb8F network hardhat
This will spin up the hardhats own test block-chain node and deploy the contracts on that node.
Next,Let's write some unit tests π©βπ»
Create lottery_unit_test.js
inside test directory
const {expect} = require('chai');
const {ethers, getChainId, deployments} = require('hardhat');
describe("LotteryGame Unit Tests", () => {
let LotteryGame;
let lotteryGame;
let vrfCoordinatorV2Mock;
let chainId;
let deployer;
let user2;
let user3;
before(async () => {
[deployer, user2, user3] = await ethers.getSigners();
chainId = await getChainId();
await deployments.fixture(["main"]);
vrfCoordinatorV2Mock = await deployments.get("VRFCoordinatorV2Mock")
vrfCMock = await ethers.getContractAt(
"VRFCoordinatorV2Mock",
vrfCoordinatorV2Mock.address
);
LotteryGame = await deployments.get("Lottery");
lotteryGame = await ethers.getContractAt(
"Lottery",
LotteryGame.address
);
});
it("Should Pick A Pick A Random Winner", async () => {
const newLottery = await ethers.getContractAt(
"Lottery", LotteryGame.address, deployer
);
await newLottery.startLottery();
await newLottery.connect(user2).enterLottery(1, {
value: ethers.utils.parseEther("0.5"),
});
await newLottery.connect(user3).enterLottery(1, {
value: ethers.utils.parseEther("0.5"),
});
await expect(newLottery.pickWinner(1))
.to.emit(newLottery, "RandomnessRequested")
const requestId = await newLottery.s_requestId()
// simulate callback from the oracle network
await expect(
vrfCMock.fulfillRandomWords(requestId, newLottery.address)
).to.emit(newLottery, "WinnerDeclared")
});
});
Let's go through one of the tests which simulates the Oracle.
"Should Pick A Pick A Random Winner"
First we start a lottery called newLottery
then we enter with two of our test hardhat test accounts.Then we call pickWinner()
function and expect it to emit event called ("RandomnessRequested") which will return a request_id from our vrfMock contract. Then we make sure our deployed VrfMock emits the event WinnerDeclared
. which will be emitted once our VrfMock calls the fulfillRandomWords()
. which we have simulated
// simulate callback from the oracle network
await expect(vrfCMock.fulfillRandomWords(requestId, newLottery.address)
).to.emit(newLottery, "WinnerDeclared")
Let's run our tests
> npx hardhat test test/lottery_unit_test.js
LotteryGame Unit Tests
β Should Pick A Pick A Random Winner (55ms)
1 passing (2s)
The expect
which we are using is from chaijs testing lib.They have some great examples here chai matchers
Deploying to rinkeby test net
This will deploy all our contracts to Rinkeby test net.
> npx hardhat deploy --network rinkeby
Resources
As this was a brief overview explaining my experience with smart contract and chainlink oracle. There is Lot that you can improve with these contracts.
- i recommend you to go through the hardhat-deploy doc which is straight forward easy to understand
- Hardhat starter-kit
- Read Chainlink docs one of the awesome documentation.
And let everything sink in you
Bonus
i have this full code on my github here.You can also find the client implementation which i have done using reactJs.
Feel free to give a β
Posted on March 28, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.