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?