Use this file to discover all available pages before exploring further.
No oracle network is immune to delays, outages, or edge cases. A well-designed protocol anticipates these scenarios and responds correctly — not by proceeding with bad data, but by degrading gracefully to a safe state while maintaining as much functionality as possible.This page covers every fallback pattern available to protocols consuming IFÁ Labs feeds, from simple secondary oracle integration to full circuit breaker architectures.
Fallback strategies exist on a spectrum between two extremes:
MAXIMUM SAFETY MAXIMUM AVAILABILITY │ │ ▼ ▼ Hard revert Protocol pause Secondary oracle Cached price on any issue on critical feeds with consensus with wide TTL │ │ │ │ Zero risk High safety Balanced Higher risk Zero uptime Some downtime Low downtime Always up
The right position on this spectrum depends on your protocol’s risk profile. A lending protocol running liquidations should sit far left. A price display dashboard can sit far right. Most production protocols need different strategies for different operations.
Use when: Liquidations, collateral valuation, minting, settlement — any operation where executing with bad data causes direct financial harm.Trade-off: Zero tolerance for oracle issues. If the feed is stale, the function reverts — users cannot execute until the feed recovers.
When using a secondary oracle as fallback, emit an event when fallback is triggered. Off-chain monitoring should alert your team immediately — fallback usage is a signal that the primary feed has an issue requiring investigation.
For maximum price accuracy and manipulation resistance, aggregate prices from multiple independent oracles and take the median. A single manipulated or stale oracle cannot move the median significantly if the others are healthy.
Use when: High-value lending protocols, large stablecoin vaults, or any protocol where the cost of a manipulated price is significant enough to justify the additional gas and integration complexity.
Different protocol operations have different tolerance for oracle uncertainty. Structure your protocol so that the strictness of price requirements scales with the risk of the operation.
contract TieredOracleProtocol { IIfaPriceFeed public constant ORACLE = IIfaPriceFeed(0xA9F17344689C2c2328F94464998db1d3e35B80dC); uint256 constant TIER_1_MAX_AGE = 1800; // 30 min — liquidations uint256 constant TIER_2_MAX_AGE = 3600; // 1 hour — borrowing uint256 constant TIER_3_MAX_AGE = 7200; // 2 hours — deposits, withdrawals uint256 constant TIER_4_MAX_AGE = 86400; // 24 hours — read-only display /// @notice Tier 1 — strictest. For liquidation logic only. function getLiquidationPrice(bytes32 assetId) internal view returns (int256) { (IIfaPriceFeed.PriceFeed memory info, bool exists) = ORACLE.getAssetInfo(assetId); require(exists, "IFA: unsupported"); require(_isFresh(info.lastUpdateTime, TIER_1_MAX_AGE), "IFA: stale for liquidation"); require(_isReasonable(info.price), "IFA: deviation too high"); return info.price; } /// @notice Tier 2 — for borrowing and minting. function getBorrowPrice(bytes32 assetId) internal view returns (int256) { (IIfaPriceFeed.PriceFeed memory info, bool exists) = ORACLE.getAssetInfo(assetId); require(exists, "IFA: unsupported"); require(_isFresh(info.lastUpdateTime, TIER_2_MAX_AGE), "IFA: stale for borrow"); return info.price; } /// @notice Tier 3 — for deposits and withdrawals. function getDepositPrice(bytes32 assetId) internal view returns (int256) { (IIfaPriceFeed.PriceFeed memory info, bool exists) = ORACLE.getAssetInfo(assetId); require(exists, "IFA: unsupported"); require( _isFresh(info.lastUpdateTime, TIER_3_MAX_AGE), "IFA: stale for deposit" ); return info.price; } /// @notice Tier 4 — for display only. Never use for financial logic. function getDisplayPrice(bytes32 assetId) external view returns (int256 price, bool isFresh) { (IIfaPriceFeed.PriceFeed memory info, bool exists) = ORACLE.getAssetInfo(assetId); if (!exists) return (0, false); return ( info.price, _isFresh(info.lastUpdateTime, TIER_4_MAX_AGE) ); }}
Use when: Complex protocols with multiple operation types — lending, borrowing, liquidation, deposits, withdrawals — that have genuinely different risk profiles and should not be constrained by the strictest possible staleness requirement across all functions.
Pattern 5: Automatic Protocol Pause with Guardian Recovery
When the primary feed fails in a way that cannot be resolved by a fallback — both primary and secondary are stale simultaneously — automatically pause sensitive operations and require guardian intervention to resume.
contract PausableOracleProtocol { IIfaPriceFeed public constant ORACLE = IIfaPriceFeed(0xA9F17344689C2c2328F94464998db1d3e35B80dC); bool public paused; address public guardian; uint256 public constant MAX_PRICE_AGE = 3600; event ProtocolPaused(bytes32 indexed assetId, uint256 timestamp, string reason); event ProtocolResumed(address indexed guardian, uint256 timestamp); error ProtocolIsPaused(); error NotGuardian(); error OracleHealthCheckFailed(bytes32 assetId); modifier onlyWhenHealthy(bytes32 assetId) { if (paused) revert ProtocolIsPaused(); (IIfaPriceFeed.PriceFeed memory info, bool exists) = ORACLE.getAssetInfo(assetId); if (!exists || !_isFresh(info.lastUpdateTime, MAX_PRICE_AGE)) { paused = true; emit ProtocolPaused(assetId, block.timestamp, !exists ? "feed_missing" : "feed_stale"); revert ProtocolIsPaused(); } _; } /// @notice Critical operations — paused if oracle is unhealthy function borrow(bytes32 collateralAssetId, uint256 amount) external onlyWhenHealthy(collateralAssetId) { // Borrow logic — only executes with a healthy oracle } /// @notice Safe operations — always available regardless of oracle state function withdrawCollateral(uint256 amount) external { // Withdrawals should remain available even when paused // so users can exit positions safely } /// @notice Guardian resumes the protocol after oracle issue is resolved function resume() external { if (msg.sender != guardian) revert NotGuardian(); // Verify oracle is actually healthy before resuming // Add checks for specific assets your protocol uses paused = false; emit ProtocolResumed(msg.sender, block.timestamp); } /// @notice Guardian can update staleness threshold if needed function setMaxPriceAge(uint256 newMaxAge) external { if (msg.sender != guardian) revert NotGuardian(); // Update MAX_PRICE_AGE — use a storage variable if you need this }}
Always keep withdrawal and position-closing functions available even when the protocol is paused due to oracle issues. Users must be able to exit positions safely regardless of oracle state. Blocking withdrawals during a pause traps user funds and creates additional risk.
For protocols that cannot rely on spot prices alone, a Time-Weighted Average Price (TWAP) derived from on-chain DEX pools provides a manipulation-resistant fallback that is independent of the oracle infrastructure entirely.
TWAP prices from DEX pools are manipulation-resistant for longer periods (30+ minutes) but can be manipulated at the block level with sufficient capital. Use TWAP as a fallback for short-term oracle outages — not as a replacement for a reliable oracle network.
Categorize every protocol function by its tolerance for oracle uncertainty. Liquidations are Tier 1 — strictest. Display functions are Tier 4 — most lenient.
2
Choose a pattern per tier
Select the appropriate fallback pattern for each operation tier. You may use different patterns for different tiers within the same protocol.
3
Implement withdrawal safety
Confirm that withdrawals and position-closing functions remain available under all oracle failure scenarios — including full protocol pause.
4
Wire up the guardian
Define who the guardian is — a multisig, a timelock, or a governance contract — and confirm they can respond to a pause event within your acceptable recovery time window.
5
Emit events on fallback usage
Every time a fallback path is taken, emit an event. Off-chain monitoring should alert your team immediately when fallback is triggered.
6
Test all failure modes
Use Foundry’s vm.mockCall to simulate stale prices, missing feeds, and secondary oracle failures. Every fallback path should have a test that exercises it.
7
Document your runbook
Write a response procedure for every fallback scenario — what triggers it, what the guardian should check, what actions are available, and when to resume.
contract FallbackTest is Test { MyProtocol protocol; address constant ORACLE = 0xA9F17344689C2c2328F94464998db1d3e35B80dC; bytes32 constant USDT = 0x6ca0cef6107263f3b09a51448617b659278cff744f0e702c24a2f88c91e65a0d; function setUp() public { protocol = new MyProtocol(address(0xGuardian)); } /// @notice Simulate a stale feed and verify hard revert function test_stalePrice_revertsOnCriticalOperation() public { // Warp time 2 hours past the last update vm.warp(block.timestamp + 7200); vm.expectRevert("IFA: price feed is stale"); protocol.borrow(USDT, 1000e18); } /// @notice Simulate stale primary — verify fallback is used function test_stalePrice_fallbackUsed() public { vm.warp(block.timestamp + 7200); // Mock secondary oracle returning a fresh price vm.mockCall( address(protocol.SECONDARY()), abi.encodeWithSelector(ISecondaryOracle.getPrice.selector), abi.encode(int256(1e18), block.timestamp) ); // Should not revert — fallback provides the price protocol.borrow(USDT, 1000e18); } /// @notice Verify auto-pause fires when oracle is stale function test_stalePricePausesProtocol() public { assertFalse(protocol.paused()); vm.warp(block.timestamp + 7200); vm.expectEmit(true, false, false, true); emit ProtocolPaused(USDT, block.timestamp, "feed_stale"); vm.expectRevert(ProtocolIsPaused.selector); protocol.borrow(USDT, 1000e18); assertTrue(protocol.paused()); } /// @notice Verify withdrawals remain available when paused function test_paused_withdrawalStillWorks() public { vm.warp(block.timestamp + 7200); // Trigger pause try protocol.borrow(USDT, 1000e18) {} catch {} assertTrue(protocol.paused()); // Withdrawal should still work protocol.withdrawCollateral(500e18); }}