Back to Cheat Sheets

📉 Price Oracle Manipulation

OWASP Smart Contract Top 10 (2025) - SC02

HIGH RISK

📋 What Is It?

Price Oracle Manipulation occurs when a contract makes a financial decision - how much to lend, mint, redeem, or liquidate - based on a price read from a source an attacker can move within the same transaction. Nothing in the contract's own logic is "buggy"; the flaw is trusting a manipulable number. A DEX pool's reserves, a swap quote, or a token balance all reflect instantaneous market state, and a flash loan lets anyone move that state risk-free and atomically. The attacker distorts the price, trades against the contract while it reads the distorted value, and repays the loan in the same block.

SC02 OWASP Rank
Flash Loan Key Enabler
Atomic Exploit

⚠️ Common Exploits

  • Spot Reserves: Price derived from getReserves() of one pool
  • Swap Quotes: getAmountsOut() used as a price feed
  • balanceOf Valuation: LP/share value from raw pool balances
  • Single Source: One venue with no independent cross-check
  • Stale Feed: latestRoundData used without a freshness check
  • No Sanity Bounds: A 90% price swing accepted as truth
  • Flash-Loan Skew: Manipulate-borrow-repay in one atomic tx

🔴 Attack Flow

1. Flash-loan a large amount of capital
↓
2. Swap heavily to skew the target pool's price
↓
3. Victim reads the distorted spot price
↓
4. Borrow / mint / redeem against the fake value
↓
5. IMPACT: Repay loan, keep the difference - pool drained!

❌ Vulnerable Code

// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; // Prices collateral from ONE Uniswap V2 pool's spot reserves. // A flash-loaned swap moves the reserves in the same tx → over-borrow. interface IUniswapV2Pair { function getReserves() external view returns (uint112, uint112, uint32); } contract LendingVulnerable { IUniswapV2Pair public pair; // WETH / TOKEN pool function collateralValue(uint256 amt) public view returns (uint256) { (uint112 rWeth, uint112 rToken, ) = pair.getReserves(); uint256 price = uint256(rWeth) * 1e18 / uint256(rToken); return amt * price / 1e18; ← spot price = attacker-controlled } function borrow(uint256 collateral, uint256 borrowAmount) external { require(borrowAmount <= collateralValue(collateral), "under-collat"); // ... transfer collateral in, send borrowAmount out ... } }

✅ Secure Code

// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; // Prices collateral from a checked Chainlink feed, cross-checked // against a TWAP. No single on-chain pool can move an aggregated feed. interface AggregatorV3Interface { function latestRoundData() external view returns ( uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); function decimals() external view returns (uint8); } contract LendingSecure { AggregatorV3Interface public feed; // TOKEN / USD uint256 public constant MAX_STALENESS = 1 hours; function _price() internal view returns (uint256) { (uint80 roundId, int256 answer, , uint256 updatedAt, uint80 answeredInRound) = feed.latestRoundData(); require(answer > 0, "bad price"); // sign require(answeredInRound >= roundId, "stale round"); // completeness require(block.timestamp - updatedAt <= MAX_STALENESS, "stale"); return uint256(answer); } function collateralValue(uint256 amt) public view returns (uint256) { return amt * _price() / (10 ** feed.decimals()); } // Also: cross-check vs a 30-min Uniswap V3 TWAP and reject // divergence > 2%; pause on stale/disagreeing sources (fail closed). }

✓ Prevention Checklist

  • Use robust decentralized feeds (Chainlink), not spot reads
  • Check updatedAt freshness on every feed read
  • Reject non-positive answers and incomplete rounds
  • Use a TWAP for any on-chain source you must read
  • Cross-check multiple independent sources
  • Enforce deviation and min/max sanity bounds
  • Never price from balanceOf or raw reserves
  • Value LP tokens via fair-value formula, not live balances
  • Add circuit breakers, pauses, and per-block rate limits
  • Assume unlimited flash-loaned capital in the threat model
  • Fork-test the pricing path under a simulated flash loan

🔍 Detection & Tools

Analysis Tools:

Slither Mythril Echidna Foundry Manticore

Oracles & Infra:

Chainlink Uniswap V3 TWAP Tenderly OpenZeppelin

How to Test:

  • Fork-test: flash-loan, skew the pool, call the victim function
  • Assert the protocol does NOT over-lend / mis-mint / mis-liquidate
  • Fuzz/invariant: no one-tx sequence extracts more than deposited
  • Grep pricing paths for getReserves, getAmountsOut, balanceOf
  • Monitor: feed staleness, large single-tx swaps in priced pools

🌍 Real-World

  • Flash-loan collateral mispricing (bZx / Harvest class): A lending or vault protocol priced an asset from a DEX pool's spot state; an attacker flash-loaned, skewed the pool, then borrowed or redeemed against the inflated valuation, leaving bad debt for honest depositors.
  • LP-token / balanceOf mispricing class: A protocol valued LP-token collateral from live pool balances; an attacker inflated those balances within the transaction and borrowed against value that did not exist.
  • Stale / single-source feed failure: A protocol relied on one source with no freshness check or fallback; trades and liquidations executed against a price that no longer matched the market.
  • Enabling factor: In each class the root cause is trusting a manipulable price; flash-loan atomicity turns "expensive to move" into "free to move."

📌 Quick Tips

  • DO NOT use getReserves/getAmountsOut as a price
  • DO NOT value shares/LP tokens by balanceOf
  • DO NOT trust a feed without a freshness check
  • DO use decentralized feeds and TWAPs
  • DO cross-check sources and bound deviation
  • DO design for unlimited atomic capital
  • DO add circuit breakers and rate limits