Build a Lottery Smart Contract with Chainlink VRF Random Numbers: A Step-by-Step Guide
Tags: tutorial, solidity, web3, blockchain
Published: 2023-05-14 22:42:27

## Overview

In this article, we will be exploring how to create a lottery smart contract using [Chainlink VRF](https://docs.chain.link/vrf/v2/introduction) (Verifiable Random Function) random numbers. Chainlink VRF is a secure and reliable way to generate random numbers on the blockchain, making it ideal for use in lotteries and other games of chance.

By using Chainlink VRF, we can ensure that our lottery is fair and tamper-proof, giving players the confidence to participate and the assurance that the results are unbiased. We will walk through the steps of building a lottery smart contract using Chainlink VRF, and by the end of the tutorial, you will have a fully functional lottery smart contract that you can use as a template for your own projects.

You can find the entire code for the contract in the [GitHub Repository](https://github.com/Envoy-VC/Smart-Contracts/blob/main/Intermediate/Simple%20Lottery/Lottery.sol)

{% embed https://github.com/Envoy-VC/Smart-Contracts noreadme %}

---

## Prerequisites

Before you begin with this tutorial make sure you are comfortable with the following:

-   Basic knowledge of Solidity and the Ethereum blockchain
    
-   Experience with compiling and deploying contracts using Remix IDE
    
-   Passion for building cool things
    

Before starting to build the actual Lottery Contract let's take a look at Chainlink VRF and set up the needed stuff to use it in our Contract

---

## Create a Chainlink VRF Subscription

Chainlink VRF is a powerful tool for generating random numbers on the blockchain, and it offers two methods for requesting randomness: **subscription** and **direct funding**. In this article, we'll focus on the subscription method, which allows you to fund requests for multiple consumer contracts from a single subscription account.

To create a Chainlink VRF subscription, you'll need to follow a few simple steps.

### Step 1 - Creating a Subscription

To create a subscription for Chainlink VRF, follow these steps:

1.  Navigate to the subscription manager page at [https://vrf.chain.link](https://vrf.chain.link) and connect your Metamask wallet. Ensure that your wallet is set to the Sepolia testnet.
    
    ![Create Subscription](https://cdn.hashnode.com/res/hashnode/image/upload/v1684103009066/17fbadf2-b2bc-4692-b0bf-f72b6afe7b2e.png)
2.  Click on the "`Create Subscription`" button and fill in the required details. Once you have entered the necessary information, click on "Create Subscription" again.
    
    ![Fill in Subscription Details](https://cdn.hashnode.com/res/hashnode/image/upload/v1684103010894/120d119d-aff7-47f7-b7b4-2274996f3d31.png)
3.  You will be prompted to approve the transaction on Metamask. Approve the transaction and wait until it is confirmed on the chain.
    

### Step 2 - Fund your Subscription

To fund your subscription and obtain random numbers, you will need to pay LINK tokens. To obtain testnet LINK tokens, visit [https://faucets.chain.link/sepolia](https://faucets.chain.link/sepolia).

After obtaining LINK tokens, return to your subscription dashboard and click on the "`Fund Subscription`" button located under the "`Actions`" dropdown in the top right corner of the page. Enter the number of LINK tokens you wish to fund in the input box and click on "`Confirm`". You will then be prompted with a Metamask popup. Click on "`Approve`" to complete the transaction.

![Fund You Subscription with LINK Tokens](https://cdn.hashnode.com/res/hashnode/image/upload/v1684103030482/b91221e5-d4cb-44f1-91bc-8c9711e76da7.png)

That's It. We have successfully created a subscription to get Random Numbers, now we can move to the smart contract.

---

## Create Lottery Contract

### Initializing Contract

To begin creating the lottery smart contract, we need to specify the license of the contract, and the pragma version, and import the necessary modules.

We will be using the _Strings_ module from `@openzeppelin/contracts` to provide custom error messages, and the _VRFConsumerBaseV2_ and _VRFCoordinatorV2Interface_ modules from `@chainlink/contracts` to implement random number functionality.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.14;

import "@openzeppelin/contracts/utils/Strings.sol";
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
```

Next, we will create a contract called Lottery that will inherit from VRFConsumerBaseV2.

```solidity
contract Lottery is VRFConsumerBaseV2 {
	// Code...
}
```

### State Variables

To pass on the configuration to the VRFConsumerBaseV2 contract, we need to declare some state variables.

```solidity
VRFCoordinatorV2Interface COORDINATOR;
uint64 subscriptionId;
address vrfCoordinator = 0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625;
bytes32 keyHash =
	0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c;
uint32 callbackGasLimit = 100000;
uint16 requestConfirmations = 3;
uint32 numWords = 1;

uint256 public lotteryCount = 0;
```

Here is what each variable does:

1.  `COORDINATOR` - This is the contract address that we will use to request randomness.
    
2.  `subscriptionId` - This is the subscription ID that this contract uses for funding requests.
    
3.  `vrfCoordinator` - This is the address of the coordinator on the Sepolia testnet.
    
4.  `keyHash` - This is the gas lane key hash value, which functions as an ID of the off-chain VRF job that runs in response to requests.
    
5.  `callbackGasLimit` - This is the limit for how much gas to use for the callback request to your contract's function.
    
6.  `requestConfirmations` - This is how many confirmations the Chainlink node should wait before responding. The longer the node waits, the more secure the random value is.
    
7.  `numWords` - This is how many random values to request. If you can use several random values in a single callback, you can reduce the amount of gas that you spend per random value.
    
8.  `lotteryCount` - This is the variable that keeps track of the total lotteries.
    

To keep track of the data for the lottery, we need to create some structures and mappings.

```solidity
struct LotteryData {
        address lotteryOperator;
        uint256 ticketPrice;
        uint256 maxTickets;
        uint256 operatorCommissionPercentage;
        uint256 expiration;
        address lotteryWinner;
        address[] tickets;
}

struct LotteryStatus {
	uint256 lotteryId;
	bool fulfilled;
	bool exists;
	uint256[] randomNumber;
}

mapping(uint256 => LotteryData) public lottery;
mapping(uint256 => LotteryStatus) public requests;
```

Here is what each structure and mapping does:

1.  `LotteryData` struct - This structure keeps a record of the `lotteryOperator`, which is the address that can start and end a lottery, `ticketPrice`, which is the price of each ticket, `maxTickets`, which is the maximum number of tickets, `operatorCommissionPercentage`, which is a percentage of the total pool that is sent to the operator, `expiration`, which is the time when the lottery ends, `lotteryWinner`, which is the address of the winner, and `tickets`, which stores all the tickets for the lottery.
    
2.  `LotteryStatus` struct - This structure keeps track of the request sent to the VRFCoordinator after the lottery is ended. It stores some states like the request fulfilled status and the `randomNumber`.
    
3.  `lottery` mapping - This mapping maps `lotteryCount` to the `LotteryData` struct.
    
4.  `requests` mapping - This mapping maps to `LotteryStatus`.
    

### Constructor

The constructor function is used to initialize the Lottery contract. It takes in a `subscriptionId` as a parameter, which is used to fund requests for random numbers.

Inside the constructor function, the COORDINATOR variable is set to an instance of the VRFCoordinatorV2Interface contract using the `vrfCoordinator` address. The `subscriptionId` variable is set to the `_subscriptionId` parameter passed to the function.

```solidity
constructor(uint64 _subscriptionId) VRFConsumerBaseV2(vrfCoordinator) {
    COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
    subscriptionId = _subscriptionId;
}
```

### Events

The Lottery contract has several events that are emitted during different stages of the lottery process.

```solidity
event LotteryCreated(
	address lotteryOperator,
	uint256 ticketPrice,
	uint256 maxTickets,
	uint256 operatorCommissionPercentage,
	uint256 expiration
);

event LogTicketCommission(
	uint256 lotteryId,
	address lotteryOperator,
	uint256 amount
);

event TicketsBought(
	address buyer,
	uint256 lotteryId,
	uint256 ticketsBought
);

event LotteryWinnerRequestSent(
	uint256 lotteryId,
	uint256 requestId,
	uint32 numWords
);

event RequestFulfilled(uint256 requestId, uint256[] randomWords);

event LotteryWinnerDrawn(uint256 lotteryId, address lotteryWinner);

event LotteryClaimed(
	uint256 lotteryId,
	address lotteryWinner,
	uint256 amount
);
```

Here is what each event does:

1.  **LotteryCreated** - This event is emitted when a new lottery is created. It includes information about the `lotteryOperator`, `ticketPrice`, `maxTickets`, `operatorCommissionPercentage`, and `expiration`.
    
2.  **LogTicketCommission** - This event is emitted when lottery commission is sent to the operator. It includes information about the `lotteryId`, `lotteryOperator`, and `amount`.
    
3.  **TicketsBought** - This event is emitted when a user buys tickets. It includes information about the `buyer`, `lotteryId`, and `ticketsBought`.
    
4.  **LotteryWinnerRequestSent** - This event is emitted when a random number request is sent to Chainlink VRF. It includes information about the `lotteryId`, `requestId`, and `numWords`.
    
5.  **RequestFulfilled** - This event is emitted when a random number request is fulfilled by Chainlink VRF. It includes information about the `requestId` and `randomWords`.
    
6.  **LotteryWinnerDrawn** - This event is emitted when a lottery winner is drawn. It includes information about the `lotteryId` and `lotteryWinner`.
    
7.  **LotteryClaimed** - This event is emitted when a lottery winner claims their prize. It includes information about the `lotteryId`, `lotteryWinner`, and `amount`.
    

### Modifiers

The Lottery contract has two modifiers that are used to restrict access to certain functions.

```solidity
modifier onlyOperator(uint256 _lotteryId) {
	require(
		(msg.sender == lottery[_lotteryId].lotteryOperator),
		"Error: Caller is not the lottery operator"
	);
	_;
}

modifier canClaimLottery(uint256 _lotteryId) {
	require(
		(lottery[_lotteryId].lotteryWinner != address(0x0)),
		"Error: Lottery Winner not yet drawn"
	);
	require(
		(msg.sender == lottery[_lotteryId].lotteryWinner ||
			msg.sender == lottery[_lotteryId].lotteryOperator),
		"Error: Caller is not the lottery winner"
	);
	_;
}
```

Here is what each modifier does:

1.  **onlyOperator** - This modifier restricts access to functions that can only be called by the `lotteryOperator`. It takes in a `_lotteryId` parameter to ensure that the caller is the operator of the specified lottery. If the caller is not the operator, an error message is returned.
    
2.  **canClaimLottery** - This modifier restricts access to functions that can only be called by the `lotteryWinner` or `lotteryOperator` after the winner has been drawn. It takes in a `_lotteryId` parameter to ensure that the caller is either the winner or the operator of the specified lottery. If the winner has not been drawn yet or the caller is not the winner or operator, an error message is returned.
    

### Read Functions

The Lottery contract has a read function called `getRemainingTickets` that returns the number of remaining tickets for a specified lottery.

```solidity
function getRemainingTickets(uint256 _lotteryId) public view returns (uint256) {
    return lottery[_lotteryId].maxTickets - lottery[_lotteryId].tickets.length;
}
```

### Write Functions

The Lottery contract has a function called `createLottery` that is used to create a new lottery.

```solidity
function createLottery(
	address _lotteryOperator,
	uint256 _ticketPrice,
	uint256 _maxTickets,
	uint256 _operatorCommissionPercentage,
	uint256 _expiration
) public {
	require(
		_lotteryOperator != address(0),
		"Error: Lottery operator cannot be 0x0"
	);
	require(
		(_operatorCommissionPercentage >= 0 &&
			_operatorCommissionPercentage % 5 == 0),
		"Error: Commission percentage should be greater than zero and multiple of 5"
	);
	require(
		_expiration > block.timestamp,
		"Error: Expiration must be greater than current block timestamp"
	);
	require(_maxTickets > 0, "Error: Max tickets must be greater than 0");
	require(_ticketPrice > 0, "Error: Ticket price must be greater than 0");
	address[] memory ticketsArray;
	lotteryCount++;
	lottery[lotteryCount] = LotteryData({
		lotteryOperator: _lotteryOperator,
		ticketPrice: _ticketPrice,
		maxTickets: _maxTickets,
		operatorCommissionPercentage: _operatorCommissionPercentage,
		expiration: _expiration,
		lotteryWinner: address(0),
		tickets: ticketsArray
	});
	emit LotteryCreated(
		_lotteryOperator,
		_ticketPrice,
		_maxTickets,
		_operatorCommissionPercentage,
		_expiration
	);
}
```

Here is what the function does:

This function takes in several parameters including the `lotteryOperator`, `ticketPrice`, `maxTickets`, `operatorCommissionPercentage`, and `expiration`. It first checks that the `lotteryOperator` is not the null address, the `operatorCommissionPercentage` is greater than or equal to zero and a multiple of 5, and the expiration is greater than the current block timestamp. It also checks that the `maxTickets` and `ticketPrice` are greater than zero.

If all of these conditions are met, a new `LotteryData` struct is created with the specified parameters and an empty tickets array. The `lotteryCount` is incremented and the new LotteryData struct is added to the `lottery` mapping with the `lotteryCount` as the key. Finally, the `LotteryCreated` event is emitted with information about the new lottery.

Next we have the `BuyTickets` Function that is used to buy tickets for a specified lottery.

```solidity
function BuyTickets(uint256 _lotteryId, uint256 _tickets) public payable {
	uint256 amount = msg.value;
	require(
		_tickets > 0,
		"Error: Number of tickets must be greater than 0"
	);
	require(
		_tickets <= getRemainingTickets(_lotteryId),
		"Error: Number of tickets must be less than or equal to remaining tickets"
	);
	require(
		amount >= _tickets * lottery[_lotteryId].ticketPrice,
		"Error: Ether value must be equal to number of tickets times ticket price"
	);
	require(
		block.timestamp < lottery[_lotteryId].expiration,
		"Error: Lottery has expired"
	);

	LotteryData storage currentLottery = lottery[_lotteryId];

	for (uint i = 0; i < _tickets; i++) {
		currentLottery.tickets.push(msg.sender);
	}

	emit TicketsBought(msg.sender, _lotteryId, _tickets);
}
```

Here is what the function does:

This function takes in two parameters including the `_lotteryId` and `_tickets` to specify which lottery to buy tickets for and how many tickets to buy. It first checks that the `_tickets` parameter is greater than zero, that the number of tickets being purchased is less than or equal to the remaining tickets for the specified lottery, and that the amount of ether sent is equal to the number of tickets times the ticket price. It also checks that the specified lottery has not expired.

If all of these conditions are met, the function adds the buyer's address to the tickets array in the LotteryData struct for the specified lottery `_tickets` the number of times. Finally, the `TicketsBought` event is emitted with information about the buyer, the lottery, and the number of tickets purchased.

Next, we have the `DrawLotteryWinner` function is used to draw a winner for a specified lottery.

```solidity
function DrawLotteryWinner(
	uint256 _lotteryId
) external onlyOperator(_lotteryId) returns (uint256 requestId) {
	require(
		block.timestamp > lottery[_lotteryId].expiration,
		"Error: Lottery has not yet expired"
	);
	require(
		lottery[_lotteryId].lotteryWinner == address(0),
		"Error: Lottery winner already drawn"
	);
	requestId = COORDINATOR.requestRandomWords(
		keyHash,
		subscriptionId,
		requestConfirmations,
		callbackGasLimit,
		numWords
	);
	requests[requestId] = LotteryStatus({
		lotteryId: _lotteryId,
		randomNumber: new uint256[](0),
		exists: true,
		fulfilled: false
	});
	emit LotteryWinnerRequestSent(_lotteryId, requestId, numWords);
	return requestId;
}
```

Here is what the function does:

This function takes in one parameter, `_lotteryId`, to specify which lottery to draw a winner for. This function uses `onlyOperator` modifier which ensures that only the Lottery Operator can Draw the Lottery Results. Then It checks that the specified lottery has expired and that a winner has not already been drawn.

If these conditions are met, the function calls the `requestRandomWords` function from the VRFConsumerBase contract to generate a random number. The function creates a new `LotteryStatus` struct with the specified lottery ID, an empty `randomNumber` array, and a flag indicating that the request exists but has not yet been fulfilled. The function then emits a `LotteryWinnerRequestSent` event with information about the lottery, the request ID, and the number of words requested. Finally, the function returns the request ID.

The last write function is called `ClaimLottery` that is used to claim the prize for a specified lottery.

```solidity
function ClaimLottery(
	uint256 _lotteryId
) public canClaimLottery(_lotteryId) {
	LotteryData storage currentLottery = lottery[_lotteryId];
	uint256 vaultAmount = currentLottery.tickets.length *
		currentLottery.ticketPrice;

	uint256 operatorCommission = vaultAmount /
		(100 / currentLottery.operatorCommissionPercentage);

	(bool sentCommission, ) = payable(currentLottery.lotteryOperator).call{
		value: operatorCommission
	}("");
	require(sentCommission);
	emit LogTicketCommission(
		_lotteryId,
		currentLottery.lotteryOperator,
		operatorCommission
	);

	uint256 winnerAmount = vaultAmount - operatorCommission;

	(bool sentWinner, ) = payable(currentLottery.lotteryWinner).call{
		value: winnerAmount
	}("");
	require(sentWinner);
	emit LotteryClaimed(
		_lotteryId,
		currentLottery.lotteryWinner,
		winnerAmount
	);
}
```

Here is what the function does:

This function takes in one parameter, `_lotteryId`, to specify which lottery to claim the prize for. It has the `canClaimLottery` modifier which restricts the Lottery Winner and the Operator to only call the function. Then It checks that the specified lottery has a winner and that the caller is the winner.

If these conditions are met, the function calculates the total amount of ether in the lottery vault by multiplying the number of tickets sold by the ticket price. It then calculates the operator commission by dividing the vault amount by the operator commission percentage and sends the commission to the lottery operator. The function then calculates the winner amount by subtracting the operator commission from the vault amount and sends the winner amount to the lottery winner. Finally, the function emits a `LogTicketCommission` event with information about the operator commission and a `LotteryClaimed` event with information about the lottery and the winner.

### Internal Functions

The Lottery contract has an internal function called `fulfillRandomWords` which overrides the Chainlink implementation that is used to handle the fulfillment of a randomness request sent in the `DrawLottery` Funtion.

```solidity
function fulfillRandomWords(
	uint256 _requestId,
	uint256[] memory _randomWords
) internal override {
	require(requests[_requestId].exists, "Error: Request not found");
	uint256 lotteryId = requests[_requestId].lotteryId;
	requests[_requestId].fulfilled = true;
	requests[_requestId].randomNumber = _randomWords;
	uint256 winnerIndex = _randomWords[0] %
		lottery[lotteryId].tickets.length;
	lottery[lotteryId].lotteryWinner = lottery[lotteryId].tickets[
		winnerIndex
	];
	emit RequestFulfilled(_requestId, _randomWords);
}
```

Here is what the function does:

This function takes in two parameters, `_requestId` and `_randomWords`, to specify the ID of the request being fulfilled and the random words generated by the VRF coordinator. It first checks that the request exists in the `requests` mapping.

If the request exists, the function retrieves the lottery ID associated with the request and sets the fulfilled flag to true for the request. The function then stores the random words in the `randomNumber` array for the request and calculates the index of the winning ticket by taking the modulus of the first random number with the number of tickets sold for the lottery.

The function then sets the `lotteryWinner` address for the specified lottery to the address of the ticket holder at the calculated index. Finally, the function emits a `RequestFulfilled` event with information about the request and the random words.

---

## Deploying Contract

To deploy the Lottery contract through Remix, you will need to follow these steps:

1.  Open [Remix](https://remix.ethereum.org/) and create a new Solidity file.
    
2.  Copy and paste the Lottery contract code into the file.
    
3.  Compile the contract by selecting the appropriate compiler version and clicking on the "`Compile`" button.
    
4.  Once the contract is compiled, click on the "Run" tab and select the "Deploy & Run Transactions" option.Then choose `Injected Provider - Metamask` under the Environments Dropdown and make sure you are on Sepolia Testnet.
    
5.  In the "Deploy" section, select the appropriate account to deploy the contract from and select the Lottery contract from the dropdown menu.
    
6.  Enter the constructor arguments for the Lottery contract. One of the constructor arguments is the subscription ID that was generated when creating a VRF subscription. You will need to copy this subscription ID and paste it into the constructor arguments field.
    
7.  Click on the "Transact" button to deploy the contract.
    
8.  Once the contract is deployed, we need to whitelist the contract in our Subscription so it can request random numbers.
    

---

## Add Consumer to Subscription Manager

To whitelist our contract in the subscription so that it can request random numbers, go to your subscription dashboard and click on "`Add Consumer`" button. then enter the deployed Lottery contract address in the input field and click "`Add Consumer`". You will be prompted with a Metamask popup to approve the transaction. That's all, we've added our Lottery Contract to the total and the user can now request random numbers from the VRF.

![Add Consumer to Subscription Manager](https://cdn.hashnode.com/res/hashnode/image/upload/v1684140050459/64b37b0e-08f0-4375-a892-57cc53c63d3b.png)

---

## Conclusion

🎉 Congratulations on successfully deploying the Lottery contract!

In conclusion, the Lottery contract is a Solidity smart contract that allows for the creation and management of lotteries on the Ethereum blockchain. The contract uses Chainlink's VRF to generate random numbers for selecting lottery winners in a secure and decentralized manner. The contract also includes features such as ticket purchasing, operator commission, and prize claiming.

To make the Lottery contract even better, one possible improvement would be to add more advanced features such as multi-round lotteries, variable ticket prices, and automatic prize distribution. Additionally, the contract could be audited by a third-party security firm to ensure that it is free from vulnerabilities and exploits.

Overall, the Lottery contract is a great example of how smart contracts can be used to create decentralized and transparent lotteries on the blockchain.

👍 If you found this technical writing helpful and would like to see more content like this, please consider following the blog!

🔍 You can also connect with me on [GitHub](https://github.com/Envoy-VC) and [Twitter](https://twitter.com/Envoy_1084) for more updates and technical discussions. Thank you for reading!

---