操作码使我们能够预测将CREATE2部署合约的地址,而无需这样做。这为提高用户引导和可扩展性提供了许多可能性。
在本指南中,我们将预先计算将部署合约的地址并将 Ether 发送到该地址。然后,我们将在同一个地址部署一个可升级的合约,并使用它来检索之前发送到那里的资金。
本指南介绍了 OpenZeppelin 工具的高级用法,并且需要熟悉 Solidity、开发区块链和 OpenZeppelin CLI。
如需复习这些主题,请前往部署智能合约并与之交互。
部署智能合约的主要方式有两种:使用CREATE和CREATE2流。我们将简要概述它们的工作原理及其核心区别。
如果您已经熟悉背后的目标CREATE2,请随意跳过。
智能合约既可以由其他合约(使用Solidity 的new关键字)创建,也可以由普通账户(例如运行时oz deploy)创建。在这两种情况下,新合约的地址都以相同的方式计算:作为发送者自己的地址和随机数的函数。
new_address = hash(sender, nonce)
每个账户都有一个关联的 nonce:对于普通账户,它在每笔交易中都会增加,而对于合约账户,它会在每次合约创建时增加。随机数不能重复使用,它们必须是顺序的。
这意味着可以预测下一个创建的合约将被部署到的地址,但前提是在此之前没有其他交易发生——这对于反事实系统来说是一个不受欢迎的属性。
这个操作码背后的整个想法是使生成的地址独立于未来的事件。无论区块链上可能发生什么,始终可以在预先计算的地址上部署合约。
新地址具有以下功能:
0xFF, 一个防止与CREATE发件人自己的地址
盐(发件人提供的任意值)
待部署合约的字节码
new_address = hash(0xFF, sender, salt, bytecode)
CREATE2保证如果sender部署bytecodeusingCREATE2和提供的salt,它将存储在new_address.
因为bytecode包含在这个计算中,所以其他代理可以依赖这样一个事实,即如果一个合约被部署到new_address,它将是他们知道的一个。这是反事实部署背后的关键概念。
因为CREATE2是 EVM 操作码,它通常只能由智能合约使用,而不能由外部账户使用。然而,OpenZeppelin CLI 提供了一种CREATE2直接从终端运行类似部署的便捷方式。
我们将从使用合约初始化一个新的 OpenZeppelin 项目Vault开始:
// contracts/Vault.sol
pragma solidity ^0.5.0;
import "@openzeppelin/upgrades/contracts/Initializable.sol";
contract Vault is Initializable {
address payable owner;
function initialize(address payable _owner) initializer public {
owner = _owner;
}
function withdraw() public {
require(owner == msg.sender);
owner.transfer(address(this).balance);
}
}
我们将计算Vault将要部署的地址,并将以太币发送到那里。然后,我们将Vault使用CREATE2并调用该withdraw方法进行部署,检索在部署之前发送给它的资金。
虽然这个简单的例子听起来很傻,但能够与尚不存在的合约交互是一个非常强大的工具,并且是状态通道、入职解决方案和抢先预防计划背后的关键构建块。
回想一下,只有智能合约可以使用CREATE2:为了让 OpenZeppelin CLI 能够提供等效的行为,我们需要先进行一些设置。
在底层,CLI 将使用合约工厂来部署来自 Solidity 的可升级合约。这意味着我们需要使用两个不经常使用的低级 CLI 命令:oz add和oz push:
$ npx oz add
? Pick which contracts you want to add Vault
✓ Added contract Vault
$ npx oz push
✓ Contract Vault deployed
All contracts have been deployed
您可以在不了解幕后情况的情况下安全使用CREATE2,但如果您想深入了解血淋淋的细节,请从了解OpenZeppelin 升级代理开始。
有了这个设置,我们可以通过调用任意命令查询CLI 以获取我们的合约将被部署的地址:saltoz create2
$ npx oz create2 --query --salt 12345 --network development
✓ Deployed ProxyFactory at 0x4e08589Cd399474157f24f591B9fB100D1adD5d9
Any contract created with salt 12345 will be deployed to the following address
0x4e08589Cd399474157f24f591B9fB100D1adD5d9
整洁的!我们现在可以与计算出的地址进行交互,知道我们以后可以在那里部署合约。
在正常情况下,将资金发送到随机的以太坊地址是一个坏主意。然而,在这里,我们知道我们将能够Vault在计算出的地址进行部署并取回我们的资金。所以让我们开始吧!
发送 Ether 的最简单方法是使用oz transfer:
$ $ npx oz transfer
? Pick a network development
? Choose the account to send transactions from (0) 0xA84577357099567A750f542C2C002B0aA680d477
? Enter the receiver account 0x98329e006610472e6B372C080833f6D79ED833cf
? Enter an amount to transfer 10 ether
✓ Funds sent. Transaction hash: 0x9cff31198a80cefb9541e5cf406433f985490a4d786b72bb7e07139ae293657d
因为该地址没有字节码,我们也没有它的私钥,所以除了检查资金是否确实存在之外,我们无能为力:
$ npx oz balance
? Enter an address to query its balance 0x98329e006610472e6B372C080833f6D79ED833cf
Balance: 10 ETH
让我们把他们找回来。
CREATE2dpeloyments 使用相同的oz create2命令执行,这次没有--query选项。
回想一下,它的所有者Vault有一个initialize方法:我们将使用我们控制的一个帐户来调用它。
$ npx oz create2 Vault --salt 12345 --init --args 0xA84577357099567A750f542C2C002B0aA680d477 --network development
✓ Instance created at 0x98329e006610472e6B372C080833f6D79ED833cf
如果一切顺利,我们现在应该能够withdraw从我们的Vault:
$ npx oz send-tx
? Pick a network development
? Pick an instance Vault at 0x272F769068bDB8740e44E6e0E852b97c8C4865b0
? Select which function withdraw()
✓ Transaction successful. Transaction hash: 0xb0a67ba8a198a0d86814519ed12de8fbeaaaab151ae3b70f67a608236627ec4b
成功!可以肯定的是,让我们验证一下Vault确实是空的:
$ npx oz balance
? Enter an address to query its balance 0x98329e006610472e6B372C080833f6D79ED833cf
Balance: 0 ETH
我们已经将资金发送到我们预先计算好的地址,知道我们以后可以在那里部署合约并取回它们。作为奖励,我们的Vault合约可以通过oz upgrade!
