Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

可升级代理漏洞

代理通过 DELEGATECALL 执行实现合约的代码。委托执行时,业务状态保存在代理中,address(this) 为代理地址,msg.sender 保留代理收到调用时的调用者。直接调用实现地址则使用实现自己的状态。

代理模式与升级入口

模式升级逻辑权限与状态
Transparent位于代理侧,OpenZeppelin 实现通常由 ProxyAdmin 管理代理区分管理员调用与业务调用
UUPS位于当前实现中,经代理委托执行实现检查权限并修改代理的实现地址槽
BeaconBeacon 保存实现地址多个代理使用该 Beacon 返回的实现

模式名称不决定具体库版本的接口。OpenZeppelin 5.4.0 的 UUPS 升级入口为 upgradeToAndCall;原始 EIP-1822 与现代 OpenZeppelin 使用的实现地址槽也不相同,见 UUPS。

存储槽冲突

手写代理若把 owner、implementation 等变量放在线性布局起始位置,委托执行的实现变量可能写入相同槽位。冲突发生在代理的存储中,并不是两个部署地址天然共享全部存储。

EIP-1967 为实现地址、管理员和 Beacon 定义特殊槽位,降低与 Solidity 常规布局重叠的风险。该设计依赖哈希安全假设,不能阻止实现代码主动向这些槽位写入数据。

代理初始化与实现初始化

代理初始化设置代理的 owner、角色和业务参数。若代理部署后仍暴露未保护的初始化入口,攻击者可能抢先调用并取得代理权限。部署时附带初始化数据,可将创建代理和设置状态放在同一笔交易中。

实现合约的构造函数仅影响实现地址。OpenZeppelin 可升级实现通常在构造函数调用 _disableInitializers(),锁定实现自身的初始化入口;该操作不会替代理完成初始化。

直接调用实现的 initialize() 不会自动修改代理的 owner。历史 UUPS 漏洞涉及特定旧版本的直接升级调用和委托自毁路径,影响的是实现代码可用性,详见代理进阶。

OpenZeppelin 5.4.0 的升级入口使用 onlyProxy 检查活动代理上下文,再由 _authorizeUpgrade 检查升级权限。msg.sender == address(this) 表示调用者是合约自身,不能替代上述两个检查。

函数选择器与外部执行权限

代理管理接口与业务接口可能出现相同选择器。透明代理按管理员身份区分路由;UUPS 将升级接口放在实现中,但仍需避免业务接口冲突,并保护升级权限。

外部输入若能选择 delegatecall 目标,可能让不可信代码修改当前合约的全部状态。普通 call 使用被调用合约的上下文,也可能调用未正确鉴权的外部入口,但不能直接通过 ABI 调用 internal 或 private 函数。

消息转发和插件系统需要同时约束调用者、目标、方法与参数。只限制目标地址不能证明所有可调用方法都安全。

升级时的布局兼容

// V1:a 在 slot 0,b 在 slot 1
uint256 public a;
uint256 public b;

// 不兼容的 V2:b 改为 slot 0,将读取旧 a 的值
uint256 public b;

普通线性布局的升级通常保留既有变量顺序、类型和含义,再按兼容规则追加状态。__gap 用于预留布局空间,不能自动修复已删除或重排的业务变量。

ERC-7201 按命名空间组织存储,但命名空间内部仍需保持布局兼容。从旧布局切换到命名空间布局需要明确的数据迁移;改变寻址方式不会自动搬移旧值。

升级安全条件

  • 代理初始化与实现锁定分别验证,初始化参数对应明确的权限主体。
  • UUPS 检查调用上下文、升级授权及新实现的兼容接口。
  • 新旧布局与业务不变量保持兼容,迁移和重复初始化路径可被验证。
  • 升级治理明确多签、时间锁或其他授权机制,管理员能力纳入信任模型。
  • 部署地址、实现槽、源码版本和实际执行字节码相互对应。

参考资料