2026/6/22 22:16:20

智能合约库合约自动化验证:基于属性测试与模糊测试的工程实践

智能合约库合约自动化验证:基于属性测试与模糊测试的工程实践 1. 项目概述当库合约验证遇上自动化测试思维在智能合约开发领域尤其是涉及DeFi、NFT等复杂金融逻辑的场景安全是悬在每一位开发者头顶的达摩克利斯之剑。我们常常听到“合约已通过审计”这样的保证但审计本身是一个高强度、高成本且可能遗漏边缘案例的手动过程。更棘手的是那些作为基础设施的“库合约”——它们不直接持有资产却为无数业务合约提供关键的数学运算、安全检查和数据结构。一个微小的漏洞比如经典的整数溢出就可能通过库合约这个“心脏”传导至所有依赖它的“器官”造成系统性风险。传统的验证方法如形式化验证虽然严谨但门槛极高、耗时漫长难以融入快速迭代的敏捷开发流程。“基于测试的库合约验证”这个标题恰恰指向了上述痛点的交汇处。它不是在谈论又一个测试框架而是提出了一种方法论上的融合将客户端程序验证的自动化能力与针对库合约的特性相结合形成一套新的质量保障范式。简单来说就是用自动化生成和执行“验证用例”的方式来系统性地证明一个库合约在各种极端和合法输入下的行为是否符合预期。这听起来像是“单元测试的超级加强版”但其内核更接近“属性测试”与“符号执行”的混合体目标是实现更高程度的自动化验证覆盖。这种方法适合谁首先是智能合约的核心开发者和安全工程师他们需要为团队的关键组件建立坚不可摧的信任基石。其次是那些构建开发者工具平台或中间件的团队他们提供的库合约会被成千上万的开发者使用其安全性责任重大。最后对于任何希望将“安全左移”、在开发早期就引入强力自动化检查的区块链项目这套思路都具有极高的参考价值。它不追求替代形式化验证而是旨在填补从手工测试到完全形式化验证之间的巨大空白提供一个性价比更高、更易实施的自动化验证方案。2. 核心思路拆解为什么是“测试”与“验证”的结合要理解这个新方法我们得先拆解几个关键概念并看看传统做法遇到了哪些瓶颈。2.1 库合约的独特挑战与验证困境库合约Library Contract在Solidity中是一种特殊的合约它部署后其代码逻辑可以被其他合约“委托调用”delegatecall复用但本身没有独立的存储状态。常见的例子包括安全数学运算库如OpenZeppelin的SafeMath尽管Solidity 0.8已内置、数据结构库如EnumerableSet、Token标准工具库等。它的验证难点在于状态隔离性库合约本身无状态其行为完全依赖于调用者的存储上下文。验证时必须考虑其在与不同状态的主合约交互时的表现。输入空间的复杂性库函数通常操作基础数据类型如uint256, address。一个简单的加法函数其输入空间是所有可能的uint256对2^256 * 2^256穷举测试不可能。组合爆炸库函数很少被单独使用往往是多个函数组合调用。验证单个函数正确不难难的是验证函数序列在各种状态路径下的正确性。泛型与抽象性库合约设计追求通用性这导致其测试用例设计比具体业务合约更抽象更需要从“属性”而非“具体值”的角度去思考。传统的单元测试如使用Waffle、Hardhat针对具体输入输出覆盖有限。形式化验证如使用Certora、SMTChecker能证明所有可能输入但需要编写复杂的规范Specification且对开发者数学和逻辑功底要求极高。2.2 自动化客户端程序验证的启示“自动化客户端程序验证”这个概念源于传统软件工程特别是在编译器、数据库等系统软件测试中。其核心思想是不预设具体的输入输出对而是定义程序必须遵守的“属性”或“不变式”然后由工具自动生成海量测试输入运行程序并检查这些属性是否始终被满足。一个经典工具是“QuickCheck”及其衍生品它通过随机生成输入并验证属性来发现反例。在智能合约领域类似思想体现在像Echidna或Manticore这样的模糊测试/符号执行工具上。它们能自动探索合约的执行路径并检查用户定义的“不变式”是否被违反。这个方法的优势是自动化程度高能发现开发者意想不到的边缘案例。但它通常针对完整的业务合约其生成的测试用例可能对库合约这种“无状态”或“弱状态”的组件不够精准浪费大量资源在无关的路径探索上。2.3 新方法的融合与创新点“基于测试的库合约验证”正是将上述两者结合并针对库合约特点进行优化以“属性”为测试核心不再编写assert(add(1,2) 3)这样的具体用例而是编写assert(add(x, y) x add(x, y) y)对于非负整数加法这样的属性。验证工具的目标是尝试找到一个反例来推翻这个属性。深度结合库合约上下文验证工具会被“告知”正在测试的是一个库合约。因此它在生成测试时会智能地模拟一个调用者合约的存储状态并生成对库函数有意义的调用序列而不是随机调用任何函数。分层验证策略基础属性验证针对单个函数验证其数学属性如交换律、结合律、安全属性如无溢出。状态不变式验证针对涉及状态操作的库函数如向一个集合添加元素验证操作前后必须保持的不变式如集合元素唯一性。组合序列验证自动生成函数调用序列如先push再pop验证序列执行后的状态是否符合预期。反馈驱动的用例生成当工具发现一个属性被违反时它不仅报告反例还会分析导致反例的输入和路径并以此为导向生成更多类似的“危险”测试用例实现定向深化测试。这种方法本质上构建了一个针对库合约的、自动化的、基于属性的测试验证系统。它比单元测试更强大比完全的形式化验证更轻量、更易上手。3. 实战构建打造你的库合约验证流水线理论说得再多不如动手搭建一套。下面我将以一个经典的、简化的“安全整数运算库”为例演示如何从零开始实施这套方法。我们将使用Foundry框架因为它内置了强大的模糊测试功能forge fuzz非常适合实现基于属性的测试。3.1 环境与工具选型为什么选择Foundry原生支持模糊测试forge test命令直接集成模糊测试无需复杂配置可以随机生成输入运行测试成千上万次。性能极佳用Rust编写测试执行速度远超其他基于JavaScript的框架。便捷的作弊码提供了vm.assume等作弊码可以方便地对模糊测试的输入空间进行约束使其更符合库合约的验证场景。与Solidity深度集成测试也用Solidity写对合约开发者更友好。项目初始化# 安装Foundry curl -L https://foundry.paradigm.xyz | bash foundryup # 创建一个新项目 forge init lib-verification-demo cd lib-verification-demo3.2 定义待验证的库合约我们先创建一个有潜在风险的库合约而不是一个完美的SafeMath。这样更有验证价值。创建src/LibMath.sol// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; /** * 一个“有待验证”的数学库故意留有一些不明显的边界情况。 */ library LibMath { /** * 返回两个数字中的最大值。 * dev 潜在问题如果a和b相等逻辑没问题但实现是否最优 */ function max(uint256 a, uint256 b) internal pure returns (uint256) { return a b ? a : b; } /** * 返回 a 的 b 次方。 * dev 潜在风险指数过大时的溢出问题0的0次方定义问题。 */ function pow(uint256 a, uint256 b) internal pure returns (uint256) { if (b 0) { // 按照数学惯例任何数的0次方为1包括0^0这里我们定义为1但存在争议 return 1; } uint256 result a; for (uint256 i 1; i b; i) { result * a; // 注意这里没有检查乘法溢出 } return result; } /** * 计算平均值 (a b) / 2防止中间加法溢出。 * dev 常见的安全写法。 */ function average(uint256 a, uint256 b) internal pure returns (uint256) { // (a b) (a ^ b) / 2 是另一种不溢出的写法这里我们用标准安全写法 return (a 1) (b 1) ((a 1) (b 1)); } }这个库看起来简单但pow函数存在明显的溢出漏洞且average函数的实现虽然防溢出但逻辑是否正确需要验证。3.3 编写基于属性的验证测试传统的单元测试我们会这样写test/MathUnit.t.solfunction testMax() public { assertEq(LibMath.max(5, 10), 10); assertEq(LibMath.max(10, 5), 10); assertEq(LibMath.max(5, 5), 5); }这只能验证几个固定点。现在我们编写基于属性的验证测试test/MathProperty.t.sol// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import {Test} from forge-std/Test.sol; import {LibMath} from ../src/LibMath.sol; contract MathPropertyTest is Test { // 测试1max函数的属性验证 function testFuzz_MaxProperties(uint256 a, uint256 b) public pure { // 属性1结果必须大于等于a和b uint256 result LibMath.max(a, b); assert(result a); assert(result b); // 属性2结果必须等于a或等于b assert(result a || result b); // 属性3交换律 - max(a,b) max(b,a) assert(LibMath.max(a, b) LibMath.max(b, a)); } // 测试2pow函数的属性验证 (很快会发现问题) function testFuzz_PowProperties(uint256 a, uint256 b) public { // 使用vm.assume约束输入使测试更高效、更有意义。 // 避免指数b过大导致循环次数过多耗尽Gas。 vm.assume(b 10); // 限制指数大小便于演示。实际中可能需要更复杂的约束。 uint256 result LibMath.pow(a, b); // 属性1当指数为0时结果必须为1 if (b 0) { assert(result 1); } else { // 属性2当指数为1时结果必须等于底数a if (b 1) { assert(result a); } // 属性3对于 a 1, b 1结果应该 a 可能溢出导致断言失败 // 这是一个“可能被推翻”的属性正是我们想发现的边界情况。 if (a 1 b 1) { assert(result a); } } // 注意我们故意没有验证乘法溢出因为这就是我们要找的bug。 } // 测试3average函数的属性验证 function testFuzz_AverageProperties(uint256 a, uint256 b) public pure { uint256 result LibMath.average(a, b); // 属性1平均值应在a和b之间含等于 uint256 minVal a b ? a : b; uint256 maxVal a b ? b : a; assert(result minVal); assert(result maxVal); // 属性2平均值的两倍应约等于 ab (考虑整数除法舍入) // 由于是向下取整所以 2*result ab assert(2 * result a b); // 并且 ab 2*(result 1) 因为result是floor值 assert(a b 2 * (result 1)); } }关键解析testFuzz_前缀告诉Foundry这是一个模糊测试函数。uint256 a, uint256 b参数Foundry会自动为这些参数生成随机值默认256次运行。vm.assume(b 10)这是一个“作弊码”它过滤随机输入只允许b 10的输入进入测试逻辑。这能避免无意义的超大指数导致测试极慢将计算资源集中在更有价值的输入空间。这是库合约验证中的关键技巧合理约束输入域。断言assert这里断言的是“属性”而非具体值。例如assert(result a)表达的是“最大值结果一定不小于a”这个普遍属性。3.4 运行验证与发现问题运行模糊测试forge test --match-test testFuzz_PowProperties -vvv很快测试会失败。Foundry会输出类似以下信息[FAIL. Reason: assertion failed] testFuzz_PowProperties(uint256,uint256) (runs: 42, μ: 46025, ~: 46025) Counterexample: calldata0x... a2 b5 Traces: ... Error: Assertion Failed它告诉我们当a2,b5时属性result a失败了。我们计算一下2^5 3232 2 成立啊为什么失败仔细看我们的pow函数实现当b5时循环执行4次乘法result从2开始依次变为4, 8, 16, 32。看起来没问题。但这里Foundry揭示了一个更深层的问题模糊测试器在大量随机运行中可能发现了溢出情况。让我们修改测试加入溢出检查并放宽vm.assume的限制来寻找真正的溢出点function testFuzz_PowProperties_Overflow(uint256 a, uint256 b) public { // 先尝试计算如果溢出则整个交易会回滚测试失败。 // 我们需要捕获这个回滚并将其视为属性违反。 try LibMath.pow(a, b) returns (uint256 result) { // 如果没溢出执行和之前类似的属性检查 if (b 0) { assert(result 1); } // 可以添加更多属性... } catch (bytes memory /*lowLevelData*/) { // 如果捕获到错误很可能是溢出回滚则测试“通过”因为我们发现了问题。 // 在模糊测试中我们通常希望assert不被触发。这里我们可以直接返回。 return; } }运行这个测试并增加模糊测试的运行次数forge test --match-test testFuzz_PowProperties_Overflow --fuzz-runs 10000经过更多次运行你可能会发现当a很大时即使b2也会导致溢出。例如a2^128那么a*a就会超过uint256的范围。这就是我们库合约的致命漏洞它完全没有检查乘法溢出。3.5 修复漏洞并增强验证修复LibMath.pow函数function pow(uint256 a, uint256 b) internal pure returns (uint256) { if (b 0) { return 1; } uint256 result a; for (uint256 i 1; i b; i) { // 添加溢出检查 unchecked { // 在unchecked块内执行乘法但之前需要检查 if (result type(uint256).max / a) { revert(Multiplication overflow); } result * a; } } return result; }或者更优地使用Solidity 0.8的内置溢出检查或者直接使用OpenZeppelin的SafeMath库。修复后我们还需要更新验证属性。一个好的实践是专门为安全属性编写测试function testFuzz_PowNoOverflow(uint256 a, uint256 b) public { // 约束b不要太大避免循环过多导致测试Gas耗尽 vm.assume(b 10); // 这个测试的核心属性是函数执行不应回滚除非是预期的错误如输入不合法。 // 在Foundry中如果函数revert测试就会失败。所以这个测试本身就是在验证“无异常”属性。 // 我们可以通过try-catch来使其更明确 try LibMath.pow(a, b) returns (uint256 result) { // 正常执行测试通过 } catch (bytes memory lowLevelData) { // 如果revert了我们判断是否是预期的溢出错误 bytes4 expectedSelector bytes4(keccak256(Error(string))); bytes4 receivedSelector bytes4(lowLevelData); // 如果不是我们定义的溢出错误则测试失败 if (receivedSelector ! expectedSelector || keccak256(lowLevelData) ! keccak256(abi.encodeWithSignature(Error(string), Multiplication overflow))) { fail(Unexpected revert); } // 如果是预期的溢出错误可以认为测试通过或者根据业务逻辑处理 } }4. 进阶验证策略模拟调用者与状态不变式上面的例子验证了纯计算函数。对于管理状态的库合约如一个AddressSet库验证需要模拟调用者合约的状态。4.1 创建带状态的库合约与测试夹具创建src/AddressSetLib.sollibrary AddressSetLib { struct Set { address[] _values; mapping(address uint256) _indexes; // value - index1 } function add(Set storage set, address value) internal { if (!contains(set, value)) { set._values.push(value); set._indexes[value] set._values.length; // 存储 index1 } } function remove(Set storage set, address value) internal { uint256 valueIndex set._indexes[value]; require(valueIndex ! 0, AddressSet: value not in set); uint256 toDeleteIndex valueIndex - 1; uint256 lastIndex set._values.length - 1; if (lastIndex ! toDeleteIndex) { address lastValue set._values[lastIndex]; set._values[toDeleteIndex] lastValue; set._indexes[lastValue] valueIndex; // 更新被移动元素的索引 } set._values.pop(); delete set._indexes[value]; } function contains(Set storage set, address value) internal view returns (bool) { return set._indexes[value] ! 0; } function length(Set storage set) internal view returns (uint256) { return set._values.length; } }编写属性验证测试test/AddressSetProperty.t.sol。这里的关键是我们需要一个测试夹具Test Harness即一个模拟的调用者合约contract AddressSetHarness { using AddressSetLib for AddressSetLib.Set; AddressSetLib.Set internal set; // 对外暴露库函数供测试合约调用 function add(address value) public { set.add(value); } function remove(address value) public { set.remove(value); } function contains(address value) public view returns (bool) { return set.contains(value); } function length() public view returns (uint256) { return set.length(); } // 一个辅助函数用于获取内部数组状态仅用于测试验证 function getValues() public view returns (address[] memory) { // 注意实际库可能不暴露此方法这里仅为验证需要 // 更佳实践是通过公开的length()和contains()来推断属性 // 此处简化演示 return set._values; } } contract AddressSetPropertyTest is Test { AddressSetHarness harness; function setUp() public { harness new AddressSetHarness(); } // 属性1添加元素后集合必须包含该元素 function testFuzz_AddContains(address addr) public { vm.assume(addr ! address(0)); // 假设不允许零地址根据库的实际情况调整 uint256 oldLength harness.length(); harness.add(addr); assertTrue(harness.contains(addr)); assertEq(harness.length(), oldLength 1); } // 属性2移除一个存在的元素后集合不再包含该元素 function testFuzz_RemoveNotContains(address addr1, address addr2) public { vm.assume(addr1 ! address(0) addr2 ! address(0) addr1 ! addr2); harness.add(addr1); harness.add(addr2); uint256 oldLength harness.length(); harness.remove(addr1); assertFalse(harness.contains(addr1)); assertTrue(harness.contains(addr2)); // 确保其他元素不受影响 assertEq(harness.length(), oldLength - 1); } // 属性3不变式 - 集合中元素唯一 // 这个属性很难通过直接调用add来验证因为add内部有去重逻辑。 // 我们可以通过“暴力”方式验证随机调用一系列操作后检查内部数组是否唯一。 function testFuzz_Invariant_Uniqueness(uint8[10] memory ops, address[10] memory addrs) public { // ops: 0add, 1remove (如果存在) // 这是一个简单的序列模糊测试 for (uint i 0; i 10; i) { if (ops[i] % 2 0) { harness.add(addrs[i]); } else { try harness.remove(addrs[i]) {} catch {} } } // 验证唯一性通过公开的getValues检查仅测试用 address[] memory values harness.getValues(); for (uint i 0; i values.length; i) { for (uint j i 1; j values.length; j) { assertTrue(values[i] ! values[j], Duplicate element found); } } } // 属性4不变式 - 索引映射的一致性 // 验证 _indexes 映射与 _values 数组始终保持一致。 function testFuzz_Invariant_IndexConsistency(address addr) public { // 这个测试需要能访问内部映射在真实场景下可能做不到。 // 一种方法是在测试夹具中暴露一个“验证一致性”的函数该函数遍历_values并检查_indexes。 // 这体现了“为验证而设计”的思想在库合约或测试夹具中预留验证钩子。 // 此处略过具体实现。 } }4.2 使用Foundry的Invariant Testing进行状态机测试对于这类状态库Foundry的不变式测试Invariant Testing是更强大的武器。它允许你定义一些关于合约状态必须始终为真的“不变式”然后随机调用一系列函数包括有状态变化的函数试图打破这些不变式。创建test/AddressSetInvariant.t.solcontract AddressSetInvariantTest is Test { AddressSetHarness harness; // 我们用一个内部集合来追踪我们“认为”的状态与合约实际状态做对比 address[] internal expectedValues; function setUp() public { harness new AddressSetHarness(); // 绑定目标合约并指定哪些函数可以被随机调用 targetContract(address(harness)); } // 这是一个“不变式”函数。Foundry会随机调用harness的add和remove并在每次调用后检查此函数。 function invariant_LengthMatchesActual() public view { // 不变式1我们追踪的expectedValues长度应与合约length()一致 // 注意我们需要在add/remove操作时同步更新expectedValues这需要更复杂的设置。 // 简化版我们只验证一个更基本的不变式。 } // 更实用的方法使用“幽灵变量”Ghost Variables和“钩子”Hooks // Foundry允许你定义targetSelector和targetSender等来引导模糊器。 }实操心得对于复杂的库合约完全自动化的不变式测试设置可能很复杂。一个更务实的策略是优先验证核心安全属性如无溢出、无重入、权限检查。编写基于属性的模糊测试针对关键函数组合如add后remove。使用“差分测试”用一份简单的、可证明正确的参考实现如一个Python脚本与你的库合约在相同的随机输入下运行对比结果。Foundry的ffi功能可以调用外部脚本实现这一点。5. 常见问题、排查技巧与优化策略在实际将这套方法融入开发流程时你会遇到各种挑战。以下是我从实践中总结的一些常见问题和应对技巧。5.1 模糊测试效率低下找不到深层Bug问题表现运行了上万次测试但只发现一些浅显的错误或者运行速度很慢。排查与优化合理使用vm.assume约束输入无约束的随机输入大部分是无效的。例如测试一个排序函数可以假设输入数组长度小于某个值避免Gas耗尽。但约束不能过强否则会错过边界情况。技巧是先宽后紧。先不加约束运行观察哪些输入经常导致回滚或消耗大量Gas然后针对性地添加约束。引导模糊器Foundry允许你设置targetSelector让模糊器更倾向于调用某些函数。对于状态库可以设置add函数的权重高于remove因为先有添加才有移除。function setUp() public { targetContract(address(harness)); // 设置函数调用权重非直接API需通过选择器频率实现 // 一种方法是在测试中随机数决定调用哪个函数并赋予不同概率。 }使用“种子”和“语料库”当模糊测试发现一个有趣的失败案例时Foundry会保存这个输入作为“语料”。下次运行时会优先使用这些“种子”输入进行变异从而更深入地探索相关路径。确保你的.ffi缓存目录被版本控制系统忽略但本地保存。属性设计要精准模糊测试的效力高度依赖于属性定义的质量。模糊的属性如“函数不应回滚”有用但精密的属性如“映射_indexes中每个键的值减1必须是一个有效的_values数组索引”能发现更微妙的逻辑错误。多花时间思考“什么必须永远为真”。5.2 测试结果非确定性或难以复现问题表现模糊测试有时失败有时成功无法稳定复现某个Bug。排查与解决固定随机种子使用--fuzz-seed参数可以复现某次运行。forge test --match-test testFuzz_PowProperties --fuzz-seed 12345检查测试的纯净性确保每个测试函数都是独立的不会受到setUp或其他测试函数留下的状态影响。在Foundry中默认每个test函数都会重新部署合约但setUp会在每个test前运行。对于模糊测试每次模糊运行是在同一个合约实例上进行的吗这取决于测试设计。对于需要完全隔离的测试可以在模糊测试函数内部部署新实例。审查vm.assume条件可能某个条件过于严格导致只有在极罕见的随机数组合下才能进入核心测试逻辑。尝试放宽假设或者使用vm.assume的否定形式来探索被排除的区域。5.3 如何处理外部依赖或复杂环境问题表现库合约可能依赖于特定的区块链状态如block.timestamp或调用其他合约。策略使用作弊码模拟环境Foundry的vm提供了大量作弊码。vm.warp(uint256)模拟时间戳。vm.roll(uint256)模拟区块号。vm.mockCall(address, bytes memory, bytes memory)模拟对其他合约的调用返回指定数据。vm.deal(address, uint256)给地址提供ETH。 在你的属性测试中可以将这些环境变量也作为模糊输入的一部分。function testFuzz_DependsOnTime(uint256 time, uint256 data) public { vm.warp(time); // ... 执行依赖于block.timestamp的库函数逻辑 }为依赖接口创建Mock对象如果库合约调用一个外部接口应创建一个该接口的Mock实现并在测试中将其部署并设置给库合约使用。确保Mock的行为是确定性的或者其行为也可以由模糊测试器控制。5.4 集成到CI/CD流水线目标每次代码提交或合并请求时自动运行这套验证测试。方案配置Foundry脚本在foundry.toml中配置测试参数。[fuzz] runs 10000 # CI环境中可以适当减少如5000以平衡速度与覆盖率 seed 0x123... # 可选固定种子确保CI运行确定性编写CI脚本以GitHub Actions为例name: Library Verification Tests on: [push, pull_request] jobs: verify: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - name: Install Foundry uses: foundry-rs/foundry-toolchainv1 - name: Run Property Tests run: | forge test --match-contract *Property* --fuzz-runs 5000 -vv - name: Run Invariant Tests (if any) run: | forge test --match-contract *Invariant* --fuzz-runs 10000 -vv设置失败阈值可以设定如果发现任何属性被违反即测试失败则CI流程失败。也可以使用forge coverage生成覆盖率报告并设置一个最低覆盖率门槛如“属性测试分支覆盖率需达到85%”。5.5 度量与改进验证效果光有测试不够还需要知道测试有多好。代码覆盖率使用forge coverage。关注分支覆盖率而不仅仅是行覆盖率。确保你的属性测试触发了库合约中所有的if-else分支。突变测试这是一个进阶技巧。使用工具如universalmutator自动在你的库合约源代码中注入小错误“突变体”例如把改成把改成-。然后运行你的属性测试套件。如果你的测试套件足够强大它应该能“杀死”这些突变体即测试失败。存活下来的突变体指示了你的测试覆盖的盲区。这能极大地帮助你改进属性设计。手动代码审查自动化测试不能完全替代人脑。定期与团队成员进行代码走查重点关注属性测试覆盖不到的领域比如复杂的重入场景、与特定EVM操作码的交互等。将基于测试的库合约验证融入开发习惯初期会带来一些额外工作量但一旦建立起流水线和属性思维它将成为你代码库最可靠的自动守护者。它不能保证100%无bug但能将那些通过普通单元测试难以发现的、深藏在条件分支和复杂状态交互中的漏洞大规模地暴露出来从而显著提升合约尤其是作为基石的核心库合约的安全性与可靠性。