12 · 预言机:链下世界往链上送数据的唯一桥
第三层第三篇。01–11 的所有合约逻辑只看链上 storage;但 11 §4.2 健康因子公式里那个
price_i从哪来? 我们LiquidationBot.t.solmock 的AaveOracle.getAssetPrice(WETH)又是什么? 这一篇答:EVM 看不到链外世界(确定性的代价,07 §4.1),所以一切外部数据——价格、汇率、 比赛结果、随机数、油价、天气——都要靠"预言机"这种"链下数据 + 链上验证"的中间件送进合约; 它是 DeFi 最关键又最脆弱的依赖,历史上 $1B+ 资金被它的脆弱性吃掉。结构见README.md§1。
0. 一句话本质
Oracle(预言机)= 把链下数据按"采集 → 签名/聚合 → 链上验证 → 合约读取"路径搬上链的基础设施。 链上合约不能主动联网,所以它需要的所有外部信息都得有人推上来或被它拉过来**;这个角色就是 Oracle。 它三种主流形态:push(Chainlink 节点周期推)、pull(Pyth 用户用时拉签名)、TWAP(从 AMM 历史推导)。 每种都有自己的攻击面——历史上 Mango Markets $117M、Bonq $120M、Cream $130M 的去处都在这一篇。**
1. 怎么产生的:它解决什么问题
01–11 留下的最大缺口:
- EVM 不能联网,那 Aave 的
WETH 价 = $3000是怎么知道的? ⇒ 它"知道"不来。某个链下节点采到价格 → 签名 → 推一笔 tx 到链上一个"喂价合约" → Aave 内部去那个合约读。 - 既然要靠链下节点,那谁信节点不撒谎? ⇒ 不止 1 个节点:典型 Chainlink ETH/USD 喂价由 31 个独立节点各采各的,链上合约取中位数。 单个节点撒谎 → 中位数不会被拉走。多节点同时撒谎 → 经济成本极高(节点要质押 LINK,撒谎被 slash)。
- 节点多久推一次?我们不停查会浪费 gas 吗? ⇒ 推的条件:(a) 价格变化超过 deviation threshold(如 ETH/USD 是 0.5%)或 (b) 距上次推过了 heartbeat(ETH/USD 是 3600 秒),二者满足任一就推。不变就不推——这是为什么"链上 ETH 价" 可能跟 Binance 实时价差 0.5% 但合约觉得稳定。
- 为什么 11 §6 里我们能直接 mock
AaveOracle.getAssetPrice改 WETH 价 45%? ⇒ 因为 AaveOracle 是个包装合约:它去问 Chainlink AggregatorV3,再做点 staleness / decimal 规范。 fork 测试里vm.mockCall(AaveOracle, "getAssetPrice(WETH)", returnValue=$1650)让所有读它的合约 都看到这个返回值,绕开 Chainlink 节点共识。这只在测试环境合法,主网无人能这么改。 - 既然合约依赖 Oracle,那"操纵 Oracle 就能洗劫协议"是真的吗?
⇒ 是。这是 DeFi 黑客的头号攻击面。形式有三:
- 操纵 spot-price oracle(合约用某个 V2 pair 即时价当 oracle → 闪电贷砸偏 → 协议按错价清算或抵押)
- 操纵短窗 TWAP(窗口 < 1 块时还能砸偏;现代项目都用 ≥ 5 块)
- 价格饲源延迟 / 卡死(heartbeat 长 + 网络故障 → 用旧价计算,可能错杀健康仓位或包庇不健康仓位)
- Pyth 跟 Chainlink 有什么不同?为什么 Aave / GMX 等新协议越来越用 Pyth? ⇒ Chainlink 是 push:节点主动推到链上 → 链上一直保留最新值 → 读它免费(一次 SLOAD)。 Pyth 是 pull:节点把签好名的价格挂在链下 → 用户用时把那份签名包进 tx,链上验证 → 用一次推一次。 Pull 不烧"无人需要的"链上推送 gas、价格更新更密集(毫秒级);代价是每次用户要带签名 + 多一点 verify gas。
- Uniswap V3 自带的 TWAP oracle 又是啥? ⇒ V3 pool 每个 swap 写一个 observation;任何人可以问 V3 pool"过去 N 秒的几何平均价"。 优势:不靠外部节点;攻击者要操纵 TWAP 必须持续多块持仓砸偏,成本随时间 × 池深爆炸增长。 劣势:价格滞后(窗口越长越滞后);浅池仍可被操纵;只能用于 V3 上有深池的资产对。
- 历史上具体哪些项目被 oracle 攻击过?多少钱?
- Compound 2020-11:DAI 价被 Coinbase 上瞬间砸偏,Compound 用了它当 oracle → 错杀 $89M 清算
- Mango Markets 2022-10:Avraham Eisenberg 用 1100万 USDC 砸高 MNGO 价 → 用虚高估值借走 $117M → 后被起诉
- Cream Finance 2021-10:yUSDVault 价格被刷高 → 攻击者借走 $130M
- Bonq DAO 2023-02:用 spot-price 当 oracle 的 ALBT 池被砸 → 借走 $120M
- Inverse Finance 2022-04:INV 池被砸偏 → $15M
- 共同点:用一个浅池/单源 spot price 当 oracle 给借贷 / 衍生品估值。
→ 这一篇答全部。
2. 它本质在做的"那一件事"
链上世界的"知识"被严格限制在自己的状态机里(这是确定性、可验证、可重放的代价,07 §4.1)。 Oracle 的工作就是把链外世界的数据用密码学 + 经济激励 + 多源聚合三层武装后送进来**, 让 DeFi 协议能在"我不能上网"的硬约束下继续运转。它是一切链上金融的脚手架—— 也因此是其最常被攻击的部位(金融性合约的市值越大,oracle 操纵的预期收益越高)。**
→ 这是为什么 11 §4 / §7 反复强调 "oracle 一抖 HF 跟着抖":HF 这条线是冷血公式,但喂进去的价格不冷血。
3. 类比:村庄"邮差 + 多人公证"
01–11 的村庄已经有了账本、代金券、自动找零桶、无人当铺。但村庄合约里没有"现在 ETH 多少美元" 这种链外信息。12 加这一类基础设施:
┌── Push 模式(Chainlink)────────────────────────────────────┐
│ │
│ 31 个邮差,各自跑去外面(Binance、Coinbase、Kraken)抄价 │
│ → 各自签名:"我 Alice,看到的是 $3000.12,时间 12:00:01" │
│ 邮差们的签名汇总到链上"喂价合约"(AggregatorV3) │
│ → 合约取中位数 = $3000,写到 storage │
│ Aave / 你的合约 / 任何人 → 直接读这个 storage(一次 SLOAD)│
│ │
│ 邮差什么时候来推? │
│ - 价格相对上次变了 ≥ 0.5%(deviation threshold) │
│ - 或者距上次过了 1 小时(heartbeat) │
│ - 满足任一即推,否则保持上次值 │
│ │
│ 这就解释了 "链上 ETH 价跟 Binance 差 0.3% 也算正常" │
└─────────────────────────────────────────────────────────────┘
┌── Pull 模式(Pyth)─────────────────────────────────────────┐
│ │
│ 邮差不主动跑链上推,而是把签好名的最新价挂在"消息板" │
│ Wormhole / 自家网络上每毫秒更新 │
│ 你要用价时: │
│ 1. 自己从消息板把最新签名拉下来 │
│ 2. 把签名包进你的 tx (作为 calldata) │
│ 3. 合约里调 pyth.updatePriceFeeds(签名) → 验签 → 用 │
│ 好处:邮差不浪费 gas 推没人用的更新;价格更新可极频繁 │
│ 坏处:每次使用需要用户/dApp 主动带签名(多一步 UX) │
└─────────────────────────────────────────────────────────────┘
┌── TWAP 模式(Uni V3 / Curve EMA)─────────────────────────┐
│ │
│ 不靠外部邮差,只看本村自己的自动找零桶(10 AMM)历史成交价 │
│ "过去 600 秒的几何平均价 = ?" │
│ 攻击者想砸偏 TWAP → 必须持续多块持仓砸偏,成本 = 池深 × 时间│
│ 好处:无外部依赖;攻击成本随窗口 × 池深线性涨 │
│ 坏处:滞后;浅池仍可被操纵;只能用 V3 有深池的资产对 │
└─────────────────────────────────────────────────────────────┘
→ 三套机制各有所长。生产 DeFi 协议常多源混合:主用 Chainlink,TWAP 当 sanity check, Pyth 当低延迟补充。
4. 核心关键点(5 条)
-
EVM 看不到链外 → 一切外部数据需 Oracle。
- "用 chainlink 在 Solidity 调一个 https://api.binance.com" 这种事不可能——EVM 没网络栈
- Chainlink Functions / API3 等"任意外部数据"方案本质上也是同一架构:链下节点拉数据 + 链上多签验证
- 这条硬约束的来源是 07 §4.1 确定性 + 03 §4.5 state root 一致性——每个节点必须独立算出同结果。 如果合约能联网,节点 A 在北京、节点 B 在纽约采到不同数 → state 不一致 → 共识崩溃
-
三种主流架构(push / pull / TWAP):
维度 Push (Chainlink) Pull (Pyth) TWAP (Uni V3) 谁推 节点周期推 节点签名挂消息板 任何 swap 自动写 读延迟 高(heartbeat 间隔) 低(毫秒级,但要拉) 中(窗口长度) 读 gas 极低(一次 SLOAD) 中(验签 ~30k gas) 低(一次 view 调用) 外部依赖 Chainlink 节点 Pyth 网络 无 攻击 节点共谋(极贵) 同 砸偏窗口(贵) 适用 主流币 + 大池 低延迟衍生品 V3 深池 -
聚合 + 多源是核心抗操纵机制:
- N 个节点取中位数:单点撒谎 → 被剔除
- 节点质押 + slashing:撒谎成本 > 撒谎收益(理论上)
- deviation threshold:小波动不推 → 减少链上 gas 与噪声
- 多源喂价(如 Chainlink 自身就跑多个 CEX/DEX 节点聚合)→ 不依赖某一交易所
-
"用 spot price 当 oracle"是 DeFi 头号自杀方式:
- 闪电贷可以无本金把一个浅池价砸偏 1000%(13 待写)
- 用 V2 pair 单次 spot price 当 oracle → 一次 tx 内砸偏 + 用错价借/清算 + 还原 → 协议被掏空
- 历史血案(§1.8)的共同点都是这条
- 正确做法:用 Chainlink 喂价 / Uni V3 多窗口 TWAP / 多源混合,永远不要用一个浅池即时价
-
Staleness check(陈旧数据检查)是合约的必备手套:
- 喂价合约会返回
updatedAt(最近一次更新的时间戳) - 你的合约必须检查:
require(block.timestamp - updatedAt <= maxAge, "STALE") - 没做这检查 + 链上喂价网络故障 → 合约用上周价继续工作 → 错杀健康仓位 / 包庇不健康仓位
- Aave V3 / Compound III / Morpho 都做这检查;许多 fork 自小 protocol 漏掉过,被攻击
- 喂价合约会返回
5. 必须先认识的前置名词(小词典)
| 名词 | 一句话 | 类比 |
|---|---|---|
| Oracle | 把链下数据搬上链的中间件 | 邮差 |
| AggregatorV3 / latestRoundData | Chainlink 喂价合约接口 / 读取入口 | 公告栏 + 一格便条 |
| deviation threshold | 价格变化超此即推送 | 邮差出门门槛 |
| heartbeat | 即使没变也强制推送的最大间隔 | 强制签到 |
| staleness | 喂价过期(updatedAt 太久前) | 便条过期 |
| median oracle | 取 N 个节点中位数报价 | 多人公证取中间值 |
| TWAP (Time-Weighted Average Price) | 一段时间内成交价的几何/算术平均 | 一段时间均价 |
| Uni V3 observation | V3 pool 每个 swap 写的一条价格快照 | 桶记的每笔流水 |
| push / pull oracle | 节点主动推 / 用户用时拉 | 邮差送上门 / 你去取 |
| Pyth | Wormhole 系 pull oracle,金融级低延迟 | 高速消息板 |
| AaveOracle | Aave V3 包装 Chainlink 的中介合约 | 当铺的估价员 |
| Chainlink ETH/USD | 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419 |
ETH 价喂价合约 |
| decimals (price) | 价格 reported 的小数位 (Chainlink: 8) | 价位精度 |
| manipulation surface | 操纵 oracle 可获利的所有路径 | 邮差被买通的方式 |
| flash-loan price oracle attack | 闪电贷 + 浅池 spot 操纵的复合攻击 | 临时巨富把价砸偏 |
6. 一个完整过程走一遍:我们 mock 的那笔 oracle 调用
照搬 test_Liquidation_FlashLoanFunded 的 oracle mock 行为,把整条调用链展开:
真实主网(不 mock)
─── LiquidationBot 调 POOL.liquidationCall ─────────────────
↓
─── POOL 内部计算 collateralToSeize ────────────────────────
↓
─── POOL 调 ORACLE.getAssetPrice(WETH) ─────────────────────
↓
─── AaveOracle 内部 ────────────────────────────────────────
1. lookup chainlinkAggregator = chainlinkAggregators[WETH]
= 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419 (ETH/USD)
2. (uint80 round, int256 answer, uint256 startedAt,
uint256 updatedAt, uint80 answeredInRound)
= AggregatorV3(chainlinkAggregator).latestRoundData();
3. 校验:
require(answer > 0, "INVALID_PRICE")
require(block.timestamp - updatedAt <= 1 day, "STALE")
4. 归一化 decimals: chainlink price = 3000_00000000 (8 decimals)
转成 AaveOracle 标准 (8 decimals 同样保留) → return 300000000000
↓
─── POOL 用此价 × LiqThreshold × balances 算 HF ────────────
↓
─── HF < 1 ⇒ 允许清算; HF < 0.95 ⇒ 全额清算 ────────────────
我们 fork 测试里 mock 后
─── setUp() 里 ──────────────────────────────────────────
vm.mockCall(
ORACLE,
abi.encodeWithSignature("getAssetPrice(address)", WETH),
abi.encode(165000000000) // $1650 × 1e8
)
⇕
─── 任何合约(包括 POOL)调 ORACLE.getAssetPrice(WETH) ─
→ 直接返回 $1650,**绕过整个 Chainlink 路径**
⇕
─── POOL 用 $1650 算 HF ⇒ victim HF ≈ 0.57 ⇒ 全额清算 ──
→ 这个单点 mock 让我们模拟"WETH 价砸 45%"成为可能。 关键洞察:fork 测试可以这么干(Foundry cheat),但主网上没有人能这么改 AaveOracle—— 它的 chainlinkAggregator 映射是 owner-only setter,又被治理控制。 你想"操纵"主网的 WETH/USD 价:得真把 31 个 Chainlink 节点串通起来(不现实)或砸偏底层 Binance/Coinbase 价(资本量巨大且只能维持极短时间)。
但协议自己用错 oracle就麻烦了
- Bonq DAO 2023:用一个浅池 V2 ALBT/USDC pair 的 spot price 当 ALBT oracle → 借 $130k 闪电贷 → swap → ALBT 价瞬间 ×100 → 用虚高估值借走 $120M → swap 还原 → 落袋
- Mango Markets 2022:用一个 Solana 上的 spot oracle → 用 $11M 砸高 MNGO 价 ×7 → 借 $117M USDC + BTC + SOL → 落袋 → 后被告自首
- 教训:合约的 oracle 选择 = 整个协议的安全上限。永远别省这一步。
7. 常见误区(逐条拍死)
| 误区 | 实情 |
|---|---|
| "Chainlink 价是实时的" | 是"deviation 或 heartbeat 内"。3600 秒 heartbeat + 0.5% deviation 意味着可能 1 小时不动 |
| "Smart contract 可以 fetch https" | 不可以。EVM 无网络栈;"链外数据"只能由 oracle 推送/被拉签验 |
| "Pyth 比 Chainlink 总是更好" | 不一定。Pyth 频率高但需要 dApp 把签名包进 tx(UX 摩擦 + 用户得付 verify gas)。简单借贷协议用 Chainlink 更顺 |
| "用 Uni V3 TWAP 就万无一失" | TWAP 在浅池仍可被多块持续砸偏;窗口选太短(< 5 块)也可单 tx 砸偏 |
| "Chainlink 节点也可能撒谎" | 单节点可以,但 N 个节点取中位数 + 每个节点要质押 LINK → 经济上不划算撒谎 |
"我可以直接 cast call AaveOracle setAssetSources(...) 改主网 oracle" |
改不了。setter 是 owner-only,owner 是 Aave Governance(多签);fork 测试里 vm.startPrank(owner) 才行 |
| "Oracle 攻击都是 protocol bug" | 大多是 protocol 的 oracle 选择错(用浅池 spot),不是 Chainlink 本身被攻破 |
| "TWAP 比 spot 一定贵砸偏" | 是。但浅池 5 分钟 TWAP 仍 可能比 deep pool 的 spot 还容易砸——深度才是关键 |
| "AaveOracle 价格永远准" | 它只是 Chainlink 价的包装 + staleness 检查。如果 Chainlink 网络当机超过 staleness 阈值,AaveOracle 会 revert → 整个 Aave 借贷功能停摆 |
| "我自己跑个 RPC 节点就避免 oracle 问题" | 完全无关。RPC 节点是 05 §3,只让你看到链上当前 state;oracle 是把外部世界数据上链的另一层 |
| "oracle decimals 跟 token decimals 一样" | 不一样。Chainlink price feed 一般 8 decimals;token 自己 6/18 decimals(09 §4.3);混淆会差几个量级 |
8. 与本项目实践的对应
我们 LiquidationBot.t.sol 的 oracle mock(11 §6 接此)
- 关键一行:
vm.mockCall(ORACLE, abi.encodeWithSignature("getAssetPrice(address)", WETH), abi.encode(165000000000))- ORACLE =
0x54586bE62E3c3580375aE3723C145253060Ca0C2 165000000000= $1650 × 1e8(Chainlink 8 decimals)- mock 是 fork 测试唯一能"瞬间砸 45%"的途径;主网这么改要么是 Aave 治理通过(不可能短时),要么真砸 Binance(不可行)
- ORACLE =
- mock 是 Foundry 的 cheat:本质是 vm 拦截 EVM 对该地址 + 该 selector 的所有 STATICCALL,强行返回这串字节。 Foundry 在 anvil fork 之上插入这层拦截器,主网当然没这种东西。
Aave V3 主网 oracle cheatsheet
- AaveOracle:
0x54586bE62E3c3580375aE3723C145253060Ca0C2 - Chainlink ETH/USD AggregatorV3:
0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419 - Chainlink USDC/USD AggregatorV3:
0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6 - Chainlink BTC/USD AggregatorV3:
0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c - 各币 mapping 在 AaveOracle.sourcesOfAssets[asset] 一查即得
- Heartbeat: ETH/USD 约 3600 秒;USDC/USD 约 86400 秒;BTC/USD 约 3600 秒
- Deviation threshold: ETH/USD 0.5%;USDC/USD 0.25%
自我更正记录的回看(接 docs 2026-05-19)
- 我们当时观察"跨链 / 跨池价差 ≈ 0" → 现在能反向解释: 这正是因为 oracle + 套利者的反馈环把价格稳住了。如果 oracle 当机 / 卡 stale,价差可能短时拉开 → 此时"机会"突然涌现 → 也是基础设施齐全者收割的窗口(普通人捕不到,因为他们没察觉 oracle 卡 stale)
- 这就是"残差是 footprint 不是 absence"的精确机制——oracle 稳的时候没机会,oracle 抖的时候机会大但需配套基础设施
与 13 / 15 的承接
- 13 闪电贷:oracle 操纵攻击的"放大器"。无本金 → 砸偏浅池 oracle → 用错价获利 → 还本金。 没有闪电贷,oracle 攻击门槛会高 ~100×(需要真本金)
- 14 mempool / 15 MEV:清算时序竞争里"谁先看到 oracle 更新 + HF 跨线"的就吃肉。 Oracle 更新本身是一笔链上 tx → mempool 里能看到 → 提前部署清算 bundle 抢入
项目设计层面教训
- 我们项目没自己写 oracle(用的都是 Aave / Uni 自带的)→ 这是对的,不要自造 oracle
- 如果未来要扩展(如做 LP 估值 / NFT 估值)→ 优先用 Chainlink + 多源混合 + 严格 staleness check
- 永远不要用一个 V2 pair 即时 reserves 比当 oracle
9. 自检问题(答得出才算过)
- 为什么 EVM 不能直接调外部 API?这跟 03 / 07 哪条性质冲突?
- Push / Pull / TWAP 三种 oracle 模式各自的优缺点、典型项目、读取 gas、攻击面?
- Chainlink 的 deviation threshold 和 heartbeat 各自起什么作用?为什么不每秒推一次?
- 写出"使用 spot price 当 oracle 被闪电贷攻击"的攻击路径(5 步)。为什么用 TWAP 能挡?
- Staleness check 是什么?没做它的合约可能被怎样利用?
- 我们
LiquidationBot.t.sol里vm.mockCall在 fork 里 work、主网不 work,原理是什么? - AaveOracle、Chainlink AggregatorV3、Pyth 三者的关系?AaveOracle 自己产价吗?
- 历史上 Mango Markets / Bonq / Cream 的 oracle 攻击共同点是什么?设计协议时怎么避免?
── Q&A / 更正记录区(按日期追加)──
规范见
README.md§0。质疑/反馈/纠错按日期累积,不抹错误轨迹。
- (2026-05-21 建立。暂无 Q&A。)