存储中状态变量的布局

合约的状态变量以紧凑的方式存储在存储中,因此多个值有时会使用相同的存储槽位。除了动态大小的数组和映射(见下文),数据从第一个状态变量开始连续存储,第一个状态变量存储在槽位 0 中。对于每个变量,根据其类型确定大小(以字节为单位)。如果可能,多个连续的项目需要少于 32 个字节,则根据以下规则打包到单个存储槽位中。

  • 存储槽位中的第一个项目以低位对齐存储。

  • 值类型仅使用存储它们所需的字节数。

  • 如果值类型不适合存储槽位剩余的部分,它将存储在下一个存储槽位中。

  • 结构体和数组数据始终开始一个新槽位,其项目根据这些规则紧密打包。

  • 在结构体或数组数据后面的项目始终开始一个新存储槽位。

对于使用继承的合约,状态变量的顺序由从最基底合约开始的合约的 C3 线性化顺序决定。如果上述规则允许,来自不同合约的状态变量会共享相同的存储槽位。

结构体和数组的元素依次存储,就像它们作为单个值给出一样。

警告

当使用小于 32 字节的元素时,合约的 gas 使用量可能会更高。这是因为 EVM 每次操作 32 个字节。因此,如果元素小于 32 个字节,EVM 必须使用更多操作才能将元素的大小从 32 个字节减少到所需的大小。

如果你正在处理存储值,使用缩减大小的类型可能是有益的,因为编译器会将多个元素打包到一个存储槽位中,从而将多个读取或写入操作合并到一个操作中。但是,如果你没有同时读取或写入槽位中的所有值,这可能会产生相反的效果:当一个值写入多值存储槽位时,必须先读取存储槽位,然后将其与新值组合起来,以便同一个槽位中的其他数据不会被破坏。

处理函数参数或内存值时,没有固有的优势,因为编译器不会打包这些值。

最后,为了让 EVM 能够对此进行优化,请确保你尝试对存储变量和 struct 成员进行排序,以便它们能够紧密打包。例如,按 uint128, uint128, uint256 的顺序声明你的存储变量,而不是 uint128, uint256, uint128,因为前者只占用两个存储槽位,而后者则占用三个。

注意

存储中状态变量的布局被认为是 Solidity 的外部接口的一部分,因为存储指针可以传递给库。这意味着对本节中概述的规则的任何更改都被认为是语言的重大更改,并且由于其关键性质,在执行之前应仔细考虑。如果出现这种重大更改,我们希望发布一个兼容模式,其中编译器将生成支持旧布局的字节码。

映射和动态数组

由于其不可预测的大小,映射和动态大小的数组类型不能存储在它们之前和之后的州变量“之间”。相反,它们被认为只占用 32 个字节,关于 上述规则,它们包含的元素存储在一个不同的存储槽位中,该槽位是使用 Keccak-256 哈希计算的。

假设在应用 存储布局规则 后,映射或数组的存储位置最终成为一个槽位 p。对于动态数组,此槽位存储数组中的元素数量(字节数组和字符串除外,见 下文)。对于映射,槽位保持为空,但仍然需要它来确保即使两个映射彼此相邻,它们的内容最终也会位于不同的存储位置。

数组数据从 keccak256(p) 开始,以与静态大小的数组数据相同的方式布局:一个元素接着一个元素,如果元素不长于 16 个字节,则可能共享存储槽位。动态数组的动态数组递归地应用此规则。元素 x[i][j] 的位置,其中 x 的类型是 uint24[][],计算如下(同样假设 x 本身存储在槽位 p 中):槽位是 keccak256(keccak256(p) + i) + floor(j / floor(256 / 24)),并且可以使用 (v >> ((j % floor(256 / 24)) * 24)) & type(uint24).max 从槽位数据 v 中获取元素。

与映射键 k 对应的值位于 keccak256(h(k) . p),其中 . 是连接,h 是一个函数,根据其类型应用于键

  • 对于值类型,h 以与在内存中存储值时相同的方式将值填充到 32 个字节。

  • 对于字符串和字节数组,h(k) 只是未填充的数据。

如果映射值是非值类型,则计算出的槽位标记数据的开始位置。例如,如果该值是结构体类型,则必须添加一个与结构体成员相对应的偏移量才能到达该成员。

例如,考虑以下合约

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;


contract C {
    struct S { uint16 a; uint16 b; uint256 c; }
    uint x;
    mapping(uint => mapping(uint => S)) data;
}

让我们计算 data[4][9].c 的存储位置。映射本身的位置是 1(变量 x 有 32 个字节在它之前)。这意味着 data[4] 存储在 keccak256(uint256(4) . uint256(1)) 中。 data[4] 的类型再次是一个映射,data[4][9] 的数据从槽位 keccak256(uint256(9) . keccak256(uint256(4) . uint256(1))) 开始。成员 c 在结构体 S 中的槽位偏移量是 1,因为 ab 打包在一个槽位中。这意味着 data[4][9].c 的槽位是 keccak256(uint256(9) . keccak256(uint256(4) . uint256(1))) + 1。值的类型是 uint256,因此它使用一个槽位。

bytesstring

bytesstring 的编码方式相同。一般来说,编码类似于 bytes1[],因为有一个用于数组本身的槽位,以及一个使用该槽位位置的 keccak256 哈希计算的数据区域。但是,对于短值(短于 32 个字节),数组元素与长度一起存储在同一个槽位中。

特别地:如果数据长度最多为 31 字节,则元素存储在高位字节(左对齐),最低位字节存储值为 length * 2。对于存储数据长度为 32 字节或更长的字节数组,主槽位 p 存储 length * 2 + 1,数据按通常方式存储在 keccak256(p) 中。这意味着可以通过检查最低位是否设置来区分短数组和长数组:短数组(未设置)和长数组(已设置)。

注意

目前不支持处理无效编码的槽位,但可能会在将来添加。如果通过 IR 编译,读取无效编码的槽位会导致 Panic(0x22) 错误。

JSON 输出

可以通过 标准 JSON 接口 请求合约的存储布局。输出是一个 JSON 对象,包含两个键,storagetypesstorage 对象是一个数组,每个元素具有以下形式

{
    "astId": 2,
    "contract": "fileA:A",
    "label": "x",
    "offset": 0,
    "slot": "0",
    "type": "t_uint256"
}

上面的示例是来自源代码单元 fileAcontract A { uint x; } 的存储布局,并且

  • astId 是状态变量声明的 AST 节点的 ID

  • contract 是包含路径前缀的合约名称

  • label 是状态变量的名称

  • offset 是根据编码在存储槽位内的字节偏移量

  • slot 是状态变量所在的或开始的存储槽位。此数字可能非常大,因此它的 JSON 值表示为字符串。

  • type 是一个标识符,用作变量类型信息的键(在下面描述)

给定的 type,在本例中为 t_uint256,表示 types 中的一个元素,其形式为

{
    "encoding": "inplace",
    "label": "uint256",
    "numberOfBytes": "32",
}

其中

  • encoding 如何在存储中对数据进行编码,其中可能的值为

    • inplace:数据在存储中连续排列(参见 以上)。

    • mapping:基于 Keccak-256 哈希的方法(参见 以上)。

    • dynamic_array:基于 Keccak-256 哈希的方法(参见 以上)。

    • bytes:根据数据大小,单槽位或基于 Keccak-256 哈希的方法(参见 以上)。

  • label 是规范的类型名称。

  • numberOfBytes 是使用的字节数(作为十进制字符串)。请注意,如果 numberOfBytes > 32,这意味着使用了多个槽位。

除了以上四个之外,某些类型还具有其他信息。映射包含其 keyvalue 类型(再次引用此类型映射中的条目),数组具有其 base 类型,结构列出了其 members,其格式与顶层 storage 相同(参见 以上)。

注意

合约存储布局的 JSON 输出格式仍然被认为是实验性的,并且可能会在 Solidity 的非破坏性版本中发生更改。

以下示例显示了一个合约及其存储布局,包含值类型和引用类型、打包编码的类型以及嵌套类型。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract A {
    struct S {
        uint128 a;
        uint128 b;
        uint[2] staticArray;
        uint[] dynArray;
    }

    uint x;
    uint y;
    S s;
    address addr;
    mapping(uint => mapping(address => bool)) map;
    uint[] array;
    string s1;
    bytes b1;
}
{
  "storage": [
    {
      "astId": 15,
      "contract": "fileA:A",
      "label": "x",
      "offset": 0,
      "slot": "0",
      "type": "t_uint256"
    },
    {
      "astId": 17,
      "contract": "fileA:A",
      "label": "y",
      "offset": 0,
      "slot": "1",
      "type": "t_uint256"
    },
    {
      "astId": 20,
      "contract": "fileA:A",
      "label": "s",
      "offset": 0,
      "slot": "2",
      "type": "t_struct(S)13_storage"
    },
    {
      "astId": 22,
      "contract": "fileA:A",
      "label": "addr",
      "offset": 0,
      "slot": "6",
      "type": "t_address"
    },
    {
      "astId": 28,
      "contract": "fileA:A",
      "label": "map",
      "offset": 0,
      "slot": "7",
      "type": "t_mapping(t_uint256,t_mapping(t_address,t_bool))"
    },
    {
      "astId": 31,
      "contract": "fileA:A",
      "label": "array",
      "offset": 0,
      "slot": "8",
      "type": "t_array(t_uint256)dyn_storage"
    },
    {
      "astId": 33,
      "contract": "fileA:A",
      "label": "s1",
      "offset": 0,
      "slot": "9",
      "type": "t_string_storage"
    },
    {
      "astId": 35,
      "contract": "fileA:A",
      "label": "b1",
      "offset": 0,
      "slot": "10",
      "type": "t_bytes_storage"
    }
  ],
  "types": {
    "t_address": {
      "encoding": "inplace",
      "label": "address",
      "numberOfBytes": "20"
    },
    "t_array(t_uint256)2_storage": {
      "base": "t_uint256",
      "encoding": "inplace",
      "label": "uint256[2]",
      "numberOfBytes": "64"
    },
    "t_array(t_uint256)dyn_storage": {
      "base": "t_uint256",
      "encoding": "dynamic_array",
      "label": "uint256[]",
      "numberOfBytes": "32"
    },
    "t_bool": {
      "encoding": "inplace",
      "label": "bool",
      "numberOfBytes": "1"
    },
    "t_bytes_storage": {
      "encoding": "bytes",
      "label": "bytes",
      "numberOfBytes": "32"
    },
    "t_mapping(t_address,t_bool)": {
      "encoding": "mapping",
      "key": "t_address",
      "label": "mapping(address => bool)",
      "numberOfBytes": "32",
      "value": "t_bool"
    },
    "t_mapping(t_uint256,t_mapping(t_address,t_bool))": {
      "encoding": "mapping",
      "key": "t_uint256",
      "label": "mapping(uint256 => mapping(address => bool))",
      "numberOfBytes": "32",
      "value": "t_mapping(t_address,t_bool)"
    },
    "t_string_storage": {
      "encoding": "bytes",
      "label": "string",
      "numberOfBytes": "32"
    },
    "t_struct(S)13_storage": {
      "encoding": "inplace",
      "label": "struct A.S",
      "members": [
        {
          "astId": 3,
          "contract": "fileA:A",
          "label": "a",
          "offset": 0,
          "slot": "0",
          "type": "t_uint128"
        },
        {
          "astId": 5,
          "contract": "fileA:A",
          "label": "b",
          "offset": 16,
          "slot": "0",
          "type": "t_uint128"
        },
        {
          "astId": 9,
          "contract": "fileA:A",
          "label": "staticArray",
          "offset": 0,
          "slot": "1",
          "type": "t_array(t_uint256)2_storage"
        },
        {
          "astId": 12,
          "contract": "fileA:A",
          "label": "dynArray",
          "offset": 0,
          "slot": "3",
          "type": "t_array(t_uint256)dyn_storage"
        }
      ],
      "numberOfBytes": "128"
    },
    "t_uint128": {
      "encoding": "inplace",
      "label": "uint128",
      "numberOfBytes": "16"
    },
    "t_uint256": {
      "encoding": "inplace",
      "label": "uint256",
      "numberOfBytes": "32"
    }
  }
}