китаев.tech

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.

Создайте проект:

bash
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:

typescript
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}`);

Запустите:

bash
npx tsx src/read-eth-usd.ts

Скрипт выведет примерно такой результат:

text
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:

solidity
// 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.

Частые ошибки

Игнорировать decimalsanswer не является обычным числом с плавающей точкой. Масштаб берите из decimals().

Не проверять stale price → старый answer может быть формально валидным, но опасным для borrow, liquidation или mint logic.

Не проверять answer <= 0 → price feed должен вернуть положительную цену. Ноль или отрицательное значение нужно отклонять.

Использовать feed не той сети → адреса отличаются между Ethereum, Arbitrum, Base, Polygon и тестовыми сетями.


Что дальше

Материалы китаev.tech публикуются в образовательных целях и не являются инвестиционной рекомендацией. Примеры кода и описания протоколов — для обучения; использование в продакшне на ваш собственный риск. Дисклеймер и политика конфиденциальности.