Skip to main content
Sigvex

Cross-Chain Oracle Inconsistency Exploit Generator

Sigvex exploit generator that validates cross-chain oracle inconsistency vulnerabilities including L2 sequencer downtime, oracle price lag between chains, and missing the off-chain aggregator sequencer uptime feed checks.

Cross-Chain Oracle Inconsistency Exploit Generator

Overview

The cross-chain oracle inconsistency exploit generator validates findings from the cross_chain_oracle, oracle_lag, sequencer_uptime, and related detectors by executing the target contract under four oracle scenarios: synchronized mainnet and L2 oracles, L2 lagging by 5 minutes (5% price difference), L2 sequencer down with 1-hour stale data, and L2 oracle ahead of mainnet. Missing sequencer uptime validation is the most critical finding (confidence 0.95).

L2 chains (Arbitrum, Optimism, Base) use a sequencer that batches and orders transactions. When the sequencer goes down, aggregated oracle prices stop updating — but the last price remains on-chain. Protocols that read the oracle without first checking the off-chain aggregator sequencer uptime feed will use prices that are up to 1 hour old (or more). Venus Protocol suffered $100M+ in losses from oracle staleness. Multiple L2 protocols have experienced losses during sequencer downtime when contracts accepted stale prices for liquidation decisions.

Note: Exploit generation in Sigvex is for vulnerability validation purposes only.

Attack Scenario

Sequencer downtime exploitation:

  1. The L2 sequencer goes offline for 90 minutes (e.g., Arbitrum or Optimism maintenance).
  2. The off-chain aggregator ETH/USD feed stops updating — stuck at its last reported price of $2000.
  3. The real market price moves to $1800 during the downtime.
  4. An attacker monitors the mempool, waiting for the sequencer to come back online.
  5. In the first block after restart, the attacker submits a liquidation transaction.
  6. The oracle still reports $2000 (hasn’t updated yet), but the attacker knows the real price is $1800.
  7. The attacker triggers invalid liquidations on positions that would be healthy at $1800.

Cross-chain oracle lag arbitrage:

  1. A protocol uses two aggregated price feed: one on Ethereum mainnet, one on an L2.
  2. The L2 oracle lags by 5 minutes and reports $1900 while mainnet reports $2000.
  3. The attacker borrows on the L2 at the underpriced $1900 collateral valuation.
  4. The attacker bridges to mainnet and sells at $2000.
  5. The 5% price lag creates consistent arbitrage until the lag closes.

Exploit Mechanics

The generator sets up three simulated oracle addresses:

  • Mainnet oracle — reporting the current market price
  • L2 oracle — reporting a lagging or stale price
  • Sequencer uptime feed — reporting the sequencer online/offline status

Base timestamp: 2024-01-01. Current market price: $2000 (in 8-decimal the off-chain aggregator units).

Scenario Chain ID L2 price L2 timestamp Sequencer Confirmed if
1 — Synchronized 1 (mainnet) $2000 current online Baseline
2 — L2 lagging 10 (Optimism) $1900 (-5%) -300s online Succeeds (0.85)
3 — Sequencer down 42161 (Arbitrum) $1800 (-10%) -3600s offline (0) Succeeds (0.95)
4 — Reverse lag 137 (Polygon) $2000 (fresh) current online Succeeds (0.70)

Verdict:

  • Synchronized succeeds and Sequencer-down succeeds → no L2 sequencer uptime validation (confidence 0.95): critical.
  • Synchronized succeeds and L2 lagging succeeds → cross-chain oracle lag (confidence 0.85): high.
  • Synchronized succeeds and Reverse lag succeeds → oracle freshness asymmetry (confidence 0.70): medium.
  • Sequencer-down reverts → protected: sequencer uptime check implemented.
// VULNERABLE: No sequencer uptime check on L2
contract VulnerableL2Protocol {
    AggregatorV3Interface public priceFeed;

    function getPrice() public view returns (uint256) {
        (, int256 price,, uint256 updatedAt,) = priceFeed.latestRoundData();
        // CRITICAL: No sequencer uptime check!
        // During downtime, updatedAt is 1+ hour ago
        require(price > 0, "Invalid price");
        return uint256(price);
    }

    function liquidate(address user) external {
        uint256 price = getPrice(); // Could be 1+ hour old if sequencer down!
        // Liquidation based on stale price...
    }
}

// SECURE: L2 sequencer uptime check + grace period
contract SafeL2Protocol {
    AggregatorV3Interface public priceFeed;
    ISequencerUptimeFeed public sequencerFeed; // the off-chain aggregator L2 sequencer feed
    uint256 constant GRACE_PERIOD = 3600; // 1 hour after sequencer restarts

    function getPrice() public view returns (uint256) {
        // Step 1: Check sequencer is online
        (, int256 sequencerStatus, uint256 startedAt,,) = sequencerFeed.latestRoundData();
        require(sequencerStatus == 1, "Sequencer offline");

        // Step 2: Grace period after restart (prices may still be stale)
        require(block.timestamp - startedAt > GRACE_PERIOD, "Sequencer recently restarted");

        // Step 3: Standard price validation
        (, int256 price,, uint256 updatedAt, uint80 answeredInRound) = priceFeed.latestRoundData();
        uint80 roundId;
        (roundId,,,,) = priceFeed.latestRoundData();
        require(price > 0, "Invalid price");
        require(updatedAt >= block.timestamp - 3600, "Price too stale");
        require(answeredInRound >= roundId, "Incomplete round");

        return uint256(price);
    }
}

// SECURE: Cross-chain oracle validation
contract SafeCrossChain {
    AggregatorV3Interface public mainnetFeed;
    AggregatorV3Interface public l2Feed;
    uint256 constant MAX_TIME_DIFF = 120;  // 2 min max lag
    uint256 constant MAX_PRICE_DIFF = 200; // 2% max deviation (basis points)

    function getCrossChainPrice() public view returns (uint256) {
        (, int256 mainnetPrice,, uint256 mainnetTime,) = mainnetFeed.latestRoundData();
        (, int256 l2Price,, uint256 l2Time,) = l2Feed.latestRoundData();

        // Validate timestamp difference
        uint256 timeDiff = mainnetTime > l2Time ? mainnetTime - l2Time : l2Time - mainnetTime;
        require(timeDiff < MAX_TIME_DIFF, "Oracle lag too high");

        // Validate price deviation
        uint256 priceDiff = uint256(mainnetPrice > l2Price ? mainnetPrice - l2Price : l2Price - mainnetPrice);
        uint256 maxDiff = uint256(mainnetPrice) * MAX_PRICE_DIFF / 10000;
        require(priceDiff < maxDiff, "Price deviation too high");

        return uint256((mainnetPrice + l2Price) / 2);
    }
}

Remediation

Implement a layered defense for L2 oracle use:

// Required for all L2 deployments using the off-chain aggregator
// Arbitrum: 0xFdB631F5EE196F0ed6FAa767959853A9F217697D
// Optimism: 0x371EAD81c9102C9BF4874A9075FFFf170F2Ee389
ISequencerUptimeFeed sequencerFeed = ISequencerUptimeFeed(SEQUENCER_FEED_ADDRESS);

(, int256 answer, uint256 startedAt,,) = sequencerFeed.latestRoundData();
require(answer == 1, "Sequencer down");
require(block.timestamp - startedAt > GRACE_PERIOD, "Sequencer recently restarted");

Cross-chain oracle validation checklist:

  1. Sequencer uptime: Always check the off-chain aggregator L2 sequencer uptime feed before reading any price on Arbitrum, Optimism, or Base.
  2. Grace period: After a sequencer restart, wait 1+ hour before trusting prices — stale data may still be in the system.
  3. Timestamp validation: Reject prices where |mainnetTime - l2Time| > 2 minutes.
  4. Price deviation: Reject cross-chain price averages if the two sources differ by more than 2%.
  5. Circuit breaker: Pause the protocol if any oracle validation fails.
  6. Minimum staleness: Use the strictest freshness requirement across all oracle sources.