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.