Revamping the Foundation: Enhancing Smart Contract and Proxies for Future-proof Performance [7/8]
In this current publication, we intend to bring attention to comprehending Metamorphosis Smart Contracts using CREATE2.
Metamorphosis Smart Contracts using CREATE2
Until now, all Smart Contracts have been connected to another Smart Contract through delegatecall, whereby the proxy address remains unchanged and all requests are handled by the proxy. Is it possible to substitute an entire Smart Contract with a different one?
It has been discovered that a solution does exist, and it’s called “Metamorphosis Smart Contracts”.
By using the Metamorphosis Smart Contracts approach, a Smart Contract is deployed which deploys another Smart Contract and subsequently replaces its own bytecode with the bytecode of the newly deployed Smart Contract. This can be compared to how Jim communicates with Scotty to transport objects. Let’s delve deeper into how this process functions.
An Introduction to CREATE2: How It Functions
Here’s a brief overview of CREATE2’s functionality. CREATE2 is an assembly operation code designed for Solidity that enables the creation of a Smart Contract at a specified address. One of the benefits of using CREATE2 is that this address is predetermined.
Typically, the address of a Smart Contract is generated by combining the deployer’s address with a nonce, which increases incrementally. However, with CREATE2, there is no nonce, and a user-defined salt is used instead.
As a result, it’s possible to know the address of a Smart Contract in advance using CREATE2, which has the following specifications.
keccak256(0xff ++ deployersAddr ++ salt ++ keccak256(bytecode))[12:]
- 0xFF, a constant
- the address of the deployer, so the Smart Contracts address that sends the CREATE2
- A random salt
- And the hashed bytecode that will be deployed on that particular address
This will provide you with the location where the newly created Smart Contract has been deployed.
Let’s give it a shot:
To begin, we must have a factory contract that is capable of deploying contracts:
//SPDX-License-Identifier: MIT
pragma solidity 0.8.1;
contract Factory {
event Deployed(address _addr);
function deploy(uint salt, bytes calldata bytecode) public {
bytes memory implInitCode = bytecode;
address addr;
assembly {
let encoded_data := add(0x20, implInitCode) // load initialization code.
let encoded_size := mload(implInitCode) // load init code's length.
addr := create2(0, encoded_data, encoded_size, salt)
}
emit Deployed(addr);
}
}
It’s a relatively simple process. Once a new contract is deployed, we emit the address as an event. From there, we can utilize this address to deploy additional Smart Contracts. The location at which these Smart Contracts are deployed is predetermined, according to EIP-1014.
keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:]
Miguel Mota has done an excellent job creating a single function that calculates the address for CREATE2. However, we won’t be utilizing this function; instead, we’ll be taking a step-by-step approach!
To begin, let’s deploy the following Smart Contract with the Factory. Insert it into the current file.
contract NoConstructor {
uint public myUint = 5;
}
Next, navigate to the Solidity Compiler and copy the bytecode from the Web3-create, ensuring that you have selected the correct Contract.
Checkout the popup:
Afterward, proceed to the Deploy tab, and deploy the Factory initially. Then, utilize the bytecode to deploy the NoConstructor Contract using Create2.
At present, the salt is a numeric value, which can begin with any number — I’m starting with 1. This value is utilized to determine the final address of the contract. The bytecode, as previously mentioned, is just the bytecode we copied earlier. Select “transact” and access the Transaction details. It should display the address of the newly deployed NoConstructor contract via the Factory contract:
Calculating this address beforehand is quite simple! We can perform this task directly in the console of Remix:
factoryAddress = "ENTER_FACTORY_ADDRESS"
bytecode = "0x6080604052600560005534801561001557600080fd5b5060b3806100246000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c806306540f7e14602d575b600080fd5b60336047565b604051603e9190605a565b60405180910390f35b60005481565b6054816073565b82525050565b6000602082019050606d6000830184604d565b92915050565b600081905091905056fea264697066735822122019e87f67a50e9a888075265bb077e909763324a0aae35530f1359e047b40e06064736f6c63430008010033"
salt = 1;
"0x" + web3.utils.sha3('0xff' + factoryAddress.slice(2) + web3.eth.abi.encodeParameter('uint256',salt).slice(2).toString() + web3.utils.sha3(bytecode).slice(2).toString()).slice(-40);
Essentially, we can copy and paste each line in succession. The end outcome should be identical to the address emitted by the Factory Smart Contract:
CREATE2 with Constructor Argument
How does it work when a Constructor is involved? The process is somewhat different. Essentially, the data that the constructor receives as an argument must be attached to the init-bytecode. In other words, it should be appended. Let’s test it out by running an example.
Insert the following code into the already-existing file:
contract WithConstructor {
address public owner;
constructor(address _owner) {
owner = _owner;
}
}
If you wish to deploy this Smart Contract, you must include a properly encoded address at the end of it. How can you encode the address?
Begin by copying the address from the address dropdown. Then, input the following command into the console:
web3.eth.abi.encodeParameter(‘address’, “THE_ADDRESS”)
Next, copy the output while excluding the initial “0x,” and add it to the bytecode you’re deploying using the Factory contract.
In my scenario, I’m deploying the following bytecode and address combination:
0x608060405234801561001057600080fd5b506040516102043803806102048339818101604052810190610032919061008d565b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550506100ff565b600081519050610087816100e8565b92915050565b60006020828403121561009f57600080fd5b60006100ad84828501610078565b91505092915050565b60006100c1826100c8565b9050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6100f1816100b6565b81146100fc57600080fd5b50565b60f78061010d6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80638da5cb5b14602d575b600080fd5b60336047565b604051603e91906078565b60405180910390f35b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6072816091565b82525050565b6000602082019050608b6000830184606b565b92915050565b6000609a8260a1565b9050919050565b600073ffffffffffffffffffffffffffffffffffffffff8216905091905056fea26469706673582212207debf1ceacd0990dc89fd5c4d429bcd8cddbc1899ed06c9d40d571067827229764736f6c634300080100330000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4
So, now interact with the Smart Contract:
After executing the steps mentioned above, the output should display the address provided in the constructor.
That’s excellent news, because now you have an understanding of how to deploy Smart Contracts using a CREATE2 opcode. But, you can’t alter the bytecode, right? Since the hash of the bytecode is used to create the new contract address.
Well, that’s not entirely accurate. (I’m sure you suspected that there was a workaround…)
Overwriting Smart Contracts
Create a new file in Remix and include the subsequent smart contracts to see how the bytecode is replaced:
//SPDX-License-Identifier: MIT
pragma 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));
}
}
The smart contract described does two things: firstly, it contacts the msg.sender
to inquire an address, and secondly, it overwrites its own bytecode with the bytecode running on that address. This functionality can be used by deploying the contract and then using the resulting address as the bytecode to be deployed through CREATE2.
- Deploy the Factory
- use Test1 bytecode with salt=1 to deploy the Test1.
- Tell Remix that Test1 runs on the address of the Metamorphic contract
- Set the “myUint” to whatever value you want, it works
- Kill Test1
- Deploy Test2 bytecode using the same salt=1
- It will deploy a different bytecode to the same address!
- Get comfortable that setUint now doubles the input amount.
- Imagine what this does with a Token Contract you thought it safe to use.
The following passage discusses the potential dangers of using a metamorphic smart contract, which allows the bytecode to be changed after deployment. This could be problematic for contracts such as tokens or DeFi projects, as it could result in the loss of trust. It is suggested to look for a selfdestruct functionality and follow the chain of deployers to investigate if create2 was used. If a metamorphic smart contract is discovered, it is a red flag.
What follows is the next step?
In the next article, we will summarize everything we’ve covered and conclude the series of this articles!
Useful links :
Standards for Smart Contract Upgrades and Eternal Storage without Proxy
Storage and Storage Collisions, and elucidate the EIP-897 standard for our initial genuine Proxy
EIP-1822 UUPS standard and EIP-1967 Standard Proxy Storage Slots .
EIP-1538: Transparent Contract Standard and EIP-2535: Diamond Standard.
discord: am.dd#3991