Create Publication

We are looking for publications that demonstrate building dApps or smart contracts!
See the full list of Gitcoin bounties that are eligible for rewards.

Solution Thumbnail

Example Permissionless Voting Stateful Smart Contract Application

Overview

The following example walks through building a permissionless voting application on the Algorand blockchain. This application allows anyone with an Algorand address to cast a vote, but verifies that users can only cast one vote per account. This example, built as a stateful smart contract, is written in TEAL. A version of permissionless voting is also available in Python using the PyTEAL library and is explained on the Algorand developer site. A permissioned voting application is detailed in this Algorand solution.

This article will begin by covering the design of how the application should work and then will detail how to build the application on Algorand.

Design Overview

This Application is designed to allow any Algorand account to vote, but only once per account. Additionally if an account closes out participation in the smart contract before the vote is over, their previously cast vote will be nullified. The application is designed to set round ranges in which accounts can opt in to the smart contract. A different set of round ranges is also set to allow voting.

To create this type of application on Algorand, there are four steps that must be supported.

  1. Create Voting Smart Contract - A user needs to create the voting smart contract on the Algorand blockchain and pass the round ranges for registering and voting. The creator address is passed into the creation method. This is used only to allow the creator to delete or update the voting smart contract.
  2. Register to Vote - Voters need to register with the voting smart contract by opting into the contract. Registering to vote occurs between a set of rounds that is set during the creation of the contract.
  3. Vote - Voters cast their ballot by calling the smart contract and passing their candidate of choice. Double voting is prevented.
  4. Close Out - Voters can close out their participation in the voting smart contract. If this occurs before the end of the voting period, their vote is nullified.

EditorImages/2020/09/01 19:00/1.png

EditorImages/2020/09/01 19:00/2.png

EditorImages/2020/09/01 19:01/3.png

Each step in this architecture is explained in the following sections.

Info

This solution solely makes use of goal to make the application calls against the stateful application. The SDKs also provide the same functionality and can be used instead of goal.

Voting Smart Contract Creation - Step 1

The goal command-line tool provides a set of application-related commands that are used to manipulate applications. The goal app create command is used to create the application. This is a specific application transaction to the blockchain, and similar to the way Algorand Assets work, it will return an application ID. Several parameters are passed to the creation method. These parameters primarily revolve around how much storage the application uses. In stateful smart contracts, you specify storage as either global or local. Global storage represents the amount of space that is available to the application itself, and local storage represents the amount of space in every account’s balance record that will be used by the application per account.

EditorImages/2020/09/01 19:02/4.png

Seven global variables ( one byte slice and six integers) and one local storage variable (byte) are used in the voting application. The global byte slice is for the creator address and the six global integers represent the round ranges for registering and voting. The local byte slice is used to store an account’s vote, i.e. candidate A or candidate B.

$ goal app create --creator {CentralAccount}   --approval-prog ./vote.teal --global-byteslices 1 --global-ints 6 --local-byteslices 1 --local-ints 0 --app-arg "int:1" --app-arg "int:20" --app-arg "int:20" --app-arg "int:100" --clear-prog ./vote_opt_out.teal -d ~/node/data

In this example, several application arguments are also passed to the create method. These are stateful smart contract application arguments and represent the round ranges for registering and voting. This voting application uses these ranges instead of timestamps. The application can be modified to use timestamps if needed. The approval and clear programs are also passed to the create method. For more information on argument passing and stateful smart contract creation, see the developer documentation.

The TEAL code for the stateful smart contract performs the following operations.

  • Check to see that the application ID is not set, indicating * this is a creation call.
  • Store the creator address to global state.
  • Store both register and voting round ranges to global state.

// Approval Program
#pragma version 2
// check if the app is being created
// if so save creator
int 0
txn ApplicationID
==
bz not_creation
byte "Creator"
txn Sender
app_global_put

// 4 args on creation
// transaction will fail
// if 4 args are not passed during creation
txn NumAppArgs
int 4
==
bz failed

byte "RegBegin"
txna ApplicationArgs 0
btoi
app_global_put

byte "RegEnd"
txna ApplicationArgs 1
btoi
app_global_put
byte "VoteBegin"
txna ApplicationArgs 2
btoi
app_global_put

byte "VoteEnd"
txna ApplicationArgs 3
btoi
app_global_put

int 1
return
not_creation:

Voters Opt Into Voting Smart Contract - Step 2

Users must opt into stateful smart contracts if the contract uses local storage. This application stores a voter’s choice in local storage and requires that users opt-in.

EditorImages/2020/09/01 19:07/5.png

Opting is done using goal or the SDKs.

$ goal app optin  --app-id {APPID} --from {ACCOUNT} -d ~/node/data

The voting app checks the following conditions.

  • Checks the transaction OnCompletion type and verifies it is an opt in operation.
  • Verify that the round is currently between registration begin and end rounds.

// register
txn OnCompletion
int OptIn
==
bnz register
.
.
.
register:
global Round
byte "RegBegin"
app_global_get
>=
global Round
byte "RegEnd"
app_global_get
<=
&&
bz failed
int 1
return

For more information on opting into a stateful smart contract see the developer documentation.

Users Vote - Step 3

Once registered with the smart contract, users can vote. This requires voters to submit an Application call transaction, passing two application arguments. The first argument should contain the string “vote” and the second should contain the candidate’s name.

EditorImages/2020/09/01 19:09/2.png

The goal command for casting a vote is shown below.

$ goal app call --app-id {APPID}  --app-arg "str:vote" --app-arg "str:candidatea" --from {ACCOUNT}  -d ~/node/data

The smart contract processes this request with the following operations.

  • Verify the first application argument contains the string “vote.”
  • Verify the vote call is between the beginning and end of the voting round ranges.
  • Check to see if the voter has opted into the smart contract.
  • Inspect the voter’s account to check to see if this account has already voted.
  • If the account has already voted, return failure.
  • Verify that the user is either voting for candidate A or B.
  • Read the candidate’s current total from the global state and increment the value.
  • Store the candidate choice to the user’s local state.

// vote
txna ApplicationArgs 0
byte "vote" 
==
bnz vote
int 0
return

vote:
global Round
byte "VoteBegin"
app_global_get
>=
global Round
byte "VoteEnd"
app_global_get
<=
&&
bz failed

// Check that the account has opted in
// account offset (0 == sender, 1 == txn.accounts[0], 2 == txn.accounts[1], etc..)
int 0 
txn ApplicationID
app_opted_in
bz failed

// check local to see if they have voted
int 0 // sender
txn ApplicationID
byte "voted"

// 3 pars get popped
// two values pushed 0/1 and value
// 0/1 should be on top of stack
app_local_get_ex 

// Value must not exist
// else user already voted
bnz voted

// read existing vote candidate
// i think i need a pop here
pop
txna ApplicationArgs 1
byte "candidatea" 
==

txna ApplicationArgs 1
byte "candidateb" 
==
||
bz failed
int 0

txna ApplicationArgs 1
app_global_get_ex
bnz increment_existing
pop
int 0

increment_existing:
int 1
+
store 1
txna ApplicationArgs 1
load 1
app_global_put
int 0 // sender
byte "voted"
txna ApplicationArgs 1
app_local_put
int 1
return

voted:
pop
int 0
return

For more information on stateful smart contracts see the developer documentation.

Users Close Out Participation - Step 4

Once the vote is over, the application supports closing out a users participation in the voting smart contract. If the user closes out before the end of the voting period, their vote is nullified.

EditorImages/2020/09/01 19:13/3.png

The goal command for closing out participation is shown below.

$ goal app closeout --app-id {APPID}  --from {ACCOUNT}  -d ~/node/data

The smart contract processes this request with the following operations.

  • Verify this is a close out application transaction.
  • If the vote round ranges have expired, return.
  • Check to see if the user voted (read from local state), if not return.
  • Decrement the vote count of the candidate user voted (read from global state).
  • Store updated vote count for the specific candidate (save to global state).

// check for closeout
int CloseOut
txn OnCompletion
==
bnz close_out
.
.
.
// call if this is a closeout op

close_out:

//see if the vote is over
global Round
byte "VoteEnd"
app_global_get
>
bnz finished

// check local to see if they have voted
int 0 // sender
txn ApplicationID
byte "voted"

// 3 pars get popped
// two values pushed 0/1 and value
// 0/1 should be on top of stack

app_local_get_ex 

// Value must not exist
// else user voted
bnz voted_c
pop
int 1
return

voted_c:
// vote candidate is at the top of the stack
// read existing vote candidate
store 1
int 0 // current smart contract
load 1
app_global_get_ex
store 3
store 4
load 3
bnz decrement_existing

// did not find candidate
int 1
return

// decrement the vote
decrement_existing:
load 4
int 1
-
store 2
load 1
load 2
app_global_put
int 1
return

Users can close out of a contract by using a closeout or clear application transaction. The logic used for closeout and clear in this example are exactly the same. This logic is implemented in the approval and clear programs.

Conclusion

The permissionless-voting application illustrates building a simple voting application using a stateful smart contract. The full source code for the application is available on Github.