Understanding Solidity’s create & create2 with Tornado Cash $1M Hack
Can we compute a smart contract address before deployment?
Go ahead, take a guess.
If you said yes, give yourself a pat on the back — because you’re right!
I used to believe smart contract addresses were generated randomly. Silly me!
In reality, smart contract addresses are deterministic. This means that given the deployer’s address and nonce, we can determine what the contract address will be.
In this Blog, I’ll explain this concept without diving too deeply into assembly language.
However, if you’re interested in delving deeper, I’ll share relevant resources for further exploration.
Create
The ‘create’ opcode is the most commonly used contract creation opcode. When contracts are deployed from scripts or other development environments, the create opcode is the low-level instruction executed by the EVM to deploy and generate a contract address
contract deployer {
function deploy() public {
address deployed_contract_address = address(new A());
// some logic ...
}
}
The contract address is computed by the EVM using the following logic:
contract_address = keccak256(deployer_address, nonce); // with RLP encoding of [sender, nonce]
We can replicate the same logic in Solidity using the following function:
function computeAddress(address _origin, uint _nonce) public pure returns (address) {
bytes memory data;
if (_nonce == 0x00) data = abi.encodePacked(bytes1(0xd6), bytes1(0x94), _origin, bytes1(0x80));
else if (_nonce <= 0x7f) data = abi.encodePacked(bytes1(0xd6), bytes1(0x94), _origin, uint8(_nonce));
else if (_nonce <= 0xff) data = abi.encodePacked(bytes1(0xd7), bytes1(0x94), _origin, bytes1(0x81), uint8(_nonce));
else if (_nonce <= 0xffff) data = abi.encodePacked(bytes1(0xd8), bytes1(0x94), _origin, bytes1(0x82), uint16(_nonce));
else if (_nonce <= 0xffffff) data = abi.encodePacked(bytes1(0xd9), bytes1(0x94), _origin, bytes1(0x83), uint24(_nonce));
else data = abi.encodePacked(bytes1(0xda), bytes1(0x94), _origin, bytes1(0x84), uint32(_nonce));
return address(uint160(uint256(keccak256(data))));
}
Create 2
1. A fixed prefix, which is always ‘0xFF’.
2. The sender’s address ensures the contract is tied to a specific creator.
3. A chosen salt value adds uniqueness to the contract address.
4. The bytecode contains the code of the new contract to be deployed.
By combining these parameters, CREATE2 computes a deterministic address for the new contract, which remains the same even as the blockchain evolves
contract deployer {
function deploy(uint256 salt) public {
address deployed_contract_address = address(new A{salt: bytes32(salt)}());
// some logic ...
}
}
The contract address is computed by the EVM using the following logic:
deployed_contract_address = keccak256(0xFF, deployer_address, salt, contract_bytecode);
We can replicate the same logic in Solidity using the following function:
function predictAddress(uint salt) public view returns( address){
return address(uint160(uint(keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
bytes32(salt),
keccak256(abi.encodePacked(
type(SimpleContract).creationCode,
abi.encode()
))
)))));
}
Note: You will find all the code in this repository
Deterministic smart contracts can be both a blessing and a curse.
Metamorphic Smart Contracts
with the same bytecode and salt, result in the same address in each iteration.
Tornado Cash Hack
The Tornado Cash hack exploited the use of metamorphic smart contracts. Here’s how the attack unfolded:
Step 1
1. Create a deployer contract.
2. Use `CREATE2` to deploy a buffer contract.
3. The buffer contract then deploys a proposal contract using `CREATE` (nonce=1).
Step 2
1. Once the proposal gets approved by the governance.
2. Self-destruct both the buffer and proposal contracts to reset the nonce.
Step 3
1. Deploy the buffer contract again using `CREATE2` (same as step 1).
2. The buffer contract now deploys malicious code (as the nonce is 1). The malicious code is deployed on the same address as the proposal contract, which was approved by the DAO governance contract.
3. the governance contract executes the logic in the malicious contract
The code for this attack can be found in my GitHub repository if you would like to replicate it.
Using the same steps above, the hacker managed to withdraw 100,000 TORN tokens worth nearly $1M.
There have been numerous hacks using `CREATE2`. Relevant information is attached below if you are interested.