UUPS:通用可升级代理标准(ERC-1822)
- 原文链接:https://www.rareskills.io/post/uups-proxy
- 译者:AI翻译官,校对:翻译小组
- 本文永久链接:learnblockchain.cn/article…
UUPS 模式是一种代理模式,其中升级功能位于实现合约中,但通过代理合约中的 delegatecall 更改存储在代理合约中的实现地址。上层机制如下动画所示:
与透明可升级代理类似,UUPS 模式通过完全消除代理中的公共函数来解决函数选择器冲突的问题。
正如我们在关于透明可升级代理的文章中所述,功能性以太坊代理至少需要以下两个特性:
一个存储槽:保存实现合约的地址。
一个机制:允许管理员更改实现地址。
ERC-1967 标准规定了保存实现地址的存储槽的位置,但并未规定如何更改实现的地址,也就是说,它将升级机制的选择留给开发者。
UUPS 是一种代理模式,其中更改实现合约地址的机制位于实现合约本身,而不是在代理合约中。
这一差异在以下简化代码中得到了说明:
在升级过程中,_upgradeLogic()函数被 delegatecall 到 UUPSProxy。与透明可升级代理不同,不需要 AdminProxy —— 如果需要,普通 EOA 可以作为管理员。
透明可升级代理使用 AdminProxy 来保持管理员地址不变。由于透明可升级代理必须在每个交易中将 msg.sender 与管理员进行比较,因此希望将 msg.sender 与不可变变量进行比较。然而,UUPS 代理只需要在显式调用 _upgradeLogic()时检查 msg.sender 是否为管理员(这会 delegatecall 到 _upgradeLogic()到实现中)。
这种模式的一个优点是实现逻辑本身可以被升级,也就是说,升级机制可以从一个实现修改到另一个实现。例如,可以从简单的升级逻辑过渡到更复杂的投票或时间锁机制。
这一标准的重要权衡是,如果对缺乏有效升级机制的新实现合约进行升级,则升级链结束,因为无法迁移到下一个实现。换句话说,由于升级机制本身可能是可升级的,存在破坏升级机制的风险。
为了应对这一权衡,一些提案提出在迁移到新实现合约之前,首先检查其是否具有有效的升级机制。UUPS 就是其中一个提案。
在本文中,我们将一般性地解释 UUPS 的工作原理,详细检查 OpenZeppelin 的实现,并讨论在使用此模式时必须考虑的一些漏洞。
OpenZeppelin 目前为透明和 UUPS 代理标准提供实现,但我们推荐使用后者 。原因在于,除了修改升级机制的灵活性外,UUPS 实现更轻,因此在部署和使用过程中消耗更少的 gas。
这是因为不需要部署管理合约或检查交易是否来自合约所有者,这在透明代理中是必需的。然而,这种模式中的每个新实现合约确实需要一个升级函数,这稍微提高了新实现合约的部署成本。
如果实现合约在使用 UUPS 时遇到 24kb 的大小限制,则透明可升级模式可能更合适,因为它不需要包含升级逻辑。
UUPS 最初在 ERC-1822 中定义。
正如我们在前一节中看到的,必须防止代理合约接受不实现 UUPS 标准的新实现合约。换句话说,任何尝试迁移到不符合 UUPS 的实现合约的行为都应回滚。
这就是标准要求每个实现合约都包含一个签名为 proxiableUUID()的函数的原因。该函数的目的是作为兼容性检查,以确保新实现合约遵循通用可升级代理标准。
该函数应返回存储实现地址的存储槽尽管返回值是任意的,标准的支持者本可以定义该函数返回一个字符串,如“嘿,我是 UUPS 合规的”,但返回存储槽更节省 gas。
其思路是在实际迁移到新实现之前调用 proxiableUUID()函数。如果新实现合约正确实现了 proxiableUUID(),则被视为 UUPS 合规,迁移应继续。否则,交易必须回滚。
成功迁移和失败迁移尝试的过程如下图所示。
原始提案建议存储槽地址由公式 keccak256("PROXIABLE")定义,然而,由于 OpenZeppelin 实现使用 ERC-1967 标准,因此其实现中的槽地址由 keccak256("eip1967.proxy.implementation") - 1 定义。我们稍后将在代码中看到这一点。
下面可以看到迁移到新实现的过程动画:https://img.learnblockchain.cn/2024/mp4/upgrade_uups.mp4
在 OpenZeppelin 库中实现 UUPS 标准的合约名为 UUPSUpgradeable.sol。该合约应由实现合约继承,而不是由代理合约继承。 代理通常继承自 ERC1967Proxy,这是一个符合 ERC-1967 标准的最小代理合约。
UUPSUpgradeable.sol 的目的有两个:
提供每个实现必须包含的 proxiableUUID()
函数,以确保 UUPS 合规,
还提供 updateToAndCall()
函数,用于迁移到新实现。正如我们所见,具有此目的的函数必须在每个实现合约中存在。
proxiableUUID() 函数必须在新的实现合约 之前 调用迁移,定义如下,并返回 ERC-1967 标准的存储槽。
function proxiableUUID() external view virtual notDelegated returns (bytes32) {
return ERC1967Utils.IMPLEMENTATION_SLOT; // 符合 ERC-1967 标准
}
负责升级到下一个实现的函数可以有任何名称。由于它是在实现合约内定义的,因此没有函数签名冲突的风险,这在透明代理中也会发生...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!