Crypto OS
Technical Crypto OSStage 4 · DeFi Engineering

T21 · Oracle

How can an onchain contract know an outside price?

Capabilities practiced
BuilderProtocol Literacy
Hands on
Connect a price feed, record its update frequency and deviation threshold, and simulate how your contract handles stale data.
AI Lab
Have AI analyze the risk of using a spot pool price as an oracle. Calculate yourself how much capital manipulation would require.

A Real-World Problem

You are building a collateralized lending contract. Users deposit token X and borrow a stablecoin.

The key rule is simple: collateral value must exceed the loan by a specified multiple. In code it is one line:

uint256 collateralValue = amount * getPrice(token) / 1e18;

The whole problem is hidden in getPrice. A contract has no network connection: it cannot make an HTTP request or read an exchange's order book. It can only read data that already exists onchain.

Is there an available onchain price? An AMM pool has one. Divide its reserves and you get a price, free and current, with no third party.

So you write:

function getPrice(address token) public view returns (uint256) {
    (uint256 r0, uint256 r1) = pair.getReserves();
    return r1 * 1e18 / r0;     // What is this line worth?
}

Bring back the final Lab from T19: doubling the price in a constant-product pool takes an input of roughly 41% of one side's reserve.

If the pool holds one million on that side, that is 410,000. It sounds expensive.

But the attacker does not need to own the 410,000. They can borrow it within one transaction, manipulate the price, borrow an excessive amount from your contract, move the price back, and repay the temporary loan. It all happens in one block. Required starting capital is nearly zero; fees are the main cost.

That single line can cost your contract every asset it holds.

Thought Experiment

Imagine your contract as a person locked in a windowless room with only a slit in the door.

Their job is to decide how much money to give visitors based on the gold price outside. They cannot see outside themselves.

Someone passes a note through the slit with a number on it.

Should they act on the number? First they need answers to four questions, which make up this chapter:

First, who wrote the note? If any passerby can put a note through the slit, the price is whatever the last passerby wrote. The person needs recognizable, approved sources.

Second, when was the note written? Without a time, they cannot distinguish a price from five seconds ago from one from three days ago. The latter can be fatal in a fast market.

Third, how far is it from the previous note? If yesterday said 100 and today says 10,000, this probably is not the market. It may be a prank or a failed source.

Fourth, what if no note arrives for a long time? Should they keep using the last one, stop working, or act conservatively?

None of these questions has a universally correct answer, but each needs an explicit answer in code. If you leave it unwritten, you choose a dangerous default.

Now consider the AMM pool. It is a note anyone can pay to rewrite, without even a timestamp.

Your Decision

Your lending contract needs a price. Which source do you choose?

What Happens

Compare the sources:

SourceManipulation costLagDependencyMain failure mode
Pool spot priceVery low, using borrowed fundsNoneNoneRewritten within one transaction
Same-pool TWAPModerate; must be sustainedWindow lengthNoneSevere lag in a fast market
External price feedHigh; multiple offchain parties must failSeconds to minutesThird-party contract and operatorStale, delayed, or clamped at a bound
Median of sourcesHighestDepends on slowest sourceMultiple third partiesHandling a failed source

The cost column matters most. In one sentence:

There is no intrinsically “safe price source,” only a source whose manipulation cost exceeds the value extractable from it.

Make that a ratio, the most useful tool in this chapter:

Oracle safety margin = cost to manipulate the source / maximum value the attacker can extract

Less than 1   →  this configuration will eventually be attacked
Near 1        →  market or gas changes can reverse the margin
Far above 1   →  safe for now, but both numerator and denominator change over time

Note the denominator. It is not the protocol's total value locked, but the maximum withdrawable at this false price. Those can differ greatly. Even if an asset is just 1% of total deposits, if it can back a loan of the entire stablecoin pool, the denominator is the entire pool.

Also, neither numerator nor denominator is constant.

Liquidity migrates and a pool shallows, lowering the numerator. The protocol raises a collateral factor, increasing the denominator. A configuration that was safe at launch can become unsafe six months later without changing one line of code.

Build a Model

First examine the entire pipeline and how each part can fail:

  1. Actual market price
  2. Data providers
  3. Offchain aggregation
  4. Onchain price contract
  5. Your contract’s decision
Five stages with separate failure modes. You control only the last, but must plan for failure in the first four.
StageTypical failureWhat your final stage can do
Actual marketLiquidity dries up; no fair price existsTrip a circuit breaker and pause affected operations
Data providerA source is down or reports a bad numberRely on the aggregator's median
Offchain aggregationUpdates stop or lag growsCheck the timestamp
Onchain price contractUpgrade, pause, or bound valueCheck range and plausibility
Your contractUses the result without checksEverything in this chapter

You must understand four parameters for each price source. They are configurable, and vary greatly across sources, assets, and chains. The table gives only rough ranges; look up actual numbers yourself in the Lab's first step:

ParameterMeaningTypical scaleWhy it matters
HeartbeatLongest promised update intervalMinutes to hoursSets the basis for staleness limits
DeviationChange that triggers an early updateFractions to several percentDetermines age during quiet periods
DecimalsPrecision of returned valueUsually 8 or 18Misreading it changes scale by orders of magnitude
Value boundsMinimum and maximum allowed by aggregationAsset-dependentExtreme moves may clamp to a false value

The last row deserves attention. Some feeds have upper and lower limits by design. When the real price leaves the range, the feed returns a boundary value, not reality, and the API response still looks normal. A fresh timestamp alone will not catch it.

Put four checks in code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IPriceSource {
    // Return the price and the time when this price was produced
    function latest() external view returns (int256 price, uint256 updatedAt);
}

contract PriceGuard {
    IPriceSource public source;
    uint256 public maxStaleness;   // Maximum allowed age, usually a multiple of heartbeat
    uint256 public minPrice;       // Plausible lower bound
    uint256 public maxPrice;       // Plausible upper bound
    uint256 public maxJumpPercent; // Maximum jump from last trusted price, as a percentage

    uint256 private lastGoodPrice;
    uint256 private lastGoodAt;

    error StalePrice(uint256 age);
    error PriceOutOfRange(uint256 price);
    error PriceJump(uint256 from, uint256 to);

    function readPrice() public view returns (uint256) {
        (int256 raw, uint256 updatedAt) = source.latest();

        // Check one: reject zero and negative values
        if (raw <= 0) revert PriceOutOfRange(0);
        uint256 price = uint256(raw);

        // Check two: reject stale values
        uint256 age = block.timestamp - updatedAt;
        if (age > maxStaleness) revert StalePrice(age);

        // Check three: reject values at the aggregator's bounds
        if (price <= minPrice || price >= maxPrice) revert PriceOutOfRange(price);

        // Check four: limit jumps relative to the last trusted price
        if (lastGoodPrice > 0) {
            uint256 hi = price > lastGoodPrice ? price : lastGoodPrice;
            uint256 lo = price > lastGoodPrice ? lastGoodPrice : price;
            if ((hi - lo) * 100 > lo * maxJumpPercent) revert PriceJump(lastGoodPrice, price);
        }

        return price;
    }
}

Of the four, the third is most often missed; the fourth is most debatable.

The fourth check is debatable because real markets really can gap by tens of percent at once. Set the threshold too tightly and you reject every price precisely when liquidations need one, creating bad debt. There is no universal value; you balance manipulation resistance against availability.

More important than all four is what your contract does when data is unavailable. There are three basic choices, and you must select explicitly:

PolicyActionSuitable forCost
Revert everythingReject every operationBorrowing and collateral withdrawalAlso blocks liquidations
DegradeAllow only actions that reduce riskRepayment and added collateralMore complex logic by entry point
Fallback priceUse a backup source or last good priceLiquidationBackup can itself be manipulated

The easiest mistake is a blanket revert. It appears safest, but freezes liquidation just when the feed fails, often during a volatile market when liquidation is most needed.

Classify operations instead: risk-increasing actions require a fresh price; risk-reducing actions should remain possible without one.

What It Is Called

OracleOracle

A mechanism that brings information from outside the chain onchain.

This is fundamentally a trust problem, not merely an engineering problem. The chain lets everyone verify one history, but cannot verify today's external gold price by itself. Something offchain must supply that trust.

Price FeedPrice feed

An onchain price contract and the operation behind its continuing updates.

At minimum, read the price and the time it was produced. An interface returning only a price, without a timestamp, cannot be used safely.

HeartbeatHeartbeat

The feed's promised maximum interval between updates, even when the price is unchanged.

It guides your staleness limit. A limit shorter than the heartbeat causes false failures in normal operation; one that is too long permits stale data. A common approach is a multiple of heartbeat, configured separately for each asset.

Deviation ThresholdDeviation threshold

The relative price change that triggers an update ahead of the heartbeat.

Together with heartbeat, it determines freshness: quiet markets update on heartbeat and moving markets on deviation. An old-looking value during stable prices may be fine; the real danger is an update that cannot keep up with volatility.

TWAPTime-weighted average price

An average calculated from accumulated prices across a time window.

It improves manipulation cost, not necessarily accuracy. The attacker must sustain a false price across the whole window instead of pushing once. The cost is lag; window length directly trades response speed against attack resistance.

Price ManipulationPrice manipulation

Using trades to change a source's reported price and profit from contracts that depend on it.

It may use entirely legitimate trades rather than a code exploit: move the price, trigger the target contract, and restore the price within one transaction. Every function can work as written while the logic is defeated.

Circuit BreakerCircuit breaker

A deliberate halt to some functions when an anomaly is detected.

The key question is what to stop. Stopping everything also stops liquidations; stopping only risk-increasing operations protects the protocol while leaving room for recovery.

Hands-on

Hands onIntegrate a price feed, measure updates and deviation, and simulate how your contract handles stale dataAny testnet, local fork, and contract test frameworkFree, using only testnet and a local fork

Investigate before writing code. Choose one onchain feed and two assets: a major asset and one with clearly lower liquidity. Fill in this table:

Major assetSmaller asset
Heartbeat (promised maximum update interval)
Deviation threshold
Returned precision
Actual updates in the last 24 hours
Longest interval between updates
Value bounds, if any

Calculate the final two rows from actual onchain records, not documentation. Documentation promises behavior; chain history shows what happened. Differences are not rare.

Implement PriceGuard with all four checks: nonpositive, stale, out of bounds, and excessive jump. Give each a distinct error so tests can tell them apart.

Read successfully on a fork. Confirm the price matches the block explorer, especially its precision. Treating eight decimals as eighteen changes the result by ten orders of magnitude without a code error.

Inject four faults and test each. Replace the price source with a controllable stub:

  1. Timestamp remains an hour old → staleness error
  2. Return zero → nonpositive-value error
  3. Return the upper bound → range error
  4. Jump tenfold from the last price → jump error

Your first protection layer is complete only when all four are distinguishable.

Make the important decision: what happens on expiry?

Add four entry points to your contract: borrow, withdraw collateral, repay, and liquidate. Explicitly specify behavior with an unavailable price and test each:

Entry pointWhen price is staleYour reason
Borrow
Withdraw collateral
Repay
Liquidate

If every row says “revert,” reconsider the third and fourth. Blocking repayment and liquidation while a feed is down actively increases bad debt.

Calculate manipulation cost. This is the chapter's central exercise.

Return to the opening spot-price design and do the complete calculation:

Scenario: your contract values token X using one pool's spot price
          The pool has depth D on one side; your contract has L stablecoins to lend
          Collateral factor c (e.g. 0.75 means 100 of collateral backs 75 of debt)

Step 1: To raise X's reported price to m times its initial level, required input
        is approximately D * (sqrt(m) - 1)

Step 2: With X worth V at its true price, the attacker can borrow at the false price
        min(V * m * c, L)

Step 3: Net attack cost
        Funds used to move the pool can be borrowed and repaid in the same transaction;
        initial principal needed is near zero
        Actual cost = fees on two swaps + gas + flash-loan fee
        approximately D * (sqrt(m) - 1) * 0.6% + other costs

Step 4: Profit = Step 2 - Step 3 - the collateral's true value

Step 5: Safety margin = Step 3 / Step 2

Run three numeric cases: D of one million, ten million, and fifty million; m of 2; L of five million.

The first case's safety margin is far below 1. Record the number. Use it as a gate rather than a curiosity: before adding any source, calculate the ratio and reject a configuration beneath an acceptable multiple.

Then invert the calculation: with L at five million, how deep must the pool be for this attack to become unprofitable? That figure is the minimum liquidity requirement for listing this collateral safely.

Use only a testnet and local fork, never real funds. Real funds appear only in the capstone after Stage 5 security acceptance.

AI Lab

AI LabAsk AI to assess spot-pool oracle risk, then calculate manipulation capital and cost yourselfLevel 2 · AI Copilot

Ask in three steps:

Step one:
A lending protocol values collateral using the instant reserve ratio in a
constant-product pool. The pool has depth D on one side; the protocol can lend L.
Describe the full attack and derive a cost formula that accepts actual numbers.

Step two:
Replace that source with a TWAP from the same pool over N blocks.
Derive manipulation cost again. How many times higher is it than Step one,
and on what assumptions does that multiple depend?

Step three:
List five risks still present in the Step two design. Rank them by
probability of loss times loss size, and give a detectable signal for each.

Most models can outline step one, but cost arithmetic is where they fail. Many say “the attacker needs 410,000 of principal,” missing that it can be borrowed and repaid within the transaction. Fees are the actual expense. That difference can be two orders of magnitude and decides whether the configuration is safe.

A common step-two mistake is saying only “much safer” without a calculable multiple. Ask what the attacker pays in each block and how arbitrageurs trade against them.

Step three is the real output of this Lab. Compare its risks with the table you filled in during Lab step five and look for missing avenues.

Do not trust any specific parameter it gives you. Verify each onchain. Models readily invent plausible numbers here, and the error is difficult to notice.

Verify these points yourself after using AI

  • Does its cost calculation account for borrowing and repaying capital in one transaction? Counting only required principal is the largest mistake
  • Does its formula match your Lab calculation, particularly the square root?
  • Did it mistake extractable value for total value locked instead of the amount actually borrowable at the false price?
  • How much does it claim TWAP raises cost? Can you derive that multiple yourself?
  • Did it note how block time can strongly change manipulation cost within a window?
  • Did you verify every specific parameter it offers (heartbeat, threshold, collateral factor) onchain instead of trusting memory?

Real-World Case

Valuing collateral from a spot-pool priceRepeated since 2020

The attack pattern is remarkably consistent: borrow a large sum in one transaction, lift the collateral asset's price in a pool, deposit a small quantity to borrow stablecoins far beyond its real value, then restore the pool price and repay the temporary loan.

No contract bug is required. Every call is legal, transfer succeeds, and check passes. What failed is the assumption that the price was trustworthy.

This is one of the industry's largest classes of economic loss. The repair can sometimes be as small as changing the price-source address.

A shallow-market asset is accepted as collateralMultiple incidents

A protocol lists an illiquid asset as collateral. The feed may be fine; the asset's market depth is too small, so manipulation is cheap everywhere.

An attacker can actually buy the market price higher, use the asset as collateral to borrow substantial major assets, then abandon the collateral. The protocol holds assets it cannot sell and incurs bad debt.

Lesson: oracle safety concerns the depth behind a price, not only whether the quoted number is right. Collateral eligibility depends on market depth and protocol exposure limits, not simply whether a feed exists.

A feed stops updating while the contract keeps using stale dataRepeatedMultiple chains

Network congestion, provider failure, or an asset's feed deprecation halts updates.

Without a timestamp check, the contract keeps using the last price. In a falling market, positions that should be liquidated still look healthy. When updates resume, many positions become insolvent at once.

That is why timestamp checks are mandatory. Staleness limits also need per-asset settings; heartbeats can differ by an order of magnitude.

A price is clamped at a bound during extreme volatilityHigh-volatility periods

The real price drops below the aggregator's configured lower bound. The feed returns that bound. Its interface looks normal: recent timestamp, positive price, and valid format.

Dependent protocols continue to treat collateral as worth the bound, so liquidation does not start. By the time the bound is changed, positions are badly undercollateralized.

Freshness checks alone are insufficient; check plausibility too. Compare the feed against an independent second source and trip a circuit breaker if divergence exceeds a threshold. In practice, multi-source checks are often most useful for detecting anomalies, not merely improving precision.

Change One Variable

What ifShorten the TWAP window from 30 minutes to 5 minutes

Response becomes six times faster, liquidations happen sooner, and the T22 bad-debt risk falls.

Manipulation cost falls roughly with window length. Worse, the attacker spends less time exposed to reverse arbitrage. They need hold the false price for a shorter period and lose less to arbitrageurs, so actual cost can drop faster than linearly.

There is no universal window length. Derive it from how much value the price protects: pick an acceptable safety margin and work backward from the formula in this chapter.

What ifMove to a chain with 0.4-second blocks instead of 12-second blocks

The same thirty-block window shrinks from six minutes to twelve seconds. If your code measures the window in block counts, deployment silently cuts protection by a factor of thirty.

If it measures timestamps instead, the duration stays the same but includes more blocks, so an attacker may need to maintain the price across more opportunities for arbitrage and pay more.

This assumption is easy to miss when deploying across chains. Recalculate every time-related parameter for the new chain instead of copying it.

What ifAdd an independent feed and pause when their prices diverge too far

A single failure no longer passes unnoticed, and you gain a signal that detects anomalies proactively.

Availability falls: either source can jitter and pause the protocol more often than expected.

What happens after a pause matters more than the trigger. Keep repayment and adding collateral available during the pause. Otherwise, at the market's most stressful moment, you block users' only way to repair their positions.

What ifAccept collateral with only a few hundred thousand of market depth

The numerator, manipulation cost, collapses while the denominator, extractable value, remains. The safety margin falls below 1.

Changing feeds cannot fix this, because the asset itself is easy to move, regardless of where you read its price. Even with a perfect feed, the attacker can buy the real market higher.

The effective control is an exposure cap: limit total borrowing against the asset so the maximum extractable value stays below the cost of moving its market. This is a parameter problem, not a data-source problem.

Takeaway Questions

2
Why do we need blockchains?

Why use a blockchain? This chapter shows its boundary: the chain can make everyone agree on one history, but it cannot verify external facts by itself. An oracle is the interface at that boundary and often the least decentralized part of the protocol.

8
Who supplies liquidity?

Who provides liquidity? Here liquidity determines the numerator of manipulation cost. An asset's market depth is the security budget of its price. If depth moves away, a protocol becomes unsafe with no code change.

9
Who bears the risk?

Who bears the risk? Oracle failures drain protocol reserves and LPs, not the data provider. Remember that mismatch: the party supplying the price bears little of the loss when it is wrong.

10
What is the incentive?

What is the incentive? Ask why the providers report honestly: slashable stake, reputation, or a contractual promise? The answer determines how deeply to trust that source.

Chapter Check

Key idea

Oracles are among the most common single points of failure and attack vectors in protocols.

Finished this chapter’s lab? Mark it completeView all progress

On this page