本文深入探讨了Solidity中的ABI编码机制,详细解释了函数选择器和参数编码的原理,特别是静态类型和动态类型的编码方式,并通过一个实际的调用数据解析示例展示了如何手动解码ABI编码的数据。
GM fam,欢迎来到我的第一篇 Medium 博客文章。
昨晚我看到 chad z0age 的一条推文,意识到自己对 ABI 编码的工作原理了解不够,因此在阅读了 solidity docs 后,这是我个人的消化总结。
z0age 的 nerd snipe
正如你可能已经知道的,在 Solidity 中,ABI 用于编码函数调用和数据结构,以便在与智能合约交互时进行通信。ABI 指定了函数、错误和事件参数的编码和解码方式。
调用智能合约函数时,我们必须指定两个重要的事项:
在本文中,我们将快速浏览前者,重点关注后者。
函数调用的前四个字节的 calldata 指定要调用的函数:这些是该函数签名的 keccak256 的前 4 个字节。
签名定义为包含函数名称的字符串,后面是按逗号分隔的参数类型列表,用括号括起来。
例如:
1. "transferFrom(address,address,uint256)"
是臭名昭著的 ERC20 方法的签名。
2. "addressProcessBundle((uint256[2],address[],(uint256,(uint256,address,bytes)[])[]))"
是使用由固定大小数组、动态大小数组和结构体组成的动态大小数组的签名的一个例子,该结构体包含一个数字和一个包含数字、地址及动态大小数组的另一个结构体。
对上述字符串进行哈希并提取左侧的前 4 个字节将得到它们的选择器:
cast sig == gud tool
从 calldata 的第五个字节开始,编码的参数紧随其后。
在 static 和 dynamic 类型之间有一个重要的区别:static 类型是就地编码的,而 dynamic 类型是编码在当前“块”的参数之后的进一步位置。
动态类型包括:bytes
, string
, T[]
, T[k]
(对于任何动态的 T
和 k >= 0
)以及 (T1, .., Tk)
,如果对于某些 1 <= i <= k
,Ti
是动态的。
所有其他类型都是静态的!
在深入编码的正式规范之前,我们定义:
len(a)
表示二进制字符串 a
的字节数enc
,实际编码,作为 ABI 值与二进制字符串的映射,使得 len(enc(a))
仅在 a
是动态类型时依赖于 a
的值注意,根据 enc
的定义,如果 a
是静态类型,则其编码不依赖于其值(反之亦然)!
深呼吸,我们将深入探讨。
对于任何 ABI 值 X
,定义递归地 enc(X)
,基于其类型。
(T1, .., Tk)
,其中 k >= 0
和任何类型 T1
, .., Tk
:编码由 k
个“头”元素和 k
个“尾”元素组成,定义为 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))
(这复杂的表达方式意味着,在查找 X
的编码时,如果 X
是静态的,则会找到一个从结构体基准点的 offset,在这里找到实际编码。)
我们将在最后的例子中回来讨论这个
T[k]
(任何 T
和 k
):enc(X) = enc((X[0], .., X[k-1]))
即作为具有 k
个同一类型元素的元组编码
T[]
的 k
:enc(X) = enc(k) enc((X[0], .. , X[k-1]))
即作为具有 k
个同一类型元素的元组,前缀为元素的数量!
k
的 bytes
:enc(X) = enc(k) pad_right(X)
即作为字节数随后跟随实际字节序列,用 32 的倍数进行右填充
string
:enc(X) = enc(enc_utf8(X))
即 X
为 UTF-8,并被视为 bytes
uint<M>
:enc(X)
是 X
的大端编码,左侧填充使得 len(enc(X)) == 32
int<M>
:enc(X)
是 X
的大端二补数编码,左侧填充 0xff
如果 X
为负,并用零字节填充,如果 X
为非负,使得 len(enc(X)) == 32
bool
: 编码为 uint8
,其中 1
用于 true
,0
用于 false
bytes<M>
:enc(X)
是字节序列,尾部填充零以使得 len(enc(X)) == 32
我还可以展示像 fixed
和 ufixed
这样的类型的编码,但不会,因为它们在 Solidity v0.8.19 中仍然没有完全支持。
现在,我想带你了解如何读取原始 calldata 并手动解码它(如果你想挑战一下,可以尝试手动编码)。
让我们以对 Balancer’s Vault 的调用的数据作为例,特别是对它的 swap
函数的调用,因为它需要两个结构体和两个 uint256 作为参数。
这里 是我们将要分析的交易。
首先,让我们抓取 Etherscan 显示的 calldata(它非常好地为我们分块 32 字节的 calldata):
Function: swap((bytes32,uint8,address,address,uint256,bytes), (address,bool,address,bool), uint256, uint256)
MethodID: 0x52bbbe29 从参数编码块开始的偏移量
00000000000000000000000000000000000000000000000000000000000000e0 0x0000
0000000000000000000000008d7e58c0ebf988dbb31a993696286106964dd4f4 0x0020
0000000000000000000000000000000000000000000000000000000000000000 0x0040
0000000000000000000000008d7e58c0ebf988dbb31a993696286106964dd4f4 0x0060
0000000000000000000000000000000000000000000000000000000000000000 0x0080
0000000000000000000000000000000000000000000b3a7f984c82f6ffa3d428 0x00a0
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 0x00c0
929a9b6d40e4723f690db77a7ebb65d3254be1e00002000000000000000004d0 0x00e0
0000000000000000000000000000000000000000000000000000000000000000 0x0100
0000000000000000000000000000000000000000000000000000000000000000 0x0120
000000000000000000000000677d4fbbcdd9093d725b0042081ab0b67c63d121 0x0140
00000000000000000000000000000000000000000000000006f05b59d3b20000 0x0160
00000000000000000000000000000000000000000000000000000000000000c0 0x0180
0000000000000000000000000000000000000000000000000000000000000000 0x01a0
让我们从上到下逐个处理每个 32 字节字:
bytes
成员。address
。这确实是第二个结构体的第一个成员;在接下来的位置,直到 0x9f,你可以看到所有其他成员。type(uint256).max
。到目前为止,我们知道了:
我们找到了头部
现在我们找到 4 个参数中的 3 个,最后一个参数没多少地方可以隐藏:读取第一个结构体的头作为一个偏移量,我们指向第八个字,从顶部开始,这是第一个结构体的第一个成员,一个 bytes32
元素。
之后,在每个字中,我们可以找到所有后续结构体成员,直到找到一个 0xc0,最后应该是一个 bytes
成员。
起初,这可能不太有意义,因为在从 0xc0 开始的字中放置了第二个 uint256
,所以这是怎么回事呢?
解决这个混淆的是要理解,这个偏移量不是从参数编码的 0x00
字节解释,而是从 第一结构体成员列出的位置 的偏移量,所以是 0xe0
。
那么这个 bytes
成员在哪里呢?在以 0xe0 + 0xc0 = 0x01a0
开始的字中!鉴于这是一个空的 bytes 数组,这个槽编码为 0
而没有列出后续数据。
完整的图景是这样的:
calldata 的分解
希望这对你来说是一次有趣的阅读体验,并且你像我一样学到了新东西。
如果你想继续尝试更多奇特的类型组合(例如,结构体的动态大小数组,其中有包含 bytes 成员的结构体的数组),我推荐你从 foundry 工具链中使用 cast
:构建一些随机签名,用这些疯狂的类型并通过 cast abi-encode
传递它们以及你喜欢的任何数据,尝试完成我们今天所做的练习。
下次见,匿名者。
- 原文链接: medium.com/@ljmanini/abi...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!