The Promise and the Peril of Metamorphic Contracts
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
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
selfdestructopcode is reachable by the contract, either directly or via
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
EXTCODEHASHor 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
initializethat performs the same role) and that also has the capability to
- 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
CREATE2call is a natural choice.
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
- When the time comes to change the metamorphic contract, simply
selfdestructthe 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
selfdestructright 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
delegatecallout 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
selfdestructwill 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.”