深入理解智能合约的存储槽机制
前言
什么是存储槽?
- 每个键是一个32字节(256位)的数字,称为"存储槽"(storage slot)
- 每个值同样是一个32字节的数据
存储槽分配规则
基本类型的存储
- 如果变量小于32字节且能与前一个变量共享存储槽,则会被打包在一起
- 如果无法与前一个变量共享(超出32字节限制),则分配新的存储槽
复杂类型的存储
- 定长数组:元素连续存储,起始位置是该变量被分配的槽
- 动态数组:长度存储在变量槽p,元素存储起始位置为keccak256(p)
uint256[] dynamicArray; // 长度存在槽p,元素从keccak256(p)开始
- 映射:映射本身不占用存储空间(只预留一个槽位),元素位置通过公式计算:
- 结构体:成员变量按声明顺序连续分配槽位
继承与存储槽
- 首先分配OwnableUpgradeable的变量
- 然后是AccessControlEnumerableUpgradeable的变量
- 依此类推,最后是DomainNFTV1自身的变量
存储槽优化技术
1. 变量打包
2. 常量与不可变变量
- 常量直接嵌入到字节码中
- 不可变变量在构造函数中赋值,然后嵌入字节码
3. 库的使用
4. gap保留机制在可扩展合约中,通常会保留一些存储槽用于未来的变量:
这确保后续添加变量时,不会影响现有存储布局。## 存储槽的查看与计算通过web3.eth.getStorageAt或ethers.provider.getStorageAt可以直接查看存储槽内容:
对于复杂存储位置的计算:
实际案例分析:DomainNFTV1中的存储布局以DomainNFTV1合约为例:
存储布局简析:
1. _currentTokenId: 占用一个存储槽,通过Counters库管理
2. _baseURI_: 字符串长度存储在一个槽中,内容存储在keccak256(槽位)起始的连续槽中
3. curatorRoyaltyFee: 由于只有96位,理论上可以与其他小变量共享槽位
4. isFrozenTokenId: 映射本身占一个槽位,元素分散存储
存储槽访问的Gas消耗存储操作
存储槽访问的Gas消耗存储操作是以太坊中最昂贵的操作之一:
- SLOAD(读取存储): 2100 gas (cold), 100 gas (warm)
- SSTORE(写入存储): 20000+ gas(首次写入),5000+ gas(修改)
这就是为什么优化存储布局如此重要 - 通过变量打包,一次SLOAD/SSTORE操作可以处理多个变量。

浙公网安备 33010602011771号