Caloogy 文章
English

合约审计和形式化验证在做什么

日期
2026 年 6 月 16 日
系列
链上
阅读
4 分钟
主题
OnchainSmart contractAuditFormal verification

一句话版本

智能合约一旦部署就难以修改,而且直接管着钱,所以上线前的检查格外重要。常见手段从轻到重:静态分析(自动找常见错误模式)、模糊测试和不变量测试(随机生成大量调用序列,检查某些性质是否始终成立)、人工审计(专家逐行阅读)、形式化验证(用数学证明代码满足规格)。 但"审计过"不等于"安全":审计只覆盖给定版本、给定范围,而且审计员也会漏。理解每种手段能保证什么、不能保证什么,才能正确解读审计报告。

全文分三层:

  1. 通俗理解:体检、压力测试和数学证明
  2. 实际用处:主流工具、为什么"审计过"仍被盗、怎么读审计报告
  3. 原理与工程要点:不变量怎么写、形式化验证的边界

第一层:通俗理解

1.1 四种检查

手段 比喻 擅长 不擅长
静态分析 体检的常规项目 快速发现已知的错误模式 业务逻辑错误
模糊 / 不变量测试 压力测试:成千上万种随机操作 发现人没想到的调用顺序 需要先定义"什么算错"
人工审计 专家会诊 业务逻辑、经济模型、设计缺陷 受时间和人的注意力限制
形式化验证 数学证明 在规格范围内穷尽所有情况 规格本身写错就白证

1.2 "什么算对"是最难的

所有手段都需要回答一个问题:这个合约应该满足什么性质?比如:

把这些性质写清楚,往往比找 bug 本身更有价值。


第二层:实际用处

2.1 主流工具

工具 类型
Slither 静态分析,检测重入、未初始化变量、权限缺失等上百种模式
Foundry(fuzz / invariant) 模糊测试和不变量测试,直接用 Solidity 写测试
Echidna、Medusa 基于性质的模糊测试器
Certora、Halmos、SMTChecker 形式化验证 / 符号执行
主网分叉测试 在本地复制主网状态,测试和真实协议的交互(见《闪电贷与价格操纵》)

2.2 为什么"审计过"仍被盗

原因 说明
审计范围外 只审计了核心合约,没审计后来加的模块、前端、部署脚本
审计后又改了代码 审计的是 v1,上线的是"修了几个小问题的" v1.1
组合风险 合约本身没问题,但和其他协议组合后出问题(如只读重入、价格预言机)
经济攻击 代码按设计运行,但设计本身在极端市场条件下可被利用
运维与密钥 合约没问题,管理员私钥被盗(见《可升级合约与管理员私钥》)
工具链 编译器 bug(见《重入攻击》 里的 Curve 事件)
审计员漏了 时间有限,人会疲劳

2.3 怎么读审计报告


第三层:原理与工程要点

3.1 不变量测试

不变量是"无论用户怎样调用、按什么顺序调用,都必须始终成立"的性质。测试框架会随机生成大量调用序列,每一步之后检查不变量:

// 不变量:合约实际持有的代币 ≥ 记录的所有用户余额之和
function invariant_solvent() public {
    assertGe(token.balanceOf(address(vault)), vault.totalDeposits());
}

好的不变量来自对业务的理解:

类别 例子
偿付能力 资产 ≥ 负债
守恒 总供应量 = 所有余额之和
权限 非所有者调用任何管理函数都失败
单调性 某个计数器只增不减;某个价格不会在一笔交易内变化超过某个比例
无利可图 任何"存入后立即取出"的序列,不会让调用者获利

3.2 形式化验证

把合约代码和性质都翻译成数学逻辑,用求解器证明"对于所有可能的输入和状态,性质都成立";如果不成立,求解器会给出一个具体的反例。

它比测试强在穷尽:测试只能试有限多种情况,证明覆盖全部。但它有明确的边界:

3.3 一个推荐的流程

  1. 写清楚规格:合约应该做什么、不应该做什么、有哪些不变量;
  2. 开发时持续跑静态分析和单元测试;
  3. 写不变量测试,长时间跑模糊测试;
  4. 对核心逻辑做形式化验证(如果条件允许);
  5. 多家独立审计;
  6. 上线后:漏洞赏金、实时监控、暂停开关、限额(见《Fail-closed》)。

每一步都只能降低风险,不能消除风险。"纵深防御"的意思是:前面几层漏掉的问题,后面几层还有机会抓住或者限制损失。


总结

层次 要点
四种手段 静态分析、模糊 / 不变量测试、人工审计、形式化验证
核心难题 先定义"什么算对":写清规格和不变量
审计局限 范围、版本、组合风险、经济攻击、运维
实践 多层叠加;上线后还要有赏金、监控和熔断