10分钟智能合约:进阶实战-7.3 Damn Vulnerable DeFi 项目解析与热身

18 阅读5分钟

欢迎订阅专栏10分钟智能合约:进阶实战

Damn Vulnerable DeFi 项目解析与热身

Damn Vulnerable DeFi 是智能合约安全领域最具代表性的实战训练平台。本文将带你全面了解这个项目,并完成第一个挑战 —— Unstoppable 的热身。


一、项目概述

Damn Vulnerable DeFi 是由 OpenZeppelin 的安全专家发起、现由 The Red Guild 维护的免费开源 DeFi 安全靶场。它通过一系列精心设计的挑战,让你在攻防实战中掌握智能合约安全的核心技能。

核心理念:Learning by breaking。只有亲手攻破一个协议,才能真正理解如何防御。

截至 V4 版本,项目已全面迁移至 Foundry 框架,所有合约升级到 Solidity 0.8.25,并新增了涵盖 AMM、清算、Merkle 证明、跨链桥提款等主题的挑战。挑战内容覆盖闪电贷、价格预言机、治理、NFT、DEX、时间锁、元交易、可升级性等 DeFi 核心安全领域。


二、环境搭建

1. 前置条件

开始之前,请确保已安装:

  • Git:用于克隆仓库
  • Foundry:核心开发工具链
  • 对 Solidity 和智能合约安全的基础了解
  • 一个有效的以太坊 RPC URL(用于需要主网分叉的挑战)

提示:如果不想在本地安装 Foundry,也可以使用 VSCode 的 Devcontainer 功能(需安装 Docker Desktop),容器内已预置所有必要工具。

2. 安装步骤

# 1. 克隆仓库
git clone https://github.com/theredguild/damn-vulnerable-defi.git
cd damn-vulnerable-defi

# 2. 切换到最新版本(推荐 V4)
git checkout v4.1.0

# 3. 配置环境变量
cp .env.sample .env
# 在 .env 中添加你的 RPC URL(如 Infura 或 Alchemy)

# 4. 安装依赖
forge install

3. 验证环境

运行任意挑战的测试,确认环境正常:

forge test --match-contract Unstoppable -vvvv

如果看到测试失败(红色输出),说明环境已就绪 —— 你的任务就是让它变绿。


三、挑战工作流

每个挑战都遵循统一的结构:

组件位置用途
挑战说明src/<挑战名>/README.md描述场景、目标和约束
漏洞合约src/<挑战名>/包含漏洞的 Solidity 合约
测试文件test/<挑战名>/<挑战名>.t.solFoundry 测试,在此实现攻击

标准解题流程

  1. 阅读挑战说明:理解场景、目标和约束。
  2. 分析漏洞合约:找出安全缺陷。
  3. 编写攻击代码:在测试文件的 test_challenge() 函数中实现利用逻辑。
  4. 运行测试验证forge test --match-contract <挑战名> -vvvv

四、热身挑战:Unstoppable

Unstoppable 是 Damn Vulnerable DeFi 的第一个挑战,也是最佳的入门起点。它的难度适中,能让你快速熟悉项目的解题流程。

1. 挑战背景

有一个代币化金库(Vault),存入了 100 万个 DVT 代币,在宽限期结束前提供免费的闪电贷。开发团队在测试网上运行了一个公开测试版,并部署了一个监控合约来检查闪电贷功能的可用性。

你的目标:从 10 个 DVT 代币的余额开始,让金库停止提供闪电贷服务。这是一个典型的拒绝服务(DoS)攻击挑战。

2. 漏洞分析

核心漏洞藏在 UnstoppableVault.flashLoan() 函数中:

function flashLoan(...) external returns (bool) {
    // ... 其他检查 ...
    uint256 balanceBefore = totalAssets();
    if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance();
    // ... 执行闪电贷 ...
}

这个检查要求 totalAssets()(金库实际持有的资产数量)必须等于 convertToShares(totalSupply)(根据总份额计算出的资产数量)。

关键问题totalAssets()totalSupply 之间的平衡可以被外部操作破坏。

poolBalance 变量在用户调用 depositTokens() 时更新。但是,如果有人直接向金库合约转账 DVT 代币(不经过 depositTokens 函数),会发生什么?

  • 金库的 balanceOf(address(this)) 增加了
  • poolBalance 没有变化
  • 两个追踪器不同步了

flashLoan 函数执行检查时,convertToShares(totalSupply) != balanceBefore,交易会 revert,闪电贷功能永久失效。

3. 攻击方法

攻击极其简单:

  1. 你有 10 个 DVT 代币的初始余额。
  2. 直接调用 DVT 代币的 transfer 函数,向金库合约转账 任意数量(比如 1 个 DVT)。
  3. 金库的 poolBalance 与实际余额不再一致。
  4. 此后所有 flashLoan 调用都会在 InvalidBalance 检查处失败。
  5. 金库停止提供闪电贷

不需要复杂的合约,只需要一笔简单的代币转账。

4. 解题代码(V4 版本)

test/unstoppable/Unstoppable.t.soltest_unstoppable() 函数中添加:

function test_unstoppable() public checkSolvedByPlayer {
    // 直接向金库转账 1 个 DVT,破坏 poolBalance 与实际余额的同步
    token.transfer(address(vault), 1);
}

注意:V4 版本中,挑战要求将 rescued 资产存入指定的 recovery 账户。解题后需调用 vault.withdraw() 将资产转回 recovery 地址。


五、总结

Unstoppable 挑战揭示了 DeFi 协议中一个常见的设计缺陷:依赖多种方式追踪同一状态,但未确保它们在所有情况下保持同步

  • 漏洞本质:金库使用 poolBalancetoken.balanceOf(address(this)) 两种方式追踪余额,但直接转账可以绕过 poolBalance 的更新。
  • 攻击手法:一笔简单的 ERC20 transfer 即可触发 DoS。
  • 防御启示:合约应统一状态更新入口,避免外部操作绕过内部记账逻辑。

完成这个挑战后,你将熟悉 Damn Vulnerable DeFi 的完整解题流程。接下来可以挑战更复杂的题目,如 Puppet(预言机操纵)、Selfie(治理攻击)等。

延伸资源