为什么要学习 Solidity 困难的东西 [ ABI 编码系列:第 0 部分 ]

本文主要讨论了Solidity智能合约开发中ABI编码的重要性。

Solidity 很简单。

它是一种简单而优美的语言。

随着优秀的教育资源、课程、开发工具和 LLM 的兴起,学习和编写 Solidity 从未如此简单。

但这里有个残酷的真相 —— 如果每个人都很容易上手,那么脱颖而出就同样困难。

部署一个基本的 ERC20 合约就能赢得“智能合约工程师”称号的时代已经一去不复返了。

今天,精通意味着对 Solidity 的理解要超越表面。

GPT4 和 Claude 在编写基本到中级 Solidity、Foundry 测试用例,甚至理解像 Uniswap v2、Safe 等流行的代码库方面,正变得异常熟练。

那么,作为智能合约开发者,你该怎么做呢?

通过在 Solidity 和 EVM 中获得更深入的专业知识,做困难的事情

编写基本的 solidity 智能合约 - 简单。

编写 gas 优化的、高度安全的智能合约,并对底层 EVM 工作原理有更深入的理解 - 这才是困难的。

我希望你的目标是做困难的事情。

并且记住:

做困难的事情不再是一种选择,

这是一种必需。

我在 Decipher Club 的主要目标之一是创建资源,以帮助 web3 构建者提升水平并脱颖而出。

我写了一篇关于 EVM 深度剖析的长篇文章系列,并创建了 Decipher EVM Puzzles,目的也是如此。

现在,我将开始一个关于 ABI 编码和解码的新系列—— 这是在 Solidity-EVM 中要掌握的最关键但也是最困难的主题之一。

本系列的目的是让你全面了解编码的实际工作方式。最重要的是,我希望你有一个基本的思维模型:

  • 可以自己创建一个给定类型的原始 calldata 十六进制,逐字节地,
  • 可以轻松地 解读 任何给定的 calldata,并理解它的各个部分和布局。

我在我的其中一个 推文 中解释了为什么掌握 ABI 编码和解码很重要。

对于 Solidity 开发者来说,abi 编码/解码是要掌握的关键技能

你在掌握它的时候,实际上学习了大量的关键概念:

- little and big endianness(大小端)

- calldata 的复杂性

- 数据偏移量

- 静态类型与动态类型的存储

- 函数选择器和参数编码

-… pic.twitter.com/eWzmCQVWMy

— Zaryab (@zaryab_eth) May 22, 2025

但是,这是一个扩展版本,旨在鼓励你深入研究它 并阅读本文章系列的后续部分。

让我们开始吧。


为什么要学习 ABI 编码和解码?

在学习 Solidity 时,大多数开发者都在表面上进行交互:编写函数,部署合约,通过像 Remix、Hardhat 或 Foundry 这样的工具来调用它们,并让库来处理剩下的事情。

然而,在表面之下,存在着一个强大而精确的通信协议 —— ABI,或 应用程序二进制接口 —— 它精确地定义了合约如何相互通信以及与外部世界通信。

如果你曾经调用过链上的一个函数,用 ethers.js 编写过一个脚本,或者在你的智能合约中使用过 abi.encode(),那么你已经 使用 了 ABI 编码。

但是,

  • 有没有想过什么会被发送到 EVM?
  • 有没有看过原始的 calldata 十六进制,然后想 这他妈的是什么鬼东西

让我解释一下为什么你应该学习它。

ABI 编码无处不在

Solidity 可能是你编写的语言,

但 ABI 编码的语言是 EVM 说的语言

每个函数调用都是 ABI 编码的

ABI 编码是将高级 Solidity 值 —— 比如数字、字符串、数组、结构体和函数参数 —— 转换为 以太坊虚拟机 (EVM) 可以理解、传输和执行的 标准化二进制格式 的过程。

在 Solidity 中,这种编码遵循 ABI 规范,它确保:

  • 函数调用可以在合约和工具之间被理解
  • 数据结构以可预测的、类型感知的格式排列
  • 合约可以相互通信以及与链下客户端通信等等。

例如,当你调用:

myContract.foo(42, "hello");

你的钱包或脚本发送的是一个十六进制 blob(calldata)—— 类似于:

0xa9059cbb000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000068656c6c6f

这个 blob 包含:

  • 一个 4 字节的函数选择器
  • 编码后的参数,经过填充、偏移和精确对齐

如果合约错误地解释了哪怕是 一个字节,调用也会失败或行为异常。

但是,如果大部分的编码-解码都由库和 EVM 本身来处理,为什么你还要费心学习 ABI 编码是如何工作的呢?

这里有一些非常明显的答案。

为什么要学习 ABI 编码?

深入理解 ABI 编码不仅可以解锁 calldata 的解码,还可以将你的 Solidity 开发提升到低级别的、系统感知的工程水平。

这里有一个区分中级 Solidity 开发者和高级开发者的简单方法:

你对默认 ABI 编码的理解有多深?

如果你可以描述如何编码一个结构体数组,而这些结构体本身又包含结构体数组,那么你就是高级开发者

(GPT-4?中级 Solidity 开发者)

— 0age (@z0age) March 15, 2023

a. 你会获得一个完整的学习包

学习 ABI 编码规范会让你学到比编码技术本身更多的关于 EVM 的知识。

例如,在理解编码机制的同时,你最终会学到一些关键的概念,比如:

所有这些概念对于你掌握以太坊虚拟机的复杂性 和美感 同样至关重要。

这里有一些问题:

a. 到底什么是 endianness(大小端)?solidity 使用哪种类型?

b. addresses 和 bytes 的填充是如何工作的?

c. 偏移量是如何工作的?静态类型和动态类型参数编码都有偏移量吗?

d. calldata 在 gas 优化中扮演什么角色?

e. 参数的类型(静态 vs 动态)为什么在编码中很重要?

如果你没有这些问题的答案,那么你对 solidity-evm 中 ABI 编码机制的了解就不够充分。

你将从本系列文章中受益最多。

b. 你的调试技能会突飞猛进

当合约无声地失败或以不透明的消息回退时,能够读取原始 calldata 是猜测和知道之间的区别。

如果你知道如何轻松地读取原始 calldata,那么调试复杂的回退或逆向工程对你来说就不会是一场噩梦。

而这正是你与众不同的地方。

凭借对 ABI 编码工作原理的充分了解,你将能够:

  • 将选择器与函数匹配
  • 追踪格式错误的 calldata
  • 识别未对齐的偏移量或错误的填充
  • 查明导致回退的参数

在安全审计、漏洞分析或取证中,你通常只会收到交易的 calldata

对 ABI 编码/解码的扎实理解可以让你将原始 calldata 解构为函数参数,识别被篡改的 payload 或解码恶意构造的偏移量。

例如,报告错误需要深入了解编码的工作原理:


c. 你可以编写高度优化的代码

Calldata 不是免费的 —— calldata 中的每个字节都要花费 gas。

自从 EIP-2028: 之后

  • calldata 中每个非零字节的成本 - 16 gas
  • calldata 中每个零字节的成本 - 4 gas

回想一下我之前说过的话:成为一个更好的智能合约开发者意味着能够理解和编写高度 gas 优化的智能合约

深入了解 ABI 编码可以让你:

  • 折叠未使用的或冗余的字节

  • 优化动态类型布局(例如,压缩数组的头部/尾部)

  • abi.encodeabi.encodePacked手动编码 之间进行选择

  • 在内联汇编中编写内存高效的 calldata 构建器

    • *

d. 你可以深入 理解 abi.encode vs encodePacked vs encodeWithSelector

这三个方法不是可以互换的。

每个方法都有精确的规则,用于 填充,对齐,抗冲突性,尾部编码等等。

理解 ABI 规则可以让你清楚地知道何时使用:

  • abi.encode → 完全符合 ABI 的 32 字节对齐
  • abi.encodePacked → 原始字节打包(例如,哈希)
  • abi.encodeWithSelector → calldata 构造

它可以帮助你避免在混合使用它们时产生的潜在错误,尤其是在自定义哈希或代理环境中。


e. 用于 模糊测试和边界测试

对于像 Echidna、Foundry 或自定义工具这样的模糊测试器,你通常希望:

  • 构建边缘案例 ABI payload
  • 使用无效或截断的编码来测试行为
  • 手动翻转字节或改变偏移量

只有当你深入理解结构体、数组、动态值和嵌套类型的 ABI 布局时,这才有可能。


f. ABI 编码不仅仅是函数参数

Solidity ABI 规范不仅仅是一个用于构建交易 calldata 的古怪约定。

它是 EVM 的基础通信协议,其影响范围涵盖函数、事件、错误、内存模型、工具和链下集成。

理解编码的工作原理不仅可以让你掌握函数参数和 calldata 编码,它还涵盖了更多内容。

以下是 Solidity 中依赖于底层 ABI 编码的扩展领域:

a. 事件

  • 事件使用 topics(对于索引参数)和 data blobs(对于未索引的参数)进行编码。
  • 索引的动态类型(如 stringbytes)使用 keccak256 进行哈希 —— 意味着如果你想 查询日志 或编写事件解析器,你必须理解 ABI 的事件编码规则。

b. 自定义错误

  • Solidity 支持自定义错误类型,它 遵循与函数完全相同的 ABI 编码规则
  • 这意味着像 InsufficientBalance(uint256 available, uint256 required) 这样的错误数据就像调用该函数一样进行编码,包含一个 4 字节的选择器和一些参数。

c. 返回值

  • ABI 编码不仅定义了如何传递函数参数,还定义了 如何构造返回值
  • 返回多个值的函数(returns (uint, string))被编码为一个 元组,与输入参数一样,遵循相同的 head-tail 逻辑。(head-tail 逻辑是我将在后续部分详细解释的内容)

d. 内存和 Calldata 布局

  • 在编写内联汇编或低级内存操作代码时,ABI 编码定义了内存中动态值的 偏移模型
  • calldata 数组或字符串的 .offset.length 成员之所以有效,是因为它们的 ABI 定义的布局。

e. 工具和 JSON ABI

  • 合约中的每个接口定义(函数、事件、错误)都被序列化为一个 JSON ABI —— 它反映了 规范的 ABI 类型定义,包括元组和索引参数。

  • 像 Hardhat、Foundry、Ethers.js、Wagmi 和 MetaMask 这样的工具都依赖于这个 ABI 模式。

    • *

总结

就这样。

我试图提供许多理由来解释为什么:

  • solidity 智能合约开发者应该选择学习 困难的事情,
  • 以及为什么 ABI 编码规范是开始深入研究 EVM 和 Solidity 的一个好主题。

在本系列的后续部分中,我的目标是让你掌握 ABI 编码工作原理的坚实的思维模型。我将尝试尽可能多地举例,以涵盖各种类型的编码机制。

我可能还会发布一个 Decipher ABI Games( 类似于 Decipher EVM puzzles )。敬请关注。

我们下一部分见。

  • 原文链接: decipherclub.com/why-lea...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
decipherclub
decipherclub
江湖只有他的大名,没有他的介绍。