Creating A Lottery and Oracle with TezTech

BriceAldrich
12 min readApr 18, 2019

Who doesn’t dream of winning the lottery? Just by owning Tezos you may get rich without the gamble, but why not create a Tezos lottery? In this tutorial we are going to create a lottery that allows you to buy one lottery ticket with 5 random numbers. The lottery will draw one winner every week.

Before we get started lets talk about blockchains and random. There really isn’t a good way to achieve random because of the nature of on chain code, because of this we need to create a trusted Oracle to provide the randomness. This is unfortunate, but it will allow us to achieve our goal.

Contract Development

Describing a Lottery

The first thing we need to do is describe the lottery structure in our contract.

struct Lottery(
nat[] winnums,
mutez pot,
map[address=>nat[]] tickets
);

In our lottery we are going to have an array of winning numbers, defined as nat[] winnums. We will also need something to represent the current pot, mutez pot. And finally we will need a way to map participants to a ticket of random numbers, map[address=>nat[]] tickets.

Defining Storage

const mutez TICKET 5000000;
storage Lottery lottery;
storage address winner;
storage address oracle;
storage bool ended;

The first thing we need to store is the price of each ticket in Mutez, this will be constant, denoted as const mutez TICKET 5000000;. Next we need the lottery itself, storage Lottery lottery;. Then the winner, storage address winner;. The address of our trusted oracle, storage address oracle;. And finally a utility storage to say the lottery has ended,storage bool ended;.

Buying A Ticket

Next we are going to define a function to allow someone to buy a ticket in our lottery.

entry BuyTicket(nat num1, nat num2, nat num3, nat num4, nat num5){
if (AMOUNT != TICKET) {
throw(string "you must send 5000000 mutez exeactly to buy a ticket");
transfer(SENDER, AMOUNT);
}
if (input.num1 > nat 99 || input.num2 > nat 99 || input.num3 > nat 99 || input.num4 > nat 99 || input.num5 > nat 99){
throw(string "all numbers must be between 0-99")
}
if (in(storage.lottery.tickets, SENDER) == bool false) {
throw(string "cannot buy more than one ticket")
}
let nat[] nums = new list(nat);
nums.push(input.num1);
nums.push(input.num2);
nums.push(input.num3);
nums.push(input.num4);
nums.push(input.num5);
storage.lottery.pot = add(storage.lottery.pot, AMOUNT);
storage.lottery.tickets.push(SENDER, nums);
}

BuyTicket takes in 5 numbers, which will be the numbers on the buyers ticket. We then check if the AMOUNT sent in to the contract by the SENDER is equal to the ticket price. If the AMOUNT is not equal, it’s transfered back to the SENDER. Next we check that all input numbers are between 0–99. Finally we check if the SENDER already bought a ticket, and throw an error if so.

After all the logical checks we then push all the input numbers to a list called nums. We then add the AMOUNT sent into the contract to the lottery pot, and add the SENDERS numbers to the lottery tickets.

Adding Winning Numbers

Now we are going to discuss adding winning numbers. We only want our trusted oracle to add winning numbers.

entry addNumbers(nat num1, nat num2, nat num3, nat num4, nat num5){
if (SENDER != storage.oracle) {
throw(string "not authorized to add numbers");
}
let nat[] nums = new list(nat);
nums.push(input.num1);
nums.push(input.num2);
nums.push(input.num3);
nums.push(input.num4);
nums.push(input.num5);
storage.lottery.winnums = nums;
}

The entry addNumbers takes in the 5 winning numbers as input. We then perform a logical check to verify that the SENDER adding the winning numbers is our trusted oracle, if not we throw unauthorized. Finally we push all the input numbers a list called nums, and assign that tolottery.winnums.

Adding a Winner

We are now going to have the ability to declare the winner, we again don’t want to trust anyone but the oracle to do this, which is our first logical check.

entry addWinner(address winner){
if (SENDER != storage.oracle) {
throw(string "not authorized to add numbers");
}
storage.winner = input.winner;
}

In addWinner the address of the winner is passed and assigned to storage.winner.

Adding Withdraw

Finally we make a withdraw function so the winner of the contract can withdraw the pot.

entry withdraw(){
if (SENDER != storage.winner){
throw(string "your not the winner nice try");
}
if (storage.ended == bool true) {
throw(string "lottery ended")
}
transfer(storage.winner, storage.lottery.pot);
storage.ended = bool true;
}

We first verify if the SENDER is the winner. Next we check if the lottery has ended, the winner is only allowed to withdraw once. Finally we transfer the lottery pot to the winner, and set the lottery to ended.

Putting the Lottery Contract Together

struct Lottery(
nat[] winnums,
mutez pot,
map[address=>nat[]] tickets
);

const mutez TICKET 5000000;

storage Lottery lottery;
storage address winner;
storage address oracle;
storage bool ended;

entry BuyTicket(nat num1, nat num2, nat num3, nat num4, nat num5){
if (AMOUNT != TICKET) {
throw(string "you must send 5000000 mutez exeactly to buy a ticket");
transfer(SENDER, AMOUNT);
}
if (input.num1 > nat 99 || input.num2 > nat 99 || input.num3 > nat 99 || input.num4 > nat 99 || input.num5 > nat 99){
throw(string "all numbers must be between 0-99")
}
if (in(storage.lottery.tickets, SENDER) == bool false) {
throw(string "cannot buy more than one ticket")
}
let nat[] nums = new list(nat);
nums.push(input.num1);
nums.push(input.num2);
nums.push(input.num3);
nums.push(input.num4);
nums.push(input.num5);
storage.lottery.pot = add(storage.lottery.pot, AMOUNT);
storage.lottery.tickets.push(SENDER, nums);
}

entry addNumbers(nat num1, nat num2, nat num3, nat num4, nat num5){
if (SENDER != storage.oracle) {
throw(string "not authorized to add numbers");
}
let nat[] nums = new list(nat);
nums.push(input.num1);
nums.push(input.num2);
nums.push(input.num3);
nums.push(input.num4);
nums.push(input.num5);
storage.lottery.winnums = nums;
}

entry addWinner(address winner){
if (SENDER != storage.oracle) {
throw(string "not authorized to add numbers");
}
storage.winner = input.winner;
}

entry withdraw(){
if (SENDER != storage.winner){
throw(string "your not the winner nice try");
}
if (storage.ended == bool true) {
throw(string "lottery ended")
}
transfer(storage.winner, storage.lottery.pot);
storage.ended = bool true;
}

All in all, this contract is relatively simple. That’s because the meat to make this work, is actually in building the Oracle. We are going to build that next.

Oracle Development

The hard part of this lottery really is the oracle. To build this lottery’s oracle we are going to make a NodeJs Server using TezTech’s eztz and fi-compiler. Unfortunately for the sake of staying on point, I will not be going into immense detail around javascript as a language itself.

Imports and CLI and Contract

First let’s get started with some of the imports we are going to use including fi, eztz, commander, and node-schedule.

var fi = require("fi-compiler");
var eztz = require("/Users/bricealdrich/Development/eztz").eztz
var program = require('commander');
var schedule = require('node-schedule');

Now let’s define a CLI to configure our Oracle.

program
.version('0.1.0')
.option('-s, --serve', 'serve an oracale for a lottery')
.option('-k, --secret [type]', 'secret to wallet')
.option('-c, --contract [type]', 'contract address')
.option('-b, --buy <n>', 'buy a ticket')
.option('-v, --values', 'get storage values')
.parse(process.argv);

As you can see this Oracle CLI is going to allow us to serve a server ( -s), take in a secret key to be used (-k), take in the contract address (-c), allow you to buy a ticket (-b)(Bonus for later), and finally the CLI is going to allow you to get the contract storage (-v).

Next we are going to store our lottery contract in a variable called ficode to be used with the fi-code import.

var ficode = `<paste the lottery contract>`
var compiled = fi.compile(ficode);
fi.abi.load(compiled.abi);

We then use fi-code to compile our contract, and load the abi to build out arguments for operations.

Writing the Oracle Server

Now let’s handle the (-s) option for our oracle. The idea behind this server is that it will periodically pull random numbers for the lottery, until a winner is found a posted to the lottery contract.

if (program.serve){
if (!program.contract || !program.secret){
console.log("Need to specify contract address and secret")
process.exit(1);
}
var wallet = eztz.crypto.extractKeys(program.secret);
serv(wallet);
}

This logical statement handles (-s), and checks if the contract and secret key flags were passed as well. We then extract the wallet from the secret var wallet = eztz.crypto.extractKeys(program.secret); and then serv the server with the wallet. It’s important to note that the wallet will contain the keys to the oracles address.

function serv(keys) {
var j = schedule.scheduleJob('* * * * 0', function() {
FindWinner(keys)
});
}

The above code is fairly self explanatory if your familiar with node-js or a tool called cron, but basically this creates an infinite loop to perform the FindWinner task every monday (aka our server). Now let’s define FindWinner.

function FindWinner(keys) {
var query = "/chains/main/blocks/head/context/contracts/" + program.contract + "/storage"
var numbers = generateNumbers();
addNumbers(numbers, keys)
eztz.node.query(query).then(function(res) {
for (var i = 0; i < res.args[0].args[1].args[1][0].args.length; i = i + 2) {
var ticket = {
addr: res.args[0].args[1].args[1][0].args[i].string,
numbers: []
};
console.log(res.args[0].args[1].args[1][0].args[i].string)
res.args[0].args[1].args[1][0].args[i + 1].forEach(element => {
ticket.numbers.push(element.int)
});
console.log(ticket)
if (numbers.sort().toString() == ticket.numbers.sort().toString()) {
addWinner(ticket.addr, keys)
break;
}
}
}).catch(function(e) {
console.log(e)
});
}

In FindWinner we use generate the random winning numbers using a function called generateNumbers() (defined in Putting It All Together). After getting the random numbers, we publish them to our contract by using addNumbers (defined later). We then use eztz to query for our contracts storage. After getting the storage we loop through the storage values to find all the tickets. If a winning ticket is found to match those numbers we add the winner back into the contract by calling addWinner (defined later).

To get an idea of why the looping is structured the way it is, here is the JSON response to getting the contract storage.

{
"prim": "Pair",
"args": [
{
"prim": "Pair",
"args": [
[
{
"int": "0"
},
{
"int": "0"
},
{
"int": "0"
},
{
"int": "0"
},
{
"int": "0"
}
],
{
"prim": "Pair",
"args": [
{
"int": "10000000"
},
[
{
"prim": "Elt",
"args": [
{
"string": "tz1SeW2R9dahJpqn6x27tmRWhWszQuvsdPTN"
},
[
{
"int": "63"
},
{
"int": "57"
},
{
"int": "30"
},
{
"int": "33"
},
{
"int": "35"
}
]
]
}
]
]
}
]
},
{
"prim": "Pair",
"args": [
{
"string": "tz1SeW2R9dahJpqn6x27tmRWhWszQuvsdPTN"
},
{
"prim": "Pair",
"args": [
{
"string": "tz1SeW2R9dahJpqn6x27tmRWhWszQuvsdPTN"
},
{
"prim": "False"
}
]
}
]
}
]
}

It’s a pretty ugly Json blob, which explains the ugly iteration.

Now let’s define addNumbers.

function addNumbers(numbers, keys) {
var input = {
num1: numbers[0],
num2: numbers[1],
num3: numbers[2],
num4: numbers[3],
num5: numbers[4]
};
vbytes = fi.abi.entry("addNumbers", input)
vbytes = vbytes.substr(2);
var operation = {
"kind": "transaction",
"amount": "0",
"destination": program.contract,
"fee": 20000,
"gas_limit": 100000,
"parameters": {
"bytes": vbytes
}
};
eztz.rpc.sendOperation(keys.pkh, operation, keys).then(function(res) {
console.log(res);
}).catch(function(e) {
console.log(e)
});
}

In this function we take our random numbers and structure the input. We then use fi by getting the argument bytes for our operation vbytes = fi.abi.entry(“addNumbers”, input). We then construct a transaction operation and pass the parameter bytes in. Finally we use eztz to sendOperation with our oracle keys, and the operation we just definied.

Now let’s define addWinner.

function addWinner(winner, keys) {
var input = {
winner: winner
};
vbytes = fi.abi.entry("addWinner", input)
vbytes = vbytes.substr(2);
var operation = {
"kind": "transaction",
"amount": "0",
"destination": program.contract,
"fee": 20000,
"gas_limit": 100000,
"parameters": {
"bytes": vbytes
}
};
eztz.rpc.sendOperation(keys.pkh, operation, keys).then(function(res) {
console.log(res);
}).catch(function(e) {
console.log(e)
});
}

The function addWinner is very similar to addNumbers. We pass the winner in and construct the input for the abi vbytes = fi.abi.entry(“addWinner”, input). We construct our operation and use the same eztz function sendOperation.

Bonus Buy Flag

Let’s pack this tool into an all and one, and create a CLI option that allows someone to buy a ticket.

if (program.buy) {
if (!program.contract || !program.secret) {
console.log("Need to specify contract address and secret")
process.exit(1);
}
var wallet = eztz.crypto.extractKeys(program.secret);
for (var i = 0; i < program.buy; i++) {
buy(wallet)
}
}

Similarly to how we handled the serve option we check for the contract and secret as input. We then extract the keys to the wallet used for buying the ticket. We then loop and call the buy function for the quantity desired, which we define below.

function buy(keys) {
rand = generateNumbers();
var input = {
num1: rand[0],
num2: rand[1],
num3: rand[2],
num4: rand[3],
num5: rand[4]
};
vbytes = fi.abi.entry("BuyTicket", input)
vbytes = vbytes.substr(2);
var operation = {
"kind": "transaction",
"amount": "5000000",
"destination": program.contract,
"fee": 20000,
"gas_limit": 100000,
"parameters": {
"bytes": vbytes
}
};
eztz.rpc.sendOperation(keys.pkh, operation, keys).then(function(res) {
console.log(res);
}).catch(function(e) {
console.log(e)
});
}

The first thing we do is generate the random numbers for the ticket and construct the input. We pass the input to the abi to get the argument bytes fi.abi.entry(“BuyTicket”, input). Then we construct an operation and set 5000000 mutez or 5 xtz (ticket price) to the transfer amount. We then use eztz to sendOperation.

Bonus Storage Flag

Finally the last important function in this Oracle CLI is the ability to read the storage (-v).

function getStorage() {
var query = "/chains/main/blocks/head/context/contracts/" + program.contract + "/storage"
eztz.node.query(query).then(function(res) {
str = JSON.stringify(res);
str = JSON.stringify(res, null, 4);
console.log(str);
}).catch(function(e) {
console.log(e)
});
}

In getStorage we construct the RPC query to get the contract storage and pass it to eztz.node.query. We then print into a pretty JSON format to console.

Putting It All Together

var fi = require("fi-compiler");
var eztz = require("/Users/bricealdrich/Development/eztz").eztz
var program = require('commander');
var schedule = require('node-schedule');
program
.version('0.1.0')
.option('-s, --serve', 'serve an oracale for a lottery')
.option('-k, --secret [type]', 'secret to wallet')
.option('-c, --contract [type]', 'contract address')
.option('-b, --buy <n>', 'buy a ticket')
.option('-v, --values', 'get storage values')
.option('-n, --node', 'address to tezos node')
.parse(process.argv);
var ficode = `
struct Lottery(
nat[] winnums,
mutez pot,
map[address=>nat[]] tickets
);

const mutez TICKET 5000000;

storage Lottery lottery;
storage address winner;
storage address oracle;
storage bool ended;

entry BuyTicket(nat num1, nat num2, nat num3, nat num4, nat num5){
if (AMOUNT != TICKET) {
throw(string "you must send 5000000 mutez exeactly to buy a ticket");
transfer(SENDER, AMOUNT);
}
if (input.num1 > nat 99 || input.num2 > nat 99 || input.num3 > nat 99 || input.num4 > nat 99 || input.num5 > nat 99){
throw(string "all numbers must be between 0-99")
}
if (in(storage.lottery.tickets, SENDER) == bool false) {
throw(string "cannot buy more than one ticket")
}
let nat[] nums = new list(nat);
nums.push(input.num1);
nums.push(input.num2);
nums.push(input.num3);
nums.push(input.num4);
nums.push(input.num5);
storage.lottery.pot = add(storage.lottery.pot, AMOUNT);
storage.lottery.tickets.push(SENDER, nums);
}

entry addNumbers(nat num1, nat num2, nat num3, nat num4, nat num5){
if (SENDER != storage.oracle) {
throw(string "not authorized to add numbers");
}
let nat[] nums = new list(nat);
nums.push(input.num1);
nums.push(input.num2);
nums.push(input.num3);
nums.push(input.num4);
nums.push(input.num5);
storage.lottery.winnums = nums;
}

entry addWinner(address winner){
if (SENDER != storage.oracle) {
throw(string "not authorized to add numbers");
}
storage.winner = input.winner;
}

entry withdraw(){
if (SENDER != storage.winner){
throw(string "your not the winner nice try");
}
if (storage.ended == bool true) {
throw(string "lottery ended")
}
transfer(storage.winner, storage.lottery.pot);
storage.ended = bool true;
}
`;
var compiled = fi.compile(ficode);
fi.abi.load(compiled.abi);
eztz.node.setProvider("http://192.168.56.101:8732");if (program.serve) {
if (!program.contract || !program.secret) {
console.log("Need to specify contract address")
process.exit(1);
}
var wallet = eztz.crypto.extractKeys(program.secret);
serv(wallet);
}
if (program.buy) {
if (!program.contract || !program.secret) {
console.log("Need to specify contract address")
process.exit(1);
}
var wallet = eztz.crypto.extractKeys(program.secret);
for (var i = 0; i < program.buy; i++) {
buy(wallet)
}
}
if (program.values) {
getStorage();
}
function serv(keys) {
var j = schedule.scheduleJob('* * * * 0', function() {
FindWinner(keys)
});
}
function generateNumbers() {
var numbers = [];
for (var i = 0; i < 5; i++) {
numbers.push(Math.floor(Math.random() * 99) + 0);
}
return numbers;
}
function getStorage() {
var query = "/chains/main/blocks/head/context/contracts/" + program.contract + "/storage"
eztz.node.query(query).then(function(res) {
str = JSON.stringify(res);
str = JSON.stringify(res, null, 4);
console.log(str);
}).catch(function(e) {
console.log(e)
});
}
function FindWinner(keys) {
var query = "/chains/main/blocks/head/context/contracts/" + program.contract + "/storage"
var numbers = generateNumbers();
addNumbers(numbers, keys)
eztz.node.query(query).then(function(res) {
for (var i = 0; i < res.args[0].args[1].args[1][0].args.length; i = i + 2) {
var ticket = {
addr: res.args[0].args[1].args[1][0].args[i].string,
numbers: []
};
console.log(res.args[0].args[1].args[1][0].args[i].string)
res.args[0].args[1].args[1][0].args[i + 1].forEach(element => {
ticket.numbers.push(element.int)
});
console.log(ticket)
if (numbers.sort().toString() == ticket.numbers.sort().toString()) {
addWinner(ticket.addr, keys)
break;
}
}
}).catch(function(e) {
console.log(e)
});
}
function addNumbers(numbers, keys) {
var input = {
num1: numbers[0],
num2: numbers[1],
num3: numbers[2],
num4: numbers[3],
num5: numbers[4]
};
vbytes = fi.abi.entry("addNumbers", input)
vbytes = vbytes.substr(2);
var operation = {
"kind": "transaction",
"amount": "0",
"destination": program.contract,
"fee": 20000,
"gas_limit": 100000,
"parameters": {
"bytes": vbytes
}
};
eztz.rpc.sendOperation(keys.pkh, operation, keys).then(function(res) {
console.log(res);
}).catch(function(e) {
console.log(e)
});
}
function addWinner(winner, keys) {
var input = {
winner: winner
};
vbytes = fi.abi.entry("addWinner", input)
vbytes = vbytes.substr(2);
var operation = {
"kind": "transaction",
"amount": "0",
"destination": program.contract,
"fee": 20000,
"gas_limit": 100000,
"parameters": {
"bytes": vbytes
}
};
eztz.rpc.sendOperation(keys.pkh, operation, keys).then(function(res) {
console.log(res);
}).catch(function(e) {
console.log(e)
});
}
function buy(keys) {
rand = generateNumbers();
var input = {
num1: rand[0],
num2: rand[1],
num3: rand[2],
num4: rand[3],
num5: rand[4]
};
vbytes = fi.abi.entry("BuyTicket", input)
vbytes = vbytes.substr(2);
var operation = {
"kind": "transaction",
"amount": "5000000",
"destination": program.contract,
"fee": 20000,
"gas_limit": 100000,
"parameters": {
"bytes": vbytes
}
};
eztz.rpc.sendOperation(keys.pkh, operation, keys).then(function(res) {
console.log(res);
}).catch(function(e) {
console.log(e)
});
}

Usage

Getting Storage

node main.js -c "KT1ViBphpD3zAf2enzeiRpUic4e8GgeQtp1g" -k "edsk39AgGmPzvzfN63fSHBAd3TEX4ySeDgq3ZCwenEoga2Q7y1KeVp" -v

Buying A Ticket

node main.js -c "KT1ViBphpD3zAf2enzeiRpUic4e8GgeQtp1g" -k "edsk39AgGmPzvzfN63fSHBAd3TEX4ySeDgq3ZCwenEoga2Q7y1KeVp" -b 1

Running the Oracle

node main.js -c "KT1ViBphpD3zAf2enzeiRpUic4e8GgeQtp1g" -k "edsk39AgGmPzvzfN63fSHBAd3TEX4ySeDgq3ZCwenEoga2Q7y1KeVp" -s 1

Conclusion

Congrats on creating a lottery smart contract complete with the required oracle. Although this isn’t a new contract idea by any means, I think it’s a great tutorial that show cases the ideas around an oracle, as well as some of TezTech’s tools including the fi-compiler and eztz js library.

Resources

--

--

BriceAldrich

Software Development, Cryptocurrency, Investing/Personal Finance