Skip to content

Create a Lottery Contract using the Randomness Precompile

Randomness Moonbeam Banner

March 15, 2023 | by Erin Shaben


Moonbeam utilizes verifiable random functions (VRF) to generate randomness that can be verified on-chain. A VRF is a cryptographic function that takes some input and produces random values, along with a proof of authenticity that these random values were generated by the submitter. The proof can be verified by anyone to ensure the random values generated were calculated correctly.

There are two available sources of randomness that provide random inputs based on block producers' VRF keys and past randomness results: local VRF and BABE epoch randomness. Local VRF is determined directly within Moonbeam using the collator of the block's VRF key and the last block's VRF output. On the other hand, BABE epoch randomness is based on all the VRF produced by the relay chain validators during a complete epoch.

For more information on the two sources of randomness, how the request and fulfillment process works, and security considerations, please refer to the Randomness on Moonbeam page.

Moonbeam provides a Randomness Precompile, which is a Solidity interface that enables smart contract developers to generate randomness via local VRF or BABE epoch randomness using the Ethereum API. Moonbeam also provides a Randomness Consumer Solidity contract that your contract must inherit from in order to consume fulfilled randomness requests.

This guide will show you how to use the Randomness Precompile and Randomness Consumer contract to create a lottery where the winners will randomly be selected.

Checking Prerequisites

For this tutorial, you'll need the following:

  • Create or have three accounts on Moonbase Alpha to test out the lottery contract
  • All of the accounts will need to be funded with DEV tokens. You can get DEV tokens for testing on Moonbase Alpha once every 24 hours from the Moonbase Alpha Faucet
  • An empty Hardhat project that is configured for the Moonbase Alpha TestNet. For step-by-step instructions, please refer to the Creating a Hardhat Project and the Hardhat Configuration File sections of our Hardhat documentation page
  • Install the Hardhat Ethers plugin. This provides a convenient way to use the Ethers.js library to interact with the network from your Hardhat project:

    npm install @nomiclabs/hardhat-ethers


To test out the examples in this guide on Moonbeam or Moonriver, you will need to have your own endpoint and API key, which you can get from one of the supported Endpoint Providers.

Contract Setup

The following are the contracts that we'll be working with today to create our lottery:

  • Randomness.sol - the Randomness Precompile, which is a Solidity interface that allows you to request randomness, get information about randomness requests, fulfill requests, and more
  • RandomnessConsumer.sol - the Randomness Consumer, which is an abstract Solidity contract that is used to interact with the Randomness Precompile. This contract is responsible for validating the origin of randomness requests, ensuring the Randomness Precompile is always the origin, and fulfilling requests
  • Lottery.sol - an example lottery contract that we'll be building in this guide together. It will rely on the Randomness Precompile and Consumer to request random words that will be used to select a winner for our lottery

If you don't already have a contracts directory in your Hardhat project, you can create a new directory:

mkdir contracts && cd contracts

Then you can create the following three files, one for each of the aforementioned contracts:

touch Randomness.sol RandomnessConsumer.sol Lottery.sol

In the Randomness.sol file, you can paste in the Randomness Precompile contract. Similarly, in the RandomnessConsumer.sol file, you can paste in the Randomness Consumer contract.

We'll start adding the functionality to the Lottery.sol contract in the following section.

Create the Lottery Smart Contract

At a high level, the lottery contract we're creating will define the rules of the lottery, enable participation, and use randomly generated words to select winners fairly. We'll be requesting the random words via the Randomness Precompile. Then we'll use the Randomness Consumer interface to consume the results of the fulfilled request so that our contract can use the randomly generated words to select the winners and pay them out. We'll break down each step of the process as we build the lottery contract, but for now, you can review the following diagram for an overview of the process.

Diagram of the Lottery process.

This contract is for educational purposes only and is not meant for production use.

To get started, let's set up our lottery contract. We'll need to:

  • Import the Randomness.sol precompile and RandomnessConsumer.sol interface
  • Inherit the Randomness Consumer interface
  • Create a variable for the Randomness Precompile so we can easily access it's functions later on
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;

import "./Randomness.sol";
import {RandomnessConsumer} from "./RandomnessConsumer.sol";

contract Lottery is RandomnessConsumer {
    // Randomness Precompile interface
    Randomness public randomness =

Define Parameters for the Lottery and Randomness Request

Next we're going to need to define the rules of our lottery, such as:

  • The participation fee
  • The minimum and maximum number of participants
  • The minimum length of the lottery
  • The number of winners

Inside of the Lottery contract, you can add these parameters:

// The number of winners. This number corresponds to how many random words
// will be requested. Cannot exceed MAX_RANDOM_WORDS (from the Randomness
// Precompile)
uint8 public NUM_WINNERS = 2;

// The number of blocks before the request can be fulfilled (for Local VRF
// randomness). The MIN_VRF_BLOCKS_DELAY (from the Randomness Precompile) 
// provides a minimum number that is safe enough for games with low economical
// value at stake. Increasing the delay slightly reduces the probability 
// (already very low) of a collator being able to predict the pseudo-random number

// The minimum number of participants to start the lottery
uint256 public MIN_PARTICIPANTS = 3;

// The maximum number of participants allowed to participate. It is important 
// to limit the total jackpot (by limiting the number of participants) to
// guarantee the economic incentive of a collator to avoid trying to influence
// the pseudo-random. (See Randomness.sol for more details)
uint256 public MAX_PARTICIPANTS = 20;

// The fee needed to participate in the lottery. Will go into the jackpot
uint256 public PARTICIPATION_FEE = 100000 gwei;

We will also need to define some parameters specifically related to requesting randomness:

  • The gas limit for the transaction that fulfills a randomness request
  • The minimum fee needed to start the lottery and request the random words. Each request for randomness requires a fulfillment fee. The purpose of this fee is to pay for the fulfillment of a randomness request, which allows anyone to fulfill a request since the request will already have been paid for. When submitting a randomness request, a refund account can be specified, where any excess fees will be returned to. Our contract will be set up so that the owner of the lottery contract will receive the refund
  • A salt prefix and the global request count, both of which will be used to generate unique randomness requests

You can go ahead and add these parameters:

// The gas limit allowed to be used for the fulfillment, which depends on the
// code that is executed and the number of words requested. Test and adjust
// this limit based on the size of the request and the processing of the 
// callback request in the fulfillRandomWords() function
uint64 public FULFILLMENT_GAS_LIMIT = 100000;

// The minimum fee needed to start the lottery. This does not guarantee that 
// there will be enough fee to pay for the gas used by the fulfillment. 
// Ideally it should be over-estimated considering possible fluctuation of 
// the gas price. Additional fee will be refunded to the caller
uint256 public MIN_FEE = FULFILLMENT_GAS_LIMIT * 1 gwei;

// A string used to allow having different salt than other contracts
bytes32 public SALT_PREFIX = "my_demo_salt_change_me";

// Stores the global number of requests submitted. This number is used as a
// salt to make each request unique
uint256 public globalRequestCount;

Aside from these parameters, we'll need to create some variables which will be used to keep track of the current lottery:

  • The current request ID
  • The list of current participants
  • The jackpot
  • The owner of the lottery contract. This is necessary because only the owner of the contract will be allowed to start the lottery
  • The source of randomness (local VRF or BABE epoch) that is being used
// The current request id
uint256 public requestId;

// The list of current participants
address[] public participants;

// The current amount of token at stake in the lottery
uint256 public jackpot;

// the owner of the contract
address owner;

// Which randomness source to use. This correlates to the values in the
// RandomnessSource enum in the Randomness Precompile
Randomness.RandomnessSource randomnessSource;

Create the Constructor

Now that we have completed the initial set up of all of the variables required for the lottery, we can start to code the functions that will bring the lottery to life. First, we'll start off by creating a constructor function.

The constructor will accept a uint8 as the randomness source, which corresponds to the index of the type of randomness defined in the RandomnessSource enum, located in the Randomness Precompile. So, we can either pass in 0 for local VRF or 1 for BABE epoch randomness. It will also be payable, as we'll submit the deposit at the time of deployment and will be used to perform the randomness request later on.

The deposit is defined in the Randomness Precompile and is required in addition to the fulfillment fee. The deposit will be refunded to the original requester, which in our case is the owner of the contract, after the request has been fulfilled. If a request never gets fulfilled, it will expire and need to be purged. Once it is purged, the deposit will be returned.

    Randomness.RandomnessSource source
) payable RandomnessConsumer() {
    // Because this contract can only perform one randomness request at a time,
    // we only need to have one required deposit
    uint256 requiredDeposit = randomness.requiredDeposit();
    if (msg.value < requiredDeposit) {
        revert("Deposit too Low");
    // Update parameters
    randomnessSource = source;
    owner = msg.sender;
    globalRequestCount = 0;
    jackpot = 0;
    // Set the requestId to the maximum allowed value by the precompile (64 bits)
    requestId = 2 ** 64 - 1;

Add Logic to Participate in the Lottery

Next we can create the function that will allow users to participate in the lottery. The participate function will be payable as each participant will need to submit a participation fee.

The participate function will include the following logic:

  • Check that the lottery hasn't started yet using the getRequestStatus function of the Randomness Precompile. This function returns the status as defined by the RequestStatus enum. If the status is anything other than DoesNotExist, then the lottery has already been started
  • Check that the participation fee meets the requirement
  • If both of the above are true, then the participant will be added to the list of participants and their participation fee will be added to the jackpot
function participate() external payable {
    // We check that the lottery hasn't started yet
    if (
        randomness.getRequestStatus(requestId) !=
    ) {
        revert("Request already initiated");

    // Each player must submit a fee to participate, which is added to
    // the jackpot
    if (msg.value != PARTICIPATION_FEE) {
        revert("Invalid participation fee");
    jackpot += msg.value;


In the above function, we check that the lottery hasn't started yet, but what if we want to know the exact status of the lottery? Create a function that solves this problem and returns the status of the lottery.

Add Logic to Start the Lottery and Request Randomness

The logic for starting the lottery contains a crucial component: requesting randomness. As previously mentioned, only the owner of the lottery contract will be able to start the lottery. As such, the owner will need to submit the fulfillment fee for the request.

The startLottery function will include the following logic:

  • Check that the lottery hasn't started yet, as we did in the participate function
  • Check that there is an acceptable number of participants
  • Check that the fulfillment fee meets the minimum requirements
  • Check that the balance of the contract is enough to pay for the deposit. Remember how the constructor accepts the request deposit? That deposit is stored in the contract until this function is called
  • If all of the above are true, we submit the randomness request via the Randomness Precompile along with the fulfillment fee. Depending on the source of randomness, either the requestLocalVRFRandomWords or the requestRelayBabeEpochRandomWords function of the Randomness Precompile will be called along with the following parameters:
    • The address where excess fees will be refunded to
    • The fulfillment fee
    • The gas limit to use for the fulfillment
    • The salt, which is a string that is mixed with the randomness seed to obtain different random words. The globalRequestCount is used to ensure uniqueness
    • The number of random words requested, which is based off the number of winners that will be selected
    • (For local VRF only) The delay, which is the number of blocks that must pass before the request can be fulfilled

Since the lottery function should only be called by the owner, we'll also add in an onlyOwner modifer that requires the msg.sender to be the owner.

function startLottery() external payable onlyOwner {
    // Check we haven't started the randomness request yet
    if (
        randomness.getRequestStatus(requestId) !=
    ) {
        revert("Request already initiated");
    // Check that the number of participants is acceptable
    if (participants.length < MIN_PARTICIPANTS) {
        revert("Not enough participants");
    if (participants.length >= MAX_PARTICIPANTS) {
        revert("Too many participants");
    // Check the fulfillment fee is enough
    uint256 fee = msg.value;
    if (fee < MIN_FEE) {
        revert("Not enough fee");
    // Check there is enough balance on the contract to pay for the deposit.
    // This would fail only if the deposit amount required is changed in the
    // Randomness Precompile.
    uint256 requiredDeposit = randomness.requiredDeposit();
    if (address(this).balance < jackpot + requiredDeposit) {
        revert("Deposit too low");

    if (randomnessSource == Randomness.RandomnessSource.LocalVRF) {
        // Request random words using local VRF randomness
        requestId = randomness.requestLocalVRFRandomWords(
            SALT_PREFIX ^ bytes32(globalRequestCount++),
    } else {
        // Requesting random words using BABE Epoch randomness
        requestId = randomness.requestRelayBabeEpochRandomWords(
            SALT_PREFIX ^ bytes32(globalRequestCount++),

modifier onlyOwner() {
    require(msg.sender == owner);

Add Logic to Fulfill the Randomness Request

In this section, we'll be adding in two functions required to request fulfillment and handle the result of the fulfillment: fulfillRequest and fulfillRandomWords.

Our fulfillRequest function will call the fulfillRequest method of the Randomness Precompile. When this method is called, under the hood the rawFulfillRandomWords method of the Randomness Consumer is called, which will verify that the call originated from the Randomness Precompile. From there, the fulfillRandomWords function of the Randomness Consumer contract is called and the requested number of random words are computed using the block's randomness result and a given salt, and then it is returned. If the fulfillment was successful, the FulfillmentSucceeded event will be emitted; otherwise, the FulfillmentFailed event will be emitted.

For fulfilled requests, the cost of execution will be refunded from the request fee to the caller of fulfillRequest. Then any excess fees and the request deposit are transferred to the specified refund address.

Our fulfillRandomWords function defines a callback, the pickWinners function, that is responsible for handling the fulfillment. So, in our case, the callback will use the random words to select a winner and payout the winnings. The signature of our fulfillRandomWords function must match the signature of the Randomness Consumer's fulfillRandomWords function.

function fulfillRequest() public {

function fulfillRandomWords(
    uint256 /* requestId */,
    uint256[] memory randomWords
) internal override {

We'll create the logic for the pickWinners function in the next section.


What if gas prices change significantly before we request the fulfillment, and as a result this function fails? Currently, we wouldn't be able to increase the fulfillment fee. Create a function that solves this problem and allows us to increase the fulfillment fee.

Add Logic to Pick the Lottery Winners

The last step for our lottery contract will be to create the pickWinners function, which, as previously mentioned, is responsible for using the random words to select a winner of the lottery.

The pickWinners function contains the following logic:

  • Determine the number of winners. This is only necessary if you happened to change either the NUM_WINNERS or the number of MIN_PARTICIPANTS, so that the NUM_WINNERS is greater than the MIN_PARTICIPANTS
  • Calculate the amount to be awarded to the winners based on the amount in the jackpot and the total number of winners
  • Determine the winners by using the random words
  • Distribute the winnings to each of the winners, making sure to deduct the winnings from the jackpot before transferring them
// This function is called only by the fulfillment callback
function pickWinners(uint256[] memory randomWords) internal {
    // Get the total number of winners to select
    uint256 totalWinners = NUM_WINNERS < participants.length
        ? NUM_WINNERS
        : participants.length;

    // The amount distributed to each winner
    uint256 amountAwarded = jackpot / totalWinners;
    for (uint32 i = 0; i < totalWinners; i++) {
        // This is safe to index randomWords with i because we requested
        // NUM_WINNERS random words
        uint256 randomWord = randomWords[i];

        // Using modulo is not totally fair, but fair enough for this demo
        uint256 index = randomWord % participants.length;
        address payable winner = payable(participants[index]);
        delete participants[index];
        jackpot -= amountAwarded;

Congratulations! You've gone through the entire process of creating the Lottery.sol contract! You can view the completed version of the Lottery.sol contract on GitHub. Remember, this contract is for educational purposes only and is not meant for production use.


To make the contract easier to work with, add some events for when a lottery has started, a winner has been chosen, and a winner has been awarded.

Interact with the Lottery Contract

Now that we've gone through and created our lottery contract, let's deploy it and start a lottery!

Compile & Deploy the Lottery Contract

To compile our contracts, you can simply run:

npx hardhat compile

Compile the contracts using Hardhat's compile command.

After compilation, an artifacts directory is created: it holds the bytecode and metadata of the contracts, which are .json files. It’s a good idea to add this directory to your .gitignore.

Before we can deploy the Lottery.sol contract, we'll need to create a deployment script.

You can create a new directory for the script and name it scripts and add a new file to it called deploy.js:

mkdir scripts && 
touch scripts/deploy.js

Now to write the deployment script we can use ethers. Because we'll be running it with Hardhat, we don't need to import any libraries. We can simply take the following steps:

  1. Create a local instance of the lottery contract with the getContractFactory method
  2. Get the deposit required for a randomness request using the requiredDeposit function of the Randomness Precompile
  3. Use the deploy method that exists within this instance to instantiate the smart contract. You can pass in 0 to use local VRF randomness or 1 for BABE epoch randomness. For this example, we'll use local VRF randomness. We'll also need to submit the deposit upon deployment
  4. Wait for the deployment by using deployed
  5. Once deployed, we can fetch the address of the contract using the contract instance
async function main() {
  // 1. Get the contract to deploy
  const Lottery = await ethers.getContractFactory('Lottery');

  // 2. Get the required deposit amount from the Randomness Precompile
  const Randomness = await ethers.getContractAt(
  const deposit = await Randomness.requiredDeposit();

  // 3. Instantiate a new Lottery smart contract that uses local VRF
  // randomness and pass in the required deposit
  const lottery = await Lottery.deploy(0, { value: deposit });
  console.log('Deploying Lottery...');

  // 4. Waiting for the deployment to resolve
  await lottery.deployed();

  // 5. Use the contract instance to get the contract address
  console.log('Lottery deployed to:', lottery.address);

  .then(() => process.exit(0))
  .catch((error) => {

To deploy our lottery contract, we'll use the run command and specify moonbase as the network:

npx hardhat run --network moonbase scripts/deploy.js

If you're using another Moonbeam network, make sure that you specify the correct network. The network name needs to match how it's defined in the hardhat.config.js.

After a few seconds, the contract is deployed, and you should see the address in the terminal. Save the address, as we will use it to interact with this contract instance in the next step.

Deploy the Lottery contract using Hardhat's run command.

Create Scripts to Interact with the Lottery Contract

We can continue to work with our Hardhat project and create additional scripts to interact with our lottery contract and call some of it's functions. For example, to participate in the lottery, we can create another script in our scripts directory:

touch participate.js

Then we can add the following code, which will create an instance of the lottery contract using the name of the contract and the contract address. Then we can obtain the participation fee directly from the contract and call the contract's participate function:

async function participate() {
    const lottery = await ethers.getContractAt('Lottery', 'INSERT-CONTRACT-ADDRESS');

    const participationFee = await lottery.PARTICIPATION_FEE();
    const tx = await lottery.participate({ value: participationFee });
    console.log('Participation transaction hash:', tx.hash);

    .then(() => process.exit(0))
    .catch((error) => {

To run this script, you can use the following command:

npx hardhat run --network moonbase scripts/participate.js

The transaction hash will be printed to the console. You can use the hash to look up the transaction on Moonscan.

Run the partipation script using Hardhat's run command.

And that's it! You can feel free to continue creating additional scripts to perform the next steps of the lottery, such as starting the lottery and picking the winners.

This tutorial is for educational purposes only. As such, any contracts or code created in this tutorial should not be used in production.
The information presented herein has been provided by third parties and is made available solely for general information purposes. Moonbeam does not endorse any project listed and described on the Moonbeam Doc Website ( Moonbeam Foundation does not warrant the accuracy, completeness or usefulness of this information. Any reliance you place on such information is strictly at your own risk. Moonbeam Foundation disclaims all liability and responsibility arising from any reliance placed on this information by you or by anyone who may be informed of any of its contents. All statements and/or opinions expressed in these materials are solely the responsibility of the person or entity providing those materials and do not necessarily represent the opinion of Moonbeam Foundation. The information should not be construed as professional or financial advice of any kind. Advice from a suitably qualified professional should always be sought in relation to any particular matter or circumstance. The information herein may link to or integrate with other websites operated or content provided by third parties, and such other websites may link to this website. Moonbeam Foundation has no control over any such other websites or their content and will have no liability arising out of or related to such websites or their content. The existence of any such link does not constitute an endorsement of such websites, the content of the websites, or the operators of the websites. These links are being provided to you only as a convenience and you release and hold Moonbeam Foundation harmless from any and all liability arising from your use of this information or the information provided by any third-party website or service.
Last update: April 19, 2023
| Created: March 17, 2023