Metamorphic contracts are a revolutionary concept in the realm of smart contracts, introducing dynamic adaptability and flexibility. Unlike traditional contracts with fixed terms, metamorphic contracts can autonomously modify their code and behaviour based on predefined triggers or conditions. This feature allows them to adjust and evolve in response to changing circumstances, making them highly versatile and adaptable.
Metamorphic contracts offer unparalleled adaptability and flexibility, revolutionising the concept of smart contracts. They enable versatile applications across various industries. For instance, in subscription-based services, the contract can automatically adjust pricing, terms, or service levels based on user behaviour or market conditions. In supply chain management, the contract can adapt to changes in logistics, pricing agreements, or regulatory requirements. DeFi applications can benefit from self-modifying contracts by updating interest rates, collateral requirements, or other parameters in response to market fluctuations.
Key concepts and design patterns for metamorphic contracts
Key concepts and design patterns are crucial for the successful implementation of metamorphic contracts. Conditionals and triggers within the contract’s code define the circumstances under which the contract modifies its behaviour or updates its terms. Separating core logic from upgradeable modules enhances security and maintainability, enabling smooth upgrades while ensuring backward compatibility. Smart contract upgradability and modularity play pivotal roles in the effectiveness of metamorphic contracts. Upgradability empowers contract owners to introduce changes or improvements without redeployment, minimising disruptions to ongoing transactions. Modular design allows for the independent upgradability of specific contract functionalities, providing granular control over adaptive features.
Creating metamorphic Contracts
There are various ways to create metamorphic contracts, but a relatively straightforward approach involves the following steps:
Initially, deploy an implementation contract that doesn’t rely on a constructor but can have a regular function (such as
initialize) that performs a similar role. This implementation contract should also have the ability to self-destruct.
Store a reference to the implementation contract’s address in storage at a fixed, known location. The factory contract, which initiates the
CREATE2call, is a suitable choice for this purpose.
CREATE2to deploy a metamorphic contract with fixed, non-deterministic initialization code. This code retrieves the implementation address from the factory function, clones the runtime bytecode at that location, and deploys the runtime bytecode for the metamorphic contract. Alternatively, you can use an intermediate transient contract with fixed initialization code that deploys the metamorphic contract using CREATE and then immediately self-destructs.
When you need to change the metamorphic contract, simply
self-destructthe existing contract, deploy and reference a new implementation, and redeploy the contract. As the initialization code remains the same, the address of the metamorphic contract will also remain unchanged.
Changing Contract Bytecode Using CREATE2, CREATE, and SELFDESTRUCT
In Ethereum, it is possible to modify the bytecode of a fixed account by utilizing a sequence of
SELFDESTRUCT calls. These operations involve deploying and destructing contracts in a specific order. Understanding these operations is essential for comprehending the concepts discussed in this article. While the Ethereum documentation provides detailed explanations, here’s a brief overview:
CREATE2 are unique Ethereum Virtual Machine (EVM) operations used to create new accounts. A contract can only be created if the targeted account is considered “empty,” meaning it has a codesize of 0 and a nonce of 0. When a contract is created, its codesize becomes greater than 0, and the nonce becomes 1. Although this information is commonly known, it is worth highlighting a few key details about
CREATE: It calculates the address of the new contract using the formula keccak256(deployer_addr, deployer_nonce) (specifically, keccak256(rlp([deployer_addr, deployer_nonce]))[12:]).
CREATE2: It calculates the address of the new contract using the formula keccak256(_0xFF, deployer_addr, salt, bytecode) (specifically, keccak256(_0xFF, deployer_addr, salt, bytecode)[12:]).
SELFDESTRUCT “clears” an account by resetting its bytecode and nonce. For the purposes of this article, it is crucial to note that
SELFDESTRUCT resets the nonce of the account, enabling the use of
CREATE multiple times from the same address with the same nonce.
Using the sequence
SELFDESTRUCT -> … with the same bytecode from the same address results in each new CREATE operation deploying a contract to a new address because the deployer_nonce increases with each transaction.
On the other hand, if we utilize
SELFDESTRUCT -> … with the same bytecode and salt, the resulting address will remain the same in each iteration.
However, it is worth noting that in the first case (with
CREATE), the deployment address can be the same if the “deployer_nonce” remains unchanged. Here’s the trick: using
SELFDESTRUCT, we can reset the nonce of the address where the contract created with
CREATE2 resides, allowing it to be redeployed. Then, using
CREATE, we can deploy a new contract from the same address but with different code.
By combining these operations, we can implement the following example scenario:
CREATE2. The new contract account has a nonce of 1 (according to EIP161).
MutDeployer deploys MutableV1 (the first version of mutable code) using
CREATE. The new contract is deployed at the address keccak256(MutDeployerAddr, MutDeployerNonce == 1).
Users interact with
MutableV1, assuming that its code remains constant.
The owner executes
MutableV1to later deploy MutableV2 at the same address.
The owner executes
MutDeployer, making its account empty (codesize == 0 and nonce == 0).
The user repeats step 1, deploying the same MutDeployer contract using CREATE2 at the same address. Now, MutDeployer has a nonce of 1, just like in step 1.
MutableV2with new bytecode using CREATE (similar to step 2). The new contract is deployed at the same address, keccak256(MutDeployerAddr, MutDeployerNonce == 1).
Users continue to interact with
MutableV2using the same address as MutableV1.
Please note that this process allows for the modification of contract code while maintaining the same address, enabling seamless updates to contracts without requiring users to change the address they interact with. The same scenario in code:
Potential risks and vulnerabilities associated with dynamic code modification
However, dynamic code modification also introduces potential risks and vulnerabilities. Ensuring the security and integrity of adaptive capabilities becomes crucial, necessitating robust security measures and thorough testing to prevent unauthorized modifications or exploits. The complexity of metamorphic contracts raises legal and regulatory concerns regarding enforceability and contractual obligations. Collaboration with regulatory bodies and careful legal analysis are imperative to ensure compliance. Striking a balance between autonomy and governance is vital to prevent unintended consequences or abuse of adaptive features, necessitating clear governance frameworks and oversight mechanisms.
How to defend from malicious metamorphic contracts
To protect against metamorphic contracts, thorough checks and detection mechanisms are necessary. The detection process is extensively explained in the detector and article mentioned, which allows verification of whether an address can have mutable code. Here is a summary of the key checks:
Ensure that the contract does not contain the
SELFDESTRUCTopcode or utilize
DELEGATECALLto a contract with
CREATE2was used for deployment, among other indicators.
Ensure that the contract was initially deployed from a source that doesn’t allow redeployments. This can be achieved by avoiding the use of
CREATE2or by keeping track of each deployment and preventing duplicate deployments. Additionally, you need to verify that the deployer itself is not capable of metamorphosis.
Before proceeding with the transaction, confirm that the contract you’re interacting with hasn’t changed using methods like
EXTCODEHASHor similar mechanisms at the beginning of the transaction.
For most legitimate applications of
CREATE2, such as in-state channels and for counterfactual instantiation, these precautions shouldn’t pose significant challenges. In general, exercise caution when interacting with any contract that can self-destruct or undergo dangerous changes. However, if you’re seeking a lightweight upgradeable contract with appropriate controls and governance, then you need not look any further.
By conducting these checks, you can ascertain that the contract’s code is immutable. However, it’s important to note that in security scenarios, there is always a possibility of avoiding detection by constructing complex scenarios using
DELEGATECALL chains or other techniques. Therefore, caution should be exercised.
The Ugly Step-Sibling to Transparent Proxies
To summarize the comparison between metamorphic contracts and transparent proxies, we can highlight the following points:
Storage persistence: Transparent proxies preserve storage upon upgrades, whereas metamorphic contracts completely wipe the state, including the account balance. Transparent proxies are commonly used for upgradeable ERC20 or ERC721 contracts, while metamorphic contracts may be more suitable for ERC725 identity contracts or other self-sovereign contracts.
Overhead of contract calls: Calling into a metamorphic contract incurs less overhead compared to calling into a transparent proxy. Transparent proxies need to verify the caller and then delegate the call to a logic contract.
Upgrade process: Upgrading metamorphic contracts is less seamless because
selfdestructoperations are recorded in the transaction substate and executed at the end of a transaction. This means that an upgrade requires two transactions, resulting in a temporary empty contract code (which can be susceptible to intermediate usage) between the transactions. On the other hand, if a transparent proxy encounters a
selfdestruct, it will be completely destroyed, whereas a metamorphic contract can still be recovered.
Constructor usage: Neither method allows the constructor to be used during contract initialization. Instead, an initialize function is typically used immediately after setting up the new contract with the cloned implementation. The exception to this rule is when using an intermediate transient contract deployed via CREATE2 to deploy the metamorphic contract via CREATE. In such cases, constructors can still be used during metamorphic contract deployment.
Please note that these are general considerations, and the suitability of each approach depends on the specific requirements and use cases of the contract being developed.
A case study: the attack on Tornado Cash Governance contracts.
An individual with malicious intentions successfully submitted a deceitful proposal that went unnoticed and was subsequently approved by the voters of the DAO token. As a result, they managed to obtain 1.2 million votes and take control of the situation.
Through a series of unforeseen events, Tornado Cash’s governance has been hijacked by means of a deceptive proposal, resembling a trojan horse. Consequently, this proposal has effectively bestowed complete control of the DAO upon a single address.
Despite the smart contracts preventing the draining of approximately $275 million from the privacy pools, the exploiter managed to obtain control over the TORN governance token. This granted them the ability to modify the router and redirect deposits and withdrawals, as well as administrative privileges over Nova, the Gnosis chain deployment.
Nevertheless, there is still a glimmer of hope, suggesting that not all hope is lost.
Shortly before midday UTC, the exploiter released a new proposal to undo the previously made alterations.
Assuming there are no unforeseen complications, this turn of events could be considered a fortunate escape for the Tornado Cash community, avoiding potentially disastrous consequences.
The past year has presented numerous challenges for the prominent crypto mixer in the DeFi ecosystem.
Throughout the year, DeFi’s go-to crypto mixer has faced a series of difficulties. These include the imposition of OFAC sanctions in August and the incarceration of core developer Alexey Pertsev, who has since been released pending trial.
In addition, concerns arose a few weeks ago regarding potential exploitation of Tornado’s governance system. This involved the creation of multiple addresses and the locking of 0 TORN tokens in the governance vault. However, since no immediate consequences were observed, it was ultimately disregarded as an unsuccessful endeavor.
Decoy Or perhaps Or preparatory move ?
The addresses of the exploiters involved are as follows:
Exploiter address 1:
Exploiter address 2:
The Proposal contract:
The attack was executed discreetly, concealed within a proposal aimed at penalizing specific relayers suspected of dishonest behavior. Although the code appeared to employ similar logic to a previous proposal, the attacker had incorporated an additional function that enabled them to trigger self-destruction of the contract.
The proposal contract was released through a deployer contract, utilizing a combination of CREATE and CREATE2 opcodes. Leveraging the deterministic deployment process, the attacker exploited the ability to deploy new code to the address authorized by the governance.
By leveraging the selfDestruct function, the hacker successfully eradicated the authorized code, thereby resetting their nonce in the process. This manipulation enabled them to redeploy the malicious contract at the exact same address.
Option A: Use CREATE2 opcode to create a malicious proposal contract. This will raise a flag since CREATE2 and self-destruct are used together.
Option B: Use CREATE2 to create the deployer contract (0x7dc8), which further deploys the malicious proposal contract (0xc503) using CREATE.
Then the deployer contract is self-destructed (to reset the nonce), and it can create a new proposal at the same address (0xc503).
The reason behind this lies in the fact that the address of the deployed contract using CREATE is determined by the sender and nonce value (in this case, nonce = 1). Exploiting this, the attacker was able to generate a new proposal with an identical address (0xc503).
These techniques for creating ‘metamorphic contracts’ are one reason that some have called for the selfDestruct opcode to be deprecated.
The malicious proposal then assigned 10,000 TORN to all addresses created in last week’s (presumed) failed exploit. These were unlocked and withdrawn from the vault, granting the exploiter 1.2M votes (against 700k legitimate votes) and full control of Tornado governance.
Refer to BlockSec’s chart for a comprehensive breakdown of the various stages involved in the attack:
In an unforeseen twist, the exploiter eventually submitted a proposal to undo the consequences of their hostile takeover, effectively returning control to the DAO as it was prior to the attack.
Apart from the 430 ETH (~$750k) profit from dumping TORN (any guesses where the attacker chose to launder the profits?), the hacker may have other motivations:
Either they’re giga trolling or it will end up being an expensive but not disastrous lesson in Governance security.
As community member Tornadosaurus-Hex pointed out:
I mean note that we don’t even have a choice in regards to this proposal but it is still important nonetheless.
CONCLUSION (Importance of thorough testing and auditing)
Thorough testing and auditing are of paramount importance in mitigating risks associated with metamorphic contracts. Rigorous testing ensures the reliability and functionality of the contract’s adaptive capabilities. Comprehensive audits help identify vulnerabilities and potential exploits, bolstering the overall security of the contract.
SCT ITALIA, as a network of experts specializing in smart contract security, plays a crucial role in preventing such attacks. By conducting thorough security audits, they can identify vulnerabilities and weaknesses in smart contracts, helping to protect against malicious exploits and ensuring the integrity of governance systems. The incident with Tornado Cash emphasizes the necessity of such audits to maintain trust and security within the DeFi ecosystem.