The Promise and the Peril of Metamorphic Contracts

0age
5 min readFeb 25, 2019

--

There’s a new kid on the Ethereum blockchain that’s moving in after the Constantinople hardfork.

If you know anything about smart contracts on Ethereum, you know that they’re immutable — once the bytecode has been deployed, it stays put unless the contract calls selfdestruct.

If that sounds obvious enough to you, brace yourself… because the rules of the game are about to change. The Constantinople hardfork includes the infamous EIP-1014, which introduces a new opcode, CREATE2. This opcode enables a new form of wild magic where, under the right conditions, a contract can be destroyed, then redeployed to the same address with new bytecode.

Uh-oh, Satoshi: we’re not in Kansas anymore.

The Curious Case of CREATE2 Contracts

The address of a contract that has been deployed by CREATE2 is dependent on the caller’s address, a supplied salt parameter, and the initialization code of the contract that will be created. If any of these arguments are altered, it will result in an altered contract address as well. This would seem to suggest that, since you can’t change the contract initialization code, you can’t change the final bytecode.

Even if that were true, there is still the new danger of a contract being destroyed and recreated, which will wipe its storage. However, there is another wrinkle to consider: the contract’s initialization code may in fact be non-deterministic, meaning that additional factors can cause the resultant bytecode to change. As one example of non-determinism, the initialization code could call in to some external contract with mutable storage and use the returned data to construct the final bytecode.

By utilizing non-deterministic initialization code, contracts can suddenly mutate in-place and swap in arbitrary bytecode. If upgradeability is a bug, then this monstrosity’s a caterpillar that turns into a tarantula with six heads: in other words, a metamorphic contract.

Defense Against the Dark Arts

If the idea of a contract suddenly and unexpectedly metamorphosing doesn’t sit well with you, there are a few remedies at hand to protect against it:

  • Ensure that no selfdestruct opcode is reachable by the contract, either directly or via delegatecall or callcode. If the contract cannot be selfdestructed, it cannot be redeployed.
  • Ensure that the contract was deployed from a source that does not permit redeploys (for instance, by not using CREATE2, or by storing each deployment and preventing duplicate deployments). You’ll also need to make sure that the deployer is not metamorphic itself.
  • Ensure that the contract you are interacting with has not changed via EXTCODEHASH or the like at the start of the transaction before proceeding with the rest of the transaction.

For most honest applications of CREATE2 (such as in state channels and for counterfactual instantiation), this shouldn’t be too much of an issue. In general, you should be very cautious of interacting with any contract that can selfdestruct or otherwise change in a dangerous manner. But if you’re looking for a light-weight upgradeable contract, hopefully one with appropriate controls and governance, then look no further!

The Recipe for Disaster

There’s more than one way to re-skin a cat, but a relatively straightforward approach to creating metamorphic contracts is as follows:

  • First deploy an implementation contract, one that does not rely on a constructor (but can have a regular function like initialize that performs the same role) and that also has the capability to selfdestruct.
  • Then, store a reference to the address of the implementation contract in storage at a fixed, known location. The factory contract that will be kicking off the CREATE2 call is a natural choice.
  • Using CREATE2, deploy a metamorphic contract with fixed, non-deterministic initialization code that will retrieve the implementation address from the factory function, clone the runtime bytecode at that location, and use it to deploy the metamorphic contract’s runtime bytecode. (You can also use an intermediate transient contract with fixed initialization code that then deploys the metamorphic contract via CREATE, then immediately selfdestructs.)
  • When the time comes to change the metamorphic contract, simply selfdestruct the existing contract, deploy and reference a new implementation, and redeploy the contract, which will now clone the new implementation. Since the initialization code is the same, the address of the metamorphic contract will also be the same!

This method, as well as a method for preventing the deployment of metamorphic contracts while still having access to CREATE2, can be found at this repository, and a detailed breakdown of how to construct initialization code that can be used to reference and clone a contract can be found here. Proceed with abundant caution and welcome to a brave and wild new world!

The Ugly Step-Sibling to Transparent Proxies

Finally, a quick comparison to the most prevalent existing method for creating upgradeable contracts, transparent proxies:

  • Upgrades with transparent proxies will persist storage upon upgrades, whereas metamorphic contracts will wipe the state completely (including account balance — be careful not to forward the endowment from selfdestruct right back to the same address). This makes transparent proxies the natural choice for upgradeable ERC20 or ERC721 contracts, whereas metamorphic contracts are possibly more suitable for ERC725 identity contracts or other self-sovereign contracts.
  • The overhead of calling into a metamorphic contract will be reduced compared to calling into a transparent proxy, as the transparent proxy must first check the caller to make sure it’s not the upgrade admin, then delegatecall out to a logic contract.
  • The upgrade process will not be as smooth for metamorphic contracts, as selfdestruct operations are recorded in the transaction substate and performed at the end of a transaction. This means that an upgrade will require two transactions, and so the contract code will be empty (and susceptible to intermediate usage) between the two transactions. On the flip side, a transparent proxy that hits a selfdestruct will be obliterated, but a metamorphic contract can still be recovered.
  • Neither method allows for the constructor to be used during contract initialization. Instead, you should supply an initialize function that is called immediately after setting up the new contract with the cloned implementation. The exception to this rule is if you use an intermediate transient contract, deployed via CREATE2, to deploy the metamorphic contract via CREATE. In that event, you can still use constructors when deploying the metamorphic contract.

Thanks to Jason Carver for bringing this phenomenon to light, to Martin Holst Swende (@mhswende) for the contract cloning mechanism used by the metamorphic contract factory, to the whole Zeppelin team, and to everyone contributing to exploration and education on this topic. Frankenstein’s monster has been reborn — but in Mary Shelly’s words, “thus strangely are our souls constructed, and by slight ligaments are we bound to prosperity and ruin.”

--

--

0age

Head of Protocol Development @ OpenSea (views are my own)