# Calldata数据结构解析

By [Fuzzingq](https://paragraph.com/@liusy) · 2024-05-30

---

在研究multicall+ERC2771造成任意地址欺骗的漏洞时发现自己对calldata的内部数据结构不太明了。因此特地研究了下,在此记录。

### 什么是calldata

calldata是智能合约调用时按照约定规范做编码和结构化处理后的数据。client在调用合约函数时，通过abi.encodeWithSignature或abi.encodeWithSelector可以把函数选择器和参数列表打包成calldata数据。

### calldata中如何存储数据

在calldata中,约定俗成的前4个bytes为函数选择器。函数选择器由函数原型进行keccak-256计算生成，如：

    bytes4 selector = bytes4(keccak256("transfer(address,uint256)"))
    

在函数选择器之后的就是编码过的实参。根据参数数据类型不同，在calldata的存储方式也不相同（以32字节作为一个编码单元）。 大致的存储编排方式如下:

*   int uint address bool等类型的参数值会以16进制原样存储
    
*   strings array 等类型的参数值 会以偏移量 + 长度的方式来定义存储:先通过偏移量定位到偏移量，再通过32字节定义的偏移量找到数据的具体存储位置
    

### 例1

如果调用如下函数

    function transfer(uint256 amount, address to) external;
    
    // calldatar如下
    0xb7760c8f000000000000000000000000000000000000000000000000000000004d866d9200000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45
    
    // 格式化成之后
    0xb7760c8f
    000000000000000000000000000000000000000000000000000000004d866d92
    00000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45
    

按照上方所说，前4个字节为函数选择器，可以通过cast 4byte 0xb7760c8f还原出函数原型

![](https://storage.googleapis.com/papyrus_images/bc78a7bf2517c93979dadc844ad13b736bb9b73dc9216df38a9f59ab31d0ec65.png)

该函数的两个参数分别为uint256 address类型，均为静态。所以会原样存储。

uint amount = 0x4d866d92

address to = 0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45

### 例2

调用如下函数

    transfer(uint256[] amount, address to);
    
    // 产生如下calldata
    
    0x8229ffb60000000000000000000000000000000000000000000000000000000000000040000000000000000000000000f8e81d47203a594245e36c48e151709f0c19fbe8000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000004d200000000000000000000000000000000000000000000000000000000000011d700000000000000000000000000000000000000000000000000000000000022ce
    
    // 格式化之后
    0x8229ffb6
    0000000000000000000000000000000000000000000000000000000000000040
    000000000000000000000000f8e81d47203a594245e36c48e151709f0c19fbe8
    0000000000000000000000000000000000000000000000000000000000000003
    00000000000000000000000000000000000000000000000000000000000004d2
    00000000000000000000000000000000000000000000000000000000000011d7
    00000000000000000000000000000000000000000000000000000000000022ce
    

通过cast 4byte 0x8229ffb6

由于第一个参数为uint256\[\]类型，存储方式为偏移量+长度，因此第一个32字节0x40=64 也就是2个32字节。因此第一个参数的实际存储位置为2个32字节偏移之后。然后是第二个32字节，由于是address类型的参数 因此原样存储。

address to = 0xf8e81d47203a594245e36c48e151709f0c19fbe8

然后我们看到2个32字节之后的下一个32字节 值为0x3 也就uint\[\]的长度为3。也就是之后的三个32字节 就是原样存储的uint值了。 分别为: 0x4d2 0x11d7 0x22ce

### 例3

如下calldata 怎么反推出函数原型和参数？

#### step 1

先看第一个4字节 cast 4byte 0xac9650d8 -> multicall(bytes\[\]) 由于是array问题，采用偏移+长度的方式存储。因此偏移量为第一个32字节：0x20。偏移到第二个32字节，也就是长度值：0x3 说明bytes\[\]数组长度为3。而bytes也是一个动态类型，存储方式也是偏移+长度。所以之后的3个32字节：0x60 0x120 0x2c0分别为 bytes\[0\] bytes\[1\] 和bytes\[2\]的起始地址（存储长度的地址）

0x60 : 3个32字节 0x120: 9个32字节 0x2c0：22个32字节

#### step2

跳到偏移0x60处，看到值为0x84 就是说bytes\[0\]的长度为0x84个字节，而又由于已知函数为multicall，所以每个bytes实际上是子函数的calldata：

    13ead56200000000000000000000000061fe7a5257b963f231e1ef6e22cb3b4c
    6e28c531000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead908
    3c756cc200000000000000000000000000000000000000000000000000000000
    00002710000000000000000000000000000000000000000000831162ce86bc88
    052f80fd
    
    // 格式化之后
    13ead562
    00000000000000000000000061fe7a5257b963f231e1ef6e22cb3b4c6e28c531
    000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
    0000000000000000000000000000000000000000000000000000000000002710
    000000000000000000000000000000000000000000831162ce86bc88052f80fd
    

其中前四个字节就是子函数的选择器 通过cast 4byte 13ead562 -> createAndInitializePoolIfNecessary(address,address,uint24,uint160)

函数参数分别是address address uint24 uint160，均属于静态类型。原样存储。因此可以分析出multicall调用的三个子函数中第一个函数如下： createAndInitializePoolIfNecessary(address,address,uint24,uint160) 参数1 address = 61fe7a5257b963f231e1ef6e22cb3b4c6e28c531 参数2 address = c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 参数3 uint24 = 0x2710 参数4 uint160 = 0x831162ce86bc88052f80fd

#### step3

同理我们走到0x120: 9个32字节这里 看到值为0x164 也就是bytes\[1\]长度为0x164个字节,整理之后为

其中cast 4byte 88316456 -> mint((address,address,uint24,int24,int24,uint256,uint256,uint256,uint256,address,uint256)) 均为静态类型 依次往下找即可，最终调用参数为：

    mint(
        address(0x61fe7a5257b963f231e1ef6e22cb3b4c6e28c531),
        address(0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2),
        0x2710,
        0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffaf178,
        0,
        0x2e3bdc25349196582d720,
        0xc249fdd327780000,
        0x2e1e525c2ef9dcec50c53,
        0xc1cd7c9adfb0d9dc,
        address(0xed6c2cb9bf89a2d290e59025837454bf1f144c50),
        0x635ce8bf
    )
    

以此类推，最后一个bytes\[2\]也就很好解析了。

cast 4byte 12210e8a → refundETH()

最终的calldata切分结果：

![](https://storage.googleapis.com/papyrus_images/b9de7714a30cdf18819ab4142f13120149ba0c5db4e0c969cab6da9804e9878c.png)

---

*Originally published on [Fuzzingq](https://paragraph.com/@liusy/calldata)*
