本文档是使用代理模式的基本指南,主要讨论了使用代理的优缺点、代理的类型(推荐 UUPS 代理),以及使用代理的最佳实践,包括使用 OpenZeppelin 的代理合约、使用 Initializer 而不是 Constructor、单次交易中设置和初始化等。
使用代理:一份必要的指南
注意:在本文中,我将把没有使用代理模式的合约称为“普通合约”。这只是为了给非代理合约贴上一个标签,并不意味着它们不那么令人兴奋。
仅仅因为它是一个智能合约并不意味着它应该自动成为一个代理。代理应该被认为是用于特定工作的工具,而不是一种普遍的生活方式选择。普通方法和代理方法都有优点和缺点。
我们大多数人想要代理的主要原因是它们允许我们规避区块链最突出的特性之一:永久性。
一个被代理的合约是可以更改的。你可以添加功能,或者修改你之前添加的功能。如果这是你需要的东西,那么你几乎肯定想使用代理。
因为它们可以被修改,代理将允许你撤销许多过去的错误。你是否不小心将数百万美元的用户资金困在你的前沿定制代币化同质化超级金库中?不用担心,有了代理,你可以添加一个新函数 sorryAboutThat()
,每个人都可以调用它来取回他们的代币。
在许多情况下,你无法更改智能合约这一事实将被认为是一个至关重要的优势。无法更改的代码可以被依赖在长期内保持相同的行为。这可能正是你想要的。
控制代理升级功能的帐户基本上可以将该合约变成任何东西。没有任何当前功能是固定的,并且可以根据代理管理员的意愿添加,更改或删除任何功能。
当然,你只会负责任地使用这种权力,但你的用户可能不想依赖它。他们可能会担心合约可能会变成将所有投资的代币发送到一个未知的地址,然后调用 selfdestruct()
的东西。
向你的用户证明这种事情永远不会发生的一种方法是使用普通合约。
调用代理总是比直接调用普通合约中的等效函数稍微昂贵一些。这只是一个非常小的差异,但如果你想最小化 gas 成本并且真的不需要代理,也许你最好选择普通合约。当然,代理的部署成本总是更高,因为你总是会为相同的功能部署更多的合约。
尽管有很多工具可以使使用代理变得简单易用(这篇博文就是一个开始),但代理始终是增加复杂性的来源。部署和使用代理比部署和使用等效的普通合约需要更多的时间和精力,因为它们本质上更复杂。也许你最好将这些精力投入到你项目的其他部分?
代理引入了一些安全问题。我们将尝试在此处警告你最常见的担忧以及如何避免它们,但如果你根本不使用代理,则可以绝对确定地避免它们。
在通常讨论的代理类型中,普遍接受的建议是通用可升级代理标准,通常称为 UUPS,官方1发音为“oops”(尽管我不确定这是否真的流行起来)。
如果你只想使用基本的代理,请选择 UUPS 并跳到下一节。
如果你好奇为什么推荐 UUPS 而不是其主要替代方案透明代理,主要是因为2 UUPS 代理具有删除其可升级性的能力,从而锁定其代码。
还有各种高级代理,超出了本文的范围。信标代理3 允许你一次升级多个合约。模块化代理系统4 允许一个合约从其他合约安装和卸载函数组。它们都需要对代理有很好的理解才能安全使用。
特别是对于代理合约本身,请务必使用OpenZeppelin 的代理合约而不进行修改。它们已经过专家的透彻检查并在实际项目中进行了测试。这是一个可靠的构建基础。
代理是具有隐藏危险的棘手事物,因此除非你熟悉所有技术陷阱(这远远超出了本文的范围),否则不要修改 OpenZeppelin 代码或向代理合约添加功能。将你的编码精力集中在你自己的项目的功能上,在你的实现合约中。
对于 UUPS,你希望使用 ERC1967Proxy
作为你的代理合约并在你的实现合约中继承自 UUPSUpgradeable
。
对于透明代理,你希望使用 TransparentUpgradeableProxy
作为你的代理(并参阅下面关于 ProxyAdmin
的内容)。
构造函数的工作方式与你在代理合约中习惯的方式不同。始终使用初始化器代替。初始化器的工作方式与构造函数非常相似,但它们是普通函数,因此你需要使用类似 Openzeppelin 的 Initializable
5来确保它们在部署后不会被调用。
如果你正在使用 Openzeppelin 的 Initializable
,请将此添加到你的合约中:
constructor() {
_disableInitializers();
}
如果你正在使用不同的初始化系统,请注意你确实需要某种构造函数,并且它需要做的是禁用初始化器。这是一种安全措施,可防止攻击者直接初始化你的实现合约。实现合约中的构造函数不会为代理执行,因此你的初始化器仍会在需要时运行。
你不希望在你的合约已部署但未初始化时允许任何人与它进行交互,因此最好在单个交易中完成这两项操作。有三种常见的方法可以实现此目的:
有几个系统可以为你处理此问题。Openzeppelin 有 Hardhat 和 Truffle 的插件6。还有透明代理的 Brownie mix7。
这是一种更高级的技术,可以提供更精细的控制,但需要对代理机制有更深入的了解。如果你的实现合约首先部署,则可以部署代理,并在其构造函数8中对实现合约的初始化器进行编码调用。
如果你要部署整套合约,你可能想要创建一个合约,该合约本身部署所有代理和实现合约,并且还调用它们的所有初始化器。如果你有一个复杂的系统想要在单个交易中在链上部署,这可能是一个很棒的方法。
如果可以,请在仅调用一次的单个函数中部署所有内容。如果这不可能,请确保你在同一个函数调用中部署和初始化每个合约。不要在一个函数中部署,然后在另一个函数中初始化。
哦,你的单次使用部署合约应该是普通合约,而不是代理。
代理系统越多越不好。确保你已将每个合约配置为仅使用一个系统,并且如果可能,请在整个项目中仅使用该代理系统。
你可能认为将你的代理指向其他人的合约是安全的,但通常这不是一个好主意。其他人的合约可能会做不可预测的事情,例如 selfdestruct()
。还有一些称为变形智能合约9的讨厌的东西,它们实际上可以修改其代码(但不是代理的推荐替代方案)。为了确保你可以信任合约不会更改,你应该自己部署它;假设你可以信任自己。
如果你要部署透明代理,最好使用 Openzeppelin 合约 ProxyAdmin
10作为代理的管理员。这提供了一些可用的函数来执行代理的管理功能。如果你不使用像 ProxyAdmin
这样的系统,代理的管理员帐户将无法以任何方式与合约交互,除了调用其代理管理功能。
对于 UUPS 代理,也可以使用 ProxyAdmin
。它仍然兼容,但没有提供那么多好处。
https://ethereum-magicians.org/t/eip-1822-universal-upgradeable-proxy-standard-uups/2842 ↩
https://docs.openzeppelin.com/contracts/4.x/api/proxy#transparent-vs-uups [↩](https://blog.sigmaprime.io/proxy-guide.html#fnref:ii "Jump back to footnote 2 in the text".html)
https://docs.openzeppelin.com/contracts/4.x/api/proxy#beacon [↩](https://blog.sigmaprime.io/proxy-guide.html#fnref:iii "Jump back to footnote 3 in the text".html)
https://archive.devcon.org/archive/watch/6/unlimited-size-contracts-using-solidity/?tab=YouTube ↩
https://docs.openzeppelin.com/contracts/4.x/api/proxy#Initializable [↩](https://blog.sigmaprime.io/proxy-guide.html#fnref:v "Jump back to footnote 5 in the text".html)
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/d00acef4059807535af0bd0dd0ddf619747a044b/contracts/proxy/ERC1967/ERC1967Proxy.sol#L22-L24 ↩
https://mixbytes.io/blog/metamorphic-smart-contracts-is-evm-code-truly-immutable ↩
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/transparent/ProxyAdmin.sol ↩
- 原文链接: blog.sigmaprime.io/proxy...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!