# Calldata数据结构解析 **Published by:** [Fuzzingq](https://paragraph.com/@liusy/) **Published on:** 2024-05-30 **URL:** https://paragraph.com/@liusy/calldata ## Content 在研究multicall+ERC2771造成任意地址欺骗的漏洞时发现自己对calldata的内部数据结构不太明了。因此特地研究了下,在此记录。什么是calldatacalldata是智能合约调用时按照约定规范做编码和结构化处理后的数据。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还原出函数原型该函数的两个参数分别为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 = 0x831162ce86bc88052f80fdstep3同理我们走到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切分结果: ## Publication Information - [Fuzzingq](https://paragraph.com/@liusy/): Publication homepage - [All Posts](https://paragraph.com/@liusy/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@liusy): Subscribe to updates - [Twitter](https://twitter.com/_fuzzingq): Follow on Twitter