Skip to main content
Sigvex

Single Oracle Dependency Remediation

How to eliminate single oracle dependency by implementing multi-source price architectures, cross-validation, and circuit breakers that keep protocols functioning when any individual feed fails.

Single Oracle Dependency Remediation

Overview

Relying on a single price oracle creates a critical single point of failure. When that oracle pauses (off-chain feed heartbeat expiry during high volatility), is manipulated (flash loan attack on a spot price source), or becomes temporarily unavailable (L2 sequencer outage), any protocol with no fallback is immediately at risk. A failed oracle either bricks the protocol — every transaction reverts because the price read fails — or permits exploitation if the failure produces a zero or otherwise pathological price value.

The Venus Protocol suffered over $100M in losses when its BNB price oracle was manipulated with no secondary source to cross-reference. The pattern is consistent: single-source dependency removes the ability to detect manipulation through comparison, and removes the ability to degrade gracefully when a source is unavailable.

Related Detector: Oracle Manipulation Detector

Before (Vulnerable)

interface AggregatorV3Interface {
    function latestRoundData() external view returns (
        uint80 roundId, int256 answer, uint256 startedAt,
        uint256 updatedAt, uint80 answeredInRound
    );
}

contract VulnerableLending {
    AggregatorV3Interface public immutable priceOracle;

    function getCollateralValue(address user) public view returns (uint256) {
        // VULNERABLE: single oracle, no fallback
        // If priceOracle is unavailable, this call reverts and the
        // entire protocol is bricked.
        (, int256 price,,,) = priceOracle.latestRoundData();
        require(price > 0, "Invalid price");
        return collateral[user] * uint256(price) / 1e8;
    }
}

After (Fixed)

interface AggregatorV3Interface {
    function latestRoundData() external view returns (
        uint80 roundId, int256 answer, uint256 startedAt,
        uint256 updatedAt, uint80 answeredInRound
    );
}

interface ITwapOracle {
    function consult(address token, uint32 secondsAgo) external view returns (uint256);
}

contract SafeLending {
    AggregatorV3Interface public primaryOracle;
    ITwapOracle public fallbackOracle;
    address public immutable token;

    uint256 public constant MAX_STALENESS    = 1 hours;
    uint256 public constant MAX_DEVIATION_BPS = 200; // 2%

    function getCollateralValue(address user) public view returns (uint256) {
        uint256 price = _getSafePrice();
        return collateral[user] * price / 1e8;
    }

    function _getSafePrice() internal view returns (uint256) {
        uint256 primaryPrice  = _tryPrimaryOracle();
        uint256 fallbackPrice = fallbackOracle.consult(token, 1800); // 30-min TWAP

        if (primaryPrice == 0) {
            // Primary unavailable — use fallback directly
            return fallbackPrice;
        }

        // Both sources available — cross-validate to detect manipulation
        uint256 deviation = primaryPrice > fallbackPrice
            ? (primaryPrice - fallbackPrice) * 10000 / primaryPrice
            : (fallbackPrice - primaryPrice) * 10000 / fallbackPrice;

        require(deviation <= MAX_DEVIATION_BPS, "Oracle deviation too large");
        return primaryPrice;
    }

    function _tryPrimaryOracle() internal view returns (uint256) {
        try primaryOracle.latestRoundData() returns (
            uint80 roundId, int256 price, , uint256 updatedAt, uint80 answeredInRound
        ) {
            if (
                price > 0
                && updatedAt >= block.timestamp - MAX_STALENESS
                && answeredInRound >= roundId
            ) {
                return uint256(price);
            }
        } catch {}
        return 0; // Signal failure to caller
    }
}

Alternative Mitigations

Median of three independent oracles — resistant to any single oracle being manipulated or unavailable:

contract ThreeOracleLending {
    AggregatorV3Interface public chainlinkFeed;
    AggregatorV3Interface public bandFeed;
    ITwapOracle           public uniswapTwap;

    function _medianPrice() internal view returns (uint256) {
        uint256[3] memory prices = [
            _safeFeed(),
            _safeBand(),
            _safeTwap()
        ];

        // Require at least two sources available
        uint256 available = (prices[0] > 0 ? 1 : 0)
                          + (prices[1] > 0 ? 1 : 0)
                          + (prices[2] > 0 ? 1 : 0);
        require(available >= 2, "Insufficient oracle sources");

        // Simple sort to find median
        if (prices[0] > prices[1]) (prices[0], prices[1]) = (prices[1], prices[0]);
        if (prices[1] > prices[2]) (prices[1], prices[2]) = (prices[2], prices[1]);
        if (prices[0] > prices[1]) (prices[0], prices[1]) = (prices[1], prices[0]);

        return prices[1]; // Middle value
    }
}

Circuit breaker on oracle failure — pause price-sensitive operations automatically rather than proceeding with degraded data:

contract CircuitBreakerLending {
    bool   public oracleCircuitBroken;
    address public keeper;

    modifier requireHealthyOracle() {
        require(!oracleCircuitBroken, "Oracle circuit open — protocol paused");
        _;
    }

    // Keeper monitors off-chain and trips the breaker if all sources diverge
    function tripCircuitBreaker() external {
        require(msg.sender == keeper, "Only keeper");
        oracleCircuitBroken = true;
        emit CircuitBreakerTripped(block.timestamp);
    }

    function restoreCircuit(uint256 newPrice) external {
        require(msg.sender == keeper, "Only keeper");
        // Validate new price before restoring
        require(newPrice > 0, "Invalid price");
        oracleCircuitBroken = false;
    }

    function borrow(uint256 amount) external requireHealthyOracle {
        // Protected — will not execute when oracle data is unreliable
        uint256 price = _getSafePrice();
        // ...
    }
}

L2 sequencer uptime check — on Optimism, Arbitrum, and other L2s, off-chain aggregated feeds may be stale during a sequencer outage while still technically passing freshness checks. Always verify sequencer status first:

interface ISequencerUptimeFeed {
    function latestRoundData() external view returns (
        uint80, int256 answer, uint256 startedAt, uint256, uint80
    );
}

contract L2SafeLending {
    ISequencerUptimeFeed public constant SEQUENCER_FEED =
        ISequencerUptimeFeed(0xFdB631F5EE196F0ed6FAa767959853A9F217697D);

    uint256 public constant GRACE_PERIOD = 3600; // 1 hour after sequencer restarts

    function _checkSequencer() internal view {
        (, int256 answer, uint256 startedAt,,) = SEQUENCER_FEED.latestRoundData();
        // answer == 0 means sequencer is up; 1 means it is down
        require(answer == 0, "Sequencer offline");
        require(block.timestamp - startedAt > GRACE_PERIOD, "Grace period active");
    }
}

Common Mistakes

Using try/catch but then falling through to zero — a try/catch that silently returns zero converts oracle failure into a data-integrity problem. Always return a sentinel value and handle it explicitly in the caller.

Setting the fallback oracle to a shorter TWAP than the primary — if the primary is a 30-minute TWAP and the fallback is a 5-minute TWAP, the fallback provides meaningfully weaker manipulation resistance. Match or exceed the primary’s time horizon.

Cross-validating with a correlated source — using an off-chain aggregated feed as primary and an on-chain AMM price as fallback can fail simultaneously during a flash loan attack if the attacker manipulates the AMM (which feeds into the off-chain feed operator’s deviationThreshold trigger). Use sources with independent data pipelines.

Neglecting the sequencer uptime feed on L2 deployments — off-chain aggregated price feeds on L2s can report the last known price during a sequencer outage rather than failing. Without the sequencer uptime check, a protocol may believe its oracle is healthy while the price is hours old.

References