How contract migration works

Smart contracts can be compromised: they can have bugs, the owner’s wallet can be stolen, or they can be trapped due to an incorrect setting. If you develop a smart contract for your business, you must be prepared to react to events such as these. In many cases, the only available solution is to deploy a new instance of the contract and migrate your data to it.

If you plan to develop an upgradable contract, a migration procedure will spare you the dangers of an upgradability mechanism.

Read this blog post for a detailed description of how contract migration works.

You need a contract migration capability

Even a bug-free contract can be hijacked with stolen private keys. The recent Bancor and KICKICO hacks showed that attackers can compromise smart contract wallets. In attacks like these, it may be impossible to fix the deployed smart contract, even if the contract has an upgradability mechanism. A new instance of the contract will need to be deployed and properly initialized to restore functionality to your users.

Therefore, all smart contract developers must integrate a migration procedure during the contract design phase and companies must be prepared to run the migration in case of compromise.

A migration has two steps:

  1. Recovering the data to migrate
  2. Writing the data to the new contract

Let’s walk through the details, costs and operational consequences.

How to perform the migration

Step 1: Data recovery

You need to read the data from a particular block on the blockchain. To recover from an incident (hack or failure), you need to use the block before the incident or filter the attacker’s actions.

If possible, pause your contract. It is more transparent for your users, and prevents attackers from taking advantage of users who are not aware of the migration.

The recovery of data will depend on your data structure.

For public variables of simple types (such as uint, or address), it is trivial to retrieve the value through their getters. For private variables, you can either rely on events or you can compute the offset in memory of the variable, then use the getStorageAt function to retrieve its value.

Arrays are easily recovered, too, since the number of elements is known. You can use the techniques described above.

The situation is a bit more complex for mappings. Keys of a mapping are not stored. You need to recover them to access the values. To simplify off-chain tracking, we recommend emitting events when a value is stored in a mapping.

For ERC20 token contracts, you can find the list of all the holders by tracking the addresses of the Transfer events. This process is difficult. We have prepared two options to help: in the first, you can scan the blockchain and retrieve the holders yourself; in the second, you can rely on the publicly available Google BigTable archive of the Ethereum blockchain.

If you are not familiar with the web3 API to extract information from the blockchain, you can use ethereum-etl, which provides a set of scripts to simplify the data extraction.

If you don’t have a synchronized blockchain, you can use the Google BigQuery API. Figure 1 shows how to collect all the addresses of a given token through BigQuery:

SELECT from_address FROM `bigquery-public-data.ethereum_blockchain.token_transfers` AS token_transfers WHERE token_transfers.token_address = 0x41424344
Union DISTINCT
SELECT to_address FROM `bigquery-public-data.ethereum_blockchain.token_transfers` AS token_transfers WHERE token_transfers.token_address = 0x41424344'

Figure 1: Using Google BigQuery to recover the addresses present in all Transfer events of the token at address 0x41424344

BigQuery provides access to the block number, so you can adapt this query to return the transactions up to a particular block.

Once your recover all the holder’s addresses, you can query the balanceOf function offline to recover the balance associated to each holder. Filter accounts with an empty balance.

Now that we know how to retrieve the data to be migrated, let’s write the data to the new contract.

Step 2: Data writing

Once you collect the data, you need to initiate your new contract.

For simple variables, you can set the values through the constructor of the contract.

The situation is slightly more complex and costly if your data cannot be held in a single transaction. Each transaction is included in a block, which limits the total amount of gas that can be used by its transactions (the so-called “GasLimit”). If the gas cost of a transaction approaches or exceeds this limit, miners won’t include it in a block. As a result, if you have a large amount of data to migrate, you must split the migration into several transactions.

The solution is to add an initialization state to your contract, where only the owner can change the state variables, and users can’t take any action.

For an ERC20 token, the process would take these steps:

  1. Deploy the contract in the initialization state,
  2. Migrate the balances,
  3. Move the contract’s state to production.

The initialization state can be implemented with a Pausable feature and a boolean indicating the initialization state.

To reduce the cost, the migration of the balances can be implemented with a batch transfer function that lets you set multiple accounts in a single transaction:

/**
* @dev Initiate the account of destinations[i] with values[i]. The function must only be called before
* any transfer of tokens (duringInitialization). The caller must check that destinations are unique addresses.
* For a large number of destinations, separate the balances initialization in different calls to batchTransfer.
* @param destinations List of addresses to set the values
* @param values List of values to set
*/
function batchTransfer(address[] destinations, uint256[] values) duringInitialization onlyOwner external{
require(destinations.length == values.length);

uint256 length = destinations.length;
uint i;

for(i=0; i < length; i++){
balances[destinations[i]] = values[i];
emit Transfer(0x0, destinations[i], values[i]);
}
}

Figure 2: An example of a batchTransfer function

Migration concerns

When migrating a contract, two major concerns arise:

  1. How much will the migration cost?
  2. What is the impact on exchanges?

Migration cost

The recovery of data is done off-chain and therefore is free. Ethereum-etl can be used locally. Google‘s BigQuery API offers sufficient free credit to cover its usage.

However, each transaction sent to the network and each byte stored by the new contract has a cost.

Using the batchTransfer function of Figure 2, the transfer of 200 accounts costs around 2.4M gas, which is $5.04 with an average gas price (10 Gwei) at the time of this article (use ETH Gas Station to recalculate this figure for today’s prices). Roughly speaking, you need $0.025 to migrate one balance.

If we look at the number of holders for the top five ERC20 tokens ranked by their market cap, we have:

Token Holders Cost of Migration
BNB 300,000 $7,500
VEN 45,000 $1,200
MKR 5,000 $125
OMG 660,000 $16,500
ZRX 60,000 $1,500

If you migrate additional information (such as the allowance), the cost will be higher. Even so, these amounts are low in comparison to the amount of money that these tokens represent, and the potential cost of a failed upgrade.

Exchanges

The deployment of a new contract may have operational consequences. For token-based contracts, it is important to collaborate with exchanges during a migration to be sure that the new contract will be listed and the previous one will be discarded.

Fortunately, previous token migration events (such as Augur, Vechain, and Tron), showed that exchanges are likely to cooperate.

Contract Migration versus Upgradable Contracts

In our previous blog post, we discussed a trend in smart contract design: the addition of an upgradability mechanism to the contract.

We saw several drawbacks to upgradeable contracts:

  • Detailed low-level expertise in EVM and Solidity is required. Delegatecall-based proxies requires the developer to master EVM and Solidity internals.
  • Increased complexity and code size. The contract is harder to review and is more likely to contain bugs and security issues.
  • Increased number of keys to handle. The contract will need multiple authorized users (owner, upgrader). The more authorized users, the larger the attack surface.
  • Increased gas cost of each transaction. The contract becomes less competitive than the same version without an upgrade mechanism.
  • They encourage solving problems after deployment. Developers tend to test and review contracts more thoroughly if they know that they can’t be updated easily.
  • They reduce users’ trust in the contract. Users need to trust the contract’s owner, which prevents a truly decentralized system.

A contract should have an upgradable mechanism only if there is a strong argument for it, such as:

  • The contract requires frequent updates. If the contract is meant to be modified on a regular basis, the cost of regular migration may be high enough to justify an upgradability mechanism.
  • The contract requires a fixed address. The migration of a contract necessitates the use of a new address, which may break interactions with third parties (such as with other contracts).

Contract migrations achieve the benefits of an upgrade with few of the downsides. The main advantage of an upgrade over a migration is a cheaper cost of the upgrade. However, this cost does not justify all the drawbacks.

Recommendations

Prepare a migration procedure prior to contract deployment.

Use events to facilitate data tracking.

If you go for an upgradable contract, you must also prepare a migration procedure, as your keys can be compromised, or your contract can suffer from incorrect and irreversible manipulation.

Smart contracts bring a new paradigm of development. Their immutable nature requires users to re-think the way they build applications and demands thorough design and development procedures.

Contact us if you need help in creating, verifying, or applying your migration procedure.

In the meantime, join our free Ethereum security office hours if you have any questions regarding the security of your contract.

4 thoughts on “How contract migration works

  1. Pingback: ブロックチェーンとセキュリティ in NYC – BLOCK RABBIT | ブロックラビット

  2. Pingback: Empire Hacking: Ethereum Edition 2 | Trail of Bits Blog

  3. Pingback: Hashing It Out #35: Constantinople Postponement – Trail of Bits & ChainSecurity | The Bitcoin Podcast Network

  4. Pingback: Manticore discovers the ENS bug | Trail of Bits Blog

Leave a Reply