Blog

Future-Proofing ERC20 Tokens

Since its introduction in late November 2015, the ERC20 token standard has become the canonical interface against which custom currencies are implemented on the Ethereum blockchain. As of this writing, the aggregate market cap of ERC20 tokens on the Ethereum blockchain is on the order of tens of billions of dollars. This domination of mindshare suggests that the number of applications that support transacting with ERC20 tokens is going to blow up in short order. This trend has already started with the demand for ERC20-compliance in wallets. At the application level, as the community resolves questions of governance and communal decision making around ERC20 tokens, we should expect to see the level of complexity of applications making fundamental use of ERC20 tokens grow far beyond that of wallets. I believe that these are questions that are already being discussed in the MKR community.

Since the introduction of the ERC20 token standard, the community has discovered flaws in various ERC20 implementations. For example:

  1. The inability to recover tokens inadvertently sent to contracts or wallets which do not know how to interface with them. (This motivated the EIP223, which cites the total loss of wealth to this issue as exceeding $3,000,000.)

  2. ERC20 API: An Attack Vector on Approve/TransferFrom Methods

  3. The recent BatchOverflow exploit.

Moreover, with proposals like ERC223 and now ERC777, we are seeing the contract developer community riff more and more significantly on the ideas of ERC20. Given the pace at which the landscape is changing, we should be prepared for a scenario wherein some descendant of the ERC20 interface usurps its current position on the Ethereum blockchain.

All this means that, when we deploy either a token or a contract which interfaces with such a token, we should aim to make it as easy as possible to upgrade the underlying token implementation as possible.

The primary challenge when upgrading ERC20 tokens is how to transfer the state of the token’s internal ledgers which denote:

  1. the number of tokens belonging to each account,

  2. the number of tokens one account has authorized another account to transfer on its behalf.

The reason this is difficult is that, if these ledgers are stored within the smart contract itself, the fact that they can be quite large makes it very expensive in terms of transactional overhead to copy them over to a new contract.

One class of proposals for writing upgradeable smart contracts involves decoupling the storage functionality of those contracts from the actual logic. The following articles show how this idea has developed over time:

  1. Writing upgradeable contracts in Solidity by Elena Dimitrova

  2. Upgradable Solidity Contract Design by David Rugendyke

  3. Upgradable Contracts in Solidity by Hassan Abdel-Rahman

For tokens that are intended for frequent use as currencies, however, the extra gas expenditure induced by such inter-contract architectures may not be desirable. When an entity performs a transaction with an ERC20 token, it is also implicitly paying ETH to make the token transaction possible. A heavy per-transaction ETH cost will surely affect the utility of the token at scale.

For ERC20 tokens, Anton Bukov of BitClave has recently proposed lazy migrations: The Easy Way to Upgrade Smart Contracts. The idea allows you to define a linear chain of ERC20 contract upgrades as follows:

  1. Write your ERC20 tokens so that they can be frozen. After a token contract has been frozen, it will allow no additional transactions to take place against it. After freezing, its state will never change.

  2. When you deploy the latest version of your token, you should point it at the previous (now frozen) version of the token contract.

  3. The new contract can tell if a wallet has migrated its balances using its migratedBalances map.

  4. Whenever a transfer is initiated, the token contract checks whether the sender’s balances have already been migrated from the prior contract and, if not, it applies the migration lazily at that time.

  5. Once a migration has been applied, the migratedBalances for the address it was applied to is set to true so that no such migration will be applied a second time against the current contract.

Although Bukov elaborated on this workflow only for the transfer function, a similar mechanism can also be applied to allowances, with the appropriate checks being performed in the approval and transferFrom methods, and it does yield a scheme which allows migrations without terrible gas overhead in the average case.

I have three critiques of Bukov’s scheme:

  1. The freezing of old contracts limits composability. Bukov inherits from the OpenZeppelin Pausable contract in his ERC20 implementation but does not specify the whenNotPaused modifier on his ERC20 methods. Even so, his exposition is very clear that balances have to be frozen on the old contract. This means that, if someone using the scheme deployed an initial version C0 of a contract implementing the suggested interface, then upgraded it to C1 (thus freezing C0), and finally upgraded it to C2 (thus freezing C1), then it would be impossible for someone holding C0-tokens to migrate their tokens to C2 because the migration would require them to update their balance on C1 as an intermediate step. Likely the right solution is for transfers and approvals to be frozen but for the migration methods to not be frozen (in fact, they can take the whenPaused modifier from the Pausable contract).

  2. The worst-case gas overhead for any particular contract “view” operation is linear in the number of upgrades that have been performed. For example, if you performed 10 upgrades and someone wanted to query the balance of an account which had not migrated since the initial contract deployment, their balance call would result in 10 external contract calls, meaning a gas overhead in excess of 10 times the gas cost of the cheapest balanceOf call on each of the deployments. This could be alleviated by triggering migrations even on such a call to the current contract. At any rate, such chaining of upgrades would require the implementer to address my earlier point of critique.

  3. The act of freezing the old token deployment is a violent one. Unless the token authority can clearly communicate the upgrade to the community of token users well in advance of the actual freeze, we can well assume that such an upgrade would be disruptive to a non-trivial proportion of the token’s user base.

The first two points are technical and yield to technical solutions. The third piece must be treated differently. It concerns the implicit contract that exists between a token issuing authority and the users of that token. The utility of the token in question in its capacity as a currency would be damaged by such a disruption in its functionality as such. This could have a significant effect on the economic dynamics of markets in which the token is commonly used.

Interestingly, this is a scenario that we have a very recent analog for in the world of fiat currencies — the 2016 demonetization of high denomination Indian banknotes. It is hard to tell, given how recently the demonetization took place, what its effects on Indian GDP have been. Especially since there is a lot of propaganda surrounding the matter in every direction. The statistics on the growth of quarterly GDP compared to the same quarter a year prior, as reported by the OECD, do not outright falsify the hypothesis that demonetization did have a significant impact on the Indian economy. If the GDP growth rate was affected by demonetization, whether or not it was justified can only be judged within the context of Indian government policies and is not really of concern to us. What really matters in this discussion is that the Indian government did violate the implicit contract that it had with its citizens by invalidating certain denominations of currency that it had put into circulation and that the violation of this contract may have had a significant impact on the Indian economy. For an organization issuing ERC20 tokens, to the extent that they buy this analogy between freezing an older version of an ERC20 contract and forcefully taking currency out of circulation, the downside inherent to the possibility of the previous statement (applied to an economy in which the token is being used as a currency) should be ample reason to shy away from an upgrade process that involves freezing of contracts. Such organizations may instead favor an upgrade process that more closely reflects more conventional methods of currency circulation.

The question remains — what is a gentler way to perform ERC20 contract upgrades while maintaining the benefits of the lazy Bukov method? Referring back to the analogy between ERC20 contract updates with the management of different representations of a circulating currency, the main requirement is as follows:

Requirement 1: Bearers of older versions of the ERC20 token should be able to continue transacting with their older tokens if they so choose.

Of course, we must also deviate from the analogy with currency circulation because, unlike the fiat currency case, we should not assume the existence of bank-like entities which bring their token stores to a Federal Reserve-like governing entity for inspection. The beauty of blockchains is that such roles and relationships are no longer necessary in order for an economy to function. This means that a bearer of an old version of a given token cannot be forced to accept payment in any other version of the token. Therefore:

Requirement 2: Token bearers, if they want to use an upgraded version of a given token, must initiate the migration of their holdings by themselves (or through an authorized proxy).

This means that a token’s governing entity or organization cannot by fiat force all the users of the token to use some upgraded version. The upgrade will have to stand on its merits. This is much more consistent with the ideals of a decentralized economy than the authoritarian alternative.

Finally, as far as token bearers are concerned, there must be a way to preserve the version-independent value of the token. For example, in the extreme situation that an ERC20 token X allowed an account to migrate its balance over, in full, from any other ERC20 token, the token X would become worthless because any individual could issue their own ERC20 token giving themselves a very large balance and then migrate this balance in its entirety over to X. There has to be some control over which contracts are valid sources for migrations of wealth:

Requirement 3: Any ERC20 contract we deploy should have a whitelist of ERC20 contracts that it upgrades, i.e. ERC20 tokens whose bearers can migrate their wealth to the contract in question.

Thus we can think of our ecosystem of upgradable (and upgraded) ERC20 contracts as forming a directed graph in which the whitelists define the directed edges (with the contract containing the whitelist as the terminal vertex).

With this requirement, we also have to address the issue of how this control is represented. We should not completely downplay the importance of the token’s governing body, especially in the current landscape where such bodies are legitimately bringing value to token bearers by making markets for their tokens. Even though today, these governing entities are generally corporations, the hope is that in the future these entities will be governance-related contracts. Either way, it is important for the ERC20 contract implementing the upgraded functionality to have a concept of a governor, who is able to make some set of decisions about its state. For upgrades, the governor should be able to explicitly specify which contracts are whitelisted for token migrations. This gives us the following requirements:

Requirement 4: Any ERC20 contract we deploy should have a concept of a governor, a distinguished address which can call a privileged set of governance-related methods on the contract.

Requirement 5: At a minimum, in order to enable ERC20 token upgrades, the governor of the upgraded contract must be able to whitelist previous ERC20 contract deployments for token migration.

Finally, there is one requirement which is important with respect to the ERC20 specification:

Requirement 6: The upgraded ERC20 contract must modify its totalSupply after every migration.

An ERC20 contract which conforms to these requirements at a minimum can be used to upgrade prior ERC20 contract deployments in a principled way and can do so in a composable way. We have open sourced an ERC20 implementation which does exactly this: https://github.com/doc-ai/nrn-brainstem/blob/master/src/stem.sol (with tests).

We have also used this contract to deploy an NRN token to the Ropsten testnet at the following address:

0x654ffbe6d907caf34cae0f0bbd043114c8980a7c

Please contact us with your wallet address if you would like me to deposit some NRN into your account.

Note that the requirements presented in this article represent the minimal functionality required to enable upgrades in the manner that I suggested. There are many features that one can layer onto a contract that implements these requirements. For example:

Incentivizing upgrades by offering a token reward to bearers of old versions of a token who upgrade within a certain amount of time after deployment of the upgrade.

Setting exchange rates on migrations of whitelisted tokens. This allows contracts of the type described here to form a totally decentralized exchange for ERC20 tokens.

If there is interest in these use cases, I can expand on them in a follow-up article.

I would love your feedback on the upgrade process proposed in this blog post, as well as on the contract and tests in general. Your contributions on https://github.com/doc-ai/nrn-brainstem are very welcome. Finally, if you would like are considering deploying an ERC20 upgrade using this methodology, we should definitely talk.