iT邦幫忙

2022 iThome 鐵人賽

DAY 13
0

Create2

Create2(EIP-1014)

create2 是 EVM 中的一個 OpCode,用於 Deploy 一個合約中的子合約,且其地址由 Deployer 決定而非任何區塊鏈上的 State,這代表使用 create2 創造出來的合約地址是可以預先計算出來的。

地址計算公式為:

keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:]
  • 0xff: 255,為一個常數
  • address: 20 bytes,此地只是 deploy New Contract 的 Deployer
  • salt: 32 bytes,User 提供的一個 input,通常為一個隨機數
  • init_code: 為一組創建合約用的 bytecode,constructor 和其參數都會被包含在 init_code 裡面,下文有範例可看

在 Solidity 中使用 create2 有幾種方法:

  1. inline assembly
  2. new contract

1. inline assembly

我們可以從 Precompute Contract Address with Create2 查看實例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Factory {
    // Returns the address of the newly deployed contract
    function deploy(
        address _owner,
        uint _foo,
        bytes32 _salt
    ) public payable returns (address) {
        // This syntax is a newer way to invoke create2 without assembly, you just need to pass salt
        // https://docs.soliditylang.org/en/latest/control-structures.html#salted-contract-creations-create2
        return address(new TestContract{salt: _salt}(_owner, _foo));
    }
}

// This is the older way of doing it using assembly
contract FactoryAssembly {
    event Deployed(address addr, uint salt);

    // 1. Get bytecode of contract to be deployed
    // NOTE: _owner and _foo are arguments of the TestContract's constructor
    function getBytecode(address _owner, uint _foo) public pure returns (bytes memory) {
        bytes memory bytecode = type(TestContract).creationCode;

        return abi.encodePacked(bytecode, abi.encode(_owner, _foo));
    }

    // 2. Compute the address of the contract to be deployed
    // NOTE: _salt is a random number used to create an address
    function getAddress(bytes memory bytecode, uint _salt)
        public
        view
        returns (address)
    {
        bytes32 hash = keccak256(
            abi.encodePacked(bytes1(0xff), address(this), _salt, keccak256(bytecode))
        );

        // NOTE: cast last 20 bytes of hash to address
        return address(uint160(uint(hash)));
    }

    // 3. Deploy the contract
    // NOTE:
    // Check the event log Deployed which contains the address of the deployed TestContract.
    // The address in the log should equal the address computed from above.
    function deploy(bytes memory bytecode, uint _salt) public payable {
        address addr;

        /*
        NOTE: How to call create2

        create2(v, p, n, s)
        create new contract with code at memory p to p + n
        and send v wei
        and return the new address
        where new address = first 20 bytes of keccak256(0xff + address(this) + s + keccak256(mem[p…(p+n)))
              s = big-endian 256-bit value
        */
        assembly {
            addr := create2(
                callvalue(), // wei sent with current call
                // Actual code starts after skipping the first 32 bytes
                add(bytecode, 0x20),
                mload(bytecode), // Load the size of code contained in the first 32 bytes
                _salt // Salt from function arguments
            )

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

        emit Deployed(addr, _salt);
    }
}

contract TestContract {
    address public owner;
    uint public foo;

    constructor(address _owner, uint _foo) payable {
        owner = _owner;
        foo = _foo;
    }

    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

上述 addr := create2(callvalue(), add(bytecode, 0x20), mload(bytecode), salt) 中:

  • add(bytecode, 0x20) 用來載入初始的 code
  • mload(bytecode) 用來載入初始 code 的 size
  • callvalue() 可以為 0
  • bytecode 為 constructor 和其參數一起編譯後產生的 bytecode

計算合約地址的實例:

function calCreate2Address(creatorAddress, salt, bytecode){
  shaContent = '0xff' 
    + creatorAddress.slice(2) 
    + web3.eth.abi.encodeParameter('uint256',salt).slice(2).toString() 
    + web3.utils.sha3(bytecode).slice(2).toString();
  addr = "0x" + web3.utils.sha3(shaContent).slice(-40);
  return addr;
}

2. new

以下內容參考自官方文件: Solidity - Creating Contracts via new

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract D {
    uint public x;
    constructor(uint a) payable {
        x = a;
    }
}

contract C {
    D d = new D(4); // will be executed as part of C's constructor

    function createD(uint arg) public {
        D newD = new D(arg);
        newD.x();
    }

    function createAndEndowD(uint arg, uint amount) public payable {
        // Send ether along with the creation
        D newD = new D{value: amount}(arg);
        newD.x();
    }
}

從以上的例子我們可以知道如果我們要使用 salt(a bytes32 value) 這個參數,就會是另外一個創建合約的機制,且這個過程不會讓 contract nonce 上升。 如果我們不斷從一個一樣的 sender 送出 create2,且參數都是一樣的 init_code 和 salt 就能夠生產合約到一樣的 address 上。如果合約擁有 selfdestruct 並且搭配其他鏈上狀態(區塊資訊或者其他合約的 body),就可以在同一個地址上不斷更換合約程式碼。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract D {
    uint public x;
    constructor(uint a) {
        x = a;
    }
}

contract C {
    function createDSalted(bytes32 salt, uint arg) public {
        // This complicated expression just tells you how the address
        // can be pre-computed. It is just there for illustration.
        // You actually only need ``new D{salt: salt}(arg)``.
        address predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
            bytes1(0xff),
            address(this),
            salt,
            keccak256(abi.encodePacked(
                type(D).creationCode,
                arg
            ))
        )))));

        D d = new D{salt: salt}(arg);
        require(address(d) == predictedAddress);
    }
}

Possible problems

  1. 如果合約裡面有 selfdesturct() 存在,那就有可能把合約銷毀並且重新佈署一次合約在相同地址上。
  2. 有沒有可能在相同合約地址上部屬不同合約,或者在 Proxy 中使用 create2
  3. 原本的 create opcode 是使用呼叫者合約的 nonce counter 還有其地址來算出目標合約的地址,無論合約創建成功與否 nonce 都會增加,這代表舊的 nonce 都不能再被用於交易。因此在 create2 之後有心人士可以找出一個加密參數,以此來故意去碰撞產生相同的佈署地址。

特別拉 Nonce 出來講,在 geth 中 CREATE 和 CREATE2 都是執行同一個 [create](https://github.com/ethereum/go-ethereum/blob/master/core/vm/evm.go#L408-L482),在 EIP158EIP-161: State trie clearing (invariant-preserving alternative) 規定了 contract nonce 是從 1 開始。

if evm.chainRules.IsEIP158 {
    evm.StateDB.SetNonce(address, 1)
}
...
ret, err := run(evm, contract, nil, false)

Closing

關於 contract nonce 在未來 nonce 的主題篇幅會講述。

Reference


最後歡迎大家拍打餵食大學生0x2b83c71A59b926137D3E1f37EF20394d0495d72d


上一篇
Day 12 - Contract Proxy: OpenZeppelin Upgradable Proxy
下一篇
Day 14 - Multi Signature
系列文
Smart Contract Development Breakdown30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言