Hunt single-block oracle manipulation — spot-price AMM oracles, manipulable TWAP, dependent calculations, missing staleness checks.
59
70%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Critical
Do not install without reviewing
Fix and improve this skill with Tessl
tessl review fix ./packages/decepticon/decepticon/skills/standard/contracts/oracle-manipulation/SKILL.mdDeFi 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:
reserve1 / reserve0 of a Uniswap V2 pool. Anyone with enough capital (or a flash loan) can push the price for one block.totalSupply() of an LP token alongside reserves.# Common patterns
grep -rn 'getReserves\|getAmountsOut\|getPriceFromSqrtPriceX96\|latestAnswer\|latestRoundData' src/
# Custom oracle reads
grep -rn 'IPriceOracle\|getPrice\|consult' src/For each call:
slot0.sqrtPriceX96?
→ manipulableOracleLibrary.consult?
→ check the secondsAgo window (>= 1800s = 30 min is the safe minimum)latestRoundData() call?
→ check: is updatedAt validated? Is answeredInRound >= roundId? Is answer > 0? Are L2 sequencer feeds checked?// MISSING — vulnerable
(, int256 price, , , ) = priceFeed.latestRoundData();
// GOOD — explicit staleness + sequencer
(uint80 roundId, int256 price, , uint256 updatedAt, uint80 answeredInRound) = priceFeed.latestRoundData();
require(price > 0, "ORACLE_NEGATIVE");
require(updatedAt > block.timestamp - MAX_DELAY, "ORACLE_STALE");
require(answeredInRound >= roundId, "ORACLE_OLD_ROUND");
// On L2: also check sequencer uptime feed (L2 SequencerUptimeFeed)The bug isn't oracle-reading — it's oracle-trusting. Find where the price drives a state change:
// 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");
}
}Decepticon helper:
foundry_oracle_test(target="LendingPool", price_feed="UniV2Pair",
token0="WETH", token1="TARGETTOKEN", target_path="src/LendingPool.sol")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
}| Manipulation surface | Typical impact | Severity |
|---|---|---|
| Spot-price oracle drives liquidation | Drain LPs via fake liquidations | Critical |
| Spot-price drives borrow limit | Borrow more than collateral worth | Critical |
| TWAP < 30 min | Still manipulable on low-liquidity pairs | High |
| TWAP 30 min+ on high-liquidity ETH-USDC | Hard to exploit profitably | Medium-Low |
| Missing staleness check, but feed updates often | Edge-case impact during outages | Medium |
| L2 sequencer not checked | Exploitable during sequencer downtime | High (timing-dependent) |
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H = 10.0 (Crit)// Use Chainlink properly, not spot-price
function getPrice() internal view returns (uint256) {
(uint80 roundId, int256 answer, , uint256 updatedAt, uint80 answeredInRound)
= priceFeed.latestRoundData();
require(answer > 0, "neg price");
require(updatedAt > block.timestamp - 1 hours, "stale");
require(answeredInRound >= roundId, "old round");
// L2 only:
(, int256 sequencerStatus, , uint256 sequencerStart, ) = sequencerFeed.latestRoundData();
require(sequencerStatus == 0, "sequencer down");
require(block.timestamp - sequencerStart > 1 hours, "grace period");
return uint256(answer);
}
// Where Chainlink unavailable: use 30+ min Uniswap V3 TWAP w/ deep-liquidity pool
function getTwap(uint32 secondsAgo) internal view returns (uint160 sqrtPriceX96) {
(int24 arithmeticMeanTick, ) = OracleLibrary.consult(pool, secondsAgo);
return TickMath.getSqrtRatioAtTick(arithmeticMeanTick);
}e34afba
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.