写点什么

使用 Echidna 进行智能合约库测试的完整指南

作者:qife122
  • 2025-08-23
    福建
  • 本文字数:4093 字

    阅读完需:约 13 分钟

使用 Echidna 测试智能合约库 - Trail of Bits 博客

Alex Groce


2020 年 8 月 17 日


区块链, 模糊测试


本文将展示如何使用 Echidna 模糊测试器测试智能合约。具体内容包括:


  1. 通过差异模糊测试的变体发现我们在 Set Protocol 审计期间找到的一个 bug

  2. 为您自己的智能合约库指定和检查有用属性


我们将演示如何通过 crytic.io 完成所有这些操作,该平台提供 GitHub 集成和额外的安全检查。

库可能引入风险

在单个智能合约中发现 bug 至关重要:一个合约可能管理重要的经济资源(无论是代币还是以太坊),漏洞造成的损失可能达数百万美元。但可以说,以太坊区块链上存在比任何单个合约更重要的代码:库代码。


库可能被许多高价值合约共享,因此,比如 SafeMath 中一个微妙的未知 bug 可能允许攻击者利用不止一个而是多个关键合约。这种基础设施代码的关键性在区块链环境之外也得到了充分理解——广泛使用的库(如 TLS 或 sqlite)中的 bug 具有传染性,可能感染所有依赖该易受攻击库的代码。


库测试通常侧重于检测内存安全漏洞。然而,在区块链上,我们不太担心避免堆栈冲突或从包含私钥的区域进行 memcpy;我们最担心的是库代码的语义正确性。智能合约在"代码即法律"的金融世界中运行,如果库在某些情况下计算出错误结果,这种"法律漏洞"可能传播到调用合约,并允许攻击者使合约行为异常。


此类漏洞可能产生其他后果,而不仅仅是使库产生错误结果;如果攻击者可以强制库代码意外恢复,他们就拥有了潜在拒绝服务攻击的关键。如果攻击者可以使库函数进入失控循环,他们可以将拒绝服务与昂贵的 gas 消耗结合起来。


这就是 Trail of Bits 在管理地址数组的库的旧版本中发现的 bug 的本质,如 Set Protocol 代码审计中所述。


有问题的代码如下:


/*** 返回是否存在重复项。以O(n^2)运行。* @param A 要搜索的数组* @return 如果存在重复项则返回true,否则返回false*/function hasDuplicate(address[] memory A) returns (bool){   for (uint256 i = 0; i < A.length - 1; i++) {     for (uint256 j = i + 1; j < A.length; j++) {       if (A[i] == A[j]) {          return true;       }     } } return false;}
复制代码


问题是如果 A.length 为 0(A 为空),那么 A.length - 1 会下溢,外部(i)循环会遍历整个 uint256 值集。在这种情况下,内部(j)循环不会执行,因此我们有一个紧密的循环(基本上)永远什么都不做。当然,这个过程总是会耗尽 gas,而进行 hasDuplicate 调用的交易将失败。如果攻击者可以在正确的位置产生一个空数组,那么使用 hasDuplicate 对地址数组强制执行某些不变量的合约可能会被禁用——可能是永久性的。

有关详细信息,请参阅我们的示例代码,并查看有关使用 Echidna 的教程。


在高层次上,该库提供了用于管理地址数组的便捷函数。典型用例涉及使用地址白名单进行访问控制。AddressArrayUtils.sol 有 19 个要测试的函数:


function indexOf(address[] memory A, address a)function contains(address[] memory A, address a)function indexOfFromEnd(address[] A, address a)function extend(address[] memory A, address[] memory B)function append(address[] memory A, address a)function sExtend(address[] storage A, address[] storage B)function intersect(address[] memory A, address[] memory B)function union(address[] memory A, address[] memory B)function unionB(address[] memory A, address[] memory B)function difference(address[] memory A, address[] memory B)function sReverse(address[] storage A)function pop(address[] memory A, uint256 index)function remove(address[] memory A, address a)function sPop(address[] storage A, uint256 index)function sPopCheap(address[] storage A, uint256 index)function sRemoveCheap(address[] storage A, address a)function hasDuplicate(address[] memory A)function isEqual(address[] memory A, address[] memory B)function argGet(address[] memory A, uint256[] memory indexArray)
复制代码


看起来很多,但许多函数效果相似,因为 AddressArrayUtils 提供了 extend、reverse、pop 和 remove 的功能版本(对内存数组参数进行操作)和变异版本(需要存储数组)。您可以看到,一旦我们为 pop 编写了测试,为 sPop 编写测试可能不会太困难。

基于属性的模糊测试 101

我们的工作是获取我们感兴趣的函数——这里是所有函数——然后:


  1. 弄清楚每个函数的作用

  2. 编写测试确保函数做到这一点!


当然,一种方法是编写大量单元测试,但这有问题。如果我们想彻底测试库,这将是一项繁重的工作,而且坦率地说,我们可能会做得很糟糕。我们确定能想到每个边缘情况吗?即使我们尝试覆盖所有源代码,像 hasDuplicate bug 这样涉及缺失源代码的 bug 也很容易被遗漏。


我们希望使用基于属性的测试来指定所有可能输入的一般行为,然后生成大量输入。编写行为的一般描述比编写任何具体的"给定输入 X,函数应该做/返回 Y"测试更困难。但是编写所有需要的具体测试的工作量将过大。最重要的是,即使做得非常好的手动单元测试也找不到攻击者寻找的那种奇怪边缘情况 bug。

Echidna 测试工具:hasDuplicate

测试库的代码最明显的是它比库本身还大!在这种情况下这并不罕见。不要被吓倒;与库不同,测试工具可以作为进行中的工作来处理,并慢慢改进和扩展,效果很好。测试开发本质上是增量的,如果您有像 Echidna 这样的工具来放大您的投资,即使很小的努力也能提供相当大的好处。


对于一个具体例子,让我们看看 hasDuplicate bug。我们想检查:


  1. 如果存在重复项,hasDuplicate 会报告它

  2. 如果没有重复项,hasDuplicate 会报告没有


我们可以重新实现 hasDuplicate 本身,但这在一般情况下没有太大帮助(在这里,它可能让我们找到 bug)。如果我们有另一个独立开发的高质量地址数组实用程序库,我们可以比较它,这种方法称为差异测试。不幸的是,我们通常没有这样的参考库。


我们这里的方法是应用较弱版本的差异测试,通过寻找库中另一个可以不调用 hasDuplicate 检测重复项的函数。为此,我们将使用 indexOf 和 indexOfFromEnd 来检查项目的索引(从 0 开始)是否与从数组末尾执行搜索时的索引相同:


for (uint i = 0; i < addrs1.length; i++) {  (i1, b) = AddressArrayUtils.indexOf(addrs1, addrs1[i]);  (i2, b) = AddressArrayUtils.indexOfFromEnd(addrs1, addrs1[i]);  if (i1 != (i2-1)) { // -1 因为fromEnd返回偏移1    hasDup = true;  }}return hasDup == AddressArrayUtils.hasDuplicate(addrs1);}
复制代码


在我们的 addressarrayutils 演示中查看完整的示例代码


此代码遍历 addrs1 并找到每个元素第一次出现的索引。如果没有重复项,当然,这将始终是 i 本身。代码然后找到元素最后一次出现的索引(即从末尾)。如果这两个索引不同,则存在重复项。在 Echidna 中,属性只是布尔 Solidity 函数,通常如果满足属性则返回 true(我们将在下面看到例外),如果它们恢复或返回 false 则失败。现在我们的 hasDuplicate 测试正在测试 hasDuplicate 和两个 indexOf 函数。如果它们不一致,Echidna 会告诉我们。


现在我们可以添加几个要模糊设置的函数来设置 addrs1。


让我们在 Crytic 上运行此属性:


hasDuplicate 的属性测试在 Crytic 中失败


首先,crytic_hasDuplicate 失败:


crytic_hasDuplicate: failed!  Call sequence:    set_addr(0x0)
复制代码


触发交易序列极其简单:不要向 addrs1 添加任何内容,然后对其调用 hasDuplicate。就这样——由此产生的失控循环将耗尽您的 gas 预算,Crytic/Echidna 将告诉您属性失败。当 Echidna 将失败最小化为最简单的可能序列时,会产生 0x0 地址。


我们的其他属性(crytic_revert_remove 和 crytic_remove)通过,这很好。如果我们修复 hasDuplicate 中的 bug,那么我们的所有测试都将通过:


所有三个属性测试现在在 Crytic 中通过


crytic_hasDuplicate: fuzzing (2928/10000)告诉我们,由于昂贵的 hasDuplicate 属性没有快速失败,在我们达到五分钟的超时之前,每个属性最多 10,000 个测试中只执行了 3,000 个。

Echidna 测试工具:库的其余部分

现在我们已经看到了一个测试示例,以下是构建其余测试的一些基本建议(正如我们为 addressarrayutils_demo 存储库所做的那样):


  • 尝试不同的方式计算相同的东西。您拥有的函数的"差异"版本越多,就越有可能发现其中一个是否错误。例如,查看我们交叉检查 indexOf、contains 和 indexOfFromEnd 的所有方式。

  • 测试恢复。如果您像我们这里一样在属性名称前添加前缀_revert_,则属性仅在所有调用都恢复时才通过。这确保代码在应该失败时失败。

  • 不要忘记检查明显的简单不变量,例如,数组与自身的差异始终为空(ourEqual(AddressArrayUtils.difference(addrs1, addrs1), empty))。

  • 其他测试中的不变量检查和前提条件也可以作为测试函数的交叉检查。请注意,hasDuplicate 在许多并非旨在检查 hasDuplicate 的测试中被调用;只是知道数组无重复可以在许多其他行为中建立额外的不变量,例如,在任何位置删除地址 X 后,数组将不再包含 X。

使用 Crytic 启动和运行

您可以通过下载和安装工具或使用我们的 docker 构建来自己运行 Echidna 测试——但使用 Crytic 平台将基于属性的 Echidna 测试、Slither 静态分析(包括公共版本 Slither 中不可用的新分析器)、可升级性检查以及您自己的单元测试集成到与版本控制相连的无缝环境中。此外,addressarrayutils_demo 存储库显示了基于属性测试所需的一切:它可以像创建最小的 Truffle 设置、添加带有 Echidna 属性的 crytic.sol 文件以及在 Crytic 中的存储库配置中打开基于属性的测试一样简单。


立即注册 Crytic,如果您有问题,请加入我们的 Slack 频道(#crytic)或在 Twitter 上关注 @CryticCI。


如果您喜欢这篇文章,请分享:TwitterLinkedInGitHubMastodonHacker News


页面内容


近期帖子


使用 Deptective 调查您的依赖项


系好安全带,Buttercup,AIxCC 的评分回合正在进行中!


使您的智能合约超越私钥风险


Go 解析器中意外的安全隐患


我们审查首批 DKLs23 库的收获


来自 Silence Laboratories 的库


© 2025 Trail of Bits。


使用 Hugo 和 Mainroad 主题生成。更多精彩内容 请关注我的个人公众号 公众号(办公 AI 智能小助手)公众号二维码


办公AI智能小助手


用户头像

qife122

关注

还未添加个人签名 2021-05-19 加入

还未添加个人简介

评论

发布
暂无评论
使用Echidna进行智能合约库测试的完整指南_区块链_qife122_InfoQ写作社区