本文探讨了以太坊可升级智能合约的多种代理模式,包括 UUPS、Transparent、Beacon 和 Diamond 代理模式,这些模式旨在实现安全且可扩展的 dApp 设计。
探索以太坊可升级智能合约:UUPS、透明代理、信标代理和钻石代理模式,实现安全且可扩展的 dApp 设计。
2025-07-18 - 12 分钟阅读
Web3 安全
threesigma's twitterthreesigma's linkedinthreesigma's github
默认情况下,以太坊上的智能合约是不可变的,一旦部署,其代码就无法更改。这种不可变性是信任的基石,确保合约规则不会被随意篡改。然而,不可变性也有一个缺点:如果发现了一个严重的错误或需要新的功能,原始合约就无法修改。唯一简单的方法是部署一个新的合约并迁移所有的用户和数据,这是一个繁琐的过程,涉及移动状态、更新地址以及说服用户切换到新的部署。在实践中,这是容易出错的、昂贵的且会中断用户的。可升级的智能合约通过引入一个间接层来解决这个问题,该间接层将合约的状态与其逻辑分离。在可升级的架构中,用户与代理合约进行交互,该代理合约持有持久状态并将调用委托给包含代码的逻辑合约(实现)。要升级,只需部署一个新的实现合约,并将代理指向这个新的代码,同时代理的地址和存储的数据保持不变。通过这种方式,开发者可以在保持合约的地址、状态和余额不变的情况下修改合约代码。
以太坊中可升级的智能合约通常使用代理模式来实现。在这种设计中,代理合约充当一个前端,存储合约的所有状态,并使用 EVM DELEGATECALL 指令将任何函数调用委托给一个单独的实现合约。DELEGATECALL 允许代理在代理的上下文中执行来自实现合约的代码,这意味着实现的代码可以像操作自己的存储一样操作代理的存储。结果是,用户总是与代理的地址进行交互,但是可以通过更改代理指向的实现地址来交换逻辑。已经出现了几种代理模式来促进升级,每种模式都有其自身的机制和用例。2025 年最广泛使用的模式是透明代理、UUPS 代理、信标代理和钻石(多面)代理。所有这些都依赖于一些底层标准:
在有了这些基础知识之后,让我们概述一下每种主要的代理模式:
透明代理模式是最早流行的可升级代理设计之一(被 OpenZeppelin 较早的 SDK 采用)。透明代理包含内置的逻辑来区分“管理员”调用和“用户”调用。代理有一个管理员地址(在 OpenZeppelin 的实现中,通常通过一个单独的 ProxyAdmin 合约来管理),该地址被授权可以升级合约。当从管理员那里发出调用时,代理不会将这些调用转发到实现。相反,如果调用数据与管理员函数之一(如 upgrade 函数)匹配,代理将执行其自身的管理员例程。这可以防止管理员意外地调用到代理自身的逻辑中。相反,来自非管理员帐户的调用总是被转发到实现逻辑。这种分离解决了潜在的函数名称冲突,并确保管理员不能无意中触发实现函数(“透明”的方面)。在实践中,一个透明代理设置包括:
当在透明模式下使用 OpenZeppelin 的 Upgrades Plugin 时,一个典型的部署流程是:
这种模式之所以被称为“透明”,是因为普通用户不能调用仅限管理员的函数,任何用户调用都会被转发到实现,只有管理员可以触发特殊的升级路径。透明代理的一个缺点是存在轻微的 gas 开销:在每次调用时,代理必须对照管理员地址检查调用者,并可能对照一个函数白名单来决定如何路由调用。此外,额外的 ProxyAdmin 层意味着需要部署和管理更多的合约。关键点:透明代理在代理本身中有一个显式的升级接口,并限制谁可以使用它。这是一个经过验证的模式,具有强大的实现,但正如我们将看到的,更新的模式旨在使代理更加轻量。
UUPS(通用可升级代理标准)是一种代理模式,它将升级逻辑从代理中移出并移入到实现合约中。这个名字来自 ERC-1822,它首次描述了这个设计。在 UUPS 架构中,代理非常简单,本质上只是一个委托调用的存储持有者(通常使用一个基本的 ERC1967Proxy 合约),它没有用于升级或管理员检查的特殊代码。相反,实现合约本身包含一个函数(通常是 upgradeTo(address newImpl) 或类似的函数),当由一个授权账户调用时,它将直接将新的实现地址写入到代理的存储槽中(使用已知的 EIP-1967 槽)。实际上,实现合约负责升级指向它的代理。OpenZeppelin 的 UUPSUpgradeable 基合约通过提供一个内部的 _authorizeUpgrade() 函数(将被访问控制逻辑覆盖)和一个 upgradeTo 函数来实现这一点,如果获得授权,该函数将执行升级。因为升级函数存在于实现中,所以只有包含 UUPS 逻辑的合约才能触发代理上的升级。代理本身不检查谁在调用,它将委托调用,而逻辑合约将在 _authorizeUpgrade 中强制执行权限。
UUPS 代理消除了对 ProxyAdmin 合约的需求,并降低了部署成本。代理实际上只是委托调用所需的最低限度(因此在 gas 方面更便宜且更简单)。OpenZeppelin 指出,“TransparentUpgradeableProxy 的部署成本高于 UUPS 代理的可能性”,并且由于其轻量级的多功能性,他们现在建议在大多数用例中使用 UUPS。另一个好处是,如果想要锁定代码(使合约在此后有效地不可变),可以在最终实现中完全删除升级逻辑。开发者可以通过升级到一个不包含 UUPS 升级函数的实现,通过放弃所有权(如果所使用的访问控制系统允许)或通过其他方式禁用升级函数(通过一个标志)来实现这一点。如果有人想要放弃可升级性,这为代理提供了一条“升级退出”路径。
必须确保只有授权的实体才能调用 UUPS 中的 upgradeTo 函数。通常,实现继承 UUPSUpgradeable,开发者覆盖 _authorizeUpgrade 以允许只有所有者(或一个多重签名/DAO)才能调用它。如果忽略了这一步,攻击者可以直接在代理上调用 upgradeTo(它被委托给实现),从而用恶意代码替换实现。同样重要的是,UUPS 实现本身是升级安全的。事实上,早期版本的 OpenZeppelin 的 UUPS 有一个已知的漏洞,如果一个实现合约没有被初始化,攻击者可以部署他们自己的实现并执行升级(我们将在第 2 部分中讨论这个问题)。OpenZeppelin v4.5+ 添加了一个 ERC1822 兼容性检查到 UUPS,以防止设置非 UUPS 实现,避免通过升级到缺少升级逻辑的合约而意外地“破坏”代理。总之,UUPS 代理用以下方式回答了“我们将升级逻辑放在哪里?”这个问题:将其放在实现中。这种模式已经变得流行,因为它提供了与透明代理相同的功能结果,但开销更少。折衷方案是,每个实现都携带一些额外的代码(升级函数),并且需要仔细设计以避免错误地允许从错误的上下文中进行升级(例如,将 UUPS 实现与透明代理一起使用可能是危险的)。如果使用得当,UUPS 被认为是当今安全且高效的标准。
信标代理模式涉及一个名为信标的中间合约来管理多个代理的实现地址。这由早期的 DeFi 借贷初创公司 Dharma(2018-2020)推广,并且在你有许多应该全部使用相同实现(并且一起升级)的代理实例时很有用。该设置是:
通过这种模式,升级所有代理就像在信标上调用一次 upgradeBeacon(newImpl)(由管理员执行)一样简单,所有 BeaconProxy 实例然后将开始委托给信标返回的新实现。这对于像工厂合约这样的场景来说是有效的,这些工厂合约为用户生成许多克隆代理:与逐个升级数百个代理(与透明代理或 UUPS 代理一样,每个代理都是独立的)不同,对信标的单一交易可以一次更新所有这些代理的逻辑。OpenZeppelin 提供了 UpgradeableBeacon 和 BeaconProxy 合约。信标有其自身的管理员和升级函数,而每个 BeaconProxy 只是简单地实现 _implementation() 以从信标的地址获取实现。值得注意的是,代理本身没有管理员逻辑,并且不能单独升级,信标是升级控制的单一点。一个含义是,如果一个项目使用信标模式,所有实例都信任信标的所有者。例如,假设一个 DeFi 平台使用一个信标部署了 100 个 vault 代理; 如果信标的管理员密钥被泄露,攻击者可以将信标升级为恶意逻辑,从而一次影响所有 100 个 vault。相反,使用单独的代理(透明代理/UUPS 代理),每个代理都可能拥有单独的管理员,或者至少爆炸半径是一次一个代理(除非它们都共享相同的管理员密钥)。简而言之,信标代理对于可升级的克隆工厂很有用,提供了一种原子的多重升级。这种模式在可升级的 ERC20 或 ERC721 工厂合约以及在许多实例中重复使用一个模板的其他场景中得到了应用。
钻石标准 (EIP-2535) 将代理的可升级性提升到一个更细粒度的级别。“钻石”本质上是一个代理,它可以有多个实现合约(称为 facets)来管理不同的函数。钻石维护一个从函数选择器到 facet 地址的映射,而不是一个单一的实现地址。有一些标准化的函数(称为 loupe 函数)可以通过一个称为 diamondCut 的操作来添加、替换或删除此映射中的函数。在一个交易中,你可以替换多个函数以指向新的 facet 合约(因此可以原子地升级多个逻辑片段)。钻石的回退函数然后根据函数选择器将调用分派到适当的 facet,类似于路由器的工作方式。
钻石模式本质上概括了代理的思想:一个透明代理或 UUPS 代理就像一个只有一个 facet 的钻石。一个钻石可以有 N 个 facets,这使得开发者可以非常灵活地编写和升级复杂的系统。它对于大型系统特别有用(例如,游戏应用程序 Aavegotchi 和 DeFi 协议 BarnBridge 是钻石的已知采用者)。缺点:钻石可以说是最复杂的实现和推理模式。升级逻辑 (diamondCut) 本身必须小心保护,以避免恶意替换。存储管理也更加复杂,通常钻石使用一种方法,其中每个 facet 使用其自己的存储“命名空间”或结构以避免槽冲突(更多关于命名空间的信息请参见第 4.3.3 节)。用于钻石的工具不太主流,尽管存在标准,并且 EIP 作者提供了参考实现。
第 1 部分概述了什么是可升级智能合约,以及如何使用各种代理模式来实现它们。我们了解了透明代理与 UUPS 代理之间的区别(以及为什么 OpenZeppelin 倾向于 UUPS 用于新项目),以及更高级的信标和钻石模式。掌握了这些基础知识,我们就可以深入研究可升级合约的阴暗面了。在第 2 部分中,我们将探讨与可升级合约相关的常见漏洞、陷阱和真实世界的黑客攻击(从未初始化的代理到恶意升级),从过去的失败中吸取教训,为未来的安全设计提供信息。
- 原文链接: threesigma.xyz/blog/web3...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!