How to Build a Blockchain Lottery

Why Make a Decentralized Lottery?

Building a blockchain lottery (or a decentralized lottery) is only a few contracts worth of code and relatively easy to spin up. Developers using Chainlink Verifiable Random Function (VRF) and the Chainlink Alarm Clock have an easy-to-maintain lottery contract that is secure, perpetual, and provably random. However, before we get into how to build a decentralized lottery, let’s quickly look into why we should make a blockchain lottery.

Provably Random / Incorruptible

Current lotteries require you to trust that whoever is running the lottery is going to run it honestly. Sadly, this isn’t always the case. Recently, there was an incident where the lottery was rigged for over $14 Million. This should never happen; a player should never have to worry that the people running the lottery have the ability to cheat. Blockchain lotteries can fix this by providing a platform that can’t be corrupted or hacked, where the numbers are chosen at random, and everyone can prove that they are chosen at random.

This is easily the most important reason why lotteries would be drastically better on blockchains. An essential part of any lottery is the trust that each entrant has in their money being treated fairly.

Lower Overhead

With all centralized applications, overhead is a nightmare. State lotteries currently have to pay:

  1. Staff to maintain the servers for the lotteries
  2. Staff to maintain the tickets and packaging
  3. Television draws, radio and online ads
  4. Staff to create new games

As things stand, blockchain lotteries can’t solve all of this, but they can solve at least #1 and #4. Once the contracts are already made and proven to be secure, you never have to reinvent the wheel. You can just place a front-end on top of the same open-sourced lottery smart contract. The servers are now totally replaced with the blockchain that you’re running on.

How do We Build a Blockchain Based Decentralized Lottery?

We are going to be following along with the code from this Chainlink Lottery github repo for its simplicity. There are a number of examples out there including the CandyShop (an ETH Global HackMoney winner), lotto-buffalo (a Colorado GameJam winner), and Buffi’s WoF (a Colorado GameJam winner).

For this walkthrough, we will be using a single oracle to fuel our lottery. This would make the lottery centralized around these single nodes. Ideally, you’d want to go the extra mile and set up or connect a network to several alarm clocks and Chainlink VRFs so that the lottery is truly decentralized, but that should be fairly trivial after getting the initial part set up.

The code used in this tutorial has not been audited or reviewed and is purely for educational purposes. Please also check local laws and government regulations before attempting to run a lottery, decentralized or not, as lotteries are highly regulated.

Setup

To keep things simple, we will be going through this demo as if you were using Remix. You can use that link to deploy all the code used in the walkthrough. However, we’d recommend learning Truffle/Buidler and React to have a full development suite/app. With Truffle/Buidler and React, you can quickly iterate through tests, deployments, and add a front-end to your application. For a deeper dive on how to develop with Truffle in your smart contracts, be sure to check out this tutorial blog. You’ll notice the syntax for imports is a little different on Nodejs applications than in Remix, but other than that, all the code below will be the same. You can also clone or fork any of the repositories from above to play with them yourself.

Let’s Start Building

Conceptually, there are only a few things that we need to do in a lottery.

  1. Enter the lottery and keep track of entries
  2. Randomly pick the winner at a certain time
  3. Manage payout
  4. Repeat

To get started with this first part, we create the ‘Lottery.sol’ contract. We want to create some instance variables to keep track of the players, state of the lottery, and maybe how many lotteries we’ve played so far (‘lotteryId’). We of course want our contract to inherit ChainlinkClient, so we can make use of the Chainlink Alarm.

You’ll notice that we are ignoring the interfaces and governance pieces for now. We’ll talk about that later.

pragma solidity ^0.6.6;
import "github.com/smartcontractkit/chainlink/evm-contracts/src/v0.6/ChainlinkClient.sol";

contract Lottery is ChainlinkClient {
    enum LOTTERY_STATE { OPEN, CLOSED, CALCULATING_WINNER }
    LOTTERY_STATE public lottery_state;
    address payable[] public players;
    uint256 public lotteryId;
}

Next we will need our constructor, where we can set some initial variables. We set the lottery’s state to closed (since it hasn’t started yet), and we set the ‘lotteryId’ to ‘1’. We also need the `setPublicChainlinkToken()` to interact with Chainlink oracles.

constructor() public {
    setPublicChainlinkToken();
    lotteryId = 1;
    lottery_state = LOTTERY_STATE.CLOSED;
}

Set a Timer for the Lottery

Great, so how do we start the lottery? How do we make it so the lottery will start and end when we want it to? This is where the Chainlink Alarm comes in, we can create a function that connects to a Chainlink Alarm that will stop/close the lottery when the time is up.

We need to specify the ‘CHAINLINK_ALARM_JOB_ID’ and ‘CHAINLINK_ALARM_ORACLE’ with Chainlink nodes that support the Chainlink Alarm. You can find them in the documentation. This function tells the alarm clock to return to the ‘fulfill_alarm function’ after ‘now + duration’. duration is the amount of time you want the alarm to wait in seconds. When duration has passed the alarm will call ‘fulfill_alarm’.

Next, we want ‘fulfill_alarm’ to be able to pick the winner. We do this, by adding the ‘pickWinner’ function, which we will describe later.

function start_new_lottery(uint256 duration) public {
    require(lottery_state == LOTTERY_STATE.CLOSED, "can't start a new lottery yet");
    lottery_state = LOTTERY_STATE.OPEN;
    Chainlink.Request memory req = buildChainlinkRequest(CHAINLINK_ALARM_JOB_ID, address(this), this.fulfill_alarm.selector);
    req.addUint("until", now + duration);
    sendChainlinkRequestTo(CHAINLINK_ALARM_ORACLE, req, ORACLE_PAYMENT);
}

function fulfill_alarm(bytes32 _requestId)
    public
    recordChainlinkFulfillment(_requestId)
{
    require(lottery_state == LOTTERY_STATE.OPEN, "The lottery hasn't even started!");
    lottery_state = LOTTERY_STATE.CALCULATING_WINNER;
    lotteryId = lotteryId + 1;
    pickWinner();
}

Enter the Lottery

Now that we know how to start the lottery, players need to be able to enter the lottery by buying tickets in ETH. You can set a ‘MINIMUM’ ticket price, or just set it so the user has to send a specific amount of ETH to be entered into the lottery.

The following code checks that the lottery is open and the user enters the minimum amount to obtain a ticket. It then pushes the user onto the dynamic array of entered users.

function enter() public payable {
    assert(msg.value == MINIMUM);
    assert(lottery_state == LOTTERY_STATE.OPEN);
    players.push(msg.sender);
}

This is all we need to do for user management. Now we move onto the fun part, picking the winner.

Picking a Winner

In our demo we are going to have a different contract handle drawing a random number using a Chainlink VRF. We will have to build interfaces for all our contracts so that they can easily interact with each other, and then make a governance contract that connects them each to each other. (You could also have them govern each other, but if you decide you want a lot of features for your lottery and a lot of smart contracts, it’s easy to put them all in a single governance contract).

The ‘pickWinner’ function will look something like this:

function pickWinner() private {
    require(lottery_state == LOTTERY_STATE.CALCULATING_WINNER, "You aren't at that stage yet!");
    RandomnessInterface(governance.randomness()).getRandom(lotteryId, lotteryId);
    //this kicks off the request and returns through fulfill_random
}

In this function we are saying:

“Get me the address of the Randomness smart contract defined in the ‘governance.randomness()’ function. That Randomness smart contract will have a ‘getRandom’ function that takes two uint256’s as parameters.”

Connecting the Lottery to the Randomness

We want to define a Governance smart contract to connect our contract that will be handling getting random numbers with our lottery contract. Our governance contract will then look something like this:

pragma solidity ^0.6.6;

contract Governance {
    uint256 public one_time;
    address public lottery;
    address public randomness;
    constructor() public {
    }
  
    function init(address _lottery, address _randomness) public {
        require(_randomness != address(0), "governance/no-randomnesss-address");
        require(_lottery != address(0), "no-lottery-address-given");
        randomness = _randomness;
        lottery = _lottery;
    }
}

And the init function should be called every time we deploy the lottery contract and the randomness contract so that they each know what the address of the other contract is.

The interfaces will then look pretty simple:

pragma solidity 0.6.6;

interface RandomnessInterface {
    function randomNumber(uint) external view returns (uint);
    function getRandom(uint, uint) external;
}

The interface is going to be whatever few functions need to be shared with the other smart contract. For a deeper dive on why interfaces are needed, be sure to check out this deeper dive into interfaces vs abstract contracts. You can see the sample setup of the interfaces in Remix if you’d like to see them in action.

Once we have them connected with a governance contract and interfaces, we can start looking into getting a random number.

Getting a Random Number

Getting a random number is almost the same as the demo code in the Chainlink VRF documentation. We are testing it on Ropsten so we can nearly run with the exact code from the docs.

/**
  * Constructor inherits VRFConsumerBase
  * 
  * Network: Ropsten
  * Chainlink VRF Coordinator address: 0xf720CF1B963e0e7bE9F58fd471EFa67e7bF00cfb
  * LINK token address:                0x20fE562d797A42Dcb3399062AE9546cd06f63280
  * Key Hash: 0xced103054e349b8dfb51352f0f8fa9b5d20dde3d06f9f43cb2b85bc64b238205
  */
constructor(address _governance) 
    VRFConsumerBase(
        0xf720CF1B963e0e7bE9F58fd471EFa67e7bF00cfb, // VRF Coordinator
        0x20fE562d797A42Dcb3399062AE9546cd06f63280  // LINK Token
    ) 
    public
{
    keyHash = 0xced103054e349b8dfb51352f0f8fa9b5d20dde3d06f9f43cb2b85bc64b238205;
    fee = 0.1 * 10 ** 18; // 0.1 LINK
    governance = GovernanceInterface(_governance);
}

In the constructor, we will add a piece about connecting to the governance contract. The only other difference will be in the ‘fulfillRandomness’ function, where we will pass the random number we got back to the lottery smart contract.

/**
  * Callback function used by VRF Coordinator
  */
function fulfillRandomness(bytes32 requestId, uint256 randomness) external override {
    require(msg.sender == vrfCoordinator, "Fulfillment only permitted by Coordinator");
    most_recent_random = randomness;
    uint lotteryId = requestIds[requestId];
    randomNumber[lotteryId] = randomness;
    LotteryInterface(governance.lottery()).fulfill_random(randomness);
}

Picking a Winner

We call the ‘fulfill_random’ of the Lottery contract to pick the winner based off of the random number. We use a simple modulo to get the random number. So back in the lottery contract, we would have:

function fulfill_random(uint256 randomness) external {
    require(lottery_state == LOTTERY_STATE.CALCULATING_WINNER, "You aren't at that stage yet!");
    require(randomness > 0, "random-not-found");
    uint256 index = randomness % players.length;
    players[index].transfer(address(this).balance);
    players = new address payable[](0);
    lottery_state = LOTTERY_STATE.CLOSED;
}

And this would close the lottery. You could then have this contract loop by having it then call the ‘start_new_lottery’ function, and your lottery could run perpetually forever, so long as your contracts were always funded with LINK to cover oracle gas costs.

If you learned something new here, want to show off what you’ve built, developed a frontend for some of the demo repos, or even improved any of the repos with a PR, make sure you share it on TwitterDiscord, or Reddit, and hashtag your repos with #chainlink and #ChainlinkVRF.

If you’re developing a product that could benefit from Chainlink oracles or would like to assist in the open-source development of the Chainlink network, visit the developer documentation or join the technical discussion on Discord.

The Chainlink Fall 2021 Hackathon kicks off October 22, 2021. Whether you’re a developer, creator, artist, blockchain expert, or completely new to the space, this hackathon is the perfect place to kickstart your smart contract development journey and learn from industry-leading mentors. Secure your spot today to compete for over $300k in prizes. 

Need Integration Support?
Talk to an expert
Faucets
Get testnet tokens
Read the Docs
Technical documentation