Metamorphic Contracts

Breaking down metamorphic contracts for a beginner.

Jayakumar
Coinmonks
Published in
5 min readJul 19, 2022

--

Previous article: https://medium.com/coinmonks/dark-side-of-create2-opcode-6b6838a42d71

Previous article TL;DR: we have deployed and destroyed 2 contracts and deployed them in the same address using create and create2 opcodes.

Before continuing I wish to address a common question related to previous article.

What was the previous article trying to say ( Non Ethereum developers)?

Smart contract are entirely mutable if written in such a way.

Okay now, let’s move on. First let’s start with deploying entirely new code to the target address

Follow the steps in the previous article till step 4 and now redeploy the CreatorContract and instead of deploying the target contract’s code, deploy any code it would still be deployed at same address.

Here is the code for it.

// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.12;contract Target {address public owner;constructor() {owner = msg.sender;}function approve (address _spender,uint _value) public  returns (string memory) {return "Thug Life BGM playing...";}function destroy() public {selfdestruct(payable(msg.sender));}}

Transaction hashes:

Target contract self-destruct - 0x3cb50d84b48c2ec1b6d0c58a925fe6fb56d515aedbcd843d5e0bb9e562378a20

Target contract deployment - 0xb311cd648cdb08313e90abeefa27b54f68128543f5b484eb515d84ff974a5388

Observations:

  1. Contract nonces increases only when it creates new contracts using create or create2 opcode.
  2. Contracts are initialised with nonce 1 in the newer contracts and old contracts are initialised with nonce 0.(for greater context read → EIP-161)
  3. Any contracts with self-destruct or the contracts that interacts with other contracts via delegate call is unsafe.
  4. Address changes when Create2 constructor arguments changes.

5. Why can’t we use this instead of proxies ? → answer is that previous state stored in the contract gets destroyed and that is not something we require.

Metamorphic contracts :

  • What we saw above is how can you redeploy contracts with different code in the same address using create opcode .
  • Let us now look into how to redeploy contracts with different code in the same address using create2 opcode .
  • Gist of metamorphic contract → The idea is to deploy a smart contract that, upon deployment, replaces its own bytecode with a different bytecode. So, the bytecode you run through CREATE2 is always the same, and that calls back to the Factory and replaces itself during deployment.
//SPDX-License-Identifier: MITpragma solidity 0.8.1;contract Factory {
mapping (address => address) _implementations;
event Deployed(address _addr);function deploy(uint salt, bytes calldata bytecode) public {bytes memory implInitCode = bytecode;// assign the initialization code for the metamorphic contract.
bytes memory metamorphicCode = (
hex"5860208158601c335a63aaf10f428752fa158151803b80938091923cf3"
);
// determine the address of the metamorphic contract.
address metamorphicContractAddress = _getMetamorphicContractAddress(salt, metamorphicCode);
// declare a variable for the address of the implementation contract.
address implementationContract;
// load implementation init code and length, then deploy via CREATE.
/* solhint-disable no-inline-assembly */
assembly {
let encoded_data := add(0x20, implInitCode) // load initialization code.
let encoded_size := mload(implInitCode) // load init code's length.
implementationContract := create( // call CREATE with 3 arguments.
0, // do not forward any endowment.
encoded_data, // pass in initialization code.
encoded_size // pass in init code's length.
)
} /* solhint-enable no-inline-assembly */
//first we deploy the code we want to deploy on a separate address
// store the implementation to be retrieved by the metamorphic contract.
_implementations[metamorphicContractAddress] = implementationContract;
address addr;
assembly {
let encoded_data := add(0x20, metamorphicCode) // load initialization code.
let encoded_size := mload(metamorphicCode) // load init code's length.
addr := create2(0, encoded_data, encoded_size, salt)
}
require(
addr == metamorphicContractAddress,
"Failed to deploy the new metamorphic contract."
);
emit Deployed(addr);
}
/**
* @dev Internal view function for calculating a metamorphic contract address
* given a particular salt.
*/
function _getMetamorphicContractAddress(
uint256 salt,
bytes memory metamorphicCode
) internal view returns (address) {
// determine the address of the metamorphic contract.
return address(
uint160( // downcast to match the address type.
uint256( // convert to uint to truncate upper digits.
keccak256( // compute the CREATE2 hash using 4 inputs.
abi.encodePacked( // pack all inputs to the hash together.
hex"ff", // start with 0xff to distinguish from RLP.
address(this), // this contract will be the caller.
salt, // pass in the supplied salt value.
keccak256(
abi.encodePacked(
metamorphicCode
)
) // the init code hash.
)
)
)
)
);
}
//those two functions are getting called by the metamorphic Contract
function getImplementation() external view returns (address implementation) {
return _implementations[msg.sender];
}
}contract Test1 {
uint public myUint;
function setUint(uint _myUint) public {
myUint = _myUint;
}
function killme() public {
selfdestruct(payable(msg.sender));
}
}
contract Test2 {
uint public myUint;
function setUint(uint _myUint) public {
myUint = 2*_myUint;
}
function killme() public {
selfdestruct(payable(msg.sender));
}
}
  1. Deploy the Factory
  2. use Test1 bytecode with salt=1 to deploy the Test1.
  3. Tell Remix that Test1 runs on the address of the Metamorphic contract
  4. Set the “myUint” to whatever value you want, it works
  5. Kill Test1
  6. Deploy Test2 bytecode using the same salt=1
  7. It will deploy a different bytecode to the same address!!!
  8. Get comfortable that setUint now doubles the input amount.

Observations :

  1. There are two types of contract bytecode (i.e)
  • CreationCode
  • RuntimeCode

2. Creation Code →

  • Memory byte array that contains the creation bytecode of the contract.
  • This can be used in inline assembly to build custom creation routines, especially by using the create2 opcode.
  • This property can not be accessed in the contract itself or any derived contract.
  • It causes the bytecode to be included in the bytecode of the call site and thus circular references like that are not possible.

3. RuntimeCode →

  • Memory byte array that contains the runtime bytecode of the contract.
  • This is the code that is usually deployed by the constructor of C.
  • If C has a constructor that uses inline assembly, this might be different from the actually deployed bytecode.
  • Also note that libraries modify their runtime bytecode at time of deployment to guard against regular calls.
  • The same restrictions as with .creationCode also apply for this property.

Due to the above mentioned reasons i’m currently unable to verify the create2 metamorphic contracts on polyscan. In the next article I would breakdown

“ 5860208158601c335a63aaf10f428752fa158151803b80938091923cf3” this piece of bytecode at evm playground for better context.

New to trading? Try crypto trading bots or copy trading

For in-depth understanding kindly head over to https://github.com/0age/metamorphic

Detailed breakdown of metamorphic contract working is in this repo and I will post the next article which would breaking this repo.( PS → it is well commented repo )

Good articles to read more about this topic :

  1. https://medium.com/@jason.carver/defend-against-wild-magic-in-the-next-ethereum-upgrade-b008247839d2.
  2. https://medium.com/zeppelin-blog/the-promise-and-the-peril-of-metamorphic-contracts-9eb8b8413c5e.

--

--

Jayakumar
Coinmonks

Blockchain Developer → Solidity || De-Fi || NFT || Audits