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.
Open the starter code for this tutorial in the Flow Playground: https://play.onflow.org/75a6ce58-74bb-468f-b7bd-425716c93c3e
The tutorial will be asking you to take various actions to interact with this code.
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.
- Deploy the contract to account 1
- Create proposals for users to vote on
- Use a transaction with multiple signers to directly transfer the
Ballot
resource to another account. - Record and cast your vote in the central Voting contract
- 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 account 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.
Deploy the Contract
Open Account 0x01
which has the ApprovalVoting
contract.
Click the Deploy button to deploy it to account 0x01
"
/*
*
* In this example, we want to create a simple approval voting contract
* where a polling place issues ballots to addresses.
*
* The run a vote, the Admin deploys the smart contract,
* then initializes the proposals
* using the initialize_proposals.cdc transaction.
* The array of proposals cannot be modified after it has been initialized.
*
* Then they will give ballots to users by
* using the issue_ballot.cdc transaction.
*
* Every user with a ballot is allowed to approve any number of proposals.
* A user can choose their votes and cast them
* with the cast_vote.cdc transaction.
*
*/
pub contract ApprovalVoting {
//list of proposals to be approved
pub var proposals: [String]
// number of votes per proposal
pub let votes: {Int: Int}
// This is the resource that is issued to users.
// When a user gets a Ballot object, they call the `vote` function
// to include their votes, and then cast it in the smart contract
// using the `cast` function to have their vote included in the polling
pub resource Ballot {
// array of all the proposals
pub let proposals: [String]
// corresponds to an array index in proposals after a vote
pub var choices: {Int: Bool}
init() {
self.proposals = ApprovalVoting.proposals
self.choices = {}
// Set each choice to false
var i = 0
while i < self.proposals.length {
self.choices[i] = false
i = i + 1
}
}
// modifies the ballot
// to indicate which proposals it is voting for
pub fun vote(proposal: Int) {
pre {
self.proposals[proposal] != nil: "Cannot vote for a proposal that doesn't exist"
}
self.choices[proposal] = true
}
}
// Resource that the Administrator of the vote controls to
// initialize the proposals and to pass out ballot resources to voters
pub resource Administrator {
// function to initialize all the proposals for the voting
pub fun initializeProposals(_ proposals: [String]) {
pre {
ApprovalVoting.proposals.length == 0: "Proposals can only be initialized once"
proposals.length > 0: "Cannot initialize with no proposals"
}
ApprovalVoting.proposals = proposals
// Set each tally of votes to zero
var i = 0
while i < proposals.length {
ApprovalVoting.votes[i] = 0
i = i + 1
}
}
// The admin calls this function to create a new Ballot
// that can be transferred to another user
pub fun issueBallot(): @Ballot {
return <-create Ballot()
}
}
// A user moves their ballot to this function in the contract where
// its votes are tallied and the ballot is destroyed
pub fun cast(ballot: @Ballot) {
var index = 0
// look through the ballot
while index < self.proposals.length {
if ballot.choices[index]! {
// tally the vote if it is approved
self.votes[index] = self.votes[index]! + 1
}
index = index + 1;
}
// Destroy the ballot because it has been tallied
destroy ballot
}
// initializes the contract by setting the proposals and votes to empty
// and creating a new Admin resource to put in storage
init() {
self.proposals = []
self.votes = {}
self.account.save<@Administrator>(<-create Administrator(), to: /storage/VotingAdmin)
}
}
This contract implements a simple voting mechanism where an administrator can initialize it with an array of proposals to vote on by using the initializeProposals
function.
// function to initialize all the proposals for the voting
pub fun initializeProposals(_ proposals: [String]) {
pre {
ApprovalVoting.proposals.length == 0: "Proposals can only be initialized once"
proposals.length > 0: "Cannot initialize with no proposals"
}
ApprovalVoting.proposals = proposals
// Set each tally of votes to zero
var i = 0
while i < proposals.length {
ApprovalVoting.votes[i] = 0
i = i + 1
}
}
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.
pub fun vote(proposal: Int) {
pre {
self.proposals[proposal] != nil: "Cannot vote for a proposal that doesn't exist"
}
self.choices[proposal] = true
}
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
.
// A user moves their ballot to this function in the contract where
// its votes are tallied and the ballot is destroyed
pub fun cast(ballot: @Ballot) {
var index = 0
// look through the ballot
while index < self.proposals.length {
if ballot.choices[index]! {
// tally the vote if it is approved
self.votes[index] = self.votes[index]! + 1
}
index = index + 1;
}
// Destroy the ballot because it has been tallied
destroy ballot
}
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.
- Initialize Proposals
- Send
Ballot
to a voter - Cast Vote
We have a transaction for each step that we provide for you.
"You should have the ApprovalVoting already deployed to account 0x01
.
Open Transaction 1 which should have Transaction1.cdc
Submit the transaction with account 0x01
selected as the only signer.
import ApprovalVoting from 0x01
// This transaction allows the administrator of the Voting contract
// to create new proposals for voting and save them to the smart contract
transaction {
prepare(admin: AuthAccount) {
// borrow a reference to the admin Resource
let adminRef = admin.borrow<&ApprovalVoting.Administrator>(from: /storage/VotingAdmin)!
// Call the initializeProposals function
// to create the proposals array as an array of strings
adminRef.initializeProposals(
["Longer Shot Clock", "Trampolines instead of hardwood floors"]
)
log("Proposals Initialized!")
}
post {
ApprovalVoting.proposals.length == 2
}
}
This transaction allows the administrator of the Voting 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 multiple-transaction signers 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.
Open Transaction 2 which should have Transaction2.cdc
.
Select account 0x01
as a signer first, then also select account 0x02
.
Submit the transaction by clicking the Send
button
import ApprovalVoting from 0x01
// This transaction allows the administrator of the Voting contract
// to create a new ballot and store it in a voter's account
// The voter and the administrator have to both sign the transaction
// so it can access their storage
transaction {
prepare(admin: AuthAccount, voter: AuthAccount) {
// borrow a reference to the admin Resource
let adminRef = admin.borrow<&ApprovalVoting.Administrator>(from: /storage/VotingAdmin)!
// create a new Ballot by calling the issueBallot
// function of the admin Reference
let ballot <- adminRef.issueBallot()
// store that ballot in the voter's account storage
voter.save<@ApprovalVoting.Ballot>(<-ballot, to: /storage/Ballot)
log("Ballot transferred to voter")
}
}
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 opening the account 2 tab and seeing the Ballot
resource shown in the Resources box.
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.
Open Transaction 3 which should contain Transaction3.cdc
.
Select account 0x02
as the only transaction signer
Click the send
button to submit the transaction.
import ApprovalVoting from 0x01
// This transaction allows a voter to select the votes they would like to make
// and cast that vote by using the castVote function
// of the ApprovalVoting smart contract
transaction {
prepare(voter: AuthAccount) {
// take the voter's ballot our of storage
let ballot <- voter.load<@ApprovalVoting.Ballot>(from: /storage/Ballot)!
// Vote on the proposal
ballot.vote(proposal: 1)
// Cast the vote by submitting it to the smart contract
ApprovalVoting.cast(ballot: <-ballot)
log("Vote cast and tallied")
}
}
In this transaction, the user votes for one of the proposals, and then moves their Ballot back to the smart contract 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.
Open a Script 1 which should contain the code below.
Click the execute
button to run the script.
import ApprovalVoting from 0x01
// This script allows anyone to read the tallied votes for each proposal
//
pub fun main() {
// Access the public fields of the contract to log
// the proposal names and vote counts
log("Number of Votes for Proposal 1:")
log(ApprovalVoting.proposals[0])
log(ApprovalVoting.votes[0])
log("Number of Votes for Proposal 2:")
log(ApprovalVoting.proposals[1])
log(ApprovalVoting.votes[1])
}
You should see something like this print:
"Number of Votes for Proposal 1:"
"Longer Shot Clock"
0
"Number of Votes for Proposal 2:"
"Trampolines instead of hardwood floors"
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.