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 Permissioned Voting Stateful Smart Contract Application

Overview

Online voting applications can be very useful, but most suffer from a lack of transparency, which makes them an ideal candidate for the blockchain. This is especially true if you are using them for casual polling of customers or getting public opinion on a specific topic. If the stakes of a vote are more consequential, this presents a problem for blockchain application builders. Public blockchain applications have some basic anonymity, and with that, several challenges need to be addressed. The most prominent issue is double voting. How do you prevent an individual from creating multiple accounts and voting more than once? What is needed is a permissioned voting application. Permissioned voting applications should only allow one individual to vote once. This walkthrough explains one method of doing this on the Algorand blockchain.

This application will involve using many of the Algorand technologies, including stateless smart contracts, atomic transfers, standard assets, and asset transactions. This article will begin with a design overview and then go into the details on how to build the application on Algorand.

Design Overview

To implement a permissioned voting application on Algorand, a central authority is needed to provide users the right to vote. In this example, this is handled by an Algorand Standard Asset. The central authority creates a vote token and then gives voters who have registered one voting token. The voter then registers within a round range with the voting smart contract, by optioning into the contract. Voters then vote by grouping two transactions. The first is a smart contract call to vote for either candidate A or candidate B, and the second is transferring the vote token back to the central authority. Voting is only allowed within the voting range.

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

  1. Create Asset - Central authority needs to create a voting token using Algorand ASAs. Voters need to opt into this asset, and the asset ID needs to be stored in the stateful smart contract to verify when the user votes, and confirm they are spending their voting token. The central authority needs to send voters one vote token.
  2. Create Voting Smart Contract - Central authority 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 the voting smart contract.
  3. Register to Vote - Voters need to register with the voting smart contract by optioning into the contract. Registering to vote occurs between a set of rounds that is set during the creation of the contract.
  4. Vote - Voters vote by atomically grouping two transactions and submitting them to the blockchain. The first transaction is a call to the smart contract casting a vote for either candidate A or candidate B. The second transaction is an asset transfer from the voter to the central authority to spend their voting token.

EditorImages/2020/08/18 17:34/1.png

EditorImages/2020/08/18 17:35/2.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.

Asset Creation - Step 1

The central authority first needs to create a voting token. This can be done with the SDKs, the goal command-line tool, or using an asset creation IDE like algodesk.io.

EditorImages/2020/08/18 17:44/3.png

Below is an example of creating a voting token using goal.

$ goal asset create --creator {ACCOUNT} --total 1000 --unitname votetkn --decimals 0 -d ~/node/data

In this example, 1000 vote tokens are created. Voters then need to opt into the voting token.

$ goal asset send -a 0 -f {VOTER_ACCOUNT} -t {VOTER_ACCOUNT}  --creator {CENTRAL_ACCOUNT} --assetid {ASSETID} -d ~/node/data

The assetID returned from the blockchain after creating the vote token must be passed into the above call. The central authority should then send the vote token to voters who registered with the central authority. How the central authority handles registration is not explained in this article.

$ goal asset send -a 1 -f {CENTRAL_ACCOUNT} -t {VOTER_ACCOUNT}  --creator {CENTRAL_ACCOUNT} --assetid {ASSETID} -d ~/node

The assetID for the voting token should be hardcoded into the voting smart contract. Optionally it could be passed in as an argument. The assetID is discussed more in step 4.

Voting Smart Contract Creation - Step 2

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/08/18 17:48/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 the vote for a specific account, i.e. candidate A or candidate B.

$ goal app create --creator {CENTRAL_ACCOUNT}   --approval-prog ./p_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 ./p_vote_opt_out.teal

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 must be used on creation
txn NumAppArgs
int 4
==
bz failed

// set round ranges
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 3

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/08/18 17:53/5.png

Opting in is done using goal or the SDKs.

$ goal app optin  --app-id {APPID} --from {ACCOUNT} --app-arg "str:register" -d ~/node/data

The voting app uses an application argument with the value of “register” to perform the opt in operation. The smart contract executes the following actions.

  • Checks that the first argument to the smart contract is the word “register”.
  • Verifies that the round is currently between registration begin and end rounds.
  • Verifies that the account has opted in.

// register

txna ApplicationArgs 0
byte "register"
==
bnz register

.
.
.

register:
global Round
byte "RegBegin"
app_global_get
>=

global Round
byte "RegEnd"
app_global_get
<=
&&

int OptIn
txn OnCompletion
==
&&

bz failed
int 1
return

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

Users Vote - Step 4

Once registered with the smart contract, users can vote. This requires using an atomic transfer with two transactions. The first should be a call to the stateful smart contract casting a vote, and the second should be an asset transfer (voting token) from the voter to the central authority.

EditorImages/2020/08/18 17:55/6.png

The atomic transfer in this example, is done with the goal command-line tool.

$ goal app call --app-id {APPID} --app-arg "str:vote" --app-arg "str:candidatea" --from {ACCOUNT}  --out=unsignedtransaction1.tx
$ goal asset send --from={ACCOUNT} --to={CENTRAL_ACCOUNT} --creator {CENTRAL_ACCOUNT} --assetid {VOTE_TOKEN_ID} --fee=1000 --amount=1 --out=unsignedtransaction2.tx

$ cat unsignedtransaction1.tx unsignedtransaction2.tx > combinedtransactions.tx
$ goal clerk group -i combinedtransactions.tx -o groupedtransactions.tx 
$ goal clerk sign -i groupedtransactions.tx -o signout.tx
$ goal clerk rawsend -f signout.tx

The smart contract processes this request with the following operations.

  • Verifies the first application argument contains the string “vote”.
  • Verifies the vote call is between the beginning and end of the voting round ranges.
  • Checks to see if the voter has opted into the smart contract.
  • Inspects the voter’s account to verify that they have at least one vote token (hardcoded and should be changed).
  • Verifies that two transactions are in the group.
  • Checks that the second transaction is an asset transfer, and the token transferred is the vote token.
  • Checks that the second transaction receiver is the creator of the application.
  • Checks if the account has already voted, and if so, just returns true with no change to global state.
  • Verifies that the user is either voting for candidate A or B.
  • Reads the candidate’s current total from the global state and increments the value.
  • Stores the candidate choice to the user’s local state.

// vote
txna ApplicationArgs 0
byte "vote" 
==
bnz vote
.
.
.
vote:

// verify in voting rounds

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 if they have the vote token
// assuming assetid 2. This should
// be changed to appropriate asset id
// sender
int 0

// hard-coded assetid
int 2
// returns frozen asset balance
// pop frozen
asset_holding_get AssetBalance
pop

// does voter have at least 1 vote token
int 1
>=
bz failed

// two transactions
global GroupSize
int 2
==
bz failed

// second tx is an asset xfer
gtxn 1 TypeEnum
int 4
==
bz failed

// creator receiving the vote token
byte "Creator"
app_global_get
gtxn 1 AssetReceiver
==
bz failed

// verify the proper token spent
gtxn 1 XferAsset
// hard coded and should be changed
int 2
==
bz failed

// spent 1 vote token
gtxn 1 AssetAmount
int 1
==
bz failed

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

// if voted skip incrementing count
bnz voted
pop

// can only vote for candidatea
// or candidateb
txna ApplicationArgs 1
byte "candidatea" 
==
txna ApplicationArgs 1
byte "candidateb" 
==
||
bz failed

// read existing vote candidate
// in global state and increment vote
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

// store the voters choice in local state
int 0 //sender
byte "voted"
txna ApplicationArgs 1
app_local_put

int 1
return

voted:
pop
int 1
return

For more information on atomic transfers, stateful smart contracts, or assets, see the developer documentation.

Conclusion

The permissioned-voting application illustrates using several of Algorand’s layer-1 features to implement a functional smart contract. The full source code for the application is available on Github.