Aventus Protocol (Part 1): Deep Dive on Voting
We are excited to have a look at the first component of the Aventus Protocol: An AVT stake weighted voting module, allowing Aventus network participants to have a part in shaping the future of the protocol.
Before we jump into the details we would like to thank Colony, Aragon and Zeppelin amongst many others for their previous work around voting, gas optimising design, upgradability patterns etc.
Voting Mechanics
Our voting process progresses from setup through 4 phases until a vote is closed and the result is known.
Phase 0: Setup
Anyone can create a vote which requires putting up a deposit in AVT. Once the vote completes this deposit is returned to the creator irrespective of the outcome. The deposit makes it infeasible to spam the Aventus network with unnecessary distracting content.
Setting up a vote involves submitting the description (describing what the vote is about), adding up to 4 options that can be voted for and finalising the vote by specifying the start of the lobbying phase and an interval for how long the voting phase should be (minimum 1 week).
We have decided to create no financial incentive for voting on the direction of the Aventus protocol (this does not apply to later components such as voting on event and application legitimacy). As an AVT holder you should have an incentive to participate in votes, since they influence the direction of the protocol. Initially these are advisory and non binding, but once the protocol has been fully developed, deployed and a sufficient degree of adoption can be observed, binding votes will be introduced. Thus it should be obvious that any AVT token holder acting in their own self interest, should participate in votes to steer decisions in the direction they believe maximises the utility of the network thereby driving underlying value of AVT, without any further incentive.
Phase 1: Lobbying
The lobbying phase lasts for double the voting phase (i.e. 2 x the finalised interval length, so the minimum is 2 weeks). Nothing happens on chain during this time — it is intended to give the community time to discuss proposals and lobby for the various options.
Phase 2: Voting
Once the voting phase starts, any AVT holder can cast their vote. Once a vote is cast it cannot be changed.
The vote is AVT weighted to prevent sybil attacks (e.g. you could create and vote from many Ethereum addresses to skew results if stake did not matter) and to ensure that those with the most exposure get a proportional say in any major vote.
All data on a blockchain is publicly visible which presents a problem when voting since you should have no knowledge of the likelihood of any option winning before you cast your vote. To get around this, participants submit an obscured vote (i.e. the hash of the signature corresponding to the voters public key of the option voted on).
Phase 3: Revealing
Before the reveal phase starts, voters need to transfer the AVT stake they wish to count towards the tally, to a lock contract. Once the reveal phase starts all voters’ funds are locked until the vote is revealed. Note: locked means no funds can be deposited or withdrawn from the lock contract so it is important to get as much AVT as desired into the lock contract before the reveal period. Since revealing a vote is entirely in the control of the voter their immediate usage is never impaired, unlike some of the previous voting designs (e.g. the DAO).
Revealing a vote is done by submitting the option voted on and the signature thereof. The smart contract hashes the signature and checks that it is the same as that submitted during the voting phase and that is is from the appropriate public key. Once everything checks out, the amount of AVT in the lock contract is added to the tally for the specified option and the lock on the AVT is released so the voter can withdraw their funds again if desired (however if the voter intends to repeatedly vote they may as well leave it be until needed).
Phase 4: Result
Once the reveal phase ends the vote is finalised and the result is known. Note: revealing votes is still possible even after the reveal phase has ended since funds remain locked indefinitely if a vote is cast, to incentivise users to vote only if they actually intend on participating.
Implementation
We have open sourced the code for the Aventus protocol voting, which can be found on GitHub.
All smart contacts inherit from interfaces, so that they can reference each other without incurring an additional gas overhead. When a smart contract references another its compiled code contains the compiled code of all other referenced contracts. Interfaces means only function signatures are included not their implementation drastically reducing deployment costs.
We use libraries (deployed as singletons) for most business logic. This means common functionality that will be repeatedly used only needs to be deployed once and other smart contracts referencing it will be much cheaper to deploy.
Storage.sol
Storage is a smart contract (inheriting from the interface IStorage.sol) which can be thought of as the Aventus database. All data is stored in this contract, which has been designed to be general enough to never have to be updated. All types we will use exist in this contract, addressable by a hash (i.e. mapping(bytes32=>type)).
This design has a slight gas overhead for reading or mutating state on chain, however this means all other protocol contracts created do not need to allocate storage space for their state directly and can thus be upgraded without copying any state. This is much more gas efficient since storage opcodes are very costly.
Owned.sol
Owned is a simple smart contract intended to be inherited from to give other contracts the ability to specify and update an owner and expose an onlyOwner access modifier to prevent certain functions from being called by anyone but the owner. This also contains functionality to self destruct the smart contract, which is desirable if an upgrade is pushed and should only every be possible by a trusted owner.
LLock.sol
Lock is a library containing all locking functionality with all functions intended to be bound to the Storage smart contract (i.e. use LLock for Storage). It allows users to deposit and withdraw AVT as long as they do not have any unrevealed votes. It can be frozen in case an attack on the Aventus network occurs to ensure network participants’ AVT remains safe. It will initially be rolled out with a restricted mode enabled, which will not allow any address to vote with more than a maximum of AVT at any time and it will only accept an overall maximum amount of AVT across all voters. These thresholds will be slowly increased as AVT holders become more familiar with the process and eventually disabled.
LVote.sol
Vote is a library containing all voting functionality, again with all function intended to be bound to the Storage smart contract (i.e. use LVote for Storage). Most of the functionality was described in the voting mechanics section, however we will briefly touch on the data structure used to keep track of votes cast and locked AVT for unrevealed votes.
A doubly linked list, ordered by reveal start time is maintained for each user as shown in the figure from Colony below. Each node contains another doubly linked list ordered by vote id.
We do not want to have to traverse the whole data structure and incur the gas overhead of doing so when inserting a vote. Voters will be able to calculate in memory (i.e. off chain, locally) what position their vote should be inserted in the two lists and supply this as a parameter when voting, taking the computational complexity from linear to constant (i.e. way more gas efficient). Before inserting the entry the smart contract validates that the previous and next entry are as expected (i.e. the user is not trying to cheat) and then inserts it. We need the vote id doubly liked list within each of the nodes in the top list for votes with identical reveal periods.
AventusVote.sol
AventusVote is a smart contract inheriting from Owned, which provides all voting functionality by binding Lock and Vote libraries to the Storage type. It accepts the Storage smart contract as a parameter upon creation. Setting thresholds for the restricted voting mode and freezing AVT in the lock contract are restricted to only the owner of the AventusVote smart contract. Full implementation below.
contract AventusVote is Owned {
using LLock for IStorage;
using LVote for IStorage;IStorage s;/**
* @dev Constructor
* @param s_ Persistent storage contract
*/
function AventusVote(IStorage s_) {
s = s_;
}/**
* @dev Withdraw locked, staked AVT not used in an active vote
* @param amount Amount to withdraw from lock
*/
function withdraw(uint amount) {
s.withdraw(amount);
}/**
* @dev Deposit & lock AVT for stake weighted votes
* @param amount Amount to withdraw from lock
*/
function deposit(uint amount) {
s.deposit(amount);
}// @dev Toggle the ability to lock funds for staking (For security)
function toggleLockFreeze()
onlyOwner
{
s.toggleLockFreeze();
}/**
* @dev Set up safety controls for initial release of voting
* @param restricted True if we are in restricted mode
* @param amount Maximum amount of AVT any account can lock up at a time
* @param balance Maximum amount of AVT that can be locked up in total
*/
function setThresholds(bool restricted, uint amount, uint balance)
onlyOwner
{
s.setThresholds(restricted, amount, balance);
}/**
* @dev Create a proposal to be voted on
* @param desc Either just a title or a pointer to IPFS details
* @return uint ID of newly created proposal
*/
function createVote(string desc) {
s.createVote(desc);
}/**
* @dev Add an option to a proposal that voters can choose
* @param id Proposal ID
* @param option Description of option
*/
function addVoteOption(uint id, string option) {
s.addVoteOption(id, option);
}/**
* @dev Finish setting up votes with time intervals & start
* @param id Proposal ID
* @param start The start date of the cooldown period, after which vote starts
* @param interval The amount of time the vote and reveal periods last for
*/
function finaliseVote(uint id, uint start, uint interval) {
s.finaliseVote(id, start, interval);
}/**
* @dev Cast a vote on one of a given proposal's options
* @param id Proposal ID
* @param secret The secret vote: Sha3(signed Sha3(option ID))
* @param prevTime The previous revealStart time that locked the user's funds
* @param prevId The previous proposal ID at the current revealStart time
*/
function castVote(uint id, bytes32 secret, uint prevTime, uint prevId) {
s.castVote(id, secret, prevTime, prevId);
}/**
* @dev Reveal a vote on a proposal
* @param id Proposal ID
* @param optId ID of option that was voted on
* @param v User's ECDSA signature(sha3(optID)) v value
* @param r User's ECDSA signature(sha3(optID)) r value
* @param s_ User's ECDSA signature(sha3(optID)) s value
*/
function revealVote(uint id, uint optId, uint8 v, bytes32 r, bytes32 s_) {
s.revealVote(id, optId, v, r, s_);
}
}
Limitations and Future Work
As with all software, it is never completely done. It can always be extended and improved and we apply the same philosophy to the Aventus protocol. We have published the first iteration but there is much to come in the future as the Ethereum blockchain evolves and offers more advance functionality and as we expand on the protocol.
Our current design is primarily lacking when it comes to upgrading either of the libraries: Lock or Vote. If we were to upgrade these currently we would have to redeploy the Aventus Vote smart contract also. Whilst that is not a huge overhead since it is rather efficient (gas wise), it would be better if we could decouple the dependance between the Aventus Vote smart contract and the libraries themselves using a proxy pattern as suggested by Zeppelin illustrated below although this does still have its own issues.
We are also yet to finalise and release our unit and integration testing suite for the code and have an audit of the system once more of the protocol is completed.
This module discussed here will be deployed on the Rinkeby test net (as well as a snapshot of AVT balances to make the test net environment is as close to the main net as possible) with the corresponding DApp UI this week. We intend to test it on there and have the bug bounty running in parallel until we feel as comfortable as possible with the security aspects before deploying on the main Ethereum network.