Skip to main content
Sigvex

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 aggregated price 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,) = IAmmV2Pair(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 a concentrated-liquidity AMM 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,) = IAmmV3Pool(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": "critical",
  "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

  1. Price source identification: Recognizes calls to balanceOf, getReserves, slot0, and similar pool-state queries.
  2. Financial usage tracking: Traces data flow to detect whether the price value influences loan ratios, liquidation thresholds, token minting rates, or treasury calculations.
  3. TWAP check: Verifies whether any time-accumulator pattern (a constant-product AMM/V3 TWAP, a major weighted-pool AMM TWAP) is present and guards the price reading.
  4. 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 an aggregated price feed itself (impersonating a compromised aggregator node) is not in scope.
  • Flash Loan — detects flash loan price manipulation patterns
  • Slippage Validation — detects missing slippage protection relying on manipulable price sources