Voting¶
Warning
This is example code for learning purposes. Do not use in production without thorough review and testing.
In this contract, we will implement a system for participants to vote on a list
of proposals. The chairperson of the contract will be able to give each
participant the right to vote, and each participant may choose to vote, or
delegate their vote to another voter. Finally, a winning proposal will be
determined upon calling the winningProposal() method, which iterates through
all the proposals and returns the one with the greatest number of votes.
1#pragma version >0.3.10
2
3# Voting with delegation.
4
5# Information about voters
6struct Voter:
7 # weight is accumulated by delegation
8 weight: int128
9 # if true, that person already voted (which includes voting by delegating)
10 voted: bool
11 # person delegated to
12 delegate: address
13 # index of the voted proposal, which is not meaningful unless `voted` is True.
14 vote: int128
15
16# Users can create proposals
17struct Proposal:
18 # short name (up to 32 bytes)
19 name: bytes32
20 # number of accumulated votes
21 voteCount: int128
22
23voters: public(HashMap[address, Voter])
24proposals: public(HashMap[int128, Proposal])
25voterCount: public(int128)
26chairperson: public(address)
27int128Proposals: public(int128)
28
29
30@view
31@internal
32def _delegated(addr: address) -> bool:
33 return self.voters[addr].delegate != empty(address)
34
35
36@view
37@external
38def delegated(addr: address) -> bool:
39 return self._delegated(addr)
40
41
42@view
43@internal
44def _directlyVoted(addr: address) -> bool:
45 return self.voters[addr].voted and (self.voters[addr].delegate == empty(address))
46
47
48@view
49@external
50def directlyVoted(addr: address) -> bool:
51 return self._directlyVoted(addr)
52
53
54# Setup global variables
55@deploy
56def __init__(_proposalNames: bytes32[2]):
57 self.chairperson = msg.sender
58 self.voterCount = 0
59 for i: int128 in range(2):
60 self.proposals[i] = Proposal(
61 name=_proposalNames[i],
62 voteCount=0
63 )
64 self.int128Proposals += 1
65
66# Give a `voter` the right to vote on this ballot.
67# This may only be called by the `chairperson`.
68@external
69def giveRightToVote(voter: address):
70 # Throws if the sender is not the chairperson.
71 assert msg.sender == self.chairperson
72 # Throws if the voter has already voted.
73 assert not self.voters[voter].voted
74 # Throws if the voter's voting weight isn't 0.
75 assert self.voters[voter].weight == 0
76 self.voters[voter].weight = 1
77 self.voterCount += 1
78
79# Used by `delegate` below, callable externally via `forwardWeight`
80@internal
81def _forwardWeight(delegate_with_weight_to_forward: address):
82 assert self._delegated(delegate_with_weight_to_forward)
83 # Throw if there is nothing to do:
84 assert self.voters[delegate_with_weight_to_forward].weight > 0
85
86 target: address = self.voters[delegate_with_weight_to_forward].delegate
87 for i: int128 in range(4):
88 if self._delegated(target):
89 target = self.voters[target].delegate
90 # The following effectively detects cycles of length <= 5,
91 # in which the delegation is given back to the delegator.
92 # This could be done for any int128ber of loops,
93 # or even infinitely with a while loop.
94 # However, cycles aren't actually problematic for correctness;
95 # they just result in spoiled votes.
96 # So, in the production version, this should instead be
97 # the responsibility of the contract's client, and this
98 # check should be removed.
99 assert target != delegate_with_weight_to_forward
100 else:
101 # Weight will be moved to someone who directly voted or
102 # hasn't voted.
103 break
104
105 weight_to_forward: int128 = self.voters[delegate_with_weight_to_forward].weight
106 self.voters[delegate_with_weight_to_forward].weight = 0
107 self.voters[target].weight += weight_to_forward
108
109 if self._directlyVoted(target):
110 self.proposals[self.voters[target].vote].voteCount += weight_to_forward
111 self.voters[target].weight = 0
112
113 # To reiterate: if target is also a delegate, this function will need
114 # to be called again, similarly to as above.
115
116# Public function to call _forwardWeight
117@external
118def forwardWeight(delegate_with_weight_to_forward: address):
119 self._forwardWeight(delegate_with_weight_to_forward)
120
121# Delegate your vote to the voter `to`.
122@external
123def delegate(to: address):
124 # Throws if the sender has already voted
125 assert not self.voters[msg.sender].voted
126 # Throws if the sender tries to delegate their vote to themselves or to
127 # the default address value of 0x0000000000000000000000000000000000000000
128 # (the latter might not be problematic, but I don't want to think about it).
129 assert to != msg.sender
130 assert to != empty(address)
131
132 self.voters[msg.sender].voted = True
133 self.voters[msg.sender].delegate = to
134
135 # This call will throw if and only if this delegation would cause a loop
136 # of length <= 5 that ends up delegating back to the delegator.
137 self._forwardWeight(msg.sender)
138
139# Give your vote (including votes delegated to you)
140# to proposal `proposals[proposal].name`.
141@external
142def vote(proposal: int128):
143 # can't vote twice
144 assert not self.voters[msg.sender].voted
145 # can only vote on legitimate proposals
146 assert proposal < self.int128Proposals
147
148 self.voters[msg.sender].vote = proposal
149 self.voters[msg.sender].voted = True
150
151 # transfer msg.sender's weight to proposal
152 self.proposals[proposal].voteCount += self.voters[msg.sender].weight
153 self.voters[msg.sender].weight = 0
154
155# Computes the winning proposal taking all
156# previous votes into account.
157@view
158@internal
159def _winningProposal() -> int128:
160 winning_vote_count: int128 = 0
161 winning_proposal: int128 = 0
162 for i: int128 in range(2):
163 if self.proposals[i].voteCount > winning_vote_count:
164 winning_vote_count = self.proposals[i].voteCount
165 winning_proposal = i
166 return winning_proposal
167
168@view
169@external
170def winningProposal() -> int128:
171 return self._winningProposal()
172
173
174# Calls winningProposal() function to get the index
175# of the winner contained in the proposals array and then
176# returns the name of the winner
177@view
178@external
179def winnerName() -> bytes32:
180 return self.proposals[self._winningProposal()].name
As we can see, this is the contract of moderate length which we will dissect section by section. Let’s begin!
3# Voting with delegation.
4
5# Information about voters
6struct Voter:
7 # weight is accumulated by delegation
8 weight: int128
9 # if true, that person already voted (which includes voting by delegating)
10 voted: bool
11 # person delegated to
12 delegate: address
13 # index of the voted proposal, which is not meaningful unless `voted` is True.
14 vote: int128
15
16# Users can create proposals
17struct Proposal:
18 # short name (up to 32 bytes)
19 name: bytes32
20 # number of accumulated votes
21 voteCount: int128
22
23voters: public(HashMap[address, Voter])
24proposals: public(HashMap[int128, Proposal])
25voterCount: public(int128)
26chairperson: public(address)
27int128Proposals: public(int128)
The variable voters is initialized as a mapping where the key is
the voter’s public address and the value is a struct describing the
voter’s properties: weight, voted, delegate, and vote, along
with their respective data types.
Similarly, the proposals variable is initialized as a public mapping
with int128 as the key’s datatype and a struct to represent each proposal
with the properties name and voteCount. Like our last example, we can
access any value by key’ing into the mapping with a number just as one would
with an index in an array.
Then, voterCount and chairperson are initialized as public with
their respective datatypes.
Let’s move onto the constructor.
54# Setup global variables
55@deploy
56def __init__(_proposalNames: bytes32[2]):
57 self.chairperson = msg.sender
58 self.voterCount = 0
59 for i: int128 in range(2):
60 self.proposals[i] = Proposal(
61 name=_proposalNames[i],
62 voteCount=0
63 )
64 self.int128Proposals += 1
In the constructor, we hard-coded the contract to accept an
array argument of exactly two proposal names of type bytes32 for the contracts
initialization. Because upon initialization, the __init__() method is called
by the contract creator, we have access to the contract creator’s address with
msg.sender and store it in the contract variable self.chairperson. We
also initialize the contract variable self.voterCount to zero to initially
represent the number of votes allowed. This value will be incremented as each
participant in the contract is given the right to vote by the method
giveRightToVote(), which we will explore next. We loop through the two
proposals from the argument and insert them into proposals mapping with
their respective index in the original array as its key.
Now that the initial setup is done, let’s take a look at the functionality.
66# Give a `voter` the right to vote on this ballot.
67# This may only be called by the `chairperson`.
68@external
69def giveRightToVote(voter: address):
70 # Throws if the sender is not the chairperson.
71 assert msg.sender == self.chairperson
72 # Throws if the voter has already voted.
73 assert not self.voters[voter].voted
74 # Throws if the voter's voting weight isn't 0.
75 assert self.voters[voter].weight == 0
76 self.voters[voter].weight = 1
77 self.voterCount += 1
Note
Throughout this contract, we use a pattern where @external functions return data from @internal functions that have the same name prepended with an underscore. This is because Vyper does not allow calls between external functions within the same contract. The internal function handles the logic and allows internal access, while the external function acts as a getter to allow external viewing.
We need a way to control who has the ability to vote. The method
giveRightToVote() is a method callable by only the chairperson by taking
a voter address and granting it the right to vote by setting the voter’s
weight property. We sequentially check for 3 conditions using assert.
The assert not statement will check for falsy boolean values -
in this case, we want to know that the voter has not already voted. To represent
voting power, we will set their weight to 1 and we will keep track of the
total number of voters by incrementing voterCount.
121# Delegate your vote to the voter `to`.
122@external
123def delegate(to: address):
124 # Throws if the sender has already voted
125 assert not self.voters[msg.sender].voted
126 # Throws if the sender tries to delegate their vote to themselves or to
127 # the default address value of 0x0000000000000000000000000000000000000000
128 # (the latter might not be problematic, but I don't want to think about it).
129 assert to != msg.sender
130 assert to != empty(address)
131
132 self.voters[msg.sender].voted = True
133 self.voters[msg.sender].delegate = to
134
135 # This call will throw if and only if this delegation would cause a loop
136 # of length <= 5 that ends up delegating back to the delegator.
137 self._forwardWeight(msg.sender)
In the method delegate, firstly, we check to see that msg.sender has not
already voted and secondly, that the target delegate and the msg.sender are
not the same. Voters shouldn’t be able to delegate votes to themselves. We then
mark the msg.sender as having voted and record the delegate address. Finally,
we call _forwardWeight() which handles following the chain of delegation and
transferring voting weight appropriately.
139# Give your vote (including votes delegated to you)
140# to proposal `proposals[proposal].name`.
141@external
142def vote(proposal: int128):
143 # can't vote twice
144 assert not self.voters[msg.sender].voted
145 # can only vote on legitimate proposals
146 assert proposal < self.int128Proposals
147
148 self.voters[msg.sender].vote = proposal
149 self.voters[msg.sender].voted = True
150
151 # transfer msg.sender's weight to proposal
152 self.proposals[proposal].voteCount += self.voters[msg.sender].weight
153 self.voters[msg.sender].weight = 0
Now, let’s take a look at the logic inside the vote() method, which is
surprisingly simple. The method takes the key of the proposal in the proposals
mapping as an argument, check that the method caller had not already voted,
sets the voter’s vote property to the proposal key, and increments the
proposals voteCount by the voter’s weight.
With all the basic functionality complete, what’s left is simply returning
the winning proposal. To do this, we have two methods: winningProposal(),
which returns the key of the proposal, and winnerName(), returning the
name of the proposal. Notice the @view decorator on these two methods.
The @view decorator indicates that these functions only read contract state
and do not modify it. When called externally (not as part of a transaction),
view functions do not cost gas.
155# Computes the winning proposal taking all
156# previous votes into account.
157@view
158@internal
159def _winningProposal() -> int128:
160 winning_vote_count: int128 = 0
161 winning_proposal: int128 = 0
162 for i: int128 in range(2):
163 if self.proposals[i].voteCount > winning_vote_count:
164 winning_vote_count = self.proposals[i].voteCount
165 winning_proposal = i
166 return winning_proposal
167
168@view
169@external
170def winningProposal() -> int128:
171 return self._winningProposal()
The _winningProposal() method returns the key of proposal in the proposals
mapping. We will keep track of greatest number of votes and the winning
proposal with the variables winningVoteCount and winningProposal,
respectively by looping through all the proposals.
winningProposal() is an external function allowing access to _winningProposal().
174# Calls winningProposal() function to get the index
175# of the winner contained in the proposals array and then
176# returns the name of the winner
177@view
178@external
179def winnerName() -> bytes32:
180 return self.proposals[self._winningProposal()].name
And finally, the winnerName() method returns the name of the proposal by
key’ing into the proposals mapping with the return result of the
winningProposal() method.
And there you have it - a voting contract. Currently, many transactions are needed to assign the rights to vote to all participants. As an exercise, can we try to optimize this?