Photo by dylan nolte on Unsplash

How To Build a Verifiably Random Lottery Smart Contract on Ethereum

By alexroan | Blockchain Developer | 11 Jun 2020


True randomness has been near impossible on Ethereum. This is because transactions need to be verified by multiple nodes on the network to be confirmed. If a smart contract function were truly random, each node that verified a transaction using that function would come to a different result, meaning the transaction would never be confirmed.

A recent announcement by one of the biggest players in the Ethereum ecosystem has caused excitement around this very problem. Using a system called a Verifiable Random Function (VRF), Ethereum smart contracts can now generate random numbers.

This means concepts that seemed a perfect fit with smart contracts but couldn’t be brought to life because they required random numbers now can be.

One such concept is that of a lottery.


Building a Lottery Smart Contract

Our lottery is going to have three phases. The first is open, where new numbers can be submitted by anyone for a small fee. The second is closed, where no new numbers can be submitted and the random number is being generated. The third is finished, where the number has been generated and the winner has been paid.

If no one wins, the lottery contract can be rolled over, increasing the jackpot.

Defining the phases

Phases should restrict actions so only permitted operations can be performed. For example, the only phase that should allow new submissions is the open phase. If the lottery is closed or finished, the contract should forbid new submissions.

Using enum, we can define as many phases as we want. Let’s call it LotteryState. In our state variables, we define the following:

enum LotteryState { Open, Closed, Finished }
LotteryState public state;

Now that the enumeration is defined, we can set rules (require statements) in the functions, ensuring the current state of the contract is what we expect it to be.

Given that these require statements are likely to look similar throughout the contract, let’s minimise it. We can define a modifier that performs the require statement, and we can assign it to any function we wish.

modifier isState(LotteryState _state) {
require(state == _state, "Wrong state for this action");
_;
}

Now when we define functions, we can add this modifier to ensure the current state of the lottery is what we expect it to be.

Submitting numbers

Anyone should be allowed to submit a number so long as the minimum entry fee is paid. However, each entrant can’t submit the same number more than once. The only state that should allow new submissions is the open state.

Here’s our submitNumber function:

function submitNumber(uint _number) public payable isState(LotteryState.Open) {
  require(msg.value >= entryFee, "Minimum entry fee required");
  require(entries[_number].add(msg.sender), "Cannot submit the same number more than once");
  numbers.push(_number);
  numberOfEntries++;
  payable(owner()).transfer(ownerCut);
  emit NewEntry(msg.sender, _number);
}

Figure 1

Line 1 defines the name, the single _number parameter, and the fact it’s public and payable. It also adds the isState modifier to ensure the lottery is open.

Line 2 ensures the correct entry fee has been paid, and line 3 ensures the sender of the message hasn’t already submitted that number and adds it to the entries in the process.

The variable entries refers to a mapping defining the guessed number and a set of addresses that have entered that number. It’s defined like this:

mapping(uint => EnumerableSet.AddressSet) entries;

AddressSet refers to the OpenZeppelin EnumerableSet contract, which provides added functionality for primitive types.

Once the checks are complete, the following four lines add the number to the guesses, pay out a small percentage of the owner cut, and emit a NewEntry event.

Drawing the number

If you’ve read this article on how to use VRF, then you’ll know generating a random number isn’t as simple as calling a single function (like Math.random() in JavaScript).

To generate a random number, you must request randomness from the VRF coordinator and implement a function that VRF can call back to with the response. For this, we need to define a VRF consumer (the details of creating a VRF consumer can be found here), which we call RandomNumberGenerator in Figure 2.

pragma solidity ^0.6.2;

import "./VRFConsumerBase.sol";
import "./Lottery.sol";

contract RandomNumberGenerator is VRFConsumerBase {

    address requester;
    bytes32 keyHash;
    uint256 fee;

    constructor(address _vrfCoordinator, address _link, bytes32 _keyHash, uint256 _fee)
        VRFConsumerBase(_vrfCoordinator, _link) public {
            keyHash = _keyHash;
            fee = _fee;
    }

    function fulfillRandomness(bytes32 _requestId, uint256 _randomness) external override {
        Lottery(requester).numberDrawn(_requestId, _randomness);
    }

    function request(uint256 _seed) public returns(bytes32 requestId) {
        require(keyHash != bytes32(0), "Must have valid key hash");
        requester = msg.sender;
        return this.requestRandomness(keyHash, fee, _seed);
    }
}

Figure 2

Our lottery will take the address of this contract as an injected parameter upon construction. When drawing the number, it’ll call the request function. This requests randomness from VRF, which, in turn, supplies the response to filfullRandomness on line 18. You can see in Figure 2 calls that this calls back to our Lottery contract with numberDrawn. Let’s define those functions:

function drawNumber(uint256 _seed) public onlyOwner isState(LotteryState.Open) {
	_changeState(LotteryState.Closed);
	randomNumberRequestId = RandomNumberGenerator(randomNumberGenerator).request(_seed);
	emit NumberRequested(randomNumberRequestId);
}
  
function numberDrawn(bytes32 _randomNumberRequestId, uint _randomNumber) public onlyRandomGenerator isState(LotteryState.Closed) {
	if (_randomNumberRequestId == randomNumberRequestId) {
		winningNumber = _randomNumber;
		emit NumberDrawn(_randomNumberRequestId, _randomNumber);
		_payout(entries[_randomNumber]);
		_changeState(LotteryState.Finished);
	}
}

Figure 3

drawNumber can only be called by the owner of the lottery in our definition on line 1 and can only be called when the lottery is in the open state.

numberDrawn on line 7 is the function that fulfillRandomness calls back to once the random number has been received by VRF. It ensures the request-id is the ID that was returned from the request, emits an event, pays out the winner, and changes the state of the lottery to Finished.


The Full Code

pragma solidity >=0.6.2;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/EnumerableSet.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";
import "./RandomNumberGenerator.sol";

contract Lottery is Ownable{

	using EnumerableSet for EnumerableSet.AddressSet;
	using Address for address;
	using SafeMath for uint;

	enum LotteryState { Open, Closed, Finished }

	mapping(uint => EnumerableSet.AddressSet) entries;
	uint[] numbers;
	LotteryState public state;
	uint public numberOfEntries;
	uint public entryFee;
	uint public ownerCut;
	uint public winningNumber;
	address randomNumberGenerator;
	bytes32 randomNumberRequestId;

	event LotteryStateChanged(LotteryState newState);
	event NewEntry(address player, uint number);
	event NumberRequested(bytes32 requestId);
	event NumberDrawn(bytes32 requestId, uint winningNumber);

	// modifiers
	modifier isState(LotteryState _state) {
		require(state == _state, "Wrong state for this action");
		_;
	}

	modifier onlyRandomGenerator {
		require(msg.sender == randomNumberGenerator, "Must be correct generator");
		_;
	}

	//constructor
	constructor (uint _entryFee, uint _ownerCut, address _randomNumberGenerator) public Ownable() {
		require(_entryFee > 0, "Entry fee must be greater than 0");
		require(_ownerCut < _entryFee, "Entry fee must be greater than owner cut");
		require(_randomNumberGenerator != address(0), "Random number generator must be valid address");
		require(_randomNumberGenerator.isContract(), "Random number generator must be smart contract");
		entryFee = _entryFee;
		ownerCut = _ownerCut;
		randomNumberGenerator = _randomNumberGenerator;
		_changeState(LotteryState.Open);
	}

	//functions
	function submitNumber(uint _number) public payable isState(LotteryState.Open) {
		require(msg.value >= entryFee, "Minimum entry fee required");
		require(entries[_number].add(msg.sender), "Cannot submit the same number more than once");
		numbers.push(_number);
		numberOfEntries++;
		payable(owner()).transfer(ownerCut);
		emit NewEntry(msg.sender, _number);
	}

	function drawNumber(uint256 _seed) public onlyOwner isState(LotteryState.Open) {
		_changeState(LotteryState.Closed);
		randomNumberRequestId = RandomNumberGenerator(randomNumberGenerator).request(_seed);
		emit NumberRequested(randomNumberRequestId);
	}

	function rollover() public onlyOwner isState(LotteryState.Finished) {
		//rollover new lottery
	}

	function numberDrawn(bytes32 _randomNumberRequestId, uint _randomNumber) public onlyRandomGenerator isState(LotteryState.Closed) {
		if (_randomNumberRequestId == randomNumberRequestId) {
			winningNumber = _randomNumber;
			emit NumberDrawn(_randomNumberRequestId, _randomNumber);
			_payout(entries[_randomNumber]);
			_changeState(LotteryState.Finished);
		}
	}

	function _payout(EnumerableSet.AddressSet storage winners) private {
		uint balance = address(this).balance;
		for (uint index = 0; index < winners.length(); index++) {
			payable(winners.at(index)).transfer(balance.div(winners.length()));
		}
	}

	function _changeState(LotteryState _newState) private {
		state = _newState;
		emit LotteryStateChanged(state);
	}
}

Figure 4

This is a primitive implementation, but it shows how the advent of verifiable randomness on the blockchain reduces the amount of complexity in contracts like lotteries. Previous gambling contracts needed to use hashing mechanisms, time-based mechanisms, block-based mechanisms, etc — all of which are vulnerable.


Learn More

If you’re interested in Blockchain Development, I write tutorials, walkthroughs, hints, and tips on how to get started and build a portfolio. Check out this evolving list of Blockchain Development Resources.

If you enjoyed this post and want to learn more about Blockchain Development or the Blockchain Space in general, I highly recommend signing up to the Blockgeeks platform. They have courses on a wide range of topics in the industry, from Coding to Marketing to Trading. It has proven to be an invaluable tool for my development in the Blockchain space.

 

How do you rate this article?

8


alexroan
alexroan

Blockchain Developer


Blockchain Developer
Blockchain Developer

Tutorials, walkthrough, hints and tips on Blockchain Development for all levels of expertise.

Send a $0.01 microtip in crypto to the author, and earn yourself as you read!

20% to author / 80% to me.
We pay the tips from our rewards pool.