After Constantinople, contracts can be upgraded in-place. Discover new and fun ways to lose your cryptoassets.

̶M̶y̶ ̶o̶p̶i̶n̶i̶o̶n̶ ̶i̶s̶ ̶t̶h̶a̶t̶ ̶w̶e̶ ̶s̶h̶o̶u̶l̶d̶ ̶r̶e̶m̶o̶v̶e̶ ̶E̶I̶P̶-̶1̶0̶1̶4̶ ̶f̶r̶o̶m̶ ̶C̶o̶n̶s̶t̶a̶n̶t̶i̶n̶o̶p̶l̶e̶. It’s staying in, so please spread the word on how to think in a Post-Constantinople world...

One of my favorite authors growing up was Tamora Pierce. Sadly, the stories have largely faded for me, but one fun thing stuck: in the Immortals series, the protagonist can shape-shift into animals. Readers are happy to see Daine use her “Wild Magic” (animal shape-shifting) to save the day. But in the wrong hands, it is a nasty weapon. One minute a kitten purrs in front of you, and the next you are being tossed around by a rhinoceros.

I prefer the Bufficorn

Why the Wild Magic tangent? Because shape-shifting is a great analogy for something new coming in the next Ethereum network upgrade.

CREATE2

Constantinople introduces a new opcode, CREATE2 . It has some interesting features, but also some quirks that don’t seem widely understood, yet.

The good

The core proposition of CREATE2 is that you can commit to init code for deploying a contract, then deploy to a predictable address with that init code. One of the most interesting use cases is using it as an arbitration contract for Layer 2 interactions. If something doesn’t go right in Layer 2, execute the arbitration contract to correct the problem. Arbitration contracts are already possible, but usually they must be deployed before the Layer 2 interactions take place, because everyone needs to agree on what the contract is and where it is. With CREATE2 you can wait to deploy the contract until after arbitration is needed (which is ideally rare), enabling a dramatic increase in scalability, and decrease in gas costs. I’ve heard this called “Counterfactual Contracts.”

The quirky

CREATE2 comes with a quirk, at least as defined in EIP 1014. A CREATE2 contract can be re-deployed to the same address after it has been destroyed. After a selfdestruct() a contract is completely removed from state, so redeploying to the same address is permitted.

The ugly

Being a bit clever, you can deploy different bytecode to the same address, and/or re-deploy a contract using a standard CREATE . I’ll save the details on exactly how to implement that for a follow-up post.

Until Constantinople, the mental model of contract deployment is that a contract could be in three states: “not yet deployed”, “deployed”, or “self-destructed”. After Constantinople, we add a fourth state: “redeployed”. Based on some casual conversations, and an informal poll, many people are not aware of this change. Without knowing about this possibility, you could be taken advantage of.

In this post, I’ll use Wild Magic to refer to a redeployed contract with different bytecode. We’ll also discuss Zombie Contracts that are revived with identical bytecode.

Black-Hat Wild Magic

Wild Magic can be used to deploy an upgraded contract with fixed bugs, or it could be used to try to separate you from your cryptoassets (aka ether & tokens). Here’s something that a black-hat might try:

Launch a DEX contract that promises to trade DAI for OMG The Dapp asks you to approve the contract to have access to your full DAI balance You’re no fool, so you go verify the source code:

pragma solidity ^0.5.3; contract OMGVendor {

// We verified the addresses & contracts for both

ERC20 OMG = ERC20(0xd2...);

ERC20 DAI = ERC20(0x89...);



function vend(uint omg_to_buy) external {

// Silly hard-coding of price 1:1

uint dai_cost = omg_to_buy;



// We verified transferFrom reverts if either transfer fails

DAI.transferFrom(msg.sender, address(this), dai_cost);

OMG.transferFrom(address(this), msg.sender, omg_to_buy);

}



function shutdown() external {

if (msg.sender == 0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7) {

selfdestruct(msg.sender);

}

}

}

4. The source is simple and legit! It will only transfer out as much DAI as you request, and will send you OMG back.

5. The author was even a good citizen, and included a selfdestruct() , so they can clean the contract up after usage

6. You approve the contract to have full access to your DAI balance

Before Constantinople, the potential downside is that the contract is destroyed before your vend() transaction is mined.

After Constantinople, the potential downside is draining all of the DAI you hold.

The black-hat can destroy the contract and replace it with one that steals all your DAI!

Defending Against Black-Hats

There are several options to protect yourself from Wild Magic in the hands of black-hats:

Don’t interact with destructible contracts

Validate the transaction deployment history (recursively!)

During execution, verify the target contract’s EXTCODEHASH before calling it

Indestructible Contracts

Wild Magic requires a self-destruct step before upgrading. So one approach is to verify that destroying the contract is impossible. This is somewhat constraining. It is also a bit trickier than looking for a selfdestruct() call in Solidity. Anything that invokes a CALLCODE or DELEGATECALL also might trigger a SELFDESTRUCT .

With a little more nuance, you might work to make sure that the contract is not immediately destroyable. For example, it might not be able to selfdestruct until it emits an event declaring the intention and waits for 72 hours. As long as you have enough time to react to the event (for example, un-approving the contract’s access to your funds), then you might be safe, depending on the contract.

Making sure that the contract doesn’t have a selfdestruct() (or CALLCODE or DELEGATECALL ) is a straightforward way to confirm that you don’t need to worry about Wild Magic. Sometimes it’s valuable to have destructible contracts, so let’s take a look at some more options for protecting yourself…

Validate the Deployment History

The simplest way to invoke Wild Magic is to deploy a contract with CREATE2 . On the other end of the spectrum, you know a contract is not re-deployable (even if it has a self-destruct) if you can verify that it was created directly by an EOA. Let’s talk about all the scenarios in between.

If the contract in question was created by another contract, then you could verify it was created with a CREATE call instead of CREATE2 . But look out! Even if the contract being audited was launched with CREATE , its parent might have been deployed with CREATE2 . If both are destructible, then the target contract is still potentially malleable. So you need to follow the chain of transactions, from the one that created your target contract, to the one that created that parent contract, to the one that created the grandparent contract, all the way back to the EOA. If none of them used CREATE2 , then you know that no Wild Magic is possible. (You can short-cut a bit, if you get to a contract that was deployed pre-Constantinople)

Finally, even if there was a CREATE2 in the deploy chain, you have another option: you could verify the init code used to generate each of the contracts in the chain. How to do that is out of scope, but it involves verifying the init code cannot produce different bytecode for the contract. If you can prove an always-static result for the most-recent CREATE2 contract bytecode in the deployment chain, then you know that Wild Magic is impossible. (Thanks Nick for identifying a bug in an earlier version.)

If you have to interact with a destructible contract, and the deployment chain audit fails (so Wild Magic is possible), then you may only have one more option: verify that the contract bytecode hasn’t changed before interacting with it.

Verifying the Target Bytecode

Before calling a contract, you run an audit on the deployed bytecode. After the audit, keep a hash of the audited code. You can use another new opcode in Constantinople, EXTCODEHASH - which helps you cheaply confirm that the code hasn’t changed. Note that using EXTCODEHASH is not an option when you invoke a contract directly from an EOA. Any untrusted contract should be interacted with through a proxy that does this bytecode check. (Due to front-running attacks, you cannot get any confidence from verifying bytecode outside the EVM)

Of course, this means that your contract becomes tightly-coupled to the implementation of the target contract. That’s an unfortunate pattern, but if the contract is destructible, and the init code is malleable, it seems to be your only option.