Oracle & Price Manipulation Vulnerability Patterns
Distilled from 300+ Code4rena audit findings. Oracle-related vulnerabilities form the largest category of critical findings in the dataset (~64 files). Every DeFi protocol that touches prices, collateral, or asset valuation is exposed to this class of attack.
Category Overview
Oracle manipulation attacks follow two primary vectors:
- Spot price manipulation — flash loans move an on-chain price right before a price read
- Oracle configuration bugs — missing validation allows stale, out-of-range, or admin-controlled prices to be accepted as valid
Off-Chain / Admin-Controlled Values
1. Trust in Admin-Supplied Critical Values Without Validation
Pattern: A protocol allows an admin or off-chain actor to set a value (e.g., expectedPayout, targetPrice, collateralRatio) that directly controls how much users receive or owe. No on-chain validation bounds this value against market reality.
Real-world example: A lottery contract sets expectedPayout off-chain. An admin can set it to drain the prize pool in a single claim. The contract treats admin input as ground truth.
Fix: For any value that determines fund flows:
- Compute it on-chain from raw data when possible
- If admin-supplied, validate against on-chain oracle bounds:
require(adminValue >= oracle.price() * 0.95 && adminValue <= oracle.price() * 1.05) - Apply time-locks before accepting new values
Chainlink Oracle Misconfiguration
2. Missing Min/Max Price Circuit Breakers
Pattern: Chainlink aggregators for many assets (especially newly listed tokens) have minAnswer and maxAnswer bounds configured off-chain. If the asset's price drops below minAnswer, Chainlink continues to report minAnswer rather than the true price. Protocols that only check answer > 0 will consume the artificially high floor price, over-valuing collateral and enabling under-collateralized borrowing.
Vulnerable pattern:
(, int256 price,,,) = chainlinkFeed.latestRoundData();
require(price > 0, "invalid price");
return uint256(price);
Fix:
(, int256 price,, uint256 updatedAt,) = chainlinkFeed.latestRoundData();
require(price > int256(MIN_PRICE) && price < int256(MAX_PRICE), "price out of circuit breaker bounds");
require(block.timestamp - updatedAt <= MAX_PRICE_AGE, "stale price");
return uint256(price);
3. Stale Price Acceptance
Pattern: latestRoundData() is called but the updatedAt timestamp is not checked. During periods of network congestion or Chainlink node downtime, the feed may not update for hours. A protocol accepting stale prices can be exploited when the true market price has moved significantly.
Fix: Enforce a maximum age: require(block.timestamp - updatedAt <= 3600, "price older than 1h"). The appropriate maximum age depends on the asset's volatility and the protocol's risk tolerance (typically 1–24 hours for most assets).
4. Single-Oracle Dependency
Pattern: Using only one oracle source creates a single point of failure. If the oracle is compromised, goes offline, or reports incorrect data, the protocol has no fallback.
Fix: Use a primary oracle with a secondary fallback. Compare the two values: require(abs(primary - secondary) / primary < 0.02, "oracle deviation > 2%"). If they diverge, pause the protocol.
On-Chain Spot Price Manipulation
5. AMM Spot Price as Collateral Oracle
Pattern: A protocol uses the current spot price from a Uniswap V2 or V3 pool (ratio of reserves) as the canonical price for collateral valuation. Flash loans can temporarily push this price to any value within a single transaction, allowing an attacker to borrow against inflated collateral.
Fix: Never use AMM spot prices for lending/minting decisions. Use:
- Chainlink for widely-listed assets
- TWAP (Time-Weighted Average Price) with a window ≥ 30 minutes for long-tail assets
6. Inadequate TWAP Window
Pattern: A protocol uses a TWAP oracle but with a window that is too short (e.g., 1–5 minutes). A well-capitalized attacker can sustain the price manipulation for the duration of the TWAP window, making the manipulation "average out" to the desired manipulated price.
Fix: TWAP windows should be:
- Minimum 30 minutes for volatile assets
- 1–4 hours for collateral in conservative lending protocols
- Document and justify the chosen window
Cross-Chain and Multi-Chain Issues
7. Hardcoded Block-Time Constants
Pattern: Oracle-related logic uses block count–based timeouts (e.g., MIN_AUCTION_DURATION = 7200 blocks) that assume Ethereum's ~12s block time. On BSC (~3s) or Avalanche (~2s), this window is 4–6× shorter than intended, enabling attacks that depend on duration (e.g., Dutch auctions, grace periods, TWAP accumulation).
Fix: Use block.timestamp for duration-based logic. If block counts are necessary, make the constant configurable or use chain-specific deployments.
8. Cross-Chain Price Discrepancies
Pattern: A protocol operating on multiple chains uses different oracle configurations per chain. Price discrepancies between chains can be exploited via cross-chain arbitrage if the protocol doesn't account for bridging costs and delays.
Fix: Ensure oracle configurations are consistent across deployments. Apply conservative staleness thresholds that account for potential chain-specific delays.
Manipulation via Protocol's Own State
9. Self-Referential Price Calculation
Pattern: A protocol calculates the price of its own token or derivative using a formula that can be manipulated by interacting with the protocol itself. For example, a vault token priced as totalAssets / totalSupply can be manipulated by donating assets or burning tokens.
Fix: Add minimum liquidity requirements, fee-on-deposit mechanisms, and track virtual balances separately from actual token balances.
Audit Checklist
- Every
latestRoundData()call: areminAnswer,maxAnswer, andupdatedAtvalidated? - Are AMM spot prices used anywhere for collateral valuation? → Replace with TWAP or Chainlink
- TWAP windows: are they ≥ 30 minutes for volatile assets?
- Single oracle dependency: is there a fallback?
- Admin-supplied values: are they bounded against on-chain market prices?
- Block-number–based time constants: are they correct for all target chains?
- Is there a circuit breaker / pause if oracle values deviate unexpectedly?
- Is the protocol's own token used as collateral, creating reflexive risk?