Articles
No Results
Article Thumbnail Image

Creating Stateful Algorand Smart Contracts in Python with PyTeal

Earlier this year, we introduced PyTeal, a Python language binding for writing Algorand Smart Contracts. We have now updated PyTeal to be able to create TEAL v2 smart contracts, including stateful smart contracts. If you’re not familiar with stateful smart contracts, check out this overview. PyTeal can be found on GitHub here: https://github.com/algorand/pyteal.

New Features

This new version of PyTeal, v0.6.0, adds many new operations and the ability to store state on the Algorand blockchain. Let’s check it out!

Imperative Programming

One of the big differences between TEAL v1 and v2 is a TEAL program’s ability to not only signal success or failure with a return code but to also produce side effects on data stored in the Algorand blockchain. As a result, we’ve augmented PyTeal’s functional programming interfaces with support for imperative programming. PyTeal v0.6.0 improves programmers’ ability to manage control flow on a granular level, adding:

  • Seq, a new expression for creating a sequence of expressions.
  • Assert, a new expression for asserting that a condition is true.
  • Return, a new expression that immediately exits the program with a return code.
  • Single branch If statements.

Additionally, programmers can manipulate state with new PyTeal operations:

  • Reading and writing to application global state with App.globalPut, App.globalGet, App.globalDel.
  • Reading and writing to account local state with App.localPut, App.localGet, App.localDel.
  • Performing extended reads with App.localGetEx and App.globalGetEx.
  • Reading and writing to temporary scratch slots with ScratchLoad and ScratchStore.

Other Improvements

We’ve also added new features to make writing TEAL v2 applications easier and more powerful:

  • Bitwise arithmetic expressions: &, |, ^, ~.
  • The ability to create byte strings from UTF-8 strings with Bytes.
  • Defining what type of smart contract you are writing with Mode.Signature and Mode.Application.
  • A new way to compile PyTeal programs, compileTeal(program, mode).

The PyTeal documentation contains more information about new and existing features.

A Stateful Example

The biggest new feature in this release of PyTeal is the ability to create stateful smart contracts, which have the ability to read and write key-value pairs on the Algorand blockchain. This allows smart contracts to perform complex tasks, like running auctions, managing crowdfunding campaigns, or hosting a survey or poll. Let’s take a look at a smart contract that implements a basic voting application with PyTeal.

This example has two main parts: the approval_program and clear_state_program functions. In a stateful smart contract, the approval program is responsible for most application calls, including accounts opting into a contract. There are two ways for an account to opt out of a smart contract: closing out, and clearing state. The approval program is used to close out accounts, and it can control if a close out is allowed or not. The clear state program is responsible for clearing an account’s state, and this method of opting out cannot be stopped by the smart contract.

In this example, closing out and clearing state are handled the same way, so the contents of the clear state program is the same as the on_closeout branch of the approval program.

from pyteal import *

def approval_program():
    on_creation = Seq([
        App.globalPut(Bytes("Creator"), Txn.sender()),
        Assert(Txn.application_args.length() == Int(4)),
        App.globalPut(Bytes("RegBegin"), Btoi(Txn.application_args[0])),
        App.globalPut(Bytes("RegEnd"), Btoi(Txn.application_args[1])),
        App.globalPut(Bytes("VoteBegin"), Btoi(Txn.application_args[2])),
        App.globalPut(Bytes("VoteEnd"), Btoi(Txn.application_args[3])),
        Return(Int(1))
    ])

    is_creator = Txn.sender() == App.globalGet(Bytes("Creator"))

    get_vote_of_sender = App.localGetEx(Int(0), App.id(), Bytes("voted"))

    on_closeout = Seq([
        get_vote_of_sender,
        If(And(Global.round() <= App.globalGet(Bytes("VoteEnd")), get_vote_of_sender.hasValue()),
            App.globalPut(get_vote_of_sender.value(), App.globalGet(get_vote_of_sender.value()) - Int(1))
        ),
        Return(Int(1))
    ])

    on_register = Return(And(
        Global.round() >= App.globalGet(Bytes("RegBegin")),
        Global.round() <= App.globalGet(Bytes("RegEnd"))
    ))

    choice = Txn.application_args[1]
    choice_tally = App.globalGet(choice)
    on_vote = Seq([
        Assert(And(
            Global.round() >= App.globalGet(Bytes("VoteBegin")),
            Global.round() <= App.globalGet(Bytes("VoteEnd"))
        )),
        get_vote_of_sender,
        If(get_vote_of_sender.hasValue(),
            Return(Int(0))
        ),
        App.globalPut(choice, choice_tally + Int(1)),
        App.localPut(Int(0), Bytes("voted"), choice),
        Return(Int(1))
    ])

    program = Cond(
        [Txn.application_id() == Int(0), on_creation],
        [Txn.on_completion() == OnComplete.DeleteApplication, Return(is_creator)],
        [Txn.on_completion() == OnComplete.UpdateApplication, Return(is_creator)],
        [Txn.on_completion() == OnComplete.CloseOut, on_closeout],
        [Txn.on_completion() == OnComplete.OptIn, on_register],
        [Txn.application_args[0] == Bytes("vote"), on_vote]
    )

    return program

def clear_state_program():
    get_vote_of_sender = App.localGetEx(Int(0), App.id(), Bytes("voted"))
    program = Seq([
        get_vote_of_sender,
        If(And(Global.round() <= App.globalGet(Bytes("VoteEnd")), get_vote_of_sender.hasValue()),
            App.globalPut(get_vote_of_sender.value(), App.globalGet(get_vote_of_sender.value()) - Int(1))
        ),
        Return(Int(1))
    ])

    return program

with open('vote_approval.teal', 'w') as f:
    compiled = compileTeal(approval_program(), Mode.Application)
    f.write(compiled)

with open('vote_clear_state.teal', 'w') as f:
    compiled = compileTeal(clear_state_program(), Mode.Application)
    f.write(compiled)

This smart contract conducts a poll with multiple choices. Each choice is an arbitrary byte string, and any account is able to register and vote for any single choice.

The program has a configurable registration period defined by the global state keys RegBegin and RegEnd which restricts when accounts can register to vote. There is also a separate configurable voting period defined by the global state keys VotingBegin and VotingEnd which restricts when voting can take place.

An account must register in order to vote. Accounts cannot vote more than once, and if an account opts out of the application before the voting period has concluded, its vote is discarded. The results are visible in the global state of the application, and the winner is the choice with the highest number of votes.

Let’s check out the individual pieces that make up this smart contract.

Main Conditional

program = Cond(
    [Txn.application_id() == Int(0), on_creation],
    [Txn.on_completion() == OnComplete.DeleteApplication, Return(is_creator)],
    [Txn.on_completion() == OnComplete.UpdateApplication, Return(is_creator)],
    [Txn.on_completion() == OnComplete.CloseOut, on_closeout],
    [Txn.on_completion() == OnComplete.OptIn, on_register],
    [Txn.application_args[0] == Bytes("vote"), on_vote]
)

This statement is the heart of the smart contract. Based on how the contract is called, it chooses which operation to run. For example, if Txn.application_id() is 0, then the code from on_creation runs. If Txn.on_completion() is OnComplete.OptIn, then on_register runs. If Txn.application_args[0] is "vote", then on_vote runs. If none of these cases are true, then the program will exit an with error. Let’s look at each of these cases below.

On Creation

on_creation = Seq([
    App.globalPut(Bytes("Creator"), Txn.sender()),
    Assert(Txn.application_args.length() == Int(4)),
    App.globalPut(Bytes("RegBegin"), Btoi(Txn.application_args[0])),
    App.globalPut(Bytes("RegEnd"), Btoi(Txn.application_args[1])),
    App.globalPut(Bytes("VoteBegin"), Btoi(Txn.application_args[2])),
    App.globalPut(Bytes("VoteEnd"), Btoi(Txn.application_args[3])),
    Return(Int(1))
])

This part of the program is responsible for setting up the initial state of the smart contract. It writes the following keys to its global state: Creator, RegBegin, RegEnd, VoteBegin, VoteEnd. The values of these keys are determined by the application call arguments from the Txn.application_args list.

On Register

on_register = Return(And(
    Global.round() >= App.globalGet(Bytes("RegBegin")),
    Global.round() <= App.globalGet(Bytes("RegEnd"))
))

This code runs wherever an account opts into the smart contract. It returns true if the current round is between RegBegin and RegEnd, meaning that registration can only occur during this period.

On Vote

choice = Txn.application_args[1]
choice_tally = App.globalGet(choice)
on_vote = Seq([
    Assert(And(
        Global.round() >= App.globalGet(Bytes("VoteBegin")),
        Global.round() <= App.globalGet(Bytes("VoteEnd"))
    )),
    get_vote_of_sender,
    If(get_vote_of_sender.hasValue(),
        Return(Int(0))
    ),
    App.globalPut(choice, choice_tally + Int(1)),
    App.localPut(Int(0), Bytes("voted"), choice),
    Return(Int(1))
])

This section is responsible for casting an account’s vote. First, on_vote uses an Assert statement to make sure the current round is within VoteBegin and VoteEnd. Then, get_vote_of_sender is used to check whether the sender’s account has the key "voted" in their local state. The variable get_vote_of_sender is defined earlier in the program as App.localGetEx(Int(0), App.id(), Bytes("voted")), which is an extended get operation. In contrast to normal get operations, the extended version lets us check if a key exists instead of returning a default value of 0 for missing keys. If the account has the "voted" key in their local state, they have already voted and the program fails by returning 0.

If the account has not yet voted, the program gets the choice that the sender wants to vote for from Txn.application_args[1]. It also gets choice_tally, the current number of votes that choice has. The code increases the tally by 1 and writes the new value back to global state. Then, it records that the account has successfully voted by writing the choice they voted for to the key "voted" in their account’s local state.

The on_closeout code is similar, except it discards an account’s vote when they opt out of the smart contract.

Conclusion

Stateful smart contracts are powerful tools, and the latest version of PyTeal can make writing them easier. More examples of stateful and stateless contracts in PyTeal are available in the documentation. Additionally, the complete guide for manipulating state with PyTeal is a great resource that can help clear up how state works and what you can do with it.

Install PyTeal today to start creating Algorand Smart Contracts!

Subscribe to the developer newsletter to get the latest news on PyTeal and Algorand’s other developer tools.

python

pyteal

stateful smart contracts

August 26, 2020