Skip to main content
Version: 1.0

9. Voting Contract

In this tutorial, we're going to deploy a contract that allows users to vote on multiple proposals that a voting administrator controls.


tip

Open the starter code for this tutorial in the Flow Playground:

https://play.onflow.org/d120f0a7-d411-4243-bc59-5125a84f99b3


The tutorial will be asking you to take various actions to interact with this code.

info

The code in this tutorial and in the playground uses Cadence 0.42. The link will still work with the current version of the playground, but when the playground is updated to Cadence 1.0, the link will be replaced with a 1.0-compatible version. It is recommended that since Flow is so close to upgrading to Cadence 1.0, that you learn Cadence 1.0 features and syntax.

info

Instructions that require you to take action are always included in a callout box like this one. These highlighted actions are all that you need to do to get your code running, but reading the rest is necessary to understand the language's design.

With the advent of blockchain technology and smart contracts, it has become popular to try to create decentralized voting mechanisms that allow large groups of users to vote completely on chain. This tutorial will provide a trivial example for how this might be achieved by using a resource-oriented programming model.

We'll take you through these steps to get comfortable with the Voting contract.

  1. Deploy the contract to account 0x01
  2. Create proposals for users to vote on
  3. Use a transaction with multiple signers to directly transfer the Ballot resource to another account.
  4. Record and cast your vote in the central Voting contract
  5. Read the results of the vote

Before proceeding with this tutorial, we highly recommend following the instructions in Getting Started and Hello, World! to learn how to use the Playground tools and to learn the fundamentals of Cadence.

A Voting Contract in Cadence​

In this contract, a Ballot is represented as a resource.

An administrator can give Ballots to other accounts, then those accounts mark which proposals they vote for and submit the Ballot to the central smart contract to have their votes recorded.

Using a resource type is logical for this application, because if a user wants to delegate their vote, they can send that Ballot to another account, and the use case of voting ballots benefits from the uniqueness and existence guarantees inherent to resources.

Deploy the Contract​

Time to deploy the contract we'll be working with:

info
  1. Open Contract 1 - the ApprovalVoting contract.
  2. In the bottom right deployment modal, press the arrow to expand and make sure account 0x01 is selected as the signer.
  3. Click the Deploy button to deploy it to account 0x01

Deploy ApprovalVoting to account 0x01

The deployed contract should have the following contents:

ApprovalVoting.cdc

_127
/*
_127
*
_127
* In this example, we want to create a simple approval voting contract
_127
* where a polling place issues ballots to addresses.
_127
*
_127
* The run a vote, the Admin deploys the smart contract,
_127
* then initializes the proposals
_127
* using the initialize_proposals.cdc transaction.
_127
* The array of proposals cannot be modified after it has been initialized.
_127
*
_127
* Then they will give ballots to users by
_127
* using the issue_ballot.cdc transaction.
_127
*
_127
* Every user with a ballot is allowed to approve any number of proposals.
_127
* A user can choose their votes and cast them
_127
* with the cast_vote.cdc transaction.
_127
*
_127
*/
_127
_127
access(all)
_127
contract ApprovalVoting {
_127
_127
//list of proposals to be approved
_127
access(all)
_127
var proposals: [String]
_127
_127
// number of votes per proposal
_127
access(all)
_127
let votes: {Int: Int}
_127
_127
// This is the resource that is issued to users.
_127
// When a user gets a Ballot object, they call the `vote` function
_127
// to include their votes, and then cast it in the smart contract
_127
// using the `cast` function to have their vote included in the polling
_127
access(all)
_127
resource Ballot {
_127
_127
// array of all the proposals
_127
access(all)
_127
let proposals: [String]
_127
_127
// corresponds to an array index in proposals after a vote
_127
access(all)
_127
var choices: {Int: Bool}
_127
_127
init() {
_127
self.proposals = ApprovalVoting.proposals
_127
self.choices = {}
_127
_127
// Set each choice to false
_127
var i = 0
_127
while i < self.proposals.length {
_127
self.choices[i] = false
_127
i = i + 1
_127
}
_127
}
_127
_127
// modifies the ballot
_127
// to indicate which proposals it is voting for
_127
access(all)
_127
fun vote(proposal: Int) {
_127
pre {
_127
self.proposals[proposal] != nil: "Cannot vote for a proposal that doesn't exist"
_127
}
_127
self.choices[proposal] = true
_127
}
_127
}
_127
_127
// Resource that the Administrator of the vote controls to
_127
// initialize the proposals and to pass out ballot resources to voters
_127
access(all)
_127
resource Administrator {
_127
_127
// function to initialize all the proposals for the voting
_127
access(all)
_127
fun initializeProposals(_ proposals: [String]) {
_127
pre {
_127
ApprovalVoting.proposals.length == 0: "Proposals can only be initialized once"
_127
proposals.length > 0: "Cannot initialize with no proposals"
_127
}
_127
ApprovalVoting.proposals = proposals
_127
_127
// Set each tally of votes to zero
_127
var i = 0
_127
while i < proposals.length {
_127
ApprovalVoting.votes[i] = 0
_127
i = i + 1
_127
}
_127
}
_127
_127
// The admin calls this function to create a new Ballot
_127
// that can be transferred to another user
_127
access(all)
_127
fun issueBallot(): @Ballot {
_127
return <-create Ballot()
_127
}
_127
}
_127
_127
// A user moves their ballot to this function in the contract where
_127
// its votes are tallied and the ballot is destroyed
_127
access(all)
_127
fun cast(ballot: @Ballot) {
_127
var index = 0
_127
// look through the ballot
_127
while index < self.proposals.length {
_127
if ballot.choices[index]! {
_127
// tally the vote if it is approved
_127
self.votes[index] = self.votes[index]! + 1
_127
}
_127
index = index + 1;
_127
}
_127
// Destroy the ballot because it has been tallied
_127
destroy ballot
_127
}
_127
_127
// initializes the contract by setting the proposals and votes to empty
_127
// and creating a new Admin resource to put in storage
_127
init() {
_127
self.proposals = []
_127
self.votes = {}
_127
_127
self.account.storage.save(
_127
<-create Administrator(),
_127
to: /storage/VotingAdmin
_127
)
_127
}
_127
}

This contract implements a simple voting mechanism where an Administrator can initialize a vote with an array of proposals to vote on by using the initializeProposals function.


_16
// function to initialize all the proposals for the voting
_16
access(all)
_16
fun initializeProposals(_ proposals: [String]) {
_16
pre {
_16
ApprovalVoting.proposals.length == 0: "Proposals can only be initialized once"
_16
proposals.length > 0: "Cannot initialize with no proposals"
_16
}
_16
ApprovalVoting.proposals = proposals
_16
_16
// Set each tally of votes to zero
_16
var i = 0
_16
while i < proposals.length {
_16
ApprovalVoting.votes[i] = 0
_16
i = i + 1
_16
}
_16
}

Then they can give Ballot resources to other accounts. The other accounts can record their votes on their Ballot resource by calling the vote function.


_10
access(all)
_10
fun vote(proposal: Int) {
_10
pre {
_10
self.proposals[proposal] != nil: "Cannot vote for a proposal that doesn't exist"
_10
}
_10
self.choices[proposal] = true
_10
}

After a user has voted, they submit their vote to the central smart contract by calling the cast function, which records the votes in the Ballot and destroys the used Ballot.


_16
// A user moves their ballot to this function in the contract where
_16
// its votes are tallied and the ballot is destroyed
_16
access(all)
_16
fun cast(ballot: @Ballot) {
_16
var index = 0
_16
// look through the ballot
_16
while index < self.proposals.length {
_16
if ballot.choices[index]! {
_16
// tally the vote if it is approved
_16
self.votes[index] = self.votes[index]! + 1
_16
}
_16
index = index + 1;
_16
}
_16
// Destroy the ballot because it has been tallied
_16
destroy ballot
_16
}

When the voting time ends, the administrator can read the tallies for each proposal to see if a proposal has received the right number of votes.

Perform Voting​

Performing the common actions in this voting contract only takes three types of transactions.

  1. Initialize Proposals
  2. Send Ballot to a voter
  3. Cast Vote

We have a transaction for each step that we provide for you. With the ApprovalVoting contract to account 0x01:

info
  1. Open Transaction 1 which should have Transaction1.cdc
  2. Submit the transaction with account 0x01 selected as the only signer.
Transaction1.cdc

_25
import ApprovalVoting from 0x01
_25
_25
// This transaction allows the administrator of the Voting contract
_25
// to create new proposals for voting and save them to the smart contract
_25
_25
transaction {
_25
prepare(admin: AuthAccount) {
_25
_25
// borrow a reference to the admin Resource
_25
let adminRef = admin.storage.borrow<&ApprovalVoting.Administrator>(from: /storage/VotingAdmin)!
_25
_25
// Call the initializeProposals function
_25
// to create the proposals array as an array of strings
_25
adminRef.initializeProposals(
_25
["Longer Shot Clock", "Trampolines instead of hardwood floors"]
_25
)
_25
_25
log("Proposals Initialized!")
_25
}
_25
_25
post {
_25
ApprovalVoting.proposals.length == 2
_25
}
_25
_25
}

This transaction allows the Administrator of the contract to create new proposals for voting and save them to the smart contract. They do this by calling the initializeProposals function on their stored Administrator resource, giving it two new proposals to vote on. We use the post block to ensure that there were two proposals created, like we wished for.

Next, the Administrator needs to hand out Ballots to the voters. There isn't an easy deposit function this time for them to send a Ballot to another account, so how would they do it?

This is where multi-signed transactions can come in handy!

Selecting multiple Accounts as Signers​

A transaction has access to the private account objects of every account that signed it, so if both the admin and the voter sign a transaction, the admin can directly move a Ballot resource object to the other account's storage.

In the Flow playground, you can select multiple accounts to sign a transaction to be able to access the private account objects of both accounts.

To select multiple signers, you first need to include two arguments in the prepare block of your transaction:

prepare(acct1: AuthAccount, acct2: AuthAccount)

The playground will give you an error if the number of selected signers is different than the number of arguments to the prepare block. The playground also maps the accounts you select as signers to the arguments in the order that you select them. The first account you select will be the first argument, and the second account you select is the second argument.

info
  1. Open Transaction 2 which should have Transaction2.cdc.
  2. Select account 0x01 as a signer first, then also select account 0x02.
  3. Submit the transaction by clicking the Send button
Transaction2.cdc

_24
_24
import ApprovalVoting from 0x01
_24
_24
// This transaction allows the administrator of the Voting contract
_24
// to create a new ballot and store it in a voter's account
_24
// The voter and the administrator have to both sign the transaction
_24
// so it can access their storage
_24
_24
transaction {
_24
prepare(admin: AuthAccount, voter: AuthAccount) {
_24
_24
// borrow a reference to the admin Resource
_24
let adminRef = admin.storage.borrow<&ApprovalVoting.Administrator>(from: /storage/VotingAdmin)!
_24
_24
// create a new Ballot by calling the issueBallot
_24
// function of the admin Reference
_24
let ballot <- adminRef.issueBallot()
_24
_24
// store that ballot in the voter's account storage
_24
voter.storage.save(<-ballot, to: /storage/Ballot)
_24
_24
log("Ballot transferred to voter")
_24
}
_24
}

This transaction has two signers as prepare parameters, so it is able to access both of their private AuthAccount objects, and therefore their private account storage.

Because of this, we can perform a direct transfer of the Ballot by creating it with the admin's issueBallot function and then directly store it in the voter's storage by using the save function.

Account 0x02 should now have a Ballot resource object in its account storage. You can confirm this by selecting 0x02 from the lower-left sidebar and seeing Ballot resource listed under the Storage field.

Casting a Vote​

Now that account 0x02 has a Ballot in their storage, they can cast their vote. To do this, they will call the vote method on their stored resource, then cast that Ballot by passing it to the cast function in the main smart contract.

info
  1. Open Transaction 3 which should contain Transaction3.cdc.
  2. Select account 0x02 as the only transaction signer.
  3. Click the send button to submit the transaction.
Transaction3.cdc

_21
import ApprovalVoting from 0x01
_21
_21
// This transaction allows a voter to select the votes they would like to make
_21
// and cast that vote by using the castVote function
_21
// of the ApprovalVoting smart contract
_21
_21
transaction {
_21
prepare(voter: AuthAccount) {
_21
_21
// take the voter's ballot our of storage
_21
let ballot <- voter.storage.load<@ApprovalVoting.Ballot>(from: /storage/Ballot)!
_21
_21
// Vote on the proposal
_21
ballot.vote(proposal: 1)
_21
_21
// Cast the vote by submitting it to the smart contract
_21
ApprovalVoting.cast(ballot: <-ballot)
_21
_21
log("Vote cast and tallied")
_21
}
_21
}

In this transaction, the user votes for one of the proposals, and then moves their Ballot back to the smart contract via the cast() method where the vote is tallied.

Reading the result of the vote​

At any time, anyone could read the current tally of votes by directly reading the fields of the contract. You can use a script to do that, since it does not need to modify storage.

info
  1. Open a Script 1 which should contain the code below.
  2. Click the execute button to run the script.
Script1.cdc

_20
import ApprovalVoting from 0x01
_20
_20
// This script allows anyone to read the tallied votes for each proposal
_20
//
_20
_20
access(all)
_20
fun main() {
_20
_20
// Access the public fields of the contract to log
_20
// the proposal names and vote counts
_20
_20
log("Number of Votes for Proposal 1:")
_20
log(ApprovalVoting.proposals[0])
_20
log(ApprovalVoting.votes[0])
_20
_20
log("Number of Votes for Proposal 2:")
_20
log(ApprovalVoting.proposals[1])
_20
log(ApprovalVoting.votes[1])
_20
_20
}

You should see something like this print:


_10
"Number of Votes for Proposal 1:"
_10
"Longer Shot Clock"
_10
0
_10
"Number of Votes for Proposal 2:"
_10
"Trampolines instead of hardwood floors"
_10
1

This shows that one vote was cast for proposal 1 and no votes were cast for proposal 2.

Other Voting possibilities​

This contract was a very simple example of voting in Cadence. It clearly couldn't be used for a real-world voting situation, but hopefully you can see what kind of features could be added to it to ensure practicality and security.