Solidity中 函数选择器及参数编码

学习文章

应用二进制接口说明
官方文档中的应用二进制接口部分内容。前面部分摘录自官方文档,主要是对后面参数编码进行学习和理解。

函数选择器

一个函数调用数据的前 4 字节,指定了要调用的函数。这就是某个函数签名的 Keccak 哈希的前 4 字节(高位在左的大端序) (译注:这里的“高位在左的大端序“,指最高位字节存储在最低位地址上的一种串行化编码方式,即高位字节在左)。 这种签名被定义为基础原型的规范表达,基础原型即是函数名称加上由括号括起来的参数类型列表,参数类型间由一个逗号分隔开,且没有空格。

类型

参考官方文章:

  • uint<M>M 位的无符号整数,0 < M <= 256M % 8 == 0。例如:uint32uint8uint256
  • int<M>:以 2 的补码作为符号的 M 位整数,0 < M <= 256M % 8 == 0
  • address:除了字面上的意思和语言类型的区别以外,等价于 uint160。在计算和 函数选择器Function Selector 中,通常使用 address
  • uintintuint256int256 各自的同义词。在计算和 函数选择器Function Selector 中,通常使用 uint256int256
  • bool:等价于 uint8,取值限定为 0 或 1 。在计算和 函数选择器Function Selector 中,通常使用 bool
  • fixed<M>x<N>M 位的有符号的固定小数位的十进制数字 8 <= M <= 256M % 8 == 0、且 0 < N <= 80。其值 v 即是 v / (10 ** N)。(也就是说,这种类型是由 M 位的二进制数据所保存的,有 N 位小数的十进制数值。译者注。)
  • ufixed<M>x<N>:无符号的 fixed<M>x<N>
  • fixedufixedfixed128x18ufixed128x18 各自的同义词。在计算和 函数选择器Function Selector 中,通常使用 fixed128x18ufixed128x18
  • bytes<M>M 字节的二进制类型,0 < M <= 32
  • function:一个地址(20 字节)之后紧跟一个 函数选择器Function Selector (4 字节)。编码之后等价于 bytes24

定长数组类型:

  • <type>[M]:有 M 个元素的定长数组,M >= 0,数组元素为给定类型。

非定长类型:

  • bytes:动态大小的字节序列。
  • string:动态大小的 unicode 字符串,通常呈现为 UTF-8 编码。
  • <type>[]:元素为给定类型的变长数组。

编码的形式化说明

注意区分一下动态和静态类型,下面会用到:

  • bytes
  • string
  • 任意类型 T 的变长数组 T[]
  • 任意动态类型 T 的定长数组 T[k]k >= 0
  • 由动态的 Ti1 <= i <= k)构成的 元组tuple (T1,...,Tk)

所有其他类型都被称为“静态”。

定义: len(a) 是一个二进制字符串 a 的字节长度。len(a) 的类型被呈现为 uint256

我们把实际的编码 enc 定义为一个由ABI类型到二进制字符串的值的映射;因而,当且仅当 X 的类型是动态的,len(enc(X)) (即 X 经编码后的实际长度,译者注)才会依赖于 X 的值。

定义: 对任意ABI值 X,我们根据 X 的实际类型递归地定义 enc(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)),并且 当 Ti 为静态类型时,headtail 被定义为

    head(X(i)) = enc(X(i)) and tail(X(i)) = "" (空字符串)

    否则,比如 Ti 是动态类型时,它们被定义为

    head(X(i)) = enc(len(head(X(1)) ... head(X(k-1)) tail(X(1)) ... tail(X(i-1)))) tail(X(i)) = enc(X(i))

    注意,在动态类型的情况下,由于 head 部分的长度仅取决于类型而非值,所以 head(X(i)) 是定义明确的。它的值是从 enc(X) 的开头算起的,tail(X(i)) 的起始位在 enc(X) 中的偏移量。

  • T[k] 对于任意 Tk

    enc(X) = enc((X[0], ..., X[k-1]))

    即是说,它就像是个由相同类型的 k 个元素组成的 元组tuple 那样被编码的。

  • T[]Xk 个元素(k 被呈现为类型 uint256):

    enc(X) = enc(k) enc([X[1], ..., X[k]])

    即是说,它就像是个由静态大小 k 的数组那样被编码的,且由元素的个数作为前缀。

  • 具有 k (呈现为类型 uint256)长度的 bytes

    enc(X) = enc(k) pad_right(X),即是说,字节数被编码为 uint256,紧跟着实际的 X 的字节码序列,再在高位(左侧)补上可以使 len(enc(X)) 成为 32 的倍数的最少数量的 0 值字节数据。

  • string

    enc(X) = enc(enc_utf8(X)),即是说,X 被 utf-8 编码,且在后续编码中将这个值解释为 bytes 类型。注意,在随后的编码中使用的长度是其 utf-8 编码的字符串的字节数,而不是其字符数。

  • uint<M>enc(X) 是在 X 的大端序编码的高位(左侧)补充若干 0 值字节以使其长度成为 32 字节。

  • address:与 uint160 的情况相同。

  • int<M>enc(X) 是在 X 的大端序的 2 的补码编码的高位(左侧)添加若干字节数据以使其长度成为 32 字节;对于负数,添加值为 0xff (即 8 位全为 1,译者注)的字节数据,对于非负数,添加 0 值(即 8 位全为 0,译者注)字节数据。

  • bool:与 uint8 的情况相同,1 用来表示 true0 表示 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 字节而添加的若干 0 值字节。

注意,对于任意的 Xlen(enc(X)) 都是 32 的倍数。

上面的部分需要细细看一下。

例子

pragma solidity ^0.4.16;

contract Foo {
  function bar(bytes3[2]) public pure {}
  function baz(uint32 x, bool y) public pure returns (bool r) { r = x > 32 || y; }
  function sam(bytes, bool, uint[]) public pure {}
}

baz函数传 69和true

baz函数的方法ID是0xcdcd77c0。接下来就是参数的编码。
首先应用定义:
在这里插入图片描述
有uint32和bool,二者都是静态,因此tail都是0,直接递归算head。
head(uint32)=enc(uint32),69应用定义:
在这里插入图片描述
0x0000000000000000000000000000000000000000000000000000000000000045

同理,true应用定义:
在这里插入图片描述
0x0000000000000000000000000000000000000000000000000000000000000001
所以最终得到:
0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001

看懂了第一个,接下来就是慢慢递归应用那些定义了。

bar函数传 [“abc”, “def”]

byte3[2]是静态类型,因此tail为空,直接求tail。
数组满足定义,然后递归求元组:
在这里插入图片描述
对于这个元组2个元素"abc"和"def"都是bytes3类型,都是静态,tail为空,直接求head,满足这个:
在这里插入图片描述
需要注意,这里是右填充。
因此"abc"编码为:0x6162630000000000000000000000000000000000000000000000000000000000
"def"编码为:0x6465660000000000000000000000000000000000000000000000000000000000,加上前面的函数ID,得到:
0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000

sam函数传 “dave”、true和[1,2,3]

可以看到有2个动态的,bytes和uint[]。在有动态的情况下再按照官方的那种思考感觉很迷,按照ctfwiki总结的来应用就比较简单了:

动态类型的数据,比如动态数组,结构体,变长字节,其编码后存储其 offsetlengthdata

  • 先把参数顺序存储:如果是定长数据类型,直接存储其 data,如果是变长数据类型,先存储其 offset
  • 顺序遍历变长数据:先存储 offset,对于第一个变长数据,先存储其 offset = 0x20 * number ( number 是函数参数的个数 );对于下一个变长数据,其 offset = offset_of_prev + 0x20 + 0x20 * number (第一个 0x20 是存储前一个变长数据的长度占用的大小,number 是前一个变长数据的元素个数)
  • 顺序遍历变长数据:存储完 offset ,接着就是遍历每个变长数据,分别存储其 lengthdata
  • ( ps: 对于结构体这样的类型,存储的时候可把结构体内元素看成是一个新函数的参数,这样的话,对于结构体中的第一个变长数据,其 offset = 0x20 * numnum 是结构体元素的个数 )

bytes的offset,0x20*3,即:

0x0000000000000000000000000000000000000000000000000000000000000060

然后是bool的data:0x0000000000000000000000000000000000000000000000000000000000000001
然后是uint[]的offset是offset_of_prev + 0x20 + 0x20 * number,即之前的96+32+32*1,即160:

0x00000000000000000000000000000000000000000000000000000000000000a0

然后是bytes内容的编码:
在这里插入图片描述

先是enc(4):0x0000000000000000000000000000000000000000000000000000000000000004
然后是pad_right(x):0x6461766500000000000000000000000000000000000000000000000000000000
然后就是uint[]内容的编码:
在这里插入图片描述
enc(3)是0x0000000000000000000000000000000000000000000000000000000000000003
然后enc(1,2,3)按照前面那样,就分别是:
0x0000000000000000000000000000000000000000000000000000000000000001,
0x0000000000000000000000000000000000000000000000000000000000000002,
0x0000000000000000000000000000000000000000000000000000000000000003
再把所有这些拼接起来:

0xa5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003

动态类型的使用

前面其实已经涉及到了动态类型,但是官方并未对着做过多的解释,在这一块官方的解释就非常的详细了。

只看这个例子:

让我们使用相同的原理来对一个签名为 g(uint[][],string[]),参数值为 ([[1, 2], [3]], ["one", "two", "three"]) 的函数来进行编码。

函数的签名就不说了,参数是两个动态:uint[][]和string[],需要分别计算二者的偏移量和数据部分。

再来看,计算[1, 2], [3],也就是数据部分,还得再嵌套一层,算[1,2]的偏移量和数据部分,还有[3]的偏移量和数据部分。

先看数据部分,[1,2]编码后是:

  • 0x0000000000000000000000000000000000000000000000000000000000000002 (第一个数组中的元素数量 2;元素本身是 12)
  • 0x0000000000000000000000000000000000000000000000000000000000000001 (第一个元素)
  • 0x0000000000000000000000000000000000000000000000000000000000000002 (第二个元素)

[3]的编码是:

  • 0x0000000000000000000000000000000000000000000000000000000000000001 (第二个数组中的元素数量 1;元素数据是 3)
  • 0x0000000000000000000000000000000000000000000000000000000000000003 (第一个元素)

然后需要知道这个动态数组[[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

这就懂了,同理再看["one", "two", "three"],对第二个根数组的嵌入字符串进行编码:

  • 0x0000000000000000000000000000000000000000000000000000000000000003 (单词 "one" 中的字符个数)
  • 0x6f6e650000000000000000000000000000000000000000000000000000000000 (单词 "one" 的 utf8 编码)
  • 0x0000000000000000000000000000000000000000000000000000000000000003 (单词 "two" 中的字符个数)
  • 0x74776f0000000000000000000000000000000000000000000000000000000000 (单词 "two" 的 utf8 编码)
  • 0x0000000000000000000000000000000000000000000000000000000000000005 (单词 "three" 中的字符个数)
  • 0x7468726565000000000000000000000000000000000000000000000000000000 (单词 "three" 的 utf8 编码)

再看偏移量:

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

然后我们对第一个根数组的长度进行编码:

  • 0x0000000000000000000000000000000000000000000000000000000000000002 (第一个根数组的元素数量 2;这些元素本身是 [1, 2][3])

而后我们对第二个根数组的长度进行编码:

  • 0x0000000000000000000000000000000000000000000000000000000000000003 (第二个根数组的元素数量 3;这些字符串本身是 "one""two""three")

然后就要开始计算这两个大部分的偏移量了:

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

接下来就是拼接的事情了。

上述的大部分都是摘录是官方文档,一开始确实看的很懵,但是最后看到这些例子,就感觉很清楚了。学到了学到了。

此外再看一下ctfwiki上的一个例子:

pragma solidity >=0.4.16 <0.9.0;
pragma experimental ABIEncoderV2;

contract Demo {
    struct Test {
        string name;
        string policies;
        uint num;
    }

    uint public x;
    function test5(uint, Test memory test) public { x = 1; }
}
test5(0x123[“ cxy”,“ pika”,123]

首先是0x123的:

0x0000000000000000000000000000000000000000000000000000000000000123

然后就是第二个参数,这个结构体的偏移量。
按照ctfwiki上的总结,或者按照官方文档的思考方式都可。
偏移量,即可以直接算:offset = 0x20 * number ( number 是函数参数的个数 ),是64,也可以:

0x4ca373dc                                                             // function selector
0 - 0x0000000000000000000000000000000000000000000000000000000000000123 // data of first parameter
1 - a // offset of second parameter,偏移量
2 - xxxx//uint是静态已经结束,因此这里一定是结构体关于data的编码了,因此偏移量指向第二行的开始(64字节)

都可以知道是0x0000000000000000000000000000000000000000000000000000000000000040
接下来就是进入这个结构体,这个结构体是string,string,uint,结构体内元素可当成函数参数拆分。因此接下来就是又一个嵌套,先是第一个string的偏移量,然后第二个string的偏移量,然后是uint的编码,然后就是第一个string的data编码和第二个string的data编码。

最终应该是这样:

0x4ca373dc                                                             // function selector
0 - 0x0000000000000000000000000000000000000000000000000000000000000123 // data of first parameter
1 - 0x0000000000000000000000000000000000000000000000000000000000000040 //结构体的偏移量
2 - 0x0000000000000000000000000000000000000000000000000000000000000060 //第一个string的偏移量
3 - 0x00000000000000000000000000000000000000000000000000000000000000a0 //第二个string的偏移量
4 - 0x000000000000000000000000000000000000000000000000000000000000007b //uint的编码
5 - 0x0000000000000000000000000000000000000000000000000000000000000003 //第一个string的length
6 - 0x6378790000000000000000000000000000000000000000000000000000000000 //第一个string的data
7 - 0x0000000000000000000000000000000000000000000000000000000000000004 //第二个string的length
8 - 0x70696b6100000000000000000000000000000000000000000000000000000000 //第二个string的data

学到了学到了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值