$1,000,000 in security audit grants are live now, Apply here →

← Resource Hub

Oracle Manipulation Attacks: From Spot Price to TWAP

Updated 9 min read

Quick answer. An oracle manipulation attack moves a price that a protocol reads from onchain values, such as a Uniswap pool's slot0, within the same transaction. The attacker then borrows against inflated collateral and walks away with an insolvent loan.

The oracle manipulation attack is the most common exploit in DeFi. The research backs this up.

"SoK: Decentralized Finance (DeFi) Attacks," by researchers from institutions including UC Berkeley, ETH Zurich and Imperial College London, found price oracle attacks to be the most frequent incident type.

This guide covers the crux of oracle manipulation, the canonical attack with vulnerable code and numbers, and a more complex real case.

It then covers time-weighted average prices (TWAPs) as a partial fix, how even a TWAP can be manipulated, and what to use instead.

The Crux of Oracle Manipulation

Take a DeFi protocol that needs the price of wrapped ether (WETH). It calls an oracle contract, with something like getPrice.

With a Chainlink oracle, prices are aggregated offchain and pushed to the contract. Calling it returns the latest aggregate price.

With a Uniswap pool as the oracle, you would read slot0: the current square root price stored in the pool right now.

The protocol then uses that price, for example to decide what someone's collateral is worth and how many stablecoins to lend against it.

The problem arises when the oracle computes its price onchain. Then it can be manipulated within the same transaction that interacts with the protocol.

A Lending Example

Say a lending protocol requires a 150% collateralization ratio. Put down $150 of WETH and you can borrow at most $100 of DAI.

To value your WETH, the protocol asks the oracle. If someone can swap on the Uniswap pool before the protocol reads slot0, they control the price it sees.

Suppose ETH is at $1,000 and the attacker's collateral is 0.15 WETH. A huge swap, perhaps funded by a flash loan, pushes the pool price to $2,000.

Now the protocol values the 0.15 WETH at $300, and the attacker borrows 200 DAI instead of 100.

In the real world, on centralized exchanges and other DEXes, ETH is still $1,000. But the oracle is the protocol's whole world for pricing ether.

The protocol has opened a position with $150 of real collateral and paid out $200 of DAI.

The attacker swaps back to return the pool to $1,000 and walks away with the $50 difference. They have no reason to repay a loan that is immediately unhealthy.

The position's collateralization is 150 divided by 200: 75%. It needs liquidating, but it is already insolvent.

What Vulnerable Code Looks Like

Here is a very simple lending contract. It accepts ether as collateral, lends out DAI, and requires 120% collateralization.

Put down $1.20 of ether and you can borrow $1 of DAI. Below that ratio, your position is liquidated.

The gap between 100% (exactly solvent) and 120% is the protocol's buffer. It leaves room for liquidations to happen within some range of blocks before a position goes insolvent.

function borrow(uint256 amount) external payable {
    Position memory p = positions[msg.sender];
    p.borrowed += amount;
    p.collateral += msg.value;
    positions[msg.sender] = p;
    require(isPositionHealthy(msg.sender), "unhealthy");
    dai.transfer(msg.sender, amount);
}

function getCollateralPrice() public view returns (uint256) {
    // reads the pool's current spot price: manipulable in the same transaction
    (uint160 sqrtPriceX96, , , , , , ) = pool.slot0();
    return _toPrice(sqrtPriceX96); // conversion omitted
}

The borrow function loads the position, adds the borrowed amount and the ether sent, and checks the position is healthy before paying out.

The isPositionHealthy check computes a health factor: the collateral value divided by the borrowed value. Decimals are ignored here for simplicity.

It also assumes DAI is worth exactly $1, which is a bad assumption, kept only for simplicity.

The interesting part is getCollateralPrice. It uses a Uniswap V3 pool as its oracle, a poor choice, and reads slot0 directly.

The slot0 value holds only the current price in the pool. If the price sits at a point on the x * y = k curve, that point is what the protocol reads.

Executing the Attack

Anyone can swap on Uniswap and move the sqrtPriceX96 value in slot0.

In this pool, token0 is DAI and token1 is WETH. Swapping WETH out of the pool shrinks its WETH reserve, so the price of WETH rises.

The attacker pushes the price up, and at the inflated price calls borrow. The health check values their ether at that price, so a position that is really insolvent passes.

The Numbers

Say ETH's real market price is $5,000. For simplicity, ignore concentrated liquidity and treat the pool as a plain x * y = k curve.

The pool holds 10 ETH and 50,000 DAI, so k is 500,000.

  1. The attacker takes 5 ETH out of the pool and puts 50,000 DAI in. With a flash loan, they do not even need the DAI up front.
  2. The pool now prices ETH at $20,000.
  3. They deposit the 5 ETH as collateral, which the protocol values at $100,000.
  4. At a 150% collateralization ratio, they borrow about $66,667 of DAI. The position looks perfectly healthy.
  5. At the real price, the collateral is worth only $25,000, so the position is badly undercollateralized.
  6. They spend the $66,667 buying ETH elsewhere at $5,000, about 13.33 ETH.
  7. They sell 5 ETH back into the pool to restore the original price and get their 50,000 DAI back, then repay any flash loan.
  8. They keep about 8.33 ETH, roughly $40,000 of profit, ignoring fees.

Alarm bells should go off anytime a protocol reads slot0, or derives any price from values that live onchain.

A Complex Example: Cream Finance

Manipulation does not always look like the canonical example. The Cream Finance exploit relied on yUSD, and it had nothing to do with reading a Uniswap pool.

It came from how the oracle valued yUSD: the balance of yCRV in the Yearn vault contract divided by the total supply of yUSD.

That is a canonical way to value one share of something that holds a fractional share of all the deposited yCRV.

The attacker artificially inflated the yUSD price by inflating the vault's yCRV balance. Cream accepted yUSD as collateral, so that collateral looked far more valuable than it was.

They opened a position with less collateral than borrowed value and walked away with the excess, draining something like $120 million from the protocol.

The full exploit was rather complex, but this price inflation was a key part.

The moral: when a price is derived entirely from onchain values, dig deeper. There is often a manipulation attack. The same balance-over-supply pattern drives the ERC-4626 inflation attack.

TWAP: A Partial Solution

Uniswap's partial solution for onchain oracles adds time to the equation.

In the lending example and in the Cream exploit, the issue was time: the price moved within a single block, in a single transaction.

A time-weighted average price (TWAP) averages price observations over a window. It may combine the price from the last block, four blocks ago, eight blocks ago and so on, over something like 30 minutes.

Say that average puts ether at $1,547.

The current slot0 price is not yet an observation in the TWAP. So a flash loan or any manipulation within one transaction does not affect it.

In that sense it is a good solution. Manipulation becomes far more capital-intensive, because the price has to actually stay moved in the blockchain's state for several blocks.

How a TWAP Can Still Be Manipulated

To move a TWAP, keep the price manipulated for as many blocks as possible.

Say that in block 100 the attacker swaps the price of ether up to $5,000, against a real price of about $1,500. That takes a good deal of capital, and no flash loan, since the swap must persist.

The hard part comes next. For 30 blocks, nobody can be allowed to arbitrage the pool.

Every arbitrageur wants to sell ether into it at $5,000. The pool has to be effectively inaccessible until the attacker swaps it back in block 130.

Those 30 blocks now count in the 30-minute window. Instead of $1,547, the TWAP might read $1,736. In practice it would take even more to cause a noticeable inflation.

Then the attacker deposits WETH into a lending protocol at about $1,700 when it is worth about $1,500, and may borrow more than they put down.

Proof of Stake Changes the Math

On a proof of work network, gaining enough hash power to control 30 blocks is extremely unlikely and cost prohibitive.

Ethereum's move to proof of stake makes this more interesting, and perhaps more capital-efficient. Depending on the pool and the resources at hand, it might be possible.

Uniswap researched exactly what it takes. In a two-block manipulation, a block proposer who knows they hold consecutive blocks swaps the price up in the first block.

They make sure nobody else swaps in that block, then swap first in the next block. That writes the inflated price into an observation.

To move the 30-minute TWAP of the USDC/WETH pool by 20% with two blocks, Uniswap estimated a swap of about 709 billion USDC. No onchain exploit yields that kind of capital, so it is not realistic.

Three blocks in a row changes the picture. Uniswap estimated a validator with about 1% of stake proposes three consecutive blocks roughly 0.19 times a month, about once every five months.

With three blocks, the cost on USDC/WETH falls to about $978 million. Lower-liquidity pools would be even more susceptible.

With four, five, six or seven consecutive blocks, a profitable manipulation becomes more possible, though still probably unlikely.

Best Practices

The takeaway may be to use Chainlink every time. In our view, at the time of recording (2023), it was the best option for your oracle needs.

With Chainlink, you trust the consensus mechanism behind its price feeds.

With a TWAP, you are also trusting someone. You trust large entities with big stakes as block proposers, like Coinbase and Lido, not to manipulate the price across blocks they control.

Use this checklist when reviewing any protocol that consumes a price:

  • Does the protocol read a spot price, such as Uniswap's slot0?
  • Is any price derived from onchain values, such as a balance divided by a supply, that can move within one transaction?
  • If a TWAP is used, how much capital and how many consecutive blocks would it take to move it enough to profit?
  • Could an attacker open an insolvent position at the manipulated price and walk away with the excess?

Manipulated prices also feed other attacks. See sandwich attacks for how swaps move prices against users.

How Guardian Helps

Guardian's smart contract audits review economic assumptions and failure modes, and senior researchers attack oracle and integration risk with exploit-driven review and proof-of-concept depth.

See smart contract audits or Vanguard.

Frequently Asked Questions

What is an oracle manipulation attack?

An attack that moves the price a protocol reads from an onchain source, such as a DEX pool, in the same transaction it interacts with the protocol. The attacker borrows against inflated collateral, restores the price and keeps the excess.

Why is reading Uniswap slot0 dangerous?

slot0 holds the pool's current spot price. Anyone can move it with a large swap, often funded by a flash loan, then interact with the protocol at the manipulated price in the same transaction.

Does a TWAP prevent oracle manipulation?

Partly. A TWAP ignores the current price, so single-transaction and flash loan manipulation fails. But an attacker who controls consecutive blocks, which proof of stake makes more plausible, can still move it with enough capital.

What oracle should a protocol use?

In our view at the time of recording (2023), Chainlink was the best option. A TWAP still relies on large block proposers not manipulating prices across consecutive blocks they control.

Sources

Pressure-test your price feeds

Guardian attacks your oracle integrations with the same manipulation paths an attacker would try.