How to build a Lottery App with Solidity, Chainlink VRF V2.5 and React.js(Vite)

Ernesto - Oct 22 - - Dev Community

In this tutorial, we will walk through how to build a decentralized lottery system using Solidity and Chainlink VRF.

The Smart Lottery contract allows users to create and enter lotteries, with a winner chosen at random after the lottery ends. To ensure fairness and transparency, we’ll leverage Chainlink's Verifiable Random Function (VRF) for random number generation, which guarantees a tamper-proof selection process.

By the end of this tutorial, you’ll have a comprehensive understanding of the contract structure and how to integrate Chainlink VRF for secure randomness in smart contracts.

Chainlink VRF Overview

Chainlink VRF is a decentralized oracle solution that provides verifiable randomness to smart contracts. In the context of the lottery, it ensures that the winner is chosen randomly, and the process is tamper-proof. Chainlink's VRF uses cryptographic proofs to guarantee the integrity of the random number used in the lottery.

Key Features

  • Multiple Lotteries: The contract allows for multiple lotteries, each with its own ID, entry fee, start time, and end time.
  • Random Winner Selection: Chainlink VRF ensures that a provably random winner is selected for each lottery.
  • Secure Fund Distribution: After the lottery ends, the winner can withdraw the pooled funds.

Prerequisite

  • You should have a basic understanding of Solidity
  • You should have a basic knowledge of React Js.
  • You should have Nodejs and Foundry installed on your PC.

Project Setup and Installation For Solidity

Run the command below to initiate a foundry project, we will be using foundry framework to build the smart contract.

 forge init lottery
Enter fullscreen mode Exit fullscreen mode

Open the lottery folder on Vscode or your favorite code editor, and delete the scripts/counter.s.sol, src/counter.sol, and test/counter.t.sol.

Install all dependencies

  forge install foundry-rs/forge-std --no-commit && forge install smartcontractkit/chainlink-brownie-contracts --no-commit 
Enter fullscreen mode Exit fullscreen mode

Code Overview

If your style learning is code first, you can find the code for the smart contracts here(Frontend can be found here). This dapp is also deployed live and you can interact with it here
The smart contract is deployed on sepolia - 0x62251FD8F91e7D4D09dB5251bD9fBC0fB92A16f5

Imports and Interfaces

VRFConsumerBaseV2Plus.sol: is the base contract that the contract inherits from. It provides the necessary functions and structure to interact with Chainlink’s VRF V2.5.

By inheriting from this, the contract can securely request and receive random numbers from Chainlink's VRF service, which ensures that the random numbers used (for picking lottery winners, in this case) are provably fair and tamper-proof.

VRFV2PlusClient.sol : contains the low-level functions and data structures required to interact with the Chainlink VRF V2.5 service. It helps handle the details of submitting randomness requests, managing gas limits, and other settings for the VRF process.

In this context, it ensures that the lottery can request random numbers with parameters like gas limits and request confirmations, and securely retrieve those numbers from Chainlink’s nodes.

  import "lib/chainlink-brownie-contracts/contracts/src/v0.8/dev/vrf/VRFConsumerBaseV2Plus.sol";
    import "lib/chainlink-brownie-contracts/contracts/src/v0.8/dev/vrf/libraries/VRFV2PlusClient.sol";
Enter fullscreen mode Exit fullscreen mode

State variables

vrfCoordinator: The address of the Chainlink VRF coordinator, which is responsible for handling the random number request.

s_subscriptionId: Chainlink VRF subscription ID, which allows the contract to fund and receive VRF services.

keyHash: A unique identifier for the VRF key associated with the subscription.

requestConfirmations, callbackGasLimit, numWords: These parameters specify how the Chainlink VRF will handle the random number request, including the number of confirmations, gas limits, and words (numbers) returned.

lotteryId: Tracks the current lottery being managed by the contract.

       address vrfCoordinator = 0x9DdfaCa8183c41ad55329BdeeD9F6A8d53168B1B;
        uint256 s_subscriptionId;
        bytes32 keyHash = 0x787d74caea10b2b357790d5b5247c2f63d1d91572a9846f780606e4d953677ae;
        uint16 requestConfirmations = 3;
        uint32 callbackGasLimit = 100000;
        uint32 numWords = 1;
        uint256 public lotteryId = 0;
Enter fullscreen mode Exit fullscreen mode

Lottery Struct

The Lottery struct models the data for each individual lottery, including:

  • LotteryId: A unique identifier for each lottery.
  • players: An array that stores the addresses of participants.
  • entryFee: The amount of ETH required to enter the lottery.
  • winner: The address of the randomly selected winner.
  • lotteryStartTime and lotteryEndTime: The timestamps for when the lottery starts and ends.
  • s_requestId: The request ID associated with the Chainlink VRF service call for random number generation.
  • creator: The address of the user who created the lottery.
 struct Lottery {
            uint256 LotteryId;
            address[] players;
            uint256 entryFee;
            address winner;
            uint256 lotteryStartTime;
            uint256 lotteryEndTime;
            uint256 s_requestId;
            address creator;
        }
Enter fullscreen mode Exit fullscreen mode

Events

Events are used to log significant occurrences within the contract. In this contract, the following events are defined:

LotteryCreated: Emitted when a new lottery is created.

LotteryEntered: Emitted when a player enters the lottery.

WinnerPicked: Emitted when a winner is selected.

RequestFulfilled: Emitted when a random number request is fulfilled by Chainlink VRF.

        event LotteryCreated(uint256 indexed lotteryId);
        event LotteryEntered(address indexed player);
        event WinnerPicked(address indexed winner);
        event RequestFulfilled(uint256 requestId, uint256[] randomWords);
Enter fullscreen mode Exit fullscreen mode

Mappings

Mappings are used for key-value storage in Solidity. This contract uses mappings for:

  • idToLottery: Maps a lottery ID to the corresponding Lottery struct, allowing the contract to manage multiple lotteries at once.

  • RequestIdToId: Maps the Chainlink VRF request ID to the specific lottery ID, ensuring that the random number returned is linked to the correct lottery.


       // Mapping
        mapping(uint256 => Lottery) public idToLottery;
        mapping(uint256 => uint256) public RequestIdToId;
Enter fullscreen mode Exit fullscreen mode

Constructor

The constructor is called once when the contract is deployed. It initializes the Chainlink VRF subscription ID and sets up the necessary configurations for interacting with Chainlink’s random number service.

     constructor(uint256 subscriptionId) VRFConsumerBaseV2Plus(vrfCoordinator) {
            s_subscriptionId = subscriptionId;
        }
Enter fullscreen mode Exit fullscreen mode

Core Functions

createLottery

This function allows users to create a new lottery. The creator specifies the entry fee, start time, and end time. This function stores the relevant details in the idToLottery mapping.

    // Create a Lottery
        function createLottery(uint256 entryFee, uint256 startTime, uint256 endTime) public payable {
            idToLottery[lotteryId].LotteryId = lotteryId;
            idToLottery[lotteryId].entryFee = entryFee;
            idToLottery[lotteryId].lotteryStartTime = startTime;
            idToLottery[lotteryId].lotteryEndTime = endTime;
            idToLottery[lotteryId].creator = msg.sender;
            emit LotteryCreated(lotteryId);
            lotteryId++;
        }


Enter fullscreen mode Exit fullscreen mode

enterLottery

This function allows users to participate in a lottery by paying the required entry fee. This function checks if the lottery is ongoing and if the correct amount of ETH is provided before adding the player to the list of participants.

    // Enter the lottery by paying the entry fee
        function enterLottery(uint256 id) public payable {
            require(id < lotteryId, "Lottery does not exists");
            require(idToLottery[id].lotteryStartTime < block.timestamp, "Lottery still ongoing");
            require(idToLottery[id].lotteryEndTime > block.timestamp, "Lottery has ended");
            require(msg.value >= idToLottery[id].entryFee, "Not enough ETH to enter");
            idToLottery[id].players.push(msg.sender);
            emit LotteryEntered(msg.sender);
        }
Enter fullscreen mode Exit fullscreen mode

DrawLotteryWinner

This function is used to initiate the process of selecting a winner once the lottery has ended. This function calls Chainlink VRF’s requestRandomWords to get a random number.

  function DrawLotteryWinner(uint256 id) external returns (uint256 requestId) {
            require(msg.sender == idToLottery[id].creator, "Only Lottery creator can draw lottery");
            require(idToLottery[id].lotteryEndTime < block.timestamp, "Lottery still ongoing");
            requestId = s_vrfCoordinator.requestRandomWords(
                VRFV2PlusClient.RandomWordsRequest({
                    keyHash: keyHash,
                    subId: s_subscriptionId,
                    requestConfirmations: requestConfirmations,
                    callbackGasLimit: callbackGasLimit,
                    numWords: numWords,
                    extraArgs: VRFV2PlusClient._argsToBytes(VRFV2PlusClient.ExtraArgsV1({nativePayment: false}))
                })
            );
            RequestIdToId[requestId] = id;
            return requestId;
        }

Enter fullscreen mode Exit fullscreen mode

fulfillRandomWords
This internal function is triggered automatically when Chainlink VRF returns the random number. It calculates the winner by taking the modulo of the random number and the number of participants, to ensure fairness.

  function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal override {
            uint256 id = RequestIdToId[_requestId];
            uint256 winnerIndex = _randomWords[0] % idToLottery[id].players.length;
            idToLottery[id].winner = idToLottery[id].players[winnerIndex];
            emit RequestFulfilled(_requestId, _randomWords);
        }
Enter fullscreen mode Exit fullscreen mode

withdrawWinnings

This function allows the winner to withdraw the prize, which is the total of all the entry fees pooled together.

   function withdrawWinnings(uint256 id) public {
            require(msg.sender == idToLottery[id].winner, "You are not the winner");
            address payable winner = payable(idToLottery[id].winner);
            uint256 amount = idToLottery[id].players.length * idToLottery[id].entryFee;
            (bool sent, bytes memory data) = winner.call{value: amount}("");
            require(sent, "Failed to send Ether");
        }
Enter fullscreen mode Exit fullscreen mode

Project Setup and Installation For React.js

Run the command below and follow the prompt to initiate a React project, we will be using react.js to build the frontend for the Lottery App.

 npm create vite@latest
Enter fullscreen mode Exit fullscreen mode

Install the necessary dependencies.

  npm install npm install ethers react-toastify 
Enter fullscreen mode Exit fullscreen mode

App.js Overview

Imports

App.css - contains styling for the components.
lotteryABI - The JSON files contain the ABI (Application Binary Interface) definitions of the smart contracts, which is necessary for interacting with the contracts via ethers.js.
ethers.js - A JavaScript library that allows interactions with the Ethereum blockchain. It is used to create contract instances, send transactions, and interact with smart contracts.

    import './App.css'
    import { ethers } from "ethers";
    import lotteryABI from "../abi/lottery.json";
Enter fullscreen mode Exit fullscreen mode

State, UseEffect and Refs

They are used to manage state and refer to user input. The lottery data (list, gId, lot, winner, etc.) is stored in state, while useRef references HTML input elements like the start and end time for lotteries.

The useEffect is used to triggers the retrieval of past lottery data (getLastLottery) and fetches real-time information whenever the user changes the selected lottery ID.

     const startRef = useRef();
      const endRef = useRef();
      const entryRef = useRef();
      const selectRef = useRef();
      const selectRef2 = useRef();
      const selectRef3 = useRef();

      const [list, setList] = useState([]);
      const [gId, setGID] = useState(0);
      const [gId2, setGID2] = useState(0);
      const [lot, setLot] = useState({});
      const [winner, setWinner] = useState("0x");

     useEffect(() => {
        getlastLottery();
        getLottery();
        getWinner();
      }, [gId, gId2]);
Enter fullscreen mode Exit fullscreen mode

Smart Contract Interaction

createWriteContract

This function sets up a connection to the contract via a Web3 provider using the current user's wallet (Metamask), allowing them to interact with the contract in a writable (state-changing) manner.

  const createWriteContract = async () => {
        const { ethereum } = window;
        const provider = new ethers.providers.Web3Provider(ethereum);
        const signer = await provider.getSigner();
        const priceContract = new ethers.Contract(lotteryAddress, lotteryABI.abi, signer);
        return priceContract;
      }
Enter fullscreen mode Exit fullscreen mode

createGetContract

This function creates a read-only connection to the contract, enabling the frontend to fetch data from the blockchain without altering its state.

   const createGetContract = async () => {
        const { ethereum } = window;
        const provider = new ethers.providers.Web3Provider(ethereum)
        const priceContract = new ethers.Contract(lotteryAddress, lotteryABI.abi, provider);
        return priceContract;
      }

Enter fullscreen mode Exit fullscreen mode

createLottery

create lottery

The createLottery function allows users to create a new lottery by providing three inputs: the entry fee, start date, and end date. These values are captured from the input fields, converted to appropriate formats (e.g., Ether to Wei for the entry fee and UNIX timestamps for dates), and then sent to the smart contract. The blockchain transaction is processed asynchronously, with real-time feedback provided to the user using react-toastify notifications.

const createLottery = async (evt) => {
        evt.preventDefault();
        const contract = await createWriteContract();
        const id = toast.loading("Creating Lottery....");
        try {
          const start = Math.floor(new Date(startRef.current.value).getTime() / 1000);
          const end = Math.floor(new Date(endRef.current.value).getTime() / 1000);
          const entry = ethers.utils.parseEther(entryRef.current.value);
          const tx = await contract.createLottery(entry, start, end);
          await tx.wait();
          setTimeout(() => {
            window.location.href = "/";
          }, 10000);
          toast.update(id, {
            render: "Transaction successfull",
            type: "success",
            isLoading: false,
            autoClose: 10000,
            closeButton: true,
          });
        } catch (error) {
          console.log(error);
          toast.update(id, {
            render: `${error.reason}`,
            type: "error",
            isLoading: false,
            autoClose: 10000,
            closeButton: true,
          });
        }
      };
Enter fullscreen mode Exit fullscreen mode

getlastLottery

This function fetches the total number of lotteries created by querying the smart contract’s lotteryId() function and updates the state with the list of lotteries available for selection. Each lottery is displayed as an option in a dropdown menu.

const getlastLottery = async () => {
        const contract = await createGetContract();
        const lotteryCount = await contract.lotteryId();
        let arr = [];
        for (let i = 0; i < Number(lotteryCount); i++) {
          arr.push({ lotteryId: i, lotteryName: "Lottery " + i })
        }
        setList(arr);
      };
Enter fullscreen mode Exit fullscreen mode

getLottery

get lottery

This function fetches details (start time, end time, entry fee) of the currently selected lottery from the blockchain using its ID (gId).

 const getLottery = async () => {
        const contract = await createGetContract();
        const lottery = await contract.idToLottery(gId);
        let obj = {
          startTime: Number(lottery.lotteryStartTime),
          endTime: Number(lottery.lotteryEndTime),
          entryFee: Number(lottery.entryFee),
        }
        setLot(obj);
      };
Enter fullscreen mode Exit fullscreen mode

getWinner

get winner

The lottery winner is determined through the smart contract, which uses Chainlink's Verifiable Random Function (VRF) for randomness. Once a lottery is concluded, users can check the winner by selecting a lottery ID (gId2), and the winner’s Ethereum address is fetched from the blockchain using the contract’s idToLottery() function.

 const getWinner = async () => {
        const contract = await createGetContract();
        const lottery = await contract.idToLottery(gId2);
        setWinner(lottery.winner);
      };
Enter fullscreen mode Exit fullscreen mode

withdrawWinning

withdraw winning

Winners can withdraw their earnings using the withdrawWinning function, which interacts with the smart contract’s withdrawWinnings() function. The user's wallet signs the transaction, and upon success, the winnings are transferred to the winner’s account.

 const withdrawWinning = async (evt) => {
        evt.preventDefault();
        const contract = await createWriteContract();
        const id = toast.loading("Withdrawing....");
        try {
          const tx = await contract.withdrawWinnings(selectRef3.current.value);
          await tx.wait();
          setTimeout(() => {
            window.location.href = "/";
          }, 10000);
          toast.update(id, {
            render: "Transaction successfull",
            type: "success",
            isLoading: false,
            autoClose: 10000,
            closeButton: true,
          });
        } catch (error) {
          console.log(error);
          toast.update(id, {
            render: `${error.reason}`,
            type: "error",
            isLoading: false,
            autoClose: 10000,
            closeButton: true,
          });
        }
      };
Enter fullscreen mode Exit fullscreen mode

Frontend UI

The UI is divided into four main sections:

  • Create Lottery: Users can input the entry fee, start date, and end date for a new lottery, which is then created on the blockchain.

  • Enter Lottery: Displays the available lotteries, including details like entry fees, start and end dates, fetched from the smart contract and users can participate in a lottery.

  • Check Winner: After selecting a lottery, users can see the address of the winner for concluded lotteries.

  • Withdraw Winnings: If the user is the winner, they can withdraw their winnings by selecting the appropriate lottery and initiating the withdrawal process.

final ui

Conclusion

This tutorial demonstrates how to build a lottery app with solidity, chainlink VRF 2.5 and react.js. This guide covers the essential components of creating a secure and transparent lottery system by integrating smart contracts, randomness via Chainlink VRF (Verifiable Random Function), and a responsive user interface with React.js.

Solidity enables you to write the smart contract logic that governs the lottery, ensuring fairness and immutability. Chainlink VRF guarantees a tamper-proof randomness source for selecting winners, ensuring that no entity, including developers, can influence the outcome.

React.js offers an intuitive interface for users to interact with the smart contract, submit transactions, and retrieve data in real time. Using ethers.js to bridge the blockchain with the frontend, users can seamlessly participate in the lottery, check results, and withdraw winnings.

. .