合约ABI规范

基本设计

合约应用二进制接口(ABI)是在以太坊生态系统中与合约交互的标准方式, 包括从区块链外部和合约间的交互。数据根据其类型进行编码,如本规范中所述。 编码不是自描述的,因此需要一种特定的概要(schema)来进行解码。

我们假设合约的接口函数是强类型的,在编译时就知道,并且是静态的。 我们假设所有合约在编译时都有它们所调用的任何合约的接口定义。

本规范不涉及其接口是动态的或其他只有在运行时才知道的合约。

函数选择器

一个函数调用数据的前四个字节指定了要调用的函数。 它是函数签名的 Keccak-256 哈希值的前4字节(高位在左的大端序)。 签名被定义为基本原型的典型表达,没有数据位置的指定, 也就是带有括号的参数类型列表的函数名。参数类型由一个逗号分割 - 不使用空格。

备注

一个函数的返回类型不是这个签名的一部分。在 Solidity的函数重载 中, 返回类型不被考虑。原因是为了保持函数调用解析与上下文无关。 然而 JSON描述的ABI 却同时包含了输入和输出。

参数编码

从第5字节开始是被编码的参数。这种编码也被用在其他地方, 比如,返回值和事件的参数也会被用同样的方式进行编码, 而用来指定函数的4个字节则不需要再进行编码。

类型

以下是基础类型:

  • uint<M>M 位的无符号整数, 0 < M <= 256M % 8 == 0。例如: uint32uint8uint256

  • int<M>: 以 2 的补码作为符号的 M 位整数, 0 < M <= 256M % 8 == 0

  • address: 除了字面上的意思和语言类型的区别以外,等价于 uint160, 在计算和函数选择器中,通常使用 address

  • uintintuint256int256 各自的同义词. 在计算和函数选择器中,通常使用 uint256int256

  • bool: 等价于 uint8,取值限定为 0 或 1。在计算和函数选择器中,通常使用 bool

  • fixed<M>x<N>M 位的有符号的固定小数位的十进制数字, 8 <= M <= 256M % 8 == 0, 且 0 < N <= 80, 其中值 vv / (10 ** N)

  • ufixed<M>x<N>: 无符号的 fixed<M>x<N>.

  • fixedufixedfixed128x18ufixed128x18 各自的同义词。 在计算和函数选择器中,通常使用 fixed128x18ufixed128x18

  • bytes<M>M 字节的二进制类型, 0 < M <= 32

  • function: 一个地址(20 字节)之后紧跟一个函数选择器 (4 字节)。编码之后等价于 bytes24

以下是定长数组类型:

  • <type>[M]: 有 M 个元素的定长数组, M >= 0,数组元素为给定类型。

    备注

    虽然这个ABI规范可以表达零元素的固定长度数组,但编译器不支持它们。

以下是非定长类型:

  • bytes: 动态大小的字节序列。

  • string: 动态大小的 unicode 字符串,通常呈现为 UTF-8 编码。

  • <type>[]: 元素为给定类型的变长数组。

可以将若干类型放到一对括号中,用逗号分隔开,以此来构成一个元组(tuple):

  • (T1,T2,...,Tn): 由 T1,..., Tnn >= 0 构成的 元组

用元组构成元组,用元组构成数组等等也是可能的。另外也可以构成零元组(当 n == 0 时)。

将Solidity映射到ABI类型

Solidity 支持上面介绍的除了元祖之外的所有同名类型。 另一方面,一些 Solidity 类型不被 ABI 支持。 下表在左栏显示了不属于ABI的Solidity类型,在右栏显示了代表它们的ABI类型。

Solidity

ABI

address payable

address

合约

address

枚举

uint8

用户自定义类型

其基本值类型

结构体

元组(tuple)

警告

0.8.0 版本之前,枚举可以有超过256个成员,并由最小的整数类型表示,其大小刚好可以容纳任何成员的值。

编码的设计标准

编码被设计为具有以下属性,如果一些参数是嵌套的数组,这些属性特别有用:

  1. 访问一个值所需的读取次数最多是参数数组结构内的值的深度, 即需要四次读取次数来检索 a_i[k][l][r]。 在ABI的前一个版本中,在最坏的情况下,读取次数的数量与动态参数的总数成线性比例。

  2. 变量或数组元素的数据不与其他数据交错,它是可重定位的,即它只使用相对的 “地址”。

编码的形式化规范

我们区分了静态和动态类型。静态类型是直接编码的, 而动态类型是在当前块之后的一个单独分配的位置进行编码。

定义: 以下类型被称为“动态”:

  • bytes

  • string

  • 任意类型 T 的数组 T[]

  • 任意动态类型 T 的定长数组 T[k],其中 k >= 0

  • 由动态的 Ti1 <= i <= k )构成的元组 (T1,...,Tk)

所有其他类型都被称为“静态”。

定义: len(a) 是一个二进制字符串 a 的字节长度。 len(a) 的类型被呈现为 uint256

我们把实际的编码 enc 定义为一个由ABI类型到二进制字符串的值的映射, 因而,当且仅当 X 的类型是动态的, len(enc(X)) 才会依赖于 X 的值。

定义: 对任意ABI值 X,我们根据 X 的实际类型递归地定义 enc(X)

  • (T1,...,Tk) 对于 k >= 0 且任意类型 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)) 是定义明确的。 它的值是从 enc(X) 的开始算起的, tail(X(i)) 的起始位在 head(X(i)) 中的偏移量。

  • T[k] 对于任意 Tk

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

    即,它就像是个由相同类型的 k 个元素组成的元组那样被编码的。

  • T[]Xk 个元素 ( 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 的倍数的最少数量的 0 值字节数据。

  • 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 的补码编码的高位(左侧)添加若干字节数据以使其长度成为 32 字节; 对于负数,添加值为 0xff 的字节数据,对于正数,添加 0 值字节数据。

  • bool: 与 uint8 的情况相同, 1 用来表示 true0 表示 false

  • 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 字节而添加的若干 0 值字节。

注意,对于任意的 Xlen(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))

也就是说,返回值会被组合为一个元组(tuple)进行编码。

示例

给定一个合约:

// 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。这源自ASCII格式的 baz(uint32,bool) 签名的 Keccak 哈希的前 4 字节。

  • 0x0000000000000000000000000000000000000000000000000000000000000045: 第一个参数, 一个被用 0 值字节补充到 32 字节的 uint32 值 69

  • 0x0000000000000000000000000000000000000000000000000000000000000001: 第二个参数, 一个被用 0 值字节补充到 32 字节的 boolean 值 true

合起来就是:

0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001

它返回一个 bool。比如它返回 false, 那么它的输出将是一个字节数组 0x0000000000000000000000000000000000000000000000000000000000000000, 一个 bool 值。

如果我们想用 ["abc", "def"] 做参数调用 bar,我们总共需要传送 68 字节,可以分解为:

  • 0xfce353f6: 方法ID。源自 bar(bytes3[2]) 的签名。

  • 0x6162630000000000000000000000000000000000000000000000000000000000: 第一个参数的第一部分, 一个 bytes3"abc" (左对齐)。

  • 0x6465660000000000000000000000000000000000000000000000000000000000: 第一个参数的第二部分, 一个 bytes3"def" (左对齐)。

合起来就是:

0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000

如果我们想用 "dave"true[1,2,3] 作为参数调用 sam, 我们总共需要传送 292 字节,可以分解为:

  • 0xa5643bf2: 方法ID。这是从签名 sam(bytes,bool,uint256[]) 中导出的。注意, uint 被替换为其典型代表 uint256

  • 0x0000000000000000000000000000000000000000000000000000000000000060: 第一个参数(动态类型)的数据部分的位置,即从参数编码块开始位置算起的字节数。在这里,是 0x60

  • 0x0000000000000000000000000000000000000000000000000000000000000001: 第二个参数:boolean 的 true。

  • 0x00000000000000000000000000000000000000000000000000000000000000a0: 第三个参数(动态类型)的数据部分的位置,由字节数计量。在这里,是 0xa0

  • 0x0000000000000000000000000000000000000000000000000000000000000004: 第一个参数的数据部分,以字节数组的元素个数作为开始,在这里,是 4。

  • 0x6461766500000000000000000000000000000000000000000000000000000000: 第一个参数的内容: "dave" 的 UTF-8 编码(在这里等同于 ASCII 编码),并在右侧(低位)用 0 值字节补充到 32 字节。

  • 0x0000000000000000000000000000000000000000000000000000000000000003: 第三个参数的数据部分,以数组的元素个数作为开始,在这里,是 3。

  • 0x0000000000000000000000000000000000000000000000000000000000000001: 第三个参数的第一个数组元素。

  • 0x0000000000000000000000000000000000000000000000000000000000000002: 第三个参数的第二个数组元素。

  • 0x0000000000000000000000000000000000000000000000000000000000000003: 第三个参数的第三个数组元素。

合起来就是:

0xa5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003

动态类型的使用

用值为 (0x123, [0x456, 0x789], "1234567890", "Hello, world!") 的签名参数调用 函数 f(uint256,uint32[],bytes10,bytes),其的编码方式如下:

我们取 keccak("f(uint256,uint32[],bytes10,bytes)") 的前四个字节,即 0x8be65246。 然后我们对所有四个参数的头部部分进行编码。对静态类型 uint256bytes10, 这些是我们要直接传递的值,而对于动态类型 uint32[]bytes, 我们使用其数据区开始的偏移量,从需编码的值的开始位置算起 (即不计算包含函数签名哈希值的前四个字节)。也就是:

  • 0x00000000000000000000000000000000000000000000000000000000000001230x123 补充到 32 字节)

  • 0x0000000000000000000000000000000000000000000000000000000000000080 (第二个参数的数据部分起始位置的偏移量,4*32 字节,正好是头部的大小)

  • 0x3132333435363738393000000000000000000000000000000000000000000000"1234567890" 从右边补充到 32 字节)

  • 0x00000000000000000000000000000000000000000000000000000000000000e0 (第四个参数的数据部分起始位置的偏移量 = 第一个动态参数的数据部分起始位置的偏移量 + 第一个动态参数的数据部分的长度 = 4*32 + 3*32,参考后文)

在此之后,跟着第一个动态参数的数据部分, [0x456, 0x789]

  • 0x0000000000000000000000000000000000000000000000000000000000000002 (数组元素个数,2)

  • 0x0000000000000000000000000000000000000000000000000000000000000456 (第一个数组元素)

  • 0x0000000000000000000000000000000000000000000000000000000000000789 (第二个数组元素)

最后,我们将第二个动态参数的数据部分 "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, 2], [3]] 的第一个嵌入的动态数组 [1, 2] 的长度和数据进行编码:

  • 0x0000000000000000000000000000000000000000000000000000000000000002 (第一个数组中的元素数量 2;元素本身是 12)

  • 0x0000000000000000000000000000000000000000000000000000000000000001 (第一个元素)

  • 0x0000000000000000000000000000000000000000000000000000000000000002 (第二个元素)

然后我们对第一个根数组 [[1, 2], [3]] 的第二个嵌入式动态数组 [3] 的长度和数据进行编码:

  • 0x0000000000000000000000000000000000000000000000000000000000000001 (第二个数组中的元素数量 1;元素数据是 3)

  • 0x0000000000000000000000000000000000000000000000000000000000000003 (第一个元素)

然后我们需要为各自的动态数组 [1, 2][3] 找到偏移量 ab。 为了计算偏移量,我们可以看一下第一个根数组的编码数据 [[1, 2], [3]] 在编码中枚举每一行。

0 - a                                                                - [1, 2] 的偏移量
1 - b                                                                - [3] 的偏移量
2 - 0000000000000000000000000000000000000000000000000000000000000002 - [1, 2] 数组的计数
3 - 0000000000000000000000000000000000000000000000000000000000000001 - 1 的编码
4 - 0000000000000000000000000000000000000000000000000000000000000002 - 2 的编码
5 - 0000000000000000000000000000000000000000000000000000000000000001 - [3] 数组的计数
6 - 0000000000000000000000000000000000000000000000000000000000000003 - 3 的编码

偏移量 a 指向数组 [1, 2] 内容的开始位置,即第 2 行的开始(64 字节); 所以 a = 0x0000000000000000000000000000000000000000000000000000000000000040

偏移量 b 指向数组 [3] 内容的开始位置,即第 5 行的开始(160 字节); 所以 b = 0x00000000000000000000000000000000000000000000000000000000000000a0

然后我们对第二个根数组的嵌入字符串进行编码:

  • 0x0000000000000000000000000000000000000000000000000000000000000003 (单词 "one" 中的字符个数)

  • 0x6f6e650000000000000000000000000000000000000000000000000000000000 (单词 "one" 的 utf8 编码)

  • 0x0000000000000000000000000000000000000000000000000000000000000003 (单词 "two" 中的字符个数)

  • 0x74776f0000000000000000000000000000000000000000000000000000000000 (单词 "two" 的 utf8 编码)

  • 0x0000000000000000000000000000000000000000000000000000000000000005 (单词 "three" 中的字符个数)

  • 0x7468726565000000000000000000000000000000000000000000000000000000 (单词 "three" 的 utf8 编码)

作为与第一个根数组的并列,因为字符串也属于动态元素,我们也需要找到它们的偏移量 c, de

0 - c                                                                - "one" 的偏移量
1 - d                                                                - "two" 的偏移量
2 - e                                                                - "three" 的偏移量
3 - 0000000000000000000000000000000000000000000000000000000000000003 - "one" 的字符计数
4 - 6f6e650000000000000000000000000000000000000000000000000000000000 - "one" 的编码
5 - 0000000000000000000000000000000000000000000000000000000000000003 - "two" 的字符计数
6 - 74776f0000000000000000000000000000000000000000000000000000000000 - "two" 的编码
7 - 0000000000000000000000000000000000000000000000000000000000000005 - "three" 的字符计数
8 - 7468726565000000000000000000000000000000000000000000000000000000 - "three" 的编码

偏移量 c 指向字符串 "one" 内容的开始位置,即第 3 行的开始(96 字节); 所以 c = 0x0000000000000000000000000000000000000000000000000000000000000060

偏移量 d 指向字符串 "two" 内容的开始位置,即第 5 行的开始(160 字节); 所以 d = 0x00000000000000000000000000000000000000000000000000000000000000a0

偏移量 e 指向字符串 "three" 内容的开始位置,即第 7 行的开始(224 字节); 所以 e = 0x00000000000000000000000000000000000000000000000000000000000000e0

注意,根数组的嵌入元素的编码并不互相依赖,且具有对于函数签名 g(string[],uint256[][]) 所相同的编码。

然后我们对第一个根数组的长度进行编码:

  • 0x0000000000000000000000000000000000000000000000000000000000000002 (第一个根数组的元素数量 2;这些元素本身是 [1, 2][3])

而后我们对第二个根数组的长度进行编码:

  • 0x0000000000000000000000000000000000000000000000000000000000000003 (第二个根数组的元素数量 3;这些字符串本身是 "one""two""three")

最后,我们找到根动态数组元素 [[1, 2], [3]]["one", "two", "three"] 的偏移量 fg。 汇编数据的正确顺序如下:

0x2289b18c                                                            - 函数签名
 0 - f                                                                - [[1, 2], [3]] 的偏移量
 1 - g                                                                - ["one", "two", "three"] 的偏移量
 2 - 0000000000000000000000000000000000000000000000000000000000000002 - [[1, 2], [3]] 的元素计数
 3 - 0000000000000000000000000000000000000000000000000000000000000040 - [1, 2] 的偏移量
 4 - 00000000000000000000000000000000000000000000000000000000000000a0 - [3] 的偏移量
 5 - 0000000000000000000000000000000000000000000000000000000000000002 - [1, 2] 的元素计数
 6 - 0000000000000000000000000000000000000000000000000000000000000001 - 1 的编码
 7 - 0000000000000000000000000000000000000000000000000000000000000002 - 2 的编码
 8 - 0000000000000000000000000000000000000000000000000000000000000001 - [3] 的元素计数
 9 - 0000000000000000000000000000000000000000000000000000000000000003 - 3 的编码
10 - 0000000000000000000000000000000000000000000000000000000000000003 - ["one", "two", "three"] 的元素计数
11 - 0000000000000000000000000000000000000000000000000000000000000060 - 的偏移量"one"
12 - 00000000000000000000000000000000000000000000000000000000000000a0 - 的偏移量"two"
13 - 00000000000000000000000000000000000000000000000000000000000000e0 - 的偏移量"three"
14 - 0000000000000000000000000000000000000000000000000000000000000003 - "one" 的字符计数
15 - 6f6e650000000000000000000000000000000000000000000000000000000000 - "one" 的编码
16 - 0000000000000000000000000000000000000000000000000000000000000003 - "two" 的字符计数
17 - 74776f0000000000000000000000000000000000000000000000000000000000 - "two" 的编码
18 - 0000000000000000000000000000000000000000000000000000000000000005 - "three" 的字符计数
19 - 7468726565000000000000000000000000000000000000000000000000000000 - "three" 的编码

偏移量 f 指向数组 [[1, 2], [3]] 内容的开始位置,即第 2 行的开始(64 字节); 所以 f = 0x0000000000000000000000000000000000000000000000000000000000000040

偏移量 g 指向数组 ["one", "two", "three"] 内容的开始位置,即第 10 行的开始(320 字节); 所以 g = 0x0000000000000000000000000000000000000000000000000000000000000140

事件

事件是Ethereum日志/事件观察协议的一个抽象。日志条目提供了合约的地址, 一系列最多四个主题和一些任意长度的二进制数据。 事件利用现有的函数ABI,以便将其(连同接口规范)解释为一个正确的类型化结构。

给定一个事件名称和一系列的事件参数,我们把它们分成两个子系列:那些有索引的和那些没有索引的。 那些被索引的参数,可能多达3个(对于非匿名事件)或4个(对于匿名事件), 与事件签名的Keccak散列一起使用,形成日志条目的主题。 那些没有索引的则构成事件的字节数组。

实际上,使用该ABI的日志条目被描述为:

  • address: 合约的地址(由以太坊真正提供);

  • topics[0]keccak(EVENT_NAME+"("+EVENT_ARGS.map(canonical_type_of).join(",")+")") canonical_type_of 是一个可以返回给定参数的权威类型的函数,例如,对 uint indexed foo 它会返回 uint256)。 如果事件被声明为 anonymous,那么 topics[0] 不会被生成;

  • topics[n]: 如果事件没有被声明为 anonymous, 则为 abi_encode(EVENT_INDEXED_ARGS[n - 1]) 或者如果它被声明为该类型,则为 abi_encode(EVENT_INDEXED_ARGS[n])EVENT_INDEXED_ARGS 是被索引的 EVENT_ARGS 的系列);

  • dataEVENT_NON_INDEXED_ARGS 的ABI编码 ( EVENT_NON_INDEXED_ARGS 是一系列没有索引的 EVENT_ARGSabi_encode 是ABI编码函数, 用于从一个函数返回一系列类型的值,如上所述)。

对于所有长度不超过32字节的类型, EVENT_INDEXED_ARGS 数组直接包含数值,填充或符号扩展(对于有符号整数)到32字节, 就像常规ABI编码一样。然而,对于所有 “复杂” 类型或动态长度的类型,包括所有数组, stringbytes 和结构, EVENT_INDEXED_ARGS 将包含 Keccak散列 的特殊就地编码值(见 索引事件参数的编码), 而不是直接编码的值。这允许应用程序有效地查询动态长度类型的值(通过设置编码值的哈希值作为主题), 但使应用程序无法解码他们没有查询到的索引值。对于动态长度类型, 应用程序开发人员面临着对预定值的快速搜索(如果参数有索引)和任意值的可读性之间的权衡(这要求参数不被索引)。 开发者可以通过定义具有两个参数的事件 -- 一个是索引的,一个是不索引的 -- 来克服这种权衡,实现高效搜索和任意可读性。

错误

在合约内部发生故障的情况下,合约可以使用一个特殊的操作码来中止执行,并恢复所有的状态变化。 除了这些效果之外,描述性数据可以返回给调用者。 这种描述性数据是对一个错误及其参数的编码,其方式与函数调用的数据相同。

作为一个例子,让我们考虑以下合约,它的 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(0, amount) 对函数 InsufficientBalance(uint256,uint256) 的调用方式相同。 即 0xcf479181uint256(0)uint256(amount)

错误选择器 0x000000000xffffffff 是保留给将来使用的。

警告

永远不要相信错误数据。 默认情况下,错误数据通过外部调用在链向上冒泡产生, 这意味着一个合约可能会收到一个它直接调用的任何合约中没有定义的错误。 此外,任何合约都可以通过返回与错误签名相匹配的数据来伪造任何错误,即使该错误没有在任何地方定义。

JSON

合约接口的JSON格式是由一个函数,事件和错误描述的数组给出的。 一个函数描述是一个带有字段的JSON对象:

  • type"function""constructor""receive""接收以太币" 函数 ) 或者 "fallback""默认" 函数);

  • name: 函数名称;

  • inputs: 数组对象,每个数组对象会包含:

    • name: 参数名称;

    • type: 参数的权威类型(详见下文)

    • components: 供元组(tuple) 类型使用(详见下文)

  • outputs: 一个类似于 inputs 的数组对象。

  • stateMutability: 为下列值之一: pure指定为不读取区块链状态), view指定为不修改区块链状态), nonpayable (函数不接受以太币 - 默认选项) 和 payable (函数可接收以太币)。

构造函数(constructor), receive 函数 和 fallback 函数没有 nameoutputs 属性。 receive 函数 和 fallback 函数也没有 inputs 属性。

备注

向不接收以太币函数发送非零的以太币将使交易回滚。

备注

在Solidity中,状态可变性 不可支付 是完全不指定状态可变性时的修饰语。

一个事件描述是一个有极其相似字段的 JSON 对象:

  • type: 总是 "event"

  • name: 事件名称;

  • inputs: 对象数组,每个数组对象会包含:

    • name: 参数名称。

    • type: 参数的规范类型(详见下文)。

    • components: 供元组(tuple) 类型使用(详见下文)

    • indexed: 如果该字段是日志主题的一部分,则为 true,如果它是日志数据段之一,则为 false

  • anonymous: 如果事件被声明为 anonymous,则为 true

错误消息如下:

  • type: 总是 "error"

  • name: 错误名称;

  • inputs: 对象数组,每个数组对象会包含:

    • name: 参数名称。

    • type: 参数的权威类型(相见下文)。

    • components: 供元组(tuple) 类型使用(详见下文)。

备注

在 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 为前缀, 也就是说,在 tuple 之后紧跟一个 [] 或有整数 k[k],才 能确定一个元组。 元组的组件元素会被存储在成员 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 解码器目前并不强制执行严格模式,但编码器总是以严格模式创建数据。

非标准打包模式

通过 abi.encodePacked(),Solidity支持一种非标准的打包模式,其中:

  • 短于32字节的类型直接连接,没有填充或符号扩展。

  • 动态类型是直接编码的,没有长度。

  • 数组元素被填充,但仍被是直接编码

此外,不支持结构以及嵌套数组。

例如,对 int16(-1), bytes1(0x42), uint16(0x03), string("Hello, world!") 进行编码将生成如下结果

0xffff42000348656c6c6f2c20776f726c6421
  ^^^^                                 int16(-1)
      ^^                               bytes1(0x42)
        ^^^^                           uint16(0x03)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^ 字符串("Hello, world!") 没有长度字段

更具体地说:

  • 在编码过程中,所有东西都是直接编码的。这意味着没有像ABI编码那样区分头和尾,也没有对数组的长度进行编码。

  • abi.encodePacked 的直接参数被编码, 只要不是数组(或 stringbytes ),就不需要填充。

  • 一个数组的编码是其元素的编码 填充的连接。

  • 动态大小的类型,如 stringbytesuint[],在编码时没有长度字段。

  • stringbytes 的编码不会在末尾应用填充, 除非它是数组或结构体的一部分(然后它被填充为32字节的倍数)。

一般来说,只要有两个动态大小的元素,编码就会模糊不清,因为缺少长度字段。

如果需要填充,可以使用明确的类型转换: abi.encodePacked(uint16(0x12)) == hex"0012"

由于在调用函数时不使用打包编码,所以没有特别支持预留函数选择器。 由于编码是模糊的,所以没有解码功能。

警告

如果使用 keccak256(abi.encodePacked(a,b)) 并且 ab 都是动态类型, 那么通过将 a 的部分移动到 b 中,很容易在哈希值中产生冲突,反之亦然。 更具体地说, abi.encodePacked("a", "bc") == abi.encodePacked("ab", "c")。 如果你使用 abi.encodePacked 进行签名、认证或数据完整性,确保总是使用相同的类型, 并检查其中最多一个是动态的。除非有令人信服的理由,否则应首选 abi.encode

索引事件参数的编码

不属于值类型的索引事件参数,即数组和结构,不直接存储, 而是存储一个编码的 Keccak-256 哈希值。这个编码的定义如下:

  • bytesstring 值的编码只是字符串的内容,没有任何填充或长度前缀。

  • 结构的编码是其成员编码的串联,总是填充为32字节的倍数(甚至是 bytesstring)。

  • 数组的编码(包括动态和静态大小)是其元素编码的连接, 总是填充为32字节的倍数(甚至是 bytesstring),没有任何长度前缀。

在上面,像往常一样,一个负数被填充符号扩展,而不是零填充。 bytesNN 类型被填充在右边,而 uintNN / intNN 被填充在左边。

警告

如果一个结构包含一个以上的动态大小的数组,那么它的编码是不明确的。 正因为如此,要经常重新检查事件数据,不要只依赖基于索引参数的搜索结果。