应用二进制接口说明

基本设计

以太坊Ethereum 生态系统中, 应用二进制接口Application Binary Interface(ABI) 是从区块链外部与合约进行交互以及合约与合约间进行交互的一种标准方式。 数据会根据其类型按照这份手册中说明的方法进行编码。这种编码并不是可以自描述的,而是需要一种特定的概要(schema)来进行解码。

我们假定合约函数的接口都是强类型的,且在编译时可确定的和静态的;不提供自我检查机制。同时假定在编译时,所有合约要调用的其他合约接口定义都是可用的。

这份手册并不针对那些动态合约接口或者仅在运行时才可获知的合约接口。如果这种场景变得很重要,你可以使用 以太坊Ethereum 生态系统中其他更合适的基础设施来处理它们。

函数选择器Function Selector

一个函数调用数据的前 4 字节,指定了要调用的函数。这就是某个函数签名的 Keccak 哈希的前 4 字节(高位在左的大端序) (译注:这里的“高位在左的大端序“,指最高位字节存储在最低位地址上的一种串行化编码方式,即高位字节在左)。 函数签名被定义为基础原型的规范表达,而基础原型是函数名称加上由括号括起来的参数类型列表,参数类型间由一个逗号分隔开,且没有空格。

注解

函数的返回类型并不是这个签名的一部分。在 Solidity 的函数重载 中,返回值并没有被考虑。这是为了使对函数调用的解析保持上下文无关。 然而 应用二进制接口Application Binary Interface(ABI) 的 JSON 描述中包含了即包含了输入也包含了输出。(参考 JSON ABI)。

参数编码

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

类型编码

以下是基础类型:

  • uint<M>M 位的无符号整数, 0 < M <= 256M % 8 == 0。例如: uint32uint8uint256
  • int<M>:以 2 的补码作为符号的 M 位整数, 0 < M <= 256M % 8 == 0
  • address:除了字面上的意思和语言类型的区别以外,等价于 uint160。在计算和 函数选择器Function Selector 中,通常使用 address
  • uintintuint256int256 各自的同义词。在计算和 函数选择器Function Selector 中,通常使用 uint256int256
  • bool:等价于 uint8,取值限定为 0 或 1 。在计算和 函数选择器Function Selector 中,通常使用 bool
  • fixed<M>x<N>M 位的有符号的固定小数位的十进制数字 8 <= M <= 256M % 8 == 0、且 0 < N <= 80。其值 v 即是 v / (10 ** N)。(也就是说,这种类型是由 M 位的二进制数据所保存的,有 N 位小数的十进制数值。译者注。)
  • ufixed<M>x<N>:无符号的 fixed<M>x<N>
  • fixedufixedfixed128x18ufixed128x18 各自的同义词。在计算和 函数选择器Function Selector 中,通常使用 fixed128x18ufixed128x18
  • bytes<M>M 字节的二进制类型, 0 < M <= 32
  • function:一个地址(20 字节)之后紧跟一个 函数选择器Function Selector (4 字节)。编码之后等价于 bytes24

以下是定长数组类型:

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

    注解

    尽管此ABI规范可以表示零个元素的定长数组,但编译器不支持它们。

以下是非定长类型:

  • bytes:动态大小的字节序列。
  • string:动态大小的 unicode 字符串,通常呈现为 UTF-8 编码。
  • <type>[]:元素为给定类型的变长数组。

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

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

元组tuple 构成 元组tuple、用 元组tuple 构成数组等等也是可能的。另外也可以构成“零元组(zero-tuples)”,就是 n = 0 的情况。

Solidity 到 ABI 类型 映射

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

Solidity ABI
address payable address
contract address
enum uint8
user defined value types its underlying value type
struct tuple

警告

0.8.0 版本之前,枚举(enums) 可以多余 256 个成员并且可以使用最小可保存的整型来保存他们。

编码的设计准则

我们现在来正式讲述编码,它具有如下属性,如果参数是嵌套的数组,这些属性非常有用:

  1. 读取的次数取决于参数数组结构中的最大深度;也就是说,要取得 a_i[k][l][r] 需要读取 4 次。在先前的ABI版本中,在最糟的情况下,读取的次数会随着动态参数的总数而线性地增长。
  2. 变量或数组元素的数据不与其他数据交错(不会被插入其他的数据,),并且它是可以再定位的。它们只会使用相对的“地址”。

编码的形式化说明

我们需要区分静态和动态类型。静态类型会被直接编码,动态类型则会在当前数据块之后单独分配的位置被编码。

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

  • bytes
  • string
  • 任意类型 T 的变长数组 T[]
  • 任意动态类型 T 的定长数组 T[k]k >= 0
  • 由动态的 Ti1 <= i <= k)构成的 元组tuple (T1,...,Tk)

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

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

我们把实际的编码 enc 定义为一个由ABI类型到二进制字符串的值的映射;因而,当且仅当 X 的类型是动态的, len(enc(X)) (即 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)),并且 当 Ti 为静态类型时, headtail 被定义为

    head(X(i)) = enc(X(i)) and tail(X(i)) = "" (空字符串)

    否则,比如 Ti 是动态类型时,它们被定义为

    head(X(i)) = enc(len(head(X(1)) ... head(X(k-1)) tail(X(1)) ... tail(X(i-1)))) tail(X(i)) = enc(X(i))

    注意,在动态类型的情况下,由于 head 部分的长度仅取决于类型而非值,所以 head(X(i)) 是定义明确的。它的值是从 enc(X) 的开头算起的,tail(X(i)) 的起始位在 enc(X) 中的偏移量。

  • T[k] 对于任意 Tk

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

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

  • T[]Xk 个元素( k 被呈现为类型 uint256):

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

    即是说,它就像是个由静态大小 k 的数组那样被编码的,且由元素的个数作为前缀。

  • 具有 k (呈现为类型 uint256)长度的 bytes

    enc(X) = enc(k) pad_right(X),即是说,字节数被编码为 uint256,紧跟着实际的 X 的字节码序列,再在高位(左侧)补上可以使 len(enc(X)) 成为 32 的倍数的最少数量的 0 值字节数据。

  • string

    enc(X) = enc(enc_utf8(X)),即是说, X 被 UFT-8 编码,且在后续编码中将这个值解释为 bytes 类型。注意,在随后的编码中使用的长度是其 UFT-8 编码的字符串的字节数,而不是其字符数。

  • uint<M>enc(X) 是在 X 的大端序编码的高位(左侧)补充若干 0 值字节以使其长度成为 32 字节。

  • address:与 uint160 的情况相同。

  • int<M>enc(X) 是在 X 的大端序的 2 的补码编码的高位(左侧)添加若干字节数据以使其长度成为 32 字节;对于负数,添加值为 0xff (即 8 位全为 1,译者注)的字节数据,对于非负数,添加 0 值(即 8 位全为 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 的倍数。

函数选择器Function Selector 和参数编码

大体而言,一个以 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]) public pure {}
  function baz(uint32 x, bool y) public pure returns (bool r) { r = x > 32 || y; }
  function sam(bytes, bool, uint[]) 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(uint,uint32[],bytes10,bytes) 的调用会通过以下方式进行编码:

取得 sha3("f(uint256,uint32[],bytes10,bytes)") 的前 4 字节,也就是 0x8be65246。 然后我们对所有 4 个参数的头部进行编码。对静态类型 uint256bytes10 是可以直接传过去的值;对于动态类型 uint32[]bytes,我们使用的字节数偏移量是它们的数据区域的起始位置,由需编码的值的开始位置算起(也就是说,不计算包含了函数签名的前 4 字节),这就是:

  • 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 字节)

最后,合并到一起的编码就是(为了清晰,在 函数选择器Function Selector 和每 32 字节之后加了换行):

0x8be65246
  0000000000000000000000000000000000000000000000000000000000000123
  0000000000000000000000000000000000000000000000000000000000000080
  3132333435363738393000000000000000000000000000000000000000000000
  00000000000000000000000000000000000000000000000000000000000000e0
  0000000000000000000000000000000000000000000000000000000000000002
  0000000000000000000000000000000000000000000000000000000000000456
  0000000000000000000000000000000000000000000000000000000000000789
  000000000000000000000000000000000000000000000000000000000000000d
  48656c6c6f2c20776f726c642100000000000000000000000000000000000000

让我们使用相同的原理来对一个签名为 g(uint[][],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] 的偏移量。要计算这个偏移量,我们可以来看一下第一个根数组 [[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[],uint[][]) 所相同的编码。

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

  • 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 的日志/事件监视协议的一个抽象。日志项提供了合约的地址、一系列的主题(最多 4 项)和一些任意长度的二进制数据。为了使用合适的类型数据结构来演绎这些功能(与接口定义一起),事件沿用了既存的 ABI 函数。

给定了事件名称和事件参数之后,我们将其分解为两个子集:已索引的和未索引的。已索引的部分,最多有 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]) ,否则则为 abi_encode(EVENT_INDEXED_ARGS[n])``( ``EVENT_INDEXED_ARGS 是已索引的 EVENT_ARGS);
  • dataabi_serialise(EVENT_NON_INDEXED_ARGS)EVENT_NON_INDEXED_ARGS 是未索引的 EVENT_ARGSabi_serialise 是一个用来从某个函数返回一系列类型值的ABI序列化函数,就像上文所讲的那样)。

对于所有定长的Solidity类型, EVENT_INDEXED_ARGS 数组会直接包含32字节的编码值。然而,对于 动态长度的类型 ,包含 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) 编码一样。 例如为: 0xcf479181, uint256(0), uint256(amount).

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

警告

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

JSON

合约接口的JSON格式是用来描述函数,事件或错误描述的一个数组。一个函数的描述是一个有如下字段的JSON对象:

  • type"function""constructor""fallback"未命名的 “缺省” 函数
  • name:函数名称;
  • inputs:对象数组,每个数组对象会包含:
    • name:参数名称;
    • type:参数的权威类型(详见下文)
    • components:供 元组tuple 类型使用(详见下文)
  • outputs:一个类似于 inputs 的对象数组,如果函数无返回值时可以被省略;
  • payable:如果函数接受 以太币Ether ,为 true;缺省为 false
  • stateMutability:为下列值之一: pure指定为不读取区块链状态), view指定为不修改区块链状态), nonpayable (默认值:不接收 Ether)和 payable (与上文 payable 一样)。

type 可以被省略,缺省为 "function"

构造函数 Constructor 和 fallback 函数没有 nameoutputs。Fallback 函数也没有 inputs

注解

向 non-payable(即不接受 以太币Ether )的函数发送非零值的 以太币Ether 会回退交易。

注解

状态可变性 nonpayable 是默认的,不用显示指定。

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

  • type:总是 "event"
  • name:事件名称;
  • inputs:对象数组,每个数组对象会包含:
    • name:参数名称;
    • type:参数的权威类型(相见下文);
    • components:供 元组tuple 类型使用(详见下文);
    • 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 = 0x12345678901234567890123456789012; }
  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 { 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": []
}]

处理 元组tuple 类型

尽管名称被有意地不作为 ABI 编码的一部分,但将它们包含进JSON来显示给最终用户是非常合理的。其结构会按下列方式进行嵌套:

一个拥有 nametype 和潜在的 components 成员的对象描述了某种类型的变量。 直至到达一个 元组tuple 类型且到那点的存储在 type 属性中的字符串以 tuple 为前缀,也就是说,在 tuple 之后紧跟一个 [] 或有整数 k[k],才能确定一个 元组tuple元组tuple 的组件元素会被存储在成员 components 中,它是一个数组类型,且与顶级对象具有同样的结构,只是在这里不允许已索引的( indexed)数组元素。

作为例子,代码

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 memoryt, 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 解码器目前并不强制执行严格模式,但编码器总是以严格模式创建数据。

非标准打包模式(Non-standard Packed Mode)

通过 abi.encodePacked(), Solidity 支持一种非标准打包模式处理以下情形:

  • 长度低于 32 字节的类型,会直接拼接,既不会进行补 0 操作,也不会进行符号扩展
  • 动态类型会直接进行编码,并且不包含长度信息。
  • 数组元素会填充,但仍旧会就地编码。

例如,对 int1, bytes1, uint16, string 用数值 -1, 0x42, 0x2424, "Hello, world!" 进行编码将生成如下结果

0xff42242448656c6c6f2c20776f726c6421
  ^^                                 int1(-1)
    ^^                               bytes1(0x42)
      ^^^^                           uint16(0x2424)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^ string("Hello, world!") without a length field

更具体地说:

  • 在编码过程中,所有内容均是就地编码,因此在编码中,没有头和尾的区别,而且数组的长度也不会被编码。
  • abi.encodePacked 的参数以不填充的方式编码,只要它们不是数组(或 stringbytes)。
  • 数组的编码是由其元素的编码及其填充(padding)的拼接
  • 动态大小的类型如 string, bytesuint[] 在编码时,不包含长度字段
  • stringbytes 的编码不会在末尾进行填充(padding),除非它是一个数组或结构的一部分(此时会填充为 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

事件索引参数的编码

对于不是值类型的事件索引参数,如:数组和结构,是不直接存储的,而是存储一个keccak256-hash编码。 这个编码被定义如下:

  • bytesstring 的编码只是字符串的内容,没有任何填充或长度前缀。
  • 结构体的编码是其成员编码的拼接,总是填充为32字节的倍数(即便是 bytesstring 类型)。
  • 数组(包含动态和静态大小的数组)的编码是其元素的编码的拼接,总是填充为32字节的倍数(即便是 bytesstring 类型),并且没有长度前缀

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

警告

如果一个结构体包含一个以上的动态大小的数组,那么其编码会模糊有歧义。正因为如此,要经常重新检查事件数据,不能仅仅依靠索引参数的结果。