Chainlink Price Feed
Как читать цену из Chainlink Price Feed через latestRoundData, учитывать decimals и проверять свежесть oracle-данных в ethers v6.
Price Feed даёт цену из oracle
Chainlink Price Feed — это on-chain контракт, который возвращает цену актива из сети оракулов.
В DeFi такие feed используют для collateral, borrow limits, liquidation checks и risk logic. В отличие от spot-цены AMM, feed не зависит от одной сделки в одном пуле.
Читаем ETH/USD feed
Пример читает реальный Chainlink ETH/USD Price Feed в Ethereum mainnet.
Создайте проект:
mkdir chainlink-price-feed-example
cd chainlink-price-feed-example
npm init -y
npm pkg set type="module"
npm install ethers tsx
mkdir srcСоздайте файл src/read-eth-usd.ts:
import { ethers } from "ethers";
const RPC_URL = "https://ethereum-rpc.publicnode.com";
const ETH_USD_FEED = "0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419";
const MAX_PRICE_AGE_SECONDS = 60 * 60;
const AGGREGATOR_V3_ABI = [
"function decimals() external view returns (uint8)",
"function description() external view returns (string)",
"function latestRoundData() external view returns (uint80 roundId,int256 answer,uint256 startedAt,uint256 updatedAt,uint80 answeredInRound)",
];
const provider = new ethers.JsonRpcProvider(RPC_URL);
const feed = new ethers.Contract(ETH_USD_FEED, AGGREGATOR_V3_ABI, provider);
const [decimals, description] = await Promise.all([
feed.decimals(),
feed.description(),
]);
const [roundId, answer, startedAt, updatedAt] = await feed.latestRoundData();
const now = Math.floor(Date.now() / 1000);
if (answer <= 0n) {
throw new Error("Invalid price: answer must be positive");
}
if (updatedAt === 0n) {
throw new Error("Invalid price: round is not complete");
}
const priceAge = BigInt(now) - updatedAt;
if (priceAge > BigInt(MAX_PRICE_AGE_SECONDS)) {
throw new Error(`Stale price: updated ${priceAge.toString()} seconds ago`);
}
const price = ethers.formatUnits(answer, decimals);
console.log(`Feed: ${description}`);
console.log(`Round ID: ${roundId.toString()}`);
console.log(`Started at: ${new Date(Number(startedAt) * 1000).toISOString()}`);
console.log(`Updated at: ${new Date(Number(updatedAt) * 1000).toISOString()}`);
console.log(`ETH/USD: ${price}`);Запустите:
npx tsx src/read-eth-usd.tsСкрипт выведет примерно такой результат:
Feed: ETH / USD
Round ID: 110680464442257336946
Started at: 2026-06-20T16:50:47.000Z
Updated at: 2026-06-20T16:50:47.000Z
ETH/USD: 3542.18420300Числа будут отличаться: feed обновляется по правилам Chainlink oracle network.
Как это работает
ETH_USD_FEED — proxy-адрес Chainlink ETH/USD feed в Ethereum mainnet.
decimals() — количество знаков после запятой в answer. Для многих USD feed это 8, но не нужно хардкодить.
description() — человекочитаемое имя feed, например ETH / USD.
latestRoundData() — возвращает последнюю round-запись: roundId, answer, startedAt, updatedAt, answeredInRound.
answer — цена без десятичной точки. Если ETH/USD равен 3542.18420300, контракт может вернуть 354218420300 при decimals = 8.
updatedAt — timestamp последнего обновления. По нему проверяют, не устарела ли цена.
answeredInRound — legacy-поле. В новых интеграциях обычно проверяют answer, updatedAt и freshness policy.
Проверка в Solidity
Если цена нужна контракту, проверяйте её прямо on-chain:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface AggregatorV3Interface {
function decimals() external view returns (uint8);
function latestRoundData()
external
view
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
);
}
contract EthUsdPriceReader {
AggregatorV3Interface public immutable feed;
uint256 public immutable maxPriceAge;
error InvalidPrice();
error StalePrice();
constructor(address feedAddress, uint256 maxPriceAgeSeconds) {
feed = AggregatorV3Interface(feedAddress);
maxPriceAge = maxPriceAgeSeconds;
}
function latestEthUsd() external view returns (uint256 price, uint8 decimals) {
(, int256 answer,, uint256 updatedAt,) = feed.latestRoundData();
if (answer <= 0) {
revert InvalidPrice();
}
if (updatedAt == 0 || block.timestamp - updatedAt > maxPriceAge) {
revert StalePrice();
}
return (uint256(answer), feed.decimals());
}
}Для ETH/USD mainnet передайте feedAddress = 0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419, maxPriceAgeSeconds = 3600.
Частые ошибки
Игнорировать decimals → answer не является обычным числом с плавающей точкой. Масштаб берите из decimals().
Не проверять stale price → старый answer может быть формально валидным, но опасным для borrow, liquidation или mint logic.
Не проверять answer <= 0 → price feed должен вернуть положительную цену. Ноль или отрицательное значение нужно отклонять.
Использовать feed не той сети → адреса отличаются между Ethereum, Arbitrum, Base, Polygon и тестовыми сетями.
Что дальше
- Oracle manipulation — почему spot-цена AMM опасна для lending и collateral logic
- Цена токена из Uniswap V3 через Quoter — сравнить oracle feed со spot-quote из DEX
- Borrow из Aave V3 — увидеть, где lending-протокол зависит от oracle-цен