一句话版本
智能合约一旦部署就难以修改,而且直接管着钱,所以上线前的检查格外重要。常见手段从轻到重:静态分析(自动找常见错误模式)、模糊测试和不变量测试(随机生成大量调用序列,检查某些性质是否始终成立)、人工审计(专家逐行阅读)、形式化验证(用数学证明代码满足规格)。 但"审计过"不等于"安全":审计只覆盖给定版本、给定范围,而且审计员也会漏。理解每种手段能保证什么、不能保证什么,才能正确解读审计报告。
全文分三层:
- 通俗理解:体检、压力测试和数学证明
- 实际用处:主流工具、为什么"审计过"仍被盗、怎么读审计报告
- 原理与工程要点:不变量怎么写、形式化验证的边界
第一层:通俗理解
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 怎么读审计报告
- 看范围:审计了哪些文件、哪个提交(commit hash)?和线上部署的字节码一致吗?
- 看发现:高危、中危问题各有几个?修复状态是"已修复""已确认但不修"还是"有争议"?
- 看审计方:是否独立;同一项目是否有多家审计、是否有漏洞赏金计划(见《负责任披露与漏洞赏金》);
- 看时间:审计是多久以前的?之后有没有升级过?
第三层:原理与工程要点
3.1 不变量测试
不变量是"无论用户怎样调用、按什么顺序调用,都必须始终成立"的性质。测试框架会随机生成大量调用序列,每一步之后检查不变量:
// 不变量:合约实际持有的代币 ≥ 记录的所有用户余额之和
function invariant_solvent() public {
assertGe(token.balanceOf(address(vault)), vault.totalDeposits());
}
好的不变量来自对业务的理解:
| 类别 | 例子 |
|---|---|
| 偿付能力 | 资产 ≥ 负债 |
| 守恒 | 总供应量 = 所有余额之和 |
| 权限 | 非所有者调用任何管理函数都失败 |
| 单调性 | 某个计数器只增不减;某个价格不会在一笔交易内变化超过某个比例 |
| 无利可图 | 任何"存入后立即取出"的序列,不会让调用者获利 |
3.2 形式化验证
把合约代码和性质都翻译成数学逻辑,用求解器证明"对于所有可能的输入和状态,性质都成立";如果不成立,求解器会给出一个具体的反例。
它比测试强在穷尽:测试只能试有限多种情况,证明覆盖全部。但它有明确的边界:
- 只证明你写下的性质:漏写了一条重要性质,证明照样"通过";
- 模型假设:比如假设外部合约行为正常,而真实世界里外部合约可能是攻击者写的;
- 成本高:需要专门的人才,复杂合约可能证明不出来(超时)。
3.3 一个推荐的流程
- 写清楚规格:合约应该做什么、不应该做什么、有哪些不变量;
- 开发时持续跑静态分析和单元测试;
- 写不变量测试,长时间跑模糊测试;
- 对核心逻辑做形式化验证(如果条件允许);
- 多家独立审计;
- 上线后:漏洞赏金、实时监控、暂停开关、限额(见《Fail-closed》)。
每一步都只能降低风险,不能消除风险。"纵深防御"的意思是:前面几层漏掉的问题,后面几层还有机会抓住或者限制损失。
总结
| 层次 | 要点 |
|---|---|
| 四种手段 | 静态分析、模糊 / 不变量测试、人工审计、形式化验证 |
| 核心难题 | 先定义"什么算对":写清规格和不变量 |
| 审计局限 | 范围、版本、组合风险、经济攻击、运维 |
| 实践 | 多层叠加;上线后还要有赏金、监控和熔断 |