本文作为确定性部署系列的第二部分,深入探讨了在多条链上以相同地址部署合约的两种方法:CREATE2工厂和CREATE3机制。文章详细解释了CREATE2操作码的地址计算方式,并介绍了四个现有的CREATE2工厂实现。同时,它阐述了CREATE3如何通过巧妙利用CREATE操作码,解决CREATE2在初始化代码包含可变参数时的局限性。
这是关于确定性部署系列的第二篇文章,我们在其中探讨一个问题:我们如何在多个链上以相同的地址部署合约?
在第一部分中,我们讨论了三种可能的答案:使用相同的私钥和 nonce 进行部署,使用 Nick 的方法,以及预签名交易。在本文中,我们将讨论另外两种方法:CREATE2 工厂和 CREATE3。
本文将解释的两种方法都基于 CREATE2 操作码,所以让我们快速回顾一下它的工作原理。
正如我们在上一篇文章中看到的,合约可以使用 CREATE 和 CREATE2 操作码创建其他合约。CREATE 操作码的行为与部署交易基本相同:
执行一些 init code 以获取新合约的运行时代码。
创建的合约的地址取决于发送者的地址和 nonce。对于部署交易,发送者是签署交易的 EOA。对于 CREATE,它是执行操作码的合约的地址。
CREATE2 操作码与 CREATE 类似,但地址的计算方式有所不同。请记住,对于 CREATE 和部署交易,公式是:
address = keccak256(rlp([sender, nonce]))[12:]
而对于 CREATE2,它是:
address = keccak256(0xff ++ sender ++ salt ++ keccak256(initcode))[12:]
其中 salt 是传递给操作码的额外 32 字节输入。
注意现在哪些参数影响生成的地址。发送者仍然是一个参数,但我们用 salt 和 init code 的哈希值取代了 nonce。
假设有一个工厂合约,它接收一些 init code 和一个 salt,并使用它们执行 CREATE2 操作码。我们还假设这个工厂部署在两个链上,我们使用它们来部署一个合约。

在这两种情况下,创建的合约会拥有相同的地址吗?
正如我们在上一节中解释的,地址将取决于:
传递给 CREATE2 的 init code 和 salt
工厂的地址
由于我们部署的是同一个合约,因此 init code 将是相同的。并且我们传递了 salt,所以在这两种情况下我们可以使用相同的值。
换句话说,只要工厂的地址在两个链上都相同,部署合约的地址就会相同。但这正是我们首先想要解决的问题!这是一个先有鸡还是先有蛋的问题。话虽如此,如果我们设法解决了它,我们也就为其他人解决了这个问题,因为工厂合约就可以被无需许可地使用了。
在上一篇文章中,我们看到了一些解决这个问题的方案。我们能否使用其中一种方案在许多链上部署一个任何人都可以使用的 CREATE2 工厂?事实证明,至少存在四个这样的工厂。让我们简要地探讨每一个。
这是我们将讨论的最早的 CREATE2 工厂:它在五年多前部署。它是一个非常简洁的合约,只接收一个 salt 和一个 init code,然后用它们调用 CREATE2,并返回创建合约的地址(如果该地址已经有代码则回滚)。它的整个运行时代码只有69 字节的低级代码。
这个工厂是使用 Nick 的方法部署的。1 部署交易看起来像这样:
{
"to": null, // 这是一个部署交易
"input": "0x6045...0cf3", // init code
// ...其他字段...
"r": "0x2222222222222222222222222222222222222222222222222222222222222222",
"s": "0x2222222222222222222222222222222222222222222222222222222222222222",
"v": "0x1b",
}
注意 r 和 s 字段的值。在我们解释 Nick 方法时,我们说过任何随机值都有很大可能成为一个有效的签名,这是真的。但使用随机值的问题在于,你无法证明你实际上没有生成该签名的私钥。这就是为什么这个交易使用“非我袖中数”(nothing-up-my-sleeve numbers),例如 0x2222...2222。
与之前的工厂不同,create2deployer 是用 Solidity 编写的。这使得它更复杂,运行时也更大,但反过来也意味着它可以拥有一些有用的视图函数,并且更容易从其他合约调用。这个工厂是使用一个受管理的私钥部署的。
这是由 Safe 团队制作的 Arachnid 确定性部署代理的一个分支。唯一的区别在于它的部署方式:它使用一个受管理的私钥,而不是 Nick 的方法,这使得它可以在强制执行重放保护的链中部署。
CreateX 由 create2deployer 的同一作者开发,并在至少两个方面对其进行了改进:
它拥有更多功能(例如 CREATE3,我们稍后会解释)
它使用预签名交易2进行部署,并保留一个受管理的私钥作为备份
这四个工厂已被广泛部署。它们都可以在 evmdiff 列出的链中找到。我查看了 l2beat 中的 rollup 列表,发现需要查到第 15 个链才找到一个缺少工厂的例子:Morph 没有 create2deployer,至少在撰写本文时是这样。
即使某个链中缺少某些工厂,CreateX 也很可能存在:它已经部署到超过 180 个链上。
在我们讨论 CREATE2 工厂时,我们说过只要满足以下条件,我们就可以确定性地部署一个合约:
在所有目标链上都有一个位于相同地址的 CREATE2 工厂
我们每次都使用相同的 salt 和 init code
正如我们刚刚看到的,工厂已被广泛使用,所以我们可以将第一点视为既定事实。在所有这些工厂中,salt 都是由调用者提供的,所以我们每次都可以使用相同的值。
那么 init code 呢?既然我们想要部署同一个合约,很自然地会假设每次 init code 也会相同,但这不一定是真的。一个简单的反例是,一个合约接收的构造函数参数值可能在不同链之间变化。构造函数参数作为 init code 的一部分被包含在内3,这意味着 CREATE2 生成的地址会改变。4
在这种情况下,CREATE2 不是一个选项,因为 init code 会影响部署地址;这是无法避免的。我们在上一篇文章中探讨的基于部署交易的方法没有这个问题,但它们涉及的工作量要大得多。
如果我们不想使用部署交易,又不能使用 CREATE2,那么只剩下一个机制可以部署合约:CREATE 操作码。
假设我们有一个合约工厂,具有以下特点:
它有一个方法,接收合约的 init code 并使用 CREATE 部署它。
它在每个链上都具有相同的地址。
它在每个链上都具有相同的 nonce。
前两个条件很容易实现(这种工厂的代码很简单,我们可以使用 CREATE2 工厂部署它),但第三个条件更难:我们无法控制谁使用这个基于 CREATE 的工厂,每次有人使用它,它的 nonce 都会增加。但是,如果我们部署一个这样的工厂,并在同一个交易中,我们用它来部署我们的合约呢?这就是 CREATE3 方法的精髓。5

在此图中,我们有一个 Create3Factory 合约,它带有一个 deployCreate3 方法。6 绿色参数是用户提供的。MyContract 的地址将如何派生?
我们假设 Create3Factory 在每个链上都以相同的地址存在。这意味着另外两个地址是:
一次性使用的 CREATE 工厂的地址。这个合约的 init code 总是相同的,因此它的地址将仅取决于用户提供的 salt。
我们想要部署的合约的地址。因为一次性使用的工厂刚刚被创建,它的 nonce 保证为 1。这意味着只有它的地址会影响我们合约的地址。
总而言之,我们部署的合约的地址只取决于一次性使用的 CREATE 工厂的地址,而后者又只取决于用户提供的 salt。换句话说,我们合约的地址将只取决于我们传递给 deployCreate3 的 salt。它不会受到其 init code、发送者地址或任何涉及的 nonce 的影响。
虽然这实现了我们的目标,但它引入了一个新问题。当我们在某个链上使用 salt S 部署我们的合约时,任何人都可以到另一个链上,使用相同的工厂和 salt 部署任何合约。这并不理想。通常的解决方法是实现 deployCreate3,使得部署合约的地址只能由特定的 msg.sender 获得(有几种方法可以实现这一点;参见7)。从某种意义上说,这让我们回到了起点:我们需要管理一个私钥,以便每次都使用相同的 msg.sender。但消除对 nonce 的依赖是一个显著的改进。
在这篇文章中,我们涵盖了许多内容:
受管理的私钥和预签名交易
Nick 的方法
基于 CREATE2 的方法,例如 CREATE2 工厂和 CREATE3
但还有更多内容。在本系列的下一篇也是最后一篇文章中,我们将探讨解决该问题的其他一些方法,包括非常巧妙的 ERC-7955。你可以订阅该博客,以便在第三部分发布时收到通知。
init 方法,而不是在构造函数中初始化合约,并且在同一个部署交易中立即用正确的参数调用它。如果你使用 CreateX,你可以通过 deployCreate2AndInit 方法来完成。
↩Create3Factory,但如果不存在,这种模式仍然是可能的。在这种情况下,你需要部署一个合约,它调用 CREATE2 工厂,然后再调用一次性使用的工厂。这个合约不需要确定性部署。
↩CREATE2,而是通过哈希用户提供的 salt 和 msg.sender 来创建一个新的 salt。如果没有其他函数允许你使用任意 salt 来调用 CREATE2,那么这种方法可以保证生成的地址只能由该发送者使用。但这并不是唯一的方法。例如,CreateX 采取了不同的做法。
↩
- 原文链接: paragraph.com/@cetholog...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!