跳到主要内容

WTF Opcodes极简入门: 22. Create2指令

我最近在重新学以太坊opcodes,也写一个“WTF EVM Opcodes极简入门”,供小白们使用。

推特:@0xAA_Science

社区:Discord微信群官网 wtf.academy

所有代码和教程开源在github: github.com/WTFAcademy/WTF-Opcodes


上一讲我们介绍了CREATE指令,使合约有能力创建其他合约。这一讲,我们将进一步探讨CREATE2指令,它提供了一种新的方式来确定新合约的地址。

CREATE vs CREATE2

传统的CREATE指令通过调用者的地址和nonce来确定新合约的地址,而CREATE2则提供了一种新的计算方法,使我们可以在合约部署之前预知它的地址。

CREATE不同,CREATE2使用调用者地址、盐(一个自定义的256位的值)以及initcode的哈希来确定新合约的地址,计算方法如下:

address = keccak256( 0xff + sender_address + salt + keccak256(init_code))[12:]

这样的好处是,只要你知道initcode,盐值和发送者的地址,就可以预先知道新合约的地址,而不需要现在部署它。而CREATE计算的地址取决于部署账户的nonce,也就是说,在nonce不确定的情况下(合约还未部署,nonce可能会增加),没法确定新合约的地址。

CREATE2的更多介绍可以参考WTF Solidity教程第25讲

CREATE2

在EVM中,CREATE2指令的简化流程如下:

  1. 从堆栈中弹出value(向新合约发送的ETH)、mem_offsetlength(新合约的initcode在内存中的初始位置和长度)以及salt
  2. 使用上面的公式计算新合约的地址。
  3. 之后的步骤同CREATE指令:初始化新的EVM上下文、执行initcode、更新创建的账户状态、返回新合约地址或0(如果失败)。

下面,我们在极简EVM中实现CREATE2指令:

def create2(self):
if len(self.stack) < 4:
raise Exception('Stack underflow')

value = self.stack.pop()
mem_offset = self.stack.pop()
length = self.stack.pop()
salt = self.stack.pop()

# 扩展内存
if len(self.memory) < mem_offset + length:
self.memory.extend([0] * (mem_offset + length - len(self.memory)))

# 获取初始化代码
init_code = self.memory[mem_offset: mem_offset + length]

# 检查创建者的余额是否足够
creator_account = account_db[self.txn.thisAddr]
if creator_account['balance'] < value:
raise Exception('Insufficient balance to create contract!')

# 为创建者扣除指定的金额
creator_account['balance'] -= value

# 生成新的合约地址(参考geth中的方式,使用盐和initcode的hash)
init_code_hash = sha3.keccak_256(init_code).digest()
data_to_hash = b'\xff' + self.txn.thisAddr.encode() + str(salt).encode() + init_code_hash
new_contract_address_bytes = sha3.keccak_256(data_to_hash).digest()
new_contract_address = '0x' + new_contract_address_bytes[-20:].hex() # 取后20字节作为地址

# 使用txn构建上下文并执行
ctx = Transaction(to=new_contract_address,
data=init_code,
value=value,
caller=self.txn.thisAddr,
origin=self.txn.origin,
thisAddr=new_contract_address,
gasPrice=self.txn.gasPrice,
gasLimit=self.txn.gasLimit)
evm_create2 = EVM(init_code, ctx)
evm_create2.run()

# 如果EVM实例返回错误,压入0,表示创建失败
if evm_create2.success == False:
self.stack.append(0)
return

# 更新创建者的nonce
creator_account['nonce'] += 1

# 存储合约的状态
account_db[new_contract_address] = {
'balance': value,
'nonce': 0,
'storage': evm_create2.storage,
'code': evm_create2.returnData
}

# 压入新创建的合约地址
self.stack.append(int(new_contract_address, 16))

测试

  1. 使用CREATE2指令部署一个新合约,发送9 wei,但不部署任何代码:

    # CREATE2 (empty code, 9 wei balance)
    code = b"\x5f\x5f\x5f\x60\x09\xf5"
    # PUSH0 PUSH0 PUSH0 PUSH1 0x09 CREATE2
    evm = EVM(code, txn)
    evm.run()
    print(hex(evm.stack[-1]))
    # output: 0x260144093a2920f68e1ae2e26b3bd15ddd610dfe
    print(account_db[hex(evm.stack[-1])])
    # output: {'balance': 9, 'nonce': 0, 'storage': {}, 'code': bytearray(b'')}
  2. 使用CREATE2指令部署一个新合约,并将代码设置为ffffffff:

    # CREATE2 (with 4x FF)
    code = b"\x6c\x63\xff\xff\xff\xff\x60\x00\x52\x60\x04\x60\x1c\xf3\x60\x00\x52\x60\x00\x60\x0d\x60\x13\x60\x00\xf5"
    # PUSH13 0x63ffffffff6000526004601cf3 PUSH1 0x00 MSTORE PUSH1 0x00 PUSH1 0x0d PUSH1 0x13 PUSH1 0x00 CREATE2
    evm = EVM(code, txn)
    evm.run()
    print(hex(evm.stack[-1]))
    # output: 0x6dddd3288a19f0bf4eee7bfb9e168ad29e1395d0
    print(account_db[hex(evm.stack[-1])])
    # {'balance': 0, 'nonce': 0, 'storage': {}, 'code': bytearray(b'\xff\xff\xff\xff')}

总结

这一讲,我们介绍了EVM中创建合约的另一个指令,CREATE2,通过它,合约不仅可以创造其他合约,而且可以预知新合约的地址。Uniswap v2中的LP地址就是用这个方法计算的。现在,我们已经学习了144个操作码中的142个(98.6%)!