可升级智能合约详解(2025):第一部分 - Solidity中的代理和UUPS模式

本文是关于以太坊可升级智能合约的第一部分,主要介绍了可升级智能合约的概念,以及几种常见的代理模式,包括透明代理、UUPS代理、Beacon代理和Diamond代理。文章详细解释了每种模式的原理、优缺点和适用场景,为读者理解和选择合适的可升级方案提供了基础。

Image

可升级智能合约详解 (2025):第一部分 - Solidity 中的代理和 UUPS 模式

简介

以太坊上的智能合约默认是不可变的,一旦部署,其代码就无法更改。这种不可变性是信任的基石,确保合约规则不会被随意篡改。然而,不可变性也有一个缺点:如果发现了一个关键错误或需要新的功能,原始合约就无法修改。唯一简单的解决方案是部署一个新的合约并迁移所有用户和数据,这是一个繁琐的过程,包括移动状态、更新地址以及说服用户切换到新的部署。在实践中,这是容易出错、代价高昂且会中断用户的。

可升级智能合约通过引入一个间接层来解决这个问题,该间接层将合约的状态与其逻辑分离。在可升级的架构中,用户与代理合约进行交互,该代理合约持有持久状态并将调用委托给包含代码的逻辑合约(实现)。要升级,需要部署一个新的实现合约,并将代理指向这个新代码,同时代理的地址和存储的数据保持不变。通过这种方式,开发者可以修改合约代码,同时保留合约的地址、状态和余额。

代理模式

以太坊中的可升级智能合约通常使用代理模式来实现。在这种设计中,代理合约充当前端,存储合约的所有状态,并使用 EVM DELEGATECALL 指令将任何函数调用委托给单独的实现合约。DELEGATECALL 允许代理在代理的上下文中执行来自实现合约的代码,这意味着实现的代码可以像操作自己的存储一样操作代理的存储。结果是用户始终与代理的地址交互,但可以通过更改代理指向的实现地址来交换逻辑。

已经出现了几种代理模式来促进升级,每种模式都有其自身的机制和用例。2025 年最广泛使用的模式是透明代理、UUPS 代理、信标代理和钻石(多面)代理。所有这些都依赖于一些底层标准:

  • 非结构化存储:为了避免代理状态和实现状态之间的冲突,代理中保留了特定的存储槽(通常由 EIP-1967 定义)用于实现地址和其他代理数据。这确保了代理自身的变量(例如指向实现的指针)不会覆盖实现的 State Variables。EIP-1967 是一个以太坊标准,它通过采用 keccak256("eip1967.proxy.<name>") − 1 来修正三个确定性存储槽:实现、管理和信标。这些高的、伪随机的插槽最大限度地减少了与实现存储和任何其他自定义命名空间的冲突。

  • 委托逻辑:基本的代理合约的实现通常提供一个 fallback() 函数,该函数使用 delegatecall 将任何调用转发到当前的实现。然后将委托调用的成功或失败以及返回数据返回给调用者,这使得代理对最终用户来说在很大程度上是透明的。

有了这些基础知识,让我们概述一下每个主要的代理模式:

透明代理

透明代理模式是最早流行的可升级代理设计之一(被 OpenZeppelin 较早的 SDK 采用)。透明代理包括内置逻辑来区分“管理”调用和“用户”调用。代理有一个管理地址(通常在 OpenZeppelin 的实现中通过单独的 ProxyAdmin 合约来管理),该地址被授权升级合约。当从管理员处发出调用时,代理不会将这些调用转发到实现。相反,如果调用数据与其中一个管理功能(例如升级功能)匹配,代理将执行其自身的管理例程。这可以防止管理员意外调用代理自身的逻辑。相反,来自非管理帐户的调用始终转发到实现逻辑。这种拆分解决了潜在的函数名冲突,并确保管理员不会意外触发实现函数(“透明”方面)。

在实践中,透明代理的设置包括:

  • 部署了逻辑的实现合约。

  • 代理合约(通常是 TransparentUpgradeableProxy),它持有实现地址和管理地址。它实现了诸如 upgradeTo(newImpl) 这样的函数,这些函数只能由管理员调用,以及委托回退。

  • ProxyAdmin 合约(在 OpenZeppelin 的架构中),它是一个单独的合约,实际上持有一个或多个代理的升级权限。部署者或治理实体控制 ProxyAdmin,并且它反过来控制代理的升级(代理的管理员设置为 ProxyAdmin 的地址)。这种间接性允许单个多重签名或 DAO 通过单个管理合约来管理许多代理的升级。

当在透明模式下使用 OpenZeppelin 的 Upgrades Plugin 时,典型的部署流程是:

  1. 部署实现合约(逻辑合约)。

  2. 部署代理合约,将其指向实现,并在代理上调用初始化器函数(而不是构造函数)。初始化器在代理的存储中设置状态(例如,初始化变量,设置所有权)。

  3. 部署 ProxyAdmin 合约(如果尚未部署)。代理的管理员设置为这个 ProxyAdmin

  4. 为了以后进行升级,项目所有者调用 ProxyAdminupgrade(proxy, newImplementation) 函数,该函数反过来将代理的存储实现地址更新为新的地址。

这种模式被称为“透明”的,因为常规用户无法调用仅限管理员的函数,任何用户调用都将转发到实现,只有管理员可以触发特殊的升级路径。透明代理的一个缺点是存在轻微的 gas 开销:在每次调用时,代理必须对照管理员地址检查调用者,并可能检查函数白名单,以决定如何路由调用。此外,额外的 ProxyAdmin 层意味着需要部署和管理更多的合约。

关键点:透明代理在代理本身中具有显式的升级接口,并限制谁可以使用它。这是一个经过验证的模式,具有健壮的实现,但是正如我们将看到的,较新的模式旨在使代理更轻量化。

UUPS 代理

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+ 向 UUPS 添加了 ERC1822 合规性检查,以防止设置非 UUPS 实现,从而避免通过升级到缺少升级逻辑的合约而意外地“破坏”代理。

总而言之,UUPS 代理回答了“我们将升级逻辑放在哪里?”这个问题:将其放在实现中。自提供与透明代理相同的功能结果(但开销更少)以来,这种模式已变得流行。权衡之处在于,每个实现都带有额外的代码(升级功能),并且需要仔细设计以避免错误地允许从错误的上下文进行升级(例如,将 UUPS 实现与透明代理一起使用可能很危险)。通过正确的使用,UUPS 在今天被认为是安全有效的标准。

信标代理

信标代理模式涉及一个名为信标的中间合约,用于管理多个代理的实现地址。这由早期阶段的 DeFi 借贷初创公司 Dharma(2018-2020)推广,并且当你有多个应全部使用相同实现(并且应统一升级)的代理实例时,此模式非常有用。设置为:

  • 单个 UpgradeableBeacon 合约持有当前实现的地址,并且具有可以更新该地址的所有者(管理员)。

  • 部署了多个 BeaconProxy 合约,每个合约本身都不存储实现指针,而是知道信标合约的地址。当 BeaconProxy 收到调用时,它将查询信标以获取当前的实现,然后委托给该实现。

使用这种模式,升级所有代理就像在信标上(由管理员)调用一次 upgradeBeacon(newImpl) 一样简单,所有 BeaconProxy 实例随后将开始委托给信标返回的新实现。对于诸如为用户生成许多克隆代理的工厂合约之类的场景,这非常有效:与其像透明或 UUPS 那样逐个升级数百个代理(每个代理都是独立的),不如通过单个事务来信标可以一次更新所有代理的逻辑。

OpenZeppelin 提供了 UpgradeableBeaconBeaconProxy 合约。信标具有自己的管理和升级功能,而每个 BeaconProxy 仅实现 _implementation() 以从信标的地址获取数据。值得注意的是,代理本身没有管理逻辑,并且不能单独升级,信标是升级控制的单点。一个含义是,如果项目使用信标模式,则所有实例都信任信标的所有者。例如,假设 DeFi 平台使用信标部署 100 个保管库代理;如果信标的管理密钥受到威胁,攻击者可以将信标升级为恶意逻辑,从而一次影响所有 100 个保管库。相反,使用单独的代理(透明/UUPS),每个代理都可能具有单独的管理员,或者至少爆炸半径一次是一个代理(除非它们无论如何都共享相同的管理密钥)。

简而言之,信标代理对于可升级的克隆工厂很有用,可提供原子多重升级。该模式用于可升级的 ERC20 或 ERC721 工厂合约,以及在许多实例中重复使用一个模板的其他方案。

钻石 (EIP-2535)

钻石标准 (EIP-2535) 将代理升级扩展到更精细的级别。“钻石”本质上是一个代理,可以具有多个实现合约(称为 facets),这些合约控制着不同的功能。钻石没有单个实现地址,而是维护从函数选择器到 facet 地址的映射。有一些标准化的函数(称为 loupe 函数)可以通过称为 diamondCut 的操作来在此映射中添加、替换或删除函数。在一个事务中,你可以交换多个函数以指向新的 facet 合约(因此可以原子地升级多个逻辑片段)。然后,钻石的回退函数基于函数选择器将调用分派到适当的 facet,类似于路由器的功能。

钻石的主要特点:

  • 模块化:你可以将合约逻辑组织成类别 (facets) 并独立升级它们。这有助于管理可能达到大小限制的大型合约。钻石实际上没有大小限制,因为你可以随时添加更多 facets。

  • 原子升级:diamondCut 允许一次升级多个 facets,从而确保可以一致地一起更新相互依赖的函数。该标准甚至允许执行初始化函数作为 diamondCut 的一部分,以初始化正在添加的 facets 的任何新状态。

  • 内省:EIP-2535 定义了“loupe”函数,该函数使任何人都可以查询钻石当前具有哪些 facets 和函数。这有助于提高透明度,用户/UI 可以随时检查钻石以查看其组成。

钻石模式本质上概括了代理的想法:透明或 UUPS 代理就像具有一个 facet 的钻石。钻石可以具有 N 个 facets,从而使开发人员可以灵活地组成和升级复杂的系统。它对于大型系统特别有用(例如,游戏应用程序 Aavegotchi 和 DeFi 协议 BarnBridge 是钻石的已知采用者)。

缺点:钻石可以说是实现和推理的最复杂的模式。升级逻辑 (diamondCut) 本身必须经过仔细保护,以避免恶意替换。存储管理也更加复杂,通常钻石使用一种方法,其中每个 facet 都使用其自己的存储“命名空间”或结构,以避免插槽冲突(有关命名空间的更多信息,请参见 4.3.3 节)。钻石的工具不太主流,尽管存在标准,并且 EIP 作者提供了参考实现。

结论

第 1 部分概述了什么是可升级智能合约,以及如何使用各种代理模式来实现它们。我们了解了透明代理与 UUPS 代理(以及为什么 OpenZeppelin 倾向于将 UUPS 用于新项目),以及更高级的信标和钻石模式。

掌握了这些基础知识,我们就可以深入研究可升级合约的阴暗面了。在第 2 部分中,我们将探讨与可升级合约相关的常见漏洞、陷阱和真实世界的黑客攻击(从未初始化的代理到恶意升级),从过去的失败中吸取教训,以为未来的更安全的设计提供信息。

  • 原文链接: x.com/threesigmaxyz/stat...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Three Sigma
Three Sigma
Three Sigma is a blockchain engineering and auditing firm focused on improving Web3 by working closely with projects in the space.