Difficulty: Intermediate
The oracle problem is one of the fundamental challenges in blockchain development. Smart contracts are deterministic and isolated by design: they cannot access data from outside the blockchain, such as asset prices, weather data, sports scores, or any other real-world information. Oracles bridge this gap by bringing off-chain data on-chain, but this introduces a trust assumption. If an oracle provides incorrect data, any smart contract relying on it will execute based on wrong information, potentially causing catastrophic financial losses.
Chainlink is the most widely used decentralized oracle network. Rather than relying on a single data provider, Chainlink aggregates data from multiple independent node operators who each fetch data from multiple data sources. The aggregated result is posted on-chain as a price feed that any contract can read. Chainlink price feeds update based on deviation thresholds (e.g., when the price moves more than 0.5%) or heartbeat intervals (e.g., every 3600 seconds), whichever triggers first. This decentralized approach significantly reduces the risk of manipulation or single points of failure.
Time-Weighted Average Price (TWAP) oracles take a different approach by computing prices from on-chain data. Uniswap V2 and V3 both provide built-in TWAP functionality by accumulating price data over time. Instead of using the spot price (which can be manipulated within a single transaction via flash loans), TWAP calculates the average price over a window (e.g., 30 minutes). The longer the TWAP window, the more expensive manipulation becomes because an attacker would need to keep the price distorted for the entire averaging period, which is economically prohibitive.
Oracle manipulation is a major attack vector in DeFi. Spot price oracles (reading the current AMM price) are extremely vulnerable: an attacker can use a flash loan to make a large swap, distort the pool price, interact with a protocol that reads that price, and then swap back - all in one transaction. This has led to hundreds of millions in losses across DeFi protocols. Defenses include using TWAP instead of spot prices, using Chainlink feeds instead of AMM prices for critical operations, implementing price deviation checks that reject suspiciously large price movements, and using multiple oracle sources with a median or minimum aggregation strategy.
Modern oracle design also considers freshness and staleness. A price feed that hasn't updated in hours may be dangerously stale, especially in volatile markets. Protocols should check the `updatedAt` timestamp from Chainlink feeds and revert if the data is too old. Additionally, Chainlink V2 introduced off-chain reporting (OCR) which reduces on-chain costs by having nodes reach consensus off-chain and posting a single aggregated update, making price feeds more economically sustainable.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
contract PriceConsumer {
AggregatorV3Interface internal immutable priceFeed;
uint256 public constant STALENESS_THRESHOLD = 3600; // 1 hour
/
* Network: Ethereum Mainnet
* Aggregator: ETH/USD
* Address: 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419
*/
constructor(address _priceFeed) {
priceFeed = AggregatorV3Interface(_priceFeed);
}
/// @notice Get the latest ETH/USD price with staleness check
function getLatestPrice() public view returns (int256 price, uint256 updatedAt) {
(
uint80 roundId,
int256 answer,
/* uint256 startedAt */,
uint256 timeStamp,
uint80 answeredInRound
) = priceFeed.latestRoundData();
// Validation checks
require(answer > 0, "Invalid price: negative or zero");
require(timeStamp > 0, "Round not complete");
require(answeredInRound >= roundId, "Stale price: round not current");
require(
block.timestamp - timeStamp <= STALENESS_THRESHOLD,
"Price feed is stale"
);
return (answer, timeStamp);
}
/// @notice Get price with a specific number of decimals
function getPriceInUSD() external view returns (uint256) {
(int256 price, ) = getLatestPrice();
// Chainlink ETH/USD has 8 decimals, normalize to 18
uint8 decimals = priceFeed.decimals();
return uint256(price) * 10 (18 - decimals);
}
/// @notice Calculate USD value of an ETH amount
function getETHValueInUSD(uint256 ethAmount) external view returns (uint256) {
(int256 price, ) = getLatestPrice();
uint8 decimals = priceFeed.decimals();
// ethAmount is in wei (18 decimals), price has `decimals` decimals
return (ethAmount * uint256(price)) / 10 decimals;
}
}
/// @notice A lending protocol that uses Chainlink for collateral valuation
contract ChainlinkLending {
AggregatorV3Interface public immutable collateralPriceFeed;
uint256 public constant LTV = 75; // 75%
uint256 public constant STALENESS_THRESHOLD = 3600;
mapping(address => uint256) public collateral; // ETH collateral
mapping(address => uint256) public debt; // USD debt
constructor(address _priceFeed) {
collateralPriceFeed = AggregatorV3Interface(_priceFeed);
}
function getCollateralPrice() internal view returns (uint256) {
(
uint80 roundId,
int256 answer,
,
uint256 updatedAt,
uint80 answeredInRound
) = collateralPriceFeed.latestRoundData();
require(answer > 0, "Invalid price");
require(updatedAt > 0, "Incomplete round");
require(answeredInRound >= roundId, "Stale round");
require(block.timestamp - updatedAt <= STALENESS_THRESHOLD, "Stale price");
return uint256(answer); // 8 decimals
}
function depositCollateral() external payable {
collateral[msg.sender] += msg.value;
}
function borrow(uint256 usdAmount) external {
uint256 price = getCollateralPrice();
uint256 collateralValueUSD = (collateral[msg.sender] * price) / 1e18;
uint256 maxBorrow = (collateralValueUSD * LTV) / 100;
require(debt[msg.sender] + usdAmount <= maxBorrow, "Exceeds LTV");
debt[msg.sender] += usdAmount;
// In production: transfer stablecoin to borrower
}
}
This demonstrates proper Chainlink integration with essential safety checks: validating the price is positive, the round is complete, the data isn't stale, and normalizing decimals. The lending example shows how price feeds are used for collateral valuation.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @notice Simplified TWAP oracle for educational purposes
contract TWAPOracle {
struct Observation {
uint256 timestamp;
uint256 priceCumulative;
}
Observation[] public observations;
uint256 public latestPrice;
uint256 public constant TWAP_PERIOD = 30 minutes;
uint256 public constant MIN_OBSERVATIONS = 2;
event PriceUpdated(uint256 price, uint256 priceCumulative);
/// @notice Record a new price observation (called by keeper/bot)
function update(uint256 currentPrice) external {
latestPrice = currentPrice;
uint256 cumulative;
if (observations.length > 0) {
Observation memory last = observations[observations.length - 1];
uint256 elapsed = block.timestamp - last.timestamp;
cumulative = last.priceCumulative + (currentPrice * elapsed);
} else {
cumulative = 0;
}
observations.push(Observation({
timestamp: block.timestamp,
priceCumulative: cumulative
}));
emit PriceUpdated(currentPrice, cumulative);
}
/// @notice Get the TWAP over the configured period
function getTWAP() external view returns (uint256) {
require(observations.length >= MIN_OBSERVATIONS, "Insufficient data");
Observation memory latest = observations[observations.length - 1];
// Find the observation closest to TWAP_PERIOD ago
uint256 targetTime = latest.timestamp - TWAP_PERIOD;
uint256 oldIndex = 0;
for (uint256 i = observations.length - 1; i > 0; i--) {
if (observations[i].timestamp <= targetTime) {
oldIndex = i;
break;
}
}
Observation memory old = observations[oldIndex];
uint256 elapsed = latest.timestamp - old.timestamp;
require(elapsed > 0, "Same timestamp");
return (latest.priceCumulative - old.priceCumulative) / elapsed;
}
/// @notice Get spot price (more manipulable, use TWAP for critical operations)
function getSpotPrice() external view returns (uint256) {
return latestPrice;
}
}
This simplified TWAP oracle accumulates price * time values and computes the time-weighted average over a configurable period. TWAP is more resistant to flash loan manipulation than spot prices because an attacker would need to sustain the price distortion over the entire averaging window.
Oracle, Chainlink, TWAP, Oracle Manipulation