What are Price Oracles?
Price oracles are smart contracts or external services that provide price data to DeFi protocols. Since blockchains cannot directly access off-chain data, protocols rely on oracles to determine asset prices for:
- Lending protocols: Calculating collateral values and liquidation thresholds
- DEXs: Providing reference prices for swaps
- Derivatives: Settling futures and options contracts
- Stablecoins: Maintaining price pegs
- Synthetic assets: Minting and redeeming synths
The Oracle Problem
📊 Common Oracle Sources
⚠️ Risky Sources
- • Single DEX spot prices
- • Low-liquidity pool prices
- • On-chain reserves ratios
- • Single block price snapshots
✅ Safer Sources
- • Chainlink price feeds
- • Uniswap V3 TWAP oracles
- • Multi-source aggregated prices
- • Time-weighted calculations
Types of Oracle Attacks
1. Spot Price Manipulation
Using flash loans to temporarily move prices in a DEX pool, then exploiting protocols that read these manipulated prices within the same transaction.
2. TWAP Manipulation
Sustained price manipulation over multiple blocks to skew time-weighted average prices. More expensive but can bypass TWAP protections.
3. Oracle Front-Running
Watching for oracle update transactions and front-running them to exploit the price difference before and after the update.
4. Governance Oracle Attacks
Manipulating governance votes to change oracle sources or parameters to attacker-controlled addresses.
Spot Price Manipulation Deep Dive
The most common oracle attack uses flash loans to manipulate spot prices. Here's exactly how it works:
Flash Loan Large Amount
Attacker borrows millions in tokens from Aave/dYdX (no collateral needed)
Manipulate DEX Pool 💥
Swap large amount to drastically move the price in a liquidity pool
Exploit Target Protocol 💥
Protocol reads manipulated price → allows over-borrowing, unfair liquidations, or theft
Restore Price & Repay
Swap back to restore price, repay flash loan, keep the profit
All in One Transaction
TWAP Oracle Weaknesses
Time-Weighted Average Price (TWAP) oracles are more resistant to manipulation but are not foolproof. Here are their vulnerabilities:
How TWAP Works
1// Simplified TWAP calculation
2// Price is accumulated over time, then averaged
3
4priceCumulativeLast += price * timeElapsed;
5
6// To get TWAP between two points:
7twap = (priceCumulative2 - priceCumulative1) / (time2 - time1);
8
9// Example:
10// If price was $100 for 10 blocks, then $200 for 10 blocks
11// TWAP = ($100 * 10 + $200 * 10) / 20 = $150TWAP Vulnerabilities
⏰ Short TWAP Windows
TWAP windows of 10-30 minutes can still be manipulated by wealthy attackers who sustain price manipulation across multiple blocks.
💧 Low Liquidity Pools
TWAP from low liquidity pools is cheap to manipulate. Cost scales inversely with pool depth.
🆕 New Pool Attacks
Attackers can create new pools with manipulated prices that have minimal history, skewing TWAP calculations.
⛏️ Block Stuffing
In low-activity periods, attackers can fill blocks with their own transactions to control price observations.
Real-World Oracle Attack Examples
Mango Markets - $114M
October 2022Attacker Avraham Eisenberg manipulated the MNGO token price on FTX and other exchanges, then used the inflated collateral value to borrow $114M from Mango Markets on Solana.
Cream Finance - $130M
October 2021Flash loan attack manipulated the price of yUSD through Yearn's price oracle, allowing the attacker to borrow far more than their collateral was worth.
Harvest Finance - $34M
October 2020Attacker used flash loans to manipulate Curve's stablecoin pool prices, then arbitraged the difference in Harvest's vault share prices.
BonqDAO - $120M
February 2023Attacker exploited Tellor oracle's update mechanism by submitting false price data for WALBT token, inflating collateral values to drain the protocol.
Vulnerable Code Patterns
Let's examine common vulnerable patterns and their secure alternatives:
❌ Pattern 1: Direct DEX Price Reading
1// 🚨 VULNERABLE: Reading spot price from Uniswap V2
2contract VulnerableLending {
3 IUniswapV2Pair public pair;
4
5 function getPrice() public view returns (uint256) {
6 (uint112 reserve0, uint112 reserve1, ) = pair.getReserves();
7 // Direct reserve ratio = easily manipulated with flash loans!
8 return (uint256(reserve1) * 1e18) / uint256(reserve0);
9 }
10
11 function borrow(uint256 collateralAmount) external {
12 uint256 price = getPrice(); // 💥 Manipulated price
13 uint256 collateralValue = collateralAmount * price / 1e18;
14 uint256 borrowableAmount = collateralValue * 75 / 100;
15
16 // Attacker inflates price → borrows way more than allowed
17 _mint(msg.sender, borrowableAmount);
18 }
19}❌ Pattern 2: Single Source Oracle
1// 🚨 VULNERABLE: Single oracle source without validation
2contract VulnerablePriceConsumer {
3 AggregatorV3Interface public priceFeed;
4
5 function getLatestPrice() public view returns (int256) {
6 (
7 ,
8 int256 price,
9 ,
10 , // Missing: updatedAt check!
11
12 ) = priceFeed.latestRoundData();
13
14 // No staleness check!
15 // No price bounds validation!
16 // No backup oracle!
17
18 return price;
19 }
20}✅ Secure Implementation
1// ✅ SECURE: Robust oracle implementation
2contract SecureLending {
3 AggregatorV3Interface public primaryOracle;
4 AggregatorV3Interface public fallbackOracle;
5
6 uint256 public constant STALENESS_THRESHOLD = 1 hours;
7 uint256 public constant MAX_PRICE_DEVIATION = 10; // 10%
8
9 function getPrice() public view returns (uint256) {
10 // Get primary oracle price with full validation
11 (
12 uint80 roundId,
13 int256 price,
14 ,
15 uint256 updatedAt,
16 uint80 answeredInRound
17 ) = primaryOracle.latestRoundData();
18
19 // ✅ Check 1: Price is positive
20 require(price > 0, "Invalid price");
21
22 // ✅ Check 2: Data is not stale
23 require(
24 block.timestamp - updatedAt < STALENESS_THRESHOLD,
25 "Stale price data"
26 );
27
28 // ✅ Check 3: Round is complete
29 require(answeredInRound >= roundId, "Incomplete round");
30
31 // ✅ Check 4: Cross-reference with fallback
32 uint256 primaryPrice = uint256(price);
33 uint256 fallbackPrice = _getFallbackPrice();
34
35 uint256 deviation = _calculateDeviation(primaryPrice, fallbackPrice);
36 require(deviation <= MAX_PRICE_DEVIATION, "Price deviation too high");
37
38 return primaryPrice;
39 }
40
41 function _getFallbackPrice() internal view returns (uint256) {
42 (, int256 price, , uint256 updatedAt, ) = fallbackOracle.latestRoundData();
43 require(price > 0 && block.timestamp - updatedAt < STALENESS_THRESHOLD, "Fallback invalid");
44 return uint256(price);
45 }
46
47 function _calculateDeviation(uint256 a, uint256 b) internal pure returns (uint256) {
48 uint256 diff = a > b ? a - b : b - a;
49 return (diff * 100) / ((a + b) / 2);
50 }
51}Prevention Strategies
Use Chainlink Oracles
Chainlink aggregates data from multiple sources and has economic security through node staking. It's the gold standard for price feeds.
Use TWAP with Long Windows
If using on-chain oracles, implement TWAP with windows of at least 30 minutes. Longer windows = more expensive to manipulate.
Multi-Oracle Aggregation
Use multiple oracle sources and compare prices. Reject transactions if sources deviate significantly from each other.
Circuit Breakers
Implement price deviation limits that pause operations if prices move too much too quickly (e.g., >20% in one block).
Staleness Checks
Always check that oracle data is recent. Stale data can be exploited if real prices have moved significantly.
Liquidity Requirements
Only use prices from pools with sufficient liquidity. Set minimum TVL thresholds for accepted price sources.
Defense in Depth
Chainlink Best Practices
Chainlink is the most widely used oracle solution. Here's how to integrate it properly:
1// ✅ Complete Chainlink integration with all best practices
2// SPDX-License-Identifier: MIT
3pragma solidity ^0.8.19;
4
5import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
6
7contract ChainlinkPriceConsumer {
8 AggregatorV3Interface internal immutable priceFeed;
9
10 uint256 public constant STALENESS_SECONDS = 3600; // 1 hour
11 uint256 public constant MIN_PRICE = 1e6; // Minimum sanity check
12 uint256 public constant MAX_PRICE = 1e12; // Maximum sanity check
13
14 error StalePrice(uint256 updatedAt, uint256 currentTime);
15 error InvalidPrice(int256 price);
16 error IncompleteRound(uint80 roundId, uint80 answeredInRound);
17 error SequencerDown();
18
19 // For L2s like Arbitrum/Optimism
20 AggregatorV3Interface internal immutable sequencerUptimeFeed;
21 uint256 public constant GRACE_PERIOD_TIME = 3600;
22
23 constructor(address _priceFeed, address _sequencerFeed) {
24 priceFeed = AggregatorV3Interface(_priceFeed);
25 sequencerUptimeFeed = AggregatorV3Interface(_sequencerFeed);
26 }
27
28 function getLatestPrice() public view returns (uint256) {
29 // ✅ L2: Check sequencer uptime (Arbitrum/Optimism)
30 if (address(sequencerUptimeFeed) != address(0)) {
31 _checkSequencerUptime();
32 }
33
34 (
35 uint80 roundId,
36 int256 price,
37 ,
38 uint256 updatedAt,
39 uint80 answeredInRound
40 ) = priceFeed.latestRoundData();
41
42 // ✅ Check 1: Price must be positive
43 if (price <= 0) {
44 revert InvalidPrice(price);
45 }
46
47 // ✅ Check 2: Price within sanity bounds
48 uint256 uPrice = uint256(price);
49 if (uPrice < MIN_PRICE || uPrice > MAX_PRICE) {
50 revert InvalidPrice(price);
51 }
52
53 // ✅ Check 3: Data freshness
54 if (block.timestamp - updatedAt > STALENESS_SECONDS) {
55 revert StalePrice(updatedAt, block.timestamp);
56 }
57
58 // ✅ Check 4: Round completeness
59 if (answeredInRound < roundId) {
60 revert IncompleteRound(roundId, answeredInRound);
61 }
62
63 return uPrice;
64 }
65
66 function _checkSequencerUptime() internal view {
67 (, int256 answer, , uint256 startedAt, ) =
68 sequencerUptimeFeed.latestRoundData();
69
70 // answer == 0: Sequencer is up
71 // answer == 1: Sequencer is down
72 bool isSequencerUp = answer == 0;
73 if (!isSequencerUp) {
74 revert SequencerDown();
75 }
76
77 // Make sure the grace period has passed after sequencer comes back up
78 uint256 timeSinceUp = block.timestamp - startedAt;
79 if (timeSinceUp <= GRACE_PERIOD_TIME) {
80 revert SequencerDown();
81 }
82 }
83}🔗 Chainlink Feed Registry
Use the Feed Registry on mainnet to dynamically get price feeds:
0x47Fb2585D2C56Fe188D0E6ec628a38b74fCeeeDfTesting for Oracle Attacks
Use these Foundry tests to verify your oracle implementation is secure:
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.19;
3
4import "forge-std/Test.sol";
5import "../src/SecureLending.sol";
6
7contract OracleSecurityTest is Test {
8 SecureLending lending;
9 MockOracle mockOracle;
10
11 function setUp() public {
12 mockOracle = new MockOracle();
13 lending = new SecureLending(address(mockOracle));
14 }
15
16 // ✅ Test: Reject stale prices
17 function test_RevertWhen_PriceIsStale() public {
18 mockOracle.setUpdatedAt(block.timestamp - 2 hours);
19
20 vm.expectRevert("Stale price data");
21 lending.getPrice();
22 }
23
24 // ✅ Test: Reject negative prices
25 function test_RevertWhen_PriceIsNegative() public {
26 mockOracle.setPrice(-1);
27
28 vm.expectRevert("Invalid price");
29 lending.getPrice();
30 }
31
32 // ✅ Test: Reject zero prices
33 function test_RevertWhen_PriceIsZero() public {
34 mockOracle.setPrice(0);
35
36 vm.expectRevert("Invalid price");
37 lending.getPrice();
38 }
39
40 // ✅ Test: Reject large price deviations
41 function test_RevertWhen_PriceDeviationTooHigh() public {
42 // Primary oracle: $100
43 mockOracle.setPrice(100e8);
44 // Fallback oracle: $150 (50% deviation)
45 mockFallbackOracle.setPrice(150e8);
46
47 vm.expectRevert("Price deviation too high");
48 lending.getPrice();
49 }
50
51 // ✅ Fuzz test: Price manipulation resistance
52 function testFuzz_PriceManipulationResistance(uint256 manipulatedPrice) public {
53 // Bound to reasonable price range
54 manipulatedPrice = bound(manipulatedPrice, 1, type(uint128).max);
55
56 // Set manipulated price
57 mockOracle.setPrice(int256(manipulatedPrice));
58
59 // Should only accept if within deviation bounds
60 if (_isWithinBounds(manipulatedPrice)) {
61 uint256 price = lending.getPrice();
62 assertGt(price, 0);
63 } else {
64 vm.expectRevert();
65 lending.getPrice();
66 }
67 }
68
69 // ✅ Test: Flash loan attack simulation
70 function test_FlashLoanAttackPrevention() public {
71 // Simulate flash loan by manipulating price in same block
72 uint256 normalPrice = 100e8;
73 uint256 manipulatedPrice = 1000e8; // 10x manipulation
74
75 mockOracle.setPrice(int256(normalPrice));
76 uint256 borrowableNormal = lending.calculateBorrowable(1 ether);
77
78 // Attacker tries to manipulate
79 mockOracle.setPrice(int256(manipulatedPrice));
80
81 // Should be caught by deviation check
82 vm.expectRevert("Price deviation too high");
83 lending.calculateBorrowable(1 ether);
84 }
85}🛠️ Testing Tools
Foundry Fuzzing
forge test --fuzz-runs 10000Slither Oracle Check
slither . --detect oracle-manipulate