DEV Community

Cover image for Eth-Lottery - Solidity, Hardhat
Hussain'z
Hussain'z

Posted on

Eth-Lottery - Solidity, Hardhat

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)));
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Next, Let's Install Hardhat

npm install --save-dev hardhat
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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,
    },
  }
Enter fullscreen mode Exit fullscreen mode

Next, Lets install the required libraries.

npm install @chainlink/contracts @openzeppelin/contracts
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode
//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];
    }
}
Enter fullscreen mode Exit fullscreen mode

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();

Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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);
    }
Enter fullscreen mode Exit fullscreen mode

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);
      }
}
Enter fullscreen mode Exit fullscreen mode

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.

vrf sub screenshot

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]);
    }

Enter fullscreen mode Exit fullscreen mode

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,
};
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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"];

Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
    });
  });
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

And let everything sink in you

just a gag

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 โญ

https://eth-lottery-3glp1l5k0-hussainzz.vercel.app/

eth-client-demo

Buy Me A Coffee

Top comments (2)

Collapse
 
naikaqueen33 profile image
anikaqueen

When it comes to picking a random winner in a lottery smart contract, it's important to ensure that the selection process is truly random and unbiased. One way to do this is by using a random number generator that is cryptographically secure, which means that the numbers generated cannot be predicted or manipulated.
Greece Powerball Result

Collapse
 
pauljhonson614 profile image
pauljhonson614

Awesome work thanks for sharing this Lotus365 info with us.