本文介绍了模糊测试(Fuzz Testing)在智能合约中的应用,特别是在Solidity中。模糊测试通过生成随机输入来测试代码中可能被忽略的边界情况,结合不变性测试验证系统在各种情况下的预期行为。文章还探讨了有状态和无状态模糊测试、有界模型检查(BMC)以及端到端测试与模糊测试的结合,旨在提高智能合约的安全性。
2025年6月10日
让我们从理解黑客攻击是如何发生的开始。在大多数情况下,软件中的安全漏洞是由于开发者在程序的单元测试期间没有预料到的极端情况造成的,因此没有为此编写测试。但是,如果有一种方法可以用一种可以对几乎所有可能性进行压力测试的方法来解决这些不可预测的情况呢?
我们将从理解模糊测试本身开始,并在本文结束时,将这些点连接到Solidity智能合约。现在,如果你是为了模糊测试的正式教科书定义而来,我会为你省去麻烦,因为那不是你想要的,对吧?🙂
让我为你简化一下。在编写测试时,目标通常是实现 100% 的代码覆盖率。然而,即使具有完美的覆盖率,你也无法保证代码中没有潜伏的错误。这就是模糊器发挥作用的地方。模糊器生成一系列输入,以测试你在单元测试中可能遗漏的边界。
“模糊测试(或 Fuzzing)涉及将随机数据馈送到系统模拟中,以试图破坏它。”
然而,模糊测试的有效性取决于你编写的模糊测试的质量。模糊器作为软件工具,本质上是不智能的。它们在计算边界内运行,并且缺乏上下文或决策能力。
例如,在具有多种可能操作的系统中,模糊器可能会随机选择不适当的操作。以 onlyOwner 函数为例——如果模糊器使用无效地址,它自然会触发回滚。这些情况是可预测且微不足道的,通常被归类为容易获得的成果。由于你期望这些失败,因此它们应该已经在单元测试中涵盖。
为了避免在这些无关紧要的情况下浪费宝贵的模糊测试资源,绕过明显的失败并专注于重要的极端情况至关重要。因此,编写有效的模糊测试在于从过程中榨取最大的价值。
现在让我们谈谈也称为属性的不变量。事情在这里变得稍微复杂一些,但并不像看起来那么复杂。简而言之,不变量是你断言必须始终保持为真的属性或规则。
与你提供单个输入并验证预期结果的单元测试不同,不变量测试验证特定属性在许多随机输入中是否为真。模糊器使用各种各样的值重复测试这些属性,确保系统在所有情况下都按预期运行。
让我进一步简化。在 DeFi 的世界中,不变量是协议赖以维持稳定性和公平性的基本规则。这些是永远不能被打破的“法律”,无论执行什么操作。
在像 Compound 或 Aave 这样的借贷协议中,一个关键规则是用户的借贷价值永远不能超过他们的抵押品。为了进一步解释这一点,当你借入资产时,你必须存入价值高于借入价值的抵押品。这类似于抵押贷款,你不能借入超过房屋价值的金额。该协议会阻止任何将帐户置于这种不安全状态或恶化已经存在的风险情况的行为。
像 Uniswap 或 SushiSwap 这样的自动化做市商依赖于一个数学不变量来维持流动性池的平衡。此规则表示为 x * y = k,其中 x 和 y 代表代币数量,k 是一个常数。如果有人购买更多的一种代币,则价格会按比例上涨以保持不变量。
在 Staking 协议中,有一个简单但至关重要的规则:用户只能提取他们最初存入的相同数量的代币。例如,如果你 Staking 10 个代币,你可以提取 10 个代币。虽然 Staking 可以获得奖励,但本金保持不变。
<br>pragma solidity ^0.8.0;<br>contract UniswapInvariantCheck {<br> uint256 public reserveX; // X 代币储备金<br> uint256 public reserveY; // Y 代币储备金<br> constructor(uint256 _initialX, uint256 _initialY) {<br> reserveX = _initialX; // 初始化储备金<br> reserveY = _initialY;<br> }<br> // 模拟代币交换的函数<br> function swap(uint256 inputX, uint256 inputY) public {<br> require(inputX == 0 || inputY == 0, "Only one token can be swapped"); // 确保一次只交换一种代币<br> if (inputX > 0) {<br> // X 代币被交换到池中<br> uint256 newReserveX = reserveX + inputX; // 更新 X 的储备金<br> uint256 newReserveY = (reserveX * reserveY) / newReserveX; // 使用不变量计算 Y 的新储备金<br> reserveX = newReserveX; // 更新状态<br> reserveY = newReserveY;<br> } else if (inputY > 0) {<br> // Y 代币被交换到池中<br> uint256 newReserveY = reserveY + inputY; // 更新 Y 的储备金<br> uint256 newReserveX = (reserveX * reserveY) / newReserveY; // 使用不变量计算 X 的新储备金<br> reserveX = newReserveX; // 更新状态<br> reserveY = newReserveY;<br> }<br> }<br> // 检查不变量是否成立的函数<br> function invariantHolds() public view returns (bool) {<br> uint256 k = reserveX * reserveY; // 计算常数 k<br> return k == reserveX * reserveY; // 验证 x * y = k 是否成立<br> }<br>}<br> |
newReserveY = (reserveX * reserveY) / newReserveX
newReserveX = (reserveX * reserveY) / newReserveY
invariantHolds 函数检查 reserveX 和 reserveY 的乘积是否保持一致。如果任何操作破坏了这种平衡,不变量将不成立,表明实现或逻辑存在问题。
要理解模糊测试技术,让我们使用一个易碎的玻璃杯的比喻。
无状态模糊测试 独立地测试每个场景。想象一下通过以下方式测试玻璃杯:
在每种情况下,你都使用一个新的玻璃杯。过去的动作不会影响下一次测试。虽然这种方法速度很快,但它忽略了早期动作如何影响结果。
另一方面,有状态模糊测试 在所有测试中使用同一个玻璃杯。如果你在第一步敲击玻璃杯,在第二步将鹅卵石掉入其中,并在第三步将其扔掉,你会观察到累积效应。这种方法反映了现实世界的系统,在这些系统中,先前的行为会影响未来的行为,从而发现更深层次的错误。
有界模型检查(BMC)通过限制识别错误所采取的步骤来改进模糊测试。BMC 不是无休止地探索输入,而是设置逻辑“边界”。
例如,将零代币存入 AMM 可能会触发 MIN_INITIAL_SHARES 错误。由于这是一个可预测的故障,你可以引导模糊器避免此类输入,而是专注于有意义的极端情况。
将 BMC 想象为导航迷宫。你决定只检查最多需要 10 步的路径。如果错误存在于这些边界内,你会找到它。如果不是,则系统在该范围内保持稳定。
端到端(E2E)测试模拟真实世界的用户操作,确保系统按预期运行。例如,注册表单的 E2E 测试将验证各种输入:空白字段、无效电子邮件和有效凭据。
当与模糊测试结合使用时,E2E 测试会变得更加强大。虽然 E2E 测试检查正常的工作流程,但模糊测试引入了不可预测性,以测试系统在压力下的响应方式。它们共同提供了一个强大的框架,用于验证预期行为和意外行为。
对于发现传统测试可能遗漏的隐藏漏洞而言,模糊测试是一个颠覆性技术。通过将模糊测试与不变量测试相结合,我们超越了仅仅测试单个功能的范围,而是专注于系统的整体行为和稳定性。有状态模糊测试、有界模型检查和端到端测试等方法协同工作,以发现极端情况并使智能合约更安全。
- 原文链接: blog.immunebytes.com/202...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!