Free snapshot slots open this week info@inversez.com
DetectionVulnerabilitiesToolsAuditsPricingBlogFree monitoring assessmentContact
Home/Vulnerabilities/Oracle price manipulation
— · Typically critical

Oracle price manipulation

If your contract reads a price from a source an attacker can move within one transaction, they will move it. Spot prices from an AMM pool are the classic mistake: a flash loan gives an attacker enough capital to skew the pool, act on the false price, and restore it, all before anyone can react.

Vulnerable code

Do not ship this
function getPrice() public view returns (uint256) {
    (uint112 r0, uint112 r1, ) = pair.getReserves();
    return (uint256(r1) * 1e18) / uint256(r0);   // spot price
}

function borrow(uint256 collateral) external {
    uint256 value = collateral * getPrice() / 1e18;
    _lend(msg.sender, value * 80 / 100);
}

Why it breaks

getReserves() reports the pool's balance right now. An attacker takes a flash loan, swaps a large amount into the pool to push the reported price up, calls borrow against collateral now valued at several times its real worth, takes the loan, reverses the swap, and repays the flash loan. Everything settles in one transaction, so there is no window for arbitrage to correct the price first.

Watch the attack run

The same bug as a stepped sequence. Play it, or walk through with the arrow keys.

The fix

Corrected
AggregatorV3Interface internal immutable feed;

function getPrice() public view returns (uint256) {
    (
        uint80 roundId,
        int256 answer,
        ,
        uint256 updatedAt,
        uint80 answeredInRound
    ) = feed.latestRoundData();

    require(answer > 0, "bad price");
    require(answeredInRound >= roundId, "stale round");
    require(block.timestamp - updatedAt < 3600, "price too old");

    return uint256(answer);
}

The invariant

The fix above is what an audit gives you. This is what monitoring gives you: the thing that should always be true once the contract is live, stated precisely enough to check every block.

InvariantEVM · oracle manipulation

Reported price stays within a bounded deviation of a time-weighted reference over N blocks.

invariant  abs(spot - twap(30 min)) / twap(30 min) <= 0.02
invariant  block.timestamp - updatedAt < heartbeat
Backtest: not yet run. This entry carries the reasoning, not the evidence. When the invariant has been replayed against the real transactions around the exploit, the firing block, how far ahead it fired, and its false-positive rate over a clean control period will be published here, along with the recording of the run.

How to test for it

A finding you cannot reproduce is an opinion. Write the test before you write the fix, watch it fail, then make it pass.

function testFlashLoanCannotMovePrice() public {
    uint256 p1 = lending.getPrice();

    // simulate a large swap through the pool
    deal(address(weth), attacker, 10_000 ether);
    vm.prank(attacker);
    router.swapExactTokensForTokens(10_000 ether, 0, path, attacker, block.timestamp);

    assertApproxEqRel(lending.getPrice(), p1, 0.01e18);  // within 1%
}

Where it has caused real losses

Oracle manipulation has caused some of the largest DeFi losses on record. Repeatedly, the pattern is identical: a lending market prices collateral from a thin pool, an attacker borrows capital to move that pool, and the protocol lends against a price that existed for one block.

What we check during review

What we look for during review: does any pricing path touch getReserves, balanceOf, or a single-block observation? Is there a staleness check on the oracle response, and a sanity bound on how far the price may move between updates? Is there a fallback when the primary feed reverts, and can that fallback be manipulated more cheaply than the primary? Uniswap V3 TWAPs are stronger than spot but still manipulable on low-liquidity pairs if the observation window is short.

Want us to check your contract for this?

A free monitoring assessment covers one contract up to 200 lines, manually reviewed, findings back within 72 hours. This class is on the checklist for every review we run.

Free monitoring assessment

Found this useful?

We publish these because the alternative, asking you to trust us, is worth less. Send a contract and we’ll apply the same thinking to your code.

  • Reply written by the auditor who read your code
  • No sales sequence, no drip campaign, no retargeting
  • We’ll tell you if you don’t need a paid audit yet
  • Report published only with your written permission

Request your snapshot

We read every submission. If it doesn’t fit the free tier we’ll say so and quote you instead, no obligation.