Skip to main content

oracle-manipulation

Hunt single-block oracle manipulation — spot-price AMM oracles, manipulable TWAP, dependent calculations, missing staleness checks.

Aller à l'installation

Informations de source

Dépôt
BitterSecurity/Decepticon
Dernière activité de la source
2 juin 2026 à 17:25
Langue détectée de SKILL.md
anglais
Étoiles
5 565
Forks
1 053

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
oracle-manipulation
description
Hunt single-block oracle manipulation — spot-price AMM oracles, manipulable TWAP, dependent calculations, missing staleness checks.
metadata
{"subdomain":"smart-contracts","when_to_use":"oracle manipulation twap price bypass","mitre_attack":["T1565.001","T1190"]}
# Oracle Manipulation Playbook 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: 1. **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. 2. **Manipulable TWAP** — short window TWAP, or TWAP over a low-liquidity pool. 3. **Single Chainlink feed without staleness check** — feed returns 0 / stale → uses 0 in math. 4. **Custom oracle reading from manipulable storage** — e.g., a "fair-price" oracle that reads `totalSupply()` of an LP token alongside reserves. 5. **L2 sequencer offline** — L2 oracles need a "is sequencer up?" check or attacker can exploit when sequencer downs and feeds freeze. ## Audit steps ### 1. Locate price-reading code ```bash # Common patterns grep -rn 'getReserves\|getAmountsOut\|getPriceFromSqrtPriceX96\|latestAnswer\|latestRoundData' src/ # Custom oracle reads grep -rn 'IPriceOracle\|getPrice\|consult' src/ ``` ### 2. Trace each price use For each call: - 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? ### 3. Validate staleness handling ```solidity // 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) ``` ### 4. Trace the price's use 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) ```solidity // 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") ``` ### Chainlink stale check missing ```solidity 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 } ``` ## Severity calibration | 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 - Spot-oracle + flash-loan available + drains protocol: `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H` = 10.0 (Crit) - 30-min TWAP on illiquid pair: 7-8 (High) - Missing staleness + Chainlink usually fresh: 5-6 (Medium) ## Defender remediation ```solidity // 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); } ``` ## Known exemplars - bZx Feb 2020: $645k via Synthetix sUSD spot-price oracle - Harvest Finance Oct 2020: $24M via Curve pool spot price - Cream Oct 2021: $130M via Yearn share-price manipulation - Mango Markets Oct 2022: $114M via MNGO spot price on AMM - Avraham Eisenberg's pattern, exactly: deposit small collateral, manipulate oracle, borrow against inflated value - Inverse Finance Apr 2022: $15M via INV/ETH spot price - Beanstalk Apr 2022: $182M via governance + flash-loan oracle (different but related)
Voir sur GitHub