跳到正文

代理合约

代理合约在保留代理地址和状态的同时,将调用转发给实现代码。了解代理模式的运作方式,以及升级、存储和管理员风险从何而来。

更新于

仅供教育参考,不构成投资建议;投资可能产生损失。

直接答案

代理合约是一种中间合约,它把调用转发给另一个通常称为实现合约或逻辑合约的合约。在常见的 EVM 设计中,代理使用 delegatecall,因此实现合约的代码在代理的上下文中运行,而状态和余额仍保留在代理地址上。

这种间接层让系统可以在更换实现的同时保留稳定的用户交互地址。当许多代理共享代码时,它也能降低部署成本。代理并不天然可升级:有些最小代理永久指向同一个实现,而可升级代理则增加了一种受控方式来更换实现或信标。

因此,用户必须同时评估当前实现以及能够更换它的权限。仅有已验证的代理字节码,无法说明明天会执行什么代码。

运作方式

当调用到达代理时,其回退路径会把调用数据复制或转发给某个实现。使用 delegatecall 时,address(this) 是代理,存储读写作用于代理,并保留原始的 msg.sendermsg.value。随后,代理会返回实现合约的返回数据,或随实现合约一起回退。

由于代理元数据与应用状态共用代理的存储空间,标准化槽位有助于避免意外冲突。ERC-1967 为实现地址、信标地址和可选管理员定义了槽位,并建议在这些值发生变化时发出事件。该标准让代理更容易检查,但本身并不能保证升级安全。

常见设计把升级权限放在不同位置:

  • 透明代理:代理区分管理调用和普通用户调用,通常通过独立的管理员合约实现。
  • UUPS 代理:升级逻辑位于实现合约中;实现合约必须授权变更,并持续兼容预期的升级接口。
  • 信标代理:代理向信标查询其实现;更改一个信标会影响所有跟随该信标的代理。
  • 最小克隆:许多小型代理委托给共享代码,并且通常根本没有升级路径。

示例

假设一个金库代理持有用户余额,并委托给实现 A。用户通过代理地址存款,实现 A 的代码会更新代理存储中的余额记录。

随后,治理把 ERC-1967 实现槽改为实现 B。代理地址和记录的余额都没有移动,但未来的调用会执行实现 B 的代码。如果 B 保持存储布局并实现预期规则,用户就会在同一地址看到新行为。

如果 B 重新排列存储变量、遗漏授权检查,或增加一条由升级者控制的提款路径,同一次升级就可能破坏记账或暴露资产。因此,实际操作中不能只问某个合约是否为代理,还要问谁能改变其执行路径、需要多长延迟,以及经过了哪些验证。

风险

  • 升级密钥被攻破:管理员、多签或治理流程可能安装恶意或有缺陷的代码。
  • 存储布局不兼容:改变变量顺序、类型或继承关系,可能让新代码误读或覆盖已有状态。
  • 初始化失败:构造函数不会初始化代理存储;缺失或可重复执行的初始化保护,可能让其他账户取得特权角色。
  • 调用路由意外:函数选择器冲突、管理员专用路由或意外的信标,可能使实际执行路径不同于可见接口。
  • 共享升级影响面:一次信标或实现决策可能同时改变多个合约实例。

在存入资产或授予授权前,应在链上解析当前实现或信标,确认升级权限和任何时间锁,检查已验证源码与存储兼容性,并在适用时查看近期的 UpgradedBeaconUpgradedAdminChanged 事件。首次审查后仍需持续监控,因为执行路径可能改变。

常见误区

  • “代理不存储有意义的状态。”delegatecall 下,应用状态和资产通常属于代理,尽管逻辑来自另一个地址。
  • “实现合约经过验证,系统就无需信任。”升级密钥、治理、信标、初始化和未来实现仍是信任模型的一部分。
  • “每个代理都可以升级。”克隆及其他固定代理可能永久委托给同一个实现;能否升级取决于具体设计和授权代码。

相关主题

来源

导航

搜索知识库...