掌握Web3协议审计的有效测试编写技巧

  • mixbytes
  • 发布于 2024-09-25 19:38
  • 阅读 39

本文介绍了一种结构化的测试框架,旨在帮助Web3开发者编写有效的测试,从而捕捉严重的漏洞。文章强调了采用黑客思维、保持不变性思维和系统架构思维三个心态的重要性,以确保协议的安全性和可靠性。通过应用这些心态,开发者可以更准确地制定测试场景,以应对潜在的安全威胁。

image.png

介绍

编写测试对于确保协议的安全性和可靠性至关重要,尤其是在 web3 的世界中。然而,对达到 100% 代码覆盖率等常见建议常常产生误导,因为高覆盖率并不一定等同于安全性。另一个具有挑战性的建议是在测试开发过程中采用黑客思维。黑客思维不容易解释,并且需要丰富的经验和特定的流程才能有效实施。 考虑到行业的现状,许多新协议难以雇用经验丰富的开发者,使得新手在应对这一概念时常常陷入困境,通常缺乏关于合约开发的安全性方面的知识。 本文介绍了一种结构化的测试框架,以帮助开发者编写更深思熟虑、更加有效的测试,旨在捕捉协议中的严重错误。在对审计报告中的错误性质进行了广泛分析后,得出的结论是,仅有黑客思维不足以检测严重错误。除此之外,还可以应用其他两种思维。本文提出的框架鼓励采用三种独特的思维方式:黑客思维、不变性思维(或形式验证思维)和系统架构师思维。每种思维都旨在指导测试场景的创建,揭示潜在的漏洞和弱点。

测试框架概述

在我们公司,我们推广一种遵循这一原则的审计方法:审计是对代码生成问题并获得答案的过程。这听起来很简单,但这是一个强大的方法。任何现存错误的所有攻击向量都可以表示为关于特定合约或功能的问题。更重要的是,这种方法使审计人员能够更顺利地跟踪审计进展,尤其是在编写良好的协议中,错误很难被发现。在这种情况下,仅仅通过发现错误的数量跟踪进展可能会让安全研究人员感到沮丧。

这种方法也更容易向新的安全研究人员和 web3 协议开发者解释。因此,将测试框架呈现为一套可以为每个特定项目(包括各种协议的示例)进行调整的一般性问题是非常有意义的。这使得框架中的每种思维方式都可以简单地进行调整。

需要注意的是,该框架应在完成基础单元和集成测试后应用。虽然至少达到 80-90% 的测试覆盖率并覆盖所有基本用户流程的建议是有价值的,但仅有这些测试不足以确保你的协议抵御攻击。需要额外的安全层,而该框架可以提供这一层,帮助确保协议不仅能正确运作,且无漏洞。

正如下图所示,要使用该框架,你应逐一选择每种思维并回答与该思维相关的问题。这些问题的答案将生成一系列要实施的测试场景。框架的使用将针对每种思维进行详细解释,并附有示例。

黑客思维

黑客思维对许多开发者来说可能会感到沮丧,因为有效使用它需要作为安全研究员或白帽子黑客的经验。即使是像“与其问,这个应该如何工作? 不如问,这个 不应该如何工作?”这样简短而有影响力的解释,也并没有提供一套明确的行动计划来应用黑客思维(即,编写模拟潜在攻击向量的测试)。我们行业的目标是使协议安全且有韧性。如果不向新手解释黑客思维,实现这一目标将变得具有挑战性。

在本文中,我们准备了一系列具体的问题来指导开发者编写有意义的测试,而不是一系列行动步骤。这些问题旨在帮助你开发覆盖严重错误的测试,使用黑客思维。

问题 1:该协议存在哪些不应被普通用户访问的用例?

这个问题涉及由于访问管理不当而产生的潜在攻击向量。协议保护的一个关键层面是减少未经过验证(可能是恶意)用户可以访问的函数数量。请考虑当前允许任何用户的所有可能用例,并准备测试以确保特权函数只能被协议控制的授权角色访问。

例子:假设你的协议有一个函数,仅由协议控制的特定离线服务调用。这个问题暗示你需要确保该函数有适当的修饰符,并且你的测试验证它在被特权用户和随机用户调用的情况下的表现。

问题 2:用户的函数调用序列和频率应有哪些限制?

这个问题有助于识别重入漏洞。一旦在主网部署,任何用户都可以以任何顺序调用任何公共函数。测试应确认只能按照允许的合约使用流程进行调用。考虑所有应该限制的可能用例,并编写测试以验证协议阻止不允许的操作。

例子:你可能会写一个测试,确保某个函数在同一交易中(同一区块,相同时间间隔)不能与其他函数被调用,从而影响该函数的行为。这种约束可以保护协议不被恶意利用。

问题 3:用户输入参数应有什么约束?

这个问题鼓励进一步限制未经授权用户的合约使用。假设你已经限制了对特权函数的访问,并将用户流限制为仅允许预定场景。现在,考虑恶意用户是否可以传递意外值给函数,从而导致协议处理开发者未预见的情况。

例子:假设一个函数接受一个 uint256 输入,但内部将其强制转换为 uint128,假设它只会处理小于 type(uint128).max 的值。如果恶意用户操纵输入超出此限制,协议可能会继续运行但可能产生负面影响。预见所有输入参数的潜在约束,并准备测试以确认协议仅在允许的范围内运行。此步骤将通过缩小可能输入的范围来优化模糊测试和不变性测试。同时,考虑并潜在测试输入参数值的不同边界情况,例如 0、1、type(uint...).max - 1 等等,以避免因舍入和溢出可能出现的错误。

问题 4:哪些参数在调用协议时不应由用户控制?

此问题介于第一和第三问题之间。这里考虑的是输入参数的可接受值有限制,有时限制为预定义列表(白名单)或完全不受用户控制。例如,作为函数输入的 ERC20 代币地址可能需要限制,仅允许在批准列表上的代币。

例子:为一个在去中心化交易所(DEX)上执行交换的函数编写一个测试,以确保用户只能在白名单上的 DEX(例如 Uniswap、Curve 或 Balancer)中进行交换。这个问题鼓励你考虑应该由白名单控制而不是开放范围的参数,或甚至由协议在没有用户输入的情况下控制的参数。

如上所示,黑客思维应促使你考虑协议应强制实施的限制。这些限制中的每一项都应经过详细测试。不要害怕黑客思维;只需问自己这些问题,并在你的协议中实施必要的限制。

不变性思维

在尽可能多地限制你的协议后,是时候确保它按规范运行。不变性思维(或形式验证思维)是一个很好的工具。该思维方式侧重于识别代码中的不可破坏条件或不变条件。不变条件是指无论协议的状态如何都应保持为真的条件。该思维方式对于根据公式进行计算的函数特别有用。

可以使用形式验证方法来分析性证明某个不变条件总是成立。然而,这种思维方式面临的最大挑战是识别不变条件本身。拥有详细的协议规范在这个过程中非常有帮助,但对于许多旨在快速发布的新 DeFi 协议,通常缺乏这样的规范。

我们想强调,从明确的规范开始协议设计是极其有益的。在规范阶段,可以检测并解决许多问题。然而,这种方法需要额外的努力和时间,可能会有挑战性。如果你没有时间准备详细的规范,至少应编制一份关键不变条件的列表。

该思维方式的主要问题是:“代码中存在哪些不变条件?”正如前面提到的,这个问题最好适用于涉及计算的函数,因为在这些情况下更容易识别出有意义的不变条件。例如,如果你有一个用于计算代币在多个参与者之间如何分配的函数,那么一个不变条件可能是确保分配是平衡的,或者在分配后没有代币残留。具体的不变条件将依赖于函数的逻辑,因此你需要仔细定义它。

一个 Uniswap v4 类协议的常见不变条件示例是,检查确保交换中输出代币的数量不能超过特定池的余额。这个不变条件简单易行,并且可以适用于大多数去中心化交易所(DEX),确保用户资金在交换过程中得到保护。

另一个有用的不变条件是确保表示协议控制的代币总量的变量始终小于或等于合约的代币余额。这个检查使用“小于或等于”,因为在将 ERC20 代币直接转移到合约地址的情况下,严格的平等检查将无法通过。

需要注意的是,虽然提供形式验证服务的公司负责准备不变条件,但模糊测试技术也可以帮助验证它们。然而,模糊测试并不能保证不变条件在所有协议状态下都成立。如果形式验证服务在此阶段无法获得,则设置模糊测试非常重要。如 EchidnaFoundry 等工具在这里可能非常有效。

系统架构师思维

系统架构师思维对任何协议与另一个协议或外部服务的集成持高度悲观的态度。与专注于限制访问的黑客思维不同,系统架构师(SA)思维则是关于预测外部集成可能出现的故障。这需要具备一些黑客式的怀疑态度,但并不与不变性思维融合,因为形式验证所有潜在的外部集成在实践中是不切实际的。

这种思维方式考虑如果集成的服务或协议发生故障、被更新或被攻陷时,会发生什么。例如,离线服务的更新可能含有一个错误,导致它将毫秒误解为秒。一个预言机服务可能在达到某个阈值后停止更新价格信息(例如,在 LUNA 崩溃后,一些预言机提供商在达到某些阈值后停止更新价格)。一个外部的 DeFi 协议可能发布一个更新,更改奖励分配逻辑,或者增加需要查找特殊功能才能索取的代币。一个跨链桥可能因气体定价不当或内部错误而未能传递信息。总而言之,协议的外部生态系统可能会发生变化或被攻击,因此系统架构师思维以一种偏执的视角看待所有集成。

SA 思维的主要问题是:“哪些协议的依赖项是外部的并且可能失败?”这个问题并不难回答——它只需要识别所有协议集成,并假设它们可能在某一天失败。一个更具挑战性的问题是:“每个集成可能以什么方式失败?”这时,黑客思维就非常有用,促使你考虑这些集成可能出现的错误行为或故障。

例子:想象一下你的协议依赖于一个预言机服务,由于Gas费用过高,它没有在几个小时内更新某个资产的价格。大多数预言机服务提供数据,显示价格最后更新的时间,因此在合约内检查并不困难。然而,决定如何处理这种情况则更复杂。例如,如果你运行一个借贷协议,并需要这个价格来判断是否清算一个用户,根据资产的不同参数可能会适用不同的解决方案(每种方案都有其优缺点)。尽管在这里我们不会深入探讨这些选项,但它们常常需要详细考虑。

除了识别可能失败的集成之外,值得注意的是,以上哪些集成涉及可升级的智能合约。这个考量更多是关于协议架构,而不是具体的测试。这里的一般规则是,如果与可升级合约集成,则使合约可升级。

对有集成的协议的最佳实践是为所有与合约交互的离线服务创建模拟。这可以测试协议对离线服务故障的反应。例如,如果你有一个跨链桥的模拟并将其设置为阻止消息传递,你可以模拟一个跨链桥协议未能传递消息的场景,并观察你的协议如何反应。

结论

测试覆盖对于开发安全的 web3 协议至关重要,但高覆盖率本身并不保证安全。本文讨论的框架鼓励编写深思熟虑、针对性的测试,从多个角度解决潜在威胁。通过采用黑客、不变性和系统架构师思维,开发者可以设计测试用例,确认功能并发现漏洞。

对于黑客思维,你应使用以下问题:

  • 该协议存在哪些不应被普通用户访问的用例?
  • 用户的函数调用序列和频率应有哪些限制?
  • 用户输入参数应有什么约束?
  • 在调用协议时,哪些参数不应由用户控制?

对于不变性思维,只有一个问题,但它很难:“代码中存在哪些不变条件?”而对于系统架构师思维,唯一的问题是:“哪些协议的依赖项是外部的并且可能失败?”这个问题最困难的部分在于,它还需要确定外部集成的可能失败方式,以便在你的协议中正确处理他们。

记住:编写测试——保持安全!

  • MixBytes 是谁?

MixBytes 是一个专家区块链审计和安全研究团队,专门为 EVM 兼容和 Substrate 基础项目提供全面的智能合约审计和技术顾问服务。请加入我们,关注 X ,获取最新行业动态和见解。

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

0 条评论

请先 登录 后评论
mixbytes
mixbytes
Empowering Web3 businesses to build hack-resistant projects.