From eaf1b043829b313a754f405ce38326cf1b9718cd Mon Sep 17 00:00:00 2001 From: jayy04 <103467857+jayy04@users.noreply.github.com> Date: Wed, 8 Nov 2023 21:49:35 -0500 Subject: [PATCH] [CLOB-858] Add liquidation tests with different block limits (#772) * [CLOB-858] Add liquidation tests with different block limits * long tests * comments --- protocol/testutil/constants/orders.go | 28 + protocol/testutil/constants/subaccounts.go | 30 + .../clob/e2e/liquidation_deleveraging_test.go | 616 ++++++++++++++++++ 3 files changed, 674 insertions(+) diff --git a/protocol/testutil/constants/orders.go b/protocol/testutil/constants/orders.go index 92aee84893..7b1b1c3f3a 100644 --- a/protocol/testutil/constants/orders.go +++ b/protocol/testutil/constants/orders.go @@ -610,6 +610,13 @@ var ( Subticks: 10, GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 15}, } + Order_Carl_Num0_Id1_Clob0_Buy01BTC_Price49500_GTB10 = clobtypes.Order{ + OrderId: clobtypes.OrderId{SubaccountId: Carl_Num0, ClientId: 1, ClobPairId: 0}, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 10_000_000, + Subticks: 49_500_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 10}, + } Order_Carl_Num0_Id0_Clob0_Buy1BTC_Price49500_GTB10 = clobtypes.Order{ OrderId: clobtypes.OrderId{SubaccountId: Carl_Num0, ClientId: 0, ClobPairId: 0}, Side: clobtypes.Order_SIDE_BUY, @@ -624,6 +631,13 @@ var ( Subticks: 50_000_000_000, GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 10}, } + Order_Carl_Num0_Id2_Clob0_Buy1BTC_Price50500_GTB10 = clobtypes.Order{ + OrderId: clobtypes.OrderId{SubaccountId: Carl_Num0, ClientId: 2, ClobPairId: 0}, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 100_000_000, + Subticks: 50_500_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 10}, + } Order_Carl_Num0_Id1_Clob0_Buy1BTC_Price49999 = clobtypes.Order{ OrderId: clobtypes.OrderId{SubaccountId: Carl_Num0, ClientId: 1, ClobPairId: 0}, Side: clobtypes.Order_SIDE_BUY, @@ -771,6 +785,13 @@ var ( Subticks: 50_000_000_000, GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 10}, } + Order_Dave_Num0_Id2_Clob0_Sell1BTC_Price49500_GTB10 = clobtypes.Order{ + OrderId: clobtypes.OrderId{SubaccountId: Dave_Num0, ClientId: 2, ClobPairId: 0}, + Side: clobtypes.Order_SIDE_SELL, + Quantums: 100_000_000, + Subticks: 49_500_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 10}, + } Order_Dave_Num0_Id0_Clob0_Sell1BTC_Price49999_GTB10 = clobtypes.Order{ OrderId: clobtypes.OrderId{SubaccountId: Dave_Num0, ClientId: 0, ClobPairId: 0}, Side: clobtypes.Order_SIDE_SELL, @@ -792,6 +813,13 @@ var ( Subticks: 50_498_000_000, GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 10}, } + Order_Dave_Num0_Id1_Clob0_Sell01BTC_Price50500_GTB10 = clobtypes.Order{ + OrderId: clobtypes.OrderId{SubaccountId: Dave_Num0, ClientId: 1, ClobPairId: 0}, + Side: clobtypes.Order_SIDE_SELL, + Quantums: 10_000_000, + Subticks: 50_500_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 10}, + } Order_Dave_Num0_Id0_Clob0_Sell1BTC_Price50500_GTB10 = clobtypes.Order{ OrderId: clobtypes.OrderId{SubaccountId: Dave_Num0, ClientId: 0, ClobPairId: 0}, Side: clobtypes.Order_SIDE_SELL, diff --git a/protocol/testutil/constants/subaccounts.go b/protocol/testutil/constants/subaccounts.go index 05eafb7608..28575e8c0d 100644 --- a/protocol/testutil/constants/subaccounts.go +++ b/protocol/testutil/constants/subaccounts.go @@ -276,6 +276,21 @@ var ( }, }, } + Carl_Num1_1BTC_Short_50499USD = satypes.Subaccount{ + Id: &Carl_Num1, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(50_499_000_000), // $50,499 + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(-100_000_000), // -1 BTC + }, + }, + } Dave_Num0_01BTC_Long_50000USD = satypes.Subaccount{ Id: &Dave_Num0, AssetPositions: []*satypes.AssetPosition{ @@ -502,6 +517,21 @@ var ( }, }, } + Dave_Num1_1BTC_Long_49501USD_Short = satypes.Subaccount{ + Id: &Dave_Num1, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(-49_501_000_000), // -$49,501 + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(100_000_000), // 1 BTC + }, + }, + } Dave_Num1_1ETH_Long_50000USD = satypes.Subaccount{ Id: &Dave_Num1, AssetPositions: []*satypes.AssetPosition{ diff --git a/protocol/x/clob/e2e/liquidation_deleveraging_test.go b/protocol/x/clob/e2e/liquidation_deleveraging_test.go index 807ec27e10..6d67a3510f 100644 --- a/protocol/x/clob/e2e/liquidation_deleveraging_test.go +++ b/protocol/x/clob/e2e/liquidation_deleveraging_test.go @@ -20,6 +20,622 @@ import ( "github.com/stretchr/testify/require" ) +func TestLiquidationConfig(t *testing.T) { + tests := map[string]struct { + // State. + subaccounts []satypes.Subaccount + marketIdToOraclePriceOverride map[uint32]uint64 + + // Parameters. + placedMatchableOrders []clobtypes.MatchableOrder + liquidatableSubaccountIds []satypes.SubaccountId + + // Configuration. + liquidationConfig clobtypes.LiquidationsConfig + liquidityTiers []perptypes.LiquidityTier + perpetuals []perptypes.Perpetual + clobPairs []clobtypes.ClobPair + + // Expectations. + expectedSubaccounts []satypes.Subaccount + }{ + `Liquidating short respects position block limit - MinPositionNotionalLiquidated`: { + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_1BTC_Short_50499USD, + constants.Dave_Num0_1BTC_Long_50000USD, + }, + + placedMatchableOrders: []clobtypes.MatchableOrder{ + &constants.Order_Dave_Num0_Id0_Clob0_Sell1BTC_Price50000_GTB10, // Order at $50,000 + }, + liquidatableSubaccountIds: []satypes.SubaccountId{constants.Carl_Num0}, + liquidationConfig: clobtypes.LiquidationsConfig{ + MaxLiquidationFeePpm: 5_000, + FillablePriceConfig: constants.FillablePriceConfig_Default, + PositionBlockLimits: clobtypes.PositionBlockLimits{ + // 1% of $50,000 is $500 so $500 worth of BTC should get liquidated. + // However, this is smaller than the minimum position notional liquidated of $100,000, + // so the entire position should get liquidated. + MinPositionNotionalLiquidated: 100_000_000_000, // $100,000 + MaxPositionPortionLiquidatedPpm: 10_000, // 1% + }, + SubaccountBlockLimits: constants.SubaccountBlockLimits_Default, + }, + + liquidityTiers: constants.LiquidityTiers, + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance, + }, + clobPairs: []clobtypes.ClobPair{constants.ClobPair_Btc}, + + expectedSubaccounts: []satypes.Subaccount{ + { + Id: &constants.Carl_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(50_499_000_000 - 50_000_000_000 - 250_000_000), + }, + }, + }, + { + Id: &constants.Dave_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(100_000_000_000), // $100,000 + }, + }, + }, + }, + }, + `Liquidatiing long respects position block limit - MinPositionNotionalLiquidated`: { + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_1BTC_Short_100000USD, + constants.Dave_Num0_1BTC_Long_49501USD_Short, + }, + + placedMatchableOrders: []clobtypes.MatchableOrder{ + &constants.Order_Carl_Num0_Id0_Clob0_Buy1BTC_Price50000_GTB10, // Order at $50,000 + }, + liquidatableSubaccountIds: []satypes.SubaccountId{constants.Dave_Num0}, + liquidationConfig: clobtypes.LiquidationsConfig{ + MaxLiquidationFeePpm: 5_000, + FillablePriceConfig: constants.FillablePriceConfig_Default, + PositionBlockLimits: clobtypes.PositionBlockLimits{ + // 1% of $50,000 is $500 so $500 worth of BTC should get liquidated. + // However, this is smaller than the minimum position notional liquidated of $100,000, + // so the entire position should get liquidated. + MinPositionNotionalLiquidated: 100_000_000_000, // $100,000 + MaxPositionPortionLiquidatedPpm: 10_000, // 1% + }, + SubaccountBlockLimits: constants.SubaccountBlockLimits_Default, + }, + + liquidityTiers: constants.LiquidityTiers, + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance, + }, + clobPairs: []clobtypes.ClobPair{constants.ClobPair_Btc}, + + expectedSubaccounts: []satypes.Subaccount{ + { + Id: &constants.Carl_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(100_000_000_000 - 50_000_000_000), + }, + }, + }, + { + Id: &constants.Dave_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(-49_501_000_000 + 50_000_000_000 - 250_000_000), + }, + }, + }, + }, + }, + `Liquidatiing short respects position block limit - MaxPositionPortionLiquidatedPpm`: { + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_1BTC_Short_50499USD, + constants.Dave_Num0_1BTC_Long_50000USD, + }, + + placedMatchableOrders: []clobtypes.MatchableOrder{ + &constants.Order_Dave_Num0_Id0_Clob0_Sell1BTC_Price50000_GTB10, // Order at $50,000 + }, + liquidatableSubaccountIds: []satypes.SubaccountId{constants.Carl_Num0}, + liquidationConfig: clobtypes.LiquidationsConfig{ + MaxLiquidationFeePpm: 5_000, + FillablePriceConfig: constants.FillablePriceConfig_Default, + PositionBlockLimits: clobtypes.PositionBlockLimits{ + // 10% of $50,000 is $5,000 so $5,000 worth of BTC should get liquidated. + MinPositionNotionalLiquidated: 100_000_000, // $1,000 + MaxPositionPortionLiquidatedPpm: 100_000, // 10% + }, + SubaccountBlockLimits: constants.SubaccountBlockLimits_Default, + }, + + liquidityTiers: constants.LiquidityTiers, + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance, + }, + clobPairs: []clobtypes.ClobPair{constants.ClobPair_Btc}, + + expectedSubaccounts: []satypes.Subaccount{ + { + Id: &constants.Carl_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(50_499_000_000 - 5_000_000_000 - 25_000_000), + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(-90_000_000), // -0.9 BTC + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + { + Id: &constants.Dave_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(55_000_000_000), // $55,000 + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(90_000_000), // 0.9 BTC + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + }, + }, + `Liquidatiing long respects position block limit - MaxPositionPortionLiquidatedPpm`: { + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_1BTC_Short_100000USD, + constants.Dave_Num0_1BTC_Long_49501USD_Short, + }, + + placedMatchableOrders: []clobtypes.MatchableOrder{ + &constants.Order_Carl_Num0_Id0_Clob0_Buy1BTC_Price50000_GTB10, // Order at $50,000 + }, + liquidatableSubaccountIds: []satypes.SubaccountId{constants.Dave_Num0}, + liquidationConfig: clobtypes.LiquidationsConfig{ + MaxLiquidationFeePpm: 5_000, + FillablePriceConfig: constants.FillablePriceConfig_Default, + PositionBlockLimits: clobtypes.PositionBlockLimits{ + // 10% of $50,000 is $5,000 so $5,000 worth of BTC should get liquidated. + MinPositionNotionalLiquidated: 100_000_000, // $1,000 + MaxPositionPortionLiquidatedPpm: 100_000, // 10% + }, + SubaccountBlockLimits: constants.SubaccountBlockLimits_Default, + }, + + liquidityTiers: constants.LiquidityTiers, + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance, + }, + clobPairs: []clobtypes.ClobPair{constants.ClobPair_Btc}, + + expectedSubaccounts: []satypes.Subaccount{ + { + Id: &constants.Carl_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(100_000_000_000 - 5_000_000_000), + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(-90_000_000), // -0.9 BTC + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + { + Id: &constants.Dave_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(-49_501_000_000 + 5_000_000_000 - 25_000_000), + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(90_000_000), // 0.9 BTC + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + }, + }, + `Liquidating short respects subaccount block limit - MaxNotionalLiquidated`: { + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_1BTC_Short_50499USD, + constants.Dave_Num0_1BTC_Long_50000USD, + }, + + placedMatchableOrders: []clobtypes.MatchableOrder{ + &constants.Order_Dave_Num0_Id2_Clob0_Sell1BTC_Price49500_GTB10, // Order at $49,500 + }, + liquidatableSubaccountIds: []satypes.SubaccountId{constants.Carl_Num0}, + liquidationConfig: clobtypes.LiquidationsConfig{ + MaxLiquidationFeePpm: 5_000, + FillablePriceConfig: constants.FillablePriceConfig_Default, + PositionBlockLimits: constants.PositionBlockLimits_Default, + SubaccountBlockLimits: clobtypes.SubaccountBlockLimits{ + // Subaccount may only liquidate $5,000 per block. + MaxNotionalLiquidated: 5_000_000_000, + MaxQuantumsInsuranceLost: 100_000_000_000_000, + }, + }, + + liquidityTiers: constants.LiquidityTiers, + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance, + }, + clobPairs: []clobtypes.ClobPair{constants.ClobPair_Btc}, + + expectedSubaccounts: []satypes.Subaccount{ + { + Id: &constants.Carl_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(50_499_000_000 - 4_950_000_000 - 24_750_000), + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(-90_000_000), // -0.9 BTC + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + { + Id: &constants.Dave_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(54_950_000_000), // $54,950 + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(90_000_000), // 0.9 BTC + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + }, + }, + `Liquidating long respects subaccount block limit - MaxNotionalLiquidated`: { + subaccounts: []satypes.Subaccount{ + constants.Carl_Num0_1BTC_Short_100000USD, + constants.Dave_Num0_1BTC_Long_49501USD_Short, + }, + + placedMatchableOrders: []clobtypes.MatchableOrder{ + // Maker order at $50,500. + // This maker order is specifically chosen to be above the oracle price, to ensure that + // block limits use the notional value of the position (oracle price), + // and not the notional liquidated (match price) + &constants.Order_Carl_Num0_Id2_Clob0_Buy1BTC_Price50500_GTB10, // Order at $50,500 + }, + liquidatableSubaccountIds: []satypes.SubaccountId{constants.Dave_Num0}, + liquidationConfig: clobtypes.LiquidationsConfig{ + MaxLiquidationFeePpm: 5_000, + FillablePriceConfig: constants.FillablePriceConfig_Default, + PositionBlockLimits: constants.PositionBlockLimits_Default, + SubaccountBlockLimits: clobtypes.SubaccountBlockLimits{ + // Subaccount may only liquidate $5,000 per block. + MaxNotionalLiquidated: 5_000_000_000, + MaxQuantumsInsuranceLost: 100_000_000_000_000, + }, + }, + + liquidityTiers: constants.LiquidityTiers, + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance, + }, + clobPairs: []clobtypes.ClobPair{constants.ClobPair_Btc}, + + expectedSubaccounts: []satypes.Subaccount{ + { + Id: &constants.Carl_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(100_000_000_000 - 5_050_000_000), + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(-90_000_000), // -0.9 BTC + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + { + Id: &constants.Dave_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(-49_501_000_000 + 5_050_000_000 - 25_250_000), + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(90_000_000), // 0.9 BTC + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + }, + }, + `Liquidating short respects subaccount block limit - MaxQuantumsInsuranceLost`: { + subaccounts: []satypes.Subaccount{ + // Carl_Num0 is irrelevant to the test, but is used to seed the insurance fund. + constants.Carl_Num0_1BTC_Short_50499USD, + constants.Carl_Num1_1BTC_Short_50499USD, + constants.Dave_Num0_1BTC_Long_50000USD, + }, + + placedMatchableOrders: []clobtypes.MatchableOrder{ + // This order is irrelevant to the test, but is used to seed the insurance fund. + &constants.Order_Dave_Num0_Id2_Clob0_Sell1BTC_Price49500_GTB10, // Order at $49,500 + + // Bankruptcy price is $50,499, and closing at $50,500 would require $1 from the insurance fund. + // First order would transfer $0.1 from the insurance fund and would succeed. + // Second order would require $0.9 from the insurance fund and would fail since subaccounts + // may only lose $0.5 per block. + &constants.Order_Dave_Num0_Id1_Clob0_Sell01BTC_Price50500_GTB10, // Order at $50,500 + &constants.Order_Dave_Num0_Id0_Clob0_Sell1BTC_Price50500_GTB10, // Order at $50,500 + }, + liquidatableSubaccountIds: []satypes.SubaccountId{constants.Carl_Num1, constants.Carl_Num0}, + liquidationConfig: clobtypes.LiquidationsConfig{ + MaxLiquidationFeePpm: 5_000, + FillablePriceConfig: constants.FillablePriceConfig_Max_Smmr, + PositionBlockLimits: constants.PositionBlockLimits_Default, + SubaccountBlockLimits: clobtypes.SubaccountBlockLimits{ + // Subaccount may only lose $0.5 per block. + MaxNotionalLiquidated: 100_000_000_000_000, + MaxQuantumsInsuranceLost: 500_000, + }, + }, + + liquidityTiers: constants.LiquidityTiers, + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance, + }, + clobPairs: []clobtypes.ClobPair{constants.ClobPair_Btc}, + + expectedSubaccounts: []satypes.Subaccount{ + { + Id: &constants.Carl_Num1, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + // $0.1 from insurance fund + Quantums: dtypes.NewInt(50_499_000_000 - 5_050_000_000 + 100_000), + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(-90_000_000), // -0.9 BTC + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + { + Id: &constants.Dave_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(50_000_000_000 + 49_500_000_000 + 5_050_000_000), + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(-10_000_000), // -0.1 BTC + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + }, + }, + `Liquidating long respects subaccount block limit - MaxQuantumsInsuranceLost`: { + subaccounts: []satypes.Subaccount{ + // Carl_Num0 is irrelevant to the test, but is used to seed the insurance fund. + constants.Carl_Num0_1BTC_Short_100000USD, + constants.Dave_Num0_1BTC_Long_49501USD_Short, + constants.Dave_Num1_1BTC_Long_49501USD_Short, + }, + + placedMatchableOrders: []clobtypes.MatchableOrder{ + // This order is irrelevant to the test, but is used to seed the insurance fund. + &constants.Order_Carl_Num0_Id2_Clob0_Buy1BTC_Price50500_GTB10, // Order at $50,500 + + // Bankruptcy price is $49,501, and closing at $49,500 would require $1 from the insurance fund. + // First order would transfer $0.1 from the insurance fund and would succeed. + // Second order would require $0.9 from the insurance fund and would fail since subaccounts + // may only lose $0.5 per block. + &constants.Order_Carl_Num0_Id1_Clob0_Buy01BTC_Price49500_GTB10, // Order at $49,500 + &constants.Order_Carl_Num0_Id0_Clob0_Buy1BTC_Price49500_GTB10, // Order at $49,500 + }, + liquidatableSubaccountIds: []satypes.SubaccountId{constants.Dave_Num0, constants.Dave_Num1}, + liquidationConfig: clobtypes.LiquidationsConfig{ + MaxLiquidationFeePpm: 5_000, + FillablePriceConfig: constants.FillablePriceConfig_Max_Smmr, + PositionBlockLimits: constants.PositionBlockLimits_Default, + SubaccountBlockLimits: clobtypes.SubaccountBlockLimits{ + // Subaccount may only lose $0.5 per block. + MaxNotionalLiquidated: 100_000_000_000_000, + MaxQuantumsInsuranceLost: 500_000, + }, + }, + + liquidityTiers: constants.LiquidityTiers, + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance, + }, + clobPairs: []clobtypes.ClobPair{constants.ClobPair_Btc}, + + expectedSubaccounts: []satypes.Subaccount{ + { + Id: &constants.Carl_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(100_000_000_000 - 50_500_000_000 - 4_950_000_000), + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(10_000_000), // 0.1 BTC + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + { + Id: &constants.Dave_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(-49_501_000_000 + 4_950_000_000 + 100_000), + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(90_000_000), // 0.9 BTC + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + tApp := testapp.NewTestAppBuilder(t).WithGenesisDocFn(func() (genesis types.GenesisDoc) { + genesis = testapp.DefaultGenesis() + testapp.UpdateGenesisDocWithAppStateForModule( + &genesis, + func(genesisState *assettypes.GenesisState) { + genesisState.Assets = []assettypes.Asset{ + *constants.Usdc, + } + }, + ) + testapp.UpdateGenesisDocWithAppStateForModule( + &genesis, + func(genesisState *prices.GenesisState) { + // Set oracle prices in the genesis. + pricesGenesis := constants.TestPricesGenesisState + + // Make a copy of the MarketPrices slice to avoid modifying by reference. + marketPricesCopy := make([]prices.MarketPrice, len(pricesGenesis.MarketPrices)) + copy(marketPricesCopy, pricesGenesis.MarketPrices) + + for marketId, oraclePrice := range tc.marketIdToOraclePriceOverride { + exponent, exists := constants.TestMarketIdsToExponents[marketId] + require.True(t, exists) + + marketPricesCopy[marketId] = prices.MarketPrice{ + Id: marketId, + Price: oraclePrice, + Exponent: exponent, + } + } + + pricesGenesis.MarketPrices = marketPricesCopy + *genesisState = pricesGenesis + }, + ) + testapp.UpdateGenesisDocWithAppStateForModule( + &genesis, + func(genesisState *perptypes.GenesisState) { + genesisState.Params = constants.PerpetualsGenesisParams + genesisState.LiquidityTiers = tc.liquidityTiers + genesisState.Perpetuals = tc.perpetuals + }, + ) + testapp.UpdateGenesisDocWithAppStateForModule( + &genesis, + func(genesisState *satypes.GenesisState) { + genesisState.Subaccounts = tc.subaccounts + }, + ) + testapp.UpdateGenesisDocWithAppStateForModule( + &genesis, + func(genesisState *clobtypes.GenesisState) { + genesisState.ClobPairs = tc.clobPairs + genesisState.LiquidationsConfig = tc.liquidationConfig + genesisState.EquityTierLimitConfig = clobtypes.EquityTierLimitConfiguration{} + }, + ) + testapp.UpdateGenesisDocWithAppStateForModule( + &genesis, + func(genesisState *feetiertypes.GenesisState) { + genesisState.Params = constants.PerpetualFeeParamsNoFee + }, + ) + return genesis + }).Build() + + ctx := tApp.AdvanceToBlock(2, testapp.AdvanceToBlockOptions{}) + + // Create all existing orders. + existingOrderMsgs := make([]clobtypes.MsgPlaceOrder, len(tc.placedMatchableOrders)) + for i, matchableOrder := range tc.placedMatchableOrders { + existingOrderMsgs[i] = clobtypes.MsgPlaceOrder{Order: matchableOrder.MustGetOrder()} + } + for _, checkTx := range testapp.MustMakeCheckTxsWithClobMsg(ctx, tApp.App, existingOrderMsgs...) { + resp := tApp.CheckTx(checkTx) + require.Conditionf(t, resp.IsOK, "Expected CheckTx to succeed. Response: %+v", resp) + } + + _, err := tApp.App.Server.LiquidateSubaccounts(ctx, &api.LiquidateSubaccountsRequest{ + SubaccountIds: tc.liquidatableSubaccountIds, + }) + require.NoError(t, err) + + // Verify test expectations. + ctx = tApp.AdvanceToBlock(3, testapp.AdvanceToBlockOptions{}) + for _, expectedSubaccount := range tc.expectedSubaccounts { + require.Equal( + t, + expectedSubaccount, + tApp.App.SubaccountsKeeper.GetSubaccount(ctx, *expectedSubaccount.Id), + ) + } + }) + } +} + func TestPlacePerpetualLiquidation_Deleveraging(t *testing.T) { tests := map[string]struct { // State.