Skip to main content
Sigvex

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

Before (Vulnerable)

interface IConstantProductPair {
    function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32);
}

contract VulnerableProtocol {
    IConstantProductPair 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 "./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

a concentrated-liquidity AMM TWAP (on-chain, no external dependency):

interface IConcentratedLiquidityPool {
    function observe(uint32[] calldata secondsAgos)
        external view returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulatives);
}

contract TWAPOracle {
    IConcentratedLiquidityPool 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 a concentrated-liquidity AMMslot0.sqrtPriceX96 is the current tick, not a TWAP. It is as manipulable as a constant-product AMM reserves.

Ignoring oracle staleness — off-chain aggregated 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.

References