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: 标准代理存储槽规范 (Standard Proxy Storage Slots)

EIP-1967 规定了以太坊可升级代理合约(Proxy Contract)用于存储逻辑实现地址、管理地址及信标地址的标准化存储槽(Storage Slots)。

在可升级代理模式中,代理合约通过 DELEGATECALL 操作码将所有执行委托给逻辑实现合约(Implementation)。如果代理合约自身的状态变量采用编译器默认的线性分配(从 Slot 0 开始),将与实现合约的业务变量发生灾难性的存储槽碰撞(Storage Collision)。EIP-1967 为代理元数据定义独立槽位,降低与常规布局重叠的风险,并提供统一的识别约定。业务布局兼容与升级授权仍需分别验证。


核心标准存储槽定义与数学推导

EIP-1967 使用哈希派生位置,使代理元数据与编译器的常规布局分离:

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

减一后的位置没有已知的哈希原像,进一步降低与派生存储位置重叠的风险。隔离依赖密码学假设,不是对任意代码写入或所有数学碰撞的排除。

槽位用途 (Role)命名空间字符串 (Namespace)确定性存储槽物理地址 (Slot Hex)对应事件 (Event)
逻辑实现地址 (Implementation)"eip1967.proxy.implementation"0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbcUpgraded(address indexed implementation)
管理员地址 (Admin)"eip1967.proxy.admin"0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103AdminChanged(address previousAdmin, address newAdmin)
信标地址 (Beacon)"eip1967.proxy.beacon"0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50BeaconUpgraded(address indexed beacon)

架构模型:三种主流代理形态

flowchart TD
  subgraph TransparentProxy["Transparent Proxy (透明代理)"]
    UserA[普通用户] -->|delegatecall| ImplA[Implementation 业务实现]
    AdminA[ProxyAdmin 合约] -->|管理升级 upgradeTo| ProxyA[TransparentUpgradeableProxy]
  end

  subgraph UUPSProxy["UUPS Proxy (EIP-1822)"]
    UserB[用户 / Admin] -->|统一入口| ProxyB[ERC1967Proxy]
    ProxyB -->|delegatecall 包含升级逻辑| ImplB[UUPSUpgradeable 实现合约]
  end

  subgraph BeaconProxy["Beacon Proxy (信标代理)"]
    P1[Proxy 1] & P2[Proxy 2] & P3[Proxy 3] -->|读取信标指向| Beacon[UpgradeableBeacon]
    Beacon -->|统一指向新版本| ImplC[单一实现合约]
  end

1. 逻辑实现槽 (Implementation Slot)

  • 物理机制:保存当前被委托逻辑合约的 20 字节地址(存储在高位补零的 32 字节字中)。
  • 更新约束:标准建议槽位变化时发出 Upgraded 事件。OpenZeppelin 实现另外校验目标代码等条件,具体检查不等同于标准对所有代理的要求。

2. 管理员槽 (Admin Slot - 针对透明代理)

  • 函数选择器冲突规避(Selector Clashing):若实现合约拥有 transfer(address,uint256),而代理自身拥有同签名或同 4 字节选择器的管理函数,调用将产生歧义。
  • 透明路由逻辑:
    • 若 msg.sender == Admin:调用仅被代理自身的管理函数响应(如升级、更改 Admin),禁止转发给逻辑实现。
    • 若 msg.sender != Admin:调用无条件通过 DELEGATECALL 转发给逻辑实现。
  • OpenZeppelin 5.4.0 的透明代理创建独立 ProxyAdmin,并使用 immutable 管理员参与路由;这是具体库实现,不是 EIP-1967 对所有代理的强制要求。

3. 信标槽 (Beacon Slot)

  • 多实例集中升级:当工厂合约需要部署成千上万个同构代理(如用户金库、借贷仓位)时,若逐一调用 upgradeTo 升级其实现槽,Gas 成本呈线性增长。
  • 信标代理机制:代理从 Beacon 获取实现地址,更新该 Beacon 可改变依赖它的多个代理。OpenZeppelin 5.4.0 的 BeaconProxy 使用 immutable Beacon 地址参与路由,并在标准槽中记录地址,不能统一解释为每次都先读取 Beacon 存储槽。

标准槽读写示例 (Solidity & Yul)

以下代码抽取槽位访问与委托初始化。外部升级入口仍需鉴权,不能将该内部工具直接视为完整升级实现。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

library ERC1967Utils {
    bytes32 internal constant IMPLEMENTATION_SLOT =
        0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

    bytes32 internal constant ADMIN_SLOT =
        0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;

    event Upgraded(address indexed implementation);
    event AdminChanged(address previousAdmin, address newAdmin);

    function getImplementation() internal view returns (address impl) {
        assembly {
            impl := sload(IMPLEMENTATION_SLOT)
        }
    }

    function upgradeToAndCall(address newImplementation, bytes memory data) internal {
        require(newImplementation.code.length > 0, "ERC1967: new implementation is not a contract");
        
        assembly {
            sstore(IMPLEMENTATION_SLOT, newImplementation)
        }
        emit Upgraded(newImplementation);

        if (data.length > 0) {
            (bool success, ) = newImplementation.delegatecall(data);
            require(success, "ERC1967: upgrade call failed");
        }
    }
}

参考文献与规范