Crypto Training
Oracles and TWAPs: How Price Feeds Get Manipulated
Oracles are an interface between economics and code. If you integrate a price, you must model who can move it, how fast, and at what cost.
Oracles are where DeFi stops being “just code” and becomes a game against markets.
A price feed is not a number. It’s a number with an adversary and a budget.
If you don’t quantify the budget, your oracle integration is a guess.
Start with the attacker’s question#
How much does it cost to move this price enough to make money?
If the answer is less than the profit from manipulation, you’ve built an incentive-aligned exploit.
This is why oracle bugs are so devastating: you aren’t just leaking funds, you’re minting profit for anyone willing to pay the manipulation cost.
Two categories: external feeds vs on-chain markets#
| Oracle type | Example | Manipulation lever | What you need to understand |
|---|---|---|---|
| External feed | signed price updates | signer set, update cadence | staleness, quorum, key risk |
| AMM-derived | Uniswap pool price | liquidity + time | cost to move price for N blocks |
A TWAP is in the second category.
Spot price vs TWAP (what each gives you)#
Spot price is “what the pool says right now.”
TWAP is “average price over a window.”
A TWAP doesn’t remove manipulation; it forces the attacker to sustain it for longer.
| Price input | Strength | Weakness |
|---|---|---|
| Spot | responsive | cheap to manipulate in one block |
| TWAP | resists single-block pushes | still manipulable if profit surface is large |
A practical attack sketch#
Consider a lending market that:
- uses an AMM TWAP for collateral valuation
- allows borrowing up to a collateral factor
Attacker goal:
- move the oracle price up
- borrow as much as possible
- let price revert
- leave bad debt
The protocol’s defense is not “TWAP exists”. The defense is the cost to sustain the move versus the profit from borrowing.
This is why you must think like an economist for oracle security.
Estimating manipulation cost (a useful approximation)#
You don’t need a perfect model to do better than vibes.
Start with:
- how much price movement is required to meaningfully change your protocol state?
- what is the protocol profit surface (how much can the attacker extract in one action)?
Then approximate the cost to move price in the underlying market.
For constant-product AMMs (v2-style), a rough intuition:
- price impact grows non-linearly with trade size
- thin liquidity makes big moves cheap
For v3-style concentrated liquidity, the question becomes:
- what is the liquidity around the current tick range?
- how far must the price move (ticks) to reach the attacker’s target?
In practice, auditors do this pragmatically:
- simulate swaps against the pool (or use on-chain quotes) to estimate cost
- compare cost to maximum extractable value in the target protocol action
If “cost to push price for window” is smaller than “profit from borrowing”, the TWAP is not protecting you.
Uniswap v3 TWAP: integration details that matter#
The common v3 pattern is:
- call
observe([secondsAgo, 0]) - compute average tick
- convert to price
Pseudo-code:
// sketch: not production-ready math
(uint32[] memory secondsAgos) = new uint32[](2);
secondsAgos[0] = window;
secondsAgos[1] = 0;
(int56[] memory tickCumulatives,) = pool.observe(secondsAgos);
int56 delta = tickCumulatives[1] - tickCumulatives[0];
int24 meanTick = int24(delta / int56(uint56(window)));
Where integrations go wrong:
- you pick a low-liquidity pool because it’s “the official one”
- you pick a window based on vibes
- you ignore observation cardinality
- you fail open on errors (stale price accepted)
Observation cardinality and startup edge cases#
Uniswap v3 TWAP relies on observations being recorded.
If a pool has insufficient observation cardinality, you can get:
- poor TWAP quality
- reverts
- “works in tests” but fails in production because observations differ
Another edge case: at startup (fresh pool or after a reset), your TWAP may not have a full window of history. You need a defined behavior for that phase.
Safe designs explicitly handle:
- “TWAP not ready yet” (revert for borrows, allow conservative withdrawals)
- “window shortened temporarily” (and cap risk)
Window choice: a more honest table#
| Window | Security intuition | What usually breaks |
|---|---|---|
| 30s - 2m | cheap to sustain moves | manipulation still profitable |
| 10m - 30m | sustained push costs more | stale reads during volatility |
| 1h+ | hard to sustain | UX degradation, staleness, liveness issues |
No window is “safe” by default.
The correct question is: what profit does your protocol allow per action, and what does it cost to move the oracle that much for that long?
Oracle failure modes (the boring ones that still kill protocols)#
Oracle incidents aren’t always “attacker moved price.” Sometimes it’s:
- stale data accepted as fresh
- missing data causes revert -> liquidations freeze
- oracle returns 0 -> divide-by-zero or free borrowing
- wrong decimals -> 1e12 errors
This table is a good review tool:
| Failure | How it happens | Safe response |
|---|---|---|
| stale | updates stop | cap risk actions; don’t freeze withdrawals |
| revert | adapter breaks | bounded fallback; circuit breaker |
| scale bug | decimals mismatch | normalize once; test with fixtures |
| thin market | pool drained | liquidity checks; source diversity |
Defensive controls that compose#
The best oracle integrations use layered defenses.
1) Liquidity and market sanity checks#
Don’t use pools with thin liquidity for critical prices.
At minimum, track:
- pool liquidity
- price impact for a representative trade
If the pool is too small, the attacker’s budget is too low.
2) Sanity bounds#
Reject prices that jump too far relative to recent values.
This doesn’t stop all manipulation, but it raises cost and reduces blast radius.
3) Multi-source oracles#
Use multiple sources and aggregate (median).
If one source is manipulated, it won’t dominate.
4) Circuit breakers#
When oracle conditions are weird:
- cap borrow
- pause liquidations
- require manual intervention
This is not “centralization”. It’s an explicit failure mode.
5) Time delay for high-risk actions#
For actions with high profit surface (large borrows, large withdrawals), require:
- two-step commit then execute
- or a delay
This makes sustained manipulation more expensive.
Audit questions I actually ask#
- Can the attacker choose the pool (or is it hard-coded)?
- What is the smallest liquidity scenario in production?
- What is the max profit from a manipulated price in one transaction?
- Is there a safe failure mode when the oracle is stale or unavailable?
- What happens at the boundaries (startup observations, new pool, paused pool)?
If you can answer those with numbers, your oracle integration is real.
A concrete “decimals” footgun (boring, lethal)#
Oracle adapters routinely return values with different decimals:
- Chainlink often uses 8 decimals
- many ERC-20 tokens use 18
- some assets use 6
If you normalize incorrectly, you can create a 1e12 price error. That can be worse than manipulation because it can be exploited instantly and repeatedly.
Audit move: make decimal normalization a single, tested function and include fixtures for common assets.
A defensive clamp pattern#
Clamps are not a replacement for a real oracle, but they are a good blast-radius reducer:
// Sketch: clamp the next price relative to the last good price.
function clamp(uint256 lastPrice, uint256 nextPrice, uint256 maxBpsMove) internal pure returns (uint256) {
uint256 upper = lastPrice + (lastPrice * maxBpsMove) / 10_000;
uint256 lower = lastPrice - (lastPrice * maxBpsMove) / 10_000;
if (nextPrice > upper) return upper;
if (nextPrice < lower) return lower;
return nextPrice;
}
This prevents a single bad read from instantly flipping your system into insolvency. It doesn’t solve sustained manipulation, but it makes “one weird block” less catastrophic.
When to fail open vs fail closed#
Failing closed (revert) is safer for borrowing, but it can be unsafe for withdrawals if it freezes user funds.
A common pattern in mature protocols:
- withdrawals: allow with conservative pricing or with a cap
- borrowing: require oracle freshness and revert when uncertain
The point is to explicitly design failure modes instead of inheriting them from “whatever the oracle call does.”
A quick “integration contract” checklist#
If you are writing a protocol that consumes an oracle, ensure you explicitly decide:
- what happens when the oracle reverts
- what happens when the oracle is stale
- what happens when the oracle is manipulated but within bounds
Failing closed is not always correct if it freezes withdrawals. Failing open is often catastrophic.
The right design is: fail in a way that matches the asset at risk.
Further reading#
- TWAP oracle guide (Uniswap v3): https://tienshaoku.medium.com/a-guide-on-uniswap-v3-twap-oracle-2aa74a4a97c5
- Oracle threat model framing: https://samczsun.com/so-you-want-to-use-a-price-oracle/