FlareForge
Oracle LabNetworkAgentsFDCSandboxDocsAbout
API Docs

Guide, Oracle

Read FTSO Fast Updates in Solidity: sub-block oracle prices on Flare

Block-latency FTSOv2 publishes new values every voting round (about 90 seconds). Fast Updates is the layer on top that lets a weighted subset of voters commit incremental price deltas every block, so the on-chain value moves continuously instead of stepping at round boundaries. For consumers, that means the freshest oracle read on Flare, typically under two seconds old. The trade-off is a small fetch fee paid in native FLR and a tighter feed set than Scaling.

The three FTSO read paths, side by side

PathLatencyFeed countCost
Block-latency90s (voting round)~30 anchor pairsFree read
Fast Updates<2s (per block)Subset of anchorsFetch fee in FLR
Scaling90s + Merkle branch1000+ feedsCaller ships the proof

Reach for Fast Updates when the price moves matter between rounds: liquidation engines, perpetuals funding, AMM rebalance triggers. Stay on block-latency if a 90-second step is acceptable and you want the reads to be free.

Solidity: read the current value

The protocol splits the read into two contracts. FastUpdatesConfiguration maps a feed ID (same bytes21 encoding as block-latency) to a compact uint256 index. FastUpdater holds the current delta state and returns feed values for a batch of indices. You cache the index at construction time, then read in hot paths.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {ContractRegistry} from
    "@flarenetwork/flare-periphery-contracts/flare/ContractRegistry.sol";
import {IFastUpdater} from
    "@flarenetwork/flare-periphery-contracts/flare/IFastUpdater.sol";
import {IFastUpdatesConfiguration} from
    "@flarenetwork/flare-periphery-contracts/flare/IFastUpdatesConfiguration.sol";

contract FastUpdatesReader {
    uint256 private immutable FEED_INDEX;

    // Pass a bytes21 feed id in the constructor and resolve the index once.
    // FLR/USD on mainnet is 0x01464c522f55534400000000000000000000000000.
    constructor(bytes21 feedId) {
        IFastUpdatesConfiguration config =
            ContractRegistry.getFastUpdatesConfiguration();
        FEED_INDEX = config.getFeedIndex(feedId);
    }

    function currentPrice()
        external
        payable
        returns (uint256 value, int8 decimals, uint64 timestamp)
    {
        IFastUpdater updater = ContractRegistry.getFastUpdater();

        uint256[] memory indices = new uint256[](1);
        indices[0] = FEED_INDEX;

        (uint256[] memory values, int8[] memory decs, uint64 ts) =
            updater.fetchCurrentFeeds{value: msg.value}(indices);

        return (values[0], decs[0], ts);
    }
}

Three things to notice. The function is payable because fetchCurrentFeeds charges a per-feed fetch fee that the caller forwards as msg.value. The index is resolved once in the constructor because configuration changes are rare and the getter costs gas. And the timestamp is returned from the updater, not taken from block.timestamp, so you can tell how fresh the value actually is.

How the fetch fee works

Fast Updates is one of the few oracle paths on Flare where reads cost the caller something, because every read triggers a payout to the voter subset that committed the delta. The fee is paid in native FLR, set by governance, and queryable at read time from the FeeCalculator:

import {IFeeCalculator} from
    "@flarenetwork/flare-periphery-contracts/flare/IFeeCalculator.sol";

function fetchFee(bytes21 feedId) external view returns (uint256) {
    IFeeCalculator calculator = ContractRegistry.getFeeCalculator();
    bytes21[] memory ids = new bytes21[](1);
    ids[0] = feedId;
    return calculator.calculateFeeByIds(ids);
}

Quote the fee at the top of your external function, compare it against msg.value, revert early if short. Refund any surplus at the end to stay caller-friendly. Ballpark the fee in your frontend so users see the full cost of the action before signing.

Off-chain: read without paying

The fetch fee only applies when you call fetchCurrentFeeds as a transaction. For monitoring, display, or backend workers, call it as eth_call and the fee is not charged. Any full node or RPC provider will do.

import { createPublicClient, http, parseAbi } from "viem";
import { flare } from "viem/chains";

// Replace with your deployed reader or call FastUpdater directly
const FAST_UPDATER = "0x...";  // ContractRegistry.getFastUpdater() on mainnet

const abi = parseAbi([
  "function fetchCurrentFeeds(uint256[]) external payable returns (uint256[], int8[], uint64)",
]);

async function readFlrUsd(flrUsdIndex: number) {
  const client = createPublicClient({ chain: flare, transport: http() });
  const [values, decimals, ts] = await client.readContract({
    address: FAST_UPDATER,
    abi,
    functionName: "fetchCurrentFeeds",
    args: [[BigInt(flrUsdIndex)]],
  });
  const price = Number(values[0]) / 10 ** decimals[0];
  const age = Math.floor(Date.now() / 1000) - Number(ts);
  return { price, ageSeconds: age };
}

Fetch fee is bypassed because eth_call never moves value. Same contract, same math, zero cost, one RPC round trip. This is the read path every dashboard should use.

Python: pull the latest block-by-block

Same idea in Python with web3.py, useful for bots and indexers.

from web3 import Web3

FLARE_RPC   = "https://flare-api.flare.network/ext/C/rpc"
FAST_UPDATER = Web3.to_checksum_address("0x...")  # getFastUpdater()
FEED_INDEX   = 0  # resolved once via FastUpdatesConfiguration.getFeedIndex

ABI = [{
    "name": "fetchCurrentFeeds",
    "type": "function",
    "stateMutability": "payable",
    "inputs":  [{"name": "indices", "type": "uint256[]"}],
    "outputs": [
        {"name": "values",    "type": "uint256[]"},
        {"name": "decimals",  "type": "int8[]"},
        {"name": "timestamp", "type": "uint64"},
    ],
}]

w3 = Web3(Web3.HTTPProvider(FLARE_RPC))
updater = w3.eth.contract(address=FAST_UPDATER, abi=ABI)

# eth_call bypasses the fetch fee. value=0 is fine for read-only.
values, decimals, ts = updater.functions.fetchCurrentFeeds([FEED_INDEX]).call()
price = values[0] / (10 ** decimals[0])
age   = w3.eth.get_block("latest")["timestamp"] - ts
print(f"FLR/USD = {price:.6f} (age {age}s)")

Things that bite on the first integration

  • Hardcoding the index. Indices are assigned by FastUpdatesConfiguration and can be re-packed when the feed set changes. A constant in your contract is a footgun. Either resolve in the constructor from the feed ID, or expose a setter that an owner can call after governance reshuffles.
  • Forgetting the fetch fee on a state-changing call. fetchCurrentFeeds reverts if msg.value is below the configured fee. Quote the fee first, forward enough, refund surplus. An eth_call is free; a transaction is not.
  • Treating the timestamp as a trust signal. The timestamp is the block in which the most recent delta landed. If liveness drops, the timestamp stops advancing. Sanity-check against block.timestamp - ts < MAX_AGE before using the value in a liquidation path.
  • Expecting every anchor feed to be available. Fast Updates covers the subset of anchor feeds that the voter committee actively publishes deltas for. If a feed ID resolves to a sentinel index or reverts with FeedNotSupported, fall back to block-latency for that pair.

Related on the site

  • FTSOv2 in 10 lines, the cheaper block-latency path when 90s is fine.
  • FTSO Scaling with Merkle proofs, for the long tail of feeds outside the anchor set.
  • Oracle Lab, the live catalog of every block-latency feed (value, history, 24h sparkline).

← Previous guide

Read FTSOv2 prices in 10 lines of Solidity

Next guide →

Read FTSO Scaling feeds with Merkle proofs in Solidity