Security Disclosure: Aragon 0.6 Voting ("Voting v1")

December 16, 2019

🚨 In this communications release, we are disclosing a potential security vulnerability in any Aragon organizations created before April 17, 2019.

To the best of our knowledge, no users or organizations have been affected—you are safe. The vulnerability itself is extremely difficult to exploit, and would have occurred only by extreme random chance for organizations created through the Aragon client.

The vulnerability was fixed as part of Aragon 0.7's release. Because it involves a smart contract change, affected organizations—again, those created before April 17, 2019—are required to take manual action to opt into this fix. We recommend all such organizations to upgrade their apps through the App Center in the Aragon client; doing so will also automatically upgrade your organization's apps to their new 0.8 interfaces.

You should immediately avoid trusting organizations that still use the old Voting app. These organizations will display Voting v1 in their App Center and expose the old, 0.6 Voting interface. Safe organizations will display at least Voting v2.

The Voting contract, installed by default in each organization, is meant to be a decision maker. It attaches to a token, uses that token to decide how much voting power each user has in a vote, and allows the holders of that token to vote yes or no on proposals.

In the process of voting, the contract has to be careful to avoid potential double voting attacks. Our Voting contract uses a common industry pattern—historical balance checkpointing in the token—to eliminate this concern.

Accessing this historical checkpoint was where the contract—and myself personally, as the blame shows—made a mistake. The 0.6 Voting contract ("Voting v1") was misconfigured to use a nebulous checkpoint block. The details get a bit technical (see later section), but essentially, rather than using the previous block as a vote's checkpoint block, Voting mistakenly used a part of its attached token's address (all addresses can be represented as a number).

For organizations created via the Aragon client, the token address attached to Voting can be assumed to be random. The bug was difficult to notice during testing because accessing a future block's token checkpoint was logically equivalent to asking for the current block's balance, no matter how far into the future it was (e.g. end-of-the-Earth big). We have since introduced tests against this specific scenario.

Had the attached token address included a very specific string of zeroes, the Voting contract would have stopped updating its token snapshot block in new votes, and really, all sorts of mayhem could become possible as token balances changed.

I've added more details below, but suffice to say, the chance that an organization was passively affected by this particular aspect of the vulnerability was very, very low.

Three additional attack vectors, each requiring advanced technical knowledge and specific high-level permissions in an organization, are also always possible on Voting v1 due to this bug:

  1. An attacker circumventing the double voting protection by using a series of contracts to repetitively vote, transfer tokens, vote, etc. in the same transaction or block as the one a vote was created in;
  2. An attacker with the ability to control the token's balances, through minting and burning, circumventing calculations involving the historical token supply by changing the token balance in the same transaction or block as the one a vote was created in; and finally
  3. An attacker creating an organization with a manually crafted token address

The first two vectors are difficult to achieve, and the last one involves deploying a customized organization—which should induce extra scrutiny from users. We know of no attempts to utilize these vectors on a Mainnet organization.

Luckily, technical members of Aragon One found this vulnerability during the active period of AGP-18. Both of our security partners at the time, Authio and Consensys Diligence, were exceptionally responsive and helpful in guiding us through the process of responsible mitigation and disclosure.

The bug fix was released as part of Aragon 0.7, which again, requires manual action from older organizations to opt-in. And again, to be specific, we recommend all organizations to do so by going to the App Center in the Aragon client and upgrading their Voting app.

Given that a technically knowledgeable or motivated person can still create an organization with Voting v1, we are recommending users to stop trusting any organizations that have yet to upgrade to Voting v2 (or higher).

We have decided to disclose this vulnerability now that most possibly-affected, value-holding organizations have upgraded to the new version of the Voting contract. The few organizations still operating with the old Voting v1 contract generally hold no or very little value (<$100).

The technical fine print. See how the issue was fixed in the bug fix pull request.

To determine the degree of chance that an organization was passively affected by this vulnerability, we first need to understand its specific structure. Let's assume the token address is randomly generated.

The vote_ storage variable declared on line 272 is unassigned. Generally we would hope such a bug would be caught by the compiler or with static tooling, as it is effectively equivalent to undefined behavior in other systems languages (e.g. C, C++; Rust significantly reduces the chance of such errors). Solidity 0.5 actually introduced a compile-time error for this class of errors, but Solidity 0.5 was not available at the time we deployed Aragon 0.6's contracts.

In Solidity versions prior to 0.5, unassigned storage variables would default to slot 0. Now on that same line, we accessed vote_.snapshotBlock. snapshotBlock, a uint64 value, was the third element of the contract's Vote struct, so effectively, this would have read slot 0's 31st to 46th bytes (starting from the left). Coincidentally, slot 0 held the attached token address.

As an illustrative example, imagine the attached token address to be 0xbbf289d846208c16edc8474705c748aff07732db. In slot 0, the address is stored as 0x000000000000000000000001bbf289d846208c16edc8474705c748aff07732db.

The internal Vote struct defines a bool and a uint64 ahead of our uint64 snapshotBlock, so to access snapshotBlock, we first right shift the value in slot 0 by doing a DIV on 0x1000000000000000000, resulting in 0x000000000000000000000000000000000000000001bbf289d846208c16edc847.

We next mask the previous result with an AND on 0xffffffffffffffff to finally obtain 0xd846208c16edc847, or 15584179346614372000, as the uint64 value. Note that as a block, this would roughly represent a time eight trillion years into the future.

Following the above, we now know that only a specific eight bytes, out of the 40 in the token's address, play a factor. Knowing as well that the value will be taken to represent a block time, we can again reduce the problem. We only need to decide an appropriate cut-off point for our desired "safe" block and find the probability of an address being randomly generated with the specific byte configuration to cause the snapshotBlock to be earlier in time than the chosen "safe" block.

Given that Ethereum's block height increases by about three million a year—six blocks a minute, 60 minutes in an hour, 24 hours in a day, and 365 days in a year—and that we're just about to reach nine million blocks, let's say a "safe" cutoff is one billion blocks (more than 300 years into the future).

One billion is a value between 2^33 and 2^34, so let's use 2^34 to be conservative. In our 8 bytes being read (one byte is eight bits, so a total of 64 bits), that means the first 30 bits (64 - 34) must be zeros. Over the entire 20 byte (160 bit) space available for addresses, the chance of an address with these consecutive zeros is 2^(160-34) / 2^160, or roughly 0 (it is around ten zeros worth of precision, or about a million times smaller than the rate of impurities found in the purest gold humans have refined).

The chance a particular Voting app would have used a snapshotBlock above our "safe" one billion block point is left as an exercise to the reader. Organizations were almost guaranteed to use these staggeringly futuristic dates—perhaps not end-of-the-Earth big, but most probably end-of-Ethereum safe.

There's just one caveat left to cover: the above assumes that the token address provided to the Voting app was random. Technically Ethereum addresses are not truly random, but mathematically close enough to allow for the assumption that any single address is generated at random. The problem begins when one actively "mines" for a particular address, with the intention of using that token in an organization.

Vanity address generators are widely available, and would not take very long to produce an address that fits our needs—four zeros in the right place. The attacker probabilistically just needs to generate around 20 billion addresses; something that will take less than a day on generously-powered laptops in 2019.

To our knowledge, no tokens were ever created with this intent, let alone attached to any organizations advertised to potential users. But it does not mean it will not happen in the future, especially after this disclosure, and users should avoid trusting any organization still using Voting v1.

Given that technically knowledgeable or motivated persons can still create organizations with Voting v1, we recommend that you also avoid trusting any organizations that have yet to upgrade to Voting v2 (or higher).