DeFi Oracleの読み方|decimals・staleness・TWAP・L2停止を検証
DeFiの価格をfeed address、decimals、round、updatedAt、L2 sequencer、TWAP、fallbackへ分解し、利用可否と停止条件をFoundryで検証します。

DeFiの画面に「1 ETH = 2,500 USD」と出ていても、contractが同じ価格を安全に使えるとは限りません。価格feedのaddressや通貨ペアを取り違えた、8桁の値を18桁として扱った、古いroundを最新とみなした、L2 sequencer復旧直後に清算へ使った、といった別々の失敗が同じ「価格がおかしい」に見えます。
この記事は、lending、vault、derivatives、stablecoinへ価格を組み込む開発者と、そのincidentを調べる人向けです。chain → feed identity → round → 時刻 → L2稼働 → 単位変換 → 範囲 → 別sourceの順に確認し、価格を「利用可能」「古い」「異常」「未確定」へ分けられる状態を目指します。
結論は、latestRoundData()が返っただけでは不足です。application側がfeed identity、decimals、updatedAt、L2 grace period、許容範囲、fallback方針を明示し、DEX価格ならspotと観測windowを分けて判断します。
検証にはSolidity 0.8.36、Cancun EVM、Foundry 1.8.0のlocal contractを使いました。公開dataはEthereum mainnetの過去blockへread-onlyで固定した値だけを再利用し、wallet、秘密鍵、署名、公開transaction、清算、実資産操作は行いません。
最初に価格ではなく読み取り条件を固定する
調査を始めるときは、USD表示をメモする前に次の項目を一組で残します。
| 項目 | 確認する内容 | 省くと起きる混同 |
|---|---|---|
| chain | chain ID、network、L1 / L2 | 別chainの同じaddressを読む |
| feed identity | proxy address、description、base / quote | ETH/USDとBTC/USD、USD建てとETH建てを混ぜる |
| precision | decimals() |
250019228748を2,500.19228748以外に変換する |
| round | roundId、answer、startedAt、updatedAt |
値だけを最新とみなす |
| block | number、hash、timestamp | 複数readが別時点になる |
| policy | max age、range、L2 grace、fallback | 失敗時に止めるか代替値を使うか決まらない |
Chainlink Data Feeds API Referenceは、consumerがproxy addressをAggregatorV3Interfaceとして読む形を示しています。decimals()はanswerの精度、description()はfeedの説明、latestRoundData()はroundと時刻を返します。
addressをUIや記憶から転記せず、対象chainの公式feed一覧とdeployed bytecodeを照合します。description()は有用な確認項目ですが、文字列だけで正しいproxyを発見する仕組みではありません。公式addressを先に固定し、descriptionを二重確認に使う順序です。
decimalsと通貨ペアを確認してから正規化する
Ethereum mainnet ETH/USD feedの固定観測では、raw answerが250019228748、decimals()が8でした。
250019228748 / 10^8 = 2500.19228748 USD per ETH
ここで1e18形式へ揃えるなら、8桁から18桁へ10^(18 - 8)を掛けます。ただし、feedごとに8桁だと決め打ちしてはいけません。まず実際のdecimals()がapplication設定と一致することを確認し、指数とanswerの範囲からoverflowしないことを検査します。
uint8 actualDecimals = feed.decimals();
if (actualDecimals != expectedDecimals) revert WrongDecimals();
(uint80 roundId, int256 answer,, uint256 updatedAt, uint80 answeredInRound) =
feed.latestRoundData();
if (answer <= 0) revert NonPositiveAnswer();
if (updatedAt == 0 || updatedAt > block.timestamp) revert InvalidTimestamp();
if (block.timestamp - updatedAt > maxStaleness) revert StalePrice();
uint256 priceE18 = uint256(answer) * 10 ** (18 - actualDecimals);
base / quoteの向きも別に確認します。ETH/USDのanswerは1 ETHあたりのUSDです。USD/ETHとして使うなら逆数計算が必要で、2つのfeedから派生pairを作る場合は両方のdecimals、freshness、符号、丸めを独立して確認します。
roundが返ってもupdatedAtが古ければ止める
latestRoundData()は次の5項目を返します。
| field | 読み方 | 判定上の注意 |
|---|---|---|
roundId |
proxyが返すround ID | 価格の大小や新しさをIDだけで決めない |
answer |
feed固有precisionの値 | zero、negative、範囲外をapplication側で拒否する |
startedAt |
round開始時刻 | price freshnessには通常updatedAtを使う |
updatedAt |
roundのanswer更新時刻 | 0、未来、max age超過を拒否する |
answeredInRound |
過去のround完了表現 | 現行API資料ではdeprecated。単独のfreshness証拠にしない |
現在のAPI資料はansweredInRoundをdeprecatedとしています。本記事のlocal検証では、legacy tupleの不整合を見落とさないためansweredInRound < roundIdも拒否しますが、主要なfreshness判定はupdatedAtとapplicationのmax ageです。
heartbeatとdeviation thresholdはstaleness limitそのものではない
price feedは、価格変化がdeviation thresholdを超えた場合やheartbeat時間が経過した場合など、feed固有のtriggerで更新されます。値はasset、network、feed type、時点で異なります。
consumerのmaxStalenessは、applicationがそのfeedを何秒まで利用できると判断するかという別のpolicyです。公式feed pageの現在条件、marketの特性、protocolの清算や停止設計を確認し、「heartbeatが1時間だから常に3,600秒までは安全」のように同一視しません。
local検証ではmax ageを3,600秒に固定し、ちょうど境界までは受理、3,601秒前のupdatedAtはStalePriceで停止させました。この数値は検証条件であり、すべてのfeedへの推奨値ではありません。
zero・negative・outlierを別のfailureとして扱う
answerの異常は一つにまとめず、原因とpolicyを分けます。
| observed state | local result | 判断 |
|---|---|---|
answer == 0 |
NonPositiveAnswer |
0を無料・無価値と解釈せず停止 |
answer < 0 |
NonPositiveAnswer |
price用途では拒否。feed typeを再確認 |
decimals != 8 |
WrongDecimals |
古い換算式を続行しない |
updatedAt == 0 |
MissingTimestamp |
未初期化・不完全なdataとして停止 |
updatedAt > now |
FutureTimestamp |
clock / chain contextを確認 |
| normalized priceが設定範囲外 | OutOfRange |
feedが返した事実と採用可否を分離 |
検証用consumerはETH/USDの許容範囲を1,000〜5,000 USDへ固定し、6,000 USDをoutlierとして拒否します。これも記事執筆時の相場予測ではなく、circuit breakerが動くことを再現する値です。実装では上限・下限の根拠、更新権限、停止後の動作をprotocolごとに決めます。
ChainlinkのAPI資料も、多くのfeedでaggregatorのminAnswerとmaxAnswerがcircuit breakerとして使われていないため、必要ならapplication側で設計するよう案内しています。
L2ではsequencer statusを価格より先に読む
L2上でsequencerが停止すると、L2上のprice feedが値を返していても、利用者が通常どおりtransactionを投入できるとは限りません。清算側だけが復旧直後に動ける状態を避けるため、price feedより先にSequencer Uptime Feedを確認します。
Chainlink L2 Sequencer Uptime Feedsの例では、answer == 0がup、answer == 1がdownです。upへ戻った後もblock.timestamp - startedAtがgrace periodを超えるまでpriceを使いません。
(, int256 status, uint256 startedAt,,) = sequencerFeed.latestRoundData();
if (status != 0) revert SequencerDown();
if (startedAt == 0 || startedAt > block.timestamp) revert InvalidSequencerRound();
if (block.timestamp - startedAt <= gracePeriod) revert GracePeriodNotOver();
公式資料のsampleはgrace periodを3,600秒としています。local検証も同じ値を使い、down中、復旧から3,600秒ちょうど、3,600秒超過を別testにしました。実際のprotocolでは対象L2のuptime feed address、outage handling、withdrawal path、利用者保護を確認します。
Arbitrumではfeed未初期化時にstartedAt == 0となる場合があることも公式資料に明記されています。0を「十分昔からup」と計算せず、未確定として停止します。
spotとTWAPは同じpoolでも見ている時間が違う
DEX spotは現在reserveまたはcurrent tickから得る瞬間値です。TWAPは過去windowのcumulative値の差から平均を求めます。TWAPにしただけで操作不能になるわけではありませんが、短時間だけ価格を動かす操作の影響をwindow全体へ薄められます。
local AMM観測では、最初の900秒を2,000、その後60秒を4,000としました。
(2,000 × 900秒 + 4,000 × 60秒) / 960秒 = 2,125
| read | result | 意味 |
|---|---|---|
| manipulation後のspot | 4,000 | 現在reserveをすぐ反映 |
| 60秒TWAP | 4,000 | window全体が変更後なのでspotと同じ |
| 960秒TWAP | 2,125 | 900秒分の以前の価格を含む |
| 961秒TWAP | revert | そこまで古い観測がない |
Uniswap v3のOracleLibrary.consultはsecondsAgoと0の2点をobserveへ渡し、tickCumulativeの差をwindowで割ってarithmetic mean tickを得ます。tickから得る価格は幾何平均に対応し、単純なUSD価格の算術平均とは同じではありません。
さらに、poolのobservation cardinality、oldest observation、window、in-range liquidity、base / quote decimalsを確認します。Uniswap v3 coreは最初のoracle array長が1で、追加slotは誰かがstorage costを払って増やす設計です。要求windowより古い観測がなければOLD相当でrevertし得ます。
価格変化、slippage、MEVは価格影響とminimum outputを分ける記事、Uniswap v4のPoolManagerとhook内部でoracleを読む境界はPoolManager・hooks・flash accountingの検証で確認できます。
fallbackは「何でも返す予備」ではない
primaryが古いときにfallbackへ切り替える場合も、fallbackのidentity、precision、freshness、範囲を同じように検証します。primaryとfallbackが同時に有効なのに大きく離れているなら、都合のよい方を選ばずcircuit breakerで停止します。
| primary | fallback | local policy |
|---|---|---|
| valid | valid・差5%以内 | primaryを採用し、差を監視 |
| stale | valid | fallbackを採用し、その事実を返す |
| valid | valid・差5%超 | SourcesDivergeで停止 |
| invalid | invalid | BothFeedsInvalidで停止 |
| sequencer down | どの状態でも | fallbackへ進まず停止 |
同じL2上のfallbackは、sequencer downを解決しません。L1 source、DEX TWAP、manual guardian、last-good priceなどを組み合わせる場合は、更新時点、chain間delay、操作耐性、authority、停止中のactionを個別に設計します。
vaultのasset/share計算とoracle評価を分けるにはERC-4626のrounding・inflation attack検証、複数RPC readを同じ時点へ固定する方法はEthereum RPCのfixed block検証を参照してください。
公開feedをblock numberとhashへ固定して読み戻す
公開観測はEthereum mainnet block 25,869,830へ固定しています。
| field | fixed result |
|---|---|
| feed | 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419 |
| pair / decimals | ETH / USD / 8 |
| block hash | 0x53d1375a96ad48b9a0f3e30152c0f01dc142fca8dce2f22ee95270a27b365294 |
| block timestamp | 2026-08-30T18:17:59Z |
| round ID | 129127208515966894439 |
| raw answer | 250019228748 |
| startedAt | 2026-08-30T17:24:51Z |
| updatedAt | 2026-08-30T17:26:11Z |
| normalized | 2,500.19228748 USD per ETH |
2つのpublic endpointでblock hashとround tupleを照合しました。updatedAtがblock timestampより約52分前でも、それだけで異常とは限りません。feedが各blockで更新されるわけではないため、対象feedのtrigger条件とapplication max ageを合わせて判断します。
この値は2026-08-30のhistorical snapshotであり、現在価格や投資判断には使えません。block固定、raw tuple、local failure結果は公開検証JSONにまとめています。
integrationとincident調査のチェックリスト
- chain IDとfeed proxy addressを公式一覧で固定した
-
description()でbase / quoteを二重確認した -
decimals()を読み、変換前後の単位を書いた -
roundId、answer、startedAt、updatedAtを同じblockで保存した - zero、negative、未来時刻、stale、overflow、範囲外を止めた
- heartbeat / deviationとapplication max ageを分けた
- L2ではsequencer answer、
startedAt、grace periodを先に確認した - DEXではspot、window、oldest observation、liquidity、token decimalsを確認した
- fallbackも同じ基準で検証し、source divergence時の停止条件を決めた
- receipt、event、price read、最終stateを同じblock identityへ結び付けた
oracleのブランド、正常なreturn、TWAPという名前だけでは、安全性も清算結果も保証できません。どのsourceを、どの時点と単位で、どの停止条件の下で使ったかを残すと、値の異常とconsumer policyの異常を分けて調査できます。
この記事には特定asset、lending market、yield商品へのdepositや取引を促す導線はありません。将来oracle、RPC、monitoring、audit serviceを紹介する場合も、広告表記と技術検証を分離します。
確認した一次情報
- Chainlink Using Data Feeds on EVM Chains確認日: 2026/08/31
- Chainlink Data Feeds API Reference確認日: 2026/08/31
- Chainlink Selecting Quality Data Feeds確認日: 2026/08/31
- Chainlink L2 Sequencer Uptime Feeds確認日: 2026/08/31
- Chainlink ETH/USD Ethereum Mainnet feed page確認日: 2026/08/31
- Uniswap v3 OracleLibrary pinned source確認日: 2026/08/31
- Uniswap v3 Oracle v1.0.1 source確認日: 2026/08/31
- Ethereum JSON-RPC API確認日: 2026/08/31
- EIP-1898 blockHash in JSON-RPC確認日: 2026/08/31
- Foundry v1.8.0確認日: 2026/08/31



