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