理解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。

posted on 2025-09-05 17:08  肥兔子爱豆畜子  阅读(17)  评论(0)    收藏  举报

导航