コントラクトABIの仕様
基本設計
コントラクトのApplication Binary Interface (ABI)は、Ethereumエコシステム内のコントラクトと対話するためのスタンダードな方法であり、ブロックチェーンの外側からも、コントラクト間の対話のためにも使用されます。 データは、この仕様に記載されているように、その型に応じてエンコードされます。 エンコーディングは自己記述的ではないため、デコードするためにはスキーマが必要です。
コントラクトのインタフェース関数が強く型付けされ、それがコンパイル時に知られており、かつ静的であるとしています。 また、すべてのコントラクトは、それらが呼び出すコントラクトのインタフェース定義をコンパイル時に利用可能であるとしています。
この仕様では、インターフェースが動的である、あるいは、実行時にしかわからないコントラクトは扱いません。 また、ライブラリに対する ABI の仕様は わずかに異なります。
関数セレクタ
関数呼び出しのコールデータの最初の4バイトは、呼び出される関数を指定します。 これは、関数のシグネチャのKeccak-256ハッシュの最初(左、ビッグエンディアンの高次)の4バイトです。 シグネチャは、データロケーション指定子のない基本のプロトタイプの正規表現として定義されています。 つまり、関数名と括弧で囲まれたパラメータ型のリストです。 パラメータ型はコンマで分割され、スペースは使用されません。
注釈
関数の戻り値の型は、シグネチャには含まれません。 Solidityの関数オーバーロード では戻り値の型は考慮されません。 その理由は、関数呼び出しの解決をコンテキストに依存しないようにするためです。 しかし、 ABIのJSONの内容 には入力と出力の両方が含まれます。
引数のエンコーディング
5バイト目からは、エンコードされた引数が続きます。 このエンコーディングは他の場所でも使用されています。 例えば、戻り値やイベントの引数も同じようにエンコーディングされます。 ただし、関数を指定する4バイトのセレクタはありません。
型
ライブラリの ABI は、たとえばストレージ以外の構造体に対してなど、以下とは異なる型を取る場合があることに注意してください。 詳細は library selectors を参照してください。
次のような基本型があります。
uint<M>:Mビット、0 < M <= 256、M % 8 == 0の符号なし整数型。 例えばuint32、uint8、uint256。int<M>:Mビット、0 < M <= 256、M % 8 == 0の2の補数の符号付き整数型。address: 仮定される解釈と言語の型付けを除き、uint160と同等。 関数セレクタの計算にはaddressを使用。uint,int: それぞれuint256とint256の同義語。 関数セレクタの計算にはuint256とint256を使用。bool: 0と1に限定されたuint8と同等。 関数セレクタの計算にはboolを使用。fixed<M>x<N>:Mビット、8 <= M <= 256、M % 8 == 0、0 < N <= 80の符号付き固定小数点10進数で、vの値をv / (10 ** N)と表記。ufixed<M>x<N>:fixed<M>x<N>の符号なしのバリアント。fixed,ufixed: それぞれfixed128x18とufixed128x18の同義語。 関数セレクタの計算には、fixed128x18とufixed128x18を使用。bytes<M>:Mバイトのバイナリ型、0 < M <= 32。function: アドレス(20バイト)の後に関数セレクタ(4バイト)が続く。bytes24と同じようにエンコード。
次のような(固定サイズの)配列型が存在します。
<type>[M]: 与えられた型のM個の要素の固定長の配列。M >= 0。注釈
このABI仕様では、要素数が0の固定長の配列を表現できますが、コンパイラではサポートされていません。
次のような非固定サイズの型が存在します。
bytes: 動的サイズのバイトシーケンス。string: UTF-8でエンコードされていると仮定した動的サイズのユニコード文字列。<type>[]: 指定された型の要素を持つ可変長の配列。
型は、カンマで区切って括弧で囲むことでタプルにまとめることができます。
(T1,T2,...,Tn):T1, ...,Tnの各型からなるタプル。n >= 0。
タプルのタプル、タプルの配列などを作ることが可能です。
また、ゼロタプルを作ることも可能です( n == 0 )。
Solidityの型からABIの型へのマッピング
Solidityでは、タプルを除いて、上記で紹介したすべての型を同じ名前でサポートしています。 一方で、Solidityの型の中には、ABIではサポートされていないものもあります。 次の表は、左の列にABIがサポートしていないSolidityの型を、右の列にそれに対応するABIの型を示しています。
Solidity |
ABI |
|---|---|
|
|
|
|
|
|
元となる値型 |
|
|
警告
バージョン 0.8.0 以前のenumは256個以上のメンバーを持つことができ、任意のメンバーの値を保持するのに十分な大きさの最小の整数型で表されていました。
エンコーディングの設計基準
このエンコーディングは、以下のような特性を持つように設計されており、いくつかの引数が入れ子になった配列である場合には、特に便利です。
値にアクセスするために必要な読み取り回数は、最大でも引数配列構造内の値の深さ分であり、すなわち
a_i[k][l][r]を取得するためには4回の読み取りが必要です。 以前のバージョンのABIでは、最悪の場合、読み取り回数は動的パラメータの総数に比例していました。変数や配列要素のデータは、他のデータとインターリーブされておらず、相対的な「アドレス」のみを使用する、リロケータブルなものです。
エンコーディングの形式的な仕様
静的な型と動的な型を区別します。 静的型はインプレースでエンコードされ、動的型は現在のブロックの後に別個に割り当てられた場所でエンコードされます。
定義: 次のような型を「動的」と呼びます。
bytesstring任意の
Tに対してT[]任意の動的
Tと任意のk >= 0に対するT[k]ある
1 <= i <= kに対してTiが動的である場合の(T1,...,Tk)
それ以外の型は「静的」と呼ばれます。
定義: len(a) は、2進数の文字列 a のバイト数です。
len(a) の型は uint256 とします。
実際のエンコーディングである enc は、ABI型の値をバイナリ文字列にマッピングしたものと定義し、 X の型が動的である場合に限り、 len(enc(X)) が X の値に依存するようにします。
定義: 任意のABI値 X に対して、 X の型に応じて enc(X) を再帰的に定義します。
k >= 0と任意の型T1, ...,Tkに対する(T1,...,Tk)enc(X) = head(X(1)) ... head(X(k)) tail(X(1)) ... tail(X(k))ここで、
X = (X(1), ..., X(k))とheadとtailは、Tiに対して次のように定義されます。Tiが静的である場合:head(X(i)) = enc(X(i))とtail(X(i)) = ""(空の文字列)それ以外の場合、つまり
Tiが動的な場合:head(X(i)) = enc(len( head(X(1)) ... head(X(k)) tail(X(1)) ... tail(X(i-1)) ))tail(X(i)) = enc(X(i))なお、動的なケースでは、head部分の長さは型にのみ依存し、値には依存しないため、
head(X(i))はwell-definedです。head(X(i))の値は、enc(X)の開始位置に対するtail(X(i))の開始位置のオフセットです。任意の
Tとkに対するT[k]:enc(X) = enc((X[0], ..., X[k-1]))つまり、同じ型の
k要素を持つタプルであるかのようにエンコードされます。Xがkの要素を持つT[](kはuint256型とします)。enc(X) = enc(k) enc((X[0], ..., X[k-1]))つまり、同じ型の
k個の要素を持つタプル(静的サイズkの配列)であるかのようにエンコードされ、その前に要素数が付きます。長さ
kのbytes(これは型uint256であると仮定されます)。enc(X) = enc(k) pad_right(X)、すなわち、バイト数はuint256に続いてXの実際の値をバイト列として符号化し、その後にlen(enc(X))が32の倍数になるような最小数のゼロバイトが続きます。string:enc(X) = enc(enc_utf8(X))、つまりXはUTF-8でエンコードされ、この値はbytes型と解釈され、さらにエンコードされます。 なお、この後のエンコードで使用する長さは、文字数ではなく、UTF-8でエンコードされた文字列のバイト数です。uint<M>:enc(X)は、Xのビッグエンディアンのエンコーディングで、高次(左)側に0バイトをパディングし、長さが32バイトになるようにしたものです。address:uint160と同様です。int<M>:enc(X)はXのビッグエンディアンの2の補数で、高次(左)側に負のXには0xffバイト、非負のXには0バイトをパディングし、長さが32バイトになるようにしたものです。bool:uint8と同様に、trueには1、falseには0が使われます。fixed<M>x<N>:enc(X)はenc(X * 10**N)でX * 10**Nはint256と解釈されます。fixed:fixed128x18と同様です。ufixed<M>x<N>:enc(X)はenc(X * 10**N)でX * 10**Nはuint256と解釈されます。ufixed:ufixed128x18と同様です。bytes<M>:enc(X)は、Xのバイト列に末尾にゼロバイトを追加して32バイトにしたものです。
なお、任意の X に対して、 len(enc(X)) は32の倍数です。
関数セレクタと引数エンコーディング
つまり、パラメータ a_1, ..., a_n を持つ関数 f の呼び出しは、次のようにエンコードされます。
function_selector(f) enc((a_1, ..., a_n))
となり、 f の戻り値 v_1, ..., v_k は次のようにエンコードされます。
enc((v_1, ..., v_k))
つまり、値がタプルにまとめられ、エンコードされます。
例
次のコントラクトを考えます:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract Foo {
function bar(bytes3[2] memory) public pure {}
function baz(uint32 x, bool y) public pure returns (bool r) { r = x > 32 || y; }
function sam(bytes memory, bool, uint[] memory) public pure {}
}
したがって Foo の例では、
bar を ["abc", "def"] の引数で呼び出す場合、合計68バイトを渡すことになり、その内訳は以下の通りです:
0xfce353f6: メソッドID。シグネチャbar(bytes3[2])から得られます。0x6162630000000000000000000000000000000000000000000000000000000000: 第1パラメータの最初の部分で、bytes3の値"abc"(左寄せ)。0x6465660000000000000000000000000000000000000000000000000000000000: 第1パラメータの2番目の部分で、bytes3の値"def"(左寄せ)。
合わせると、
0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000
69 と true というパラメータで baz を呼び出す場合、合計68バイトを渡すことになり、その内訳は以下の通りです:
0xcdcd77c0: メソッドID。シグネチャbaz(uint32,bool)のASCII形式のKeccakハッシュの最初の4バイトです。0x0000000000000000000000000000000000000000000000000000000000000045: 第1パラメータ。 32バイトにパディングされたuint32の値69。0x0000000000000000000000000000000000000000000000000000000000000001: 第2パラメータ。 32バイトにパディングされたboolの値true。
合わせると、
0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001
この関数は単一の bool を返します。
例えば、 false を返すとしたら、その出力は単一のバイト列 0x0000000000000000000000000000000000000000000000000000000000000000 であり、これは単一のboolです。
引数 "dave" 、 true 、 [1,2,3] で sam を呼び出したい場合、合計292バイトを渡すことになり、その内訳は以下の通りです。
If we wanted to call sam with the arguments "dave", true and [1,2,3], we would
pass 292 bytes total, broken down into:
0xa5643bf2: メソッドID。シグネチャsam(bytes,bool,uint256[])から得られます。uintはその正規の表現であるuint256に置き換えられていることに注意してください。0x0000000000000000000000000000000000000000000000000000000000000060: 第1引数(動的型)のデータ部の位置で、引数ブロックの先頭からのバイト数で表します。この場合は0x60。0x0000000000000000000000000000000000000000000000000000000000000001: 第2引数: boolでtrue。0x00000000000000000000000000000000000000000000000000000000000000a0: 第3引数(動的型)のデータ部の位置で、単位はバイトです。この場合は0xa0。0x0000000000000000000000000000000000000000000000000000000000000004: 第1引数のデータ部で、バイト配列の要素数から始まります。この場合は4。0x6461766500000000000000000000000000000000000000000000000000000000: 第1引数の内容:"dave"のUTF-8(ここではASCIIと同等)エンコードで右をパディングして32バイトにしたもの。0x0000000000000000000000000000000000000000000000000000000000000003: 第3引数のデータ部で、配列の要素数から始まります。この場合は3。0x0000000000000000000000000000000000000000000000000000000000000001: 第3引数の最初のエントリ。0x0000000000000000000000000000000000000000000000000000000000000002: 第3引数の2番目のエントリ。0x0000000000000000000000000000000000000000000000000000000000000003: 第3引数の3番目のエントリ。
合わせると、
0xa5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003
動的型の使用法
シグネチャが f(uint256,uint32[],bytes10,bytes) で値が (0x123, [0x456, 0x789], "1234567890", "Hello, world!") である関数呼び出しは、以下のようにエンコードされます。
まず keccak("f(uint256,uint32[],bytes10,bytes)") の最初の4バイト、つまり 0x8be65246 を取ります。
そして、4つの引数すべてのヘッド部分をエンコードします。
静的型の uint256 と bytes10 については、これらが直接渡したい値となりますが、動的型の uint32[] と bytes については、値のエンコードの開始(つまり、関数シグネチャのハッシュを含む最初の4バイトを数えない)から測定した、データ領域の開始までのオフセットをバイト単位で使用します。
0x0000000000000000000000000000000000000000000000000000000000000123(32バイトにパディングした0x123)0x0000000000000000000000000000000000000000000000000000000000000080(第2パラメータのデータ部の開始位置へのオフセット。4*32バイトでちょうどヘッド部のサイズ)0x3132333435363738393000000000000000000000000000000000000000000000(32バイトに右をパディングした"1234567890")0x00000000000000000000000000000000000000000000000000000000000000e0(第4パラメータのデータ部の先頭へのオフセット = 第1動的パラメータのデータ部の先頭へのオフセット + 第1動的パラメータのデータ部のサイズ = 4 * 32 + 3 * 32(後述))。
これに続いて、最初の動的引数のデータ部、 [0x456, 0x789] は次のようになります。
0x0000000000000000000000000000000000000000000000000000000000000002(配列の要素数が2)0x0000000000000000000000000000000000000000000000000000000000000456(最初の要素)0x0000000000000000000000000000000000000000000000000000000000000789(2番目の要素)
最後に、2番目の動的引数である "Hello, world!" のデータ部をエンコードします。
0x000000000000000000000000000000000000000000000000000000000000000d(要素数(ここではバイト): 13)0x48656c6c6f2c20776f726c642100000000000000000000000000000000000000("Hello, world!"は32バイトで右をパディング)
すべてを合わせると、エンコーディングは次のようになります(わかりやすくするために、関数セレクタと各32バイトの後に改行しています)。
0x8be65246
0000000000000000000000000000000000000000000000000000000000000123
0000000000000000000000000000000000000000000000000000000000000080
3132333435363738393000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000e0
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000456
0000000000000000000000000000000000000000000000000000000000000789
000000000000000000000000000000000000000000000000000000000000000d
48656c6c6f2c20776f726c642100000000000000000000000000000000000000
同じ原理で、シグネチャ g(uint256[][],string[]) を持つ関数のデータを値 ([[1, 2], [3]], ["one", "two", "three"]) でエンコードしてみましょう。
ただし、エンコードの最も基本的な部分から始めます。
まず最初に、第1のルート配列 [[1, 2], [3]] の第1の埋め込み動的配列 [1, 2] の長さとデータをエンコードします。
0x0000000000000000000000000000000000000000000000000000000000000002(1番目の配列の要素数は2で、要素は1と2)0x0000000000000000000000000000000000000000000000000000000000000001(最初の要素)0x0000000000000000000000000000000000000000000000000000000000000002(2番目の要素)
そして、第1のルート配列 [[1, 2], [3]] の第2の埋め込み動的配列 [3] の長さとデータを符号化します。
0x0000000000000000000000000000000000000000000000000000000000000001(2番目の配列の要素数は1で、要素は3)0x0000000000000000000000000000000000000000000000000000000000000003(最初の要素)
次に、それぞれの動的配列 [1, 2] と [3] に対するオフセット a と b を求める必要があります。
このオフセットを計算するために、最初のルート配列 [[1, 2], [3]] のエンコードデータを見て、エンコードの各行を列挙します。
0 - a - offset of [1, 2]
1 - b - offset of [3]
2 - 0000000000000000000000000000000000000000000000000000000000000002 - count for [1, 2]
3 - 0000000000000000000000000000000000000000000000000000000000000001 - encoding of 1
4 - 0000000000000000000000000000000000000000000000000000000000000002 - encoding of 2
5 - 0000000000000000000000000000000000000000000000000000000000000001 - count for [3]
6 - 0000000000000000000000000000000000000000000000000000000000000003 - encoding of 3
オフセット a は、2行目(64バイト)の配列 [1, 2] の内容の先頭を指しているので、 a = 0x0000000000000000000000000000000000000000000000000000000000000040 となります。
オフセット b は、配列 [3] の内容の先頭である5行目(160バイト)を指しているので、 b = 0x00000000000000000000000000000000000000000000000000000000000000a0 となります。
次に、2番目のルート配列の埋め込み文字列をエンコードします。
0x0000000000000000000000000000000000000000000000000000000000000003(単語"one"の文字数)0x6f6e650000000000000000000000000000000000000000000000000000000000(単語"one"のutf8表現)0x0000000000000000000000000000000000000000000000000000000000000003(単語"two"の文字数)0x74776f0000000000000000000000000000000000000000000000000000000000(単語"two"のutf8表現)0x0000000000000000000000000000000000000000000000000000000000000005(単語"three"の文字数)0x7468726565000000000000000000000000000000000000000000000000000000(単語"three"のutf8表現)
最初のルート配列と並行して、文字列は動的な要素なので、そのオフセット c 、 d 、 e を見つける必要があります。
0 - c - offset for "one"
1 - d - offset for "two"
2 - e - offset for "three"
3 - 0000000000000000000000000000000000000000000000000000000000000003 - count for "one"
4 - 6f6e650000000000000000000000000000000000000000000000000000000000 - encoding of "one"
5 - 0000000000000000000000000000000000000000000000000000000000000003 - count for "two"
6 - 74776f0000000000000000000000000000000000000000000000000000000000 - encoding of "two"
7 - 0000000000000000000000000000000000000000000000000000000000000005 - count for "three"
8 - 7468726565000000000000000000000000000000000000000000000000000000 - encoding of "three"
オフセット c は、3行目(96バイト)である文字列 "one" の内容の開始点を指しているので、 c = 0x0000000000000000000000000000000000000000000000000000000000000060 となります。
オフセット d は、5行目(160バイト)の文字列 "two" の内容の始まりを指しているので、 d = 0x00000000000000000000000000000000000000000000000000000000000000a0 となります。
オフセット e は、7行目(224バイト)である文字列 "three" のコンテンツの開始を指しているので、 e = 0x00000000000000000000000000000000000000000000000000000000000000e0 。
なお、ルート配列の埋め込み要素の符号化は互いに依存しておらず、シグネチャ g(string[],uint256[][]) を持つ関数では同じ符号化になります。
そして、最初のルート配列の長さをエンコードします。
0x0000000000000000000000000000000000000000000000000000000000000002(最初のルート配列の要素数2、要素自体は[1, 2]と[3])
そして、2番目のルートの配列の長さをエンコードします。
0x0000000000000000000000000000000000000000000000000000000000000003(2番目のルート配列に含まれる文字列の数3、文字列自体は"one"、"two"、"three")
最後に、それぞれのルート動的配列 [[1, 2], [3]] と ["one", "two", "three"] のオフセット f と g を見つけ、正しい順序でパーツを組み立てます。
0x2289b18c - function signature
0 - f - offset of [[1, 2], [3]]
1 - g - offset of ["one", "two", "three"]
2 - 0000000000000000000000000000000000000000000000000000000000000002 - count for [[1, 2], [3]]
3 - 0000000000000000000000000000000000000000000000000000000000000040 - offset of [1, 2]
4 - 00000000000000000000000000000000000000000000000000000000000000a0 - offset of [3]
5 - 0000000000000000000000000000000000000000000000000000000000000002 - count for [1, 2]
6 - 0000000000000000000000000000000000000000000000000000000000000001 - encoding of 1
7 - 0000000000000000000000000000000000000000000000000000000000000002 - encoding of 2
8 - 0000000000000000000000000000000000000000000000000000000000000001 - count for [3]
9 - 0000000000000000000000000000000000000000000000000000000000000003 - encoding of 3
10 - 0000000000000000000000000000000000000000000000000000000000000003 - count for ["one", "two", "three"]
11 - 0000000000000000000000000000000000000000000000000000000000000060 - offset for "one"
12 - 00000000000000000000000000000000000000000000000000000000000000a0 - offset for "two"
13 - 00000000000000000000000000000000000000000000000000000000000000e0 - offset for "three"
14 - 0000000000000000000000000000000000000000000000000000000000000003 - count for "one"
15 - 6f6e650000000000000000000000000000000000000000000000000000000000 - encoding of "one"
16 - 0000000000000000000000000000000000000000000000000000000000000003 - count for "two"
17 - 74776f0000000000000000000000000000000000000000000000000000000000 - encoding of "two"
18 - 0000000000000000000000000000000000000000000000000000000000000005 - count for "three"
19 - 7468726565000000000000000000000000000000000000000000000000000000 - encoding of "three"
オフセット f は、2行目(64バイト)の配列 [[1, 2], [3]] の内容の先頭を指しているので、 f = 0x0000000000000000000000000000000000000000000000000000000000000040 となります。
オフセット g は、配列 ["one", "two", "three"] の内容の先頭である10行目(320バイト)を指しているので、 g = 0x0000000000000000000000000000000000000000000000000000000000000140 となります。
イベント
イベントは、Ethereumのログ/イベントウォッチングプロトコルを抽象化したものです。 ログエントリは、コントラクトのアドレス、最大4つのトピックのシリーズ、任意の長さのバイナリデータを提供します。 イベントは、既存の関数ABIを活用して、これを(インターフェース仕様とともに)適切に型付けされた構造として解釈します。
イベント名と一連のイベントパラメータが与えられると、それらを2つのサブシリーズに分割します。 インデックスが付けられているものは、最大で3つ(非匿名イベントの場合)または4つ(匿名イベントの場合)あり、イベント署名のKeccakハッシュと一緒にログエントリのトピックを形成するために使用されます。 インデックスが付けられていないものは、イベントのバイト配列を形成します。
事実上、このABIを使ったログエントリは次のように記述されます。
address: コントラクトのアドレス(Ethereumが本質的に提供するもの)。
topics[0]:keccak(EVENT_NAME+"("+EVENT_ARGS.map(canonical_type_of).join(",")+")")(canonical_type_ofは、与えられた引数の正規の型を単純に返す関数であり、例えばuint indexed fooの場合はuint256を返す)。 この値は、イベントがanonymousとして宣言されていない場合、topics[0]にのみ存在します。
topics[n]:abi_encode(EVENT_INDEXED_ARGS[n]) - 1])イベントがanonymousとして宣言されていない場合はabi_encode(EVENT_INDEXED_ARGS[n])、宣言されている場合はabi_encode(EVENT_INDEXED_ARGS[n])となります(EVENT_INDEXED_ARGSはインデックス化されたEVENT_ARGSのシリーズです)。
data:EVENT_NON_INDEXED_ARGSのABIエンコーディング(EVENT_NON_INDEXED_ARGSはインデックスが付いていない一連のEVENT_ARGS、abi_encodeは上述のように関数から型付けされた一連の値を返すために使用されるABIエンコーディング関数)。
長さが最大32バイトのすべての型について、 EVENT_INDEXED_ARGS 配列には、通常のABIエンコーディングと同様に、32バイトにパディングまたは符号拡張された値が直接格納されます。
しかし、すべての "複雑な"型や動的な長さの型(すべての配列、 string 、 bytes 、構造体を含む)では、 EVENT_INDEXED_ARGS には直接エンコードされた値ではなく、特別なインプレースエンコードされた値の Keccakハッシュ ( インデックスされたイベントパラメータのエンコーディング 参照)が格納されます。
これにより、アプリケーションは(エンコードされた値のハッシュをトピックとして設定することで)動的長型の値を効率的に問い合わせることができますが、アプリケーションは問い合わせていないインデックス化された値をデコードできなくなります。
動的長型の場合、アプリケーション開発者は、(引数がインデックス化されている場合の)所定の値の高速検索と(引数がインデックス化されていないことが必要な)任意の値の可読性との間でトレードオフの関係に直面します。
開発者はこのトレードオフを克服し、効率的な検索と任意の可読性の両方を達成するために、同じ値を保持することを意図した2つの引数(1つはインデックス化され、1つはインデックス化されない)を持つイベントを定義できます。
エラー
コントラクト内部で障害が発生した場合、コントラクトは特別なオペコードを使用して実行を中止し、すべての状態変化をリバートできます。 これらの効果に加えて、記述的データを呼び出し元に返すことができます。 この記述データは、関数呼び出しのデータと同じように、エラーとその引数をエンコードしたものです。
例として、 transfer 関数が常に「残高不足」というカスタムエラーでリバートしてしまう次のコントラクトを考えてみましょう。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract TestToken {
error InsufficientBalance(uint256 available, uint256 required);
function transfer(address /*to*/, uint amount) public pure {
revert InsufficientBalance(0, amount);
}
}
戻りデータは、関数 InsufficientBalance(uint256,uint256) に対する関数呼び出し InsufficientBalance(0, amount) と同じ方法でエンコードされます。
エラーセレクタ 0x00000000 と 0xffffffff は将来のために予約されています。
警告
エラーデータを信用してはいけません。 デフォルトでは、エラーデータは外部呼び出しの連鎖を通じてバブリングします。 つまり、コントラクトは、直接呼び出すコントラクトのどれにも定義されていないエラーを受け取る可能性があります。 さらに、どのコントラクトも、エラーがどこにも定義されていなくても、エラー署名に一致するデータを返すことで、どんなエラーでも偽装できます。
JSON
コントラクトのインターフェースのJSONフォーマットは、関数、イベント、エラーの記述の配列で与えられます。 関数の記述は、フィールドを持つJSONオブジェクトです。
type:"function"、"constructor"、"receive"( 「receive Ether」関数 )、"fallback"( 「default」関数 )のいずれかです。name: 関数の名前。inputs: オブジェクトの配列で、それぞれのオブジェクトは次のものを含みます。name: パラメータの名前。type: パラメータの正規の型(詳細は後述)。components: タプル型に使用(詳細は後述)。
outputs:inputsと同様のオブジェクトの配列。stateMutability: 以下のいずれかの値を持つ文字列。pure( ブロックチェーンのステートを読まないように指定 )、view( ブロックチェーンのステートを修正しないように指定 )、nonpayable(関数はEtherを受け取らない、デフォルト)とpayable(関数はEtherを受け取る)があります。
コンストラクタ、receive関数、fallback関数は name や outputs を持ちません。
receive関数とfallback関数には inputs もありません。
注釈
non-payableな関数にEtherを送ると、トランザクションがrevertします。
注釈
ステートミュータビリティ nonpayable は、Solidityではステートミュータビリティモディファイアを指定しないことで設定されます。
イベントの記述は、同様のフィールドを持つJSONオブジェクトです。
type: 常に"event"。name: イベントの名前。inputs: オブジェクトの配列で、それぞれのオブジェクトは次のものを含みます。name: パラメータの名前。type: パラメータの正規の型(詳細は後述)。components: タプル型に使用(詳細は後述)。indexed: フィールドがログのトピックの一部である場合はtrue、ログのデータセグメントの一部である場合はfalse。
anonymous: イベントがanonymousと宣言された場合はtrue。
エラーの記述は以下の通りです。
type: 常に"error"。name: エラーの名前。inputs: オブジェクトの配列で、それぞれのオブジェクトは次のものを含みます。name: パラメータの名前。type: パラメータの正規の型(詳細は後述)。components: タプル型に使用(詳細は後述)。
注釈
スマートコントラクト内の異なるファイルからエラーが発生した場合や、別のスマートコントラクトから参照されている場合など、JSON配列内に同じ名前や同一の署名を持つ複数のエラーが存在する可能性があります。 ABIでは、エラー自体の名前だけが重要で、どこで定義されているかは関係ありません。
例えば、
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract Test {
constructor() { b = hex"12345678901234567890123456789012"; }
event Event(uint indexed a, bytes32 b);
event Event2(uint indexed a, bytes32 b);
error InsufficientBalance(uint256 available, uint256 required);
function foo(uint a) public { emit Event(a, b); }
bytes32 b;
}
は、次のJSONになります。
[{
"type":"error",
"inputs": [{"name":"available","type":"uint256"},{"name":"required","type":"uint256"}],
"name":"InsufficientBalance"
}, {
"type":"event",
"inputs": [{"name":"a","type":"uint256","indexed":true},{"name":"b","type":"bytes32","indexed":false}],
"name":"Event"
}, {
"type":"event",
"inputs": [{"name":"a","type":"uint256","indexed":true},{"name":"b","type":"bytes32","indexed":false}],
"name":"Event2"
}, {
"type":"function",
"inputs": [{"name":"a","type":"uint256"}],
"name":"foo",
"outputs": []
}]
タプル型のハンドリング
名前は意図的にABIエンコーディングの一部ではありませんが、エンドユーザーに表示するためにJSONに含めることには大きな意味があります。 構造は以下の方法で入れ子になっています。
メンバー name 、 type 、そして潜在的に components を持つオブジェクトは、型付けされた変数を記述します。
タプル型に到達するまでは正規の型が決定され、その時点までの文字列記述は tuple という単語を前置した type に格納されます。
つまり、 tuple の後に整数 k を持つ [] と [k] のシーケンスが続くことになります。
その後、タプルの構成要素はメンバー components に格納されます。
components は配列型で、そこに indexed が許されないことを除いて、トップレベルのオブジェクトと同じ構造を持っています。
一例として、次のコード
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.5 <0.9.0;
pragma abicoder v2;
contract Test {
struct S { uint a; uint[] b; T[] c; }
struct T { uint x; uint y; }
function f(S memory, T memory, uint) public pure {}
function g() public pure returns (S memory, T memory, uint) {}
}
は、次のJSONになります。
[
{
"name": "f",
"type": "function",
"inputs": [
{
"name": "s",
"type": "tuple",
"components": [
{
"name": "a",
"type": "uint256"
},
{
"name": "b",
"type": "uint256[]"
},
{
"name": "c",
"type": "tuple[]",
"components": [
{
"name": "x",
"type": "uint256"
},
{
"name": "y",
"type": "uint256"
}
]
}
]
},
{
"name": "t",
"type": "tuple",
"components": [
{
"name": "x",
"type": "uint256"
},
{
"name": "y",
"type": "uint256"
}
]
},
{
"name": "a",
"type": "uint256"
}
],
"outputs": []
}
]
厳密なエンコーディングモード
厳密なエンコーディングモードとは、上記の正式な仕様で定義されているのと全く同じエンコーディングになるモードです。 つまり、データ領域にオーバーラップを生じさせないようにしながら、オフセットはできるだけ小さくしなければならず、したがってギャップは許されません。
通常、ABIデコーダはオフセットポインタに従うだけの素直な方法で書かれていますが、デコーダによってはストリクトモードを強制する場合があります。 SolidityのABIデコーダは、現在のところストリクトモードを強制していませんが、エンコーダは常にストリクトモードでデータを作成します。
非標準のパックモード
Solidityは、 abi.encodePacked() を通して、非標準のパックモードをサポートしています。
32バイトより短い型は、パディングや符号拡張なしに、直接連結されます。
動的型は、インプレースで長さの情報無しにエンコードされます。
配列の要素はパディングされますが、インプレースでエンコードされます。
また、構造体や入れ子になった配列はサポートされていません。
例として、 int16(-1), bytes1(0x42), uint16(0x03), string("Hello, world!") をエンコードすると次のようになります。
0xffff42000348656c6c6f2c20776f726c6421
^^^^ int16(-1)
^^ bytes1(0x42)
^^^^ uint16(0x03)
^^^^^^^^^^^^^^^^^^^^^^^^^^ string("Hello, world!") lengthフィールド無し
より具体的には、
エンコードの際、すべてがインプレースでエンコードされます。 つまり、ABIのエンコーディングのように、先頭と末尾の区別がなく、配列の長さもエンコードされません。
abi.encodePackedの直接引数は、配列(またはstringやbytes)でない限り、パディングなしでエンコードされます。
配列のエンコーディングは、その要素のエンコーディングとパディングを連結したものです。
string、bytes、uint[]のような動的なサイズの型は、長さフィールドなしでエンコードされます。
stringやbytesのエンコーディングでは、配列や構造体の一部でない限り、末尾にパディングが適用されません(その場合、32バイトの倍数にパディングされます)。
一般的には、動的なサイズの要素が2つあると、長さのフィールドがないため、エンコーディングが曖昧になります。
パディングが必要な場合は、明示的な型変換を行うことができます: abi.encodePacked(uint16(0x12)) == hex"0012" 。
関数を呼び出すときにはpackedエンコーディングは使われないので、関数セレクタの前に付ける特別なサポートはありません。 また、エンコーディングが曖昧なため、デコード関数はありません。
警告
keccak256(abi.encodePacked(a, b)) を使っていて、 a と b の両方が動的型の場合、 a の一部を b に移動させたり、逆に b の一部を a に移動させたりすることで、ハッシュ値の衝突を容易に工作できます。
より具体的には abi.encodePacked("a", "bc") == abi.encodePacked("ab", "c") です。
署名や認証、データの整合性のために abi.encodePacked を使用する場合は、常に同じ型を使用し、最大でもどちらかが動的型であることを確認してください。
やむを得ない理由がない限り、 abi.encode を優先すべきです。
インデックスされたイベントパラメータのエンコーディング
値型ではないインデックスされたイベントパラメータ(配列や構造体)は、直接保存されず、エンコーディングのkeccak256ハッシュが保存されます。 このエンコーディングは以下のように定義されています。
bytesとstringの値のエンコーディングは、パディングや長さのプレフィックスを含まない文字列の内容だけになります。構造体のエンコーディングは、そのメンバーのエンコーディングを32バイトの倍数にパディングして連結したものです(
bytesやstringも同様)。配列のエンコーディング(動的サイズと静的サイズどちらも)は、その要素のエンコーディングを32バイトの倍数にパディングして連結したもので(
bytesとstringも)、長さのプレフィックスはありません。
上記では、通常と同じく負の数は符号拡張でパディングされ、ゼロパディングされません。
bytesNN 型は右が、 uintNN / intNN 型は左がパディングされます。
警告
構造体に複数の動的サイズの配列が含まれていると、エンコーディングが曖昧になります。 そのため、常にイベントデータを再確認し、インデックス化されたパラメータだけに基づく検索結果に頼らないようにしてください。