合约 ABI 规范
基本设计
合约应用二进制接口 (ABI) 是以太坊生态系统中与合约交互的标准方式,无论是从区块链外部还是合约之间的交互。数据根据其类型进行编码,如本规范中所述。编码本身不包含描述信息,因此需要模式才能解码。
我们假设合约的接口函数是强类型的,在编译时已知且静态。我们假设所有合约在编译时都拥有它们调用的任何合约的接口定义。
本规范不涉及接口在运行时动态或仅在运行时已知的合约。
函数选择器
函数调用的调用数据的前四个字节指定要调用的函数。它是函数签名的 Keccak-256 哈希的前(左侧,高位在 big-endian)四个字节。签名定义为基本原型(不带数据位置说明符)的规范表达式,即带括号的参数类型列表的函数名称。参数类型用单个逗号分隔 - 不使用空格。
注意
函数的返回值不是此签名的组成部分。在 Solidity 的函数重载 中,返回值不被考虑。这样做的原因是保持函数调用解析与上下文无关。然而,ABI 的 JSON 描述 包含输入和输出。
参数编码
从第五个字节开始,编码后的参数紧随其后。这种编码也在其他地方使用,例如返回值和事件参数也以相同的方式进行编码,没有指定函数的四个字节。
类型
存在以下基本类型
uint<M>:M位的无符号整数类型,0 < M <= 256,M % 8 == 0。例如uint32,uint8,uint256。int<M>:M位的二进制补码有符号整数类型,0 < M <= 256,M % 8 == 0。address:等效于uint160,除了假定的解释和语言类型。在计算函数选择器时,使用address。uint,int:分别等效于uint256,int256。在计算函数选择器时,必须使用uint256和int256。bool:等效于uint8,限制为值 0 和 1。在计算函数选择器时,使用bool。fixed<M>x<N>:M位的有符号定点小数,8 <= M <= 256,M % 8 == 0,以及0 < N <= 80,表示值为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 规范可以表达具有零个元素的固定长度数组,但编译器不支持它们。
存在以下非固定大小类型
bytes:动态大小的字节序列。string:假定为 UTF-8 编码的动态大小的 Unicode 字符串。<type>[]:给定类型的元素的可变长度数组。
类型可以通过将它们括在括号中并用逗号分隔来组合成元组
(T1,T2,...,Tn):包含类型T1、…、Tn的元组,n >= 0
可以形成元组的元组、数组的元组等等。还可以形成零元组(其中 n == 0)。
将 Solidity 映射到 ABI 类型
Solidity 支持所有上面介绍的类型,名称相同,除了元组。另一方面,ABI 不支持某些 Solidity 类型。下表左侧显示了不是 ABI 部分的 Solidity 类型,右侧显示了代表它们的 ABI 类型。
Solidity |
ABI |
|---|---|
|
|
|
|
|
|
其底层值类型 |
|
|
警告
在版本 0.8.0 之前,枚举可以具有超过 256 个成员,并且由足够大的最小整数类型表示以容纳任何成员的值。
编码的设计标准
编码旨在具有以下属性,这些属性在某些参数是嵌套数组时特别有用
访问某个值所需的读取次数最多为该值在参数数组结构中的深度,即检索
a_i[k][l][r]需要四次读取。在 ABI 的先前版本中,在最坏情况下,读取次数与动态参数的总数成线性关系。变量或数组元素的数据不会与其他数据交织,并且它是可重新定位的,即它只使用相对“地址”。
编码的正式规范
我们区分静态类型和动态类型。静态类型在原地编码,动态类型在当前块后的单独分配的位置进行编码。
**定义:** 以下类型称为“动态”
bytesstringT[]对于任何TT[k]对于任何动态T和任何k >= 0(T1,...,Tk)如果Ti对某些1 <= i <= k为动态
所有其他类型被称为“静态”。
定义: len(a) 是二进制字符串 a 中的字节数。 假设 len(a) 的类型为 uint256。
我们将 enc(实际编码)定义为将 ABI 类型的值映射到二进制字符串的映射,使得 len(enc(X)) 依赖于 X 的值,当且仅当 X 的类型是动态的。
定义: 对于任何 ABI 值 X,我们递归地定义 enc(X),取决于 X 的类型:
(T1,...,Tk)对于k >= 0和任何类型T1,…,Tkenc(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(X(i))是定义良好的,因为头部部分的长度只取决于类型,而不依赖于值。head(X(i))的值是tail(X(i))开始相对于enc(X)开始的偏移量。T[k]对于任何T和kenc(X) = enc((X[0], ..., X[k-1]))即它被编码为好像它是具有
k个相同类型元素的元组。T[]其中X有k个元素(假设k的类型为uint256)enc(X) = enc(k) enc((X[0], ..., X[k-1]))即它被编码为好像它是具有
k个相同类型元素的元组(或静态大小为k的数组),并以元素数量作为前缀。bytes,长度为k(假设其类型为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的大端编码,在高位(左侧)用零字节填充,使得长度为 32 个字节。address:与uint160情况相同int<M>:enc(X)是X的大端二进制补码编码,对于负数X在高位(左侧)用0xff字节填充,对于非负数X用零字节填充,使得长度为 32 个字节。bool:与uint8情况相同,其中1用于true,0用于falsefixed<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 的倍数。
函数选择器和参数编码
总而言之,对函数 f 的调用,其参数为 a_1, ..., a_n,被编码为
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 示例,如果我们想要用参数 ["abc", "def"] 调用 bar,我们将传递总共 68 个字节,分解如下
0xfce353f6:方法 ID。 这是从签名bar(bytes3[2])中推导出来的。0x6162630000000000000000000000000000000000000000000000000000000000:第一个参数的第一部分,一个bytes3值"abc"(左对齐)。0x6465660000000000000000000000000000000000000000000000000000000000:第一个参数的第二部分,一个bytes3值"def"(左对齐)。
总共
0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000
如果我们想要用参数 69 和 true 调用 baz,我们将传递总共 68 个字节,可以分解如下
0xcdcd77c0:方法 ID。 这是从签名baz(uint32,bool)的 ASCII 形式的 Keccak 哈希的前 4 个字节推导出来的。0x0000000000000000000000000000000000000000000000000000000000000045:第一个参数,一个 uint32 值69,填充到 32 个字节0x0000000000000000000000000000000000000000000000000000000000000001:第二个参数 - 布尔值true,填充到 32 个字节
总共
0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001
它返回一个单独的 bool。 例如,如果它要返回 false,它的输出将是单个字节数组 0x0000000000000000000000000000000000000000000000000000000000000000,一个单独的布尔值。
如果我们想要用参数 "dave",true 和 [1,2,3] 调用 sam,我们将传递总共 292 个字节,分解如下
0xa5643bf2: 方法 ID。它源于签名sam(bytes,bool,uint256[])。注意,uint被替换为它的规范表示uint256。0x0000000000000000000000000000000000000000000000000000000000000060: 第一个参数(动态类型)的数据部分的地址,以字节为单位,从参数块的开始处测量。在本例中为0x60。0x0000000000000000000000000000000000000000000000000000000000000001: 第二个参数:布尔值 true。0x00000000000000000000000000000000000000000000000000000000000000a0: 第三个参数(动态类型)的数据部分的地址,以字节为单位。在本例中为0xa0。0x0000000000000000000000000000000000000000000000000000000000000004: 第一个参数的数据部分,它以字节数组的长度(以元素为单位)开头,在本例中为 4。0x6461766500000000000000000000000000000000000000000000000000000000: 第一个参数的内容:"dave"的 UTF-8 编码(在本例中等于 ASCII),在右侧填充到 32 个字节。0x0000000000000000000000000000000000000000000000000000000000000003: 第三个参数的数据部分,它以数组的长度(以元素为单位)开头,在本例中为 3。0x0000000000000000000000000000000000000000000000000000000000000001: 第三个参数的第一个条目。0x0000000000000000000000000000000000000000000000000000000000000002: 第三个参数的第二个条目。0x0000000000000000000000000000000000000000000000000000000000000003: 第三个参数的第三个条目。
总共
0xa5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003
动态类型的使用
对具有签名 f(uint256,uint32[],bytes10,bytes) 的函数进行调用,其值为 (0x123, [0x456, 0x789], "1234567890", "Hello, world!"),以以下方式编码。
我们取 keccak("f(uint256,uint32[],bytes10,bytes)") 的前四个字节,即 0x8be65246。然后我们编码所有四个参数的头部部分。对于静态类型 uint256 和 bytes10,它们直接是我们想要传递的值,而对于动态类型 uint32[] 和 bytes,我们使用它们的数据区域开始处的偏移量(以字节为单位),从值编码的开始处测量(即不计算包含函数签名哈希的前四个字节)。这些是
0x0000000000000000000000000000000000000000000000000000000000000123(0x123填充到 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;元素本身是1和2)0x0000000000000000000000000000000000000000000000000000000000000001(第一个元素)0x0000000000000000000000000000000000000000000000000000000000000002(第二个元素)
然后我们编码第一个根数组 [[1, 2], [3]] 的第二个嵌入式动态数组 [3] 的长度和数据。
0x0000000000000000000000000000000000000000000000000000000000000001(第二个数组中的元素数量,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 指向数组 [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、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 指向字符串 "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"] 的偏移量 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 指向数组 [[1, 2], [3]] 内容的起始位置,该内容位于第 2 行(64 字节);因此 f = 0x0000000000000000000000000000000000000000000000000000000000000040。
偏移量 g 指向数组 ["one", "two", "three"] 内容的起始位置,该内容位于第 10 行(320 字节);因此 g = 0x0000000000000000000000000000000000000000000000000000000000000140。
事件
事件是 Ethereum 日志/事件监视协议的抽象。日志条目包含合约地址、最多四个主题和一些任意长度的二进制数据。事件利用现有的函数 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]: 如果事件未声明为anonymous,则为abi_encode(EVENT_INDEXED_ARGS[n - 1]),否则为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 数组直接包含该值,并进行填充或符号扩展(对于有符号整数)至 32 字节,与常规 ABI 编码相同。但是,对于所有“复杂”类型或动态长度类型,包括所有数组、string、bytes 和结构体,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(uint256,uint256) 的函数调用 InsufficientBalance(0, amount) 相同的方式进行编码,即 0xcf479181、uint256(0)、uint256(amount)。
错误选择器 0x00000000 和 0xffffffff 预留供将来使用。
警告
切勿信任错误数据。错误数据默认情况下会通过外部调用链向上冒泡,这意味着一个合约可能会收到未在它直接调用的任何合约中定义的错误。此外,任何合约都可以通过返回与错误签名匹配的数据来伪造任何错误,即使错误在任何地方都没有定义。
JSON
合约接口的 JSON 格式由函数、事件和错误描述的数组给出。函数描述是一个 JSON 对象,包含以下字段
type:"function"、"constructor"、"receive"(“接收 Ether” 函数)或"fallback"(“默认” 函数);name: 函数的名称;inputs: 对象数组,每个对象包含name: 参数的名称。type: 参数的规范类型(详情见下文)。components: 用于元组类型(详情见下文)。
outputs: 与inputs类似的对象数组。stateMutability: 包含以下值的字符串之一:pure(指定为不读取区块链状态)、view(指定为不修改区块链状态)、nonpayable(函数不接受 Ether - 默认值)和payable(函数接受 Ether)。
构造函数、接收函数和回退函数没有 name 或 outputs。接收函数和回退函数也没有 inputs。
注意
向非可支付函数发送非零 Ether 将回滚交易。
注意
状态可变性 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 的对象描述了一个类型化的变量。规范类型将一直确定到遇到元组类型为止,并且到那时为止的字符串描述将存储在 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)
^^^^^^^^^^^^^^^^^^^^^^^^^^ string("Hello, world!") without a length field
更具体地说
在编码过程中,所有内容都在原地编码。这意味着没有头部和尾部之间的区别,如 ABI 编码中那样,并且不编码数组的长度。
只要不是数组(或
string或bytes),abi.encodePacked的直接参数将不进行填充编码。数组的编码是其元素编码的连接,并进行填充。
动态大小类型,如
string、bytes或uint[],不包含其长度字段。除非是数组或结构体的一部分(在这种情况下,将填充到 32 字节的倍数),
string或bytes的编码在末尾不进行填充。
通常,由于缺少长度字段,一旦存在两个动态大小元素,编码就会变得模棱两可。
如果需要填充,可以使用显式类型转换:abi.encodePacked(uint16(0x12)) == hex"0012"。
由于在调用函数时不使用打包编码,因此没有针对预置函数选择器的特殊支持。由于编码是模棱两可的,因此没有解码函数。
警告
如果您使用 keccak256(abi.encodePacked(a, b)) 并且 a 和 b 都是动态类型,则可以通过将 a 的部分移到 b 中,反之亦然,轻松在哈希值中制造冲突。更具体地说,abi.encodePacked("a", "bc") == abi.encodePacked("ab", "c")。如果您使用 abi.encodePacked 用于签名、身份验证或数据完整性,请确保始终使用相同的类型,并检查最多只有一个类型是动态的。除非有充分的理由,否则应优先使用 abi.encode。
索引事件参数的编码
索引事件参数,如果是非值类型,例如数组和结构体,则不会直接存储,而是存储其编码的 Keccak-256 哈希值。这种编码定义如下
bytes和string值的编码只是字符串内容,不进行任何填充或长度前缀。结构体的编码是其成员编码的连接,始终填充到 32 字节的倍数(即使是
bytes和string)。数组的编码(动态大小和静态大小)是其元素编码的连接,始终填充到 32 字节的倍数(即使是
bytes和string),不包含任何长度前缀。
如上所述,负数通常通过符号扩展填充,而不是零填充。 bytesNN 类型在右侧填充,而 uintNN / intNN 在左侧填充。
警告
如果结构体包含多个动态大小数组,则其编码是模棱两可的。因此,始终重新检查事件数据,不要仅仅依靠索引参数的搜索结果。