xyyme.eth

Posted on Mar 27, 2022Read on Mirror.xyz

CREATE2 操作码使用方法详解

CREATE2 是一个可以在合约中创建合约的操作码。我们先来举个例子看看它能干什么:

这段代码是 Uniswap v2-core 里面的工厂合约代码,使用 create2 操作码创建了 pair 合约,返回值是 pair 的地址,这样就可以逻辑中直接使用其地址进行接下来的操作。

那么 create2 到底是怎么使用呢,根据官方 EIP 文档,create2 一共接收四个参数,分别是:

  1. endowment(创建合约时往合约中打的 ETH 数量)
  2. memory_start(代码在内存中的起始位置,一般固定为 add(bytecode, 0x20)
  3. memory_length(代码长度,一般固定为 mload(bytecode)
  4. salt(随机数盐)

这里要注意的是第一个参数如果大于 0 的话,需要待部署合约的构造方法带有 payable。随机数盐是由用户自定,须为 bytes32 格式,例如在上面 Uniswap 的例子中,salt 为:

bytes32 salt = keccak256(abi.encodePacked(token0, token1));

create2 还有一个优点,相较于以前的 new 创建合约方法,可以在合约未创建之前就能够计算出合约的地址。我们之前在使用 new 创建新合约的时候,必须在取到合约对象之后,再取其 address 才能获取地址。而使用 create2,就可以这样提前计算出地址(参考):

// salt 为部署合约时使用的随机数盐
// bytecode 为合约的字节码哈希(keccak256)
// deployer 为部署合约的地址(在A合约中部署B合约,则此处为A)
function computeAddress(
    bytes32 salt,
    bytes32 bytecodeHash,
    address deployer
) internal pure returns (address) {
    bytes32 _data = keccak256(abi.encodePacked(bytes1(0xff), deployer, salt, bytecodeHash));
    return address(uint160(uint256(_data)));
}

能够提前计算出合约地址这一点,就会给我们许多想象力。比如我们可以让合约地址变成靓号(例如前缀地址是 0x000,0x666,0x888),原因就是 salt 是我们自己定义的,那么就可以像 POW 挖矿那样,不断找寻随机数,以达到目的。这里我们就不再对这个话题过多深入,感兴趣的同学可以看看 这里 或者 这里

接下来我们就尝试一下使用 create2 部署合约,方便的是,OpenZeppelin 库中就有现成的模版可以供我们使用,先来看看它是怎么实现的:

注意到注释中对于参数的要求:

  1. bytecode 必须非空
  2. 对于相同的 bytecode,salt 不能重复(否则就计算出重复的地址)
  3. 部署合约中必须有 amount 数量的 ETH(如果 amount 大于 0 的话)
  4. 如果 amount 非 0,则待部署合约必须有 payable 修饰符

逻辑还是比较简单的,我们在使用的时候直接套用模版就行,来实操一下:

pragma solidity 0.8.10;

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

contract Factory {
    event Deployed(address addr);

    // 计算合约地址的方法
    function getAddress() public view returns (address) {
        return
            Create2.computeAddress(
                keccak256("Here is salt"),
                keccak256(
                    abi.encodePacked(type(Template).creationCode, abi.encode(3))
                )
            );
    }

    // 部署合约
    function deploy() public {
        address addr = Create2.deploy(
            0,
            keccak256("Here is salt"),
            abi.encodePacked(type(Template).creationCode, abi.encode(3))
        );
        emit Deployed(addr);
    }
}

contract Template {
    uint256 public a;

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

我们使用 keccak256("Here is salt") 为盐,当然这里可以使用任何 bytes32 类型的数据。

有一点需要注意的是,对于构造函数有参数的情况,需要将参数编码并拼接在合约字节码后面作为完整的字节码传入:

abi.encodePacked(type(Template).creationCode, abi.encode(3))

我们部署一下,先调用 getAddress 计算合约地址:

然后再调用 deploy 部署合约,在事件中查看部署的地址为:

验证地址确实相同。

这里还有一点需要说一下的是,如果要在 EtherScan 中上传代码,是需要将上面的所有合约,也就是 Factory 和 Template,包括 import 的合约都需要上传,仅仅上传 Template 是无法成功的,这里当时卡了我很长时间,最后试了试全部粘贴才能成功。

由于对于相同的合约、参数、盐,create2 计算所得的合约地址都是相同的,因此我们就可以通过 create2 与 selfdestruct 相结合,在同一个地址上多次部署合约。我在这篇文章中有详细介绍。

总结

本文只介绍了 create2 的使用方法,其实它的应用场景还有很多,比如:

  1. CREATE2 在广义状态通道中的使用
  2. 通过CREATE2获得合约地址:解决交易所充值账号问题

感兴趣的同学可以多看看学习学习。

参考

https://eips.ethereum.org/EIPS/eip-1014

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Create2.sol

https://hackernoon.com/using-ethereums-create2-nw2137q7

https://www.liaoxuefeng.com/article/1430588932227106