DeFi protocols that read a price from an on-chain source are
vulnerable when the source can be moved within a single transaction
or block. Classic vectors:
Spot-price AMM oracle — reserve1 / reserve0 of a Uniswap V2 pool. Anyone with enough capital (or a flash loan) can push the price for one block.
Manipulable TWAP — short window TWAP, or TWAP over a low-liquidity pool.
Single Chainlink feed without staleness check — feed returns 0 / stale → uses 0 in math.
Custom oracle reading from manipulable storage — e.g., a "fair-price" oracle that reads totalSupply() of an LP token alongside reserves.
L2 sequencer offline — L2 oracles need a "is sequencer up?" check or attacker can exploit when sequencer downs and feeds freeze.
Is the price read from a Uniswap V2 / Sushi / Camelot pool's reserves?
→ spot price = manipulable
Is the price read from a Uniswap V3 pool's slot0.sqrtPriceX96?
→ manipulable
Is it a Uniswap V3 TWAP via OracleLibrary.consult?
→ check the secondsAgo window (>= 1800s = 30 min is the safe minimum)
Is it a Chainlink latestRoundData() call?
→ check: is updatedAt validated? Is answeredInRound >= roundId? Is answer > 0? Are L2 sequencer feeds checked?
The bug isn't oracle-reading — it's oracle-trusting. Find where the
price drives a state change:
Liquidation thresholds
Collateral valuation
Borrow limits
Swap output amounts (slippage check)
LP token pricing for vaults
PoC via Foundry
Flash-loan price push (Uniswap V2 reserves)
// Pseudo:
contract Test_oracle is Test {
function test_manipulate() public {
// 1. Flash-loan WETH from Aave / Balancer / Uniswap V3
// 2. Swap WETH → token in target pool, draining one side
// 3. Reserves now skewed → spot price way off
// 4. Call vulnerable protocol's price-dependent function
// (e.g., borrow USDC against overvalued collateral)
// 5. Reverse the swap, repay flash loan
// 6. Profit = whatever was extracted in step 4
assertGt(USDC.balanceOf(attacker), 0, "should profit");
}
}
function test_stale_price() public {
// mock the feed to return updatedAt that's 24h old
vm.mockCall(
address(priceFeed),
abi.encodeWithSignature("latestRoundData()"),
abi.encode(uint80(1), int256(STALE_PRICE), uint256(0), block.timestamp - 86400, uint80(1))
);
// call function — should revert if staleness checked, should proceed if not
target.priceDependentFunction();
// If we reach here w/o revert → bug
}