After Constantinople, contracts can be upgraded in-place. Discover new and fun ways to lose your cryptoassets.
This article will assume that Constantinople is going forward as planned. My opinion is that we should remove EIP-1014 from Constantinople, which I’ll come back to at the end. But assuming it’s staying in, users need to learn 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.
Why the Wild Magic tangent? Because shape-shifting is a great analogy for something new coming in the next network upgrade.
CREATE2
introduces a new opcode, . 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 . 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 , 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 & ). 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 OMGVendorfunction shutdown() external
}
}
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
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 . 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 . 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. (Again, this has to be done in a chain back to the EOA) If you can prove an always-static result for the contract bytecode in the whole deployment chain, then you know that Wild Magic is impossible.
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, – 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.
Published at Wed, 13 Feb 2019 21:06:25 +0000
