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 注解供布局工具识别命名空间,不会自动改变代码的实际存储位置;汇编定位与声明需要保持一致。
升级兼容与迁移
独立根降低了不同模块之间因继承布局调整产生的耦合。相同命名空间内部仍不能随意重排、改变既有字段类型或改变其业务含义。
更换命名空间标识符会改变寻址根,不会自动读取旧根中的数据。从已有线性布局切换到命名空间,也需要明确迁移方案、初始化权限和迁移后的不变量验证。
不同模块应避免复用同一命名空间。任意存储写入、错误根常量或不兼容字段修改仍可破坏状态,命名空间不是对这些行为的自动安全检查。