合同ABI规范
基础设计
合同应用程序二进制接口(ABI)是以太坊生态系统中与合同进行交互的标准方式,无论是从区块链外部还是从合同到合同的交互。数据根据其类型进行编码,如本规范所述。编码不是自描述的,因此需要一个模式才能解码。
我们假设契约的接口函数是强类型的,在编译时已知,并且是静态的。我们假设所有契约都将具有它们在编译时调用的任何契约的接口定义。
此规范不处理接口是动态的或只有在运行时才知道的契约。
功能选择器
函数调用的调用数据的前四个字节指定要调用的函数。它是函数签名的Keccak-256哈希的第一个(左,big-endian中的高位)四个字节。签名被定义为没有数据位置说明符的基本原型的规范表达式,即带有括号的参数类型列表的函数名。参数类型由一个逗号分隔-不使用空格。
注解
函数的返回类型不是此签名的一部分。在 Solidity's function overloading 不考虑返回类型。原因是保持函数调用解析上下文独立。这个 JSON description of the ABI 但是包含输入和输出。
参数编码
从第五个字节开始,随后是编码的参数。这种编码也用于其他地方,例如返回值和事件参数的编码方式相同,没有指定函数的四个字节。
类型
存在以下基本类型:
uint<M>
:无符号整数类型M
位,0 < M <= 256
,M % 8 == 0
.例如uint32
,uint8
,uint256
.int<M>
:2的有符号补码整数类型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
:动态大小的Unicode字符串假定为UTF-8编码。<type>[]
:给定类型的元素的可变长度数组。
类型可以组合成一个元组,方法是将它们括在括号内,用逗号分隔:
(T1,T2,...,Tn)
: tuple consisting of the typesT1
, ...,Tn
,n >= 0
可以形成元组的元组、元组的数组等等。也可以形成零元组(其中 n == 0
)
将Solidity映射到ABI类型
除了元组之外,Solidity支持上述所有类型的名称相同。另一方面,ABI不支持某些实体类型。下表在左栏显示不属于ABI的实体类型,在右栏显示表示它们的ABI类型。
Solidity |
ABI |
---|---|
|
|
|
|
|
|
其基础值类型 |
|
|
警告
之前的版本 0.8.0
枚举可以有256个以上的成员,并且由大到足以容纳任何成员的值的最小整数类型表示。
编码的设计标准
编码设计为具有以下属性,如果某些参数是嵌套数组,则这些属性尤其有用:
访问值所需的读取次数至多是该值在参数数组结构内的深度,即需要四次读取才能检索
a_i[k][l][r]
。在ABI的以前版本中,在最坏的情况下,读取次数与动态参数的总数成线性关系。变量或数组元素的数据不与其他数据交织,并且它是可重定位的,即它只使用相对的“地址”。
编码的形式规范
我们区分静态和动态类型。静态类型就地编码,动态类型在当前块后单独分配的位置编码。
定义: 以下类型称为“动态”类型:
bytes
string
T[]
for anyT
T[k]
for any dynamicT
and anyk >= 0
(T1,...,Tk)
ifTi
is dynamic for some1 <= i <= k
所有其他类型都称为“静态”。
定义: len(a)
是二进制字符串中的字节数 a
.的类型 len(a)
假设为 uint256
.
我们定义 enc
,实际编码,作为ABI类型值到二进制字符串的映射,以便 len(enc(X))
取决于 X
如果且仅当 X
是动态的。
定义: 对于任何ABI值 X
,我们递归地定义 enc(X)
,取决于 X
存在
(T1,...,Tk)
fork >= 0
and any typesT1
, ...,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
,前缀为元素数。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)
是的big endian编码X
,用零字节填充在高阶(左侧),使长度为32字节。address
:如uint160
案例int<M>
:enc(X)
大尾数2的补码编码是X
,在高阶(左侧)侧用0xff
负字节X
非负字节为零X
这样,长度为32字节。bool
: as in theuint8
case, where1
is used fortrue
and0
forfalse
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
如果我们想打电话 baz
带参数 69
和 true
,我们将传递总共68个字节,可以分为:
0xcdcd77c0
:方法ID。这是作为签名的ASCII格式的keccak哈希的前4个字节派生的。baz(uint32,bool)
.0x0000000000000000000000000000000000000000000000000000000000000045
:第一个参数,uint32值69
填充到32字节0x0000000000000000000000000000000000000000000000000000000000000001
:第二个参数-布尔值true
,填充到32字节
总计:
0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001
它返回一个 bool
.例如,如果它要返回 false
,其输出将是单字节数组 0x0000000000000000000000000000000000000000000000000000000000000000
一个乳房。
如果我们想打电话 bar
有了参数 ["abc", "def"]
,我们将传递总共68个字节,分为:
0xfce353f6
:方法ID。这是从签名派生的bar(bytes3[2])
.0x6162630000000000000000000000000000000000000000000000000000000000
:第一个参数的第一部分,abytes3
价值"abc"
(左对齐)。0x6465660000000000000000000000000000000000000000000000000000000000
:第一个参数的第二部分,abytes3
价值"def"
(左对齐)。
总计:
0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000
如果我们想打电话 sam
有了这些论据 "dave"
, true
和 [1,2,3]
,我们将传递总共292个字节,分为:
0xa5643bf2
:方法ID。这是从签名派生的sam(bytes,bool,uint256[])
. 注意uint
替换为其规范表示uint256
.0x0000000000000000000000000000000000000000000000000000000000000060
:第一个参数(动态类型)的数据部分的位置,从arguments块开始以字节为单位度量。在这种情况下,0x60
.0x0000000000000000000000000000000000000000000000000000000000000001
:第二个参数:布尔值真。0x00000000000000000000000000000000000000000000000000000000000000a0
:第三个参数(动态类型)的数据部分的位置,以字节为单位。在这种情况下,0xa0
.0x0000000000000000000000000000000000000000000000000000000000000004
:第一个参数的数据部分,以元素中字节数组的长度开始,在本例中为4。0x6461766500000000000000000000000000000000000000000000000000000000
:第一个参数的内容:的utf-8(在本例中等于ascii)编码"dave"
,右边填充到32个字节。0x0000000000000000000000000000000000000000000000000000000000000003
:第三个参数的数据部分,以元素中数组的长度开始,在本例中为3。0x0000000000000000000000000000000000000000000000000000000000000001
:第三个参数的第一个条目。0x0000000000000000000000000000000000000000000000000000000000000002
:第三个参数的第二个条目。0x0000000000000000000000000000000000000000000000000000000000000003
:第三个参数的第三个条目。
总计:
0xa5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003
动态类型的使用
对带有签名的函数的调用 f(uint,uint32[],bytes10,bytes)
带值 (0x123, [0x456, 0x789], "1234567890", "Hello, world!")
按以下方式编码:
我们取的前四个字节 sha3("f(uint256,uint32[],bytes10,bytes)")
,即 0x8be65246
.然后我们对所有四个参数的头部分进行编码。对于静态类型 uint256
和 bytes10
,这些直接是我们要传递的值,而对于动态类型 uint32[]
和 bytes
,我们使用以字节为单位的偏移量到其数据区域的起始处,从值编码的起始处开始测量(即不计算包含函数签名散列的前四个字节)。这些是:
0x0000000000000000000000000000000000000000000000000000000000000123
(0x123
padded to 32 bytes)0x0000000000000000000000000000000000000000000000000000000000000080
(第二个参数的数据部分的起始偏移量,4*32字节,正好是头部分的大小)0x3132333435363738393000000000000000000000000000000000000000000000
("1234567890"
padded to 32 bytes on the right)0x00000000000000000000000000000000000000000000000000000000000000e0
(第四个参数的数据部分的起始偏移量=第一个动态参数的数据部分的起始偏移量+第一个动态参数的数据部分的大小=4 * 32+3 * 32(见下文)
之后,第一个动态参数的数据部分, [0x456, 0x789]
跟随:
0x0000000000000000000000000000000000000000000000000000000000000002
(数组元素数,2)0x0000000000000000000000000000000000000000000000000000000000000456
(第一个元素)0x0000000000000000000000000000000000000000000000000000000000000789
(第二个元素)
最后,我们对第二个动态参数的数据部分进行编码, "Hello, world!"
:
0x000000000000000000000000000000000000000000000000000000000000000d
(元素数(本例中为字节):13)0x48656c6c6f2c20776f726c642100000000000000000000000000000000000000
("Hello, world!"
padded to 32 bytes on the right)
总之,编码是(在函数选择器后换行,为了清晰起见,每个32字节):
0x8be65246
0000000000000000000000000000000000000000000000000000000000000123
0000000000000000000000000000000000000000000000000000000000000080
3132333435363738393000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000e0
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000456
0000000000000000000000000000000000000000000000000000000000000789
000000000000000000000000000000000000000000000000000000000000000d
48656c6c6f2c20776f726c642100000000000000000000000000000000000000
让我们应用同样的原理来对带有签名的函数的数据进行编码 g(uint[][],string[])
带值 ([[1, 2], [3]], ["one", "two", "three"])
但从编码的最原子部分开始:
首先,我们对第一个嵌入式动态数组的长度和数据进行编码。 [1, 2]
第一个根数组的 [[1, 2], [3]]
:
0x0000000000000000000000000000000000000000000000000000000000000002
(第一个数组中的元素数,2;元素本身1
和2
)0x0000000000000000000000000000000000000000000000000000000000000001
(第一个元素)0x0000000000000000000000000000000000000000000000000000000000000002
(第二个元素)
然后对第二个嵌入式动态数组的长度和数据进行编码。 [3]
第一个根数组的 [[1, 2], [3]]
:
0x0000000000000000000000000000000000000000000000000000000000000001
(第二个数组中的元素数,1;元素为3
)0x0000000000000000000000000000000000000000000000000000000000000003
(第一个元素)
然后我们需要找到补偿 a
和 b
对于它们各自的动态数组 [1, 2]
和 [3]
.为了计算偏移量,我们可以查看第一个根数组的编码数据。 [[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
(单词的utf8表示法"one"
)0x0000000000000000000000000000000000000000000000000000000000000003
(字中的字符数"two"
)0x74776f0000000000000000000000000000000000000000000000000000000000
(单词的utf8表示法"two"
)0x0000000000000000000000000000000000000000000000000000000000000005
(字中的字符数"three"
)0x7468726565000000000000000000000000000000000000000000000000000000
(单词的utf8表示法"three"
)
与第一个根数组并行,因为字符串是动态元素,所以我们需要找到它们的偏移量。 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[],uint[][])
.
然后我们对第一个根数组的长度进行编码:
0x0000000000000000000000000000000000000000000000000000000000000002
(第一个根数组中的元素数,2;元素本身是[1, 2]
和[3]
)
然后我们对第二个根数组的长度进行编码:
0x0000000000000000000000000000000000000000000000000000000000000003
(第二个根数组中的字符串数,3;字符串本身是"one"
,"two"
和"three"
)
最后我们找到了补偿 f
和 g
对于它们各自的根动态数组 [[1, 2], [3]]
和 ["one", "two", "three"]
,并按正确的顺序组装零件:
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
.
事件
事件是以太坊日志记录/事件监视协议的抽象。日志条目提供合同的地址、一系列多达四个主题和一些任意长度的二进制数据。事件利用现有的函数abi将其(连同接口规范)解释为正确类型的结构。
给定一个事件名称和一系列事件参数,我们将它们分为两个子系列:有索引的子系列和没有索引的子系列。与事件签名的Keccak散列一起使用的那些被索引的那些可以是最多3个(对于非匿名事件)或4个(对于匿名事件),以形成日志条目的主题。未编入索引的那些形成事件的字节数组。
实际上,使用此ABI的日志条目描述为:
address
:合同地址(本质上由以太坊提供);topics[0]
:keccak(EVENT_NAME+"("+EVENT_ARGS.map(canonical_type_of).join(",")+")")
(canonical_type_of
is a function that simply returns the canonical type of a given argument, e.g. foruint indexed foo
, it would returnuint256
). This value is only present intopics[0]
if the event is not declared asanonymous
;topics[n]
:abi_encode(EVENT_INDEXED_ARGS[n - 1])
如果事件未声明为anonymous
或abi_encode(EVENT_INDEXED_ARGS[n])
如果是的话 (EVENT_INDEXED_ARGS
是一系列的EVENT_ARGS
已编入索引的);data
: ABI encoding ofEVENT_NON_INDEXED_ARGS
(EVENT_NON_INDEXED_ARGS
is the series ofEVENT_ARGS
that are not indexed,abi_encode
is the ABI encoding function used for returning a series of typed values from a function, as described above).
对于所有类型的长度,最多32个字节, EVENT_INDEXED_ARGS
数组直接包含值、填充或符号扩展(对于有符号整数)到32个字节,就像常规ABI编码一样。但是,对于所有“复杂”类型或动态长度类型,包括所有数组, string
, bytes
和结构, EVENT_INDEXED_ARGS
将包含 凯卡杂烩 特殊就地编码值(参见 索引事件参数的编码 ,而不是直接使用编码值。这允许应用程序有效地查询动态长度类型的值(通过将编码值的哈希设置为主题),但使应用程序无法解码未查询到的索引值。对于动态长度类型,应用程序开发人员面临着快速搜索预定值(如果参数被索引)和任意值易读性(这要求参数不被索引)之间的权衡。开发人员可以克服这种权衡,通过定义具有两个参数(一个索引,一个不索引)的事件来保持相同的值,从而实现高效搜索和任意易读性。
错误
如果协定内部出现故障,协定可以使用特殊操作码中止执行并恢复所有状态更改。除了这些效果之外,还可以将描述性数据返回给调用者。此描述性数据是错误及其参数的编码,编码方式与函数调用的数据相同。
作为一个例子,让我们考虑下面的合同,其 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)
。
错误选择器 0x00000000
和 0xffffffff
都保留下来以备将来使用。
警告
永远不要信任错误数据。默认情况下,错误数据通过外部调用链向上冒泡,这意味着协定可能会收到它直接调用的任何协定中没有定义的错误。此外,任何协定都可以通过返回与错误签名匹配的数据来伪造任何错误,即使错误没有在任何地方定义。
JSON
契约接口的JSON格式由一组函数、事件和错误描述给出。函数描述是包含以下字段的JSON对象:
type
:"function"
,"constructor"
,"receive"
(the "receive Ether" function 或"fallback"
(the "default" function ;name
:函数的名称;inputs
:一个对象数组,每个对象包含:name
:参数的名称。type
:参数的规范类型(更多内容见下文)。components
:用于元组类型(更多内容见下文)。
outputs
:类似于inputs
.stateMutability
: a string with one of the following values:pure
(specified to not read blockchain state),view
(specified to not modify the blockchain state),nonpayable
(function does not accept Ether - the default) andpayable
(function accepts Ether).
构造函数和回退函数从来没有 name
或 outputs
.回退函数没有 inputs
要么。
注解
向非应付功能发送非零 Ether 将恢复交易。
注解
状态可变性 nonpayable
通过根本不指定状态可变修饰符反映在实性中。
事件描述是一个具有相当相似字段的JSON对象:
type
: always"event"
name
:事件的名称。inputs
:一个对象数组,每个对象包含:name
:参数的名称。type
:参数的规范类型(更多内容见下文)。components
:用于元组类型(更多内容见下文)。indexed
:true
如果该字段是日志主题的一部分,false
如果它是日志的数据段之一。
anonymous
:true
如果事件声明为anonymous
.
错误如下所示:
type
: always"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编码一样,数组的长度也没有编码。
直接论点
abi.encodePacked
在没有填充的情况下进行编码,只要它们不是数组(或string
或bytes
)数组的编码是其元素编码的串联。 with 衬垫。
动态调整类型大小,如
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
应优先考虑。
索引事件参数的编码
索引事件参数不是值类型,即数组和结构不是直接存储的,而是存储编码的keccak256哈希。此编码定义如下:
A的编码
bytes
和string
值只是没有任何填充或长度前缀的字符串内容。结构的编码是其成员编码的串联,总是填充到32字节的倍数(偶数
bytes
和string
)数组的编码(动态和静态大小)是其元素编码的串联,总是填充到32字节的倍数(偶数
bytes
和string
)没有任何长度前缀
在上面,和往常一样,负数由符号扩展填充,而不是零填充。 bytesNN
类型填充在右侧,而 uintNN
/ intNN
左边有衬垫。
警告
如果结构包含多个动态大小的数组,则其编码不明确。因此,请务必重新检查事件数据,不要仅依赖于索引参数的搜索结果。