1. 引言
前序博客有:
- Ethereum EVM简介
- 揭秘EVM Opcodes
- 剖析Solidity合约创建EVM bytecode
Solidity底层通过SLOAD和SSTORE opcode来控制EVM storage。
2. 何为Storage?
Storage为每个合约的持久mapping,具有 2 256 − 1 2^{256}-1 2256−1个32 byte words。当在合约中设置某状态变量值时,其会存储在指定的slot中,其将持续在EVM中,除非被相同类型的其它值覆盖。
3. 何时用Storage?何时用Memory?
当首次加载某storage slot时,其是cold的,意味着需要2100 gas,后续再调用该slot时,其是warm的,仅需100 gas。而Memory更便宜,其低至3 gas(当有memory expansion时,将更贵点)。
举例如下,未优化合约:
contract C { struct S { uint256 a; uint256 b; address c; } S public s; function foo(uint256 input) external { // `s.b` is loaded from storage once: warming up the storage! if (input 50) revert; }
其中s,b
从storage中加载了2次。可优化为:创建内存变量来存储s.b
值,后续使用该内存变量。原因在于MLOAD比SLOAD便宜。即优化为:
function foo(uint256 input) external { // Initial storage load to store in memory. uint256 b = s.b; // Using MLOAD in comparison operations! if (input 50) revert;}
4. 手工分配Storage
// SPDX-License-Identifier: MITpragma solidity 0.8.6;contract C { struct S { uint16 a; // 2 bytes, 2 bytes total uint24 b; // 3 bytes, 5 bytes total address c; // 20 bytes, 25 bytes total + end of slot 0x01 address d; // 20 bytes, slot 0x02 } // I've noted the storage slots each state is located at. // A single slot is 32 bytes :) uint256 boring; // 0x00 S s_struct; // 0x01, 0x02 S[] s_array; // 0x03 mapping(uint256 => S) s_map; // 0x04 constructor() { boring = 0x288; s_struct = S({ a: 10, b: 20, c: 0xdcD49C36E69bF85FA9c5a25dEA9455602C0B289e, d: 0x4675C7e5BaAFBFFbca748158bEcBA61ef3b0a263 }); s_array.push(s_struct); s_array.push(s_struct); } function view_boring() external view returns (bytes32) { bytes32 x; assembly { x := sload(0x00) } return x; } function view_slot(uint256 slot) external view returns(bytes32) { bytes32 x; assembly { x := sload(slot) } return x; } function view_b() external view returns (uint256) { bytes32 x; assembly { // before: 00000000000000 dcd49c36e69bf85fa9c5a25dea9455602c0b289e 000014 000a // ^ // after: 0000 00000000000000 dcd49c36e69bf85fa9c5a25dea9455602c0b289e 000014 // ^ let v := shr(0x10, sload(0x01)) // If both characters aren't 0, keep the bit (1). Otherwise, set to 0. // mask: 0000000000000000000000000000000000000000000000000000000000 FFFFFF // v: 000000000000000000dcd49c36e69bf85fa9c5a25dea9455602c0b289e 000014 // result: 0000000000000000000000000000000000000000000000000000000000 000014 v := and(0xffffff, v) // Store in memory bc return uses memory. mstore(0x40, v) // Return reads left to right. // Since our value is far right we can just return 32 bytes from the 64th byte in memory. x := mload(0x40) } return uint256(x); } // unused bytes c b a // before: 00000000000000 dcd49c36e69bf85fa9c5a25dea9455602c0b289e 000014 000a // unused bytes c b a // after: 00000000000000 dcd49c36e69bf85fa9c5a25dea9455602c0b289e 0001F4 000a function set_b(uint24 b) external { assembly { // Removing the `uint16` from the right. // before: 00000000000000 dcd49c36e69bf85fa9c5a25dea9455602c0b289e 000014 000a // ^ // after: 0000 00000000000000 dcd49c36e69bf85fa9c5a25dea9455602c0b289e 000014 // ^ let new_v := shr(0x10, sload(0x01)) // Create our mask. new_v := and(0xffffff, new_v) // Input our value into the mask. new_v := xor(b, new_v) // Add back the removed `a` value bits. new_v := shl(0x10, new_v) // Replace original 32 bytes' `000014` with `0001F4`. new_v := xor(new_v, sload(0x01)) // Store our new value. sstore(0x01, new_v) } } function view_d() external view returns (address) { return s_array[0].d; } // keccak256(array_slot) + var_slot // keccak256(0x03) + 1 // Remember how `s_struct` takes up 2 slots? // The `+ 1` indicates the second slot allocation in S // For the bitpacked slot in S we use don't need the add // The next element's slot would be `+ 2` function get_element() external view returns(bytes32) { bytes32 x; assembly { // Store array slot in memory. mstore(0x0, 0x03) // Keccak does the MLOAD internally so we give the memory location. let hash := add(keccak256(0x0, 0x20), 1) // Store the return value. mstore(0x0, sload(hash)) // Return `d`. x := mload(0x0) } return x; }// 返回s_array数组的长度 function s_arrayLength () public view returns (uint r) { assembly { r := sload (3) } }// 从storage中取s_array数组的第i个32字节内容。 function s_arrayElement (uint i) public view returns (bytes32 r) { assembly { mstore (0, 3) r := sload (add (keccak256 (0, 32), i)) } }}
4.1 基础类型访问
当想要访问uint256 boring
时,访问方式可为:
assembly { let x := sload(0x00)}
4.2 访问Bitpacked结构体
所谓bitpacked,是指在单个slot(32 bytes)内存储了多个变量。在本例中,slot 0x01
中pack了共25字节内容:
uint16 a
(2 bytes).uint24 b
(3 bytes).address c
(20 bytes).
对应s_struct
的slots为:
// 0x01 0x00000000000000dcd49c36e69bf85fa9c5a25dea9455602c0b289e000014000a// 0x02 0x0000000000000000000000004675c7e5baafbffbca748158becba61ef3b0a263
Bitpacked结构体的查询和设置,可参看上面合约中的view_b
和set_b
。
4.3 访问数组结构
pragma solidity >=0.7.0 uint) internal y; // Storage slot #1 uint [] internal z; // Storage slot #2 constructor() { z.push(8); z.push(9); }// 动态数组的长度 function zLength () public view returns (uint r) { assembly { r := sload (2) } }// 动态数组中第i个元素的值 function zElement (uint i) public view returns (uint r) { assembly { mstore (0, 2) r := sload (add (keccak256 (0, 32), i)) } }}
4.4 访问mapping结构
// SPDX-License-Identifier: GPL-3.0pragma solidity >=0.7.0 address) internal y; // Storage slot #1 uint [] internal z; // Storage slot #2 constructor() { y[10] = 0xdcD49C36E69bF85FA9c5a25dEA9455602C0B289e; y[3] = 0x4675C7e5BaAFBFFbca748158bEcBA61ef3b0a263; } // 本例中,参数取(10, 1)或(3, 1)可分别获得y[10]以及y[3]的值 function getStorageValue(uint num, uint slot) public view returns (address result) { assembly { // Store num in memory scratch space (note: lookup "free memory pointer" if you need to allocate space) mstore(0, num) // Store slot number in scratch space after num mstore(32, slot) // Create hash from previously stored num and slot let hash := keccak256(0, 64) // Load mapping value using the just calculated hash result := sload(hash) } }}
4.5 访问String和Bytes结构
bytes和string中的元素在storage中均以ASCII码值表示。
// SPDX-License-Identifier: GPL-3.0pragma solidity >=0.7.0 address) internal y; // Storage slot #1 uint [] internal z; // Storage slot #2 bytes internal b; // Storage slot #3 string internal s; // Storage slot #4 constructor() { b = "0123456789012345678901234567890123456789"; s = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; } // bytes和string中的元素在storage中均以ASCII码值表示。 // bytes长度,本例中,返回值为81 function bLength () public view returns (uint r) { assembly { r := sload (3) } }// 返回bytes中第i个32字节值 //i=0,对应返回:0x3031323334353637383930313233343536373839303132333435363738393031 //i=1,对应返回:0x3233343536373839000000000000000000000000000000000000000000000000 function bElement (uint i) public view returns (bytes32 r) { assembly { mstore (0, 3) r := sload (add (keccak256 (0, 32), i)) } } // string长度,本例中,返回值为105 function sLength () public view returns (uint r) { assembly { r := sload (4) } } // 返回string中的第i个32字节值 //i=0,对应返回:0x6162636465666768696a6b6c6d6e6f707172737475767778797a414243444546 //i=1,对应返回:0x4748494a4b4c4d4e4f505152535455565758595a000000000000000000000000 function sElement (uint i) public view returns (bytes32 r) { assembly { mstore (0, 4) r := sload (add (keccak256 (0, 32), i)) } }}
参考资料
[1] A Low-Level Guide To Solidity’s Storage Management
[2] Layout of State Variables in Storage
[3] How to get access to the storage array through the solidity assembler?
[4] How to get access to the storage mapping through the solidity assembler?
[5] Storage and memory layout of strings