← Back to Blog

Oracle Manipulation Attacks: Chainlink vs TWAP vs Custom Feeds

2026-04-09 solidity smart contract security oracle manipulation chainlink TWAP flash loan DeFi security audit price feed

In October 2022, Mango Markets lost $117M in under an hour. The attacker didn't find a bug in the contract code. They read the code, understood exactly how it worked, and exploited something the protocol designers hadn't fully accounted for: they pumped the price of MNGO tokens across two accounts they controlled, inflating their collateral value, then borrowed everything the protocol had against that inflated collateral.

The price oracle was technically functioning correctly. It was reading a real market price. The market price was just wrong — because the attacker made it wrong. This was a spot oracle manipulation: the attacker moved the actual spot price on thin markets to corrupt the collateral valuation.

That's the oracle problem in DeFi. The contract code can be flawless and the protocol still gets drained.


What an Oracle Attack Actually Is

A price oracle is any mechanism that tells your contract what something is worth. DeFi protocols use price oracles for:

When an attacker can influence the value the oracle returns — even temporarily — they can manipulate any of the above. The window doesn't need to be long. A single block is often enough for spot oracles. TWAP oracles require the attacker to sustain price manipulation across many blocks — a fundamentally different and more expensive attack profile.


Chainlink is a decentralized oracle network where independent node operators fetch off-chain data and submit it on-chain. Because the price isn't derived from on-chain liquidity, flash loans cannot directly influence it. This is Chainlink's key security property.

But "can't be flash-loan manipulated" isn't the same as "can't be attacked."

Staleness and heartbeat

Chainlink feeds update when the price moves more than a deviation threshold (typically 0.5–1%) or after a heartbeat interval (often 1 hour or 24 hours depending on the feed). If the underlying market moves significantly within a heartbeat window, your protocol may be operating on a stale price.

// Common pattern — but missing staleness check
function getPrice(address feed) external view returns (uint256) {
    (, int256 price,,,) = AggregatorV3Interface(feed).latestRoundData();
    require(price > 0, "invalid price");
    return uint256(price);
}

The fix is checking updatedAt against an acceptable staleness threshold:

function getPrice(address feed, uint256 maxStaleness) external view returns (uint256) {
    (
        uint80 roundId,
        int256 price,
        ,
        uint256 updatedAt,
        uint80 answeredInRound
    ) = AggregatorV3Interface(feed).latestRoundData();

    require(price > 0, "negative or zero price");
    require(updatedAt != 0, "round not complete");
    require(answeredInRound >= roundId, "stale round");
    require(block.timestamp - updatedAt <= maxStaleness, "price too stale");

    return uint256(price);
}

Feed unavailability and sequencer downtime

On L2s (Arbitrum, Optimism), Chainlink feeds depend on the sequencer. If the sequencer goes down, the feed stops updating. When it comes back, there's a grace period before the feed is considered reliable. Protocols that skip the sequencer uptime check can be exploited immediately after a sequencer restart, when prices haven't caught up to market reality.

Chainlink provides a Sequencer Uptime Feed for this purpose. Using it is not optional if you're deploying on an L2.

Chainlink won't help you if:
- You're pricing an asset that doesn't have a Chainlink feed (long-tail tokens, LP tokens, NFTs)
- Your protocol derives a "price" from the Chainlink price in a way that's still manipulable (e.g., calculating LP token value by combining a Chainlink price with on-chain reserves)
- The underlying markets Chainlink aggregates from are thin enough to be moved by large trades


TWAP: Manipulation-Resistant but Not Free

TWAP (Time-Weighted Average Price) is computed from on-chain trade history — typically Uniswap V2 or V3 price accumulators. Because it averages over time, shifting the oracle value significantly requires moving the spot price and holding it there for the full TWAP window.

This is the critical distinction from spot price manipulation: a flash loan can manipulate a spot oracle in a single block atomically. Against a TWAP oracle, that single-block move barely registers. The attacker must sustain the price distortion across many blocks, absorbing arbitrage trades the entire time. That makes TWAP manipulation capital-intensive in ways flash loans don't help with.

// Uniswap V3 TWAP example
function getTWAP(address pool, uint32 secondsAgo) internal view returns (uint256) {
    uint32[] memory secondsAgos = new uint32[](2);
    secondsAgos[0] = secondsAgo;
    secondsAgos[1] = 0;

    (int56[] memory tickCumulatives,) = IUniswapV3Pool(pool).observe(secondsAgos);

    int56 tickCumulativeDelta = tickCumulatives[1] - tickCumulatives[0];
    int24 avgTick = int24(tickCumulativeDelta / int56(uint56(secondsAgo)));

    return TickMath.getSqrtRatioAtTick(avgTick); // convert to price
}

Common TWAP mistakes:

Where TWAP fails outright

TWAP needs trading history. If a pool has low volume, the price accumulator barely moves — the "average" reflects whatever the pool happened to be at during sparse trading. In a thin, stale pool, TWAP provides false confidence.


Custom Feeds: The Highest-Risk Category

Many protocols implement their own price calculations — typically to price assets without Chainlink feeds or to combine data sources. This is where the most critical oracle bugs live.

Spot price from reserves

The original sin of DeFi oracle design:

// Never use this as your oracle
function getSpotPrice() public view returns (uint256) {
    (uint112 reserve0, uint112 reserve1,) = pair.getReserves();
    return (reserve1 * 1e18) / reserve0; // instantaneous spot price
}

This returns the price at the moment of the call. A flash loan attack can manipulate reserve0 and reserve1 to any desired ratio within a single transaction, read the manipulated price, exploit the protocol, and repay — all atomically. Virtually every flash loan attack in DeFi history exploits this pattern in some form.

LP token pricing

LP tokens represent a share of pool liquidity. A common (and wrong) approach is:

LP price = (reserve0 * price0 + reserve1 * price1) / totalSupply

This is manipulable because reserve0 and reserve1 can be shifted by a large trade. The correct approach, formalized by Alpha Finance and widely cited after the CREAM Finance exploit, uses the geometric mean of reserves with an external price feed:

LP price = 2 * sqrt(reserve0 * reserve1) * sqrt(price0 * price1) / totalSupply

The key property of the geometric mean formula: it doesn't rely on the current reserve ratio, which is the part that's manipulable.


Audit Checklist

When reviewing oracle usage in a Solidity contract:

ContractScan's AI engine flags spot-price oracle patterns and unguarded latestRoundData() calls. LP pricing subtleties and economic attack paths require manual or AI-assisted reasoning — static analysis alone won't catch them.


There's no universal "safe oracle." The right choice depends on what asset you're pricing, what liquidity exists, and what latency your protocol can tolerate. Spot price reads on-chain are not acceptable — that vulnerability class should be extinct by now, and it isn't.


ContractScan is a multi-engine smart contract security scanner. QuickScan is free and unlimited — no sign-up required. Try it now.

Important Notes

This post is for informational and educational purposes only. It does not constitute financial, legal, or investment advice. The security analysis provided is based on available data and automated tools, which may not capture all potential vulnerabilities. Always conduct a professional audit before deploying smart contracts.

Scan your contract for this vulnerability
Free QuickScan — Unlimited quick scans. No signup required.. No signup required.
Scan a Contract →