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:

    1. 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
    2. 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());
  }
}

With this, you complete this workshop successfully!!