Deterministic Smart Contract Addresses

How to generate predictable smart contract addresses

Flavius Burca
#blockchain

There are two ways of deriving contract addresses when a new contract is being deployed by using the CREATE and CREATE2 opcodes. These two opcodes give us the ability to predict the address where a contract will be deployed before we deploy a smart contract.

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. This  opcode differs from other smart contract deployment methods in the sense that when contracts are deployed using this method, the address for the contract is generated by taking the last 20 bytes of the Keccak256 hash of the RPL-encoded deployer address and its transaction nonce. Roughly speaking, it can be generalized like:


new_address = keccak256(sender, nonce)

Therefore, for every deployer address, its nonce plays a role to determine the address of the contract being deployed. Because the sender's nonce increases with every transaction, predicting the deployment address becomes almost impossible.

Fortunately this is where CREATE2 comes in. The whole idea behind this opcode is to make the resulting address independent of future events. It was introduced in EIP1014 and it allows the creation of contracts with a deterministic address based on an input salt value. The address is determined by the last 20 bytes of the keccak-256 hash of the following: deployer address, salt, and contract initialization code. These three fields create an opportunity for determinism because developers are able to use brute force to determine the salt value (a random 32-byte hex code), which would generate the address of choice.


new_address = keccak256(0xFF, sender, salt, bytecode)

Basically, if the salt and bytecode are known beforehand, one can easily derive the contract address.

CREATE2 can only be used from a smart contract. This means that we need a Deployer contract that will perform the actual deployment of our target contract SimpleContract:


contract SimpleContract {
    uint public x;
    constructor(uint a) {
        x = a;
    }
}

contract Deployer {
    function getDeploymentAddress(
        bytes memory bytecode,
        uint _salt
    ) public view returns (address) {
        bytes32 hash = keccak256(
            abi.encodePacked(
                bytes1(0xff),
                address(this),
                _salt,
                keccak256(bytecode)
            )
        );

        return address(uint160(uint256(hash)));
    }

    function deploy(
        bytes memory bytecode,
        uint _salt
    ) public payable{
        address addr;

        assembly {
            addr := create2(
                callvalue(), // wei sent with current call
                add(bytecode, 0x20), // Actual code starts after skipping the first 32 bytes
                mload(bytecode), // Load the size of code contained in the first 32 bytes
                _salt
            )

            if iszero(extcodesize(addr)) {
                revert(0, 0)
            }
        }

        emit Deployed(addr);
    }

    event Deployed(address addr);
}

Using HardHat, the code to perform the actual deployment and retrieve the address is :


const hre = require("hardhat");
const {ethers} = require("hardhat");

async function main() {
    const Deployer = await hre.ethers.getContractFactory("Deployer");
    const deployer = await Deployer.deploy();

    const SimpleContract = await hre.ethers.getContractFactory("SimpleContract");
    const bytecode = SimpleContract.bytecode;

    // A random salt value. Play with this to get different deployment addresses
    const salt = "0x5ae426a214653200000000000000000000000000000000000000000000000000";

    // SimpleContract constructor argument
    const args = 1;

    // The init code is the bytecode + constructor arguments
    const initCode = bytecode + hre.ethers.utils.defaultAbiCoder.encode(["uint"], [args]).slice(2);

    // ethers has a useful function to compute the deployment address
    const expectedAddress = hre.ethers.utils.getCreate2Address(deployer.address, salt, hre.ethers.utils.keccak256(initCode));
    console.log(expectedAddress);

    // we can also use the Deployer contract to compute the deployment address
    const deploymentAddress = await deployer.getDeploymentAddress(initCode, salt);
    console.log(deploymentAddress);

    // perform the deployment and retrieve the deployed contract address from the Deployed event
    const tx = await deployer.deploy(initCode, salt);
    await tx.wait();
    let receipt = await ethers.provider.getTransactionReceipt(tx.hash);
    const fragment = deployer.interface.getEvent("Deployed");
    const topic = deployer.interface.getEventTopic(fragment);
    const logs = receipt.logs;
    for (let i = 0; i < logs.length; i += 1) {
        if (logs[i].topics[0] === topic) {
            const decoded = ethers.utils.defaultAbiCoder.decode(fragment.inputs, logs[i].data);
            const deployedAddress = decoded['addr'];
            console.log(deployedAddress);
            break;
        }
    }

}

main().catch((error) => {
    console.error(error);
    process.exitCode = 1;
});


The three console.log statements should output the same address.