Building a Decentralized Lottery Contract

Joseph EdohJoseph Edoh
22 min read

We will work together to make a lottery that is controlled by everyone as anyone who has opted in can start and end the lottery, within his allocated time.

Rewards are put in place to encourage more involvement for users who partake in the lottery.

Requirements

You will need the following for the contract development

Background

We’ll be writing a stateful contract utilising the Global and Local states to store our variables. We’ll also be reading the states of other applications and so you’ll need to understand maybeValues. We’ll also be using innerTransactions, Transaction fee pooling, Scratch variables and bit and byte operations.

Setup

This section covers the setup of the environment needed for contract development.

Go to the root of the directory, set up the python environment in a virtual env, and then install all the dependencies.

To get the virtual environment up and running. Run the command in the command line

$python3 -m venv venv

Then activate using

$venv/bin/activate 
# or for windows use
$venv/scripts/activate

Next is to install the Pyteal dependency.

$pip install pyteal

And that wraps it for the setup.

Smart Contract Development

Let’s go over the basic idea of the lottery.

We want to create a lottery that can be operated by anyone i.e. starting and ending etc.
Before a lottery session can be started, The Lottery application is first created, this is where the user inputs the duration in minutes and the price of the tickets for that session.

Now after creation, To start the lottery session, the user has to deposit 1 AlGO as that is the minimum required amount for an application to be able to do transactions on its own.

Next, the player has to opt-in, as before you can use this lottery (due to it being a state-full application) the user must subscribe to it.

After which the player is now able to buy tickets until the lottery duration expires. The tickets would be the ticket position (ticket slot) they hold when compared to the total number of tickets sold.

E.g. Player A buys the 6th ticket, the ticket number of that ticket would be 6.

So the winner would be gotten by generating a random number from the total number of tickets sold.

E.g. The winning ticket after the lottery session is ticket 5 out of a total of 20 tickets sold. The winner would be the player who bought the 5th ticket of that session.

When the session duration expires, and the end lottery function is called by any player (note: Player must have opted into the lottery, and user pays 1 ALGO fee to end lottery) the lottery is subjected to checks to see if it’s valid or not

Minimum of 5 tickets and 2 Players

If Valid, then the prize pool is distributed and the lucky winning ticket position is generated, then the creator, starter and ender are rewarded.

If not valid, then the lottery end time is reset using the duration provided by the lottery creator.

Finally, after the lottery session is over, users can then check to see if they possess the winning ticket and whoever finds it, triggers the function that sends the reward to him.

On the next lottery restart the lottery itself calls the previous lottery and requests the fund allocated for it.

Prize Pool Allocation

  • Lottery Winner (Lucky winner) - 50%

  • Lottery Creator (Player who creates the new lottery) - 5%

  • Lottery Starter (Player who starts the lottery) - 5%

  • Lottery Ender (Player who ends the Lottery) - 5%

  • Next Lottery (Allocation for next lottery) - 35%

So now I guess we have a brief idea of what the main part of the lottery is doing.

Global and Local Variables

All right let’s proceed to define the variables needed for us to run the lottery.

The Lottery contract is a stateful contract, meaning we’ll be utilizing both the Global states and Local states to store our variables.

So in the root of the directory, create a file contracts/lottery.py we’ll be defining a class Lottery.

class Lottery:
    class Global_Variables:  # Total of 16 global ints, 3 global bytes
        lottery_duration = Bytes("DURATION")  # uint64
        lottery_start_time = Bytes("START")  # uint64
        lottery_end_time = Bytes("END")  #  uint64
        total_no_of_players = Bytes("PLAYERS")  # uint64
        total_no_of_tickets = Bytes("TICKETS")  # uint64
        price = Bytes("PRICE")  # uint64
        prize_pool = Bytes("POOL")  # uint64
        lottery_status = Bytes("STATUS") # uint64
        winning_ticket = Bytes("WINNINGTICKET")  # uint64
        starter = Bytes("STARTER")  # Bytes
        ender = Bytes("ENDER")  # Bytes
        winner = Bytes("WINNER")  # Bytes
        winner_reward = Bytes("WINNERREWARD")   # uint64
        next_lottery_fund = Bytes("NEXTLOTTERY") # uint64
        prev_app = Bytes("PREVAPP") # uint64

        # pseudo random generator parameters (linear congruential generator)
        multiplier = Bytes("a")  #  uint64
        increment = Bytes("c")  # uint64
        modulus = Bytes("m")  # uint64
        seed = Bytes("x")  #  uint64

    class Local_Variables:  # Total of 3 local ints, 13 local bytes
        id = Bytes("ID")  # uint64
        no_of_tickets = Bytes("TICKETCOUNT")  # uint64
        is_winner = Bytes("ISWINNER") # uint64

    class App_Methods:
        start_lottery = Bytes("start")
        buy_ticket = Bytes("buy")
        end_lottery = Bytes("end")
        check_if_winner = Bytes("check")
        fund_next_lottery = Bytes("fund")

Let’s go over each variable and their use

Global Variables

These variables can be accessed by anyone who reads the application information. We can store up to 64 key-value pairs in a contract’s global state. Here we’re using 19 key-value pairs.

  1. lottery_duration - Indicates how long the lottery is supposed to last

  2. lottery_start_time - The time when the lottery session is initialized

  3. lottery_end_time - The time when the lottery ends

  4. total_no_of_players - Total number of accounts that opts into the application

  5. total_no_of_tickets - Total number of tickets sol

  6. price - Price of ticket

  7. prize_pool - Total amount allocated from ticket sales

  8. lottery_status - Status of Lottery: 0 - not started, 1 - active, 2 - lottery ended

  9. winning_ticket - Winning number (Ticket position)

  10. starter - Address of the player who starts the lottery

  11. ender - Address of player who ends the lottery

  12. winner - Address of lottery winner

  13. winner_reward - Amount Allocated for the winner

  14. next_lottery_fund - Amount allocated for the next lottery

  15. prev_app - Previous Lottery Application

The Random number Variables

For this tutorial, we are using an implementation of a linear congruential recurrence relation.

This relation helps to generate a pseudo-random number, which is a number that appears to be random but has been generated by a deterministic process.

The relation takes the form (a*x + c) % m, and as we’ve listed them in our global variables;

  • a stands for the multiplier

  • c stands for the increment

  • m stands for the modulus

  • x stands for the seed

So we’ll be using the seed value to pick our random number.

Local Variables (Local States)

These are that are stored in a unique context for each account that opts into the lottery. We can store up to 16 key-value pairs per user who opts into the contract.
- id - Player assigned id after opting to application
- no_of_tickets - Number of tickets bought by a player
- is_winner - value showing if the player is the winner or not. set to 1 if not the winner, and set to 2 if the winner

Currently, we’ve filled up 3 of the key-value pairs while the remaining 13 slots will be used to store the ticket entries of the player. The keys we’ll be using

To store the ticket entries we’ll be using bit operators, the reason for this is that we’re limited to the storage 128 bytes for each key plus value, so by using bit operators we can get storage for up to 13000 ticket entries in the local state. This is achieved by creating byte arrays that store up to 125 bytes, now this gives us access to 125 x 8 bits i.e 1000 bits. Now if use key indexes from 0 - 12 for the remaining 13 key-value pairs we can have up to 13000 bits of storage i.e. 13000 ticket entries. As stated above, this idea was gotten from this tutorial Track 65000 tickets arrays in Algorand. We’ll also be using the subroutine called convert_uint_to_bytes from the tutorial. All you need to know is that it takes in a uint and converts it to a string e.g. 28 to “28”. If you are interested in the inner workings, you can check out its source code here

So basically for every lottery session, we have 13000 ticket slots to fill and when a player buys a ticket, the player fills that ticket slot with the bit value 1 i.e. in the local state. Once all slots are filled ticket sales are closed, irrespective of the set lottery end time and when the winning ticket slot is gotten, players can check to see if they filled the winning slot and the lucky winner gets the reward.

APP Methods (Variables)

These variables are just placeholders for the NoOp methods calls that can be made to the application.

That’s it for the overview of all the variables we’ll be using in the lottery.

Approval and Clear programs

Conditions for the approval program and clear program.

  1. If the application id of the app being called is 0, and if it is, it triggers the create lottery function

  2. If not it checks if the call is marked as Oncomplete.DeleteApplication call, if it deletes the lottery.

  3. If not it checks if the call is marked asOnComplete.OptIn call, which triggers the join lottery method

  4. The rest checks are for NoOp calls wherein it checks to see if the first application argument matches the placeholders and then triggers the method attached to it.

def approval_program(self):
    return Cond(
        [
            Txn.application_id() == Int(0),
            self.create_new_lottery(),
        ],
        [
            Txn.on_completion() == OnComplete.DeleteApplication,
            self.application_deletion(),
        ],
        [
            Txn.on_completion() == OnComplete.OptIn,
            self.join_lottery(),
        ],
        [
            Txn.application_args[0] == self.AppMethods.start_lottery,
            self.start_lottery(),
        ],
        [
            Txn.application_args[0] == self.AppMethods.buy_ticket,
            self.buy_ticket(),
        ],
        [
            Txn.application_args[0] == self.AppMethods.end_lottery,
            self.end_lottery(),
        ],
        [
            Txn.application_args[0] == self.AppMethods.check_if_winner,
            self.check_if_winner(),
        ],
        [
            Txn.application_args[0] == self.AppMethods.fund_next_lottery,
            self.fund_next_lottery(),
        ],
    )

def clear_program(self):
    return Return(Int(1))

Next, We’ll be defining all the methods.

Contract Entry Points

Create Lottery

This handles the creation of the lottery application. Here the creator passes in the duration (in seconds) and price of the lottery (in micro Algos) as the Transaction application arguments. It goes on to set the default values of the global state variables.

def create_new_lottery(self):

In the return sequence first, we run the following checks
1. Check that the note attached to the transaction is algorandlottery:uv1, adding a note argument allows us to query the transaction history using the note as a filter which then returns a record of all transactions with that note, allowing us to access our previous lottery applications. More information on how to create notes can be found here
2. Check that the number of arguments attached to the transaction is 2.
3. The values of the arguments the lottery duration and the ticket price are greater than 0.
4. And that Txn.applications array is not empty. This array contains the ID of the last lottery session application ID before this one and if this lottery session is the first, it just contains the ID 0.

    Assert(
        And(
            Txn.note() == Bytes("algolottery:uv01"),
            Txn.application_args.length() == Int(2),
            Btoi(Txn.application_args[0]) != Int(0),
            Btoi(Txn.application_args[1]) > Int(0),
            Txn.applications.length() == Int(1)
        )
    ),

If all checks succeed, the rest of the variables are set to 0 except the random number generator parameters whose initial values are set similarly to those of the ZX Spectrum computer and the prev_app which is set to the ID contained in the Txn.applications array using the App.globalPut command. Then the Approve command exits the program and marks the execution as successful.

# store variables
    App.globalPut(self.Global_Variables.prev_app,
        Txn.applications[1]),
    App.globalPut(
        self.Global_Variables.lottery_duration, Btoi(Txn.application_args[0]),
        ),
    App.globalPut(
        self.Global_Variables.price, Btoi(Txn.application_args[1])
        ),
    App.globalPut(
        self.Global_Variables.total_no_of_players, Int(0)),
    App.globalPut(
        self.Global_Variables.total_no_of_tickets, Int(0)),
    App.globalPut(self.Global_Variables.prize_pool, Int(0)),
    App.globalPut(self.Global_Variables.lottery_status, Int(0)),

    # init random gen parameters with zxsprectum values
    App.globalPut(self.Global_Variables.multiplier, Int(75)),
    App.globalPut(self.Global_Variables.increment, Int(74)),
    App.globalPut(
        self.Global_Variables.modulus, Int(65537)
        ),  # (1<<16)+1 or 2^16+1
    App.globalPut(self.Global_Variables.seed, Int(28652)),
    Approve(),

Start Lottery

To start the lottery we need to send a minimum of 1 Algo to the lottery address to make it an active account as it would be carrying out transactions. Whoever carries out this transaction would be stored in the sender address global state and would have a percentage of the prize pool as an incentive for starting the lottery.

But understand that this starts lottery method is a `noOp transaction and can not transfer algo so we would also need to attach a payment transaction that moves the algo from the player's account to the lottery’s account.

We can do this by using group transactions, so in this group transaction, the first transaction would be the noOp, and the second would be the payment transaction. If any fails the whole group transaction is rejected.

def start_lottery(self):

First, we define 3 variables to be used in the method:
1. lottery duration
2. previous app id
3. The last one is a boolean variable that checks if the value of the previous application ID stored in the global state is not equal to 0, and that the value of the ID passed in from the Txn.applications array is equal to the value of the ID stored in the application’s global state. Recall that this value was stored in the global state when the application was created.

    lottery_duration = App.globalGet(
        self.Global_Variables.lottery_duration)

    prev_app = App.globalGet(self.Global_Variables.prev_app)

    valid_app_id = And(
        prev_app != Int(0),
        Txn.applications[1] == prev_app,
    )

Then in the return sequence, we run validity checks:
1. The Txn.applications array is not empty. This array contains the ID of the last lottery session application ID before this one and if this lottery session is the first, it just contains the ID 0.
2. The transaction made is a group transaction containing two separate transactions and the noOp call is ahead.
3. The details of the Payment transaction, ensuring that it is a payment transaction, that the receiver is the lottery address, and that the amount sent is greater than equal to 1 Algo (1000000 micro Algo).

    Assert(
        And(
            Txn.applications.length() == Int(1),
            Global.group_size() == Int(2),
            Txn.group_index() == Int(0),
            Gtxn[1].type_enum() == TxnType.Payment,
            Gtxn[1].receiver() == Global.current_application_address(),
            Gtxn[1].close_remainder_to() == Global.zero_address(),
            Gtxn[1].amount() >= Int(1000000),
            Gtxn[1].sender() == Gtxn[0].sender(),
        )
    )

If all the checks succeed then we update the value of the start time and end time using the Global.latest_timestamp value which is the latest confirmed block UNIX timestamp. Then stores the address of the lottery starter, and updates the lottery status to 1.

    App.globalPut(
        self.Global_Variables.lottery_start_time, 
        Global.latest_timestamp()
    ),
    App.globalPut(
        self.Global_Variables.lottery_end_time,
        Global.latest_timestamp() + lottery_duration,
    ),
    App.globalPut(self.Global_Variables.starter, Txn.sender()),
    ),
    App.globalPut(self.Global_Variables.status, Int(1)),

After that, we check if the app id is valid and if true:
1. Checks for enough fees for transaction fee pooling.
2. Next it calls the retrieve_funds, get_seed_from_prev_lottery and update_prize_pool methods

    If(valid_app_id).Then(
        Seq(
            [
                Assert(Txn.fee() >= Global.min_txn_fee() * Int(2)),
                self.retrieve_funds(Txn.applications[1]),
                self.get_seed_from_prev_lottery(
                    Txn.applications[1]),
                self.update_prize_pool(Txn.applications[1]),
            ]
        )
    ),

    Approve(),
1. Retrieve Funds method

The retrieve_funds method is used to create an inner transaction, that does an application call to the previous lottery application using its app ID. To read more on inner transactions link here.

def retrieve_funds(self, prev_lottery: Expr):
    return Seq(
        # initiate transaction to trigger fund sends
        InnerTxnBuilder.Begin(),
        InnerTxnBuilder.SetFields(
            {
                TxnField.type_enum: TxnType.ApplicationCall,
                TxnField.application_id: prev_lottery,
                TxnField.on_completion: OnComplete.NoOp,
                TxnField.application_args: [self.AppMethods.fund_next_lottery],
                TxnField.accounts: [Global.current_application_address()],
                TxnField.fee: Int(0),
            }
        ),
        InnerTxnBuilder.Submit(),
    )

This application call triggers another method in the previous lottery application called fund_next_lottery as defined in the application_args array field, wherein we pass in the fund next lottery bytes variable.
Other things to note about the Inner Txn Fields:
1. The address of the current lottery application is passed into the accounts array field using the Global.current_application_address.
2. The Txn.Fee for the Inner Txn is set to 0, to allow the transaction pooling to take effect.

2. Fund Next Lottery method

Next this fund_next_lottery method transfers the amount allocated for the next lottery out to the lottery application that calls it.

def fund_next_lottery(self):
    prize_pool_amount = ScratchVar(TealType.uint64)
    next_lottery_fund = ScratchVar(TealType.uint64)
    return Seq(
        [
            Assert(
                And(
                    Txn.accounts.length() == Int(1),
                    App.globalGet(self.Global_Variables.status) == Int(2),
                    App.globalGet(
                        self.Global_Variables.prizepool) > Int(1000000),
                    Balance(Global.current_application_address()
                            ) > Int(1000000),
                )
            ),
            prize_pool_amount.store(App.globalGet(
                self.Global_Variables.prizepool)),
            next_lottery_fund.store(
                App.globalGet(self.Global_Variables.next_lottery_fund)
            ),
            send_funds(Txn.accounts[1], next_lottery_fund.load()),
            App.globalPut(
                self.Global_Variables.prizepool,
                (prize_pool_amount.load() - next_lottery_fund.load()),
            ),
            App.globalPut(self.Global_Variables.status, Int(3)),
            Approve(),
        ]
    )

Breakdown of this method:
1. First it creates two scratch var variables of type uint64.
2. Then it runs the validity checks The accounts array of the transaction contains 1 account The status lottery application is set to 2. The prize pool contains greater than 1 algo (1000000 microAlgo) The balance of the lottery application is also greater than 1 algo (1000000 microAlgo)
3. Then store the amount of microAlgos in the prize pool and the amount allocated for the next lottery in scratch vars.
4. Next it calls the send_funds method, another inner Txn but this time is a payment transaction that sends the funds to the new lottery.

def send_funds(account: Expr, amount: Expr):
    return Seq(
        InnerTxnBuilder.Begin(),
        InnerTxnBuilder.SetFields(
            {
                TxnField.type_enum: TxnType.Payment,
                TxnField.receiver: account,
                TxnField.amount: amount,
            }
        ),
        InnerTxnBuilder.Submit(),
    )

After the algo transfer is done, the amount in the prize pool is updated, and then the lottery status is set to 3.

And that’s it for the retrieve funds method.

3. Get Seed from Previous Lottery method

Since we are using a deterministic relation to get our random numbers, it won’t be ideal to use the same initial seed for every lottery, so we get the last value of the seed from the previous lottery application.

def get_seed_from_prev_lottery(self, prev_lottery: Expr):
    get_seed = App.globalGetEx(prev_lottery, Bytes("x"))
    return Seq(
        [
            get_seed,
            If(get_seed.hasValue()).Then(
                Seq(
                    App.globalPut(self.Global_Variables.seed,
                        get_seed.value()),
                )
            ),
        ]
    )

Breakdown of this method:
1. create a variable get_seed, where we get the seed from the previous lottery.
2. In the return sequence, we get the seed, then check if it has a value and update the value of the seed in the current application.

4. Update Prize Pool method

This method updates the value of the new lottery’s prize pool after the funds have been transferred from the previous lottery. It follows the same sequence as the get_seed_from_prev_lottery method.

def update_prize_pool(self, prev_lottery: Expr):
    lottery_pool = App.globalGetEx(prev_lottery, Bytes("NEXTLOTTERY"))
    return Seq([
        lottery_pool,
            If(lottery_pool.hasValue()).Then(
                Seq(
                    App.globalPut(self.Global_Variables.prizepool,
                        lottery_pool.value())
                )
            ),
        ]
    )

Join Lottery

For players to be able to participate in the lottery they need to opt-in.

def join_lottery(self):

First, we create a scratch var no_of_players of type uint64.

    no_of_players = ScratchVar(TealType.uint64)

Then in the return sequence, we run validity checks:
1. The current timestamp is less than the set lottery end time
2. The lottery status is set to 1
3. The number of tickets bought is less than 13000 (max no of slots to be filled)

    Assert(
        And(
            Global.latest_timestamp() < App.globalGet(
                self.Global_Variables.lottery_end_time),
            App.globalGet(self.Global_Variables.status) == Int(1),
            App.globalGet(
                self.Global_Variables.total_no_of_tickets) < Int(13000)
        )
    ),

If all checks pass;
1. We set the player’s ID to be the current number of players plus 1
2. Then update the total number of players by 1
3. and set the player’s no of tickets and winner status to 0.

    no_of_players.store(
        App.globalGet(self.Global_Variables.total_no_of_players)
    ),
    App.localPut(
        Txn.accounts[0], self.Local_Variables.id, (no_of_players.load(
        ) + Int(1))
    ),
    App.globalPut(
        self.Global_Variables.total_no_of_players,
        (no_of_players.load() + Int(1)),
    ),
    App.localPut(
        Txn.accounts[0], self.Local_Variables.no_of_tickets, Int(0)),
    App.localPut(
        Txn.accounts[0], self.Local_Variables.is_winner, Int(0)),

    Approve(),

Buy Ticket

This method allows the players to fill their ticket slots. Note that this transaction would be a group transaction containing two transactions, the first being the noOp call and the second the payment transaction containing the algos.

def buy_ticket(self):

First, we define the helper variables.
The storage variables.

    old_total_tickets = ScratchVar(TealType.uint64)
    new_total_tickets = ScratchVar(TealType.uint64)
    player_existing_ticket_count = ScratchVar(TealType.uint64)
    prizepool = ScratchVar(TealType.uint64)
    no_of_tickets = Txn.application_args[1]
    ticket_array_key_as_string = ScratchVar(TealType.bytes)

We then calculate the key, byte and bit indexes for the ticket positions using the logic below. Recall our key indexes, which range from 0 - 12, byte indexes range from 0 - 125 and bit indexes from 0 - 7.

    ticket_array_key = ScratchVar(TealType.uint64)
    ticket_byte_posn = ScratchVar(TealType.uint64)
    ticket_bit_posn = ScratchVar(TealType.uint64)
    ticket_posn = ScratchVar(TealType.uint64)

    store_key_byte_bit = Seq(
        [
            Seq([
                ticket_array_key.store(
                    ticket_posn.load() / Int(1000)),
                ticket_byte_posn.store(
                    (ticket_posn.load() % Int(1000)) / Int(8)),
                ticket_bit_posn.store(
                    (ticket_posn.load() % Int(1000)) % Int(8))
            ])
        ]
    )

Next, we lazily initialize the array, according to Gidon Katten from Track 65000 tickets arrays in algorand this is important because smart contracts have an opcode cost limit of 700 so it would be far too expensive to initialize the array as a whole. Therefore we check to see if the key index already has a value in the player’s local state before filling that ticket slot, and if not, we initialize the array with 125 bytes.

    ticket_array_key_has_value = App.localGetEx(
        Txn.accounts[0],
        Txn.applications[0],
        ticket_array_key_as_string.load()
    )

    initialise_array = If(
        Not(ticket_array_key_has_value.hasValue()),
        App.localPut(
            Txn.accounts[0],
            ticket_array_key_as_string.load(),
            Bytes(
                "base16",
                "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
            ),
        ),
    )

    curr_ticket_array = App.localGet(
        Txn.accounts[0], ticket_array_key_as_string.load())

Then in the return sequence, we run validity checks:
1. The player has opted in.
2. The number of transactions within the group transaction is 2, and the noOp call is ahead of the payment transaction
3. The number of arguments passed is 2 (the buy tag and the no of tickets)
4. The current timestamp is less than the set lottery end time
5. The lottery status is set to 1
6. The player hasn’t bought over 1000 tickets. (Just for sanity reasons)
7. The number of tickets bought is less than 13000 (max no of slots to be filled)
8. The amount of tickets the player wants to buy is not more than 100 tickets at a time.
9. The details of the Payment transaction, ensuring that it is a payment transaction, that the receiver is the lottery address, and that the amount sent is equal to the estimated amount for the number of tickets.

    Assert(
        And(
            App.optedIn(Txn.accounts[0], Txn.applications[0]),
            Global.group_size() == Int(2),
            Txn.group_index() == Int(0),
            Txn.application_args.length() == Int(2),

            Global.latest_timestamp() < App.globalGet(
                self.Global_Variables.lottery_end_time),
            App.globalGet(self.Global_Variables.status) == Int(1),

            App.localGet(
                Txn.accounts[0], self.Local_Variables.no_of_tickets) <= Int(1000),
            App.globalGet(
                self.Global_Variables.total_no_of_tickets) < Int(13000),
            Btoi(no_of_tickets) <= Int(100),

            Gtxn[1].type_enum() == TxnType.Payment,
            Gtxn[1].receiver() == Global.current_application_address(),
            Gtxn[1].close_remainder_to() == Global.zero_address(),
            Gtxn[1].amount()
            == App.globalGet(self.Global_Variables.price)
            * Btoi(no_of_tickets),
            Gtxn[1].sender() == Gtxn[0].sender(),
        )

If all checks succeed.
It gets the number of tickets sold, and the number of tickets the player has already bought.

    old_total_tickets.store(
        App.globalGet(self.Global_Variables.total_no_of_tickets)
    ),

    new_total_tickets.store(
        old_total_tickets.load() + Btoi(no_of_tickets)),

    player_existing_ticket_count.store(
        App.localGet(
            Txn.accounts[0], self.Local_Variables.no_of_tickets)
    ),

The next line starts a loop to fill the slots for the ticket positions the player has bought. Say the player buys 5 tickets, it fills 5 slots starting from the last ticket slot bought.
To get the positions of the slots to be filled, the store_key_byte_bit is used to get the key, byte and bit indexes. Recall our key indexes, which range from 0 - 12, byte indexes range from 0 - 125 and bit indexes from 0 - 7.

Next, we convert the gotten key index from uint to bytes using convert_uint_to_bytes. To know how this subroutine works, check out the source code here
Then finally we fill the ticket position, We get the current ticket array, and then the byte within it, and then set the particular bit position with bit value 1.

    For(
        ticket_posn.store(old_total_tickets.load()),
        ticket_posn.load() < new_total_tickets.load(),
        ticket_posn.store(
            ticket_posn.load() + Int(1)),
    ).Do(
        store_key_byte_bit,

        ticket_array_key_as_string.store(
            convert_uint_to_bytes(Ticket.ticket_array_key.load())),

        ticket_array_key_has_value,

        initialise_array,

        App.localPut(
            Txn.accounts[0],
            ticket_array_key_as_string.load(),
            SetByte(
                curr_ticket_array,
                ticket_byte_posn.load(),
                SetBit(
                    GetByte(
                        curr_ticket_array, ticket_byte_posn.load()
                    ),
                    ticket_bit_posn.load(),
                    Int(1)
                ),
            ),
        ),
    ),

Next update the player’s ticket count, the total number of tickets sold and the prize pool.

    App.localPut(
        Txn.accounts[0],
        self.Local_Variables.no_of_tickets,
        (player_existing_ticket_count.load() + Btoi(no_of_tickets)),
    ),

    App.globalPut(
        self.Global_Variables.total_no_of_tickets, new_total_tickets.load()
    ),

    prizepool.store(App.globalGet(
        self.Global_Variables.prizepool)),
    App.globalPut(
        self.Global_Variables.prizepool,
        (prizepool.load() + Gtxn[1].amount()),
    ),

Then generate a new seed using the get_rand_number method.

    self.get_rand_number(),

    Approve(),
Random Generator Method

The method just calculates a new seed value and updates it to the contract state. x = (a*x + c) % m

def get_rand_number(self):
    x = App.globalGet(self.Global_Variables.seed)
    a = App.globalGet(self.Global_Variables.multiplier)
    c = App.globalGet(self.Global_Variables.increment)
    m = App.globalGet(self.Global_Variables.modulus)

    random_number = ((a * x) + c) % m

    return Seq([App.globalPut(self.Global_Variables.seed, random_number)])

End Lottery

After the lottery end time is reached. This method ends the lottery, and just like the start method, the player who calls the method is also given a percentage of the prize pool. The method checks if the lottery is valid for more than 2 players and 5 tickets bought, then sets the lottery as ended, if not it just restarts the lottery using the duration set on lottery creation.

def end_lottery(self):

First, we define the helper variables
Boolean check to see if the lottery is valid and a placeholder for lottery duration

    lottery_valid = And(
        App.globalGet(self.Global_Variables.total_no_of_tickets) >= Int(5),
        App.globalGet(self.Global_Variables.total_no_of_players) >= Int(2),
    ),
    lottery_duration = ScratchVar(TealType.uint64)

Logic to get the winning ticket from the seed

    win_ticket = App.globalGet(self.Global_Variables.seed) % App.globalGet(
        self.Global_Variables.total_no_of_tickets
    )

Logic to calculate rewards according to the prize pool allocation for the categories.

    calc_starter_n_ender_n_creator_rewards = (
        App.globalGet(self.Global_Variables.prizepool) * Int(5) / Int(100)
    )

    calc_winner_reward = (
        App.globalGet(self.Global_Variables.prizepool) * Int(50) / Int(100)
    )

    calc_next_lottery_fund = (
        App.globalGet(self.Global_Variables.prizepool) * Int(35) / Int(100)
    )

    rewards = ScratchVar(TealType.uint64)

    prize_pool_amount = ScratchVar(TealType.uint64)

Then in the return sequence, we run validity checks:
1. Check if the player has opted in or if the lottery session is not valid. (A boolean check returns 0 and returns 1 if true)
2. Check that the current timestamp is greater than the lottery end time.

    Assert(
        And(
            Or(
                App.optedIn(Txn.accounts[0], Txn.applications[0]),
                lottery_valid == Int(0)
            ),
            Global.latest_timestamp() > App.globalGet(
                self.Global_Variables.lottery_end_time)
        )
    ),

If checks pass then we check if the lottery is valid.
If the lottery is not we restart the lottery using the set lottery duration on app creation.

    If(lottery_valid)
    .Then(
        Seq(
            Approve()
        )
    )
    .Else(
        Seq(
            lottery_duration.store(
                App.globalGet(
                    self.Global_Variables.lottery_duration)
            ),
            App.globalPut(
                self.Global_Variables.lottery_end_time,
                (Global.latest_timestamp() + lottery_duration.load()),
            ),
        )
    ),
Approve(),

If the lottery session is valid we run another validity check, to see that the transaction sender attached a payment transaction containing 1 or more algo and that the addresses of the starter, ender and creator addresses are passed in the txn.accounts array.

    Assert(
        And(
            Global.group_size() == Int(2),
            Txn.group_index() == Int(0),
            Txn.accounts.length() == Int(3),
            Gtxn[1].type_enum() == TxnType.Payment,
            Gtxn[1].receiver(
            ) == Global.current_application_address(),
            Gtxn[1].close_remainder_to(
            ) == Global.zero_address(),
            Gtxn[1].amount() >= Int(1000000),
        )

If the second check passes, we get a new seed value.

    self.get_rand_number(),

then calculate the winning ticket position and allocations for the winner, creator, starter, ender and next lottery.

    App.globalPut(
        self.Global_Variables.winningTicket, win_ticket),

    rewards.store(calc_starter_n_ender_n_creator_rewards),

    App.globalPut(
        self.Global_Variables.winner_reward, calc_winner_reward
    ),

    App.globalPut(
        self.Global_Variables.next_lottery_fund,
        calc_next_lottery_fund,
    ),

Then we send the rewards to the starter, ender and creator, then finally update the amount left in the prize pool and set the lottery status to 2.

    send_funds(Txn.accounts[1], rewards.load()),
    send_funds(Txn.accounts[2], rewards.load()),
    send_funds(Txn.accounts[3], rewards.load()),

    prize_pool_amount.store(
        App.globalGet(self.Global_Variables.prizepool)
    ),
    App.globalPut(
        self.Global_Variables.prizepool,
        (prize_pool_amount.load() - (rewards.load() * Int(3))),
    ),
    App.globalPut(self.Global_Variables.status, Int(2)),

    Approve(),

Check if Winner

Players check to see if they filled the winning slot and send the rewards to themselves.

def check_if_winner(self):

First, we define the helper variables,

    is_winner = ScratchVar(TealType.uint64)
    winners_reward = ScratchVar(TealType.uint64)

Then in the return sequence, we run validity checks:
1. Check if the player has opted in.
2. Check that the current timestamp is greater than the lottery end time.
3. Check that the lottery status is set to 2
4. Check that the player has not called this function already i.e. that the isWinner status has not been updated.

    Assert(
        And(
            App.optedIn(Txn.accounts[0], Txn.applications[0]),
            Global.latest_timestamp() > App.globalGet(
                self.Global_Variables.lottery_end_time),
            App.globalGet(self.Global_Variables.status) == Int(2),

            App.localGet(
                Txn.accounts[0], self.Local_Variables.is_winner) == Int(0),
        )
    ),

If checks pass;
1. We use the store_key_byte_bit to get the key, byte and bit indexes of the winning ticket.
2. Next it converts the gotten key index from uint to bytes using convert_uint_to_bytes.
3. Next we lazily initialize the array and check for the bit value of the winning ticket position.

    ticket_posn.store(
        App.globalGet(self.Global_Variables.winning_ticket)
    ),

    store_key_byte_bit,

    ticket_array_key_as_string.store(
        convert_uint_to_bytes(ticket_array_key.load())),

    ticket_array_key_has_value,

    initialise_array,

    is_winner.store(
        GetBit(
            GetByte(
                curr_ticket_array,
                ticket_byte_posn.load()
            ),
            ticket_bit_posn.load()
        )
    ),

Then we check for the player's bit value;

  1. if the bit value is 1, the player is the winner and the winner’s reward is sent to the player and the winner status is updated to 2

  2. if the bit value is 0, the winner status is set to 1.

    If(is_winner.load() == Int(1)).Then(
        Seq(
            winners_reward.store(
                App.globalGet(self.Global_Variables.winner_reward)
            ),
            send_funds(
                Txn.accounts[0], winners_reward.load()),
            App.globalPut(
                self.Global_Variables.winner, Txn.sender())
        )
    ),
    App.localPut(Txn.accounts[0], self.Local_Variables.is_winner,
                    is_winner.load() + Int(1)),
    Approve(),

Conclusion

We have learned how to write a lottery contract that uses pseudo-random numbers to get their lucky winners. A cool addition you can add on your own is to try to encrypt the values of the pseudo-random number parameters, which should reduce possible predictions as currently, the values are open on the blockchain, and some smart people could try to predict where the next winning ticket could be.

Anyways that’s all for now.

You can access the source code here as well as a working react demo here algorand lottery

0
Subscribe to my newsletter

Read articles from Joseph Edoh directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Joseph Edoh
Joseph Edoh