状态变量在存储和瞬态存储中的布局

备注

本节中描述的规则适用于存储和瞬态存储数据位置。 两者布局是完全独立的,彼此之间的变量位置不相互干扰。 因此,存储和瞬态存储状态变量可以安全地交错而不会产生任何副作用。 仅支持值类型用于瞬态存储。

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

  • 存储槽中的第一项是低位对齐存储的。

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

  • 如果存储槽中的剩余空间不足以储存一个值类型,那么它会存储在下一个存储槽中。

  • 结构体和数组数据总是会开启一个新槽,并且它们的数据根据这些规则紧密打包。

  • 紧随结构体或数组数据的数据总是开始一个新的存储槽。

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

结构体和数组的元素是依次存储的,就像它们单独声明时一样。

警告

当使用小于 32 字节的元素时,合约的 gas 使用量可能会更高。 这是因为 EVM 一次处理 32 字节。 因此,如果元素小于 32 字节,EVM 必须执行额外的操作来将元素的大小从 32 字节减少到所需大小。

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

在处理函数参数或 内存memory 中的值时,没有额外的好处,因为编译器不会打包这些值。

最后,为了让 EVM 进行优化,请确保 存储storage 中的变量和 struct 成员的书写顺序允许它们被紧密地打包。 例如,按 uint128, uint128, uint256 的顺序声明存储变量,而不是 uint128, uint256, uint128, 因为前者只占用两个存储槽,而后者占用三个。

备注

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

映射和动态数组

由于不可预测大小,映射mapping 和动态数组类型不能存储在它们前后的状态变量之间。 相反,它们被视为仅占用 32 字节,关于 上述规则, 它们包含的元素从一个不同的存储槽开始存储,该槽是使用 Keccak-256 哈希计算得出的。

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

数组数据从 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 中获取该元素。

映射mapping 中的键 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))) 开始。 结构 S 中成员 c 的槽偏移量是 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 相同(见 above)。

备注

合约存储布局的 JSON 输出格式仍被视为实验性,并可能在 Solidity 的非破坏性版本中发生变化。

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

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.28;
contract A {
    struct S {
        uint128 a;
        uint128 b;
        uint[2] staticArray;
        uint[] dynArray;
    }

    uint x;
    uint transient y;
    uint w;
    uint transient z;

    S s;
    address addr;
    address transient taddr;
    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": 19,
      "contract": "fileA:A",
      "label": "w",
      "offset": 0,
      "slot": "1",
      "type": "t_uint256"
    },
    {
      "astId": 24,
      "contract": "fileA:A",
      "label": "s",
      "offset": 0,
      "slot": "2",
      "type": "t_struct(S)13_storage"
    },
    {
      "astId": 26,
      "contract": "fileA:A",
      "label": "addr",
      "offset": 0,
      "slot": "6",
      "type": "t_address"
    },
    {
      "astId": 34,
      "contract": "fileA:A",
      "label": "map",
      "offset": 0,
      "slot": "7",
      "type": "t_mapping(t_uint256,t_mapping(t_address,t_bool))"
    },
    {
      "astId": 37,
      "contract": "fileA:A",
      "label": "array",
      "offset": 0,
      "slot": "8",
      "type": "t_array(t_uint256)dyn_storage"
    },
    {
      "astId": 39,
      "contract": "fileA:A",
      "label": "s1",
      "offset": 0,
      "slot": "9",
      "type": "t_string_storage"
    },
    {
      "astId": 41,
      "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"
    }
  }
}

瞬态存储布局

{
  "storage": [
    {
      "astId": 17,
      "contract": "fileA:A",
      "label": "y",
      "offset": 0,
      "slot": "0",
      "type": "t_uint256"
    },
    {
      "astId": 21,
      "contract": "fileA:A",
      "label": "z",
      "offset": 0,
      "slot": "1",
      "type": "t_uint256"
    },
    {
      "astId": 28,
      "contract": "fileA:A",
      "label": "taddr",
      "offset": 0,
      "slot": "2",
      "type": "t_address"
    }
  ],
  "types": {
    "t_address": {
      "encoding": "inplace",
      "label": "address",
      "numberOfBytes": "20"
    },
    "t_uint256": {
      "encoding": "inplace",
      "label": "uint256",
      "numberOfBytes": "32"
    }
  }
}