Voting is at the core of what we do at Aragon. Even though there have been ample critiques to on-chain voting, it is still the most effective way for a group of diverse people to make collective decisions in a way that the decisions are legitimate and represent everyone's best interests.
Delivering the best possible voting experience is core to Aragon's success. The current Voting app that is being used in the more than 140 live Aragon DAOs (as of writing this) is already the best decentralized Voting app in the world with no close contestants.
However, in order to make Aragon widely available and useful for many types of organizations, we realized that we had to make some improvements:
- Casting a vote must be virtually free (no transaction fee or lower than $0.01) for the voter. Voter apathy is a huge issue in and of itself, therefore we must reduce friction as much as possible. Removing the psychological burden of having to pay to vote will get us a long way towards making voting more appealing and accessible.
- Projects with an existing standard token should be able to plug their token into an Aragon DAO and grant their existing token holder base with governance rights without needing to migrate their token or requiring users to lock up their tokens in order to vote.
- Non-crypto organizations should be able to cheaply onboard a large amount of stakeholders who can begin to participate in the organization's governance, without requiring them to set up a crypto wallet or purchase any cryptocurrency.
In this series of posts, we will introduce our latest research on how we intend to completely revamp Aragon's Voting infrastructure in order to achieve the above properties.
In this first post, we will begin explaining the base of this new infrastructure, EVM Storage Proofs (aka 'The Ethereum Storage Time Machine'), a new technique we have developed that can be used to prove past storage values to other contracts.
TLDR: Check out our EVM Storage Proofs implementation on Github
The double voting problem and MiniMe tokens
At the moment, Aragon Voting apps only support using MiniMe tokens as their governance token. MiniMe is an ERC20 compatible token implementation, that allows querying token balances of holders at any block height (as the vanilla ERC20 specification only provides the current balance). MiniMe tokens allow for a fairly simple solution to the double voting problem, a problem which can be exploited by an account voting with their tokens, then transferring them to another account and then voting again with this different account. If using a MiniMe token, when a vote is created, a past block number is selected as the snapshot block for the vote. When someone votes, their balance is checked at the snapshot block number, and because the same token can't be in two accounts at the same time at the end of a block, the double voting problem is prevented.
However using MiniMe tokens comes with some downsides:
- MiniMe transfers are costly (transfers are at least 50k gas more expensive): every transfer needs to add a checkpoint to both accounts involved in the transfer. This results in at least one fresh
SSTORE(for a non-zero slot) and modifying the checkpoints array length (1
SSTOREwith a 15k refund if it is not the first transfer) per account. Balance checks are also more expensive, the best case (checking the current balance) resulting in at least two additional
- Existing ERC20 tokens cannot be used for voting in Aragon, because they do not implement MineMe's balance checkpoints. Users would need to first transfer their tokens into a contract that has checkpointing or locks tokens in the contract during the vote (See staking checkpointing).
By using EVM Storage Proofs we can bring the benefits of MiniMe to any existing, live token without any changes or disruption.
EVM Storage Proofs
Background: The Ethereum Trie and Merkle Patricia Tree proofs
Ethereum uses a custom data structure for storing most of its state and data, the Merkle Patricia (Practical Algorithm To Retrieve Information Coded In Alphanumeric) Trie. Explaining in details how the data structure works is beyond the scope of this post, however these are two very good resources for understanding how the tries work in Ethereum:
- Patricia Tree spec in the Ethereum wiki: https://github.com/ethereum/wiki/wiki/Patricia-Tree
- Understanding the Ethereum trie by Ethan Buchman: https://easythereentropy.wordpress.com/2014/06/04/understanding-the-ethereum-trie/
At a very high level, the Ethereum Trie is a modified Radix tree in which every node can have a value associated with it, as well as, 16 children nodes. Most tries in Ethereum have 64 levels (therefore 64^16 = 2^256 is the maximum amount of nodes a trie can hold) whose paths are a 32 byte array (with each nibble representing the children node that must be followed when traversing the trie at each level).
Given that managing a tree with 2^256 nodes or generating proofs that require performing 64 hashes to verify, and the fact that vast portions of these trees will be empty (as in order to fill a trie you need to add an entry for every atom in the observable universe), Ethereum introduced two special types of nodes for its Tries:
- Leaf nodes: a leaf node only holds 2 values (rather than the 17 'regular' branch nodes have), the first one being the part of the path being skipped (because there are no other non-null nodes in the middle) and the second the value itself at the end of the path.
- Extension nodes: an extension node also holds two values, it allows for 'jumping' empty parts of the trie. Its first value is the path to be skipped and the second a reference to the children node that follows at that path.
So where does Ethereum use tries? Almost every piece of important state Ethereum holds is stored in a trie: the transactions trie in a block, the storage trie of a contract or the state trie.
Account proofs: from block hash to contract's storage root
An account proof is the first proof that must be verified in order to unlock being able to prove storage values from that account. Using this proof we are able to prove to a contract, the
StorageOracle in our EVM Storage Proofs implementation, what was the storage root for a given account at a past block number. This storage root is the root hash of the storage trie for the account at that block height.
In order to trustlessly perform this proof we need to:
- Get the block hash (using the
BLOCKHASHopcode) at the inputted block number and ensure it is not zero, currently only the latest 256 block hashes can be fetched, though this will change in the Constantinople hard fork.
- Verify that the hash of provided block header blob (RLP encoded) matches the block hash retrieved above, and extract the state root (merkle root of the State trie containing the state of all Ethereum accounts) from the block header.
- Once we have the state root for that block height, a merkle proof to the state of one particular account can be verified. The path for the account in the state trie is the
keccak256hash of the account's address.
- The state of an account is an RLP-encoded array with 4 values (
[nonce, balance, storageHash, codeHash]), one of which is the storage root of the account.
This proven storage root for the account at this block number can now be cached in the contract and be used to verify any amount storage proofs for that contract at that block number.
Storage proofs: from storage root to snapshotted storage values
Once the storage root has been proven and cached, storage proofs can be performed against said root. With a storage proof the
StorageOracle can be used to prove storage values from a contract at the desired block number.
Checking a storage proof is simpler than performing an account proof, just one merkle proof in the storage trie needs to be verified (its path is the
keccak256 hash of the storage slot). The last node of the proof contains the storage value, which gets returned if the proof checks out.
In case the value of a storage slot is zero, an exclusion proof can be provided, that proves to the contract that there is no node in the trie at that given path, therefore interpreting its value as 0.
Interpreting raw storage token adapters
Reading raw storage from contracts is discouraged by the EVM, to the point that there isn't an opcode to get access to these data (an equivalent to the
eth_getStorage JSON-RPC method). Public contract APIs should be the only way to get information about a contract. If some piece of storage is not exposed through the API, it means the contract architect decided that this information is an internal implementation detail and doesn't need to be consumed. When accessing storage through an EVM Storage Proof we are only able to retrieve raw storage from the contract, and any transformations or interpretations of that data must be done by the callee.
However, there are some types of contracts with API methods that directly read from storage and return that value, and whose storage slot for values are easy to derive. A prime example of this are token balances, most
balanceOf(address holder) functions just return a value from a mapping
return balances[holder]. In order for an EVM Storage Proof to return a token balance, we just need to perform a read of the storage value where the balance of a holder is stored, which is just a function of the base storage position (you can read more on this in the Solidity reference on storage layout) of the storage mapping and the address of the holder.
However, tokens with different storage layouts (like MiniMe, that just stores checkpoints for token balances, but not the current balance) require some extra work in order to use EVM Storage Proofs to fetch balances. In the case of MiniMe, two storage proofs must be done, one to the length of the checkpoints array for a holder and another one to the actual location of the latest checkpoint (position that is the result of the first proof).
We have implemented a token storage adapter in the EVM Storage Proofs repo, which allows for proving both 'vanilla' ERC20 and MiniMe token balances. In both cases, a consumer of token balances must specify to the token adapter what the base storage slot for the balances mapping is, in order to properly verify the proofs.
Proof generation: eth_getProof (EIP-1186)
The current implementation uses the new
const Web3Proofs = require('@aragon/web3-proofs') // Not published to NPM yet, requires a local 'npm link' const web3proofs = new Web3Proofs(web3.currentProvider) // Or a web3.js 1.0 compatible provider const proof = await web3proofs.getProof(contractAddress, [slot1, slot2], blockNumber)
The returned proof object contains everything that needs to be sent to the smart contract in order to prove the value of the inputted slots for the contract address.
If running on a live network, proof generation requires an archive node (unless the proof is being generated for the
latest block). Whether proofs can be generated using the Ethereum light client protocol (LES
GetProofs) is currently being researched.
Next up: Voting with proofs, proof-less voting, Aragon Network voting relayers and scalability
In this article we laid out the foundation for the new Aragon Voting infrastructure, that allows us to move away from MiniMe, being able to use tokens that are cheaper to transact with and open the door to many projects that already have an ERC20 and may want to use Aragon with little to no disruption.
In the next articles of the series, we will build on top of this work to implement a new Voting infrastructure where it is virtually free for people to vote (and in which no ETH for fees is ever needed) and there is no need to actually verify storage proofs unless they are challenged