Michael.W基于Foundry精读Openzeppelin第10期——Create2.sol

  • Michael.W
  • 更新于 2023-07-17 09:06
  • 阅读 2104

Create2库本质就是对EVM opcode CREATE2进行的一个封装,可以让开发者在非内联汇编环境下直接使用该opcode。 CREATE2是一种可提前计算合约部署地址的合约部署opcode。而传统的合约部署是通过opcode CREATE完成的。

0. 版本

[openzeppelin]:v4.8.3,[forge-std]:v1.5.6

0.1 Create2.sol

Github: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.8.3/contracts/utils/Create2.sol

Create2库本质就是对EVM opcode CREATE2进行的一个封装,可以让开发者在非内联汇编环境下直接使用该opcode。

CREATE2是一种可提前计算合约部署地址的合约部署opcode。而传统的合约部署是通过opcode CREATE完成的,部署的合约地址由deployer地址与当时交易的nonce值共同决定。

1. 目标合约

封装Create2 library成为一个可调用合约:

Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/src/utils/MockCreate2.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "openzeppelin-contracts/contracts/utils/Create2.sol";

contract MockCreate2 {
    function deploy(
        uint256 amount,
        bytes32 salt,
        bytes memory bytecode
    ) external returns (address) {
        return Create2.deploy(amount, salt, bytecode);
    }

    function computeAddress(bytes32 salt, bytes32 bytecodeHash) external view returns (address) {
        return Create2.computeAddress(salt, bytecodeHash);
    }

    function computeAddress(
        bytes32 salt,
        bytes32 bytecodeHash,
        address deployer
    ) external pure returns (address addr){
        return Create2.computeAddress(salt, bytecodeHash, deployer);
    }

    receive() external payable {}
}

全部foundry测试合约:

Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/test/utils/Create2.t.sol

2. 代码精读

2.1 deploy(uint256 amount, bytes32 salt, bytes memory bytecode)

底层调用CREATE2来部署合约。传入参数分别为:

  • amount:为执行部署合约constructor函数中需要传入的eth数量(单位为wei)。如果该值不为0,那么待部署合约的constructor函数必须是payable修饰的;
  • salt:一个随机值。在其他参数相同时,通过改变该值来改变部署合约地址;
  • bytecode:要部署合约的bytecode。在solidity中,可通过type(合约名).creationCode来获得。
    function deploy(
        uint256 amount,
        bytes32 salt,
        bytes memory bytecode
    ) internal returns (address addr) {
        // 如果待部署合约的constructor函数执行时需要传入eth,事先要求本Create2部署合约中的eth余额大于等于部署时传入的eth数量
        require(address(this).balance >= amount, "Create2: insufficient balance");
        // 要求部署合约传入的bytecode长度不为0
        require(bytecode.length != 0, "Create2: bytecode length is zero");
        /// 内联汇编中调用create2
        assembly {
            // create2的参数解释:create2(v, p, n, s)
            // v:待部署合约执行constructor函数时被deployer传入的eth值(单位为wei)
            // p:内存中指向待部署合约bytecode内容的指针
            // n: 部署合约bytecode的字节长度
            // s: salt值。注:s是一个256位的大端字节序的值
            // 如果执行错误,create2返回0。例如:部署的新合约地址与已经存在的合约地址产生冲突
            addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt)
        }

        // 如果利用create2部署成功,则得到的addr不为0
        require(addr != address(0), "Create2: Failed on deploy");
    }

foundry代码验证

contract Create2Test is Test {
    MockCreate2 mc = new MockCreate2();

    function test_Deploy() external {
        string memory name = "Michael.W";
        uint age = 18;
        // 1. deploy the contract with a non-payable constructor
        bytes32 salt = keccak256("deploy the contract with a non-payable constructor");
        // constructor params
        bytes memory encodedConstructorParams = abi.encode(name, age);
        // bytecode = creation code + constructor params
        bytes memory bytecode = abi.encodePacked(type(ContractWithConstructor).creationCode, encodedConstructorParams);
        // deploy
        address newContractAddress = mc.deploy(0, salt, bytecode);
        // check constructor params
        assertEq(name, ContractWithConstructor(newContractAddress)._name());
        assertEq(age, ContractWithConstructor(newContractAddress)._age());

        // 2. deploy the contract with a payable constructor
        vm.deal(address(mc), 3 gwei);
        salt = keccak256("deploy the contract with a payable constructor");
        bytecode = abi.encodePacked(type(ContractWithPayableConstructor).creationCode, encodedConstructorParams);
        newContractAddress = mc.deploy(1 gwei, salt, bytecode);
        assertEq(name, ContractWithConstructor(newContractAddress)._name());
        assertEq(age, ContractWithConstructor(newContractAddress)._age());
        // check eth balances
        assertEq(3 gwei - 1 gwei, address(mc).balance);
        assertEq(1 gwei, address(newContractAddress).balance);

        // revert
        // 1. revert when creates contract on the same address
        vm.expectRevert("Create2: Failed on deploy");
        mc.deploy(1 gwei, salt, bytecode);
        // 2. revert with empty bytecode
        vm.expectRevert("Create2: bytecode length is zero");
        mc.deploy(0, salt, "");
        // 3. revert with insufficient balance
        vm.deal(address(mc), 1 gwei);
        vm.expectRevert("Create2: insufficient balance");
        mc.deploy(1 gwei + 1, salt, bytecode);
    }
}

contract ContractWithConstructor{
    string public _name;
    uint public _age;
    constructor(string memory name, uint age){
        _name = name;
        _age = age;
    }
}

contract ContractWithPayableConstructor{
    string public _name;
    uint public _age;
    constructor(string memory name, uint age) payable {
        _name = name;
        _age = age;
    }
}

2.2 computeAddress(bytes32 salt, bytes32 bytecodeHash) && computeAddress(bytes32 salt, bytes32 bytecodeHash, address deployer)

以上两个computeAddress方法都是在部署合约前计算部署后的合约地址的工具方法。唯一不同的是:

computeAddress(bytes32 salt, bytes32 bytecodeHash)是利用本合约作为deployer来部署合约,而computeAddress(bytes32 salt, bytes32 bytecodeHash, address deployer)是输入deployer地址从而计算出合约部署后的地址。

注:计算合约地址需要传入bytecodeHash,而非bytecode。bytecodeHash的计算方式为keccak256(合约bytecode + constructor参数)

    // 利用部署合约bytecode的hash值与salt值,计算其利用本合约作为deployer部署该bytecode的合约地址
    function computeAddress(bytes32 salt, bytes32 bytecodeHash) internal view returns (address) {
        // 调用computeAddress(bytes32, bytes32, address)方法
        return computeAddress(salt, bytecodeHash, address(this));
    }

    // 利用部署合约bytecode的hash值/salt值和deployer地址,计算该deployer部署该bytecode的合约地址
    function computeAddress(
        bytes32 salt,
        bytes32 bytecodeHash,
        address deployer
    ) internal pure returns (address addr) {
        /// @solidity memory-safe-assembly
        assembly {
            // 获取空闲内存指针ptr
            let ptr := mload(0x40)
            // ptr的第3个字开始(即ptr向后移动2个32字节的位置)存储bytecodeHash
            mstore(add(ptr, 0x40), bytecodeHash)
            // ptr的第2个字存储salt
            mstore(add(ptr, 0x20), salt)
            // ptr的第1个字存储deployer地址。由于1个字是32字节,deployer地址是20字节,那么第1个字的前32-20=12个字节还都是0,即ptr开始的前12个字节目前为0,还没有使用到
            mstore(ptr, deployer)
            // start指针指向ptr向右偏移11个字节
            let start := add(ptr, 0x0b) 
            // 向ptr开始第11个字节里写入内容0xff
            mstore8(start, 0xff)
            //                              start
            // 从ptr 开始的memoy地址:  0 ...   0xb | 0xc - 0x1f | 0x20 - 0x3f | 0x40 - 0x5f  |
            //       内存中内容:        ...  0xff |  deployer  |     salt    | bytecodeHash |
            // 对start开始后85个字节内容取keccak256,该值就是利用CREATE2部署后的合约地址
            addr := keccak256(start, 85)
            // 85字节内容为:0xff + deployer + salt + bytecodeHash
            //   字节数:     1  +    20    +  32  +      32   
        }
    }

foundry代码验证

contract Create2Test is Test {
    MockCreate2 mc = new MockCreate2();

    function test_ComputeAddress() external {
        string memory name = "Michael.W";
        uint age = 18;
        // 1. deploy the contract with a non-payable constructor
        bytes32 salt = keccak256("deploy the contract with a non-payable constructor");
        bytes memory encodedConstructorParams = abi.encode(name, age);
        bytes memory bytecode = abi.encodePacked(type(ContractWithConstructor).creationCode, encodedConstructorParams);
        bytes32 bytecodeHash = keccak256(bytecode);
        assertEq(mc.deploy(0, salt, bytecode), mc.computeAddress(salt, bytecodeHash));

        // 2. deploy the contract with a payable constructor
        vm.deal(address(mc), 3 gwei);
        salt = keccak256("deploy the contract with a payable constructor");
        bytecode = abi.encodePacked(type(ContractWithPayableConstructor).creationCode, encodedConstructorParams);
        bytecodeHash = keccak256(bytecode);
        assertEq(mc.deploy(2 gwei, salt, bytecode), mc.computeAddress(salt, bytecodeHash));
        assertEq(address(mc).balance, 1 gwei);
    }

    function test_ComputeAddress_WithDeployer() external {
        MockCreate2 mcOther = new MockCreate2();
        string memory name = "Michael.W";
        uint age = 18;
        // 1. deploy the contract with a non-payable constructor
        bytes32 salt = keccak256("deploy the contract with a non-payable constructor");
        bytes memory encodedConstructorParams = abi.encode(name, age);
        bytes memory bytecode = abi.encodePacked(type(ContractWithConstructor).creationCode, encodedConstructorParams);
        bytes32 bytecodeHash = keccak256(bytecode);
        assertEq(mcOther.deploy(0, salt, bytecode), mc.computeAddress(salt, bytecodeHash, address(mcOther)));

        // 2. deploy the contract with a payable constructor
        vm.deal(address(mcOther), 3 gwei);
        salt = keccak256("deploy the contract with a payable constructor");
        bytecode = abi.encodePacked(type(ContractWithPayableConstructor).creationCode, encodedConstructorParams);
        bytecodeHash = keccak256(bytecode);
        assertEq(mcOther.deploy(2 gwei, salt, bytecode), mc.computeAddress(salt, bytecodeHash, address(mcOther)));
        assertEq(address(mcOther).balance, 1 gwei);
    }
}

ps:\ 本人热爱图灵,热爱中本聪,热爱V神。 以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。 同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下! 如果需要转发,麻烦注明作者。十分感谢!

1.jpeg

公众号名称:后现代泼痞浪漫主义奠基人

点赞 2
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Michael.W
Michael.W
0x93E7...0000
狂热的区块链爱好者