1. 引言

前序博客有:

  • Ethereum EVM简介
  • 揭秘EVM Opcodes
  • 剖析Solidity合约创建EVM bytecode

Solidity底层通过SLOAD和SSTORE opcode来控制EVM storage。

2. 何为Storage?

Storage为每个合约的持久mapping,具有 2 256 − 1 2^{256}-1 22561个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_bset_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