返回 SC 笔记
SC Day 63

Solidity/Audit - 审计方法论 - 流程/Checklist/工具链(Slither/Aderyn/Mythril)

### 1. 审计流程总览

2026-06-24
第三阶段:安全审计
审计方法论SlitherAderynMythril安全工具

日期: 2026-06-24 方向: Solidity 阶段: 第三阶段:安全审计 标签: #审计方法论 #Slither #Aderyn #Mythril #安全工具


今日目标

  1. 掌握专业智能合约审计的完整流程(Scoping → 手动审查 → 自动化工具 → 报告)
  2. 建立系统化的审计 Checklist(访问控制/重入/整数/预言机/闪电贷/中心化)
  3. 安装和使用 Slither、Aderyn、Mythril 三大工具
  4. 学会解读工具输出并修复问题
  5. 了解如何撰写专业审计报告

核心概念

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: 如果你只有一天时间审计一个合约,你会重点看什么?

回答思路:

  1. 先用 Slither 扫描 5 分钟,获得总体印象
  2. 重点检查:资金流入/流出函数、权限函数、外部调用
  3. 特别关注:CEI 违规、未检查的返回值、中心化后门
  4. 如果是 DeFi:价格计算、份额计算、闪电贷交互

参考资源

  1. Slither 官方文档
  2. Aderyn 官方文档
  3. Mythril 官方文档
  4. Trail of Bits - 审计 Checklist
  5. Cyfrin Updraft - 安全审计课程
  6. Secureum 审计 Checklist
  7. Solodit - 审计报告数据库
  8. Code4rena 审计竞赛