状态变量存储布局 (Storage Layout)
EVM 层面,storage 是每个合约账户一棵 slot → 32 字节值 的稀疏树(见 EVM 与内部状态)。Solidity 编译器的工作是决定状态变量如何映射到这些 slot。这条映射规则不是语言细节——代理合约的存储冲突、Gas 优化、forge inspect 的布局检查都建立在这套规则之上。
参考来源:Solidity 官方文档 · Layout of State Variables in Storage。
基本规则与打包
- 变量从 slot 0 开始按声明顺序分配,逐个往后排。
- 长度不足 32 字节的类型(
uint128、bool、address…)会与相邻变量打包进同一个 slot,从 slot 的低位开始放置(这不表示整个 EVM 采用小端字节序)。 - 打包的前提是放得下:任何变量都不会跨 slot。放不下就换新 slot,剩余空间作废。
- 一个 slot 里多个变量时,gas 上省的是一次冷
SLOAD(2100),但每次写都要读写整个 slot——“读改写“成本需要权衡,这也是变量优化的出发点。
contract SlotLayout {
uint256 a; // slot 0:独占一个 slot
uint128 b; // slot 1:低 16 字节
uint128 c; // slot 1:高 16 字节(与 b 打包)
address d; // slot 2:低 20 字节
bool e; // slot 2:字节 20(与 d 打包)
uint256 f; // slot 3:独占(不与 slot 2 的剩余空间拼合)
}
mapping 与动态数组的定位
两者都为声明本身保留一个完整槽 p,实际元素通过哈希派生位置存放:
- mapping:键
k的值位于keccak256(h(k) . p),p是 mapping 声明所在的 slot 序号,.拼接。h按键类型做定宽填充(uint等补齐 32 字节;string/bytes使用未经填充的原始字节,不先单独哈希)。 - 动态数组:
p存数组长度;对于uint256[],第i个元素位于keccak256(p) + i;较小的元素会在槽内打包,多槽结构体则按自身布局计算步长。 bytes/string特例:长度 ≤ 31 字节时数据与(长度 × 2)一起内联在 slotp;更长时 slotp存长度 × 2 + 1,数据从keccak256(p)开始连续存放。
contract Locator {
mapping(address => uint256) balances; // 声明于 slot 0
uint256[] items; // 声明于 slot 1
function balanceSlot(address who) public pure returns (bytes32) {
return keccak256(abi.encode(who, uint256(0)));
}
function itemSlot(uint256 i) public view returns (bytes32) {
return bytes32(uint256(keccak256(abi.encode(uint256(1)))) + i);
}
}
这套定位规则正是 forge inspect <contract> storage-layout、Foundry 的 stdStorage 以及各种链上存储读取工具的工作原理。
继承下的布局顺序
- 状态变量的 slot 分配遵循 C3 线性化:从最基础的合约开始,按继承链顺序依次分配,同一合约内按声明顺序。
- 基类与派生类的变量可能共享一个槽。升级风险来自改变已有字段类型、顺序或继承布局,而不是来自把升级函数放在基类。
immutable 与 constant
constant:编译期内联进字节码,不占任何 slot。immutable:也不占 slot,构造结束时把值复制进运行时字节码(PUSH 指令)。代理调用也会读取实现合约字节码中的同一值,不能把它当作每个代理独有的可变状态。
与代理合约的关系
- 代理模式下,代理合约与逻辑合约共享同一套 storage,两边各自的 slot 0 起步布局可能冲突。
- EIP-1967 用“哈希值减一“的 slot(如
keccak256("eip1967.proxy.implementation") - 1)把 implementation 地址挪出常规空间,避开冲突。 - 更彻底的方案是存储分离(如 EIP-2535 Diamond 或自定命名空间存储)。
- 实例分析:USDC 代理合约。
布局检查工具
forge inspect <Contract> storage-layout:输出每个变量的 slot、偏移、类型。solc --storage-layout:编译器原生 JSON 输出,适合 CI 中对比升级前后布局是否兼容。