Advanced

Chapter 14 / 15

Oracle Integration

Chainlink price feeds — staleness checks and typed errors.


Price oracles are a primary attack surface in DeFi — stale prices, flash-loan manipulation, and round-skipping have caused hundreds of millions in losses. This chapter shows how to consume Chainlink's latestRoundData correctly: checking freshness, validating sequencer uptime (for L2s), and handling the full tuple return.

price_feed.cov
interface IChainlinkAggregator {
    view latestRoundData() -> (
        roundId:         u80,
        answer:          i256,
        startedAt:       u256,
        updatedAt:       u256,
        answeredInRound: u80
    );
    view decimals() -> u8;
}

record PriceConsumer {
    feed:        address;
    max_staleness: duration = 3600s;   // 1 hour

    error StalePrice(age: duration, max_staleness: duration);
    error InvalidPrice(answer: i256);
    error RoundIncomplete(round_id: u80, answered_in: u80);

    view eth_usd_price() -> i256 {
        let agg = IChainlinkAggregator(self.feed);
        let (round_id, answer, _, updated_at, answered_in) =
            agg.latestRoundData();

        // Staleness check
        let age = block.timestamp - updated_at;
        if age > self.max_staleness {
            revert_with StalePrice(age, self.max_staleness);
        }

        // Sanity checks
        if answer <= 0 {
            revert_with InvalidPrice(answer);
        }
        if answered_in < round_id {
            revert_with RoundIncomplete(round_id, answered_in);
        }

        return answer;
    }
}
lending.cov — oracle-gated borrow
record SimpleLend {
    price_consumer: address;
    collateral:     map(address => amount);
    borrowed:       map(address => u256);
    ltv:            u8 = 75;   // 75% loan-to-value ratio

    error Undercollateralized(collateral_usd: u256, borrow_usd: u256);

    action borrow(usdc_amount: u256) {
        // Get current ETH price (8 decimals from Chainlink)
        let eth_price = PriceConsumer(self.price_consumer).eth_usd_price();

        let collat_eth = self.collateral[msg.sender];
        let collat_usd = collat_eth * eth_price as u256 / 1e8;
        let max_borrow = collat_usd * self.ltv / 100;

        let already_borrowed = self.borrowed[msg.sender];
        if already_borrowed + usdc_amount > max_borrow {
            revert_with Undercollateralized(collat_usd, already_borrowed + usdc_amount);
        }

        self.borrowed[msg.sender] += usdc_amount;
        // ... transfer USDC to borrower
    }
}

Annotations

Staleness checkAlways validate block.timestamp - updatedAt <= maxStaleness. A stale price is an open attack vector.
Round completenessansweredInRound >= roundId confirms the round closed normally. An incomplete round may signal a paused feed.
Negative priceChainlink answer is int256 — a zero or negative value indicates a feed error. Always reject answer <= 0.
L2 sequencerOn L2s (Arbitrum, Optimism), also check the sequencer uptime feed before trusting prices.

Key takeaways