
Beginner
Raffle contracts in Archetype
Welcome to our tutorial on Raffle contracts in Archetype, a smart contract framework for the Tezos blockchain network. In this tutorial, we will cover the basics of creating and deploying Raffle contracts using the Archetype framework. We will walk through the process of writing, testing, and deploying a Raffle contract, as well as explain the key features and advantages of using Archetype.
By the end of this tutorial, you will have a solid understanding of how to write, test, and deploy Raffle contracts using the Archetype framework on the Tezos network. You will also learn best practices for developing Raffle contracts in Archetype and how to interact with them. This tutorial is ideal for developers and researchers interested in creating and deploying Raffle contracts on the Tezos blockchain, using the Archetype framework.
This section presents the Archetype version of a raffle contract, inspired by the version presented for other languages (Ligo, Smartpy). The difference is that it uses the timelock feature to secure the winning ticket-picking process.
A raffle is a gambling game, where players buy tickets; a winning ticket is randomly picked and its owner gets the jackpot prize.
The Michelson language does not provide an instruction to generate a random number. We can’t use the current date (value of now
) as a source of randomness either. Indeed, bakers have some control on this value for the blocks they produce, and could therefore influence the result.
Raffle storage
The contract originated with the following parameters:
-
owner
is the address of the contract administrator -
jackpot
is the prize in tez -
ticket_price
is the price in tez of a ticket
archetype raffle( owner : address, jackpot : tez, ticket_price : tez)
State
The contract holds:
-
a state with 3 possible values:
-
Created
is the initial state during which tickets cannot be bought yet -
Initialised
is the state when the administrator initialises the raffle -
Transferred
is the state when prize has been transferred to the winner
-
states = | Created initial | Initialised | Transferred
-
the open date beyond which tickets can be bought, initialized to
none
-
the date beyond which tickets cannot be bought, initialized to
none
variable open_buy : option<date> = none variable close_buy : option<date> = none
The schema below illustrates the periods defined by these dates and the contract’s states:
The contract also holds:
-
The reveal fee, initialized to
none
:variable reveal_fee : option<rational> = none
- The time used to generate the timelocked value of the raffle key (it should be high enough to be compliant with the close date), initialized to
none
:variable chest_time : option<nat> = none
-
A collection that will contain the addresses of all players and their raffle key:
asset player { id : address; locked_raffle_key : chest; (* partial key *) revealed : bool = false; }
-
The raffle key, updated when a player’s partial key is revealed:
variable raffle_key : nat = 0
Entrypoints
initialise
The initialise
entrypoint is called by the contract admin (called “owner“) to set the main raffle parameters:
-
open buy is the date beyond which players can buy the ticket
-
close buy is the date beyond which players cannot buy the ticket
-
chest time is the difficulty to break players’ partial raffle key encryption
-
reveal fee the percentage of ticket price transferred when revealing a player’s raffle key
It requires that:
-
the open and close dates are consistent
-
the reveal fee be equal to or less than 1
-
the transferred amount of tez be equal to the
jackpot
storage value
It transitions from Created
state to Initialised
, and sets the raffle parameters.
transition initialise(ob : date, cb : date, t : nat, rf : rational) { called by owner require { r0 : now <= ob < cb otherwise "INVALID_OPEN_CLOSE_BUY"; r2 : rf <= 1 otherwise "INVALID_REVEAL_FEE"; r3 : transferred = jackpot otherwise "INVALID_AMOUNT" } from Created to Initialised with effect { open_buy := some(ob); close_buy := some(cb); chest_time := some(t); reveal_fee := some(rf) } }
buy
The buy
entrypoint may be called by anyone to buy a ticket. The player must transfer the encrypted value of the partial raffle key, so that the partial key value may be potentially publically known when it comes to declaring the winner ticket.
It requires that:
-
the contract be in
Initialised
state -
the transferred amount of tez be equal to the ticket price
-
the close date not be reached
It records the caller’s address in the player
collection.
entry buy (lrk : chest) { state is Initialised require { r4 : transferred = ticket_price otherwise "INVALID_TICKET_PRICE"; r5 : opt_get(open_buy) < now < opt_get(close_buy) otherwise "RAFFLE_CLOSED" } effect { player.add({ id = caller; locked_raffle_key = lrk }) } }
reveal
The reveal
entry point may be called by anyone to reveal a player’s partial key and contribute to the computation of the raffle key. The caller receives a percentage of the ticket price as a reward.
It requires that:
-
the contract be in
Initialised
state -
the date is valid beyond
close_buy
entry reveal(addr : address, k : chest_key) { state is Initialised require { r6 : opt_get(close_buy) < now otherwise "RAFFLE_OPEN"; r7 : not player[addr].revealed otherwise "PLAYER_ALREADY_REVEALED" } effect { match open_chest(k, player[addr].locked_raffle_key, opt_get(chest_time)) with | left (unlocked) -> match unpack<nat>(unlocked) with | some(partial_key) -> raffle_key += partial_key; player[addr].revealed := true | none -> player.remove(addr) end | right(open_error) -> if open_error then fail("INVALID_CHEST_KEY") else player.remove(addr) end; transfer (opt_get(reveal_fee) * ticket_price) to caller; } }
-
Note that the player
addr
maybe removed in 2 situations:- The chest key opens the chest but is unable to decypher the content; this is the case if for example the chest was not generated with the correct chest time value
- The chest is decyphered properly, but it does not contain an integer value
-
Note at last that in all cases, the caller is rewarded for the chest key when it is valid.
transfer
When all players have been revealed, anyone can call the transfer
entrypoint to transfer the jackpot to the winning ticket. It transitions to Transferred
the state:
transition %transfer() { require { r8: player.select(the.revealed).count() = player.count() otherwise "EXISTS_NOT_REVEALED" } from Initialised to Transferred with effect { transfer balance to player.nth(raffle_key % player.count()); } }