模糊测试入门与assert()函数
我们继续深入模糊测试,本篇文章介绍不变测试,这是一种将代码进行多个步骤的随机测试。
无状态模糊测试(Stateless Fuzz Testing)和不变式模糊测试(Stateful Fuzz Testing)是软件测试中的两种高级技术,尤其在智能合约和系统安全性测试中非常重要。以下是这两种测试方法的背景知识:
背景:
目的:
方法:
应用场景:
背景:
目的:
方法:
应用场景:
在实践中,无状态和不变式模糊测试往往结合使用,以覆盖不同的测试场景和确保合约的全面测试。无状态测试提供了快速、广泛的覆盖,而不变式测试则深入挖掘状态相关的复杂问题。
通过这两种测试方法,开发者可以提高智能合约的质量和可靠性,减少因逻辑错误或状态管理不当导致的安全风险。
代码如下(示例):
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
这段代码定义了一个名为 StatefulFuzzCatches
的智能合约,它包含两个公共状态变量、一个公共函数和一个修改状态的函数。以下是对合约的详细分析:
状态变量:
myValue
: 一个 uint256
类型的公共变量,初始值设为 1。storedValue
: 另一个 uint256
类型的公共变量,初始值设为 100。不变式(Invariant):
doMoreMathAgain
函数永远不应该返回 0。这意味着无论输入什么值,该函数的返回值都应该是有效的,并且不应该触发任何导致返回 0 的逻辑。doMoreMathAgain
函数:
uint128
类型的参数 myNumber
。myNumber
转换为 uint256
类型,除以 1(这是一个无操作,因为任何数除以 1 都等于其本身),然后与 myValue
相加,得到 response
。response
赋值给 storedValue
,并将 response
返回给调用者。changeValue
函数:
myValue
的值。潜在问题:
doMoreMathAgain
函数不应该返回 0。然而,如果 changeValue
函数被用来将 myValue
设置为一个可能导致 response
为 0 的值,那么不变式可能会被违反。例如,如果 myValue
被设置为0,而 myNumber
被设置为 0,那么 response
变成 0。还有一个更特殊的例子在于solidity的版本是否会溢出。但我们发现合约十分的聪明,myNumber 是一个 uint128 类型的参数。由于 uint128 可以安全地转换为 uint256 而不会发生溢出,这里使用了 uint256(myNumber) 进行转换。// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
// INVARIANT: doMoreMathAgain should never return 0
contract StatefulFuzzCatches {
uint256 public myValue = 1;
uint256 public storedValue = 100;
/*
* @dev Should never return 0
*/
function doMoreMathAgain(uint128 myNumber) public returns (uint256) {
uint256 response = (uint256(myNumber) / 1) + myValue;
storedValue = response;
return response;
}
function changeValue(uint256 newValue) public {
myValue = newValue;
}
}
在智能合约测试中,无状态模糊测试(Stateless Fuzz Testing)和不变式模糊测试(Stateful Fuzz Testing)是两种不同的测试方法,它们各自有不同的目标和实现方式:
定义:
无状态模糊测试是一种测试方法,它不考虑合约的当前状态,只关注函数的输入和输出。测试用随机或半随机生成的输入数据调用合约函数,以发现潜在的错误或异常行为。
特点:
实现方式:
示例:
function testFuzzPassesEasyInvariant(uint128 randomNumber) public {
sfc.doMoreMathAgain(randomNumber);
assert(sfc.storedValue() != 0);
}
这个测试函数接受一个随机数,调用doMoreMathAgain
函数,并断言返回值不为0。
输出
forge test --mt testFuzzPassesEasyInvariant
[⠒] Compiling...
No files changed, compilation skipped
Ran 1 test for test/invariant-break/StatefulFuzzCatchesTest.t.sol:StatefulFuzzCatchesTest
[PASS] testFuzzPassesEasyInvariant(uint128) (runs: 1000, μ: 13802, ~: 13805)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 45.85ms (42.85ms CPU time)
Ran 1 test suite in 47.24ms (45.85ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
编译通过并没有找到错误
定义:
不变式模糊测试关注合约的状态变化。它模拟合约在一系列交易后的状态,并验证合约的某些关键属性是否始终保持不变。
特点:
实现方式:
targetContract(address(sfc)); 这行代码其目的是将测试合约的地址指向被测试的合约实例,通过一些列的随机执行来测试合约的结果
示例:
function invariant_testMathDoesntReturnZero() public view {
assert(sfc.storedValue() != 0);
}
输出
[FAIL. Reason: invariant_testMathDoesntReturnZero persisted failure revert]
[Sequence]
sender=0x8Ab55842614e60Cf71E9F480BA37245e8e1277Bd addr=[src/invariant-break/StatefulFuzzCatches.sol:StatefulFuzzCatches]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=changeValue(uint256) args=[115792089237316195423570985008687907853269984665640564039457584007913129639934 [1.157e77]]
sender=0x000000000000000000000000000000000000002A addr=[src/invariant-break/StatefulFuzzCatches.sol:StatefulFuzzCatches]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=doMoreMathAgain(uint128) args=[257949695690360443 [2.579e17]]
invariant_testMathDoesntReturnZero() (runs: 1, calls: 1, reverts: 1)
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 1.23ms (485.23µs CPU time)
Ran 1 test suite in 7.00ms (1.23ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/invariant-break/StatefulFuzzCatchesTest.t.sol:StatefulFuzzCatchesTest
[FAIL. Reason: invariant_testMathDoesntReturnZero persisted failure revert]
[Sequence]
sender=0x8Ab55842614e60Cf71E9F480BA37245e8e1277Bd addr=[src/invariant-break/StatefulFuzzCatches.sol:StatefulFuzzCatches]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=changeValue(uint256) args=[115792089237316195423570985008687907853269984665640564039457584007913129639934 [1.157e77]]
sender=0x000000000000000000000000000000000000002A addr=[src/invariant-break/StatefulFuzzCatches.sol:StatefulFuzzCatches]0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f calldata=doMoreMathAgain(uint128) args=[257949695690360443 [2.579e17]]
invariant_testMathDoesntReturnZero() (runs: 1, calls: 1, reverts: 1)
可想而知这种测试可以更全面的执行到问题所在,顺便还发现了合约的溢出问题,编写
function testFuzzPassesEasyInvariant(uint128 randomNumber) public {
sfc.changeValue(type(uint256).max);
sfc.doMoreMathAgain(randomNumber);
可以得到,
Ran 1 test for test/invariant-break/StatefulFuzzCatchesTest.t.sol:StatefulFuzzCatchesTest
[FAIL. Reason: panic: arithmetic underflow or overflow (0x11); counterexample: calldata=0xfe0f460c00000000000000000000000000000000000000000000000000000000000000e5 args=[229]] testFuzzPassesEasyInvariant(uint128) (runs: 0, μ: 0, ~: 0)
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 23.99ms (22.68ms CPU time)
Ran 1 test suite in 24.99ms (23.99ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/invariant-break/StatefulFuzzCatchesTest.t.sol:StatefulFuzzCatchesTest
[FAIL. Reason: panic: arithmetic underflow or overflow (0x11); counterexample: calldata=0xfe0f460c00000000000000000000000000000000000000000000000000000000000000e5 args=[229]] testFuzzPassesEasyInvariant(uint128) (runs: 0, μ: 0, ~: 0)
Encountered a total of 1 failing tests, 0 tests succeeded`