教程 | 充分利用 CREATE2
在本文中,我们将深入探讨 CREATE2 操作码及其在反事实实例化(counterfactual instantiation)以及用户引导中的应用。我们将了解如何将 CREATE2 与初始化程序、代理以及元交易等不同的技术相结合并投入应用。这些技术为创建用户身份开辟了新的方法,甚至能让我们在创建身份之前快速迭代并修复漏洞。
科普时间
如果我们利用外部账户(EOA)(译者注:即由用户直接控制的账户)或者使用原生 CREATE 操作的合约账户创建一个合约,很容易就能确定被创建合约的地址。每个账户都有一个与之关联的 nonce (译者注:可理解为交易流水号):对外部账户而言,每发送一个交易,nonce 就会随之 +1 ;对合约账户而言,每创建一个合约,nonce 就会随之 +1。新合约的地址由创建合约交易的发送者账户地址及其 nonce 值计算得到:
合约地址 = hash (发送者地址, nonce )
虽然这种模式可以让人们很容易提前算出自己的哪个 nonce 会把合约创建到哪个地址上,但你很难真正 “占住” 地址。你必须确保自己没有拿那个 nonce 来干别的,这样,等到你想要部署自己的合约(到那个地址上)时,才能真正部署上去(译者注:不然就永远错过了,因为 nonce 只增不减)。你肯定要问,为什么要 “占住” 一个地址呢?这有什么用?
请注意:此处 “占住” 的意思是为合约预留一个地址,与占住域名(domain parking)类似,唯一的差别是我们不能选择具体的地址。实际上,我们想要的是确定未来部署合约的时候它会部署到哪个地址上,希望这个地址不要因为我们中间发了多少笔交易而变动。
反事实
反事实实例化 是在广义状态通道的背景下逐渐流行起来的概念。它指的是创建一个还未部署上链,但满足有可能部署上链这一事实条件的合约。正如《反事实的广义状态通道白皮书》中所定义的那样:
我们将以下情况称为反事实 X :
X 可能在链上发生,但是没有发生 任何参与者都可以单方面通过执行机制促使 X 发生 参与者可以表现得好像 X 已经发生了
用户引导
CREATE2 为用户引导以及钱包管理开辟了巨大的设计空间。根据 2019 年的 Dapp 调查报告显示,超过四分之三的开发者提到用户引导是 Dapp 普及的主要障碍。Burner Wallet 等近期项目设计了一些备选方案,利用 Zooko 三角中的权衡关系来减轻生成密钥的负担(项目叫做 “burner(销毁器)” 是有原因的)。基于 CREATE2 的钱包在用户引导方面有着相似的用户体验目标。然而,假设资产是由合约而非私钥持有的,那么不仅用户拥有对资产的可编程访问权限,而且私钥也可被视为一次性的。生成合约地址与生成私钥一样简单可靠。要解决合约部署成本的悖论,一种方法是等用户资产被转移到合约地址上,再部署一个合约来持有该资产。这篇文章进一步讨论了这种技术及其原理。 其设想是,不将完全的去中心化强加给新用户,而是认为新用户更有可能倾向于灵活且对用户友好的系统——即我们所说的 “渐进式去中心化”。这类系统优先考虑用户心理,并为各种专业技术、目标以及用例提供支持。它们与用户一起成长,并向着用户激励的方向不断完善。
走入 CREATE2 的世界
合约创建者的地址 作为参数的混淆值(salt) 合约创建代码
由 CREATE2 驱动的 factory 合约
contract Factory {
function deploy(bytes memory code, bytes32 salt) public returns (address addr) {
assembly {
addr := create2(0, add(code, 0x20), mload(code), salt)
if iszero(extcodesize(addr)) { revert(0, 0) }
}
}
}
构造函数 vs 初始化函数
contract Multisig {
address[] owners;
uint256 required;
function initialize(address[] memory _owners, uint256 _required) public {
require(required == 0, "Contract has already been initialized");
require(_required > 0, "At least one owner is required");
owners = _owners;
required = _required;
}
}
初始化函数是常规函数,因此在合约创建后可以随时调用它们。但是,在合约创建之后,应在同一个交易里立即调用初始化函数,从而确保没有人抢先运行初始化函数并更改实例中的初始值。
contract Factory {
function deploy(bytes memory code, bytes32 salt, bytes memory initdata) public returns (address addr) {
assembly {
addr := create2(0, add(code, 0x20), mload(code), salt)
if iszero(extcodesize(addr)) { revert(0, 0) }
}
(bool success,) = addr.call(initdata);
require(success);
}
}
现在,我们已经推迟了初始化参数的选择,接下来看看能否进一步改进。我们可以试一试推迟对合约本身的选择。
依旧是代理
contract Factory {
function deploy(address logic, bytes32 salt, bytes memory initdata) public returns (address addr) {
bytes memory code = type(Proxy).creationCode;
assembly {
addr := create2(0, add(code, 0x20), mload(code), salt)
if iszero(extcodesize(addr)) { revert(0, 0) }
}
Proxy(addr).initialize(logic);
(bool success,) = addr.call(initdata);
require(success);
}
}
虽然这种灵活性很棒,但是在不经意间将攻击向量引入了工厂合约。如果攻击者知道了用户为 CREATE2 预选的盐值,就可以在目的地址抢先调用 deploy 函数,传入不同的逻辑合约或者不同的初始化数据,让这一套合约的功能与预想的完全不同,。接下来,让我们在盐值中添加一些额外的成分,来解决这一问题。
交易发送者地址参上
contract Factory {
function deploy(
address logic, bytes32 salt, bytes memory initdata
) public returns (address addr) {
bytes32 newsalt = keccak256(abi.encodePacked(salt, msg.sender));
bytes memory code = type(Proxy).creationCode;
assembly {
addr := create2(0, add(code, 0x20), mload(code), newsalt)
if iszero(extcodesize(addr)) { revert(0, 0) }
}
Proxy(addr).initialize(logic);
(bool success,) = addr.call(initdata);
require(success);
}
}
这是 2.3.1 版本中发布的新的 ZeppelinOS 代理工厂合约的实现方式之一。你也可以使用命令行界面的 zos create2 命令行轻松使用这一功能。
$ zos create2 --salt 42 --query
> Instance using salt 42 will be deployed at 0x123456
...
$ zos create2 MyContract --salt 42 --init initialize
> Instance of MyContract deployed at 0x123456
然而,增加了部署地址必须基于发送者地址计算得出的限制之后,我们原本想要实现的元交易这一用例就泡汤了。元交易模式是,用户广播他们想要执行的交易,中继者选择该交易并将其上链。这意味着发送者地址与用户地址是不同的,因此不会生成预期的部署地址。但是幸运的是,这个问题也是可以解决的。
可以不用发送者地址,用签名者地址嘛
contract Factory {
function deploy(
address logic, bytes32 salt, bytes memory initdata, bytes memory signature
) public returns (address addr) {
address signer = keccak256(
abi.encodePacked(logic, salt, initdata, address(this))
).toEthSignedMessageHash().recover(signature);
bytes32 newsalt = keccak256(abi.encodePacked(salt, signer));
bytes memory code = type(Proxy).creationCode;
assembly {
addr := create2(0, add(code, 0x20), mload(code), newsalt)
if iszero(extcodesize(addr)) { revert(0, 0) }
}
该流程也被编码到了 ZeppelinOS 代理工厂合约中。正如我们的示例项目所示,与前文给出的例子一样,可以添加一个签名项,然后使用 zos cretate2 命令行调用该合约。
$ zos create2 --salt 43 --from 0x44
> Instance using salt 43 will be deployed at 0x654321
...
$ zos create2 MyContract --salt 43 --signature 0xabcdef --init initialize --from 0x88
> Instance of MyContract deployed at 0x654321
总而言之,这意味着任何用户都可以选择一个随机的盐值,并拥有一个唯一确定的地址可用来随时部署他们想要的合约。不仅如此,用户只需要对部署参数进行 签名 就可以直接通过他们的地址或者中继者地址执行部署程序了。
总结
(完)
(文内提供了许多超链接,请点击阅读原文到 EthFans 网站上获取)
原文链接:
https://blog.openzeppelin.com/getting-the-most-out-of-create2/
作者: SANTIAGO PALLADINO
翻译 & 校对: Aisling & 闵敏
你可能还喜欢:
引介 | Counterfactual 项目:广义的以太坊状态通道