返回 SC 笔记
SC Day 84

Solidity: Mini Lending - 自我审计 + Slither扫描 + 漏洞修复

### 一、自我审计方法论

2026-07-01
第四阶段:综合实战
审计SlitherAderyn安全MiniLending漏洞修复

日期: 2026-07-01 方向: Solidity 阶段: 第四阶段:综合实战 标签: #审计 #Slither #Aderyn #安全 #MiniLending #漏洞修复


今日目标

Mini Lending 协议的代码和测试已经完成,今天进入安全审计阶段。目标:

  1. 手动自我审计: 按照专业审计清单逐项检查
  2. 自动化工具扫描: 使用 Slither 和 Aderyn 发现潜在问题
  3. 漏洞修复: 对每个发现的问题进行修复并验证
  4. 撰写审计报告: 以专业格式输出审计报告

这是从"开发者"到"安全研究员"视角的转换——在 DeFi 领域,安全审计能力是 PM 理解协议风险的核心技能。


核心概念

一、自我审计方法论

1.1 审计清单 (DeFi Lending 专用)

┌──────────────────────────────────────────────────────┐
│              Mini Lending 自我审计清单                 │
├──────────────────────────────────────────────────────┤
│                                                      │
│  [权限与访问控制]                                     │
│  □ 1. 管理员函数是否有适当权限检查?                   │
│  □ 2. 是否存在单点失败的 owner 权限?                  │
│  □ 3. 关键参数修改是否有时间锁?                       │
│  □ 4. 紧急暂停机制是否存在?                           │
│                                                      │
│  [重入防护]                                           │
│  □ 5. 所有外部调用前是否已更新状态? (CEI模式)         │
│  □ 6. 是否使用了 ReentrancyGuard?                    │
│  □ 7. 跨函数重入是否被防护?                           │
│                                                      │
│  [数学与精度]                                         │
│  □ 8. 是否存在除零风险?                               │
│  □ 9. 乘法是否在除法之前? (减少精度损失)              │
│  □ 10. 利率计算是否正确处理了复利?                    │
│  □ 11. 不同 decimals 的代币是否正确处理?              │
│                                                      │
│  [预言机安全]                                         │
│  □ 12. 预言机价格是否有过时检查?                      │
│  □ 13. 预言机返回0价格时是否正确处理?                 │
│  □ 14. 是否使用了 TWAP 而非即时价格?                  │
│                                                      │
│  [清算机制]                                           │
│  □ 15. 清算阈值设置是否合理?                          │
│  □ 16. 清算奖励是否足以激励清算者?                    │
│  □ 17. 是否存在级联清算导致的系统风险?                │
│  □ 18. 自我清算是否被阻止?                            │
│                                                      │
│  [代币兼容性]                                         │
│  □ 19. 是否处理了 fee-on-transfer 代币?               │
│  □ 20. 是否处理了 rebasing 代币?                      │
│  □ 21. 是否支持不返回 bool 的 ERC20?                  │
│                                                      │
│  [闪电贷攻击]                                         │
│  □ 22. 存取款是否在同一交易中被限制?                  │
│  □ 23. 价格是否可以在单笔交易中被操纵?                │
│                                                      │
│  [Gas 与 DoS]                                         │
│  □ 24. 是否存在无界循环?                              │
│  □ 25. 是否存在可被堵塞的外部调用?                    │
│  □ 26. 批量操作是否有合理上限?                        │
│                                                      │
└──────────────────────────────────────────────────────┘

1.2 审计优先级

优先级类别潜在损失Mini Lending 关注点
P0 - Critical资金直接被盗全部TVL重入、授权绕过、预言机操纵
P1 - High资金间接损失部分TVL清算逻辑错误、利率计算错误
P2 - Medium功能异常临时锁定DoS、Gas 超限、精度损失
P3 - Low改进建议无直接损失代码风格、Gas优化、事件缺失

二、Slither 自动化扫描

2.1 运行 Slither

# 安装 Slither
pip install slither-analyzer

# 扫描整个项目
slither . --config-file slither.config.json

# 生成 JSON 报告
slither . --json report.json

# 只检查高严重度
slither . --filter-paths "test|lib" --exclude-informational

2.2 Slither 发现的问题与修复

发现 #1: 重入风险 (High)

Detector: reentrancy-eth
MiniLending.withdraw(address,uint256) 有重入风险:
  外部调用: token.transfer(msg.sender, amount) [line 87]
  状态变更在外部调用之后: userDeposits[msg.sender][token] -= amount [line 88]
// ===== 修复前 (有漏洞) =====
function withdraw(address token, uint256 amount) external {
    require(userDeposits[msg.sender][token] >= amount, "Insufficient balance");
    _checkHealthFactor(msg.sender);

    // 危险: 先转账再更新状态 (违反 CEI)
    IERC20(token).transfer(msg.sender, amount);       // 外部调用
    userDeposits[msg.sender][token] -= amount;         // 状态更新在后

    emit Withdrawn(msg.sender, token, amount);
}

// ===== 修复后 =====
function withdraw(address token, uint256 amount) external nonReentrant {
    require(userDeposits[msg.sender][token] >= amount, "Insufficient balance");

    // CEI: Check-Effects-Interactions
    // 1. Check: 上面的 require
    // 2. Effects: 先更新状态
    userDeposits[msg.sender][token] -= amount;

    // 检查健康因子 (在状态更新后)
    _checkHealthFactor(msg.sender);

    // 3. Interactions: 最后做外部调用
    IERC20(token).safeTransfer(msg.sender, amount);

    emit Withdrawn(msg.sender, token, amount);
}

发现 #2: 不安全的 ERC20 操作 (Medium)

Detector: unchecked-transfer
MiniLending.deposit(address,uint256): 使用了不安全的 transfer/transferFrom
某些代币不返回 bool,直接调用可能静默失败
// ===== 修复前 =====
function deposit(address token, uint256 amount) external {
    // 某些代币(如 USDT)不返回 bool
    IERC20(token).transferFrom(msg.sender, address(this), amount);
    userDeposits[msg.sender][token] += amount;
}

// ===== 修复后 =====
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract MiniLending {
    using SafeERC20 for IERC20;

    function deposit(address token, uint256 amount) external nonReentrant {
        // SafeERC20: 处理不返回 bool 的代币 + revert on failure
        uint256 balanceBefore = IERC20(token).balanceOf(address(this));
        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
        uint256 actualAmount = IERC20(token).balanceOf(address(this)) - balanceBefore;

        // 处理 fee-on-transfer 代币: 使用实际收到的金额
        userDeposits[msg.sender][token] += actualAmount;

        emit Deposited(msg.sender, token, actualAmount);
    }
}

发现 #3: 预言机价格过时风险 (High)

Detector: calls-loop (自定义检测)
getPrice() 没有检查价格更新时间,可能使用过时价格
// ===== 修复前 =====
function _getPrice(address token) internal view returns (uint256) {
    return IPriceOracle(oracle).getPrice(token);
    // 没有检查价格是否过时!
}

// ===== 修复后 =====
uint256 public constant PRICE_STALENESS_THRESHOLD = 1 hours;

function _getPrice(address token) internal view returns (uint256) {
    (uint256 price, uint256 updatedAt) = IPriceOracle(oracle).getPriceWithTimestamp(token);

    // 检查价格是否过时
    if (block.timestamp - updatedAt > PRICE_STALENESS_THRESHOLD) {
        revert StalePriceError(token, updatedAt);
    }

    // 检查价格是否为0
    if (price == 0) {
        revert ZeroPriceError(token);
    }

    return price;
}

发现 #4: 缺少事件 (Informational)

Detector: events-maths
关键参数修改没有发出事件,影响链下监控
// ===== 修复前 =====
function setCollateralFactor(address token, uint256 factor) external onlyOwner {
    markets[token].collateralFactor = factor;
    // 没有事件! 链下监控无法感知
}

// ===== 修复后 =====
event CollateralFactorUpdated(
    address indexed token,
    uint256 oldFactor,
    uint256 newFactor
);

function setCollateralFactor(address token, uint256 factor) external onlyOwner {
    require(factor <= 9000, "Factor too high"); // 最大90%
    require(factor >= 1000, "Factor too low");  // 最小10%

    uint256 oldFactor = markets[token].collateralFactor;
    markets[token].collateralFactor = factor;

    emit CollateralFactorUpdated(token, oldFactor, factor);
}

发现 #5: 清算奖励可能导致坏账 (Medium)

自我审计发现:
当抵押品价值不足以覆盖借款+清算奖励时,清算者无利可图,
可能导致无人清算 → 坏账
// ===== 修复前 =====
function liquidate(
    address borrower,
    address borrowToken,
    uint256 repayAmount
) external {
    // ...
    uint256 collateralToSeize = (repayAmount * (10000 + liquidationBonus))
        / 10000 * collateralPrice;
    // 如果 collateralToSeize > 剩余抵押品,交易 revert
    // → 无人能清算 → 坏账!
}

// ===== 修复后 =====
function liquidate(
    address borrower,
    address borrowToken,
    uint256 repayAmount
) external nonReentrant {
    require(msg.sender != borrower, "Cannot self-liquidate");
    require(getHealthFactor(borrower) < HEALTH_FACTOR_THRESHOLD, "Position is healthy");

    // 计算预期获得的抵押品
    uint256 collateralToSeize = _calculateSeizedCollateral(repayAmount);

    // 如果抵押品不足以提供完整奖励,清算全部可用抵押品
    uint256 availableCollateral = userDeposits[borrower][collateralToken];
    if (collateralToSeize > availableCollateral) {
        collateralToSeize = availableCollateral;
        // 调整实际还款金额 (根据实际获得的抵押品反算)
        repayAmount = _calculateRepayForCollateral(collateralToSeize);
    }

    // 执行清算
    userBorrows[borrower][borrowToken] -= repayAmount;
    userDeposits[borrower][collateralToken] -= collateralToSeize;

    IERC20(borrowToken).safeTransferFrom(msg.sender, address(this), repayAmount);
    IERC20(collateralToken).safeTransfer(msg.sender, collateralToSeize);

    emit Liquidated(borrower, msg.sender, repayAmount, collateralToSeize);
}

三、Aderyn 扫描

# 安装 Aderyn (Rust编写,速度快)
cargo install aderyn

# 运行扫描
aderyn . --output report.md

Aderyn 额外发现

发现 #6: 缺少零地址检查 (Low)

// ===== 修复前 =====
constructor(address _oracle) {
    oracle = _oracle;  // 如果传入 address(0) 会导致后续所有价格查询失败
}

// ===== 修复后 =====
constructor(address _oracle) {
    if (_oracle == address(0)) revert ZeroAddressError();
    oracle = _oracle;
}

发现 #7: 浮点精度问题 (Medium)

// ===== 修复前: 精度损失 =====
function _calculateInterest(uint256 principal, uint256 timeElapsed)
    internal view returns (uint256)
{
    // 先除后乘 → 精度损失!
    uint256 yearlyRate = principal / SECONDS_PER_YEAR * annualRate;
    return yearlyRate * timeElapsed;
}

// ===== 修复后: 先乘后除 =====
function _calculateInterest(uint256 principal, uint256 timeElapsed)
    internal view returns (uint256)
{
    // 先乘后除,保留精度
    // principal * annualRate * timeElapsed / SECONDS_PER_YEAR / RATE_PRECISION
    return (principal * annualRate * timeElapsed)
        / (SECONDS_PER_YEAR * RATE_PRECISION);
}

四、完整审计报告

# Mini Lending Protocol — 自我审计报告

## 1. 审计概述

| 项目 | 详情 |
|------|------|
| **协议名称** | Mini Lending |
| **审计类型** | 自我审计 + 自动化工具 |
| **审计日期** | 2026-07-01 |
| **代码版本** | commit abc123 |
| **代码行数** | ~450 LOC (核心合约) |
| **工具** | Slither v0.10.x, Aderyn v0.2.x |
| **审计员** | 自我审计 |

## 2. 发现汇总

| 严重度 | 数量 | 已修复 |
|--------|------|--------|
| Critical | 0 | - |
| High | 2 | 2 ✅ |
| Medium | 3 | 3 ✅ |
| Low | 1 | 1 ✅ |
| Informational | 1 | 1 ✅ |
| **总计** | **7** | **7 ✅** |

## 3. 详细发现

### [H-01] 重入漏洞 — withdraw 函数
- **严重度**: High
- **位置**: MiniLending.sol#L87
- **描述**: withdraw 函数在外部调用后更新状态,违反 CEI 模式
- **影响**: 攻击者可通过恶意合约重入 withdraw,提取超额资金
- **修复**: 添加 nonReentrant modifier + 重排为 CEI 顺序
- **状态**: ✅ 已修复并测试

### [H-02] 预言机价格无过时检查
- **严重度**: High
- **位置**: MiniLending.sol#L142
- **描述**: _getPrice 未检查价格更新时间,可能使用过时价格
- **影响**: 过时价格可能导致错误的清算或超额借款
- **修复**: 添加时间戳检查 + 零价格检查
- **状态**: ✅ 已修复并测试

### [M-01] 不安全的 ERC20 操作
- **严重度**: Medium
- **位置**: MiniLending.sol 多处
- **描述**: 直接使用 transfer/transferFrom,未处理不返回bool的代币
- **影响**: USDT 等代币的转账可能静默失败
- **修复**: 使用 OpenZeppelin SafeERC20
- **状态**: ✅ 已修复

### [M-02] 清算机制坏账风险
- **严重度**: Medium
- **位置**: MiniLending.sol#L165
- **描述**: 当抵押品不足以提供清算奖励时,清算revert
- **影响**: 可能导致无人清算 → 协议积累坏账
- **修复**: 允许部分清算,动态调整清算金额
- **状态**: ✅ 已修复并测试

### [M-03] 利率计算精度损失
- **严重度**: Medium
- **位置**: MiniLending.sol#L198
- **描述**: 先除后乘导致精度损失
- **影响**: 长期累积可能导致利息计算偏差
- **修复**: 调整运算顺序为先乘后除
- **状态**: ✅ 已修复

### [L-01] 缺少零地址检查
- **严重度**: Low
- **位置**: MiniLending.sol constructor
- **描述**: 构造函数未检查关键地址参数是否为零
- **影响**: 部署错误可能导致合约不可用
- **修复**: 添加零地址检查
- **状态**: ✅ 已修复

### [I-01] 关键参数修改缺少事件
- **严重度**: Informational
- **位置**: MiniLending.sol 管理函数
- **描述**: setCollateralFactor 等函数未发出事件
- **影响**: 链下监控无法追踪参数变更
- **修复**: 添加事件 + 参数范围检查
- **状态**: ✅ 已修复

## 4. 建议 (Future Improvements)

1. **添加 Timelock**: 管理员操作添加24-48小时时间锁
2. **多预言机源**: 使用 Chainlink + Pyth 双源价格
3. **利率模型升级**: 从线性利率升级到分段利率(Kink Model)
4. **闪电贷保护**: 添加存取款时间间隔限制
5. **紧急暂停**: 添加全局暂停和单市场暂停功能
6. **形式化验证**: 使用 Certora/Halmos 对核心不变量进行证明

关键要点总结

  1. 审计是一种思维方式: 不是"我写的代码有什么 bug",而是"如果我是攻击者,如何利用这个合约赚钱"。这种对抗性思维是 DeFi PM 必须具备的。

  2. 自动化工具是起点不是终点: Slither/Aderyn 能找到80%的低级错误,但复杂的逻辑漏洞(如经济模型攻击)需要人工审计。

  3. CEI 模式是第一道防线: Check-Effects-Interactions 是 Solidity 安全的基石。所有外部调用必须在状态更新之后。

  4. 审计报告是沟通工具: 专业的审计报告格式帮助团队理解风险、排优先级、追踪修复进度。

  5. fee-on-transfer 和 rebasing 代币是常见陷阱: 不要假设所有 ERC20 都是"标准"实现。使用 balanceBefore/After 模式处理实际到账金额。


常见误区

  1. 误区: 用了 ReentrancyGuard 就安全了 — ReentrancyGuard 防止同一合约的重入,但跨合约重入(read-only reentrancy)需要额外防护。

  2. 误区: 审计工具没报问题 = 合约安全 — 工具只能检测已知模式。逻辑错误、经济攻击、治理攻击需要人工审计。

  3. 误区: 修了 bug 就完了 — 每个修复都需要新的测试用例验证,并且要检查修复是否引入了新问题。

  4. 误区: 审计一次就够了 — 每次代码变更都需要重新审计受影响的部分。CI/CD 中集成 Slither 是最佳实践。


面试关联

Q: "你如何评估一个 DeFi 协议的安全性?"

回答:

我会从四个层面评估:

  1. 代码审计: 是否经过知名审计公司审计?审计覆盖范围?发现了什么问题?
  2. 经济模型: 激励是否对齐?在极端市场条件下(如 ETH 跌50%)是否仍然稳定?
  3. 运维安全: 管理密钥如何管理?是否有 Timelock?紧急暂停机制?
  4. 历史记录: 该协议或其 fork 是否曾被攻击?如何响应的?

以我们的 Mini Lending 为例,自我审计发现了7个问题(2个 High),说明即使是简单的协议也会有安全隐患。对于生产级协议,我会要求至少2家审计公司的交叉审计 + 持续的 bug bounty。


参考资源

  1. Slither Documentation — Slither 官方文档
  2. Aderyn by Cyfrin — Aderyn 安全分析工具
  3. SWC Registry — 智能合约漏洞分类标准
  4. DeFi Security Best Practices — Trail of Bits 安全最佳实践
  5. Audit Report Examples — Pashov 审计报告集
  6. Immunefi Bug Bounty — DeFi Bug Bounty 平台