Oracle Manipulation Remediation
How to eliminate oracle manipulation vulnerabilities by replacing spot prices with time-weighted averages and using decentralized oracle networks.
Oracle Manipulation Remediation
Overview
Oracle manipulation vulnerabilities arise when a protocol reads on-chain price data from a source that can be altered within a single block or transaction. The remediation is to use price sources that require sustained capital to manipulate across multiple blocks: time-weighted average prices (TWAPs) from AMMs, or decentralized oracle networks that aggregate prices off-chain.
Related Detector: Oracle Manipulation
Recommended Fix
Before (Vulnerable)
interface IUniswapV2Pair {
function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32);
}
contract VulnerableProtocol {
IUniswapV2Pair public pair;
function getPrice() public view returns (uint256) {
(uint112 r0, uint112 r1, ) = pair.getReserves();
// VULNERABLE: spot price — manipulable in the same block via flash loan
return uint256(r1) * 1e18 / uint256(r0);
}
}
After (Fixed)
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract SafeProtocol {
AggregatorV3Interface public oracle;
uint256 public constant MAX_STALENESS = 1 hours;
function getPrice() public view returns (uint256) {
(, int256 price, , uint256 updatedAt, ) = oracle.latestRoundData();
// Validate the price is recent and positive
require(block.timestamp - updatedAt <= MAX_STALENESS, "Stale oracle price");
require(price > 0, "Invalid oracle price");
return uint256(price);
}
}
Alternative Mitigations
Uniswap V3 TWAP (on-chain, no external dependency):
interface IUniswapV3Pool {
function observe(uint32[] calldata secondsAgos)
external view returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulatives);
}
contract TWAPOracle {
IUniswapV3Pool public pool;
uint32 public constant TWAP_PERIOD = 1800; // 30 minutes
function getTWAP() public view returns (int24 avgTick) {
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = TWAP_PERIOD;
secondsAgos[1] = 0;
(int56[] memory tickCumulatives, ) = pool.observe(secondsAgos);
int56 delta = tickCumulatives[1] - tickCumulatives[0];
avgTick = int24(delta / int56(uint56(TWAP_PERIOD)));
}
}
Multi-oracle median — take the median of multiple independent oracle sources to resist single-source manipulation:
function getMedianPrice(address[] calldata oracles) public view returns (uint256) {
uint256[] memory prices = new uint256[](oracles.length);
for (uint256 i = 0; i < oracles.length; i++) {
(, int256 price, , uint256 updatedAt, ) =
AggregatorV3Interface(oracles[i]).latestRoundData();
require(price > 0 && block.timestamp - updatedAt < MAX_STALENESS);
prices[i] = uint256(price);
}
return _median(prices);
}
Common Mistakes
Using slot0 from Uniswap V3 — slot0.sqrtPriceX96 is the current tick, not a TWAP. It is as manipulable as Uniswap V2 reserves.
Ignoring oracle staleness — Chainlink feeds can go stale during network congestion. Always check updatedAt against the expected heartbeat for the specific feed.
Mixing oracle timeframes — using a 5-minute TWAP for one asset and a spot price for another in the same calculation exposes the spot-priced asset to manipulation.