13 · 闪电贷:原子性 = 终极抵押品
第三层第四篇——DeFi 收官。我们项目 4 份合约每一份都用闪电贷做引擎(07 §8 数据: FlashLoanHelloWorld 359k / Arbitrageur 487k / MultiPoolArbitrageur viaIR / LiquidationBot 897k gas)。 这一篇答:"无抵押借走百万美金"这件听起来违反金融常识的事为什么在链上不仅成立、 还成了 DeFi 最被复用的"原语"——它不靠魔法,靠的就是 07 §4.4 那条原子性 + 07 §4.5 require revert 的状态全回滚。一旦你看懂这一篇,前面 06–12 的几乎所有"为什么这么设计"都串起来了。 结构见
README.md§1。
0. 一句话本质
闪电贷 = 把"原子性"当抵押品。EVM 单笔 tx 要么完整成功要么状态全回滚(07 §4.4), 所以借方不必先押钱:你借走 N、必须在同一 tx 内还回 N + premium,否则整笔 tx revert → 资金从未离开过 Pool。这不是"信任"也不是"魔法"——是 EVM 原子性 + 合约回调 + 协议简单的
require(repaid)三件套合成的奇迹。
1. 怎么产生的:它解决什么问题
01–12 把基础设施全装完了,最后一块拼图是"杠杆 / 套利 / 清算 怎么不要本金也能做":
- 传统借贷必须先押抵押品(11),是不是就没办法"零本金"做套利? ⇒ 用 Aave 闪电贷——单笔 tx 内借走、用、还,根本不需要抵押。
- 既然不要抵押,那 Pool 为什么愿意把钱借给我?谁担风险?
⇒ Pool 一点风险都不担。因为 Pool 借给你之后,同一笔 tx 里它会调一个 require:
require(balanceOf(self) >= principal + premium, "FLASH_NOT_REPAID")。 你不还 → require 失败 → 07 §4.4 整笔 tx 状态全回滚 → 钱从未离开过 Pool。 - 流程具体怎么走?我看 11 §6 用了 5 步原子流程,闪电贷在第几步?
⇒ Aave V3
flashLoanSimple的 6 步:- 你的合约调
POOL.flashLoanSimple(receiver=self, asset, amount, params, referralCode=0) - Pool 把 amount transfer 给你(你瞬间有钱了)
- Pool 回调
receiver.executeOperation(asset, amount, premium, initiator, params)(这是你写的 callback) - callback 内做你的事:套利 / 清算 / debt refinance
- callback 末尾
IERC20(asset).approve(POOL, amount + premium)+ return true - Pool 在 callback 返回后 transferFrom 你
amount + premium;require 通过 → flashLoanSimple 成功
- 你的合约调
- 5 bps premium 给谁? ⇒ 全部进 Pool(间接给所有 LP / 部分 reserve factor 给治理金库)。 V2 时代是 9 bps,V3 降到 5 bps;这数字是"协议风险溢价 + 摩擦费",对成功的套利者来说基本忽略不计。
- 闪电贷被滥用做攻击的故事都不少——它本身有漏洞吗? ⇒ 闪电贷本身没漏洞——它做的事是合规的:你借/还都 OK 才让你过。 问题在被借的钱拿去打谁:如果某个协议的 oracle / 价格机制可被砸偏(12 §1.5), 闪电贷就是放大器——把"需要 $10M 本金才能攻击"降到"任何人 0 本金就能攻击"。
- 闪电贷以外,DyDx / Uniswap V3 也有自己的闪电贷?
⇒ 有,但 API 不同:
- Aave V3
flashLoanSimple:单资产、5bps、最简(我们项目用的) - Aave V3
flashLoan:多资产 - DyDx
operate:2 wei 名义费(基本免费),但 API 复杂 - Uniswap V3 pool.swap with callback:借任一边代币、无 fee、复杂度高
- Balancer V2:免 fee(仅燃 gas),需还到原 pool
- Aave V3
- 闪电贷的真正限制是什么? ⇒ 必须单 tx 完成全部逻辑——你不能"今天借明天还"。所有获利路径必须能压缩到一次 EVM 执行内。 这就是为什么"flash loan + 跨链套利"不存在(跨链非原子,03/17 待写);闪电贷限定在一条链的一笔 tx。
→ 这一篇答全部。
2. 它本质在做的"那一件事"
把"原子性"这条 EVM 内置的硬保证(07 §4.4)货币化:因为 EVM 保证"全成或全败", Pool 可以把巨额资金借给完全陌生的合约——不用做信用评估、不用押抵押、不用催收, 只要它写一个
require(repaid)在 callback 之后就够了。一旦借方不还,整笔 tx 像没发生过; 一旦还了,Pool 反而收 5bps 净利。这是"链上独有"的金融原语—— 现实世界里没有任何机制能在 12 秒内做相同的事。
→ 闪电贷不是"违反金融常识",是 EVM 原子性给金融常识开了一个全新维度。 传统金融的"信用风险"在这里不存在——只有"原子性是否成立",而它总是成立(除非链分叉,几乎可忽略)。
3. 类比:村庄"原子化金库"
01–12 的村庄已经有:账本、代金券、自动找零桶(AMM)、无人当铺(Aave)、邮差(Oracle)。 13 加一种新机制——不是新设施,而是Aave 当铺多开的一种特殊业务:
┌── 无人当铺新增窗口:闪电贷 ─────────────────────────────────┐
│ │
│ 柜员对你说:"你能用我们金库里的钱,但有 3 条规矩: │
│ 1. 你拿走多少,**这一秒之内**必须还多少 + 0.05% │
│ 2. 还不上 → **从开始那一刻就当作没发生过** │
│ (你的钱包恢复原状、金库的钱恢复原状、所有改动一律撤销)│
│ 3. 你拿钱去干什么我不管——但还不上你白付 gas │
│ " │
│ │
│ 这"一秒之内"= 一笔 EVM tx │
│ 这"当作没发生过"= 07 §4.4 状态全回滚 │
│ 这"还不上你白付 gas"= 07 §4.4 失败只亏 gas │
│ │
│ 柜员怎么知道你还了? │
│ 柜员在你 callback 返回后,调一次 require: │
│ "你的余额 ≥ amount + 0.05%?" → ✅ 通过 / ❌ revert │
│ require 失败 = tx revert = 整一切回到开始 │
│ │
│ 所以柜员**不承担风险**:从他视角看,要么你"无中生有"地变出 │
│ 了 0.05% 的额外钱给他赚 → 他高兴;要么你 tx 整个失败 → │
│ 钱根本没出柜门 → 他也高兴。 │
└─────────────────────────────────────────────────────────────┘
→ 这就是闪电贷"无抵押"的真相:抵押品是整笔 tx 本身——你违约了,时间倒流,违约消失。 现实里没人能做这种生意;链上能,因为 EVM 让"时间倒流"是免费且必然的。
4. 核心关键点(5 条)
-
闪电贷的"无抵押"= 原子性当抵押。
- 借出和归还都在同一笔 tx 内发生
- tx 末尾 Pool 做
require(repaid);不过 → revert → 状态全回滚 → 借款这件事像从未发生 - 借方实际承担的最大损失 = gas 费用(即使逻辑 broken,至少 200k+ gas 还是要付)
- Pool 实际承担风险 = 0(除非 EVM 原子性本身被破坏,而这意味着以太坊整个完蛋了,与闪电贷无关)
-
Aave V3
flashLoanSimple完整 6 步流程(接 11 §6 / 09 §6):step 1: 调用方 → POOL.flashLoanSimple(receiver, asset, amount, params, 0) step 2: POOL → receiver: transfer(amount) (受款方瞬时持有 amount) step 3: POOL → receiver.executeOperation(asset, amount, premium, initiator, params) ↑↑↑ 你的 callback ↑↑↑ 在这里干所有事 step 4: receiver 内: ... 套利 / 清算 / refinance ... step 5: receiver 内: IERC20(asset).approve(POOL, amount+premium) return true (告诉 Pool: "我准备好了,来拉回") step 6: POOL → transferFrom(receiver, POOL, amount+premium) require(balanceCheck OK) → emit FlashLoan(...) → success- 第 3 步是回调:Pool 主动调你写的
executeOperation(接口 selector0x920f5c84,08 §6.4) - 第 5 步必须 approve:09 §6 同款机制——你不 approve,Pool 第 6 步 transferFrom 就 revert
- 6 步全在同一 tx 内——这是闪电贷的全部魔法
- 第 3 步是回调:Pool 主动调你写的
-
5 bps premium 是什么 + 算法:
- 公式:
premium = amount * 5 / 10000 - 对 26,869 USDC 借款:
26869e6 * 5 / 10000 = 13434500base units ≈ $13.43 - 一年内成功执行 1000 次:$13,430 总成本,对成功套利完全可忽略
- 5bps 是 V3 配置参数
FLASHLOAN_PREMIUM_TOTAL,可被治理调整(V2 时是 9bps) - 资金流向:90% 给 Pool(间接给 LPs),10% 给 protocol treasury(reserve factor)
- 公式:
-
闪电贷的三种主流用法(DeFi 工具书必背):
用法 干什么 我们项目里 注意 套利 借 A → 跨池 swap → 还 A + premium → 利润落袋 Arbitrageur.sol(487k gas)价差必须 > 双边手续费 + 5bps + gas 清算 借 B → 替他人还债 → 拿抵押 → swap → 还 B + premium LiquidationBot.sol(897k gas)HF < 1 + liqBonus > 总成本 抵押 / 债务重构 借 X → 还旧债 → 取出抵押 → 新协议存抵押 → 借 X → 还闪电贷 我们没做 高级 DeFi 用户跨协议优化利率 -
闪电贷攻击不是闪电贷的锅:
- bZx 2020-02:闪电贷 + Kyber spot price 操纵 → $630k(首例闪电贷攻击)
- Harvest Finance 2020-10:闪电贷 + Curve spot price 操纵 → $24M
- PancakeBunny 2021-05:闪电贷砸偏 BNB-BUNNY 池 → $200M+
- Beanstalk 2022-04:闪电贷买治理代币立刻投票通过恶意提案 → $182M
- 共同点:受害协议自身机制设计有问题(spot oracle / 治理无延迟 / 抵押估值漏洞)
- 闪电贷把"门票 $10M"变成了"门票 $200 gas",但漏洞是被找出来才能被利用的
- 正确防御方式:修协议漏洞(用 TWAP / 治理时间锁 / 多源 oracle),不是封禁闪电贷
5. 必须先认识的前置名词(小词典)
| 名词 | 一句话 | 类比 |
|---|---|---|
| flash loan | 单 tx 内借/还的无抵押贷款 | 当铺的"时间倒流"特别窗口 |
| flashLoanSimple | Aave V3 单资产闪电贷入口,最常用 | 单币特快窗 |
| flashLoan (multi) | Aave V3 多资产入口,一次借多种 | 一篮子借 |
| executeOperation | 闪电贷 callback;接口 selector 0x920f5c84 |
借方必须实现的"我会还"承诺函数 |
| premium | 借贷费(5bps) | 借一次的服务费 |
FLASHLOAN_PREMIUM_TOTAL |
Aave 配置参数 = 5 (单位 bps) | 服务费率 |
| receiver | 闪电贷的"借方合约"地址 | 业务执行人 |
| initiator | 调 flashLoanSimple 的最初地址(一般 = receiver) | 操作发起人 |
| referralCode | Aave 早期分销用,现在写 0 | 推荐码 |
| 原子性 (atomicity) | 单 tx 全成或全败(07 §4.4) | 时间倒流保证 |
IFlashLoanSimpleReceiver |
Aave 接口;你必须 is 它(08 §6.1) |
借方资格证 |
| flash swap (Uni V2/V3) | 用 swap 的 callback 借(实际无须先付) | 桶的赊账 swap |
| flash mint | 临时铸造再销毁(如 DAI、FRAX) | 印钱再撕 |
| flash loan attack | 利用闪电贷放大对其他协议漏洞的攻击 | 借大炮去攻有 bug 的城门 |
| MEV searcher | 搜寻链上机会的实体;闪电贷是其最常用工具 | 战场扫荡兵 |
6. 一个完整过程走一遍:我们 Arbitrageur 的 11 阶段细节
把 pnpm demo 这一笔从 EVM 视角放慢到逐条状态变更——前面 09/11 已经走过部分,这里只放大闪电贷那一段:
设定
- amount = 26,869 USDC =
26869 * 10^6base units - premium =
26869 * 5 / 10000 * 10^6 = 13,434,500base units ≈ $13.43 - owed =
26869013434500
阶段 1:调用 + 资金借出
─── T0: msg.sender(EOA) calls Arbitrageur.arb(routers, USDC, 26869e6) ───
─── Arbitrageur.arb 内第一行 ───
POOL.flashLoanSimple(
receiverAddress = address(this), // self
asset = USDC,
amount = 26869e6,
params = bytes(""), // 我们不用 params 字段
referralCode = 0
)
─── POOL 内部 ───
USDC.transfer(Arbitrageur, 26869e6) (Pool's USDC balance ↓; Arbitrageur's ↑)
POOL 记录: pendingFlashLoanRepay[Arbitrageur][USDC] = owed
阶段 2:POOL 回调 executeOperation(Arbitrageur 的业务逻辑)
─── POOL 调 Arbitrageur.executeOperation(USDC, 26869e6, 13434500, msg.sender, "") ───
─── Arbitrageur.executeOperation 内 ───
// 现在我有 26869e6 USDC、欠 owed
// step 1: USDC → WETH on routers[0] (Uni V2)
IERC20(USDC).approve(routers[0], 26869e6)
amountOutWETH = router0.swapExactTokensForTokens(
amountIn=26869e6, amountOutMin=0, [USDC, WETH], self, deadline)
// step 2: WETH → USDC on routers[1] (Sushi)
IERC20(WETH).approve(routers[1], amountOutWETH)
amountOutUSDC = router1.swapExactTokensForTokens(
amountIn=amountOutWETH, amountOutMin=0, [WETH, USDC], self, deadline)
// step 3: 检查赢够还了
require(amountOutUSDC >= owed, "PROFIT_FAIL")
// 08 §4.5 REVERT + Error(string)
// 失败这里就回滚整个 tx (07 §4.4)
// step 4: approve POOL 收回本金 + premium
IERC20(USDC).approve(POOL, owed)
return true // 告诉 POOL "成了"
阶段 3:POOL 验收 + 收回
─── POOL 内部继续 ───
IERC20(USDC).transferFrom(Arbitrageur, POOL, owed) (09 §6 流程)
require(USDC.balanceOf(POOL) >= POOL_balance_before_flashloan,
"FLASH_NOT_REPAID") // 这里是核心 require
// 没拿够 owed 就 revert
emit FlashLoan(target, asset, amount, premium, mode=0, ref=0)
─── flashLoanSimple 成功返回 ───
─── tx 落账 receipt.status = 1, gasUsed = 487252 ───
"失败"路径(也走一遍)
─── 如果阶段 2 step 3 那个 require(amountOutUSDC >= owed) 失败 ───
REVERT + "PROFIT_FAIL" Error data
↓
阶段 2 内所有状态变更(两次 swap、两次 approve)→ 全部回滚
↓
阶段 1 内 POOL.transfer(Arbitrageur, 26869e6) → 也回滚
↓
EOA 余额恢复原状 (gas 已花的不退,07 §4.4)
↓
tx receipt.status = 0, gasUsed = 271552 (07 §8)
↓
POOL 不变;攻击者 / 操作者亏 27 万 gas (~$10-50 取决 gas price)
→ 这就是"闪电贷的安全性 = EVM 原子性"的字节级演示。Pool 完全无风险,借方最大损失 = gas。
7. 常见误区(逐条拍死)
| 误区 | 实情 |
|---|---|
| "闪电贷是免费钱" | 不是。你必须有一个能在一笔 tx 内赚回 amount + premium 的策略,否则 revert 你亏 gas |
| "闪电贷靠对借方的信任" | 完全不靠。靠 EVM 原子性 + require(repaid)。借方信不信用、是谁都无关 |
| "闪电贷有信用风险" | 0(除非以太坊本身故障)。这是金融史上极少见的真零风险贷款 |
| "5 bps 太高" | 对成功套利完全可忽略。$1M 闪电贷的 premium = $500,远低于一般套利毛利 |
| "闪电贷不能跨链" | 对。跨链不原子(17 待写)→ 不可能跨链闪电贷。闪电贷限定单链单 tx |
| "Aave 应禁闪电贷" | 反对:闪电贷是 DeFi 价格发现 / 清算的核心工具;禁了反而让坏协议永远卡在错价上 |
| "闪电贷攻击是闪电贷的责任" | 不是。受害协议设计漏洞(spot oracle / 无 timelock / 弱抵押估值)才是问题 |
| "我合约可以检测调用者是不是闪电贷" | 几乎不可能。看 balance?借方拿到钱后就是有那么多 balance。看 origin?任何人都能搭个 EOA。任何 per-tx defense 都脆弱 |
| "闪电贷 = 闪兑(flash swap)" | 不一样。闪电贷是借 + 还,Pool 收 fee;闪兑是 Uni V2/V3 pool 直接给你币、callback 内你或还币、或还另一边币(赊账 swap) |
| "闪电贷必须用 Aave" | 不。Balancer / DyDx / Uni V3 / dForce 都有。Aave 流动性最深,最常用 |
| "params 字段没用" | 有用——把额外参数从 arb() 入口传到 executeOperation 内(因为后者签名固定)。我们项目里 params 留空,因为路径已经硬编码进 routers 参数 |
| "executeOperation 名字能改" | 不能。Aave 硬编码调用选择器 0x920f5c84(08 §6.4)。名字一改 selector 变 → Aave 调不到 → revert |
8. 与本项目实践的对应
闪电贷是我们项目的引擎——4 份合约全用。把每份合约里闪电贷的角色拆开:
FlashLoanHelloWorld.sol (359,816 gas, 07 §8)
- 最简单形式:借 1 WETH → approve POOL → return true
- 没有"业务逻辑",纯演示闪电贷协议本身
- 这是为什么 gas 最低——只是 POOL + receiver 之间两次 transfer + 两次 approve
Arbitrageur.sol (487,252 gas, 07 §8)
- 借 USDC → Uni V2 swap → Sushi swap → require 利润够 → approve POOL → return true
- 上面 §6 完整走过
- 487k 比 hello world 多了 ~127k = 两次 V2 swap 的 SSTORE + 第二次 approve
MultiPoolArbitrageur.sol(开 viaIR, 07 §1.5)
- 借 USDC → 多池路由(pool[i] → pool[j] → pool[k])→ 验利润 → 还
- 因为局部变量超 EVM stack 16-slot 窗口,必须开
via_ir = true走 Yul IR - 闭式最优 amountIn 算法在 §6 外(详 10 §8)
- 这是项目里最"算法密集"的合约
LiquidationBot.sol (897,176 gas, 07 §8 + 11 §8)
- 借 USDC → liquidationCall(victim) → 拿 WETH 抵押 + 5% bonus → V2 swap WETH→USDC → 还 USDC + 5bps
- 11 §6 完整走过
- 897k 比 arb 多了 ~410k = liquidationCall 内部状态更新 (300k+) + 一次 WETH 抵押 transfer
Phase 5 Flashbots 阶段的承接
- 单 tx 闪电贷套利 + Flashbots bundle = MEV 经典模式
- 我们
demo-flashbots.ts真实 relay 握手(structured error 证明 SDK/auth/transport 全通)= 把闪电贷套利打包成私 mempool 提交,避开公 mempool 被抢 - 14 / 15 / 16 详细展开 mempool / MEV / Flashbots 的端到端
dealUsdc 在 setup 中的角色
- 测试 setup 要先用
dealUsdc(Aave_USDC_aToken, 1_000_000_000e6)给 Aave Pool 备足闪电贷可用流动性 - 否则 Pool 自己 USDC 不够 → 闪电贷 transfer 那一步就 OOM revert
- 这是为什么 06 §4.4 + 09 §8 反复提的 storage 第 9 槽 cheat 是项目所有测试的基石
第三层完结的串接
- 10 DEX/AMM 给"换币"的水管
- 11 借贷协议给"杠杆 / 清算"的水管
- 12 预言机给"价格 = 多少"的水管
- 13 闪电贷给"启动资金 = 0"的水管
- 任何 MEV 搜寻者的工具箱本质就是这四样的组合 → 14/15 走的就是组合艺术
9. 自检问题(答得出才算过)
- 闪电贷为什么不需要抵押?精确到 EVM 原语回答(哪两条 opcode + 哪一条 require)。
- 写出 Aave V3
flashLoanSimple的 6 步流程,标出哪一步在 receiver 合约内、哪一步在 Pool 内、哪一步是 callback。 - 5 bps premium 怎么算?对 1M USDC 借款是多少?premium 流向哪里?
- 闪电贷的三种主流用法是什么?我们项目分别对应哪几份合约?各自 gas 量级及差异来源?
- 闪电贷攻击(bZx / Harvest / PancakeBunny)的共同点是什么?责任在闪电贷本身吗?正确防御方式是?
- 为什么"detect caller is using flash loan"几乎不可能?这个不可能性的根源是什么?
- 跨链闪电贷为什么不存在?这跟原子性的哪条性质冲突?
- 我们
Arbitrageur的executeOperation选择器是固定的0x920f5c84,能改吗?为什么?
── Q&A / 更正记录区(按日期追加)──
规范见
README.md§0。质疑/反馈/纠错按日期累积,不抹错误轨迹。
- (2026-05-21 建立。暂无 Q&A。)