ストレージとトランジェントストレージ内の状態変数のレイアウト
注釈
このセクションで説明されているルールは、ストレージとトランジェントストレージの両方のデータロケーションに適用されます。 それぞれのレイアウトは完全に独立しており、互いの変数のロケーションに干渉することはありません。 そのため、ストレージとトランジェントストレージの状態変数は、互いを気にすることなく安全に配置できます。 トランジェントストレージでは、値型のみがサポートされています。
コントラクトの状態変数はストレージにコンパクトに格納され、複数の値が同じストレージスロットを使用することがあります。
動的なサイズの配列やマッピング(後述)を除き、データはスロット 0 に格納された最初の状態変数から順に連続して格納されます。
各変数には、その型に応じてバイト単位のサイズが決定されます。
32バイトに満たない複数の連続したアイテムは、以下のルールに従って、可能な限り1つのストレージスロットにまとめられます。
ストレージスロットの最初のアイテムは、下位にアラインされ格納されます。
値型はそれを格納するのに必要な数のバイトしか使用しません。
値型がストレージスロットの残りの部分に収まらない場合は、次のストレージスロットに格納されます。
構造体や配列データは、常に新しいスロットで開始し、そのアイテムはこれらのルールに従って密にパッキングされます。
構造体や配列データに続くアイテムは、常に新しいストレージスロットで開始します。
継承を使用しているコントラクトでは、状態変数の順序は、最も下位のコントラクトから始まるコントラクトのC3線形化された順序によって決定されます。 上記のルールで許可されていれば、異なるコントラクトの状態変数が同じストレージスロットを共有できます。
構造体や配列の要素は、あたかも個々の値が与えられたかのように、それぞれの要素の後に格納されます。
コントラクトが カスタムストレージレイアウト を指定している場合、静的ストレージ変数に割り当てられるスロットは、レイアウトベースとして定義された値に従ってシフトされます。 動的配列やマッピングの位置も、それらが基づいている静的スロットのシフトにより間接的に影響を受けます。 カスタムレイアウトは最も派生したコントラクトで指定され、上述の順序に従って、最も基底に近いコントラクトの変数から順に、すべてのストレージスロットが調整されます。
次の例では、コントラクト C はコントラクト A と B を継承し、さらにカスタムのストレージベーススロットを指定しています。
その結果、継承ツリー内のすべてのストレージ変数スロットが C によって指定された値に従って調整されます。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.29;
struct S {
int32 x;
bool y;
}
contract A {
uint a;
uint128 transient b;
uint constant c = 10;
uint immutable d = 12;
}
contract B {
uint8[] e;
mapping(uint => S) f;
uint16 g;
uint16 h;
bytes16 transient i;
S s;
int8 k;
}
contract C is A, B layout at 42 {
bytes21 l;
uint8[10] m;
bytes5[8] n;
bytes5 o;
}
この例では、ストレージレイアウトは継承された状態変数 a から始まり、ベーススロット(スロット 42)に直接格納されます。
トランジェント変数、定数、immutable 変数は別の場所に格納されるため、 b、 i、 c、 d はストレージレイアウトに影響しません。
次に動的配列 e とマッピング f が現れます。
これらはそれぞれ 1 つのスロットを予約し、そのアドレスが 格納位置の計算 に使われます。
このスロットは他の変数と共有できません。なぜなら、計算された格納先アドレスが一意である必要があるからです。
次の変数 g と h はそれぞれ 2 バイトを必要とし、スロット 45 にそれぞれオフセット 0 と 2 でパックされます。
構造体 s は、メンバーがそれぞれ 5 バイトを必要とし、連続して詰められます。
しかし、構造体と配列は常に新しいスロットから開始されるため、 s はスロット 46 に、次の変数 k はスロット 47 に格納されます。
一方、ベースコントラクトは派生コントラクトとスロットを共有できるため、 l は新しいスロットを必要としません。
次に、10 要素の配列である m はスロット 48 に配置され、10 バイトを占有します。
n も配列ですが、その要素サイズのために最初のスロットを完全には埋めきれず、次のスロットにまたがって配置されます。
最後に、変数 o は n の要素と同じ型であるにもかかわらず、スロット 51 に配置されます。
これは、構造体や配列の後に続く変数は常に新しいスロットから開始されるというルールによるものです。
これらをすべて踏まえると、コントラクト C のストレージおよびトランジェントストレージのレイアウトは、次のように示すことができます:
Storage:
42 [aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa] 43 [eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee] 44 [ffffffffffffffffffffffffffffffff] 45 [ hhgg] 46 [ yxxxx] 47 [ lllllllllllllllllllllk] 48 [ mmmmmmmmmm] 49 [ nnnnnnnnnnnnnnnnnnnnnnnnnnnnnn] 50 [ nnnnnnnnnn] 51 [ ooooo]
Transient storage:
00 [iiiiiiiiiiiiiiiibbbbbbbbbbbbbbbb]
ストレージ指定子は、 A および B に対しては C の継承階層の一部としてのみ影響を与える点に注意してください。
それぞれが独立してデプロイされた場合、そのストレージは 0 から開始されます。
Storage layout of
A:00 [aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]
Storage layout of
B:00 [eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee] 01 [ffffffffffffffffffffffffffffffff] 02 [ hhgg] 03 [ yxxxx] 04 [ k]
警告
32バイト以下の要素を使用する場合、コントラクトのガス使用量が多くなる場合があります。 これは、EVMが一度に32バイトで動作するためです。 そのため、要素がそれよりも小さい場合、EVMは要素のサイズを32バイトから希望のサイズに縮小するために、より多くの操作を行う必要があります。
コンパイラは複数の要素を1つのストレージスロットにまとめ、複数の読み書きを1つの操作にまとめるため、ストレージの値を扱う場合はサイズの小さい型を使用することが有益な場合があります。 しかし、スロット内のすべての値を同時に読み書きしない場合、これは逆効果になります。 複数の値を持つストレージスロットに1つの値が書き込まれた場合、ストレージスロットを最初に読み込んで、同じスロットの他のデータが破壊されないように新しい値と結合する必要があります。
関数の引数やメモリの値を扱う場合は、コンパイラがこれらの値をパックしないので、本質的なメリットはありません。
最後に、EVMに最適化させるために、ストレージ変数と struct メンバーの順番を工夫して、しっかりと詰め込むようにしてください。
例えば、ストレージ変数を uint128, uint256, uint128 ではなく uint128, uint128, uint256 の順に宣言すると、前者はストレージのスロットを2つしか使用しないのに対し、後者は3つ使用することになります。
注釈
ストレージの状態変数のレイアウトは、ストレージへのポインタをライブラリに渡すことができるため、Solidityの外部インターフェースの一部とみなされます。 つまり、このセクションで説明されているルールを変更することは、言語の破壊的な変更とみなされ、その重大な性質のため、実行する前に非常に慎重に検討する必要があります。 このような変更があった場合、コンパイラが古いレイアウトをサポートするバイトコードを生成する互換モードをリリースしたいと思います。
マッピングと動的配列
マッピングや動的なサイズの配列型は、そのサイズが予測できないため、前後の状態変数の間に格納することはできません。 その代わりに、それらは 上記のルール に対して32バイトしか占有しないとみなされ、含まれる要素はKeccak-256ハッシュを使用して計算された別のストレージスロットから開始して格納されます。
マッピングや配列の格納場所が、 ストレージのレイアウトルール を適用した後にスロット p になったとします。
動的配列の場合、このスロットには、配列の要素数が格納されます(バイト配列と文字列は例外で、 ここ を参照してください)。
マッピングの場合、このスロットは空のままですが、2つのマッピングが隣り合っていても、その内容が異なる保存場所になることを保証するために必要です。
配列データは keccak256(p) から始まり、静的なサイズの配列データと同じように配置されています。
要素の長さが16バイト以下であれば、ストレージスロットを共有できる可能性があります。
動的配列の動的配列は、このルールを再帰的に適用します。
x の型が uint24[][] である要素 x[i][j] の位置は、次のように計算されます(ここでも、 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 です(32バイトの変数 x が先に存在しています)。
つまり、 data[4] は keccak256(uint256(4) . uint256(1)) に格納されます。
data[4] の型は再びマッピングで、 data[4][9] のデータはスロット keccak256(uint256(9) . keccak256(uint256(4) . uint256(1))) から始まります。
a と b は1つのスロットにパックされているので、構造体 S 内のメンバー c のスロットオフセットは 1 です。
つまり、 data[4][9].c のスロットは keccak256(uint256(9) . keccak256(uint256(4) . uint256(1))) + 1 です。
値型は uint256 なので、1つのスロットを使用します。
bytes と string
bytes と string は同じようにエンコードされます。
一般的には、配列自体を格納するスロットと、そのスロットの位置の keccak256 ハッシュを使って計算されるデータ領域があるという意味で、 bytes1[] と同様のエンコーディングになっています。
ただし、短い値(32バイトよりも)の場合は、配列の要素が長さとともに同じスロットに格納されます。
具体的には、データが最大で 31 バイトの場合、上位バイトに要素が格納され(左詰め)、下位バイトには値 length * 2 が格納されます。
32 バイト以上のデータを格納するバイト配列では、メインスロット p に length * 2 + 1 が格納され、データは通常通り keccak256(p) に格納されます。
つまり、最下位ビットがセットされているかどうかで、short(セットされていない)、long(セットされている)と、短い配列と長い配列を見分けることができるのです。
注釈
無効にエンコードされたスロットの処理は現在サポートされていませんが、将来追加される可能性があります。
IR経由でコンパイルしている場合、不正にエンコードされたスロットを読むと Panic(0x22) エラーが発生します。
JSON出力
コントラクトのストレージレイアウト(あるいはトランジェントストレージレイアウト)は、 標準JSONインターフェース を介して要求できます。
出力されるのは、 storage と types の2つのキーを含むJSONオブジェクトです。
storage オブジェクトは配列で、各要素は次のような形をしています。
{
"astId": 2,
"contract": "fileA:A",
"label": "x",
"offset": 0,
"slot": "0",
"type": "t_uint256"
}
上記の例は、ソースユニット fileA から contract A { uint x; } のストレージレイアウトと
astIdは状態変数の宣言のASTノードのIDです。contractはコントラクトの名前で、プレフィックスとしてパスを含みます。labelは状態変数の名前です。offsetはエンコーディングに応じたストレージスロット内のバイト単位のオフセットです。slotは、状態変数が存在する、あるいは、開始するストレージスロットです。 この数値は非常に大きくなる可能性があるため、JSONの値は文字列として表されます。typeは、変数の型情報のキーとなる識別子です(以下に記載)。
与えられた type 、この場合 t_uint256 は、 types の中の要素を表しており、その形は
{
"encoding": "inplace",
"label": "uint256",
"numberOfBytes": "32",
}
ここで
encodingは、データがストレージでどのようにエンコードされているかを示すもので、可能な値は以下の通りです。labelは正規化された型名です。numberOfBytesは使用されたバイト数(10進数の文字列)です。numberOfBytes > 32の場合は、複数のスロットが使用されていることを意味することに注意してください。
いくつかの型は、上記の4つの情報以外にも追加の情報を持っています。
マッピングには key 型と value 型があり(ここでも型のマッピングのエントリを参照しています)、配列には base 型があり、構造体には members 型がトップレベルの storage 型と同じ形式で記載されています( 上記 参照)。
注釈
コントラクトのストレージレイアウトの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"
}
}
}