欢迎订阅专栏: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.sol | Foundry 测试,在此实现攻击 |
标准解题流程:
- 阅读挑战说明:理解场景、目标和约束。
- 分析漏洞合约:找出安全缺陷。
- 编写攻击代码:在测试文件的
test_challenge()函数中实现利用逻辑。 - 运行测试验证:
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. 攻击方法
攻击极其简单:
- 你有 10 个 DVT 代币的初始余额。
- 直接调用 DVT 代币的
transfer函数,向金库合约转账 任意数量(比如 1 个 DVT)。 - 金库的
poolBalance与实际余额不再一致。 - 此后所有
flashLoan调用都会在InvalidBalance检查处失败。 - 金库停止提供闪电贷。
不需要复杂的合约,只需要一笔简单的代币转账。
4. 解题代码(V4 版本)
在 test/unstoppable/Unstoppable.t.sol 的 test_unstoppable() 函数中添加:
function test_unstoppable() public checkSolvedByPlayer {
// 直接向金库转账 1 个 DVT,破坏 poolBalance 与实际余额的同步
token.transfer(address(vault), 1);
}
注意:V4 版本中,挑战要求将 rescued 资产存入指定的 recovery 账户。解题后需调用
vault.withdraw()将资产转回 recovery 地址。
五、总结
Unstoppable 挑战揭示了 DeFi 协议中一个常见的设计缺陷:依赖多种方式追踪同一状态,但未确保它们在所有情况下保持同步。
- 漏洞本质:金库使用
poolBalance和token.balanceOf(address(this))两种方式追踪余额,但直接转账可以绕过poolBalance的更新。 - 攻击手法:一笔简单的 ERC20
transfer即可触发 DoS。 - 防御启示:合约应统一状态更新入口,避免外部操作绕过内部记账逻辑。
完成这个挑战后,你将熟悉 Damn Vulnerable DeFi 的完整解题流程。接下来可以挑战更复杂的题目,如 Puppet(预言机操纵)、Selfie(治理攻击)等。
延伸资源:
- 官方仓库
- Foundry 文档
- 社区题解:LearnBlockchain、CSDN 等技术社区有大量思路分享