第三层第三篇。01–11 的所有合约逻辑只看链上 storage;但 11 §4.2 健康因子公式里那个 price_i 从哪来? 我们 LiquidationBot.t.sol mock 的 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 留下的最大缺口:

  1. EVM 不能联网,那 Aave 的 WETH 价 = $3000 是怎么知道的? ⇒ 它"知道"不来。某个链下节点采到价格 → 签名 → 推一笔 tx 到链上一个"喂价合约" → Aave 内部去那个合约读。
  2. 既然要靠链下节点,那谁信节点不撒谎? ⇒ 不止 1 个节点:典型 Chainlink ETH/USD 喂价由 31 个独立节点各采各的,链上合约取中位数。 单个节点撒谎 → 中位数不会被拉走。多节点同时撒谎 → 经济成本极高(节点要质押 LINK,撒谎被 slash)。
  3. 节点多久推一次?我们不停查会浪费 gas 吗? ⇒ 推的条件:(a) 价格变化超过 deviation threshold(如 ETH/USD 是 0.5%)或 (b) 距上次推过了 heartbeat(ETH/USD 是 3600 秒),二者满足任一就推。不变就不推——这是为什么"链上 ETH 价" 可能跟 Binance 实时价差 0.5% 但合约觉得稳定。
  4. 为什么 11 §6 里我们能直接 mock AaveOracle.getAssetPrice 改 WETH 价 45%? ⇒ 因为 AaveOracle 是个包装合约:它去问 Chainlink AggregatorV3,再做点 staleness / decimal 规范。 fork 测试里 vm.mockCall(AaveOracle, "getAssetPrice(WETH)", returnValue=$1650) 让所有读它的合约 都看到这个返回值,绕开 Chainlink 节点共识。这只在测试环境合法,主网无人能这么改。
  5. 既然合约依赖 Oracle,那"操纵 Oracle 就能洗劫协议"是真的吗? ⇒ 是。这是 DeFi 黑客的头号攻击面。形式有三:
    • 操纵 spot-price oracle(合约用某个 V2 pair 即时价当 oracle → 闪电贷砸偏 → 协议按错价清算或抵押)
    • 操纵短窗 TWAP(窗口 < 1 块时还能砸偏;现代项目都用 ≥ 5 块)
    • 价格饲源延迟 / 卡死(heartbeat 长 + 网络故障 → 用旧价计算,可能错杀健康仓位或包庇不健康仓位)
  6. Pyth 跟 Chainlink 有什么不同?为什么 Aave / GMX 等新协议越来越用 Pyth? ⇒ Chainlink 是 push:节点主动推到链上 → 链上一直保留最新值 → 读它免费(一次 SLOAD)。 Pyth 是 pull:节点把签好名的价格挂在链下 → 用户用时把那份签名包进 tx,链上验证 → 用一次推一次。 Pull 不烧"无人需要的"链上推送 gas、价格更新更密集(毫秒级);代价是每次用户要带签名 + 多一点 verify gas。
  7. Uniswap V3 自带的 TWAP oracle 又是啥? ⇒ V3 pool 每个 swap 写一个 observation;任何人可以问 V3 pool"过去 N 秒的几何平均价"。 优势:不靠外部节点;攻击者要操纵 TWAP 必须持续多块持仓砸偏,成本随时间 × 池深爆炸增长。 劣势:价格滞后(窗口越长越滞后);浅池仍可被操纵;只能用于 V3 上有深池的资产对。
  8. 历史上具体哪些项目被 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 条)

  1. EVM 看不到链外 → 一切外部数据需 Oracle

    • "用 chainlink 在 Solidity 调一个 https://api.binance.com" 这种事不可能——EVM 没网络栈
    • Chainlink Functions / API3 等"任意外部数据"方案本质上也是同一架构:链下节点拉数据 + 链上多签验证
    • 这条硬约束的来源是 07 §4.1 确定性 + 03 §4.5 state root 一致性——每个节点必须独立算出同结果。 如果合约能联网,节点 A 在北京、节点 B 在纽约采到不同数 → state 不一致 → 共识崩溃
  2. 三种主流架构(push / pull / TWAP)

    维度 Push (Chainlink) Pull (Pyth) TWAP (Uni V3)
    谁推 节点周期推 节点签名挂消息板 任何 swap 自动写
    读延迟 高(heartbeat 间隔) 低(毫秒级,但要拉) 中(窗口长度)
    读 gas 极低(一次 SLOAD) 中(验签 ~30k gas) 低(一次 view 调用)
    外部依赖 Chainlink 节点 Pyth 网络
    攻击 节点共谋(极贵) 砸偏窗口(贵)
    适用 主流币 + 大池 低延迟衍生品 V3 深池
  3. 聚合 + 多源是核心抗操纵机制

    • N 个节点取中位数:单点撒谎 → 被剔除
    • 节点质押 + slashing:撒谎成本 > 撒谎收益(理论上)
    • deviation threshold:小波动不推 → 减少链上 gas 与噪声
    • 多源喂价(如 Chainlink 自身就跑多个 CEX/DEX 节点聚合)→ 不依赖某一交易所
  4. "用 spot price 当 oracle"是 DeFi 头号自杀方式

    • 闪电贷可以无本金把一个浅池价砸偏 1000%(13 待写)
    • 用 V2 pair 单次 spot price 当 oracle → 一次 tx 内砸偏 + 用错价借/清算 + 还原 → 协议被掏空
    • 历史血案(§1.8)的共同点都是这条
    • 正确做法:用 Chainlink 喂价 / Uni V3 多窗口 TWAP / 多源混合,永远不要用一个浅池即时价
  5. 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(不可行)
  • 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. 自检问题(答得出才算过)

  1. 为什么 EVM 不能直接调外部 API?这跟 03 / 07 哪条性质冲突?
  2. Push / Pull / TWAP 三种 oracle 模式各自的优缺点、典型项目、读取 gas、攻击面?
  3. Chainlink 的 deviation threshold 和 heartbeat 各自起什么作用?为什么不每秒推一次?
  4. 写出"使用 spot price 当 oracle 被闪电贷攻击"的攻击路径(5 步)。为什么用 TWAP 能挡?
  5. Staleness check 是什么?没做它的合约可能被怎样利用?
  6. 我们 LiquidationBot.t.solvm.mockCall 在 fork 里 work、主网不 work,原理是什么?
  7. AaveOracle、Chainlink AggregatorV3、Pyth 三者的关系?AaveOracle 自己产价吗?
  8. 历史上 Mango Markets / Bonq / Cream 的 oracle 攻击共同点是什么?设计协议时怎么避免?

── Q&A / 更正记录区(按日期追加)──

规范见 README.md §0。质疑/反馈/纠错按日期累积,不抹错误轨迹。

  • (2026-05-21 建立。暂无 Q&A。)