Welcome to another episode of my pyteal article👋.
To have an overview of what pyteal is, you can checkout my previous article here
The ability to design stateful smart contracts, which can read and write key-value pairs on the Algorand blockchain, is the most significant new feature in this PyTeal release.
This enables smart contracts to carry out complex activities such as auctions, crowdfunding campaigns, and survey or poll hosting.
Today, Let's take a look at a smart contract that uses PyTeal to construct a basic voting application.
The full code can be found in my repository here
What the Application Does
This contract make it simple to register and vote arbitrary decisions on the Algorand blockchain.
🔒 Voting power dependent on user's ownerships of a given Algorand Standard Asset.
🕐 Decisions divided in two distinct time-phases: First, Registration, then, Voting.
Contracts
Contract is located in the root directory
contract.py contains registering and voting logic
Running the file will compile the contract to .teal
in compiled/
:
$ python contract.py
Voting
This is the voting contract. 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.
To vote, you must first create an account. Accounts can only vote once, and votes are deleted if an account opts out of the application before the voting time ends. The results are shown in the application's global state, and the winner is the option with the most votes.
Walkthrough 🤓
Now, Let’s go through the smart contract codes...
#contract.py
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
if __name__ == "__main__":
with open("compiled/"+ "vote_approval.teal", "w") as f:
compiled = compileTeal(approval_program(), mode=Mode.Application, version=2)
f.write(compiled)
with open("compiled/"+"vote_clear_state.teal", "w") as f:
compiled = compileTeal(clear_state_program(), mode=Mode.Application, version=2)
f.write(compiled)
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.
Running this python code will produce the following vote_approval teal file in the compiled folder:
#pragma version 2
txn ApplicationID
int 0
==
bnz main_l44
txn OnCompletion
int DeleteApplication
==
bnz main_l43
txn OnCompletion
int UpdateApplication
==
bnz main_l42
txn OnCompletion
int OptIn
==
bnz main_l37
txna ApplicationArgs 0
byte "vote"
==
bnz main_l25
txna ApplicationArgs 0
byte "add_options"
==
bnz main_l7
err
main_l7:
global LatestTimestamp
byte "voting_start_time"
app_global_get
>
bnz main_l24
txna ApplicationArgs 1
byte "NULL_OPTION"
!=
bnz main_l23
int 1
return
main_l10:
txna ApplicationArgs 2
byte "NULL_OPTION"
!=
bnz main_l22
int 1
return
main_l12:
txna ApplicationArgs 3
byte "NULL_OPTION"
!=
bnz main_l21
int 1
return
main_l14:
txna ApplicationArgs 4
byte "NULL_OPTION"
!=
bnz main_l20
int 1
return
main_l16:
txna ApplicationArgs 5
byte "NULL_OPTION"
!=
bnz main_l19
int 1
return
main_l18:
int 1
return
main_l19:
byte "option_"
txna ApplicationArgs 5
concat
int 4294967296
app_global_put
b main_l18
main_l20:
byte "option_"
txna ApplicationArgs 4
concat
int 4294967296
app_global_put
b main_l16
main_l21:
byte "option_"
txna ApplicationArgs 3
concat
int 4294967296
app_global_put
b main_l14
main_l22:
byte "option_"
txna ApplicationArgs 2
concat
int 4294967296
app_global_put
b main_l12
main_l23:
byte "option_"
txna ApplicationArgs 1
concat
int 4294967296
app_global_put
b main_l10
main_l24:
int 0
return
main_l25:
global LatestTimestamp
byte "voting_start_time"
app_global_get
<
bnz main_l36
global LatestTimestamp
byte "voting_end_time"
app_global_get
>
bnz main_l35
int 0
byte "option_"
txna ApplicationArgs 1
concat
app_global_get_ex
store 2
store 3
load 2
bnz main_l29
int 0
return
main_l29:
int 0
byte "QVoteDecisionCredits"
int 0
byte "QVoteDecisionCredits"
app_local_get
txna ApplicationArgs 2
btoi
txna ApplicationArgs 2
btoi
*
-
app_local_put
int 0
byte "QVoteDecisionCredits"
app_local_get
int 0
>=
bnz main_l32
int 0
return
main_l31:
int 1
return
main_l32:
txna ApplicationArgs 3
byte "-"
==
bnz main_l34
byte "option_"
txna ApplicationArgs 1
concat
load 3
txna ApplicationArgs 2
btoi
+
app_global_put
b main_l31
main_l34:
byte "option_"
txna ApplicationArgs 1
concat
load 3
txna ApplicationArgs 2
btoi
-
app_global_put
b main_l31
main_l35:
int 0
return
main_l36:
int 0
return
main_l37:
global LatestTimestamp
byte "voting_start_time"
app_global_get
>
bnz main_l41
int 0
byte "asset_id"
app_global_get
asset_holding_get AssetBalance
store 0
store 1
load 0
bnz main_l40
int 0
return
main_l40:
int 0
byte "QVoteDecisionCredits"
load 1
byte "asset_coefficient"
app_global_get
*
app_local_put
int 1
return
main_l41:
int 0
return
main_l42:
int 0
return
main_l43:
int 0
return
main_l44:
byte "Creator"
txn Sender
app_global_put
byte "Name"
txna ApplicationArgs 0
app_global_put
byte "asset_id"
txna ApplicationArgs 6
btoi
app_global_put
byte "asset_coefficient"
txna ApplicationArgs 7
btoi
app_global_put
byte "voting_start_time"
txna ApplicationArgs 8
btoi
app_global_put
byte "voting_end_time"
txna ApplicationArgs 9
btoi
app_global_put
global LatestTimestamp
byte "voting_start_time"
app_global_get
>
bnz main_l61
txna ApplicationArgs 1
byte "NULL_OPTION"
!=
bnz main_l60
int 1
return
main_l47:
txna ApplicationArgs 2
byte "NULL_OPTION"
!=
bnz main_l59
int 1
return
main_l49:
txna ApplicationArgs 3
byte "NULL_OPTION"
!=
bnz main_l58
int 1
return
main_l51:
txna ApplicationArgs 4
byte "NULL_OPTION"
!=
bnz main_l57
int 1
return
main_l53:
txna ApplicationArgs 5
byte "NULL_OPTION"
!=
bnz main_l56
int 1
return
main_l55:
int 1
return
int 1
return
main_l56:
byte "option_"
txna ApplicationArgs 5
concat
int 4294967296
app_global_put
b main_l55
main_l57:
byte "option_"
txna ApplicationArgs 4
concat
int 4294967296
app_global_put
b main_l53
main_l58:
byte "option_"
txna ApplicationArgs 3
concat
int 4294967296
app_global_put
b main_l51
main_l59:
byte "option_"
txna ApplicationArgs 2
concat
int 4294967296
app_global_put
b main_l49
main_l60:
byte "option_"
txna ApplicationArgs 1
concat
int 4294967296
app_global_put
b main_l47
main_l61:
int 0
return
And our smart contract has successfully been compiled.
I really hope you enjoy that. Follow me for more Pyteal Content in the nearest time.👍