Skip to content

Latest commit

 

History

History
175 lines (131 loc) · 8.8 KB

File metadata and controls

175 lines (131 loc) · 8.8 KB
title tags
06. 动态类型的ABI编码
solidity
abi

WTF Solidity内部标准: 06. 动态类型的ABI编码

《WTF Solidity内部标准》教程将介绍Solidity智能合约中的存储布局,内存布局,以及ABI编码规则,帮助大家理解Solidity的内部规则。

推特:@0xAA_Science

社区:Discord微信群官网 wtf.academy

所有代码和教程开源在github: github.com/AmazingAng/WTF-Solidity-Internals


上一讲,我们介绍了静态类型的ABI编码规则。而在这一讲,我们将讲解更复杂的动态类型的ABI编码规则。

注意,abi.encode(x)编码的实际上是(x),即仅包含x一个元素的元组。所以我们这一讲给出的是复杂类型在元组中的编码规则,不在元组中的规则请见下一讲。

动态类型的ABI编码规则

我们可以把Solidity中的动态类型包括:

  1. bytesstring
  2. 动态数组。
  3. 动态类型T的定长数组T[k],其中k > 0
  4. 由任意动态类型构成的元组。

静态类型是直接编码的,而动态类型是在当前数据槽之后的一个单独分配的位置进行编码,当前块记录只分配位置的偏移量。

动态数组

对于动态数组,编码时当前数据槽记录偏移量,然后记录动态数组的长度和每一个元素,它们分别占据一个单独的数据槽(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槽会记录数组的长度,这里为30x40-0x80会分别记录数组的元素[1, 2, 3]。因此,编码后的结果为:

0x
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000003

bytesstring

bytesstring的编码方式相同,会在当前数据槽记录分配位置的偏移量,然后在分配的位置记录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。接下来0x200x40槽分别记录string[0]string[1]的偏移量,0x400x80。注意,这里的偏移量是相对于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 auint[] 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的值990x63),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编码,方便大家理解。

动态类型的不定长数组

上面我们学习了动态类型的定长数组的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。之后槽0x400x60分别记录了strings[0]strings[1]的偏移量0x400x80(相对于槽0x40)。最后,槽0x80-0xa0记录了strings[0]的字节长度和内容,槽0xc0-0xe0记录了strings[1]的字节长度和内容。因此,编码后的结果为:

0x
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000040
0000000000000000000000000000000000000000000000000000000000000080
0000000000000000000000000000000000000000000000000000000000000003
5754460000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000007
41636164656d7900000000000000000000000000000000000000000000000000

总结

这一讲,我们介绍了Solidity合约中动态类型的ABI编码规则。相比于静态类型,动态类型的ABI编码规则要复杂得多,大家需要借助例子取理解它。