| title | 06. 动态类型的ABI编码 | ||
|---|---|---|---|
| tags |
|
《WTF Solidity内部标准》教程将介绍Solidity智能合约中的存储布局,内存布局,以及ABI编码规则,帮助大家理解Solidity的内部规则。
所有代码和教程开源在github: github.com/AmazingAng/WTF-Solidity-Internals
上一讲,我们介绍了静态类型的ABI编码规则。而在这一讲,我们将讲解更复杂的动态类型的ABI编码规则。
注意,
abi.encode(x)编码的实际上是(x),即仅包含x一个元素的元组。所以我们这一讲给出的是复杂类型在元组中的编码规则,不在元组中的规则请见下一讲。
我们可以把Solidity中的动态类型包括:
bytes和string。- 动态数组。
- 动态类型
T的定长数组T[k],其中k > 0。 - 由任意动态类型构成的元组。
静态类型是直接编码的,而动态类型是在当前数据槽之后的一个单独分配的位置进行编码,当前块记录只分配位置的偏移量。
对于动态数组,编码时当前数据槽记录偏移量,然后记录动态数组的长度和每一个元素,它们分别占据一个单独的数据槽(32字节)。在下面的testAbiArray()中,我们定义了一个uint[] memory a = new uint[](3),并返回它的ABI编码。
function testAbiArray() public pure returns (bytes memory){
uint[] memory a = new uint[](3);
a[0] = 1;
a[1] = 2;
a[2] = 3;
return abi.encode(a);
}首先,0x00槽会记录偏移量,因为我们只编码一个数据,uint[]的长度和数值会在0x20开始记录,偏移量就是0x20。接下来,0x20槽会记录数组的长度,这里为3。0x40-0x80会分别记录数组的元素[1, 2, 3]。因此,编码后的结果为:
0x
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000003
bytes和string的编码方式相同,会在当前数据槽记录分配位置的偏移量,然后在分配的位置记录bytes/string的字节长度,接下来是字节内容,编码时会在右侧补若干0使其长度成为32字节的倍数。在下面的testAbiString()中,我们定义了string memory b = "WTF",并返回它的ABI编码。
function testAbiString() public pure returns (bytes memory){
string memory b = "WTF";
return abi.encode(b);
}首先,0x00槽会记录偏移量0x20。接下来0x20槽会记录字符串长度3。最后,0x40槽会记录"WTF"的UTF8编码,并在右侧补若干0使其长度成为32字节,也就是5754460000000000000000000000000000000000000000000000000000000000。因此,编码后的结果为:
0x
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000003
5754460000000000000000000000000000000000000000000000000000000000动态类型的定长数组,比如string[2],会在当前数据槽记录分配位置的偏移量,然后再在之后的槽中记录每个元素的偏移量,比如string[0]和string[1]的偏移量,最后记录元素的值。在下面的testAbiStringStaticArray()中,我们定义了string[2] memory strings = ["WTF", "Academy"],并返回它的ABI编码。
function testAbiStringStaticArray() public pure returns (bytes memory){
string[2] memory strings = ["WTF", "Academy"];
return abi.encode(strings);
}首先,0x00槽会记录数组的偏移量0x20。接下来0x20和0x40槽分别记录string[0]和string[1]的偏移量,0x40和0x80。注意,这里的偏移量是相对于0x20槽的,而不是0x00的。然后string[0],也就是"WTF",会保存在0x60-0x80槽中(0x20偏移0x40);string[1],也就是"Academy",会保存在0xa0-0xc0槽中(0x20偏移0x80)。因此,编码后的结果为:
0x
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000040
0000000000000000000000000000000000000000000000000000000000000080
0000000000000000000000000000000000000000000000000000000000000003
5754460000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000007
41636164656d7900000000000000000000000000000000000000000000000000
元组和结构体的编码方式相同,对于包含动态类型的元组/结构体,会在当前数据槽记录分配位置的偏移量,然后再分别编码成员:如果成员是静态类型,就直接在当前槽中编码;如果是动态类型,则先记录偏移量,再在后面分配的位置进行编码。在下面的合约中,我们定义了一个DynamicStruct结构体,它包含3个成员:uint a,uint[] b,和string c,后两者均为动态类型。函数testAbiDynamicStruct会返回DynamicStruct结构体的ABI编码。
struct DynamicStruct { uint a; uint[] b; string c; }
function testAbiDynamicStruct() public pure returns (bytes memory){
uint a = 99;
uint[] memory b = new uint[](3);
b[0] = 1;
b[1] = 2;
b[2] = 3;
string memory c = "WTF";
DynamicStruct memory ds = DynamicStruct(a, b, c);
return abi.encode(ds);
}首先,0x00槽会记录整个结构体的偏移量0x20。接下来开始记录数组成员,0x20槽记录uint a的值99(0x63),0x40槽记录uint[] b的偏移量0x60(相对槽0x20),0x60槽记录string c的偏移量0xe0(相对槽0x20)。最后,0x80-0xe0记录uint[] b的长度3和值[1,2,3],0x100-0x120记录string c的字节长度3和值575446。因此,编码后的结果为:
0x
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000063
0000000000000000000000000000000000000000000000000000000000000060
00000000000000000000000000000000000000000000000000000000000000e0
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000003
5754460000000000000000000000000000000000000000000000000000000000
这一章节总结一些特殊类型的ABI编码,方便大家理解。
上面我们学习了动态类型的定长数组的ABI编码规则,现在我们看看动态类型的不定长数组是如何编码的。下面testAbiStringArray()函数中我们定义了一个string[] strings变量,并返回了它的ABI编码
function testAbiStringArray() public pure returns (bytes memory){
string[] memory strings = new string[](2);
strings[0] = "WTF";
strings[1] = "Academy";
return abi.encode(strings);
}首先,0x00槽记录不定长数组的偏移量0x20。接下来0x20槽中会记录不定长数组的长度2。之后槽0x40和0x60分别记录了strings[0]和strings[1]的偏移量0x40和0x80(相对于槽0x40)。最后,槽0x80-0xa0记录了strings[0]的字节长度和内容,槽0xc0-0xe0记录了strings[1]的字节长度和内容。因此,编码后的结果为:
0x
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000040
0000000000000000000000000000000000000000000000000000000000000080
0000000000000000000000000000000000000000000000000000000000000003
5754460000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000007
41636164656d7900000000000000000000000000000000000000000000000000
这一讲,我们介绍了Solidity合约中动态类型的ABI编码规则。相比于静态类型,动态类型的ABI编码规则要复杂得多,大家需要借助例子取理解它。
