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

可升级代理进阶

代理安全涉及实现地址寻址、业务布局、调用上下文与初始化状态。标准槽位解决元数据的寻址约定,升级权限和业务状态兼容仍由实现与治理机制负责。基础机制见可升级代理漏洞。

EIP-1967 标准槽位

标准槽位由命名空间字符串的 Keccak-256 哈希减一得到:

$$ \text{slot}=\text{bytes32}(\text{uint256}(\text{keccak256}(\text{namespace}))-1) $$

用途命名空间槽位
实现地址eip1967.proxy.implementation0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
管理员eip1967.proxy.admin0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
Beaconeip1967.proxy.beacon0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50

这些槽位与常规线性布局分离,安全分析依赖哈希抗碰撞及抗原像假设,不能表述为数学上不存在任何碰撞。任意存储写入能力仍可能破坏标准槽。

eth_getStorageAt 可读取槽位值,但识别实际路由还需核对代理字节码和版本。例如某些代理将管理员或 Beacon 同时保存在 immutable 字段中,不能只凭槽位推导所有调用行为。

ERC-7201 命名空间布局

ERC-7201 为结构体指定独立存储根,Solidity 等价表达式为:

keccak256(abi.encode(uint256(keccak256(bytes(id))) - 1))
    & ~bytes32(uint256(0xff))

外层哈希避免命名空间树与普通布局树的某些直接重叠。低 8 位清零使根按 256 槽对齐,规范讨论的是未来 Verkle 状态结构下可能获得的访问局部性,并不承诺当前 EVM 访问这些槽位统一预热或免哈希。

命名空间结构体仍按普通规则打包;mapping 和动态数组仍需哈希派生其数据位置。对齐不会给每个模块划定仅有 256 槽的固定容量,也不意味着第几个字段必占第几个槽。

隔离降低继承布局变化的耦合,但模块内部重排、改变字段类型或更换命名空间仍可能丢失对旧数据的正确解释。已有线性状态迁移到新命名空间,需要显式迁移方案。

旧版 UUPS 漏洞与调用上下文

OpenZeppelin 2021 年复盘披露的路径发生在缺少直接调用保护的旧实现中:攻击者初始化实现自身,取得其升级权限,再直接调用实现的升级函数,令实现委托执行包含 SELFDESTRUCT 的代码。

在当时的 EVM 规则下,这会删除实现地址的代码。依赖该代码提供业务与升级入口的代理可能失去执行能力。代理状态并未因此被改成攻击者的 owner,漏洞也不是通过直接初始化实现来改写所有代理管理员。

OpenZeppelin 4.3.2 加入 onlyProxy 修复直接调用路径。5.4.0 同时使用实现初始化锁定、活动代理上下文检查和业务升级鉴权。三种保护针对不同状态与入口,不能互相替代。

EIP-6780 后,SELFDESTRUCT 通常转移余额而不删除既有合约的代码和存储;同一交易内创建后销毁的合约有例外。历史攻击路径需要按当时库版本和目标链规则判断。

Nomad 默认根认证问题

Nomad 2022 年事件的核心是未证明消息的默认根值与已接受根发生重叠。以下代码为抽取该因果关系的简化模型,不是完整历史合约:

mapping(bytes32 => bytes32) public messages;
mapping(bytes32 => uint256) public confirmAt;

function acceptableRoot(bytes32 root) public view returns (bool) {
    uint256 time = confirmAt[root];
    return time != 0 && block.timestamp >= time;
}

若初始化写入 confirmAt[bytes32(0)] = 1,未经证明的消息在 messages 中返回默认值零;后续只检查 acceptableRoot(messages[messageHash]),就可能把没有证明的消息当作有效消息。

漏洞来自默认值与有效状态的解释冲突,不是 mapping 或所有零值初始化天然不安全。防护可以明确区分“未证明”和“已证明”,并验证初始化参数不会使缺省状态通过认证。

升级与迁移约束

  • 比较普通布局及命名空间内部的字段变化,不只比较标准实现槽。
  • 检查初始化、reinitializer 和迁移函数的调用者、次数及执行上下文。
  • 验证升级后业务功能与继续升级能力,兼容 UUID 不能证明这两者成立。
  • 验证默认值、已授权值和已处理值在各状态转换中保持可区分。
  • 标明库版本、部署链和治理权限,避免将历史漏洞路径套用到其他实现。

参考资料