Solidity/Audit - 审计方法论 - 流程/Checklist/工具链(Slither/Aderyn/Mythril)
### 1. 审计流程总览
日期: 2026-06-24 方向: Solidity 阶段: 第三阶段:安全审计 标签: #审计方法论 #Slither #Aderyn #Mythril #安全工具
今日目标
- 掌握专业智能合约审计的完整流程(Scoping → 手动审查 → 自动化工具 → 报告)
- 建立系统化的审计 Checklist(访问控制/重入/整数/预言机/闪电贷/中心化)
- 安装和使用 Slither、Aderyn、Mythril 三大工具
- 学会解读工具输出并修复问题
- 了解如何撰写专业审计报告
核心概念
1. 审计流程总览
一次专业的智能合约审计通常分为以下阶段:
审计全流程:
Phase 0: Scoping(范围确定)
├── 理解项目背景和业务逻辑
├── 确定审计范围(哪些合约/函数)
├── 评估复杂度和所需时间
└── 收集文档:白皮书、README、设计文档
Phase 1: 初步审查(1-2天)
├── 代码通读,理解架构
├── 画出合约交互图
├── 标记关键函数和状态变量
└── 识别外部依赖(预言机、治理等)
Phase 2: 手动审计(核心阶段)
├── 逐函数深度审查
├── 按 Checklist 逐项检查
├── 攻击者思维:如何利用?
├── 跨函数交互分析
└── 经济模型安全分析
Phase 3: 自动化工具扫描
├── Slither(静态分析)
├── Aderyn(Rust 实现的快速静态分析)
├── Mythril(符号执行)
├── Foundry fuzz testing
└── 交叉验证手动发现
Phase 4: 报告撰写
├── 按严重程度分类 findings
├── 每个 finding 提供修复建议
├── 整体安全评估
└── 提交并讨论修复方案
Phase 5: 修复验证(Mitigation Review)
├── 验证修复是否正确
├── 检查修复是否引入新问题
└── 出具最终报告
2. 完整审计 Checklist
A. 访问控制 (Access Control)
□ 所有敏感函数都有正确的权限修饰符?
- onlyOwner / onlyRole / onlyAdmin
- 初始化函数只能调用一次?
□ 权限变更需要两步确认(时间锁/多签)?
- transferOwnership → pendingOwner → acceptOwnership
□ 是否有后门函数或未保护的关键操作?
- 无限 mint 权限?
- 暂停/销毁权限集中?
□ 构造函数/初始化函数是否正确?
- Proxy 模式下 initialize 是否有 initializer 修饰符?
- 是否使用了 tx.origin 做认证?
B. 重入攻击 (Reentrancy)
□ 所有外部调用是否遵循 CEI 模式?
- Checks: 验证条件
- Effects: 更新状态
- Interactions: 外部调用
□ 是否使用 ReentrancyGuard?
- 特别是涉及 ETH 转账的函数
- 跨函数重入也需要考虑
□ 是否有只读重入风险?
- view 函数读取的状态是否可能被操纵?
- Curve pool 的 read-only reentrancy 案例
C. 整数与数学 (Integer / Math)
□ 是否使用 Solidity >= 0.8(自动溢出检查)?
- 如果使用 unchecked,是否安全?
- 类型转换是否安全(uint256 → uint128)?
□ 除法精度是否有问题?
- 除法截断导致 rounding down?
- 先乘后除避免精度丢失?
- 第一个存款人攻击(share = 0)?
□ 大数乘法是否会溢出?
- price * amount 是否可能超过 uint256?
D. 预言机安全 (Oracle)
□ 价格数据来源是否可被操纵?
- 使用 TWAP 而非瞬时价格?
- 是否有多个预言机交叉验证?
□ Chainlink 预言机是否正确使用?
- 检查 latestRoundData 返回值?
- 检查价格是否过期(staleness check)?
- 处理预言机宕机情况?
□ 是否有闪电贷操纵风险?
- 使用链上 AMM 价格做抵押品估值?
E. 闪电贷相关 (Flash Loan)
□ 关键操作是否可在单笔交易中执行?
- 存款→操纵→取款 在同一交易?
- 投票→提案→执行 在同一交易?
□ 价格/余额是否依赖当前区块状态?
- 可以被闪电贷操纵?
□ 治理代币是否有快照机制?
- 防止闪电贷获取投票权
F. 中心化风险 (Centralization)
□ 管理员有哪些特权操作?
- 能否暂停合约?
- 能否修改关键参数(利率、手续费)?
- 能否升级合约逻辑?
- 能否转走用户资金?
□ 是否有时间锁(Timelock)?
- 关键操作是否需要延迟执行?
□ 多签/DAO 控制?
- 管理密钥分散程度?
□ 是否有紧急暂停机制?
- 暂停后能否恢复?
G. 其他关键检查
□ ERC20 交互安全
- 处理 fee-on-transfer 代币?
- 处理 rebase 代币?
- 处理返回值(有些代币不返回 bool)?
- 使用 SafeERC20?
□ 外部合约调用
- 返回值是否检查?
- 低级调用 (call/delegatecall) 的使用是否安全?
□ 事件完整性
- 关键状态变更是否发射事件?
- 事件参数是否正确?
□ Gas 优化安全
- 优化是否引入安全问题?
- 循环是否有 DoS 风险(gas limit)?
3. 审计工具链对比
| 工具 | 类型 | 语言 | 速度 | 误报率 | 擅长领域 |
|---|---|---|---|---|---|
| Slither | 静态分析 | Python | 快(秒级) | 中等 | 代码模式检测、数据流分析 |
| Aderyn | 静态分析 | Rust | 极快(秒级) | 低-中 | Foundry 项目、Gas 优化 |
| Mythril | 符号执行 | Python | 慢(分钟级) | 较低 | 深层逻辑漏洞、路径分析 |
| Foundry fuzz | 模糊测试 | Rust | 中等 | 极低 | 边界条件、不变量违反 |
| Echidna | 属性测试 | Haskell | 中等 | 极低 | 不变量测试 |
| Certora | 形式化验证 | 专用语言 | 慢 | 极低 | 数学证明正确性 |
代码实战
实战 1: Slither 安装和使用
# ===== 安装 Slither =====
pip install slither-analyzer
# 或者使用 pipx(推荐,避免依赖冲突)
pipx install slither-analyzer
# 验证安装
slither --version
# ===== 基本使用 =====
# 分析单个文件
slither src/VulnerableVault.sol
# 分析 Foundry 项目
slither . --foundry-compile-all
# 只显示高危和中危
slither . --filter-paths "node_modules|test" \
--exclude-informational \
--exclude-low
# 输出 JSON 格式(便于集成 CI)
slither . --json output.json
# 打印合约信息
slither . --print contract-summary
slither . --print function-summary
slither . --print inheritance-graph
实战 2: 用 Slither 扫描示例合约
创建一个有多种漏洞的示例合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/// @title VulnerableVault - 一个有多种漏洞的金库合约
/// @notice 用于审计练习
contract VulnerableVault {
address public owner;
mapping(address => uint256) public shares;
uint256 public totalShares;
IERC20 public token;
bool public paused;
// 漏洞 1: 没有事件
// 漏洞 2: owner 变更没有两步确认
constructor(address _token) {
owner = msg.sender;
token = IERC20(_token);
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
// 漏洞 3: 没有初始检查,第一个存款人攻击
function deposit(uint256 amount) external {
require(!paused, "Paused");
require(amount > 0, "Zero amount");
uint256 shareAmount;
if (totalShares == 0) {
shareAmount = amount;
} else {
// 漏洞 4: 整数除法截断
shareAmount = (amount * totalShares) / token.balanceOf(address(this));
}
// 漏洞 5: 没用 SafeERC20
token.transferFrom(msg.sender, address(this), amount);
shares[msg.sender] += shareAmount;
totalShares += shareAmount;
}
function withdraw(uint256 shareAmount) external {
require(!paused, "Paused");
require(shares[msg.sender] >= shareAmount, "Insufficient shares");
uint256 tokenAmount = (shareAmount * token.balanceOf(address(this))) / totalShares;
// 漏洞 6: 虽然不是经典重入(ERC20),但状态更新顺序不佳
shares[msg.sender] -= shareAmount;
totalShares -= shareAmount;
// 漏洞 5 重复: 没用 SafeERC20
token.transfer(msg.sender, tokenAmount);
}
// 漏洞 7: owner 可以直接转走所有资金(中心化风险)
function emergencyWithdraw(address to) external onlyOwner {
uint256 balance = token.balanceOf(address(this));
token.transfer(to, balance);
}
// 漏洞 8: 任何人可以直接发送 token 来稀释 shares
// (通过 token.transfer 直接往合约发 token)
function setPaused(bool _paused) external onlyOwner {
paused = _paused;
}
// 漏洞 9: 直接设置 owner,没有两步确认
function transferOwnership(address newOwner) external onlyOwner {
owner = newOwner;
}
}
Slither 扫描结果解读
$ slither src/VulnerableVault.sol
# 预期输出(简化):
VulnerableVault.deposit(uint256) (src/VulnerableVault.sol#30-45)
uses a dangerous strict equality:
- totalShares == 0 (src/VulnerableVault.sol#34)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#dangerous-strict-equalities
VulnerableVault.deposit(uint256) (src/VulnerableVault.sol#30-45)
ignores return value by token.transferFrom(msg.sender,address(this),amount)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#unchecked-transfer
VulnerableVault.withdraw(uint256) (src/VulnerableVault.sol#47-57)
ignores return value by token.transfer(msg.sender,tokenAmount)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#unchecked-transfer
VulnerableVault.emergencyWithdraw(address) (src/VulnerableVault.sol#60-63)
sends tokens to arbitrary user
Dangerous call: token.transfer(to,balance)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation
VulnerableVault.transferOwnership(address) (src/VulnerableVault.sol#70-72)
does not use a two-step ownership transfer
Missing zero-address validation: owner = newOwner
Missing events for critical state changes:
- VulnerableVault.deposit
- VulnerableVault.withdraw
- VulnerableVault.transferOwnership
Slither 结果分类和处理
Slither 检测器分类:
High(高危 - 必须修复):
├── unchecked-transfer: 未检查 ERC20 返回值
├── arbitrary-send: 向任意地址发送资产
├── reentrancy-eth: ETH 重入
└── controlled-delegatecall: 可控的 delegatecall
Medium(中危 - 强烈建议修复):
├── dangerous-strict-equalities: 严格等式比较
├── reentrancy-no-eth: 非 ETH 重入
├── missing-zero-check: 缺少零地址检查
└── uninitialized-state: 未初始化的状态变量
Low(低危 - 建议修复):
├── missing-events: 缺少事件
├── naming-convention: 命名不规范
└── solc-version: 编译器版本问题
Informational(信息 - 可选修复):
├── dead-code: 死代码
├── too-many-digits: 魔术数字
└── pragma: pragma 指令问题
实战 3: Aderyn 使用
# ===== 安装 Aderyn =====
# 需要 Rust 环境
cargo install aderyn
# 或者通过 npm (如果有 npm wrapper)
# npm install -g aderyn
# ===== 使用 Aderyn =====
# 扫描当前 Foundry 项目
aderyn .
# 指定输出文件
aderyn . -o report.md
# 排除特定路径
aderyn . --exclude test/ --exclude script/
Aderyn 输出示例(Markdown 格式):
# Aderyn Analysis Report
## Summary
- **Files Analyzed**: 3
- **Issues Found**: 8 (2 High, 3 Medium, 3 Low)
## High Issues
### H-1: Unchecked Return Value for ERC20 Transfer
**Severity**: High
**Location**: VulnerableVault.sol:42, 54, 62
The return value of ERC20 `transfer` and `transferFrom` calls
is not checked. Some tokens return `false` instead of reverting.
**Recommendation**: Use OpenZeppelin's `SafeERC20` library.
### H-2: Centralization Risk - Owner Can Drain Funds
**Severity**: High
**Location**: VulnerableVault.sol:60-63
The `emergencyWithdraw` function allows the owner to withdraw
all funds to any address without timelock or multisig.
**Recommendation**: Add timelock and emit events for emergency actions.
实战 4: Mythril 使用
# ===== 安装 Mythril =====
pip install mythril
# 或使用 Docker
docker pull mythril/myth
# ===== 使用 Mythril =====
# 分析单个合约(符号执行,较慢)
myth analyze src/VulnerableVault.sol --solv 0.8.20
# 限制执行时间
myth analyze src/VulnerableVault.sol --execution-timeout 300
# Docker 方式
docker run -v $(pwd):/tmp mythril/myth analyze /tmp/src/VulnerableVault.sol
# 指定交易深度(越深越慢但更全面)
myth analyze src/VulnerableVault.sol --max-depth 12
Mythril 输出示例:
==== Integer Arithmetic Bugs ====
SWC ID: 101
Severity: High
Contract: VulnerableVault
Function name: deposit(uint256)
PC address: 1234
Estimated Gas Usage: 12345 - 67890
A possible integer overflow/underflow exists in the expression:
amount * totalShares
==== Dependence on predictable environment variable ====
SWC ID: 116
Severity: Low
Contract: VulnerableVault
The contract uses block.timestamp or block.number...
实战 5: 修复漏洞后的安全版本
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable2Step.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
/// @title SecureVault - 修复所有漏洞后的安全版本
contract SecureVault is Ownable2Step, ReentrancyGuard, Pausable {
using SafeERC20 for IERC20; // 修复: SafeERC20
IERC20 public immutable token;
mapping(address => uint256) public shares;
uint256 public totalShares;
uint256 private constant MINIMUM_SHARES = 1000; // 修复: 防止第一存款人攻击
// 修复: 添加事件
event Deposited(address indexed user, uint256 amount, uint256 shares);
event Withdrawn(address indexed user, uint256 shares, uint256 amount);
event EmergencyWithdrawn(address indexed to, uint256 amount);
constructor(address _token) Ownable(msg.sender) {
require(_token != address(0), "Zero address");
token = IERC20(_token);
}
function deposit(uint256 amount) external nonReentrant whenNotPaused {
require(amount > 0, "Zero amount");
uint256 shareAmount;
uint256 currentBalance = token.balanceOf(address(this));
if (totalShares == 0) {
// 修复: 第一次存款锁定最小份额防止操纵
shareAmount = amount;
require(shareAmount > MINIMUM_SHARES, "Initial deposit too small");
// 锁定 MINIMUM_SHARES 到死地址,防止第一存款人攻击
shares[address(0xdead)] = MINIMUM_SHARES;
totalShares = MINIMUM_SHARES;
shareAmount -= MINIMUM_SHARES;
} else {
// 修复: 使用更安全的计算方式
shareAmount = (amount * totalShares) / currentBalance;
require(shareAmount > 0, "Shares too small");
}
// 修复: 使用 SafeERC20
token.safeTransferFrom(msg.sender, address(this), amount);
shares[msg.sender] += shareAmount;
totalShares += shareAmount;
emit Deposited(msg.sender, amount, shareAmount);
}
function withdraw(uint256 shareAmount) external nonReentrant whenNotPaused {
require(shareAmount > 0, "Zero shares");
require(shares[msg.sender] >= shareAmount, "Insufficient shares");
uint256 tokenAmount = (shareAmount * token.balanceOf(address(this))) / totalShares;
// CEI 模式: 先更新状态
shares[msg.sender] -= shareAmount;
totalShares -= shareAmount;
// 最后外部调用
token.safeTransfer(msg.sender, tokenAmount);
emit Withdrawn(msg.sender, shareAmount, tokenAmount);
}
// 修复: 限制紧急提取,添加时间锁(简化版)
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
// 注意: 真实项目中应该加 Timelock 控制
}
关键要点总结
审计工具使用策略
推荐工作流:
1. Slither(第一轮,秒级)
→ 快速识别常见模式问题
→ 覆盖面广,适合初筛
2. Aderyn(第二轮,秒级)
→ 与 Slither 交叉验证
→ 可能发现 Slither 遗漏的问题
3. 手动审计(核心阶段)
→ 逐函数深度审查
→ 业务逻辑和经济模型分析
→ 工具无法替代人工判断
4. Mythril(补充,分钟级)
→ 符号执行发现深层逻辑漏洞
→ 适合关键函数的深度分析
5. Foundry fuzz(持续运行)
→ 模糊测试发现边界条件
→ invariant 测试验证不变量
审计报告结构
# [项目名] 智能合约安全审计报告
## 1. 概述
- 审计范围、时间、版本
- 方法论和工具
## 2. 发现汇总
| ID | 标题 | 严重程度 | 状态 |
|----|------|---------|------|
| H-1 | ... | High | 已修复 |
| M-1 | ... | Medium | 已确认 |
## 3. 详细发现
### [H-1] 标题
- **严重程度**: High / Medium / Low / Informational
- **位置**: 文件名:行号
- **描述**: 漏洞描述
- **影响**: 可能造成的损失
- **PoC**: 攻击代码或步骤
- **建议**: 修复方案
- **状态**: 修复/确认/争议
## 4. 整体评估
- 代码质量
- 测试覆盖率
- 架构安全性
常见误区
误区 1: "工具扫描没有报告问题 = 合约安全"
纠正: 自动化工具只能发现已知模式的漏洞。业务逻辑错误、经济模型缺陷、复杂的跨合约交互问题,工具几乎无法检测。工具是辅助手段,不是替代品。
误区 2: "Slither 报告的所有 findings 都需要修复"
纠正: Slither 有一定的误报率。每个 finding 都需要人工判断:是否是真正的安全问题?在当前上下文中是否可利用?修复的成本是否合理?有时候 Slither 报告的"问题"在特定设计中是有意为之的。
误区 3: "审计一次就够了"
纠正: 合约每次重大更新都需要重新审计。即使是"小改动"也可能引入新漏洞。持续的安全实践(代码评审、自动化测试、监控)比一次性审计更重要。
误区 4: "审计公司出了报告就安全了"
纠正: 审计报告是"在审计时间内、以审计团队的能力、对特定版本代码的安全评估"。它不是安全保证。许多被审计过的合约后来仍被攻击(如 Euler Finance)。
面试关联
Q1: 描述你的智能合约审计流程?
回答框架:
我采用"工具先行 + 手动深入"的方法。
第一步: 理解项目。阅读文档和白皮书,画出合约交互图,理解资金流向和权限模型。
第二步: 运行 Slither 和 Aderyn 做快速扫描,标记所有自动化发现。
第三步: 按照系统化 Checklist 逐项手动审查:访问控制、重入、整数安全、预言机、闪电贷、中心化风险。对每个外部可调用函数思考"攻击者会如何利用?"
第四步: 对关键函数用 Mythril 做符号执行,用 Foundry 写 fuzz test 验证不变量。
第五步: 撰写审计报告,每个 finding 都有 PoC、影响评估和修复建议。
Q2: 如果你只有一天时间审计一个合约,你会重点看什么?
回答思路:
- 先用 Slither 扫描 5 分钟,获得总体印象
- 重点检查:资金流入/流出函数、权限函数、外部调用
- 特别关注:CEI 违规、未检查的返回值、中心化后门
- 如果是 DeFi:价格计算、份额计算、闪电贷交互