译文出自:登链翻译计划[1] 译者:翻译小组[2] 校对:Tiny 熊[3]
你可能想知道如何破译和读取 evm 的 calldata,然后试图读取以太坊智能合约的交易 calldata,EVM(和其他 L1 分叉)以特定的方式对静态和动态类型的 calldata 进行编码和解码,在某种程度上让数据变得很困惑,起码最初是这样的。
在这篇文章中,我们将深入研究 calldata 的编码顺序,以便你能理解任何经过验证或未经验证的智能合约交易,并理解这些字节。通过这样做,我希望能让你有能力创建自己的原始 calldata。
Calldata 是我们发送给函数的编码参数,在这里是发送给以太坊虚拟机(EVM)上的智能合约。每块 calldata 有 32 个字节长(或 64 个字符)。有两种类型的 calldata:静态和动态。
静态变量是相当简单易懂的。另一方面,动态变量则要复杂得多,这可能是你难以直观地阅读原始 calldata 的原因。然而,一旦我们了解了动态变量是如何工作的,你就能轻松地阅读原始 calldata 了。
首先,让我们了解一下 calldata 是如何编码和解码的,以便为这一切的工作建立一个基础。
要对类型进行编码,你可以将它们传入abi.encode(parameters)
方法,以生成原始 calldata。
如果你想为一个特定的接口函数编码 Calldata,你可以使用 abi.encodeWithSelector(selector, parameters)
。这将与直接传入函数和它的参数一样。
比如说:
interface A {
function transfer(uint256[] memory ids, address to) virtual external;
}
contract B {
function a(uint256[] memory ids, address to) external pure returns(bytes memory) {
return abi.encodeWithSelector(A.transfer.selector, ids, to);
}
}
方法.selector
产生了 4 个字节(称为:函数选择器),在接口上代表该方法。我们用它来告诉 EVM,我们正在向该函数发送我们的 calldata。这就是 UniswapV2 如何实现闪电兑换。
还有abi.encodePacked(...)
,它可以有效地将所有动态变量放在一起,去掉 0 的填充。它的问题是,它不能防止碰撞,只有在你确定了参数的类型和长度时才可以使用。
那么你有了 calldata,你如何解码它呢?
如果 calldata 是用abi.encode(...)
创建的,那么我们可以用abi.decode(...)
对参数进行解码,只要传入我们想把 calldata 解码成的参数。
例如:
(uint256 a, uint256 b) = abi.decode(data, (uint256, uint256))
其中data
代表被传入的 calldata。
现在我们了解了如何对参数进行编码和解码,我们可以继续讨论不同的变量类型以及它们如何反映在 calldata 输出中。
静态变量是以下类型的简单编码表示,uint
, int
, address
, bool
, bytes1
to bytes32
(包括函数选择器), 和tuple
(然而它们可以有动态变量)。
例如,假设我们正在与以下合约进行交互:
pragma solidity 0.8.17;
contract Example {
function transfer(uint256 amount, address to) external;
}
带有输入参数:
amount: 1300655506
address: 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45
我们将生成 calldata:0x000000000000000000000000000000000000000000000000000000004d866d9200000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45
但是......我们怎么读这个呢?
好吧,让我们把它分成可读的部分,首先去掉前缀0x
,然后把每一行分成 64 个字符(或 32 字节)的部分
0x
// uint256
000000000000000000000000000000000000000000000000000000004d866d92
// address
00000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45
酷,现在我们知道前 32 字节是 "uint256 amount "变量,后 32 字节是 "address to"。
但是如果我们想直接调用transfer
函数呢?
我们需要知道参数类型的顺序,并使用一种叫做 "keccak256 "的 Hash 算法,将输入的数据变成一个 32 字节的 hash 值:
在此案例中,要获取函数哈希:
function transfer(uint256 amount, address to) external;
我们会这样做:
keccak256("transfer(uint256,address)");
这将返回以下 32 字节的哈希值:
0xb7760c8fd605b6ef5a068e1720c115665f9699a5c439e3c0ee9709290ff8a3bb
为了得到函数签名,我们只需要前 4 字节(或 8 个字符,不包括0x
前缀):b7760c8f
这个 4 字节的签名,b7760c8f
,是告诉 EVM 我们正在与该函数进行交互,下面的 calldata 被作为参数传入:
例如,如果我们要调用transfer
,参数与之前的静态变量相同,其 calldata 为:
0x000000000000000000000000000000000000000000000000000000004d866d9200000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45
并在前 32 个字节的前 4 个字节的开头加上b7760c8f
:
0xb7760c8f000000000000000000000000000000000000000000000000000000004d866d9200000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45
或
0x
b7760c8f
000000000000000000000000000000000000000000000000000000004d866d92
00000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45
你可能想知道,calldata 参数究竟是如何被输入到带有签名的函数中的?
答案是,合约的字节码通过匹配目标函数b7760c8f
来读取它,然后用00000000
替换它,然后传入参数。
动态变量是非固定大小的类型,包括bytes
、string
和动态数组<T>[]
,以及固定数组<T>[N]
。
动态类型的结构总是以偏移量开始,偏移量是动态类型开始位置的十六进制表示。例如,十六进制的 "20 "代表 "32 字节"。一旦我们到达偏移量,就会有一个更小的数字代表该类型的长度。
简而言之:第一个 32 字节=偏移量,第二个 32 字节=长度,其余的是元素。
对于数组,这个长度代表数组中包含的元素数量。对于字节和字符串类型,它代表该类型的长度。例如,字符串 "Hello World!"是 12 字节的长度,每个字符是 1 字节。请记住,这些类型从 calldata 的左边开始,而不是像其他东西一样从右边开始。
例如,这里是对string
“Hello World!”
的编码:
0x
0000000000000000000000000000000000000000000000000000000000000020
000000000000000000000000000000000000000000000000000000000000000c
48656c6c6f20576f726c64210000000000000000000000000000000000000000
观察一下前 32 个字节是如何代表十六进制的偏移量20
的,也就是十进制的32
。所以我们从000000000000000000000000000000000000000000000020
开始跳过 32 字节,把我们带到下一行,十六进制为0c
,十进制为12
,代表我们的字符串
的字节长度。现在,当我们把48656c6c6f20576f726c6421
转换为字符串
类型时,会返回我们的原始值。
祝贺你! 现在你知道如何读取动态类型了。
假设我们正在与下面的合约进行交互:
pragma solidity 0.8.17;
contract Example {
function transfer(uint256[] memory ids, address to) external;
}
有了下面的 "transfer"的参数:
ids: ["1234", "4567", "8910"]
to: 0xf8e81D47203A594245E36C48e151709F0C19fBe8
我们将生成 calldata:0x8229ffb60000000000000000000000000000000000000000000000000000000000000040000000000000000000000000f8e81d47203a594245e36c48e151709f0c19fbe8000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000004d200000000000000000000000000000000000000000000000000000000000011d700000000000000000000000000000000000000000000000000000000000022ce
我们可以把它切成一个更可读的形式:
// 前缀,不管
0x
// 函数选择器 (`transfer(uint[], address)`)
8229ffb6
// `uint256[] ids` 参数数组偏移 (64-bytes below from start of this line)
0000000000000000000000000000000000000000000000000000000000000040
// `address to` param
000000000000000000000000f8e81d47203a594245e36c48e151709f0c19fbe8
// `ids` 数组长度:3
0000000000000000000000000000000000000000000000000000000000000003
// 第一个参数 `ids` 元素
00000000000000000000000000000000000000000000000000000000000004d2
// 第二个参数 `ids` 元素
00000000000000000000000000000000000000000000000000000000000011d7
// 第三个参数 `ids` 元素
00000000000000000000000000000000000000000000000000000000000022ce
请注意,数组参数是由一个偏移量来代表数组的开始位置。然后我们转到第二个参数,地址
类型,然后完成数组类型。
现在我们知道了如何读取静态参数和动态参数,让我们来剖析一个更复杂的例子!
我们将从这个[4]交易中得到一个 UniswapV3 multicall 的输入 calldata,在这里,用户从 multicall 函数中调用 3 个不同的函数:
Etherscan 很好地给了我们一个简单的解码版本:
MethodID: 0xac9650d8
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000060
0000000000000000000000000000000000000000000000000000000000000120
00000000000000000000000000000000000000000000000000000000000002c0
0000000000000000000000000000000000000000000000000000000000000084
13ead56200000000000000000000000061fe7a5257b963f231e1ef6e22cb3b4c
6e28c531000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead908
3c756cc200000000000000000000000000000000000000000000000000000000
00002710000000000000000000000000000000000000000000831162ce86bc88
052f80fd00000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000164
8831645600000000000000000000000061fe7a5257b963f231e1ef6e22cb3b4c
6e28c531000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead908
3c756cc200000000000000000000000000000000000000000000000000000000
00002710ffffffffffffffffffffffffffffffffffffffffffffffffffffffff
fffaf17800000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000002e3bdc2534919
6582d720000000000000000000000000000000000000000000000000c249fdd3
2778000000000000000000000000000000000000000000000002e1e525c2ef9d
cec50c53000000000000000000000000000000000000000000000000c1cd7c9a
dfb0d9dc000000000000000000000000ed6c2cb9bf89a2d290e59025837454bf
1f144c5000000000000000000000000000000000000000000000000000000000
635ce8bf00000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000004
12210e8a00000000000000000000000000000000000000000000000000000000
我们将对其进行一些修改,并在此基础上逐行展开,使其更具有可读性。请记住,每个值都是十六进制格式,"20 个十六进制==32 字节",以便快速参考。
MethodID: 0xac9650d8
// 数组_1 的偏移 (starting next line)
0000000000000000000000000000000000000000000000000000000000000020
// 数组_1 的长度 (how many elements in array)
0000000000000000000000000000000000000000000000000000000000000003
// 数组_1中 第一个元素 数组_1A 的偏移 (96-bytes / 32 = 3)
0000000000000000000000000000000000000000000000000000000000000060
// 数组_1中 第二个元素 数组_1B 的偏移 (288-bytes / 32 = 9)
0000000000000000000000000000000000000000000000000000000000000120
// 数组_1中 第三个元素 数组_1C 的偏移 (704-bytes / 32 = 22)
00000000000000000000000000000000000000000000000000000000000002c0
// 数组_1A 的长度 (132-bytes (inc. selector))
000000000000000000000000000000000000000000000000000000000000008
// 读接下来的 132 个字节
// 函数选择器; 4 of 132
13ead562
// 1st param; 36 of 132
00000000000000000000000061fe7a5257b963f231e1ef6e22cb3b4c6e28c531
// 2nd param; 68 of 132
000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
// 3rd param; 100 of 132
0000000000000000000000000000000000000000000000000000000000002710
// 4th param; 132 of 132
// this marks the end of array_1A
000000000000000000000000000000000000000000831162ce86bc88052f80fd
// 32-bytes of `0` indicating next elemet
0000000000000000000000000000000000000000000000000000000000000000
// length 2nd element of array_1, array_1B (356-bytes (inc. selector))
// we have 4-bytes missing due to the embedded fn selector, 13ead562
// the next fn selector, 88316456, will be inserted here
00000000000000000000000000000000000000000000000000000164
// 读接下来的 356 个字节
// 函数选择器; 4 of 356
88316456
// 1st param; 36 of 356
00000000000000000000000061fe7a5257b963f231e1ef6e22cb3b4c6e28c531
// 2nd param; 68 of 356
000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
// 3rd param; 100 of 356
0000000000000000000000000000000000000000000000000000000000002710
// 4th param; 132 of 356
// notice how all the `0`s are `f`s. this indicates a `int` type!
fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffaf178
// 5th param; 164 of 356
// we have 32-bytes of `0`, but since we're still reading the bytes
// we know this is a paramter, representing 0 of a type
0000000000000000000000000000000000000000000000000000000000000000
// 6th param; 196 of 356
00000000000000000000000000000000000000000002e3bdc25349196582d720
// 7th param; 228 of 356
000000000000000000000000000000000000000000000000c249fdd327780000
// 8th param; 260 of 356
00000000000000000000000000000000000000000002e1e525c2ef9dcec50c53
// 9th param; 292 of 356
000000000000000000000000000000000000000000000000c1cd7c9adfb0d9dc
// 10th param; 324 of 356
000000000000000000000000ed6c2cb9bf89a2d290e59025837454bf1f144c50
// 11th param; 356 of 356
// this marks the end of array_1B
00000000000000000000000000000000000000000000000000000000635ce8bf
// 32-bytes of `0` indicating next elemet
0000000000000000000000000000000000000000000000000000000000000000
// this is the same thing as before, the length!
// we can see there's only 32-bytes left so we can conclude
// that it's going to be a fn with no inputs
00000000000000000000000000000000000000000000000000000004
// a call to the fn selector 12210e8a; 4 of 4
12210e8a00000000000000000000000000000000000000000000000000000000
现在你已经能够读取原始的嵌入式动态类型了!
我希望这些信息能够帮助你理解 calldata 是如何编码、解码和读取的。为了学习,我花了一些时间来研究和试验这一切,但这是值得的。从这里开始的下一步是学习如何读取字节码,以便在最底层了解 EVM(然后一切都变得开源了>:D)。
原文链接:https://degatchi.com/articles/reading-raw-evm-calldata
[1]
登链翻译计划: https://github.com/lbc-team/Pioneer
[2]
翻译小组: https://learnblockchain.cn/people/412
[3]
Tiny 熊: https://learnblockchain.cn/people/15
[4]
这个: https://etherscan.io/tx/0x31a45e8893f0cc7de009da5546539f703ed725d076ccdf73d307df5caa8c72b3
Twitter : https://twitter.com/NUpchain Discord : https://discord.gg/pZxy3CU8mh