应用二进制接口说明
基本设计
合约应用程序二进制接口(ABI)是与以太坊生态系统中的合约进行交互的标准方式,包括区块链外部的交互和合约之间的交互。 数据根据其类型进行编码,如本规范所述。编码不是自描述的,因此需要一个模式来解码。
我们假设合约的接口函数是强类型的,在编译时已知且是静态的。我们假设所有合约在编译时都能获得它们调用的任何合约的接口定义。
本规范不涉及接口动态或仅在运行时已知的合约。此外,库的 ABI 规范是 略有不同。
函数选择器
函数调用的前四个字节的调用数据指定要调用的函数。它是函数签名的 Keccak-256 哈希的前四个字节(左侧,高位,采用大端格式)。 签名被定义为基本原型的规范表达,不带数据位置说明符,即函数名称后跟参数类型的括号列表。参数类型用单个逗号分隔——不使用空格。
备注
函数的返回类型不是此签名的一部分。在 Solidity 的函数重载 中不考虑返回类型。 原因是保持函数调用解析与上下文无关。 然而,ABI 的 JSON 描述 包含了输入和输出。
参数编码
从第五个字节开始,编码的参数紧随其后。 此编码在其他地方也使用,例如返回值和事件参数也以相同的方式编码,而用来指定函数的4个字节则不需要再进行编码。
类型
请注意,库 ABI 可以采用不同于以下的类型,例如非存储结构。 有关详细信息,请参见 库选择器。
以下是基础类型:
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 支持上述所有类型,名称相同,除了元组。 另一方面,一些 Solidity 类型不被 ABI 支持。下表左列显示不属于 ABI 的 Solidity 类型,右列显示表示它们的 ABI 类型。
Solidity |
ABI |
---|---|
|
|
|
|
|
|
its underlying value type |
|
|
警告
在版本 0.8.0
之前,枚举可以有超过 256 个成员,并由足够大的最小整数类型表示,以容纳任何成员的值。
编码的设计准则
编码旨在具有以下属性,如果参数是嵌套的数组,这些属性非常有用:
访问值所需的读取次数最多为值在参数数组结构中的深度,即检索
a_i[k][l][r]
需要四次读取。在 ABI 的早期版本中,在最坏的情况下,读取的次数会随着动态参数的总数而线性地增长。变量或数组元素的数据不会与其他数据交错,并且是可重定位的,即它仅使用相对“地址”。
编码的正式规范
我们区分静态类型和动态类型。静态类型会被直接编码,动态类型在当前块之后的单独分配位置被编码。 定义: 以下类型称为“动态”:
bytes
string
T[]
对于任何类型T
T[k]
对于任何动态类型T
和任何k >= 0
由动态的
Ti
(1 <= i <= k
)构成的 元组(T1,...,Tk)
所有其他类型都被称为“静态”。
定义: len(a)
是二进制字符串 a
中的字节数。
len(a)
的类型假定为 uint256
。
我们定义 enc
,实际编码,作为 ABI 类型值到二进制字符串的映射,使得 len(enc(X))
仅在 X
的类型为动态时依赖于 X
的值。
定义: 对于任何 ABI 值 X
,我们递归定义 enc(X)
,依赖于 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))
并且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
和k
:enc(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
的大端二进制补码编码,左侧用0xff
字节填充负X
,用零字节填充非负X
,使得长度为 32 字节。bool
:与uint8
情况相同,其中1
用于true
,0
用于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 字节的长度。
注意对于任何 X
,len(enc(X))
是 32 的倍数。
函数选择器和参数编码
总的来说,调用函数 f
,参数为 a_1, ..., a_n
被编码为
function_selector(f) enc((a_1, ..., a_n))
而返回值 v_1, ..., v_k
的 f
被编码为
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
:第一个参数的第一部分,一个bytes3
值"abc"
(左对齐)。0x6465660000000000000000000000000000000000000000000000000000000000
:第一个参数的第二部分,一个bytes3
值"def"
(左对齐)。
总共:
0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000
如果我们想调用 baz
,参数为 69
和 true
,我们将传递总共 68 字节,可以分解为:
0xcdcd77c0
:方法 ID。这是从签名baz(uint32,bool)
的 ASCII 形式的 Keccak 哈希的前 4 字节派生的。0x0000000000000000000000000000000000000000000000000000000000000045
:第一个参数,一个 uint32 值69
,填充至 32 字节0x0000000000000000000000000000000000000000000000000000000000000001
:第二个参数 - 布尔值true
,填充至 32 字节
总共:
0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001
它返回一个单一的 bool
。例如,如果它返回 false
,其输出将是单个字节数组 0x0000000000000000000000000000000000000000000000000000000000000000
,一个单一的布尔值。
如果我们想调用 sam
,参数为 "dave"
, true
和 [1,2,3]
,我们将传递总共 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 - [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"
的 UTF-8 表示)0x0000000000000000000000000000000000000000000000000000000000000003
(单词"two"
的字符数量)0x74776f0000000000000000000000000000000000000000000000000000000000
(单词"two"
的 UTF-8 表示)0x0000000000000000000000000000000000000000000000000000000000000005
(单词"three"
的字符数量)0x7468726565000000000000000000000000000000000000000000000000000000
(单词"three"
的 UTF-8 表示)
与第一个根数组并行,由于字符串是动态元素,我们需要找到它们的偏移量 c
、d
和 e
:
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"]
的偏移量 f
和 g
,并按正确顺序组装部分:
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
。
事件
事件是以太坊日志/事件监视协议的抽象。日志条目提供合约的地址、一系列最多四个主题和一些任意长度的二进制数据。事件利用现有的函数 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])
,如果声明为anonymous
,则为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
函数总是以自定义错误 “insufficient balance” 进行回退:
// 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)
。
错误选择器 0x00000000
和 0xffffffff
被保留供将来使用。
警告
永远不要信任错误数据。 错误数据默认会通过外部调用链向上冒泡,这意味着一个合约可能会收到在它直接调用的任何合约中未定义的错误。 此外,任何合约都可以通过返回与错误签名匹配的数据来伪造任何错误,即使该错误在任何地方都未定义。
JSON
合约接口的 JSON 格式由函数、事件和错误描述的数组给出。 函数描述是一个具有以下字段的 JSON 对象:
type
:"function"
,"constructor"
,"receive"
(”receive Ether” function)或"fallback"
(”default” function);name
: 函数的名称;inputs
: 一个对象数组,每个对象包含:name
: 参数的名称。type
: 参数的规范类型(见下文)。components
: 用于元组类型(见下文)。
outputs
: 一个与inputs
类似的对象数组。stateMutability
: 一个字符串,值为以下之一:pure
(指定不读取区块链状态),view
(指定不修改区块链状态),nonpayable
(函数不接受以太 - 默认值)和payable
(函数接受以太)。
构造函数、接收和回退从不具有 name
或 outputs
。接收和回退也没有 inputs
。
备注
向不可支付函数发送非零以太将导致交易回退。
备注
状态可变性 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!") 没有长度字段
更具体地说:
在编码过程中,所有内容都在就地编码。这意味着没有头和尾的区别,如同 ABI 编码,并且数组的长度不被编码。
abi.encodePacked
的直接参数在不进行填充的情况下编码,只要它们不是数组(或string
或bytes
)。数组的编码是其元素编码的连接 并且 进行填充。
动态大小的类型如
string
,bytes
或uint[]
在编码时不包含长度字段。string
或bytes
的编码在末尾不进行填充,除非它是数组或结构体的一部分(此时填充到 32 字节的倍数)。
一般来说,一旦存在两个动态大小的元素,编码就会变得模糊,因为缺少长度字段。
如果需要填充,可以使用显式类型转换: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
在左侧填充。
警告
如果结构体包含多个动态大小的数组,则其编码是模糊的。因此,请始终重新检查事件数据,不要仅依赖基于索引参数的搜索结果。