コントラクトABIの仕様

基本設計

コントラクトのApplication Binary Interface (ABI)は、Ethereumエコシステム内のコントラクトと対話するためのスタンダードな方法であり、ブロックチェーンの外側からも、コントラクト間の対話のためにも使用されます。 データは、この仕様に記載されているように、その型に応じてエンコードされます。 エンコーディングは自己記述的ではないため、デコードするためにはスキーマが必要です。

コントラクトのインタフェース関数が強く型付けされ、それがコンパイル時に知られており、かつ静的であるとしています。 また、すべてのコントラクトは、それらが呼び出すコントラクトのインタフェース定義をコンパイル時に利用可能であるとしています。

この仕様では、インターフェースが動的である、あるいは、実行時にしかわからないコントラクトは扱いません。

関数セレクタ

関数呼び出しのコールデータの最初の4バイトは、呼び出される関数を指定します。 これは、関数のシグネチャのKeccak-256ハッシュの最初(左、ビッグエンディアンの高次)の4バイトです。 シグネチャは、データロケーション指定子のない基本のプロトタイプの正規表現として定義されています。 つまり、関数名と括弧で囲まれたパラメータ型のリストです。 パラメータ型はコンマで分割され、スペースは使用されません。

注釈

関数の戻り値の型は、シグネチャには含まれません。 Solidityの関数オーバーロード では戻り値の型は考慮されません。 その理由は、関数呼び出しの解決をコンテキストに依存しないようにするためです。 しかし、 ABIのJSONの内容 には入力と出力の両方が含まれます。

引数のエンコーディング

5バイト目からは、エンコードされた引数が続きます。 このエンコーディングは他の場所でも使用されています。 例えば、戻り値やイベントの引数も同じようにエンコーディングされます。 ただし、関数を指定する4バイトのセレクタはありません。

次のような基本型があります。

  • uint<M>: M ビット、 0 < M <= 256M % 8 == 0 の符号なし整数型。 例えば uint32uint8uint256

  • int<M>: M ビット、 0 < M <= 256M % 8 == 0 の2の補数の符号付き整数型。

  • address: 仮定される解釈と言語の型付けを除き、 uint160 と同等。 関数セレクタの計算には address を使用。

  • uint, int: それぞれ uint256int256 の同義語。 関数セレクタの計算には uint256int256 を使用。

  • bool: 0と1に限定された uint8 と同等。 関数セレクタの計算には bool を使用。

  • fixed<M>x<N>: M ビット、 8 <= M <= 256M % 8 == 00 < N <= 80 の符号付き固定小数点10進数で、 v の値を v / (10 ** N) と表記。

  • ufixed<M>x<N>: fixed<M>x<N> の符号なしのバリアント。

  • fixed, ufixed: それぞれ fixed128x18ufixed128x18 の同義語。 関数セレクタの計算には、 fixed128x18ufixed128x18 を使用。

  • 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

address payable

address

contract

address

enum

uint8

user defined value types

元となる値型

struct

tuple

警告

バージョン 0.8.0 以前のenumは256個以上のメンバーを持つことができ、任意のメンバーの値を保持するのに十分な大きさの最小の整数型で表されていました。

エンコーディングの設計基準

このエンコーディングは、以下のような特性を持つように設計されており、いくつかの引数が入れ子になった配列である場合には、特に便利です。

  1. 値にアクセスするために必要な読み取り回数は、最大でも引数配列構造内の値の深さ分であり、すなわち a_i[k][l][r] を取得するためには4回の読み取りが必要です。 以前のバージョンのABIでは、最悪の場合、読み取り回数は動的パラメータの総数に比例していました。

  2. 変数や配列要素のデータは、他のデータとインターリーブされておらず、相対的な「アドレス」のみを使用する、リロケータブルなものです。

エンコーディングの形式的な仕様

静的な型と動的な型を区別します。 静的型はインプレースでエンコードされ、動的型は現在のブロックの後に別個に割り当てられた場所でエンコードされます。

定義: 次のような型を「動的」と呼びます。

  • bytes

  • string

  • 任意の 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))headtail は、 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)) の開始位置のオフセットです。

  • 任意の Tk に対する T[k]:

    enc(X) = enc((X[0], ..., X[k-1]))

    つまり、同じ型の k 要素を持つタプルであるかのようにエンコードされます。

  • Xk の要素を持つ T[]kuint256 型とします)。

    enc(X) = enc(k) enc((X[0], ..., X[k-1]))

    つまり、同じ型の k 個の要素を持つタプル(静的サイズ k の配列)であるかのようにエンコードされ、その前に要素数が付きます。

  • 長さ kbytes (これは型 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 には 1false には 0 が使われます。

  • fixed<M>x<N>: enc(X)enc(X * 10**N)X * 10**Nint256 と解釈されます。

  • fixed: fixed128x18 と同様です。

  • ufixed<M>x<N>: enc(X)enc(X * 10**N)X * 10**Nuint256 と解釈されます。

  • 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 の例では、 69true というパラメータで 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です。

bar["abc", "def"] の引数で呼び出す場合、合計68バイトを渡すことになり、その内訳は以下の通りです。

  • 0xfce353f6: メソッドID。シグネチャ bar(bytes3[2]) から得られます。

  • 0x6162630000000000000000000000000000000000000000000000000000000000: 第1パラメータの最初の部分で、 bytes3 の値 "abc" (左寄せ)。

  • 0x6465660000000000000000000000000000000000000000000000000000000000: 第1パラメータの2番目の部分で、 bytes3 の値 "def" (左寄せ)。

合わせると、

0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000

引数 "dave"true[1,2,3]sam を呼び出したい場合、合計292バイトを渡すことになり、その内訳は以下の通りです。

  • 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つの引数すべてのヘッド部分をエンコードします。 静的型の uint256bytes10 については、これらが直接渡したい値となりますが、動的型の 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で、要素は 12

  • 0x0000000000000000000000000000000000000000000000000000000000000001 (最初の要素)

  • 0x0000000000000000000000000000000000000000000000000000000000000002 (2番目の要素)

そして、第1のルート配列 [[1, 2], [3]] の第2の埋め込み動的配列 [3] の長さとデータを符号化します。

  • 0x0000000000000000000000000000000000000000000000000000000000000001 (2番目の配列の要素数は1で、要素は 3

  • 0x0000000000000000000000000000000000000000000000000000000000000003 (最初の要素)

次に、それぞれの動的配列 [1, 2][3] に対するオフセット ab を求める必要があります。 このオフセットを計算するために、最初のルート配列 [[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表現)

最初のルート配列と並行して、文字列は動的な要素なので、そのオフセット cde を見つける必要があります。

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"] のオフセット fg を見つけ、正しい順序でパーツを組み立てます。

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_ARGSabi_encode は上述のように関数から型付けされた一連の値を返すために使用されるABIエンコーディング関数)。

長さが最大32バイトのすべての型について、 EVENT_INDEXED_ARGS 配列には、通常のABIエンコーディングと同様に、32バイトにパディングまたは符号拡張された値が直接格納されます。 しかし、すべての "複雑な"型や動的な長さの型(すべての配列、 stringbytes 、構造体を含む)では、 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) と同じ方法でエンコードされます。

エラーセレクタ 0x000000000xffffffff は将来のために予約されています。

警告

エラーデータを信用してはいけません。 デフォルトでは、エラーデータは外部呼び出しの連鎖を通じてバブリングします。 つまり、コントラクトは、直接呼び出すコントラクトのどれにも定義されていないエラーを受け取る可能性があります。 さらに、どのコントラクトも、エラーがどこにも定義されていなくても、エラー署名に一致するデータを返すことで、どんなエラーでも偽装できます。

JSON

コントラクトのインターフェースのJSONフォーマットは、関数、イベント、エラーの記述の配列で与えられます。 関数の記述は、フィールドを持つJSONオブジェクトです。

コンストラクタ、receive関数、fallback関数は nameoutputs を持ちません。 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に含めることには大きな意味があります。 構造は以下のように入れ子になっています。

メンバー nametype 、そして潜在的に 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 の直接引数は、配列(または stringbytes )でない限り、パディングなしでエンコードされます。

  • 配列のエンコーディングは、その要素のエンコーディングとパディングを連結したものです。

  • stringbytesuint[] のような動的なサイズの型は、長さフィールドなしでエンコードされます。

  • stringbytes のエンコーディングでは、配列や構造体の一部でない限り、末尾にパディングが適用されません(その場合、32バイトの倍数にパディングされます)。

一般的には、動的なサイズの要素が2つあると、長さのフィールドがないため、エンコーディングが曖昧になります。

パディングが必要な場合は、明示的な型変換を行うことができます: abi.encodePacked(uint16(0x12)) == hex"0012"

関数を呼び出すときにはpackedエンコーディングは使われないので、関数セレクタの前に付ける特別なサポートはありません。 また、エンコーディングが曖昧なため、デコード関数はありません。

警告

keccak256(abi.encodePacked(a, b)) を使っていて、 ab の両方が動的型の場合、 a の一部を b に移動させたり、逆に b の一部を a に移動させたりすることで、ハッシュ値の衝突を容易に工作できます。 より具体的には abi.encodePacked("a", "bc") == abi.encodePacked("ab", "c") です。 署名や認証、データの整合性のために abi.encodePacked を使用する場合は、常に同じ型を使用し、最大でもどちらかが動的型であることを確認してください。 やむを得ない理由がない限り、 abi.encode を優先すべきです。

インデックスされたイベントパラメータのエンコーディング

値型ではないインデックスされたイベントパラメータ(配列や構造体)は、直接保存されず、エンコーディングのkeccak256ハッシュが保存されます。 このエンコーディングは以下のように定義されています。

  • bytesstring の値のエンコーディングは、パディングや長さのプレフィックスを含まない文字列の内容だけになります。

  • 構造体のエンコーディングは、そのメンバーのエンコーディングを32バイトの倍数にパディングして連結したものです( bytesstring も同様)。

  • 配列のエンコーディング(動的サイズと静的サイズどちらも)は、その要素のエンコーディングを32バイトの倍数にパディングして連結したもので( bytesstring も)、長さのプレフィックスはありません。

上記では、通常と同じく負の数は符号拡張でパディングされ、ゼロパディングされません。 bytesNN 型は右が、 uintNN / intNN 型は左がパディングされます。

警告

構造体に複数の動的サイズの配列が含まれていると、エンコーディングが曖昧になります。 そのため、常にイベントデータを再確認し、インデックス化されたパラメータだけに基づく検索結果に頼らないようにしてください。