使用Create2操作码在相同的地址部署不同的代码的合约。

  • KEN
  • 更新于 2024-03-22 11:47
  • 阅读 1718

一、Create2操作码Create2操作码常常被用来在一个合约中创建另一个合约,这是在智能合约中创建另一个合约的方法之一,另一个方法是通过Create操作码,本文重点关注Create2操作码的工作原理以及使用方式。小小提醒:本文中所提到的几种字节码词汇之间的关系:creationCode(b

一、Create2操作码

Create2操作码常常被用来在一个合约中创建另一个合约,这是在智能合约中创建另一个合约的方法之一,另一个方法是通过Create操作码,本文重点关注Create2操作码的工作原理以及使用方式。

小小提醒:

本文中所提到的几种字节码词汇之间的关系:creationCode(bytecode) = init_code + runtimecode(deployedcode)

1 Create2操作码与Create操作码

1.1 Create操作码:

1.1.1 create操作码计算新生成合约地址的方式:

new address = keccak256(rlp(senderAddress,nonce));

  1. 其中senderAddress是指交易的发起者,既可以是合约地址也可以是钱包地址(EOA地址)。
  2. nonce随机数,对于钱包地址而言,nonce值就是该地址迄今为止发起的交易数,而对于合约地址而言是就是该factory合约创建合约的总数,该factory合约每创建一个合约,其nonce值+1 。

1.1.2 create操作码创建合约的方式:

create操作码创建合约至少有两种方式(因为我只知道两种,hhh),一种是在合约代码层面创建合约,另一种方式是在底层通过create操作码创建合约。

  1. 通过合约代码使用create创建新合约:
Contract X = new Contract {value: _value} ( params )

在上述代码块中,Contract代指要创建合约的类型名称 ,X表示创建的合约实体名称,params表示在创建X合约是要传入的constructor参数,_value表示在创建合约是给其赋的初始ether数量。

  1. 以汇编形式通过create操作码创建新合约:
bytes memory initcode = type(Contract).creationCode;
assembly{
    let encode_data := add( 0x20, initcode )
    let encode_size := mload(initcode)
    deploymentAddress := create( callvalue, encode_data, encode_size)  
}

如果是以汇编的形式创建合约,在memory内存中,存储的字节码数据包含:传入的字节码长度bytecode_length和字节码bytecode本身,如果是在合约层面创建合约,则memory中仅仅只会存储字节码bytecode本身。而在汇编代码块中,上述initcode在汇编中表示的是字节码数据所存放的memory槽位。add(0x20,initcode)实际上是跳过bytecode_length。

1.2 Create2操作码:

1.2.1 Create2操作码计算新生成合约地址的方式:

new address = keccak256(0xff ++ 部署者地址 ++ salt ++ keccak256(code))[12:]

  1. 其中0xff是create2计算地址使用的固定常数
  2. 部署者地址是当前create2操作码所在的合约地址,当然也有可能是eoa地址
  3. salt 随机数盐
  4. keecak256(code), codehash,可以是任意传入code的哈希值。code可以是initCode,也可以是creationCode,还可以是runtimeCode以及任何你想传入的一段字节码。

1.2.2 create操作码创建合约的方式

与create操作码一样,create2也至少有两种创建合约的方式,一种是在合约代码层面创建合约,另一种方式是在底层通过create2操作码创建合约。

  1. 通过合约代码创建新合约:
Contract X  =  new Contract { value : _value, salt : _salt}( params )

在上述代码块中,Contract代指要创建合约的类型名称 ,X表示创建的合约实体名称,params表示在创建X合约是要传入的constructor参数,_value表示在创建合约是给其赋的初始ether数量。salt是指随机数盐。

  1. 以汇编的形式通过create2操作码创建新合约
bytes memory initcode = type(Contract).creationCode;
assembly{
    let encode_data := add(0x20, initCode)
    let encode_size := mload(initCode)
    deploymentaddress := create2(
        callvalue,
        encode_data,
        encode_size,
        salt
    )
}

1.3 Create操作码与Create2操作码的区别

create操作码与Create2操作码在创建合约是最大的不同就是Create2操作码将新合约部署的地址是可以通过已知的信息提前计算出来。而Create操作码创建合约的地址无法被预测。这是因为Create操作码在计算地址时,使用的nonce值一直在变化,很难被提前预测。而Create2操作码计算地址使用的四个操作数:1.0xff 2.senderAddress 都是确定的,也就是说我们只需要知道合约在创建新合约是使用多个3.salt 4.hash(code),便可以提前预知新合约的地址。

2. Create2操作码在同一个地址上部署两个代码不同的合约。

Create2操作码可以将两个不同代码的合约部署到同一个地址上。但是需要注意的是:create2不能在已经存在合约代码的地址上部署合约,EVM会让这样的交易revert。

在上述内容中,我们提到过Create2操作码在计算地址时会使用1.0xff 2.senderAddress 3.salt 4.code_hash,而对于同一个合约部署者而言第一次部署合约的上述四个数据其实都很容易得知,而1.0xff 2.senderAddress 3.salt 很容易保持不变,我们只需要在第二次create2是传入和第一次create2一样的数据便可,关键就在于怎样保证两次传入给create2使用的code_hash是一样的。因为大多数人固有的想法都是“两个不同代码合约的runtimecode一定不一样啊,得到的code_hash又怎能保持一致呢?”是否还记得我们在文章的上面提到过Create2使用的code_hash没有对code做出限制,也就是说keccak256(code)中的code可以为任意的code,设想如果我们传入的code是init_code呢?Create2仍然会为其计算出一个地址,但是我们只需要保证两个合约的init_code一致即可,这样一来难度就小了很多。

正常我们传入creationCode给Create2时,init_code运行完便会将runtimeCode加载到memory中以供返回,这样Create2就将runtimeCode加载到新生成的地址上。但是如果我们只往Create2操作码中推入参数init_code,我们又会遇到一个问题,init_code没有返回值呀?其实我们只需要在init_code执行完,手动的往memory中添加runtimeCode即可。

下面用例子来说明:

image.png 在上述图片中一共存在三个合约,分别是Deployer(逻辑合约),Storage(工厂合约),Owner(部署合约)。简单介绍一下这三个合约,逻辑合约是Create2操作码在新地址上实际创建的合约,Storage工厂合约将自己的init_code传入部署合约,并且返回逻辑合约的runtimeCode。Owner部署合约部署Create2操作,生成新的合约。

那么上述例子是怎样保证新生成合约的地址一样呢? 我们只需在Owner部署合约中保证始终传入的是工厂合约Storage的initcode便可,(注意上述代码,我们改变Deployer逻辑合约中的代码并不会导致Storage合约的initcode产生不同),Owner合约会使用工厂合约的initcode计算新生成的合约地址,并执行其initcode(Storage工厂合约中的constructor逻辑),我们在Storage的constructor中永久返回逻辑合约的runtimeCode,这样一来,我们便可以保证create2生成的合约地址一样,而部署出来的合约代码逻辑不同。

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

0 条评论

请先 登录 后评论
KEN
KEN
0x4e16...2573
江湖只有他的大名,没有他的介绍。