Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.ifalabs.com/llms.txt

Use this file to discover all available pages before exploring further.

Stale price data is one of the most common — and most underestimated — risks in oracle-dependent protocols. A price feed that hasn’t updated recently may no longer reflect market reality. For stablecoins, even a small undetected depeg can trigger cascading liquidations, enable arbitrage at protocol expense, or silently under-collateralize positions. This page covers why staleness happens, how to detect it reliably, and how to build protocols that respond correctly when it occurs.

Why Staleness Happens

IFÁ Labs uses a hybrid deviation + time trigger model. A price update is pushed on-chain when either:
  • The aggregated price deviates beyond the configured threshold from the last on-chain value, or
  • A maximum time interval has elapsed since the last update
This means during periods of market stability — when a stablecoin is sitting tightly at its peg — updates are intentionally infrequent. The oracle is working correctly; it simply has nothing meaningful to report. Staleness is a problem in two scenarios:
  1. Your staleness threshold is too tight for the asset’s natural update cadence. You’re rejecting valid, accurate prices because your protocol expects more frequent updates than the oracle provides.
  2. A genuine delay has occurred — a relayer issue, source outage, or network congestion has prevented a timely update. The price may be stale and potentially inaccurate.
Handling both scenarios correctly requires different responses.

Detecting Staleness

The lastUpdateTime field returned by getAssetInfo is your signal. Compare it against block.timestamp to determine price age.
function _isFresh(uint256 lastUpdateTime, uint256 maxAge)
    internal
    view
    returns (bool)
{
    return block.timestamp - lastUpdateTime <= maxAge;
}
Always use block.timestamp for this comparison — never an off-chain timestamp passed as a parameter, which can be manipulated.

Staleness Check Patterns

Pattern 1: Hard Revert

The simplest and safest approach. If the price is stale, revert. The transaction fails cleanly and no logic executes against potentially outdated data.
uint256 public constant MAX_PRICE_AGE = 3600; // 1 hour

function _getFreshPrice(bytes32 assetId)
    internal
    view
    returns (int256 price, int8 decimal)
{
    (IIfaPriceFeed.PriceFeed memory info, bool exists) =
        ORACLE.getAssetInfo(assetId);

    require(exists, "IFA: asset not supported");
    require(
        block.timestamp - info.lastUpdateTime <= MAX_PRICE_AGE,
        "IFA: price feed is stale"
    );

    return (info.price, info.decimal);
}
Best for: Lending protocols, liquidation engines, minting functions — any high-stakes operation where proceeding with stale data is worse than failing.

Pattern 2: Fallback to Secondary Oracle

If the primary feed is stale, attempt a secondary oracle before reverting. This maximises uptime without compromising safety.
function _getPriceWithFallback(bytes32 assetId)
    internal
    view
    returns (int256 price, int8 decimal)
{
    (IIfaPriceFeed.PriceFeed memory info, bool exists) =
        ORACLE.getAssetInfo(assetId);

    if (exists && block.timestamp - info.lastUpdateTime <= MAX_PRICE_AGE) {
        return (info.price, info.decimal);
    }

    // Primary stale or missing — attempt fallback
    (int256 fallbackPrice, bool fallbackFresh) = _getSecondaryOraclePrice(assetId);
    require(fallbackFresh, "IFA: all price sources stale");

    return (fallbackPrice, -18);
}
Best for: Protocols that need high availability and have a secondary oracle integration available. See Building Fallback Strategies for a complete implementation.

Pattern 3: Protocol Pause on Staleness

For critical protocols, stale data should trigger an automatic pause on sensitive operations — borrowing, liquidations, minting — while allowing safe operations like withdrawals to continue.
bool public paused;
address public guardian;

modifier whenPriceFresh(bytes32 assetId) {
    (IIfaPriceFeed.PriceFeed memory info, bool exists) =
        ORACLE.getAssetInfo(assetId);

    if (!exists || block.timestamp - info.lastUpdateTime > MAX_PRICE_AGE) {
        paused = true;
        emit ProtocolPaused(assetId, block.timestamp);
        revert("IFA: price stale — protocol paused");
    }
    _;
}

function borrow(bytes32 collateralAssetId, uint256 amount)
    external
    whenPriceFresh(collateralAssetId)
{
    // Borrow logic — only executes with a fresh price
}
Best for: Lending and borrowing protocols where a stale price during liquidation could cause significant user harm.

Pattern 4: Cached Price with Extended Tolerance

For lower-stakes operations — dashboards, analytics views, non-critical displays — cache the last known good price and serve it with a wider tolerance window.
struct CachedPrice {
    int256   price;
    int8     decimal;
    uint256  cachedAt;
}

mapping(bytes32 => CachedPrice) public priceCache;

uint256 public constant LIVE_MAX_AGE   = 3600;   // 1 hour — for critical paths
uint256 public constant CACHE_MAX_AGE  = 86400;  // 24 hours — for non-critical reads

function updateCache(bytes32 assetId) external {
    (IIfaPriceFeed.PriceFeed memory info, bool exists) =
        ORACLE.getAssetInfo(assetId);

    require(exists, "IFA: asset not supported");
    require(
        block.timestamp - info.lastUpdateTime <= LIVE_MAX_AGE,
        "IFA: source price too stale to cache"
    );

    priceCache[assetId] = CachedPrice({
        price:    info.price,
        decimal:  info.decimal,
        cachedAt: block.timestamp
    });
}

function getCachedPrice(bytes32 assetId)
    external
    view
    returns (int256 price, int8 decimal)
{
    CachedPrice memory cached = priceCache[assetId];
    require(
        block.timestamp - cached.cachedAt <= CACHE_MAX_AGE,
        "IFA: cached price expired"
    );
    return (cached.price, cached.decimal);
}
Never use cached prices for liquidation logic, collateral valuation, or any operation where an inaccurate price causes direct financial harm to users. Caching is appropriate only for read-only or display contexts.

Per-Asset Staleness Thresholds

Different assets have different update cadences. A single global threshold applied across all feeds will either be too tight for some assets or too loose for others. Use a mapping to manage per-asset configuration:
mapping(bytes32 => uint256) public maxPriceAge;

constructor() {
    // Tighter for high-stakes global stablecoins
    maxPriceAge[USDT_ASSET_ID] = 3600;   // 1 hour
    maxPriceAge[USDC_ASSET_ID] = 3600;   // 1 hour

    // Slightly wider for emerging market stablecoins
    maxPriceAge[CNGN_ASSET_ID] = 7200;   // 2 hours
    maxPriceAge[ZARP_ASSET_ID] = 7200;   // 2 hours
    maxPriceAge[BRZ_ASSET_ID]  = 7200;   // 2 hours
}

function _getFreshPriceForAsset(bytes32 assetId)
    internal
    view
    returns (int256 price, int8 decimal)
{
    (IIfaPriceFeed.PriceFeed memory info, bool exists) =
        ORACLE.getAssetInfo(assetId);

    require(exists, "IFA: asset not supported");
    require(
        block.timestamp - info.lastUpdateTime <= maxPriceAge[assetId],
        "IFA: price feed is stale"
    );

    return (info.price, info.decimal);
}

Diagnosing Staleness in Production

If your protocol is hitting staleness reverts unexpectedly, work through this checklist before adjusting thresholds:
1

Check the last update time on-chain

Go to the contract on Basescan → Read ContractgetAssetInfo → enter the asset ID. Check lastUpdateTime against the current block timestamp. Calculate the actual age in seconds.
2

Check the PriceUpdated event log

On Basescan → Events tab → filter by PriceUpdated and your asset ID. Look at the timestamp of the most recent event. This tells you the real update cadence for that asset.
3

Compare against your threshold

If the actual update interval is consistently longer than your MAX_PRICE_AGE, your threshold is too tight for this asset’s natural cadence. Widen it to match observed behaviour.
4

Check if all assets are affected

If every feed is stale simultaneously, the issue is likely a relayer delay rather than a per-asset problem. Monitor the IFÁ Labs Telegram or X for any announced incidents.
5

Report prolonged staleness

If a feed remains stale for more than 12 hours with no announcement, report it via support@ifalabs.com or the Telegram.

Best Practices Summary

  • Fail closed. Revert on stale data in critical paths. Never proceed with a potentially inaccurate price because it’s convenient.
  • Use per-asset thresholds. A single global MAX_PRICE_AGE doesn’t reflect the different update cadences of different assets.
  • Start wide, tighten with data. Deploy with a 2-hour threshold, observe real update patterns in production, and tighten from there.
  • Never adjust thresholds under pressure. If your protocol is hitting staleness reverts during a market event, that’s the system working correctly — not a bug to patch around.
  • Monitor off-chain. On-chain checks protect your contracts. Off-chain monitoring alerts your team before users are affected. See Running Price Monitoring.

Next Steps

Working with Asset IDs

Understand how asset IDs are generated and managed.

Building Fallback Strategies

Implement multi-oracle fallback for maximum protocol resilience.