合约部署合约

通过 new 创建合约 / create

使用关键字 new 可以创建一个新合约。待创建合约的完整代码必须事先知道,因此递归的创建依赖是不可能的。create主要有以下三种表现形式。

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

contract D {
    uint x;
    function D(uint a) payable {
        x = a;
    }
}

contract C {
    // 1.将作为合约的一部分执行
    D d = new D(4);

    // 2.方法内创建
    function createD1(uint arg) public {
        D newD = new D(arg);
    }

    // 3.方法内创建,并转账
    function createD2(uint arg, uint amount) public payable {
        //随合约的创建发送 ether
        D newD = (new D){value:amount}(arg);
    }
}

如示例中所示,通过使用 value 选项创建 D 的实例时可以附带发送 Ether,但是不能限制 gas 的数量。 如果创建失败(可能因为栈溢出,或没有足够的余额或其他问题),会引发异常。

这种方式也被称为 Factory 创建。工厂合约部署,也被称为 create,批量创建的时候使用,比如批量创建交易池,DeFi 类产品中批量创建借贷池等。

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

contract Account {
    address public deployer;
    address public owner;

    constructor(address _owner) payable {
        owner = _owner;
        deployer = msg.sender;
    }

    function getBalance() external view returns (uint256) {
        return address(this).balance;
    }
}

contract AccountFactory {
    Account[] public accounts;

    function deploy(address _owner) external payable {
        Account account = new Account{value: msg.value}(_owner);
        accounts.push(account);
    }
}

通过 salt 创建合约 / create2

在创建合约时,将根据创建合约的地址和每次创建合约交易时的 nonce 来计算合约的地址。如果你指定了一个可选的 salt (一个 bytes32 值),那么合约创建将使用另一种机制(create2)来生成新合约的地址:它将根据给定的 salt ,创建合约的字节码和构造函数参数来计算创建合约的地址。特别注意,这里不再使用 nonce

create2 的意义:可以在创建合约时提供更大的灵活性:你可以在创建新合约之前就推导出将要创建的合约地址。 甚至是还可以依赖此地址(即便它还不存在)来创建其他合约。一个主要用例场景是充当链下交互仲裁合约,仅在有争议时才需要创建。

案例演示 1

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

contract D {
    uint256 public x;

    constructor(uint256 a) {
        x = a;
    }
}

contract C {
    function createDSalted(bytes32 salt, uint256 arg) public {
        // 最新的语法
        D d = new D{salt: salt}(arg);

        // 之前的写法
        // 这个复杂的表达式只是告诉我们,如何预先计算地址。
        // 这里仅仅用来说明。 实际上,现在仅需要使用 `new D{salt: salt}(arg)` 即可.
        address predictedAddress = address(
            uint160(
                uint256(
                    keccak256(
                        abi.encodePacked(
                            bytes1(0xff),
                            address(this),
                            salt,
                            keccak256(
                                abi.encodePacked(type(D).creationCode, arg)
                            )
                        )
                    )
                )
            )
        );

        require(address(d) == predictedAddress);
    }
}

使用 create2 创建合约还有一些特别之处。 合约销毁后可以在同一地址重新创建。不过,即使创建字节码(creation bytecode)相同(这是要求,因为否则地址会发生变化),该新创建的合约也可能有不同的部署字节码(deployed bytecode)。 这是因为构造函数可以使用两次创建合约之间可能已更改的外部状态,并在存储合约时将其合并到部署字节码中。

小结

这种也被称为操作码部署,create 可以通过加入 salt,来预测即将生成的地址。这种创建就能预测生成地址的方式也被称为 create2 创建。

  • 加 salt ,salt 决定了合约地址,不能重复使用

    • 除非之前 salt 生成的合约被销毁了。

  • 即将部署的合约地址计算

    • uint160 格式就是地址格式了

下面是两者的简短总结:

  • 普通合约的地址生成方式: 部署者的地址 + 地址 nonce

  • 预测合约地址的方式:

bytes32 hash = keccak256(
    abi.encodePacked(
        bytes1(0xff), // 固定字符串
        address(this), // 当前工厂合约地址,固定写法
        _salt, // salt
        keccak256(bytecode) //部署合约的 bytecode
    )
);
return address(uint160(uint256(hash)));

案例代码 2

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

contract DeployWithCreate2 {
    address public deployer;
    address public owner;

    constructor(address _owner) payable {
        owner = _owner;
        deployer = msg.sender;
    }

    function getBalance() external view returns (uint256) {
        return address(this).balance;
    }
}

contract AccountFactory {
    DeployWithCreate2[] public accounts;

    function deploy(uint256 _salt) external payable {
        DeployWithCreate2 account = new DeployWithCreate2{
            salt: bytes32(_salt), // uint256 需要转为 bytes32
            value: msg.value
        }(msg.sender);
        accounts.push(account);
    }

    // 获取即将部署的地址
    function getAddress(bytes memory bytecode, uint256 _salt)
        external
        view
        returns (address)
    {
        bytes32 hash = keccak256(
            abi.encodePacked(
                bytes1(0xff), // 固定字符串
                address(this), // 当前工厂合约地址
                _salt, // salt
                keccak256(bytecode) //部署合约的 bytecode
            )
        );
        return address(uint160(uint256(hash)));
    }

    // 获取合约的 bytecode
    function getBytecode(address _owner) external pure returns (bytes memory) {
        bytes memory bytecode = type(DeployWithCreate2).creationCode;
        // 连接的参数使用 abi.encode
        return abi.encodePacked(bytecode, abi.encode(_owner));
    }
}

合约测试

  • address1 部署合约

    • address1: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4

  • 使用 address1 作为参数,获取 getBytecode 返回值。

  • 调用 getAddress

    • bytecode 参数是 getBytecode 返回值

    • salt 参数是 1

    • 计算结果是: 0x0022172A008CEdf60B1770dDD987888e5663D1Cc

  • 调用 deploy,salt 参数是 1

  • 调用 accounts[0]

    • 返回的合约地址是 0x0022172A008CEdf60B1770dDD987888e5663D1Cc,和计算的完全一样。

  • 再次调用 deploy,salt 参数是 1

    • 返回失败 transact to AccountFactory.deploy errored: VM error: revert.

用 assembly 做 create

create 部署

  • Proxy: 部署合约的方法,和修改 owner

  • Helper: 生成部署用的 bytecode 和修改 owner 的 data

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

contract Test1 {
    address public owner = msg.sender;

    function setOwner(address _owner) public {
        require(msg.sender == owner, "now owner");
        owner = _owner;
    }
}

contract Test2 {
    address public owner = msg.sender;
    uint256 public value = msg.value;
    uint256 public x;
    uint256 public y;

    constructor(uint256 _x, uint256 _y) {
        x = _x;
        y = _y;
    }
}

// contract Proxy {
//     function depolyTest1() external {
//         new Test1();
//     }

//     function depolyTest2() external payable {
//         new Test2(1, 2);
//     }
// }

// assembly 部署
contract Proxy {
    event Depoly(address);

    // fallback() external payable {}

    function depoly(bytes memory _code)
        external
        payable
        returns (address adds)
    {
        assembly {
            // create(v,p,n);
            // v 是 发送的ETH值
            // p 是 内存中机器码开始的位置
            // n 是 内存中机器码的大小
            // msg.value 不能使用,需要用 callvalue()
            adds := create(callvalue(), add(_code, 0x20), mload(_code))
        }

        require(adds != address(0), "Depoly Failed");
        emit Depoly(adds);
    }

    // 跳用
    function execute(address _target, bytes memory _data) external payable {
        (bool success, ) = _target.call{value: msg.value}(_data);
        require(success, "Failed");
    }
}

contract Helper {
    // 生成 type(contract).creationCode
    function getBytescode1() external pure returns (bytes memory bytecode) {
        bytecode = type(Test1).creationCode;
    }

    // 生成构造函数带有参数的 bytecode,参数连接后面就可以了
    function getBytescode2(uint256 _x, uint256 _y)
        external
        pure
        returns (bytes memory)
    {
        bytes memory bytecode = type(Test2).creationCode;
        // abi 全局变量
        return abi.encodePacked(bytecode, abi.encode(_x, _y));
    }

    // 调用合约方法的calldata,使用 abi.encodeWithSignature
    function getCalldata(address _owner) external pure returns (bytes memory) {
        return abi.encodeWithSignature("setOwner(address)", _owner);
    }
}

测试部署

前提条件:部署 Helper 和 Proxy 合约。

  1. 通过 getBytescode1 ,获取 Test1 需要的 bytecode

  2. 部署 Test1

  3. 获取 Test1 合约地址

  4. At Test1 Address

  5. 获取 Test1 owner 地址

  6. 通过 getCalldata ,获取 Test1 setOwner 需要的 bytecode。参数是想要设置的 Owner 地址。

  7. 执行 execute(),参数是 Test1 合约地址 和 getCalldata 返回值。

合约 2

  1. 通过 getBytescode2 ,获取 Test2 需要的 bytecode

  2. 部署 Test2,需要设置 x, y 的值,可以选择支付 ETH。

  3. 获取 Test2 合约地址

  4. At Test2 Address

  5. 查看 Test2 的值

用 assembly 做 create2

UniswapV2Factory 的创建 pair 代码如下

function createPair(address tokenA, address tokenB) external returns (address pair) {
    require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
    (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
    require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');

    // single check is sufficient
    require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); 
    bytes memory bytecode = type(UniswapV2Pair).creationCode;
    bytes32 salt = keccak256(abi.encodePacked(token0, token1));
    assembly {
        pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
    }
    IUniswapV2Pair(pair).initialize(token0, token1);
    getPair[token0][token1] = pair;
    getPair[token1][token0] = pair; // populate mapping in the reverse direction
    allPairs.push(pair);
    emit PairCreated(token0, token1, pair, allPairs.length);
}

创建合约的扩展

可以通过以太坊交易从外部或从 Solidity 合约内部创建合约。

一些集成开发环境,例如 Remix, 通过使用一些 UI 用户界面使创建合约的过程更加顺畅。 在以太坊上通过编程创建合约最好使用 JavaScript API web3.js。 现在,我们已经有了一个叫做 web3.eth.Contract 的方法能够更容易的创建合约。

创建合约时, 合约的构造函数 (一个用关键字 constructor 声明的函数)会执行一次。 构造函数是可选的。只允许有一个构造函数,这意味着不支持重载。构造函数执行完毕后,合约的最终代码将部署到区块链上。此代码包括所有公共和外部函数以及所有可以通过函数调用访问的函数。 部署的代码没有 包括构造函数代码或构造函数调用的内部函数。

在内部,构造函数参数在合约代码之后通过 ABI 编码 传递,但是如果你使用 web3.js 则不必关心这个问题。

问答题

  • 通过 new 创建合约 / create的方法

    • 1 将作为合约的一部分执行 D d = new D(4);

    • 2 方法内创建

      function createD(uint arg) public {
          D newD = new D(arg);
      }
      
    • 2 方法内创建,并转账

      function createD2(uint arg, uint amount) public payable {
          //随合约的创建发送 ether
          D newD = (new D){value:amount}(arg);
      }
      
  • create / create2 区别

    • 在创建合约时,将根据创建合约的地址和每次创建合约交易时的 nonce 来计算合约的地址。如果你指定了一个可选的 salt (一个 bytes32 值),那么合约创建将使用另一种机制(create2)来生成新合约的地址:它将根据给定的 salt ,创建合约的字节码和构造函数参数来计算创建合约的地址。特别注意,这里不再使用 nonce

  • create2 的意义

    • 可以在创建合约时提供更大的灵活性:你可以在创建新合约之前就推导出将要创建的合约地址。甚至是还可以依赖此地址(即便它还不存在)来创建其他合约。一个主要用例场景是充当链下交互仲裁合约,仅在有争议时才需要创建。

  • create2 有什么需要注意的?

    • 不能重复使用 salt

    • 使用 create2 创建合约还有一些特别之处。 合约销毁后可以在同一地址重新创建。不过,即使创建字节码(creation bytecode)相同(这是要求,因为否则地址会发生变化),该新创建的合约也可能有不同的部署字节码(deployed bytecode)。 这是因为构造函数可以使用两次创建合约之间可能已更改的外部状态,并在存储合约时将其合并到部署字节码中。

  • 如何预测 create2 合约地址

    • 核心如下

      bytes32 hash = keccak256(
          abi.encodePacked(
              bytes1(0xff), // 固定字符串
              address(this), // 当前工厂合约地址,固定写法
              _salt, // salt
              keccak256(bytecode) //部署合约的 bytecode
          )
      );
      return address(uint160(uint256(hash)));
      
    • 精确写法

      // SPDX-License-Identifier: MIT
      pragma solidity ^0.8.18;
      
      contract D {
          uint public x;
          constructor(uint a) {
              x = a;
          }
      }
      
      contract C {
          function createDSalted(bytes32 salt, uint arg) public {
              /// 这个复杂的表达式只是告诉我们,如何预先计算地址。
              /// 这里仅仅用来说明。
              /// 实际上,你仅仅需要 ``new D{salt: salt}(arg)``.
              address predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
                  bytes1(0xff), // 固定字符串
                  address(this), // 当前工厂合约地址,固定写法
                  salt, // salt
                  //部署合约的 bytecode
                  keccak256(abi.encodePacked(
                      type(D).creationCode,
                      arg
                  ))
              )))));
      
              D d = new D{salt: salt}(arg);
              require(address(d) == predictedAddress);
          }
      }