// SPDX-License-Identifier: MIT
pragma solidity ^0.8.29;
import "ifapricefeed-interface/IIfaPriceFeed.sol";
/// @title IfaIntegrationReference
/// @notice Production-ready reference contract for IFÁ Labs oracle integration.
/// Demonstrates all recommended patterns for consuming stablecoin price feeds.
/// @dev Adapt this contract to your protocol's needs. Do not deploy as-is
/// without reviewing all configuration constants for your specific use case.
contract IfaIntegrationReference {
// -------------------------------------------------------------------------
// Oracle Configuration
// -------------------------------------------------------------------------
/// @dev Oracle contract — Base Mainnet
IIfaPriceFeed public constant ORACLE =
IIfaPriceFeed(0xA9F17344689C2c2328F94464998db1d3e35B80dC);
// Asset IDs — keccak256(abi.encodePacked("SYMBOL/USD"))
bytes32 public constant USDT_ASSET_ID =
0x6ca0cef6107263f3b09a51448617b659278cff744f0e702c24a2f88c91e65a0d;
bytes32 public constant USDC_ASSET_ID =
0xf989296bde68043d307a2bc0e59de3445defc5f292eb390b80d78162c8a6b13d;
bytes32 public constant CNGN_ASSET_ID =
0x83a18c73cf75a028a24b79cbedb3b8d8ba363b748a3210ddbcaa95eec3b87b3a;
bytes32 public constant ZARP_ASSET_ID =
0x12373a3b1c4827c84bf6d7b11df100442695d0abfdb7a20d30a41d67d58e75a8;
bytes32 public constant BRZ_ASSET_ID =
0xbc60b55b031dce1ee5679098bf2f35d66a94a566124e2b233324d2bafcc6d5b5;
bytes32 public constant ETH_ASSET_ID =
0x8c3fb07cab369fe230ca4e45d095f796c4c1a30131f1799766d4fec5ee1325c0;
// -------------------------------------------------------------------------
// Risk Parameters
// -------------------------------------------------------------------------
/// @dev Default staleness threshold — 1 hour
uint256 public constant DEFAULT_MAX_AGE = 3600;
/// @dev Extended threshold for emerging market stablecoins — 2 hours
uint256 public constant EMERGING_MAX_AGE = 7200;
/// @dev Maximum acceptable peg deviation — 5% expressed in basis points
uint256 public constant MAX_DEVIATION_BPS = 500;
/// @dev Expected peg value for USD stablecoins scaled to 18 decimals
int256 public constant EXPECTED_PEG = 1e18;
// -------------------------------------------------------------------------
// Protocol State
// -------------------------------------------------------------------------
/// @dev Emergency pause — set automatically if oracle health check fails
bool public paused;
/// @dev Address authorised to unpause the protocol
address public guardian;
// -------------------------------------------------------------------------
// Price Cache
// -------------------------------------------------------------------------
/// @dev Packed struct — price in slot 0, timestamp + decimal share slot 1
struct CachedPrice {
int256 price; // 32 bytes — slot 0
uint64 cachedAt; // 8 bytes ─┐
int8 decimal; // 1 byte ─┘ slot 1 (packed)
}
mapping(bytes32 => CachedPrice) private _priceCache;
/// @dev Cache TTL for non-critical reads — 5 minutes
uint256 public constant CACHE_TTL = 300;
// -------------------------------------------------------------------------
// Per-Asset Staleness Thresholds
// -------------------------------------------------------------------------
mapping(bytes32 => uint256) public maxPriceAge;
// -------------------------------------------------------------------------
// Events
// -------------------------------------------------------------------------
event PriceRead(bytes32 indexed assetId, int256 price, uint256 timestamp);
event ProtocolPaused(bytes32 indexed assetId, uint256 timestamp);
event ProtocolUnpaused(address indexed guardian, uint256 timestamp);
event CacheUpdated(bytes32 indexed assetId, int256 price, uint256 cachedAt);
// -------------------------------------------------------------------------
// Errors
// -------------------------------------------------------------------------
error AssetNotSupported(bytes32 assetId);
error PriceFeedStale(bytes32 assetId, uint256 age, uint256 maxAge);
error PriceDeviationExceeded(bytes32 assetId, int256 price);
error ProtocolIsPaused();
error NotGuardian();
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(address _guardian) {
guardian = _guardian;
// Configure per-asset staleness thresholds
maxPriceAge[USDT_ASSET_ID] = DEFAULT_MAX_AGE;
maxPriceAge[USDC_ASSET_ID] = DEFAULT_MAX_AGE;
maxPriceAge[CNGN_ASSET_ID] = EMERGING_MAX_AGE;
maxPriceAge[ZARP_ASSET_ID] = EMERGING_MAX_AGE;
maxPriceAge[BRZ_ASSET_ID] = EMERGING_MAX_AGE;
maxPriceAge[ETH_ASSET_ID] = DEFAULT_MAX_AGE;
}
// -------------------------------------------------------------------------
// Modifiers
// -------------------------------------------------------------------------
/// @dev Reverts if the protocol has been paused due to an oracle failure
modifier whenNotPaused() {
if (paused) revert ProtocolIsPaused();
_;
}
/// @dev Pauses the protocol if the given asset's price feed is unhealthy
modifier whenOracleHealthy(bytes32 assetId) {
(IIfaPriceFeed.PriceFeed memory info, bool exists) =
ORACLE.getAssetInfo(assetId);
if (!exists || !_isFresh(info.lastUpdateTime, maxPriceAge[assetId])) {
paused = true;
emit ProtocolPaused(assetId, block.timestamp);
revert ProtocolIsPaused();
}
_;
}
// -------------------------------------------------------------------------
// Core Price Functions
// -------------------------------------------------------------------------
/// @notice Fetch and fully verify a single stablecoin price feed.
/// Performs existence, staleness, and peg deviation checks.
/// @param assetId The IFÁ Labs bytes32 asset identifier
/// @return price Verified scaled price value
/// @return decimal Negative scaling exponent (typically -18)
function getVerifiedPrice(bytes32 assetId)
public
view
returns (int256 price, int8 decimal)
{
(IIfaPriceFeed.PriceFeed memory info, bool exists) =
ORACLE.getAssetInfo(assetId);
if (!exists)
revert AssetNotSupported(assetId);
uint256 age = block.timestamp - info.lastUpdateTime;
uint256 maxAge = maxPriceAge[assetId] > 0
? maxPriceAge[assetId]
: DEFAULT_MAX_AGE;
if (!_isFresh(info.lastUpdateTime, maxAge))
revert PriceFeedStale(assetId, age, maxAge);
if (!_isReasonable(info.price))
revert PriceDeviationExceeded(assetId, info.price);
return (info.price, info.decimal);
}
/// @notice Fetch verified prices for multiple assets in a single call.
/// More gas-efficient than calling getVerifiedPrice N times.
/// @param assetIds Array of IFÁ Labs bytes32 asset identifiers
/// @return prices Array of verified scaled price values
/// @return decimals Array of negative scaling exponents
function getMultipleVerifiedPrices(bytes32[] calldata assetIds)
external
view
returns (int256[] memory prices, int8[] memory decimals)
{
(IIfaPriceFeed.PriceFeed[] memory infos, bool[] memory exists) =
ORACLE.getAssetsInfo(assetIds);
prices = new int256[](assetIds.length);
decimals = new int8[](assetIds.length);
for (uint256 i = 0; i < assetIds.length; i++) {
if (!exists[i])
revert AssetNotSupported(assetIds[i]);
uint256 maxAge = maxPriceAge[assetIds[i]] > 0
? maxPriceAge[assetIds[i]]
: DEFAULT_MAX_AGE;
if (!_isFresh(infos[i].lastUpdateTime, maxAge))
revert PriceFeedStale(
assetIds[i],
block.timestamp - infos[i].lastUpdateTime,
maxAge
);
if (!_isReasonable(infos[i].price))
revert PriceDeviationExceeded(assetIds[i], infos[i].price);
prices[i] = infos[i].price;
decimals[i] = infos[i].decimal;
}
}
/// @notice Get a derived cross-asset price.
/// For example: CNGN priced in USDT terms.
/// @param assetId0 The asset being priced
/// @param assetId1 The denominator asset
/// @param direction Forward or Backward pair direction
function getCrossAssetPrice(
bytes32 assetId0,
bytes32 assetId1,
IIfaPriceFeed.PairDirection direction
)
external
view
returns (IIfaPriceFeed.DerivedPair memory pair)
{
return ORACLE.getPairbyId(assetId0, assetId1, direction);
}
// -------------------------------------------------------------------------
// Cached Price Read (Non-Critical Paths Only)
// -------------------------------------------------------------------------
/// @notice Return a cached price if within TTL, otherwise fetch fresh.
/// For use in non-critical read paths only — not for liquidations,
/// collateral valuation, or minting.
/// @param assetId The IFÁ Labs bytes32 asset identifier
function getCachedPrice(bytes32 assetId)
external
returns (int256 price, int8 decimal)
{
return _getCachedPrice(assetId);
}
// -------------------------------------------------------------------------
// Fallback Price Read
// -------------------------------------------------------------------------
/// @notice Attempt to return a verified price. If the primary feed is stale,
/// fall back to the cache before reverting.
/// Implement secondary oracle logic here for maximum resilience.
/// @param assetId The IFÁ Labs bytes32 asset identifier
function getVerifiedPriceWithFallback(bytes32 assetId)
external
view
returns (int256 price, int8 decimal, bool fromFallback)
{
(IIfaPriceFeed.PriceFeed memory info, bool exists) =
ORACLE.getAssetInfo(assetId);
uint256 maxAge = maxPriceAge[assetId] > 0
? maxPriceAge[assetId]
: DEFAULT_MAX_AGE;
// Primary path — fresh, valid price available
if (
exists &&
_isFresh(info.lastUpdateTime, maxAge) &&
_isReasonable(info.price)
) {
return (info.price, info.decimal, false);
}
// Fallback path — primary stale or invalid
// Insert secondary oracle call here if available.
// Example:
// (int256 fallbackPrice, bool fallbackFresh) = secondaryOracle.getPrice(assetId);
// if (fallbackFresh) return (fallbackPrice, -18, true);
// If no fallback is available, revert cleanly
revert PriceFeedStale(
assetId,
block.timestamp - info.lastUpdateTime,
maxAge
);
}
// -------------------------------------------------------------------------
// Guardian Functions
// -------------------------------------------------------------------------
/// @notice Unpause the protocol after an oracle issue has been resolved.
/// Only callable by the guardian address.
function unpause() external {
if (msg.sender != guardian) revert NotGuardian();
paused = false;
emit ProtocolUnpaused(msg.sender, block.timestamp);
}
/// @notice Update the staleness threshold for a specific asset.
/// Only callable by the guardian address.
function setMaxPriceAge(bytes32 assetId, uint256 maxAge) external {
if (msg.sender != guardian) revert NotGuardian();
maxPriceAge[assetId] = maxAge;
}
// -------------------------------------------------------------------------
// Internal Helpers
// -------------------------------------------------------------------------
/// @dev Returns true if the price was updated within the allowed window.
function _isFresh(uint256 lastUpdateTime, uint256 maxAge)
internal
view
returns (bool)
{
return block.timestamp - lastUpdateTime <= maxAge;
}
/// @dev Returns true if the price is within ±MAX_DEVIATION_BPS of the peg.
/// Only applies to USD stablecoins. Do not use for ETH or volatile assets.
function _isReasonable(int256 price)
internal
pure
returns (bool)
{
int256 deviation = price - EXPECTED_PEG;
if (deviation < 0) deviation = -deviation;
return uint256(deviation) <= uint256(EXPECTED_PEG) * MAX_DEVIATION_BPS / 10000;
}
/// @dev Fetch from cache if within TTL, otherwise read from oracle and
/// update cache. Writes to storage — not a view function.
function _getCachedPrice(bytes32 assetId)
internal
returns (int256 price, int8 decimal)
{
CachedPrice storage cached = _priceCache[assetId];
if (
cached.cachedAt > 0 &&
block.timestamp - cached.cachedAt <= CACHE_TTL
) {
return (cached.price, cached.decimal);
}
// Cache miss — fetch fresh from oracle
(IIfaPriceFeed.PriceFeed memory info, bool exists) =
ORACLE.getAssetInfo(assetId);
if (!exists) revert AssetNotSupported(assetId);
_priceCache[assetId] = CachedPrice({
price: info.price,
cachedAt: uint64(block.timestamp),
decimal: info.decimal
});
emit CacheUpdated(assetId, info.price, block.timestamp);
return (info.price, info.decimal);
}
}