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

ERC-7201:命名空间存储布局

ERC-7201 为模块结构体指定独立存储根,使业务存储不完全依赖合约继承的线性布局。命名空间布局可以与普通 Solidity 布局并存;它不要求使用钻石代理,也不自动保证升级兼容。

根槽位派生

命名空间标识符 id 的根槽位使用以下 Solidity 表达式计算:

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

id 是模块的命名空间字符串。第一次哈希减一得到与常规布局分离的位置;再次哈希避免较大结构扩展到普通布局的某些派生槽。隔离性质依赖哈希安全和合理的布局范围假设。

低 8 位清零使根按 256 槽对齐。规范将其描述为未来 Verkle 状态结构下可能获得的访问优化,当前 EVM 不会因此统一预热这 256 个槽。

模块内部布局

结构体成员仍遵循 Solidity 的打包规则。多个小字段可能共用一槽,uint256 通常独占一槽,mapping 和动态数组通过哈希派生数据位置。字段顺序与槽偏移并不是一一对应。

对齐不表示模块只能使用 256 槽,也不是顺序字段可直接偏移寻址的原因。固定成员偏移来自结构体布局,mapping 与动态数组仍有其独立寻址规则。

结构体定位示例

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

library TokenStorage {
    bytes32 internal constant ROOT = 0x792b309ed1024743406dc085f9859a5c6895fd893b6d7dafa1a05c10ff9c8f00;

    /// @custom:storage-location erc7201:example.storage.ERC20
    struct Layout {
        mapping(address => uint256) balances;
        uint256 totalSupply;
    }

    function layout() internal pure returns (Layout storage state) {
        bytes32 slot = ROOT;
        assembly {
            state.slot := slot
        }
    }
}

contract NamespacedState {
    function balanceOf(address account) external view returns (uint256) {
        return TokenStorage.layout().balances[account];
    }

    function totalSupply() external view returns (uint256) {
        return TokenStorage.layout().totalSupply;
    }
}

该根由 example.storage.ERC20 按标准公式计算。NatSpec 注解供布局工具识别命名空间,不会自动改变代码的实际存储位置;汇编定位与声明需要保持一致。

升级兼容与迁移

独立根降低了不同模块之间因继承布局调整产生的耦合。相同命名空间内部仍不能随意重排、改变既有字段类型或改变其业务含义。

更换命名空间标识符会改变寻址根,不会自动读取旧根中的数据。从已有线性布局切换到命名空间,也需要明确迁移方案、初始化权限和迁移后的不变量验证。

不同模块应避免复用同一命名空间。任意存储写入、错误根常量或不兼容字段修改仍可破坏状态,命名空间不是对这些行为的自动安全检查。

参考资料