ABI解码最关键的不是记十六进制,而是先划清坐标系。函数calldata前4字节是selector,后面才是参数编码区;动态参数head里保存的offset,通常相对于参数区起点计算,不把selector算进去。
从f(uint256,string)开始
假设调用f(7,“hi”)。selector之后的参数区可拆成四个32字节槽:
- 第一个head槽直接放静态uint256值7。
- 第二个head槽放string数据相对参数区的offset。因为head共有64字节,offset是64。
- tail第一个槽放字符串字节长度2。
- tail随后写入hi的UTF-8字节,并在右侧补零到32字节边界。
这里的offset指向“长度槽”,不是直接指向字符h。若从整段calldata的第0字节切片,需要在offset外再加4字节selector长度;许多手工解码正是在这里错位。
| 参数类型 | head中存什么 | tail中存什么 |
|---|---|---|
| uint256 / address / bytes32 | 直接编码值 | 无 |
| bytes / string | 指向tail的offset | length + data + padding |
| 动态数组 | 指向tail的offset | length + 每个元素编码 |
| 含动态成员的tuple | tuple位置或offset | tuple自己的head与tail |
嵌套offset是相对哪一层
ABI编码具有递归结构。动态数组的元素若也是动态类型,数组内容区会先列出各元素的offset,再放每个元素自己的length和data。tuple中的动态成员offset则相对于该tuple编码起点。解码器应在进入每一层时更新base,不要把所有offset都当成相对于整段calldata。
三类边界检查
第一,offset与length不能让读取越过数据末尾或发生整数溢出;第二,自写解析器要限制数组长度与内存分配,防止一段恶意输入耗尽资源;第三,要区分“可被当前解码器接受”和“符合规范的strict encoding”。严格模式要求动态数据offset按顺序、尽可能小且没有空隙,但Solidity当前ABI解码器并不强制strict mode。因此跨语言系统不能假定所有成功解码的数据都有唯一、规范的字节表示;用于签名或哈希时应由可信编码器重新产生标准编码。
abi.encodePacked不使用相同的head-tail结构。多个动态值紧密拼接时可能无法唯一拆分,因此不应把本页规则套到packed数据,也不要在签名哈希中拼接两个无边界动态字段。
调试时先找对数据类型
交易input、函数返回值、event data和revert data都使用ABI思想,但起始结构不同:函数input有selector;普通返回值没有;事件indexed参数进入topics,非indexed参数进入data;自定义错误前4字节是错误selector。先确定对象,再选择ABI与base位置。
事件日志可对照topics与data解码,revert结构可看Solidity自定义错误,历史calldata对应的合约实现若可升级,还要先核对ERC-1967实现地址。
发表评论
还没有评论,来说两句吧。
评论区为展示样式,提交不会被处理。