本文深入探讨了在多链环境中实现智能合约确定性部署的关键技术 CREATE2,解释了其原理和优势,并讨论了使用 CREATE2 时需要注意的风险,例如 Metamorphic 合约的潜在漏洞以及跨链兼容性问题。此外,文章还介绍了 OpenZeppelin Defender 如何简化确定性部署流程并提高安全性。
随着 layer two 网络上用户数量的增长,许多项目寻求将其智能合约部署到多个链上,以便用户能够以更低的 gas 费用进行交易。然而,合约部署涉及到创建一个唯一的地址,该地址来源于部署者的钱包信息和合约字节码。为了确保流畅和安全的用户体验,项目可以配置他们的设置,以便每次合约部署都保留与原始合约相同的地址。
EVM 链上的智能合约通常使用 CREATE 操作码进行部署,该操作码在由部署者地址和部署者账户的 nonce(即,从该地址发送的交易计数)确定的地址处实例化一个合约。该地址来源于以下公式:
keccak256(deployingAddress ++ nonce)[12:]
在底层,合约部署只是常规交易,在接收者字段中没有指定地址。这种部署会导致一个新的合约实例在链上部署,该实例对于每个部署交易都有一个不同的、但不可复制的地址。
虽然 CREATE 为大多数合约部署提供了基础,但它无法提前获得独立于 nonce 的合约地址,这对想要跨多个 EVM 链部署的复杂去中心化应用提出了挑战。为了解决这个问题,以太坊社区在 2019 年引入了 CREATE2 操作码。这个新的操作码允许我们称之为确定性或反事实部署——即在部署之前预先确定合约地址的能力。
CREATE2 操作码与 CREATE 类似,但它引入了“salt”的概念,这是一个独立于 nonce 的参数,它与部署者的地址和合约的字节码一起用于派生地址。CREATE2 允许在交易发送之前在链下计算得到的地址处创建合约,更重要的是,它允许项目在不同的链上以相同的地址部署相同的合约,而无需担心部署者之前完成了多少交易。部署的合约的地址使用以下公式派生:
keccak256(0xff ++ deployingAddr ++ salt ++ keccak256(bytecode))[12:]
这种机制使得合约能够在实际存在于链上之前被交互,甚至被引用。这可以帮助更复杂的项目,因为他们可以知道他们的合约将被部署到的地址,从而使文档和集成更容易(每个合约在所有支持的链上只有一个地址)。
使用确定性部署的方法是与所谓的合约工厂进行交互,合约工厂是一个父合约,它预先部署在所有我们想要进行确定性部署的链上的相同地址(为了保持部署者地址相同),其唯一目的是使用 CREATE2 创建新合约。
下面是一个简单的工厂示例:
复制
强大的力量通常伴随着巨大的风险,CREATE2 也是如此。在使用采用此部署方法的合约时,考虑几个关键方面非常重要:
我们稍后将深入探讨这些要点。
“变质合约”是一个相对复杂的主题,但简单来说,它可以通过一些巧妙的技巧使合约“变质”成其他东西。
如果变质合约由工厂使用 CREATE2 部署,并使用 selfdestruct 来完成这一点,这是 EVM(目前)允许合约及其相关状态被删除并在同一地址重新部署新字节码的唯一方法。
既然确定相同地址依赖于字节码相同,这怎么可能呢?即使合约自我销毁,也应该只允许将相同的字节码部署到相同的地址。但是,如果攻击者从实现合约动态加载代码,他们将保留变质合约的 init code,生成相同的确定性地址,但能够根据需要更改运行时代码。
变质合约有合法的用例,例如作为流行的代理模式的替代方案用于升级,但总的来说,它们是危险的,因为毫无戒心的用户可能会信任它们,认为它们具有不可变代码的承诺,但实际上它们隐藏了一个后门,随时准备让攻击者在用户脚下更改字节码。在此处阅读更多。
与 EVM 兼容但具有不同实现细节的网络可能导致与以太坊不同的字节码和不同的地址派生机制。 zkSync Era 就是这种情况,并且可能导致相同的合约在不同的链上部署到不同的地址,尽管具有相同的地址派生详细信息。
虽然不是很理想,但将特权角色分配给合约的部署者(使用 msg.sender)仍然是一种常见的做法。当使用常规部署时,这运作良好,因为 msg.sender 是启动交易的 EOA。但是在确定性部署的情况下,由于它们是通过工厂合约进行的,如果将权限分配给消息的发送者,它最终会成为工厂合约,这很可能不是预期的。
由于工厂合约通常是不可变的(而且应该如此),这些角色最终会永远卡住,如果你没有办法撤销和重新分配(例如,你没有将管理角色分配给你控制下的帐户),你最终会得到一个角色无法再被管理的合约。
我们建议在分配角色时始终明确,例如将地址传递给合约的构造函数。这有助于避免角色永远卡住的情况。
现在我们已经全面介绍了确定性部署,包括它们的机制、工厂的作用、涉及的操作码以及相关的风险和挑战,你可能很渴望将这些知识应用到你的项目中。当你开始使用 hardhat/foundry/任何你喜欢的框架等工具探索确定性部署时,你可能会发现这个过程并不总是直接或用户友好的。通常,它需要额外的插件或手动编写交易并在链上部署它们。
但是,OpenZeppelin Defender 可以显著简化和增强你的确定性部署和整体部署过程的安全性。具体来说,对于使用 hardhat(以及 Foundry 即将推出)的项目,我们将便捷的方法集成到我们广泛使用的 hardhat-upgrades 插件中,从而实现轻松的 CREATE2 部署。此外,此集成还包括在 Etherscan 上进行合约验证的额外好处,使你的部署之旅更加顺畅和安全。
如果你没有 Defender 帐户,你可以在此处加入候补名单。
登录后,按照此教程指导你获取 API 密钥和机密,并配置你的部署环境。
获得 API 密钥后,就可以将一些代码添加到你的 hardhat.config 文件和你的部署脚本中了。你只需要添加一些内容:
复制
复制
要部署,运行:
$ yarn hardhat run scripts/deploy.ts – network <yourNetwork>
正如你可以从下面的例子中看到的,我们简单的 Box 合约已经部署到 Goerli 和 Sepolia 的相同地址,并且源代码已验证!
这是 Etherscan 上的结果:

为了进一步验证,你可以在 Defender 中配置的环境中查看“部署”页面,以获取部署的历史记录及其状态。我们可以看到我们的 Box 合约已成功部署和验证。
跨多个链的确定性部署仍然是大型项目及其用户的重要功能。 然而,直到今天,确定性部署一直受到运营和安全挑战的困扰。 借助 OpenZeppelin Defender,这些挑战很容易解决。 请联系我们了解更多关于我们的专业服务团队如何帮助客户应对整个开发生命周期中遇到的挑战,包括确定性部署。 或者,如果你对你的团队独立工作的能力充满信心,你可以在此处请求直接访问 Defender 的 beta 版。
- 原文链接: openzeppelin.com/news/ev...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!