闪贷攻击:九位数DeFi漏洞深度剖析
这是一篇关于闪贷(Flash Loan)攻击的深度技术文章,详细剖析了闪贷攻击的四个阶段:借用、操纵、提取和偿还。
本文翻译:每一个闪电贷攻击背后的四阶段模式,所有主要闪电贷提供商的机制对比,闪电贷放大的七种漏洞类型,从2000万到2.23亿美元规模的八个案例研究,以及真正有效的防御措施。
闪电贷让一个余额为零的攻击者在一次EVM交易期间化身为十亿美元级别的鲸鱼,然后在下一个区块确认之前完成结算。
Cetus在2025年5月因一个u256溢出损失了2.23亿美元,而闪电贷借入的流动性使之有利可图。Penpie在2024年9月因重入损失了2700万美元,而闪电贷借入的奖励代币起到了决定性作用。KyberSwap Elastic在2023年11月因一个1 wei的tick精度不对称损失了4800万美元,而闪电贷借入的交易量使其可行。
闪电贷在这些事件中都不是漏洞本身。它们是放大器,使现有的缺陷——现货价格预言机、容易受捐赠攻击的金库、顺序错误的状态更新、精度损失导致的tick边界——能在全协议规模上产出收益,而非仅局限于攻击者的资本规模。
本指南将剖析每个闪电贷攻击所遵循的四阶段结构、审计师应当一眼识别的提供商机制、在flashLoan()调用下游反复出现的漏洞类型,以及在2亿美元以上规模印证这一模式的案例研究。最后,我们将讨论那些真正有效的防御措施——以及生产代码中不断出现的反模式。
闪电贷究竟是什么

闪电贷是一种无抵押贷款,其还款不是由抵押品或信用保证,而是由EVM交易本身的原子性来保证。
贷方将代币转账到借款人的合约,然后通过任意的calldata回调该合约,并在回调结束时验证其余额是否已恢复,外加应付的任何费用。如果后置条件失败,则整个交易——包括借款、其下游的每个操作以及调用图中任何位置的状态变更——都会回滚。没有违约,只有成功或回滚。
这一特性将许多DeFi攻击的资本要求从"拥有5亿美元"压缩到"支付5万gas用于调用"。攻击者从未真正拥有这些资本,而是将其路由经过一系列合约,足够长地提取一个差额,而贷方在区块关闭前得到偿还。
闪电贷是放大器,而非漏洞的源头。它们所利用的漏洞包括价格预言机、份额会计运算、治理投票计数、份额膨胀、重入和舍入。闪电贷能将一个5000万美元头寸上0.5%的协议错误,转化为每周期25万美元的利润,并在单笔交易中执行30次。关于这些攻击所利用的基础AMM机制,请参阅我们的AMM安全基础第一部分和第二部分。
一眼识别每个提供商
审计师应当对每个主要闪电贷提供商的接口、费用表和回调形状烂熟于心。调用合约的结构是识别闪电贷增强代码时最可靠的标志。
Aave V3 —— 主力军
Aave提供了flashLoanSimple(单资产)和flashLoan(多资产,支持以开立债务代替偿还):
1// Pool.sol (Aave V3)
2function flashLoanSimple(
3 address receiverAddress,
4 address asset,
5 uint256 amount,
6 bytes calldata params,
7 uint16 referralCode
8) public virtual override;
9
10function flashLoan(
11 address receiverAddress,
12 address[] calldata assets,
13 uint256[] calldata amounts,
14 uint256[] calldata interestRateModes,
15 address onBehalfOf,
16 bytes calldata params,
17 uint16 referralCode
18) public virtual override;
19
20// IFlashLoanSimpleReceiver
21function executeOperation(
22 address asset,
23 uint256 amount,
24 uint256 premium,
25 address initiator,
26 bytes calldata params
27) external returns (bool);
当前总费用为0.05%(FLASHLOAN_PREMIUM_TOTAL),其中4个基点归协议金库,其余归流动性提供者。Aave V2收取0.09%。接收者必须在executeOperation返回前批准Pool amount + premium;然后Pool通过transferFrom拉取资金。关于周边池内部机制,请参阅我们的Aave V3 Pool.sol 详解。
dYdX Solo Margin —— 原始的"免费"闪电贷
dYdX从未构建闪电贷产品。社区发现SoloMargin.operate()接受一系列Withdraw → Call → Deposit操作,并且只在最后检查余额。借入N,在callFunction中做任何事,存入N + 2 wei,完成。这2 wei就是全部费用。
SoloMargin主网上地址为0x1E0447b19BB6EcFdAe1e4AE1694b0C3659614e4e,多年来它对于合法的套利者和攻击者而言,都是最便宜的WETH/DAI/USDC流动性来源。接收者必须实现ICallee.callFunction(address sender, Account.Info memory accountInfo, bytes memory data)。
Uniswap V3 —— flash() 与交换分离
与V2不同(闪电逻辑嵌入在swap中),V3在每个池上暴露了专用的flash():
1function flash(
2 address recipient,
3 uint256 amount0,
4 uint256 amount1,
5 bytes calldata data
6) external override lock noDelegateCall {
7 uint128 _liquidity = liquidity;
8 require(_liquidity > 0, 'L');
9 uint256 fee0 = FullMath.mulDivRoundingUp(amount0, fee, 1e6);
10 uint256 fee1 = FullMath.mulDivRoundingUp(amount1, fee, 1e6);
11 uint256 balance0Before = balance0();
12 uint256 balance1Before = balance1();
13 if (amount0 > 0) TransferHelper.safeTransfer(token0, recipient, amount0);
14 if (amount1 > 0) TransferHelper.safeTransfer(token1, recipient, amount1);
15 IUniswapV3FlashCallback(msg.sender).uniswapV3FlashCallback(fee0, fee1, data);
16 require(balance0Before.add(fee0) <= balance0After, 'F0');
17 require(balance1Before.add(fee1) <= balance1After, 'F1');
18}
费用等于池的交换费用层级(0.01%、0.05%、0.30%或1%)。借款人实现uniswapV3FlashCallback(uint256 fee0, uint256 fee1, bytes calldata data),并且必须验证msg.sender是规范V3池——即调用CallbackValidation.verifyCallback(factory, decoded.poolKey)。忘记这个检查会将回调变成公开的免费资金函数。关于V3机制的深入背景,请参阅我们的Uniswap V3 深度探究和Q64.96定点数入门。
Balancer V2 —— 零费用,全金库流动性
Balancer Vault位于0xBA12222222228d8Ba445958a75a0704d566BF2C8,在一个单一合约中持有所有池的所有代币余额,并将其作为零费用的统一闪电贷来源暴露出来。
1// IVault
2function flashLoan(
3 IFlashLoanRecipient recipient,
4 IERC20[] memory tokens,
5 uint256[] memory amounts,
6 bytes memory userData
7) external;
8
9// IFlashLoanRecipient
10function receiveFlashLoan(
11 IERC20[] memory tokens,
12 uint256[] memory amounts,
13 uint256[] memory feeAmounts, // 当前始终为0
14 bytes memory userData
15) external;
这使得Balancer成为现代攻击者的首选提供商,适用于任何不需要特定DAI的攻击载荷。Penpie攻击者在一次调用中从Balancer借入了wstETH、sUSDe、egETH和rswETH。关于Balancer底层池架构,请参阅我们的Balancer协议架构深度探究。
MakerDAO DssFlash —— 原生DAI铸造
Maker Flash Mint Module并非持有DAI作为库存,而是直接将DAI铸造到借款人的地址,最高达到治理配置的line(债务上限),然后在交易结束时通过vat.heal销毁。费用(toll)于2021年9月降至0%。该模块符合ERC-3156:
1function onFlashLoan(
2 address initiator,
3 address token,
4 uint256 amount,
5 uint256 fee,
6 bytes calldata data
7) external override returns (bytes32);
攻击者通过这种方式凭空变出数亿DAI。Cream Finance攻击者在一次调用中从Maker铸造了约5亿DAI。
费用结构并排比较

- dYdX Solo Margin —— 2 wei(实际上免费)
- Balancer V2 —— 0%
- MakerDAO DssFlash —— 0%(仅限DAI)
- Uniswap V3 —— 池费用层级(0.01% / 0.05% / 0.30% / 1%)
- Aave V3 —— 0.05%
- Aave V2 —— 0.09%
大多数现代攻击会叠加多个提供商:从Balancer借走所有可用资产,用Maker DAI补齐,再退回到Aave获取长尾资产。UwU Lend攻击者在单笔交易中从Aave V3、Aave V2、Uniswap V3、Balancer、Maker、Spark和Morpho提取了37.96亿美元的流动性。
四阶段结构

每一次闪电贷攻击——在下面的案例研究中毫无例外——都符合相同的四阶段模板。
第一阶段——借入。 攻击者合约调用闪电贷提供商的入口点。控制权通过回调返回到攻击者手中,此时持有借入的资产。没有提供抵押品;唯一的承诺是在函数返回前偿还的隐含保证。
第二阶段——操纵。 在回调内部,攻击者利用借入的资本将协议推入一种正常情况下不会出现的状态。这就是漏洞所在之处。资本扭曲了AMM的储备比率(预言机操纵),膨胀了金库的份额价格(捐赠攻击),积累了治理代币以通过恶意提案(治理攻击),达到了精度损失的tick边界(KyberSwap),触发了算术溢出(Cetus),或者只是在奖励会计更新期间重新进入了一个函数(Penpie)。
第三阶段——提取。 在协议的世界观被破坏后,攻击者执行将破坏货币化的操作:借入超过本应被允许的数量,清算某人(通常是他们自己,并享受折扣),赎回膨胀的份额,以错误的价格执行交换,或索取膨胀的奖励。
第四阶段——偿还。 攻击者撤销任何临时状态(通常通过他们操纵过的AMM交换回来),偿还本金加费用,并带走差额。交易原子性地结算;协议的账本反映了真实、不可逆转的损失。
一个典型的攻击者合约骨架如下:
1contract Exploit is IFlashLoanReceiver {
2 IPool constant POOL = IPool(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2);
3 IVictimProtocol constant VICTIM = IVictimProtocol(0x...);
4
5 function pwn(uint256 borrowAmount) external {
6 address[] memory assets = new address[](1);
7 assets[0] = USDC;
8 uint256[] memory amounts = new uint256[](1);
9 amounts[0] = borrowAmount;
10 uint256[] memory modes = new uint256[](1); // 0 = repay
11 // 第一阶段:借入
12 POOL.flashLoan(address(this), assets, amounts, modes,
13 address(this), "", 0);
14 // 当executeOperation返回时,第四阶段在此完成
15 IERC20(USDC).transfer(msg.sender, IERC20(USDC).balanceOf(address(this)));
16 }
17
18 function executeOperation(
19 address asset,
20 uint256 amount,
21 uint256 premium,
22 address /*initiator*/,
23 bytes calldata /*params*/
24 ) external override returns (bool) {
25 require(msg.sender == address(POOL), "auth");
26
27 // 第二阶段:操纵
28 IERC20(asset).approve(address(VICTIM_DEX), amount);
29 VICTIM_DEX.swap(asset, otherToken, amount); // 扭曲预言机
30
31 // 第三阶段:提取
32 VICTIM.depositOrBorrowAtNowBrokenPrice(...);
33 VICTIM.withdrawOrLiquidateForProfit(...);
34
35 // 撤销操纵以便偿还
36 VICTIM_DEX.swap(otherToken, asset, ...);
37
38 // 第四阶段:偿还(Aave将扣除amount + premium)
39 IERC20(asset).approve(address(POOL), amount + premium);
40 return true;
41 }
42}
将第二阶段和第三阶段替换为特定漏洞,就构成了本文中的每一次攻击。
追捕这些漏洞类别

闪电贷不会破坏协议,而是在你没有测试的条件下利用现有的缺陷。以下是审计师在每一个涉及资产估值、份额会计、投票或奖励的代码路径上都需要审查的清单。
单源现货预言机。 一个通过读取某个AMM池的getReserves()或某个金库的pricePerShare()来为代币定价的协议,就是在要求失去所有资金。将一笔九位数的交换通过闪电贷注入低流动性池,会在交易期间使现货价格移动50%以上。从2020年的bZx到2024年的UwU Lend,每一次攻击都是这个主题的变体。Curve已明确警告get_p()返回的是瞬时现货价格,绝不能用作预言机——UwU Lend以惨痛代价学到了这一点。我们的预言机操纵深度探究和Curve Finance核心机制逐一讲解了其中的数学原理。
对空市场的捐赠攻击。 当一个金库或借贷市场以零或接近零的供应量初始化时,直接代币转账(ERC-20 transfer,而非deposit)会在不铸造份额的情况下膨胀底层余额,将份额价格推至荒谬值。后续存款者的份额会被舍入为零。Compound V2分叉(Hundred、Onyx、Sonne、Starlay)和虚拟偏移量不足的ERC-4626金库是典型受害者。请参阅我们的Yearn金库安全文章了解ERC-4626加固模式。
治理攻击。 如果投票权由_当前_区块的快照决定,那么闪电贷可以产生临时的超级多数。Beanstalk的emergencyCommit接受在提案执行的同一个交易中存入的代币投票。在10亿稳定币之后,1.82亿美元消失了。我们在DAO治理攻击中剖析了这一更广泛的模式。
重入与闪电借入资本结合。 经典的重入已经够糟了;再加上闪电贷,攻击者可以通过外部合约给予他们的任何回调——ERC-721 onERC721Received、ERC-777 Hook、自定义代币回调——重新进入。Penpie在奖励会计期间通过恶意SY合约的重入是2024年的典型。只读重入——视图函数返回更新中的状态——是一个更微妙的变种。
Tick边界精度利用。 集中流动性AMM以高精度进行tick数学运算。KyberSwap Elastic的computeSwapStep对同一个边界检查使用了两个略微不同的算术路径,允许一次交换绕过_updateLiquidityAndCrossTick并重复计算流动性。Cetus的checked_shlw使用了错误的溢出掩码,允许攻击者为单个代币单位铸造大量流动性。
清算逻辑缺陷。 授予清算人奖励大于坏账的函数——Euler Finance的donateToReserves没有健康检查,动态关闭因子根据头寸水下程度缩放折扣,因此攻击者可以通过捐赠使自己进入一个享有75%折扣的清算状态。
奖励分配数学使用余额前/余额后差值计算奖励,但不验证余额变化来自预期来源。 Penpie的batchHarvestMarketRewards容易受到恶意代币通过重入"存入"自身作为奖励的影响。
跨协议可组合性。 当协议A读取协议B的状态进行定价时,针对B的闪电贷可以扭曲A。Cream Finance从一个内部依赖Curve池的Yearn金库读取pricePerShare()——三层乐高,三层失败模式。
案例研究:Cetus 协议 —— 2025年5月22日 —— 2.23亿美元
Cetus是Sui上最大的集中流动性DEX。此次攻击是由闪电兑换资助的流动性提供所暴露的共享数学库中的u256左移溢出漏洞。
有问题的原语是checked_shlw,它本应在将u256左移64位会导致溢出时中止。实现将输入与0xFFFFFFFFFFFFFFFF << 192进行比较,而不是0x1 << 192——阈值宽松了2⁶⁴倍。本应中止的值通过了检查,实际的移位截断了高位,产生了一个极小的中间值。
该中间值输入了get_delta_a——计算一定流动性需要多少代币A的函数。在精心选择的流动性输入下,该函数返回1。攻击者添加了"1单位代币A"的存款,并被记入了10,365,647,984,364,446,732,462,244,378,333,008单位的流动性。
流程:
- 从Cetus池本身闪电兑换10,024,321 haSUI。
- 交换以将池的价格推到极端tick范围(约~[300000,300200])。
- 以
liquidity = L*(精心选择)和1单位代币A开仓;溢出将所需存款截断为1。 - 移除流动性;收到巨额储备金。
- 偿还闪电兑换;获利。
同一有问题的原语也存在于Kriya、Momentum和Bluefin中。大约6000万美元通过Wormhole桥接到以太坊并兑换为ETH;Sui验证者通过紧急链上行动冻结了剩余的约1.62亿美元。Cetus在一周后重新启动,获得了Sui基金会3000万美元USDC贷款、金库储备和追回的资金。受影响的LP们收回了其原始流动性的85%-99%。
该漏洞存在于早期的Aptos代码中,于2023年被OtterSec标记,并且据称已被修复——但一次回归在Sui上重新引入了它,而Zellic在2025年4月的审计中没有标记,因为数学库似乎不在审计范围内。这是经典的"共享数学库"失败模式:一个辅助函数,四个协议,2.23亿美元。我们的2025年利用教训涵盖了Cetus以及当年其他基础设施级别的事件。
案例研究:Penpie (Pendle) —— 2024年9月3日 —— 2700万美元
Penpie是Pendle Finance之上的Convex风格收益提升层。此次攻击串联了三个问题:无需许可的市场注册、PendleStakingBaseUpg::batchHarvestMarketRewards缺少重入保护,以及Balancer闪电贷。
Pendle的PendleMarketFactoryV3.createNewMarket是无许可的,而PendleMarketRegisterHelper.registerPenpiePool由一个onlyVerifiedMarket修饰符门控,其验证条件是"该市场存在于Pendle工厂中"。这意味着攻击者可以先创建一个由攻击者控制的SY(标准化收益)代币支持的Pendle市场,然后在Penpie上注册一个攻击者控制的市场。
流程:
- 攻击者创建一个虚假市场并在Penpie上注册。配置两个高价值奖励代币:
0x6010_PENDLE-LPT和0x038c_PENDLE-LPT。 - 攻击者将少量虚假市场LP存入Penpie。
- 攻击者从Balancer闪电借入wstETH、sUSDe、egETH和rswETH。
- 在虚假市场上调用
PendleStaking.batchHarvestMarketRewards。 batchHarvest在redeemRewards()之前和之后测量balanceOf(rewardToken)。在redeemRewards期间,恶意SY合约重入Penpie的depositMarket,存入闪电借入的代币(这些代币被配置为奖励代币)。余额差值现在被解释为属于攻击者的"奖励"。- 重入结束;Penpie将攻击者记为虚假市场中的唯一存款者,并赋予其膨胀的奖励。
- 攻击者调用
multiclaim,提取,并偿还Balancer闪电贷。
损失:以太坊和Arbitrum上共11,113.6 ETH(约2700万美元),在UTC时间18:25至18:42之间的三笔交易中被抽走。主要攻击交易:0x7e7f9548f301d3dd863eac94e6190cb742ab6aa9d7730549ff743bf84cbd21d1。Pendle在UTC时间18:45暂停了合约,保护了另外约1.05亿美元。
值得注意的细节:Zokyo在2023年对Penpie的原始审计没有标记重入,因为当时池注册是受许可的。无需许可的注册是在2024年5月添加的,但PendleStakingBaseUpg不在AstraSec对该变更的审计范围内。我们的审计后安全指南解释了为什么每次配置变更都需要重新界定范围。
案例研究:KyberSwap Elastic —— 2023年11月22日 —— 4800万美元
KyberSwap Elastic是一个Uniswap V3风格的集中流动性AMM,带有一条额外的"再投资曲线",可将LP费用自动复投为额外的区间内流动性。再投资曲线就是漏洞的来源。
在computeSwapStep内部,函数calcReachAmount计算了将池推向下一个tick边界所需的输入量。交换是否实际越过tick的决策是通过比较产生的nextSqrtP与targetSqrtP来做出的。由于再投资流动性在两个计算之间的处理方式不同,calcReachAmount可能返回一个值,该值对于最大输入产生的nextSqrtP略大于targetSqrtP——一个1 wei的精度不对称。攻击者使用交换数量为amountToCrossTick - 1。第一个检查(输入量≤最大值)说"不越过";但实际的更新价格产生的sqrtP已超出边界;因此_updateLiquidityAndCrossTick_没有_被调用,而越过tick的流动性仍留在账面上。
USDC/ETHx上的攻击流程(主网,交易0x396a83df7361519416a6dc960d394e689dd0f158095cbc6a6c387640716f5475):
- 从Uniswap V3池闪电借入500 ETHx。
- 将246.754 ETHx交换为32,389.63 USDC,将池的tick推到305000——进入一个没有现有流动性的"真空区"。
- 以16 USDC和5.87e-3 ETHX(3,321,338,298,606,975流动性)铸造一个紧密头寸[305000,305408]。
- 移除大部分流动性。精确剩余54,880,483,538,064的"幽灵流动性"。
- 交换244.08 ETHx,将tick推到305408(上边界)。这次交换本应调用
_updateLiquidityAndCrossTick以从活跃流动性中移除攻击者的范围,但由于精度错误而没有调用。 - 反向交换:将493.638 ETHx兑换为USDC。池现在应用了未移除的流动性_加上_实际的外部流动性,造成重复计算。攻击者获得超额的USDC payout。
- 偿还闪电贷;每个周期净赚32,359 USDC + 2,548 ETHx。
总计损失:以太坊(750万),Optimism(1550万),Arbitrum(1680万),Polygon(120万),Base(31.8万),Avalanche(2.3万)。攻击者(0x502...)要求完全控制KyberDAO、高管股权和公司所有权以换取资金——这是一封现在标志性的链上赎金信。
案例研究:UwU Lend —— 2024年6月10日 —— 2300万美元
UwU Lend是0xSifu部署的Aave v2分叉,带有一个用于sUSDe的自定义后备预言机。该预言机sUSDePriceProviderBUniCatch采用了11个sUSDe价格来源的中位数。其中5个来源调用了Curve池的get_p()——Curve的现货价格函数,Curve团队明确告诉项目不要将其用作预言机。
流程:
- 从Aave V3、Aave V2、Uniswap V3、Balancer、MakerDAO、Spark和Morpho闪电借入约37.96亿美元的稳定币和ETH。
- 将其中很大一部分兑换为USDe,并注入Curve USDe/sUSDe池,使
get_p()报告的sUSDe现货价格下跌约4%。 - 以被压低的价格(sUSDe = $0.99)从UwU Lend借入sUSDe,使用其他代币作为抵押品。
- 撤销Curve交换,将sUSDe推升至1.03美元。
- 现有的杠杆头寸(尤其是攻击者之前存入的头寸)在膨胀的价格下变得可清算;攻击者清算它们,以人为抬高的抵押品估值接收uWETH。
- 偿还闪电贷;在三笔交易中净赚约1930万至2300万美元。
攻击者EOA: 0x841dDf093f5188989fA1524e7B893de64B421f47。攻击者合约: 0x21C58d8F816578b1193AEf4683E8c64405A4312E。示例交易: 0xca1bbf3b3202c89232006f1ec6624b56242850f07e0f1dadbe4f69ba0d6ac3。
Curve创始人Michael Egorov个人受到影响,损失了2350万CRV(约985万美元)存入UwU。两天后,同一攻击者又通过残余的错误配置返回,再取走了370万美元。PeckShield审计明确将预言机排除在范围之外;UwU对Aave后备预言机的修改没有经过独立的重新审计。
案例研究:Sonne Finance —— 2024年5月14日 —— 2000万美元
Sonne是一个部署在Optimism上的Compound V2分叉。漏洞是众所周知的针对新部署市场的捐赠攻击,至少之前已被利用过四次——Hundred Finance(750万)、Onyx(210万)、Starlay(210万美元)。
cToken的汇率是(totalCash + totalBorrows - totalReserves) / totalSupply。当totalSupply极小时(例如2 wei),攻击者直接将大量底层资产转移到合约——绕过mint,因此没有发行份额——totalCash飙升而totalSupply不变。汇率变得天文数字,1 wei的soToken现在价值数百万底层资产。
Sonne知道这一点。他们的内部流程是(1)以0%抵押因子部署新市场,(2)铸造并销毁一些cToken以设置非零底价,然后(3)提高抵押因子。他们将流程分成多个多签交易,间隔2天的时间锁。在Optimism上,多签执行者是无许可的。攻击者观察时间锁,执行了步骤1和3——启动新的soVELO市场并将抵押因子设为35%——而从未执行步骤2。
对soVELO的攻击(交易0x9312ae377d7ebdf3c7c3a86f80514878deb5df51aad38b6191d55db53e42b7f0):
- 从VolatileV2 AMM闪电借入35,469,150 VELO。
- 铸造2 wei的soVELO(绝对最小值)。
- 通过
transfer直接向soVELO合约捐赠35,469,150 VELO。totalCash上升;totalSupply仍然是2 wei。新汇率:17,735,851,964,756,377,265,143,988,000,000,000,000,000。 - 将2 wei soVELO转移到新合约,以其为抵押借入265 WETH(2 wei"价值"3540万VELO)。
- 调用
redeemUnderlying(35,471,603,929,512,754,530,287,976)——针对膨胀的汇率进行精度损失的赎回。 - 偿还闪电贷;在soWETH、soUSDC和soVELO市场上净赚约2000万美元。
Sonne在第一次攻击后约25分钟暂停了所有Optimism市场。Seal贡献者通过向市场添加约100 VELO,打破了空市场条件,额外挽救了约650万美元。yAudit在其审计中明确标记了"针对Hundred Finance攻击向量的保护不明确"。教训:任何可以由无关方拆分或部分执行的新市场创建流程都会使程序性缓解措施失效。应将所有步骤打包到单个交易中,或使执行受许可。
案例研究:Euler Finance —— 2023年3月13日 —— 1.97亿美元
Euler的攻击没有操纵任何预言机。它按预期顺序使用了Euler自己的函数,但产生了意外后果。
漏洞:donateToReserves(uint subAccountId, uint amount)是在eIP-14中添加的,用于修复一个单独的首位存款者汇率错误。它允许用户向协议储备捐赠eTokens(Euler的抵押品收据)。该函数没有调用checkLiquidity——捐赠操作不强制要求捐赠账户保持健康的后置条件。
1/// @notice 将eTokens捐赠给储备
2/// @param subAccountId 0为主账户,1-255为子账户
3/// @param amount 以内部记账单位计(由balanceOf返回)
4function donateToReserves(uint subAccountId, uint amount) external {
5 // ... 销毁eTokens,增加储备 ...
6 // 漏洞:没有checkLiquidity()
7}
结合Euler的递归铸造——存入→eToken→以eToken为抵押借入→重新存入→eToken——这允许攻击者:
- 从Aave闪电借入3000万DAI。
- 向Euler存入2000万DAI,收到约1960万eDAI。
- 递归
mint以加杠杆:以2000万dDAI的债务获得1.956亿eDAI的抵押品。 - 偿还1000万dDAI,暂时保持健康。
- 再次递归到更大的金额。
- 调用
donateToReserves(0, 1亿 eDAI)。这销毁了攻击者1亿eDAI的抵押品,但没有销毁相应的dDAI。健康度在该入口点没有流动性检查的情况下大幅降至1以下。 - 从另一个攻击者合约("清算人"),对现在资不抵债的第一个合约调用
liquidate()。Euler的_动态关闭因子_给予清算人一个折扣,该折扣随头寸水下程度缩放——最多可对剩余抵押品的75%给予20%折扣。清算人收到3.1亿eDAI和2.59亿dDAI。 - 从Euler提取3890万DAI。
- 偿还闪电贷;每个池净赚约887万美元。
在DAI、USDC、stETH和wBTC上重复,总计约1.97亿美元。攻击者EOA 0xb66cd966670d962c227b3eaba30a872dbfb995db;攻击合约 0x036cec1a199234fc02f72d29e596a09440825f1c。
六家审计机构审查过Euler的代码。Sherlock为审计投保并支付了330万美元索赔。攻击者最终归还了约2.4亿美元(由于谈判期间ETH价格上涨,多于被盗金额)。Cyfrin和BlockSec的事后分析都强调,如果存在断言"任何协议操作都不应使账户的风险调整后负债大于风险调整后资产"的不变量测试,这次攻击就会被捕获。我们的模糊测试与形式化验证文章涵盖了相关工具。
案例研究:Beanstalk —— 2022年4月17日 —— 1.82亿美元
Beanstalk的emergencyCommit允许任何已经上链24小时并持有2/3超级多数Stalk(治理)代币的提案在单笔交易中执行。投票权由当前在Beanstalk Diamond合约中的存款决定——包括在同一交易中存入的存款。
攻击者在4月16日提交了两个恶意BIP(提案),等待了24小时。在UTC时间4月17日12:24:
- 从Aave / Uniswap / SushiSwap闪电借入总计超过10亿美元(DAI、USDC、USDT,以及来自DEX的BEAN和LUSD)。
- 转换为Curve LP代币,这些代币在存入时计为Beanstalk治理权重。
- 存入Beanstalk;立即持有79%的投票Stalk。
- 在恶意BIP上调用
emergencyCommit,该BIP将协议储备转移到攻击者(以及25万USDC到乌克兰捐赠地址——装点门面的掩护)。 - 提取,交换回来,偿还闪电贷。
净额:在1.82亿美元协议损失中,攻击者口袋里有7600万美元。Omniscia的事后分析清楚指出了根本原因:治理功能不在任何发布前审计范围内,并且DAO设计从根本上信任了_当前_的Stalk分布,而不是在提案创建时拍摄的快照。
案例研究:Cream Finance —— 2021年10月27日 —— 1.3亿美元
Cream将Yearn金库代币(yUSDVault)列为抵押品。PriceOracleProxy通过直接读取vault.pricePerShare()来为yUSDVault定价。通过操纵底层的Curve y-pool并直接向金库捐赠Yearn 4-Curve代币,攻击者可以在没有"真实"存款的情况下将金库报告的单位价格翻倍。
两个协调的合约A和B(由0x24354d31bc9d90f6254709c32049cf866b发起):
- MakerDAO闪电铸造5亿美元DAI(DssFlash)。
- 将DAI存入Curve y-pool,收到yDAI;转换为yUSD。
- 从Aave闪电借入20亿美元ETH;将ETH作为抵押品存入Cream的合约B。
- 从Cream以ETH为抵押借入yUSD;将yUSD转移到合约A并存入。重复循环,在合约A中积累约15亿crYUSD,在合约B中积累约30亿dDebt。
- 从金库提取5亿美元yUSDVault,抽干金库的底层资产。
- 直接向金库捐赠1000万美元的Yearn 4-Curve,使
pricePerShare翻倍。 - Cream的预言机现在将合约A的15亿crYUSD估值30亿美元。在Cream看来,合约A大量超额抵押;合约A借回20亿美元ETH(偿还Aave),抽干了Cream其他市场上剩余的约1.3亿美元。
- 偿还所有闪电贷。
净额:约1.3亿美元,当时Cream Finance的所有可用流动性。这是Cream的第三次重大攻击;该协议从未恢复。
案例研究:Harvest Finance —— 2020年10月26日 —— 2400万美元
教科书级别的经济攻击。Harvest的fUSDC和fUSDT金库使用underlyingBalanceWithInvestment()来定价其份额,该函数从Curve y-pool读取实时投资余额。Curve y-pool的汇率随储备比率变动。
循环(交易0x35f8d2f572fceaac9288e5d462117850ef2694786992a8c3f6d02612277b0877,执行30多次):
- 从Uniswap闪电借入约5000万USDC + 1800万USDT。
- 在Curve y-pool中将1720万USDT交换为USDC。USDC的池权重上升;USDT下降。y-pool现在高估了USDC。
- 向fUSDC金库存入约5000万USDC。由于金库使用现在被扭曲的y-pool储备来估值USDC,份额价格降至约0.97;攻击者收到了比公平值更多的fUSDC。
- 在Curve上反向交换:1720万USDC → USDT,恢复池比率。fUSDC份额价格回升至约0.98。
- 提取fUSDC,获得约5060万USDC。净额:每迭代约61.9万美元利润。
- 偿还闪电贷。
针对fUSDC执行30次迭代,用时4分钟;针对fUSDT执行13次迭代,另外用时3分钟。总利润约2400万美元;总TVL影响约3300万美元,因为剩余金库存款者持有了现在贬值的份额。Harvest的"套利检查"有一个3%的阈值——每次迭代的价格影响约为1%,刚好低于限制。发布前由多家公司审计过。
案例研究:bZx —— 2020年2月14日和18日 —— 37万 + 64.5万美元
原始攻击。
攻击1(2月14日):
- 从dYdX闪电借入10,000 ETH。
- 在Compound上使用5,500 ETH借入112 wBTC。
- 通过bZx的
mintWithEther,用1,300 ETH开立一个5倍空头ETH/wBTC头寸。bZx的逻辑通过Kyber发送交换,而Kyber又路由到Uniswap。在小规模Uniswap wBTC池上的大额市价单将价格推高了3倍。漏洞:bZx的滑点检查未能触发。 - 在Uniswap上以膨胀的价格卖出112 wBTC——净赚约6,871 ETH。
- 偿还dYdX 10,000 ETH;带着约1,193 ETH(约37万美元)离开。
攻击2(2月18日): 纯预言机攻击。闪电借入ETH,通过Kyber的Synthetix储备将大量金额兑换为sUSD,推高sUSD的现货价格,然后以sUSD为抵押品,以被抬高的价格从bZx借入ETH。带走约2,378 ETH(约64.5万美元)。
这些是概念验证,证明闪电贷可以将任何经济计算错误武器化。五年后,rekt排行榜上一半的协议仍然因同类漏洞的闪电贷放大而损失资金。
真正有效的防御措施

当以下任何一条为真时,上述攻击就会失败。每一个涉及资产估值、份额会计、投票、奖励或新市场初始化的代码路径都是闪电贷攻击面,协议需要多个这些防御措施,而不是仅仅一个。
使用TWAP或去中心化预言机,而非现货价格
Uniswap V3暴露了observe()用于累计tick观察值——采样30分钟以上以获得抗操纵的TWAP。更好的是,基于推送的去中心化预言机(Chainlink、Pyth)从许多交易所链下获取价格,并由许多节点运营商聚合;在一个DEX上的闪电贷交换不会影响它们。实践中,攻击面出现在审计师找到自定义预言机、"后备"预言机(UwU Lend)或从自身读取AMM的金库的vault.pricePerShare()派生的价格时(Cream)。
聚合来源,但确保它们是独立的
UwU Lend使用了11个价格的中位数,但其中5个来自Curve get_p()——一个攻击向量跨越了11个输入中的5个。审计预言机来源时要寻找_相关_的操纵路径。
对状态敏感的操作进行区块延迟
在本区块铸造份额的用户应等到下个区块才能赎回。在本区块存入存款的选民应等到N个区块后的快照才拥有投票权。这一单一防御可以打破所有闪电贷治理攻击。Compound的Governor Bravo使用getPriorVotes(account, blockNumber)针对提案创建前拍摄的快照。OpenZeppelin的Governor框架默认使用此模式。Beanstalk的错误在于读取了当前投票权。
在初始化时原子性地铸造和销毁死份额
对于Compound V2分叉:在添加市场时,在同一交易中,铸造非微不足道的cToken数量(例如1e8 wei)并发送到address(0)。这使得totalSupply无法被打破地非零,并使捐赠攻击不经济。
对于ERC-4626,使用OpenZeppelin的_decimalsOffset()来添加虚拟份额和资产:
1function _convertToShares(uint256 assets, Math.Rounding rounding)
2 internal view virtual override returns (uint256)
3{
4 return assets.mulDiv(
5 totalSupply() + 10 ** _decimalsOffset(),
6 totalAssets() + 1,
7 rounding
8 );
9}
一个_decimalsOffset为6意味着攻击者需要损失大约10⁶倍于受害者的存款,才能提取受害者的存款——经济上不合理。
到处都有重入保护,包括只读函数
OpenZeppelin的ReentrancyGuard阻挡经典重入,但一个从状态正在重入调用期间更新的合约中读取的函数(只读重入)需要不同的防御:要么暴露一个notReentrant视图修饰符,要么使用一个任何读取也调用的lock()模式。Penpie的redeemRewards有nonReentrant,但batchHarvestMarketRewards没有——而恶意SY合约通过batchHarvestMarketRewards重入,而不是redeemRewards。审计每一个要么调用要么被具有nonReentrant的函数调用的公共函数。如果它本身没有防护,问为什么。
检查-效果-交互,机械地执行
任何更新余额、份额或奖励然后进行外部调用的函数必须_首先_更新状态。Penpie的batchHarvestMarketRewards通过_在_外部调用期间将奖励计算为balanceAfter - balanceBefore违反了这一原则。
每个涉及AMM的操作都有滑点限制
交换上的amountOutMinimum,存款上的minSharesOut,赎回上的minAssetsOut。Harvest Finance的金库接受了其有问题的定价产生的任何份额数量;对deposit()的严格滑点门控会拒绝攻击者的交易。
每区块的借入限制和全局速率限制
Aave V3使用了borrowCap和supplyCap。对于敏感市场,将其设置得足够低,使得闪电贷无法完全抽干头寸。这不是完美的防御——上限会带来损害而不是阻止错误——但这是一个可恢复的失败模式。
在协议级别进行不变量测试
这是可以捕获Euler的防御。Foundry的invariant_*测试、Echidna、Halmos和Certora可以表达"没有公开调用序列会使任何用户账户的负债 > 抵押品"或"totalAssets()永远不会减少,除非通过withdraw()"。Cetus的漏洞在2023年对Aptos变体的审计中被捕获——在Aptos上修复了,但在Sui上回归——并且会被像"对于所有流动性L和tick范围[a,b],get_delta_a(L,a,b)不会返回1,除非L很小"这样的属性测试捕获。我们的模糊测试入门逐步讲解了如何编写这些测试。
审计代码路径,而不仅仅是审计范围差异
上面的大多数事后分析都指出了审计师查看的具体函数——而漏洞存在于他们没有查看的函数中,或者其调用图跨越了审计边界的函数中。UwU Lend的PeckShield审计排除了预言机。Astrasec对Penpie的审计排除了PendleStakingBaseUpg,因为它没有变更。Cetus的Zellic审计排除了数学库。审计公司应该生成_并发布_他们的范围;协议团队应确保在范围内函数的调用路径上的任何代码本身都在范围内。请参阅我们的审计前检查清单了解防止这种情况的范围定义模式。
将多步骤管理流程打包到单个交易中
当中间状态危险时,将它们打包。Sonne的"部署市场→播种→设置抵押因子"是三个交易。攻击者只需要步骤1和3——没有步骤2,捐赠攻击就成功了。Multicall或打包的多签负载消除了这种竞态。或者,限制时间锁上的执行者角色,使得只有团队可以提交实际执行。
具有低于30分钟响应的暂停机制
Pendle通过22分钟内暂停,挽救了约1.05亿美元的额外Penpie损失;Cetus在数分钟内暂停,并从2.23亿美元中追回了约1.62亿美元。暂停不是针对完全攻击的防御,但它限制了重复循环的尾部风险。将暂停角色放在一个没有其他权限的专用多签上,并进行仪式彩排。
实时链上监控
Forta、Hypernative、Cyvers、BlockSec Phalcon和OpenZeppelin Defender提供实时异常检测,针对治理提案、大额闪电贷、预言机偏差和transferOwnership调用发出警报。UwU Lend在约1400万美元被抽走之前就被Cyvers首次检测到,之后达到2000多万美元的峰值。能在闪电贷攻击中生存下来的协议是那些在几分钟内响应的协议。
tx.origin != msg.sender 反模式
令人惊讶的是,许多合约试图通过检查require(tx.origin == msg.sender)来检测"这个调用是否来自闪电贷?"——即要求EOA调用者。这不是闪电贷的防御措施。
- EIP-3074和各种账户抽象模式模糊了
tx.origin。 - 攻击者可以从非闪电贷合约调用受保护函数;该检查阻止了_所有_合约(包括合法集成者),但并不能阻止一个坚定的攻击者,他们可以先闪电借入,将资金存入类似EOA的代理,然后从新交易中调用。
- 对于不需要在同一交易中的攻击——例如在当前区块进行快照投票的多区块治理——它没有帮助。
诚实的防御是使操作在假设调用者有无限资本的情况下安全。这才是威胁模型。其他任何做法都是一厢情愿。
这对审计师意味着什么
当你接到新的审计任务时,在阅读任何代码之前:
- 列出每个返回价格、份额数量或投票权重的外部视图函数。这些是你的闪电贷攻击面。
- 列出每个公开的状态变更函数,并针对每个函数,确定它读取什么状态来验证输入。从另一个合约——或从本合约,在所有更新提交之前——读取的任何状态都是潜在的破坏点。
- 列出每个回调到调用者的函数(ERC-721
onERC721Received、ERC-777 Hook、自定义回调、外部控制的代币转账)。每个都是潜在的重入入口。 - 列出每个新部署池/新上市资产的代码路径。在查看测试套件之前,先在大脑中运行捐赠攻击。
- 对于任何从范围内代码可达的范围外函数,要求有清晰的调用图理由。
模式重复。在bZx之后的五年里,rekt排行榜上一半的协议仍然因闪电贷放大同一六个漏洞类别而损失资金——现货预言机、捐赠攻击、治理快照、重入、精度损失、未检查的不变量。签名就在调用图中:借入→操纵→提取→偿还。如果你能找到一条通过协议完成该循环且产生正差额的路径,攻击者也会找到——而且他们有2.23亿美元的理由继续寻找。
联系我们
Zealynx Security审计智能合约和协议架构,重点关注经济漏洞类别——闪电贷放大、预言机操纵、治理攻击和不变量失败。如果你正在发布一个借贷市场、一个集中流动性DEX、一个ERC-4626金库,或任何链上资产定价的东西,一次范围审查只需一通电话。
请求范围审查 —— 或者在预订之前浏览我们的审计ROI分析和2026年审计定价指南。
常见问题:闪电贷攻击
- 什么是闪电贷,它与常规贷款有何不同?
闪电贷是一种无抵押贷款,在同一原子区块链交易中借入和偿还。贷方不检查信用或扣押抵押品——相反,如果还款失败,EVM会回滚整个交易,因此贷款要么完全成功,要么从未发生。相比之下,常规加密贷款要求借款人锁定抵押品(例如借款金额的150%)只要贷款保持开放。关键后果是:闪电贷允许_任何人_在单笔交易期间指挥九位数的资本,而无需拥有一个代币。请参阅我们的闪电贷词汇条目了解完整机制。
- 如果漏洞是实际问题,为什么闪电贷能实现九位数的攻击?
大多数DeFi协议中的经济漏洞——一个0.5%的定价错误、一个1 wei的tick精度不对称、一个可膨胀的份额价格——只有在攻击者资本的规模下才能产生回报。没有闪电贷,要攻击一个5000万美元的借贷市场(带有1%的现货预言机错误),攻击者需要部署足够大的真实资本来移动预言机,而这往往是他们不具备的。闪电贷压缩了资本要求。同一个漏洞现在在协议规模上产生回报。将bZx的37万美元损失转变为Cetus的2.23亿美元损失的机制并不是不同的漏洞类别——而是相同的漏洞类别加上更大的放大效应。
- 要求"无合约"检查(
tx.origin == msg.sender)能防止闪电贷攻击吗?
不能。tx.origin == msg.sender检查要求调用者是外部拥有账户(EOA)而不是合约。它阻挡了最朴素的闪电贷攻击形式,但这不是真正的防御:账户抽象(EIP-4337)和EIP-3074模式模糊了EOA的区别,而且许多闪电贷放大攻击(特别是像Beanstalk这样的治理攻击)跨越多个交易,该检查永远不会触发。它还会破坏合法的集成者(智能钱包、多签、自动化Keeper)。正确的威胁模型假设攻击者在一次交易内拥有无限资本;设计协议不变量以在该假设下成立。
- 什么是捐赠攻击,为什么它特别是一种闪电贷漏洞?
捐赠攻击(也称为份额膨胀或首位存款者攻击)是一种针对金库或借贷市场的攻击,其份额价格为totalAssets / totalSupply。当totalSupply很小时——例如市场创建后的2 wei——攻击者直接向合约转移大量底层资产(使用ERC-20 transfer,绕过deposit())。这提高了totalAssets而不铸造份额,将份额价格推至荒谬值;后续存款者被舍入为零份额,攻击者可以提取他们微小的头寸和受害者的存款。闪电贷将攻击从"需要3500万VELO来攻击soVELO"变为"从任何AMM借入3500万VELO,攻击,偿还"——只花费gas。Sonne Finance在2024年正是因此模式损失了2000万美元。
- 什么是TWAP预言机,为什么它能阻止大多数闪电贷价格操纵?
时间加权平均价格(TWAP)预言机报告资产在时间窗口内(例如30分钟)的平均价格,而不是当前区块的瞬时现货价格。由于闪电贷局限于单个交易,攻击者只能在该单个区块内推动现货价格——30分钟的平均值几乎不动。Uniswap V3为此暴露了observe(),而去中心化推送预言机(Chainlink、Pyth)通过跨多个交易所链下聚合价格实现了同样的保护。剩下的攻击面是较短的TWAP窗口(低于5分钟仍可在多个区块内以成本有效的方式操纵)、交易量小的资产,以及通过读取另一个合约的_现货_价格来为一种资产定价的协议——请参阅我们的预言机操纵指南了解完整分类。
- 我们有审计。我们能否免受闪电贷攻击?
不一定。六家公司审计了Euler,然后发生了1.97亿美元的攻击;漏洞在于一个函数(donateToReserves)完全按规范执行,但缺少了一个后置条件(checkLiquidity调用)。PeckShield审计了UwU Lend但排除了预言机;AstraSec审计了Penpie的无许可注册变更但排除了PendleStakingBaseUpg;Zellic在Sui漏洞发生前两年审计了Aptos上的Cetus。审计确认范围内代码在固定的时间范围内通过了一组特定的、专家应用的检查。真正有效的防御是_分层_的:TWAP预言机 + 每个公共函数上的重入保护 + 不变量测试 + 打包部署 + 监控 + 暂停机制。我们的审计后安全手册涵盖了报告下发后该做什么。
术语表
| 术语 | 定义 |
|---|---|
| 闪电贷 | 在单个原子交易中借入和偿还的无抵押贷款;由EVM回滚而非抵押品担保。 |
| 捐赠攻击 | 通过直接代币转账导致空金库份额价格膨胀,使后续存款者被舍入为零份额。 |
| 价格预言机操纵 | 扭曲协议的价格源(通常为AMM现货价格)以触发错误定价的借贷、清算或赎回。 |
| TWAP预言机 | 在多个区块上聚合的时间加权价格源,可消除单笔交易操纵。 |
| 重入攻击 | 状态更新提交前外部调用将控制权交还给攻击者,允许递归滥用。 |
| 只读重入 | 视图函数在重入调用期间返回更新中的状态,破坏下游消费者。 |
| 治理攻击 | 利用协议的投票机制(通常通过闪电贷投票代币)以通过恶意提案。 |
| 滑点 | 预期与执行交换价格之间的差异;本应拒绝被操纵的AMM交易的门控。 |
| 不变量测试 | 基于属性的模糊测试,断言协议范围内的真理(例如"没有账户以负债>抵押品结束")。 |
- 原文链接: zealynx.io/blogs/flash-l...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~