3MIKAN
仮想通貨直コン

DeFi Oracleの読み方|decimals・staleness・TWAP・L2停止を検証

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

3MIKANのブランドキャラクターが複数の価格計器を時刻・校正・L2稼働状態と照合し、一つの利用可否判定へまとめる記事画像

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、decimalsupdatedAt、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 roundIdanswerstartedAtupdatedAt 値だけを最新とみなす
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を二重確認に使う順序です。

価格sourceからfeed identity、round時刻、L2稼働、単位変換、範囲判定、consumer actionまでを別々の検査台として並べた概念図
価格値へ到達する前に、source、identity、時刻、chain固有条件を順番に通します。

decimalsと通貨ペアを確認してから正規化する

Ethereum mainnet ETH/USD feedの固定観測では、raw answerが250019228748decimals()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秒前のupdatedAtStalePriceで停止させました。この数値は検証条件であり、すべての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のminAnswermaxAnswerが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」と計算せず、未確定として停止します。

L2 sequencerのdown・復旧graceと、短時間に動くspot価格を長い観測windowで平均するTWAPを二つの時間軸で比較した概念図
L2の利用可否と価格の観測windowは、どちらも時間を使いますが別の判定です。

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.consultsecondsAgoと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()を読み、変換前後の単位を書いた
  • roundIdanswerstartedAtupdatedAtを同じ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を紹介する場合も、広告表記と技術検証を分離します。

確認した一次情報