Oracle Manipulation
Detects smart contracts that rely on manipulable on-chain price sources, such as DEX spot prices or single-source price feeds, without time-weighted averaging.
Oracle Manipulation
Overview
Remediation Guide: How to Fix Oracle Manipulation
The oracle manipulation detector identifies contracts that use on-chain price data from sources that can be manipulated within a single block or transaction. The primary attack vectors are DEX spot prices (getReserves(), slot0()), single-source token balances used as prices, and unvalidated off-chain oracle data.
Unlike the flash-loan detector which focuses on the flash loan execution pattern, this detector examines the oracle architecture itself: whether prices are time-weighted, whether multiple sources are aggregated, and whether there are circuit breakers against large single-block price deviations. Sigvex traces calls to known price-reading functions and checks whether the values flow into financial calculations with adequate protections.
Why This Is an Issue
Oracle manipulation attacks do not require flash loans — a sufficiently large trade in the same block can manipulate a spot price oracle. The Mango Markets exploit ($117M, October 2022) used coordinated large trades to inflate the price of MNGO tokens as collateral. The Euler Finance hack ($197M, March 2023) combined oracle dependency with a donation attack.
Even well-intentioned protocols can fall into this pattern when they use token.balanceOf(address(this)) as a price proxy (balance can be manipulated by sending tokens directly) or rely on a single Chainlink feed without staleness checks.
How to Resolve
// Before: Vulnerable — uses single DEX spot price
function getPrice(address tokenA, address tokenB) public view returns (uint256) {
(uint112 reserveA, uint112 reserveB,) = IUniswapV2Pair(pair).getReserves();
return (uint256(reserveB) * 1e18) / uint256(reserveA); // Spot price
}
// After: Multi-source with staleness checks
contract SecureOracle {
AggregatorV3Interface public primaryFeed;
AggregatorV3Interface public secondaryFeed;
uint256 constant MAX_DEVIATION_BPS = 200; // 2% max divergence
uint256 constant MAX_STALENESS = 3600; // 1 hour max age
function getPrice() public view returns (uint256) {
(, int256 price1, , uint256 ts1, ) = primaryFeed.latestRoundData();
(, int256 price2, , uint256 ts2, ) = secondaryFeed.latestRoundData();
require(block.timestamp - ts1 <= MAX_STALENESS, "Primary feed stale");
require(block.timestamp - ts2 <= MAX_STALENESS, "Secondary feed stale");
require(price1 > 0 && price2 > 0, "Invalid price");
// Verify two feeds agree within tolerance
uint256 deviation = _absDiff(uint256(price1), uint256(price2)) * 10000 / uint256(price1);
require(deviation <= MAX_DEVIATION_BPS, "Price feeds diverge");
return uint256(price1);
}
}
Examples
Vulnerable Code
contract VulnerableAMM {
address public oracle;
// Uses instantaneous pool balance ratio as price — manipulable
function getTokenPrice(address pool, address token0, address token1)
public view returns (uint256)
{
uint256 bal0 = IERC20(token0).balanceOf(pool);
uint256 bal1 = IERC20(token1).balanceOf(pool);
// VULNERABLE: balanceOf can be manipulated by direct token transfer
return (bal1 * 1e18) / bal0;
}
function liquidate(address user) external {
uint256 price = getTokenPrice(pool, token0, token1);
uint256 collateralValue = collateralBalance[user] * price / 1e18;
require(collateralValue < debtBalance[user], "Not undercollateralized");
// Process liquidation...
}
}
Fixed Code
contract SecureAMM {
// Uses Uniswap V3 TWAP over 30-minute window
function getTokenPriceTwap(address pool) public view returns (uint256) {
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = 1800; // 30 minutes ago
secondsAgos[1] = 0; // now
(int56[] memory tickCumulatives,) = IUniswapV3Pool(pool).observe(secondsAgos);
int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0];
int24 arithmeticMeanTick = int24(tickCumulativesDelta / 1800);
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(arithmeticMeanTick);
return FullMath.mulDiv(uint256(sqrtPriceX96) ** 2, 1e18, 2 ** 192);
}
}
Sample Sigvex Output
{
"detector_id": "oracle-manipulation",
"severity": "high",
"confidence": 0.81,
"description": "Function getTokenPrice() reads balanceOf() from two tokens in the same pool without time-weighting. The ratio is used directly in liquidate() collateral valuation.",
"location": { "function": "getTokenPrice(address,address,address)", "offset": 88 }
}
Detection Methodology
- Price source identification: Recognizes calls to
balanceOf,getReserves,slot0, and similar pool-state queries. - Financial usage tracking: Traces data flow to detect whether the price value influences loan ratios, liquidation thresholds, token minting rates, or treasury calculations.
- TWAP check: Verifies whether any time-accumulator pattern (Uniswap V2/V3 TWAP, Balancer TWAP) is present and guards the price reading.
- Multi-source aggregation: Checks whether multiple independent feeds are averaged and cross-checked.
Limitations
False positives:
- Governance contracts that read token prices only for display purposes are sometimes flagged.
- Protocols with custom internal price guards that use non-standard patterns may not be recognized.
False negatives:
- Oracle manipulation via governance (slow-path attacks) is not detected.
- Manipulation of a Chainlink feed itself (impersonating a compromised Chainlink node) is not in scope.
Related Detectors
- Flash Loan — detects flash loan price manipulation patterns
- Slippage Validation — detects missing slippage protection relying on manipulable price sources