web3.path

PHASE 14 Interop · ~3 hours

Cross-chain & Oracles

A blockchain can only see itself. To know "what's ETH/USD" or "did something happen on Solana", you need bridges (move value) or oracles (import data). Both are trust-minimized, not trust-free.

Goal — understand oracle design (Chainlink, UMA), messaging protocols (LayerZero, CCIP, Wormhole, Axelar), and integrate a price feed into a contract.

1. Oracles — why they're hard

A smart contract has no HTTP client. You can't fetch("https://coinbase.com/btc"). Instead, off-chain nodes ("oracles") push signed data on-chain. The contract trusts them.

Analogy — a contract is a server with no egress firewall rule. Oracles are a proxy that reads the outside world and posts signed messages through the one inbound port.

2. Chainlink price feeds — 80% of what you need

import "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";

contract PriceConsumer {
    AggregatorV3Interface feed;
    constructor(address _feed) { feed = AggregatorV3Interface(_feed); }
    function priceUSD() external view returns (int256) {
        (, int256 answer, , uint256 updatedAt,) = feed.latestRoundData();
        require(block.timestamp - updatedAt < 1 hours, "stale");
        return answer;   // 8 decimals
    }
}

3. Oracle design taxonomy

TypeHow it worksExample
PushOperators proactively update on-chainChainlink data feeds
PullUser fetches signed data off-chain, submits with txPyth, Redstone
OptimisticAnyone posts; anyone can challenge with bondUMA
VRFVerifiable randomnessChainlink VRF

Pull oracles are often cheaper (users pay gas only when they need fresh data) but more UX complexity.

4. Cross-chain messaging — the shape of the problem

Chain A: Sender contract → ? ? ? → Chain B: Receiver contract Between: "who relays, who attests, who's liable if they lie?"

All bridges boil down to: a set of validators attests to "this message happened on chain A", and chain B trusts them up to some threshold.

5. Messaging protocol cheat sheet

ProtocolTrust modelLatency
CCIP (Chainlink)DON + anti-fraud monitoringMinutes
LayerZeroOracle + Relayer (configurable)Minutes
AxelarOwn PoS validator set~1 min
Wormhole19 guardians~15 min
HyperlaneISM (modular, app-picks)Varies
Canonical rollup bridgeL1 inherits via proof7d optimistic / hours zk

6. A LayerZero-style send/receive contract

// on Chain A
function send(uint32 dstEid, bytes calldata payload) external payable {
    endpoint.send{value: msg.value}(
        MessagingParams(dstEid, peerOnB, payload, "", false), payable(msg.sender));
}
// on Chain B (callback)
function lzReceive(Origin calldata o, bytes32, bytes calldata payload, address, bytes calldata) external {
    require(msg.sender == address(endpoint), "only endpoint");
    require(peers[o.srcEid] == o.sender, "untrusted sender");
    (address to, uint256 amount) = abi.decode(payload, (address, uint256));
    token.mint(to, amount);    // e.g., burn-on-A + mint-on-B pattern
}
Rulealways validate msg.sender == endpoint and the source chain/sender. Otherwise anyone can call lzReceive with a forged payload and mint infinity.

7. The bridge trilemma (Vitalik, 2022)

You can't have all three: (a) trust-minimized, (b) generalized (arbitrary messages), (c) works across independent chains. Pick two. This is why every bridge design is a different compromise.

8. VRF — on-chain randomness

block.timestamp, blockhash and block.prevrandao are manipulable. For fair NFT reveals, lotteries, matchmaking — use Chainlink VRF.

function requestRandom() external {
    uint256 reqId = vrf.requestRandomWords(...);
    pending[reqId] = msg.sender;
}
function fulfillRandomWords(uint256 reqId, uint256[] memory words) internal override {
    // use words[0] deterministically
}

9. Patterns that actually ship

10. Project

Deliverable — two contracts: on Sepolia, lock an ERC-20; on Base Sepolia, mint a wrapped version. Use Chainlink CCIP or LayerZero testnet endpoints. Show end-to-end in your React app with a loading state while the message relays.

Quiz

Q. Your contract reads feed.latestRoundData() but doesn't check updatedAt. In a sequencer outage, the price was stale for 3 hours at $100 while the real price dropped to $50. A user borrowed against an asset at the stale price. What just happened?
Stale-price checks are mandatory. On L2s also check the L2 sequencer uptime feed — there's a dedicated Chainlink feed for "is the sequencer up".
← Phase 13Phase 15: Production Engineering →