理解Solidity存储布局
最近在看OnchainID这套身份合约,里边可升级特性部分用到了proxy模式,contract IdentityProxy里边有如下的构造方法:
//_implementationAuthority 存储真正Identity实现合约地址的容器
//initialManagementKey该身份合约的管理key
constructor(address _implementationAuthority, address initialManagementKey) {
require(_implementationAuthority != address(0), "invalid argument - zero address");
require(initialManagementKey != address(0), "invalid argument - zero address");
// 把_implementationAuthority地址存放在0x821f3e4d3d679f19eacc940c87acf846ea6eae24a63058ea750304437a62aafc
assembly {
sstore(0x821f3e4d3d679f19eacc940c87acf846ea6eae24a63058ea750304437a62aafc, _implementationAuthority)
}
//从容器中得到实现合约的地址
address logic = IImplementationAuthority(_implementationAuthority).getImplementation();
//执行delegatecall,业务逻辑用logic也就是实现合约Identity的,数据存储在当前合约IdentityProxy里
(bool success,) = logic.delegatecall(abi.encodeWithSignature("initialize(address)", initialManagementKey));
require(success, "Initialization failed.");
}
里边的内联汇编
assembly {
sstore(0x821f3e4d3d679f19eacc940c87acf846ea6eae24a63058ea750304437a62aafc, _implementationAuthority)
}
0x821f3e4d3d679f19eacc940c87acf846ea6eae24a63058ea750304437a62aafc直接指定这个而且是写死的,一开始有些疑惑,这里有必要复习一下Solidity的存储布局,结合EIP-1967的规定,就明白了。
storage
- 永久存储(链上持久化)。
- 每个合约地址下有自己的存储空间。
- Slot = 32 字节为单位,逻辑上是一个 mapping(uint256 => bytes32)。
- 所有状态变量、mapping、struct 最终都会映射到某个 slot 里。
memory
- 临时存储(函数执行时存在,执行完销毁)。
- 线性字节数组,从 0 地址往上扩展。
- 用于函数内部的动态数组、string、临时计算缓存等。
calldata
- 只读、不可修改。
- 存放函数参数(外部调用时 ABI 编码的数据)。
- 访问成本比 memory 更低。
storage存储空间
在EVM里,每个合约都会有自己的存储空间,其storageLayout逻辑上是个无限大的mapping,比如:
合约A -> Storage_A:mapping(uint256 => bytes32)
合约B -> Storage_B:mapping(uint256 => bytes32)
其中每一个映射key是uint256,一般也成为一个slot,合约里的每个storage类型的变量按照被定义的顺序依次存在自己这个mapping的slot里,例如:
contract C {
uint256 a; // slot 0
bool b; // slot 1 (可能和别的小变量打包)
mapping(uint => uint) m; // slot 2 (mapping不直接存值,只存“起点slot”)
}
为什么用0x821f3e4d3d679f19eacc940c87acf846ea6eae24a63058ea750304437a62aafc这个固定的slot
回到之前的合约代码,sstore(0x821f3e4d3d679f19eacc940c87acf846ea6eae24a63058ea750304437a62aafc, _implementationAuthority)意思是把 _implementationAuthority这个地址存放在0x821f3e4d...这个slot中;
而sload(0x821f3e4d...)意思是从第0x821f3e4d3d679f19eacc940c87acf846ea6eae24a63058ea750304437a62aafcslot取32字节的值。那么这个大的key值是怎么来的呢?
这是 EIP-1967 的约定:
为了防止普通状态变量“占用” proxy 的关键 slot(比如 implementation 地址、admin 地址),社区约定用这样一个极难撞上的大 hash 值当 slot。
比如计算规则:
implementation slot = keccak256("eip1967.proxy.implementation") - 1
admin slot = keccak256("eip1967.proxy.admin") - 1
开发者在合约里自己定义的storage变量正常情况下是从0、1、2、3...这样顺序往上走的,只要不是故意很难碰撞到这个key。
浙公网安备 33010602011771号