Part 3 - Basic voting
If you haven’t seen “Part 2 - Aragon app primer” of the tutorial, go check that out first, otherwise, you might find yourself a little lost here.
In this article, we’re going to continue building the app that we bootstrapped in the previous part, and give it basic voting functionality, learning some interesting Aragon-ish concepts along the way.
This section’s branches:
- Start: https://github.com/ajsantander/hc/tree/tutorial/part3/basic-voting-start
- End: https://github.com/ajsantander/hc/tree/tutorial/part3/basic-voting-end
As in the previous part, check out the start branch, and then create your own branch from it.
One user, one vote
It’s time to pick up the pace. We have a lot of ground to cover, and we might as well make use of our momentum. Instead of discussing what needs to be implemented and then showing you chunks of code, I’m going to point you to a diff with the changes that you need to make in your own project. I will prompt you to implement a part of those changes, we will discuss them, and repeat the process as we iterate onward.
Here’s the 1st diff:
Note1: I recommend using Github’s unified view for viewing these diffs. Green is code you need to insert or change, and red is code that you need to delete.
Note2: Diff URLs contain two commits hashes. The first one is the code from the previous section and the second one contains the new code. If you don’t want to be copying code from diffs, you can directly use the 2nd commit hash of the URL to jump directly to the end result. To do this, checkout the corresponding end branch and jump to the 2nd commit of the diff, in this case:
git checkout 8d1dd869fd4a1680b7d3d0e0a44860d21cc78b57 or https://github.com/ajsantander/hc/tree/8d1dd869fd4a1680b7d3d0e0a44860d21cc78b57
Go ahead and introduce the changes made to HCVoting.sol only. It’s a good exercise to try to understand the changes while you enter them. Once you’re done, come back so that we can discuss what you’ve just done.
If it all went well, you should be able to compile the contract with
npm run compile. Make sure to run this each time you make a change to any contracts. This way, you’ll know early on if something went wrong while adding the changes.
The first thing we did was introduce the
pragma directive, where we declare the compiler version that we want to use. I forgot to do this in the last part of the tutorial, so we’re fixing it now. Notice that we’re targeting Solidity versions within the range
0.4.24 < 0.5.0. Even though Solidity
0.5.x is quite ubiquitous these days, aragonOS uses
^0.4.24, so we need to stick to that when creating Aragon apps. This should change soon, when aragonOS is updated to use the latest versions of Solidity.
Next, we imported SafeMath, which adds additional methods to all
uint256 values with the
Then, we define a couple of internal error strings. This is a very common practice for using consistent revert errors in all Aragon apps. You will see these constants being used within require statements, such as the one used in the end of the contract, in the
/* DATA STRUCTURES */ section, things start to get a bit more interesting. We define a
Vote enum, which will be used to represent a single user’s vote, be it “Yea”, “Nay”, or “Absent” for when a vote hasn’t been cast yet. As with all uninitialized variables in Solidity, or storage slots in the EVM for that matter, this enum’s default value will be zero, or “Absent” in this case.
Still in this section, we define the basic data structure for all proposals with the
Proposal struct. For now, we’re only keeping track of the
totalNays of a proposal, and the singular vote of each user via the
Proposals are then stored in the global
proposals mapping, a property of the contract that allows us to access a proposal by a
uint256 index or id. We keep track of the proposal id of the last created proposal with
Next, we define a couple of events. These are triggered in the corresponding
vote(...) functions, a few lines further down.
Leaping over the “init” section, which we haven’t changed yet, onto the “public” section, you can see how
propose(string _metadata) is implemented, which is misleadingly simple. We first emit a ProposalCreated event with the following 3 parameters:
- (uint256) proposalId: The proposal id of the proposal being created, determined by
- (address) creator: The creator of the proposal, msg.sender
- (string) metadata: The metadata string that we passed as a parameter to create the proposal.
After emitting this event, we simply increment
numProposals. It will be 0 for the first time we create a proposal, and so the
proposalId emitted in the event will also be 0. The next time we create a proposal,
numProposals will have been incremented to 1, so
proposalId will be 1 as well, and so on. Notice how it may seem a little weird to not be doing anything else in this function. What is it that we are creating? That’s ok though, as mentioned before, uninitialized values are zero instead of null in Solidity, so the newly created proposal should have all of its values set to 0 at this point anyway. There’s nothing to save in a proposal struct for the time being. The only change of state in the contract while creating a proposal is that this proposal is now considered to exist. We will be writing to the proposal’s struct as soon as someone votes on it though.
Also, notice that we’re not storing the proposal’s metadata in the Proposal struct itself. We’re doing this intentionally to save resources, since a string has potentially unlimited size. We can do this because we are storing the string in the emitted event, and we can always use that to look up a proposal’s metadata. It’s generally a good idea to store data in events when it will only be required externally and not from within Solidity code.
vote(...) function has a bit more meat in it. We first retrieve the Proposal using the passed
_proposalId index, via the internal
_getProposal(...) function. As soon as we have the struct, we check the user’s previous vote, and if it exists, we revert. This means that if a user voted, the vote cannot be changed. We might want to change this in the future to allow a user to change votes, but this simpler implementation should be alright for now.
So, if this is the first time that the user is voting in the proposal, it will increment the proposal’s
totalNays count, depending if the user is supporting the proposal or not.
After that, we store the current vote for this user in the
votes mapping of the Proposal struct. This way, we can also query a proposal on how a particular user voted on it.
Finally, we emit a
VoteCasted event, again with the
proposalId, the user’s address, and a boolean representing whether the user supported or rejected the proposal.
Below these main, public functions, we added a few
view or “getter” functions that allow us to look into individual members of the Proposal struct, which are accessed by proposal id.
getUserVote(uint256 _proposalId, address _user) gets the
Vote.Absent status for a particular proposal id and user. Similarly, the following two functions
getTotalYeas(uint256 _proposalId) and
getTotalNays(uint256 _proposalId) can be used to retrieve the
totalNays for the specified proposal.
We could have chosen to use a single getter to query a proposal’s state, but since we don’t yet know how much this struct is going to grow as we develop the app, and to avoid eventually running into stack-too-deep issues (there’s a max of 16 values that can be returned by a function in Solidity), we choose the alternate approach of exposing each member individually. Besides, calling a function and receiving an array back is not very explicit.
At the end of the file, we define a little helper function that, given a proposal id, returns the corresponding Proposal struct. This function is internal, and is only intended to be used by other functions within the contract. Internal functions are not limited to 16 elements, and can return structs.
Moving on to Template.sol, you may notice that we’re fixing another leftover from part 2. We were using the wrong
appId for our app. This doesn’t affect us now anyway, since templates are used to deploy an Aragon organization with a specific configuration of apps in a network, and for now, we’re only deploying the organization and the app manually in tests. We will use it in future parts of the tutorial though. For now, it’s just a tidy up.
Now, go ahead and introduce the changes made to the test files. Again, take your time and try to maintain a firm grip on what's going on.
When you’re done, run
npm t. You should see a bunch of tests passing.
The first thing you should notice in these changes, is that we’ve deleted test/app.js and created a few files in its place. The idea is to test specific behaviors of the app within individual files, so that we can focus on one specific behavior at a time, without having to worry about other behaviors and how they all interact with each other.
Let’s look at propose.test.js first.
Notice how we’ve significantly compressed the way in which we deploy the organization and the app. This is all done now within a single call to
deployAllAndInitializeApp(...), which is a helper defined in the new test/helpers/deployApp.js file. This helper allows us to focus on the proposal creation behavior even further, not having to worry about deploying the organization, installing the app and all that. Go ahead and see how this is all abstracted in deployApp.js. It’ll be super useful in all of our tests.
Take your time to study how the proposal creation behavior is tested, then move on to vote.test.js. When you’re ready, let’s get back to coding.
One user, N votes
Great! The app now features basic voting, but there are a few considerations to take at this point. For instance, the current implementation only allows one vote per user/address. We want to implement weighted voting where a user’s voting power depends on the amount of org tokens owned by the user. For example, if a user owns 100 tokens, voting on a proposal adds 100 yeas to it, instead of just one.
So, next, we’re going to use a token to implement N votes per user. Again, we’re going to be looking at a diff.
Here’s the 2nd diff:
Open the diff and introduce all the changes made to HCVoting.sol in your branch. Once you’re done, come back to the article so that we can discuss these changes.
The first thing we did was import a token contract. This is not just any ERC20 token, but Aragon’s standard MiniMeToken. This has nothing to do with what Aragon token holders have it’s simply a pimped up version of the ERC20 token standard. We’re using this particular token because it has some features that will come in handy later on. We will be deploying it independently in the tests, and passing the deployed token’s address through HCVoting’s
The token is used in the
vote(...) function. Basically, instead of adding one yea or nay in a vote, we add all the tokens in possession of the user at the time of the vote to the proposal’s
totalNays. Notice how we exit early if the user has no tokens and hence no voting power.
Let’s bring in the changes from the test files now.
You don’t need to understand all the changes made to the helper functions, but if you’d like to, be my guest! We’ll be improving the helpers as we move on. In this case, I’m basically avoiding to have to modify each of the test files each time we add a parameter to the initialize function.
What’s important to understand however, is that we’ve introduced a new helper, test/helpers/deployTokens.js, which deploys our vote token. Take a look at this file to see how MiniMe tokens are deployed. Once deployed, the address of the token is passed as a parameter to the app’s
We also added a new test file, test/setup.test.js, which does not test a particular behavior of the app per-se, but verifies that the app is deployed and initialized properly. This test will also allow us to test invalid initializations of the app in the future.
Now, let’s take a look at how vote.test.js has changed.
Before creating a proposal, we’re minting vote tokens for a couple of users, and we’ve added a test that verifies that calls to
vote(...) will revert when users don’t own vote tokens. Makes sense right? Yes, and no. Tokens can be minted after the creation of a proposal, but as you will see soon, we’re going to do something about that.
Another thing added to the tests is that, when counting yeas and nays, we now take into account the number of tokens held by users. Votes no longer increment yea/nay count by
1, but by
Let’s now think about how proposals could be resolved, something that we’ve been ignoring so far. As the code is right now, all we can do is vote on a proposal, but a proposal is never resolved in any way whatsoever. A proposal, for example, should be resolved when some arbitrary yet reasonable number like 51% of the voting power supports it.
Notice that we actually couldn’t resolve proposals before tokenizing votes, when we had one vote per user, since we didn’t really have a way of knowing how many users would be voting on a proposal. In other words, we didn’t know the total voting power involved in a proposal. Now that votes are based on a token, we do. The total voting power is the total number of tokens in circulation, or the token’s
totalSupply. If a proposal gets enough yeas so that they represent 51% of the total supply, we can consider it resolved.
Here’s the 3rd diff:
Have a look at the diff for HCVoting.sol and come back to discuss the changes. You know the drill by now.
Before going into the details of the implementation, let’s quickly discuss a little refactor that we’ve made. We moved the Proposal struct out of HCVoting.sol and into ProposalBase.sol. This might make the diff a little harder to read. Remember that you can always looked at the end commit directly: https://github.com/ajsantander/hc/tree/caeaf8c8f6a97d7fc720efa0cdd61ca9620f1711/contracts
The idea here is to isolate things like the
/* GETTERS */ section, so that it doesn’t pollute the main contract as it grows. With this, HCVoting.sol can strictly focus on logic and leave the Proposal data structure definition to ProposalBase.sol.
Now, back to HCVoting.sol. We added a couple of functions to the contract, as well as a new property
requiredSupport. This property is the “51%” we were talking about earlier, but instead of it being represented in percent, its represented in “parts per million”. Why? Well, let’s suppose that we have a total token supply of 1000. Now, have a look at what happens when we use regular percent calculations:
Percent = 100 * #tokens / 1000
Yep, we’ve got a problem here. Solidity doesn’t support floating point numbers (yet), and we can’t use non-integer values to represent token quantities.
Now, have a look at the same table, but instead of using a range of 0-100 to represent percentages, let’s use the “parts per million” range 0-1000000:
PPM = 1000000 * #tokens / 1000
When using parts per million, we simply scale percentages up so that they can always be represented with integers. Having cleared this up, let’s move on to the rest of the code.
We’ve added a couple of “calculated properties”:
getConsensus(...). These are calculated properties because they’re not the usual getters where we just read a value from the Proposal struct. Read only getters now live cozily in ProposalBase.sol.
getSupport(...) performs the parts per million calculation discussed above and
getConsensus(...) evaluates a proposal’s final decision by checking if it's positive or negative vote count is above
requiredSupport, representing the result with a
Vote enum. Thus, if a proposal’s support is 51% or greater, its consensus will be Yea. If at least 51% of the voting power voted against it, its consensus will be Nay. And if neither positive or negative votes reached the 51% threshold, the proposal’s consensus will be Absent, meaning that no consensus has been reached yet.
Finally, we added the public
resolve(...) function, which evaluates the proposal’s consensus and, if consensus has been reached, the proposal’s resolved flag gets set to true. This is done so that a proposal can only be resolved once. This will be important for when we execute proposals.
Go ahead and apply the changes made to the tests now.
If you look at setup.test.js, you should see how we’re testing valid and invalid usages of the app’s new
requiredSupport initialization parameter, as promised.
Take your time to study the new resolve.test.js file, where we create proposals and vote on them to produce different consensus outcomes and validate them.
Preventing double voting
Perhaps you’ve noticed that there’s a critical issue that has to do with the way in which we calculate the resolution of a proposal. The current implementation allows for some very strange behaviors if tokens are created, destroyed, or in any way moved around while a proposal is active.
To visualize the problem, let’s suppose that at the moment in which a proposal is created, the total supply of vote tokens is 1000, 500 being in possession of user 1 and 500 in user 2. Now suppose that these two users both support the proposal by voting on it with all of their tokens, that is, 500 each, resulting in 100% positive consensus. What are the implications to the proposal if suddenly 2000 tokens were minted out of the blue? The proposal would have passed from having support of 100% to only 33%. Yikes!
But hold on… A bigger problem surfaces as soon as we’re thinking along these lines. Suppose that after user 1 votes on the proposal their tokens are transferred to a new address that the user also controls. With this new address the user can, theoretically, vote again. Even worse, this could be done indefinitely, which could result in a proposal’s
totalYeas + totalNays actually being greater than the token’s total supply. Ergo: Users can double vote. Now, that’s what we call a bug in programming.
To address both of these problems, we’ll be using a feature in the MiniMeToken vote token that allows us to not only get the current total supply and balance of a user, but get these values at a particular block or point in time. That’s right, MiniMeToken keeps a record of the balance history! Nice right? We should only care about the total supply of tokens at the moment that the proposal was created, and a user’s voting power should be limited to the amount of tokens held at such time. Perhaps it makes more sense now that we didn’t allow the creation of a proposal when no tokens existed at the time.
Let’s implement these changes.
Here’s the 4th diff:
As you can see, the Proposal struct holds a new member
propose(...) actually appears to be doing stuff! It doesn’t only increment
numProposals and emit an event anymore, but instead retrieves the Proposal struct being created and stores the current block in it. Before this change, a proposal was only written to when its first vote appeared, but now, it’s written to as soon as it’s created.
Changes were also made to replace every use of the token’s
balanceOf(...) usages to
balanceOfAt(...). This powerful feature of the MiniMeToken allows us to easily implement the solution that we discussed before.
Notice that when we write to
creationBlock, we use
getBlockNumber64() - 1. You may wonder why the
-1? Because we want to use the previous mined block as a reference and not the current one, since the current one may contain other transactions that modify the voting token supply. We can only be certain about the token supply and distribution in the previous block.
Finally, since proposals have a new member, we added a new
getCreationBlock(...) getter to retrieve the creation block of a proposal. We will be doing this every time we add a new member to the struct.
In vote.test.js, have a look at how we’re specifically testing for the scenarios that made us introduce these changes in the first place, and how double voting or token supply dilution can, effectively, not happen anymore.
This is the last iteration that we’re going to do in this part of the tutorial. Hang in there—with this final change we will have a 100% functional voting app.
So far, proposals don’t do anything per-se, they just sit in chain space holding information, perhaps looking at each other with shrugs and a bit of nodding, I don’t know. They’re basically just polls.
Aragon empowers us to do much more than just poll an organization’s members. A proposal can actually contain a script that is executed if and only if the proposal is passed by voters. This script can be anything we want it to be—anything that a smart contract can execute, that is. In this sense, the concept of a proposal becomes that much more powerful since it could encode the actions that it is signaling.
Thanks to Aragon’s EVM scripting features, this is a simple thing to implement. We will not only implement this, but go one step further, hooking up the app to aragonOS’s forwarding mechanism, which will allow the app to receive commands from other Aragon apps.
Here’s the 5th (and final) diff:
Let’s look at the changes made to HCVoting.sol.
You can see that we’ve added a role to the mix. This is interesting! Where did this role come from and why do we need it? You’ll see, soon enough.
We added two new members to the Proposal struct:
executed. The latter will help us register when a proposal’s script has been executed, and the former stores the actual script of the proposal. CallScripts are encoded as byte arrays of call data made to an address. We will see how to produce these scripts when we update our tests.
propose(...) function now takes an
_executionScript parameter. Note that we can always use empty scripts when we intend a proposal to not execute an action, and simply query org members, as we were doing before.
An important addition is the new
_executeProposal(uint256 _proposalId) internal function. This function will be called by
resolve(...) whenever the proposal in question has positive consensus.
Finally, we added three new functions to conform to the
IForwarder interface. These three functions need to be implemented by any app that wants to support forwarding. To understand what forwarding is, let’s imagine the following situation:
- Suppose that our organization has a Vault app installed. This is a simple app that enables an organization to hold funds, which can be DAI, ANT, or any other token (even ETH). Wait, can’t any contract do that? Why do we need a Vault app? The Vault makes sure that these funds do not belong to any single member of the organization, but to the entire organization.
- The organization also has a Finance app installed, which allows funds to be extracted from the Vault in a controlled way. The rig is set up so that the Finance app only allows funds to be transferred through the HCVoting app, via a proposal requiring majority consensus. People have to vote to extract funds.
- Now, a member wishes to extract 10,000 DAI from the Vault to restore the org’s paper clip supply, which is running dangerously low. When attempting to do so, since only the HCVoting app can perform this action in the Finance app, the user’s request is denied.
- However, the Aragon client understands what the user is trying to do, and doesn’t merely reject the action, but suggests the user to create a new voting proposal, so that the entire organization can vote on this action, with the user’s primary intent encoded as the proposal’s execution script. Voila! Forwarding.
To achieve this, the Aragon client first checks if the HCVoting app supports forwarding at all, and if it does, calls the HCVoting app’s
forward(...) function which internally queries
canForward(...). In our implementation, this function will return true only if the sender has the CREATE_PROPOSALS_ROLE.
If this requirement is met, we will create a new proposal with the incoming forwarded script, and no metadata. This achieves the same thing as the
authP modifier in the
propose(...) function. So basically, anyone or anything forwarding into the HCVoting app needs to have the CREATE_PROPOSALS_ROLE, which is the same requirement needed to create a proposal.
As you can see, while answering the question of what forwarding is, we’ve answered the question of what the CREATE_PROPOSALS_ROLE is for.
In our tests, since we’re not using a Vault or a Finance app, we simply grant this role to ANY_ADDRESS. If you look into test/helpers/deployApp.js, this value is
0xffffffffffffffffffffffffffffffffffffffff and is a sentinel value that the ACL understands to allow any address to have access in the role.
Before we look at the actual tests, let’s look at some other files that changed first.
We made changes to arapp.json, specifying the role that we declared in the contract. This is something that the client will use to build its GUI. We also made changes to manifest.json, populating some metadata fields for the app, which the client also makes use of.
In each of the test files, you can see how we imported an evmScript.js helper for constructing call scripts. We use it to produce the script that the
propose(...) function now requires. In most of the tests, we just use
In test/resolve.test.js however,
createProposal(...) was replaced by
createProposalWithScript(...) which constructs a script that is not empty. The script targets SomeContract.sol, which you may have scrolled over in the diff with a "wut?". It’s a mock contract that we will target with this script.
itResolvesTheProposal knows when this script is supposed to be executed and checks if
value was changed. When these tests pass, we will have effectively made an arbitrary call to an external contract an organization vote.
We also added a new test file, forwarding.test.js. In it, we simply verify that calling the app’s
forward(...) function effectively results in the creation of a proposal.
This concludes part 3 of this series. If that felt like a lot of work, it’s because it was. We actually just implemented most of the functionalities of Aragon’s official Voting app. But, most importantly while doing this, we learnt a lot of Aragon concepts such as script execution, forwarding, role management, error handling, snapshot-based token usage, and… I feel like we’re forgetting something. A lot. We learned a lot.
In the next part, we will enhance this voting implementation with fancy Holographic Consensus algorithms ✨. See you there.
PS: In the meantime, if you are hungry for code, take a look at the official Aragon apps. You should now be able to look at the contract side of things and know your way around ;D
Other parts of this series