Table of contents
- Detecting metamorphic smart contracts
- How a malicious actor can use metamorphic contracts to steal people’s funds
- How CREATE2 opens up the possibility of metamorphism
- How a metamorphic contract actually works
A critical Ethereum security assumption is that smart contract code is immutable and therefore cannot be changed once it is deployed on the blockchain. In practice, some smart contracts can change – even after they’ve been deployed. With a few clever tricks, you can create metamorphic smart contracts that “metamorphose” into something else – and by understanding what makes them possible, you can detect them.
Metamorphic smart contracts are mutable, meaning developers can change the code inside them. These smart contracts pose a serious risk to web3 users who put their trust in code that they expect to run with absolute consistency, especially as bad actors can exploit this shape-shifting ability. Imagine an attacker using the technique to “rug” people who are staking tokens in a smart contract they don’t realize is metamorphic. Attacks based on this and similar premises could equip scammers to prey on people and generally undermine trust in the full promise of decentralized systems.
To analyze whether a smart contract contains metamorphic properties, I built a simple Metamorphic Contract Detector(inspired by and building on the original work of Jason Carver, 0age, and others). Anyone can use the tool to check whether a given contract exhibits red flags that could indicate the potential for metamorphism. The method is not fool-proof: just because a smart contract shows a flag, doesn’t mean it’s necessarily metamorphic; and just because it doesn’t, doesn’t mean it’s safe. The checker merely offers a quick initial assessment that a contract might be metamorphic based on possible indicators.
Web3 users should acquaint themselves with the threats posed by metamorphic contracts so they can look out for and avoid possible attacks. Wallets and blockchain indexers can help by warning users before they interact with a smart contract that may contain metamorphic properties. This tool is intended to help in both educating people about this potential threat… and defending against it.
The Metamorphic Contract Detector I built analyzes six properties that may indicate if a smart contract is metamorphic.
In addition to building a simple command line tool for the Metamorphic Contract Detector, I built some example smart contracts that demonstrate a scam metamorphic contract staking scenario, which I describe in the next section. All the code is available in this GitHub repository.
Here is how someone might use a metamorphic smart contract as part of a scam.
First is the setup phase. The attacker deploys a smart contract at a specific address on the blockchain using two tools: metamorphic bytecode and the CREATE2 opcode. (We’ll expand on both of these concepts later.) The metamorphic bytecode then does what its name suggests and “morphs.” Here, it changes into a staking contract where users can stake ERC-20 tokens. (Again, we’ll discuss the details of this morphing trick later. Promise!)
Next comes the bait and switch. Unsuspecting users stake their tokens in this contract, lured by the possibility of earning a yield or some other perk. The attacker then deletes all the staking code and “state” – blockchain storage or memory – at this smart contract address using the SELFDESTRUCT opcode discussed in the previous section. (It should be noted that the tokens – which exist as part of a separate ERC-20 contract – persist, unaffected by the self-destructed contract.)
Finally, the rug-pull. The attacker reuses the same metamorphic bytecode used in the setup phase to “redeploy” a new contract. This new contract deploys to the same address recently vacated by the self-destructing contract. This time, however, the bytecode “morphs” (again, we’ll explain how later) into a malicious contract that can steal all the tokens staked at the contract address. Scam complete.
The risks that metamorphic smart contracts pose are by now plainly apparent. But you may still be wondering, how does this metamorphism trick actually work? To understand that, you have to probe deeper, to the bytecode-level.
CREATE2 gives developers more control over the deployment of their smart contracts than they previously had. The original CREATE opcode makes it difficult for developers to control the destination address for a to-be-deployed smart contract. With CREATE2, people can control and know the address of a particular smart contract in advance, before actually deploying it to the blockchain. This foreknowledge – plus some clever tricks – is what enables people to create metamorphic smart contracts.
How can CREATE2 predict the future? The opcode’s calculation is deterministic: as long as the inputs do not change, the address determined by CREATE2 will not change. (Even the smallest change will cause the deployment to happen somewhere else.)
More granularly, CREATE2 is a function that combines and hashes together a few elements. First, it incorporates the address of the deployer (or sender): the initiating smart contract that acts as a parent to the one to be created. Next, it adds an arbitrary number provided by the sender (or “salt”), which allows the developer to deploy the same code to different addresses (by changing the salt) and prevents overwriting existing, identical contracts. Finally, it uses the keccak256 hash of some smart contract initialization (“init”) bytecode, which is the seed that turns into a new smart contract. This hashed-together combination determines an Ethereum address and then deploys the given bytecode to that address. As long as the bytecode remains exactly the same, CREATE2 will always deploy the given bytecode to the same address on the blockchain.
Here’s what the CREATE2 formula looks like. (Note: you’ll notice another element, a “0xFF,” in the example below. This is just a constant CREATE2 uses to prevent collisions with the preceding CREATE opcode.)
Now that we have a way to deploy code to a deterministic address, how is it possible to change the code at that same address? At first, this may seem impossible. If you want to deploy new code using CREATE2, the bytecode must change, and therefore, CREATE2 will deploy to a different address. But what if a developer constructed the bytecode in such a way that it could “morph” into different code when CREATE2 deploys a smart contract?
The recipe for turning a smart contract into a metamorphic contract calls for three smart contracts in total, each playing a unique role.
One of these necessary components is the Metamorphic Contract Factory, the brain of the operation. This “Factory” is responsible for deploying the Metamorphic Contract as well as another smart contract called the Implementation Contract, so named because its code eventually becomes implemented inside the Metamorphic Contract. A subtle choreography between these three contracts results in metamorphism, as depicted in the diagram below.Let’s discuss each step, 1-7, in detail to illuminate the operations at work.
Step 1: A developer sets everything in motion
A coder designs some smart contract code – the Implementation Contract bytecode – that will eventually end up in the Metamorphic Contract. The developer sends this code to the Metamorphic Contract Factory, a smart contract whose main purpose is to deploy other smart contracts. This action sets the entire Metamorphic Contract creation process in motion.Everything that follows is a result of this initial step. Indeed, Steps 1 through 6 happen in one atomic transaction on the blockchain, meaning nearly all at once. These steps can be repeated over and over again, ad infinitum, to replace code inside the Metamorphic Contract and keep it continually morphing.
Step 2: Factory deploys Implementation Contract
The first contract the Factory deploys is the Implementation Contract, which contains implementation code. (Creative, we know.) Think of the Implementation Contract as a loading dock, or waypoint, that holds some code before it ships to its final destination, which will be, in this case, inside the Metamorphic Contract.
Step 3: Factory stores Implementation Contract address
After its deployment to the blockchain, the Implementation Contract will necessarily exist at some blockchain address. The Factory stores this contract address in its own memory (to be used later, in Step 5).
Step 4: Factory deploys Metamorphic Contract
The Factory deploys the Metamorphic Contract using CREATE2 and metamorphic bytecode. You can find a technical, in-depth walkthrough of how metamorphic bytecode works here, but suffice it to say that when metamorphic bytecode executes, it copies code from some other on-chain location – in this case, from the Implementation Contract – into the Metamorphic Contract. As we talked about in the last section, since CREATE2 is deterministic – as long as the same sender, salt, and bytecode are used – then the Metamorphic Contract address stays the same no matter how many times these steps are repeated.Below is an example of what metamorphic bytecode looks like, from the metamorphic repo by 0age. This is just one example of metamorphic bytecode – potentially innumerable variations exist, vastly complicating the detection of metamorphic contracts.
Step 5: Metamorphic bytecode queries Factory for Implementation Contract address
The metamorphic bytecode asks the Factory for the Implementation Contract address (as stored in Step 3). It doesn’t matter if the address of the Implementation Contract changes as long as the metamorphic bytecode that asks for the address stays the same. Indeed, if the developer later deploys a new Implementation Contract – such as a malicious one designed to steal tokens – it will necessarily deploy at a different blockchain address, per Step 2. This has no impact on the Metamorphic Contract’s creation.
Step 6: Implementation Contract code gets copied into the Metamorphic Contract
Using the blockchain address learned in Step 5, the metamorphic bytecode locates the code in the Implementation Contract and copies that code into the Metamorphic Contract’s local storage. This is how the Metamorphic Contract shape-shifts: by copying code from the Implementation Contract.
Step 7: Rinse and repeat
A developer can repeat Steps 1 through 6 over and over again and replace the code in the Metamorphic Contract with whatever they like by way of a new Implementation Contract. All that’s needed is to use the SELFDESTRUCT opcode – or, more deviously, DELEGATECALL opcodes that ultimately result in a SELFDESTRUCT – to remove the pre-existing code in the Metamorphic Contract. By repeating the cycle with new Implementation Contract bytecode, the Metamorphic Contract will, like magic, morph!
Using this technique for creating metamorphic contracts, a clever developer can constantly shift the ground under web3 users’ feet. Consider, for example, the scam scenario again. A developer might first deploy the Implementation Contract with token-staking code that, through the circuitous path depicted in the graphic and elaborated in the steps above, ends up in the Metamorphic Contract. The scammer could later self-destruct this code and replace it by deploying a new Implementation Contract containing token-stealing code.
Whatever gets deployed in the Implementation Contract will ultimately end up in the Metamorphic Contract. That’s the essence of the trick.
Metamorphic smart contracts break the implicit web3 social contract that what you see is what you get. Similar to the way the shell game uses three moving cups to hide a ball, the interplay of the three contracts in the creation of a metamorphic contract makes it difficult to follow the contract’s true function. The shell game is a particularly apt comparison because confidence tricksters will often use sleight of hand and misdirection to ensure they win. In the web3 version, metamorphic contract writers can similarly make the “ball” – the implementation code, that is – vanish (read: self-destruct), and they can replace it with whatever they like.
The existence of metamorphic contracts means it’s possible for web3 users to enter into contracts that can change at will – that’s why this threat is so important to understand and defend against. My Metamorphic Contract Detector offers just a first step toward identifying metamorphic contracts by the sleight of hand they employ. There are several ways the detector could be improved in the future. For example, by recursively checking the Factory (or deployer contract) that created the Metamorphic Contract, one could see if the Factory is itself metamorphic. This feature would be a useful addition to an upgraded version 2 of the Detector.
It’s worth reiterating once again: This Detector tool is not fool-proof. The flags it catches aren’t all telltale signs of metamorphic potential, but they do offer clues. Identifying these flags is just the start for a more thorough inquiry. That’s why we expanded the Detector to search for flags that could easily generate false positives, like the presence of CREATE2 or DELEGATECALL opcodes. If you have suggestions for improving the tool or want to build on or add to this initial work, get in touch with me at firstname.lastname@example.org.
Editor: Robert Hackett @rhhackett
Acknowledgements: I want to give a HUGE shoutout and thank you to Robert Hackett, Eddy Lazzarin, Sam Ragsdale, Riyaz Faizullabhoy, Noah Citron, Mason Hall, and Daejun Park for valuable feedback and advice in making this post and tool come to life.
The views expressed here are those of the individual AH Capital Management, L.L.C. (“a16z”) personnel quoted and are not the views of a16z or its affiliates. Certain information contained in here has been obtained from third-party sources, including from portfolio companies of funds managed by a16z. While taken from sources believed to be reliable, a16z has not independently verified such information and makes no representations about the enduring accuracy of the information or its appropriateness for a given situation. In addition, this content may include third-party advertisements; a16z has not reviewed such advertisements and does not endorse any advertising content contained therein.
This content is provided for informational purposes only, and should not be relied upon as legal, business, investment, or tax advice. You should consult your own advisers as to those matters. References to any securities or digital assets are for illustrative purposes only, and do not constitute an investment recommendation or offer to provide investment advisory services. Furthermore, this content is not directed at nor intended for use by any investors or prospective investors, and may not under any circumstances be relied upon when making a decision to invest in any fund managed by a16z. (An offering to invest in an a16z fund will be made only by the private placement memorandum, subscription agreement, and other relevant documentation of any such fund and should be read in their entirety.) Any investments or portfolio companies mentioned, referred to, or described are not representative of all investments in vehicles managed by a16z, and there can be no assurance that the investments will be profitable or that other investments made in the future will have similar characteristics or results. A list of investments made by funds managed by Andreessen Horowitz (excluding investments for which the issuer has not provided permission for a16z to disclose publicly as well as unannounced investments in publicly traded digital assets) is available at https://a16z.com/investments/.
Charts and graphs provided within are for informational purposes solely and should not be relied upon when making any investment decision. Past performance is not indicative of future results. The content speaks only as of the date indicated. Any projections, estimates, forecasts, targets, prospects, and/or opinions expressed in these materials are subject to change without notice and may differ or be contrary to opinions expressed by others. Please see https://a16z.com/disclosures for additional important information.