CREATE2 操作码使用方法详解
March 27th, 2022

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获得合约地址:解决交易所充值账号问题

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

参考

Subscribe to xyyme.eth
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.
More from xyyme.eth

Skeleton

Skeleton

Skeleton