Defend against “Wild Magic” in the next Ethereum upgrade

Jason Carver
9 min readFeb 13, 2019

--

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:

  1. Launch a DEX contract that promises to trade DAI for OMG
  2. The Dapp asks you to approve the contract to have access to your full DAI balance
  3. 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.

By validating the deployment history (ahead of time) or verifying the target bytecode (at runtime), you are only identifying that Wild Magic is impossible. But the contract still might have been destroyed and re-deployed with exactly the same bytecode, as a Zombie Contract.

Zombie Contracts

Even redeploying the same code to an address could be problematic in some constructions. For example, the same code could be deployed and then different state coerced. Even the same bytecode with the same storage could be problematic if revived. A couple examples:

Zombie Reset

Let’s imagine that there is a contract solely responsible for reporting on whether a shipper claims to have shipped your goods. Maybe it’s part of a larger constellation of escrow contracts. Something like:

pragma solidity ^0.5.3;contract ShipperConfirmed is mortal {
address public shipper;
bool public isConfirmed = false;

function burnInShipper(address new_shipper) external {
if (shipper == 0x0000000000000000000000000000000000000000) {
shipper = new_shipper;
}
}
function confirmShipment() external {
if (msg.sender == shipper) {
isConfirmed = true;
}
}

function shutdown() external {
if (msg.sender == 0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7) {
selfdestruct(msg.sender);
}
}
}

Now that you’re looking for it, I hope you see where this is going… At audit time, a shipper was set to an account you trust to report on shipping. Pre-Constantinople, you thought that the only way for isConfirmed() to get set to true was for that specific shipper to set it. Post-Constantinople, the black-hat can make a Zombie Contract: destroy the original contract, set himself as the new shipper, and confirm the shipment.

One mitigation is to look for any data mutations in a destructible contract. What about when the contract is indestructible?

Zombie Parent with Indestructible Child

Imagine that someone wanted to deploy a contract with a lot of state. They avoided selfdestruct() altogether, to ease Wild Magic & Zombie Contract fears. In order to get the state all deployed, they used a parent “omnipotent” contract that can arbitrarily set the state incrementally. Once the child has its state all set up, the parent is destroyed.

By now, I hope you’ve got the hang of this and spotted the problem right away. At audit time, the parent contract is destroyed, implying that its omnipotent powers have been revoked. That would have been true pre-Constantinople. Post-Constantinople, a black-hat could revive the parent contract and can modify the child at will. Simply redeploying the same parent code with the same state, adjacent to the contract under audit, is enough to take advantage of someone.

I certainly haven’t covered every possible angle. I am not a security professional. These examples are somewhat contrived and simple. Cleverer people would bury the nasty bits under some innocuous facade.

What To Do About It

(Updated) Since CREATE2 is staying in Constantinople, the main tool we have is communication & education. For posterity: my instinct was to modify the EIP to keep the contract nonce during self-destruct. That would have prevented all of the Wild Magic and Zombie misuse in this post. Unfortunately, no one in the call spoke up to agree that it was urgent enough to risk a change (I felt ambivalent myself, at this late stage).

Please make sure everyone you know is aware of how to think about contracts in a Post-Constantinople world. Wild Magic will become non-fiction around Feb 28, 2019!

A huge thank you to reviewers: Amiti, Alex Stokes, David Sanders, Brian Cloutier, and my lovely wife.

Do you have any more fun examples? Please comment on Reddit or Tweet me.

--

--