diff --git a/protocol/testutil/constants/perpetuals.go b/protocol/testutil/constants/perpetuals.go index 89f31f62f15..def5026ec93 100644 --- a/protocol/testutil/constants/perpetuals.go +++ b/protocol/testutil/constants/perpetuals.go @@ -81,6 +81,15 @@ var LiquidityTiers = []perptypes.LiquidityTier{ MaintenanceFractionPpm: 1_000_000, ImpactNotional: 50_454_000_000, }, + { + Id: 9, + Name: "9", + InitialMarginPpm: 200_000, // 20% + MaintenanceFractionPpm: 500_000, // 20% * 0.5 = 10% + ImpactNotional: 2_500_000_000, + OpenInterestUpperCap: 50_000_000_000_000, // 50mm USDC + OpenInterestLowerCap: 25_000_000_000_000, // 25mm USDC + }, { Id: 101, Name: "101", @@ -237,6 +246,19 @@ var ( FundingIndex: dtypes.ZeroInt(), OpenInterest: dtypes.ZeroInt(), } + BtcUsd_20PercentInitial_10PercentMaintenance_25mmLowerCap_50mmUpperCap = perptypes.Perpetual{ + Params: perptypes.PerpetualParams{ + Id: 0, + Ticker: "BTC-USD 20/10 margin requirements", + MarketId: uint32(0), + AtomicResolution: int32(-8), + DefaultFundingPpm: int32(0), + LiquidityTier: uint32(9), + MarketType: perptypes.PerpetualMarketType_PERPETUAL_MARKET_TYPE_CROSS, + }, + FundingIndex: dtypes.ZeroInt(), + OpenInterest: dtypes.ZeroInt(), + } BtcUsd_NoMarginRequirement = perptypes.Perpetual{ Params: perptypes.PerpetualParams{ Id: 0, diff --git a/protocol/x/clob/keeper/deleveraging.go b/protocol/x/clob/keeper/deleveraging.go index 87ed66f59af..3d205713592 100644 --- a/protocol/x/clob/keeper/deleveraging.go +++ b/protocol/x/clob/keeper/deleveraging.go @@ -395,6 +395,7 @@ func (k Keeper) OffsetSubaccountPerpetualPosition( deltaBaseQuantums, deltaQuoteQuantums, ); err == nil { + fmt.Printf("!! ProcessDeleveraging succeeced\n") // Update the remaining liquidatable quantums. deltaQuantumsRemaining.Sub(deltaQuantumsRemaining, deltaBaseQuantums) fills = append(fills, types.MatchPerpetualDeleveraging_Fill{ @@ -428,6 +429,7 @@ func (k Keeper) OffsetSubaccountPerpetualPosition( ), ) } else { + fmt.Printf("!! ProcessDeleveraging failed, err = %v\n", err) // If an error is returned, it's likely because the subaccounts' bankruptcy prices do not overlap. // TODO(CLOB-75): Support deleveraging subaccounts with non overlapping bankruptcy prices. liquidatedSubaccount := k.subaccountsKeeper.GetSubaccount(ctx, liquidatedSubaccountId) @@ -581,6 +583,7 @@ func (k Keeper) ProcessDeleveraging( } // Apply the update. + // TODO(): OIMF? success, successPerUpdate, err := k.subaccountsKeeper.UpdateSubaccounts(ctx, updates, satypes.Match) if err != nil { return err diff --git a/protocol/x/clob/keeper/deleveraging_test.go b/protocol/x/clob/keeper/deleveraging_test.go index 1bfeda123a7..90ab4994aca 100644 --- a/protocol/x/clob/keeper/deleveraging_test.go +++ b/protocol/x/clob/keeper/deleveraging_test.go @@ -455,6 +455,7 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { tests := map[string]struct { // Setup. subaccounts []satypes.Subaccount + perpOIs []perptypes.OpenInterestDelta // Parameters. liquidatedSubaccountId satypes.SubaccountId @@ -471,6 +472,12 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { constants.Carl_Num0_1BTC_Short_54999USD, constants.Dave_Num0_1BTC_Long_50000USD, }, + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: 0, + BaseQuantumsDelta: big.NewInt(100_000_000), + }, + }, liquidatedSubaccountId: constants.Carl_Num0, perpetualId: 0, deltaQuantums: big.NewInt(100_000_000), @@ -500,6 +507,12 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { constants.Carl_Num0_1BTC_Long_54999USD, constants.Dave_Num0_1BTC_Short_100000USD, }, + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: 0, + BaseQuantumsDelta: big.NewInt(100_000_000), + }, + }, liquidatedSubaccountId: constants.Carl_Num0, perpetualId: 0, deltaQuantums: big.NewInt(-100_000_000), @@ -550,6 +563,12 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { }, }, }, + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: 0, + BaseQuantumsDelta: big.NewInt(50_000_000), + }, + }, liquidatedSubaccountId: constants.Carl_Num0, perpetualId: 0, deltaQuantums: big.NewInt(100_000_000), @@ -594,7 +613,13 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { }, liquidatedSubaccountId: constants.Carl_Num0, perpetualId: 0, - deltaQuantums: big.NewInt(100_000_000), + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: 0, + BaseQuantumsDelta: big.NewInt(100_000_000), + }, + }, + deltaQuantums: big.NewInt(100_000_000), expectedSubaccounts: []satypes.Subaccount{ { Id: &constants.Carl_Num0, @@ -625,7 +650,13 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { }, liquidatedSubaccountId: constants.Carl_Num0, perpetualId: 0, - deltaQuantums: big.NewInt(100_000_000), + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: 0, + BaseQuantumsDelta: big.NewInt(100_000_000), + }, + }, + deltaQuantums: big.NewInt(100_000_000), expectedSubaccounts: []satypes.Subaccount{ { Id: &constants.Carl_Num0, @@ -656,7 +687,13 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { }, liquidatedSubaccountId: constants.Carl_Num0, perpetualId: 0, - deltaQuantums: big.NewInt(100_000_000), + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: 0, + BaseQuantumsDelta: big.NewInt(100_000_000), + }, + }, + deltaQuantums: big.NewInt(100_000_000), expectedSubaccounts: []satypes.Subaccount{ { Id: &constants.Carl_Num0, @@ -684,8 +721,14 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { constants.Carl_Num0_1BTC_Short_50000USD, constants.Dave_Num0_1BTC_Long_50001USD_Short, }, - liquidatedSubaccountId: constants.Carl_Num0, - perpetualId: 0, + liquidatedSubaccountId: constants.Carl_Num0, + perpetualId: 0, + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: 0, + BaseQuantumsDelta: big.NewInt(100_000_000), + }, + }, deltaQuantums: big.NewInt(100_000_000), expectedSubaccounts: nil, expectedFills: []types.MatchPerpetualDeleveraging_Fill{}, @@ -698,7 +741,13 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { }, liquidatedSubaccountId: constants.Carl_Num0, perpetualId: 0, - deltaQuantums: big.NewInt(100_000_000), + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: 0, + BaseQuantumsDelta: big.NewInt(100_000_000), + }, + }, + deltaQuantums: big.NewInt(100_000_000), expectedSubaccounts: []satypes.Subaccount{ // Carl's BTC short position is offset by Dave's BTC long position at $50,000 leaving // his ETH long position untouched and dropping his asset position to -$3000. @@ -765,6 +814,17 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { require.NoError(t, err) } + for _, perpOI := range tc.perpOIs { + require.NoError( + t, + ks.PerpetualsKeeper.ModifyOpenInterest( + ks.Ctx, + perpOI.PerpetualId, + perpOI.BaseQuantumsDelta, + ), + ) + } + clobPairs := []types.ClobPair{ constants.ClobPair_Btc, constants.ClobPair_Eth, @@ -862,6 +922,7 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { func TestProcessDeleveraging(t *testing.T) { tests := map[string]struct { // Setup. + perpOIs []perptypes.OpenInterestDelta liquidatedSubaccount satypes.Subaccount offsettingSubaccount satypes.Subaccount deltaQuantums *big.Int @@ -882,7 +943,13 @@ func TestProcessDeleveraging(t *testing.T) { "Liquidated: under-collateralized, TNC > 0, offsetting: well-collateralized": { liquidatedSubaccount: constants.Carl_Num0_1BTC_Short_54999USD, offsettingSubaccount: constants.Dave_Num0_1BTC_Long_50000USD, - deltaQuantums: big.NewInt(100_000_000), // 1 BTC + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: constants.ClobPair_Btc.Id, + BaseQuantumsDelta: big.NewInt(100_000_000), + }, + }, + deltaQuantums: big.NewInt(100_000_000), // 1 BTC expectedLiquidatedSubaccount: satypes.Subaccount{ Id: &constants.Carl_Num0, @@ -899,7 +966,13 @@ func TestProcessDeleveraging(t *testing.T) { "Liquidated: under-collateralized, TNC > 0, offsetting: under-collateralized, TNC > 0": { liquidatedSubaccount: constants.Carl_Num0_1BTC_Short_54999USD, offsettingSubaccount: constants.Dave_Num0_1BTC_Long_45001USD_Short, - deltaQuantums: big.NewInt(100_000_000), // 1 BTC + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: constants.ClobPair_Btc.Id, + BaseQuantumsDelta: big.NewInt(100_000_000), + }, + }, + deltaQuantums: big.NewInt(100_000_000), // 1 BTC expectedLiquidatedSubaccount: satypes.Subaccount{ Id: &constants.Carl_Num0, @@ -916,7 +989,13 @@ func TestProcessDeleveraging(t *testing.T) { "Liquidated: under-collateralized, TNC > 0, offsetting: under-collateralized, TNC == 0": { liquidatedSubaccount: constants.Carl_Num0_1BTC_Short_54999USD, offsettingSubaccount: constants.Dave_Num0_1BTC_Long_50000USD_Short, - deltaQuantums: big.NewInt(100_000_000), // 1 BTC + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: constants.ClobPair_Btc.Id, + BaseQuantumsDelta: big.NewInt(100_000_000), + }, + }, + deltaQuantums: big.NewInt(100_000_000), // 1 BTC expectedLiquidatedSubaccount: satypes.Subaccount{ Id: &constants.Carl_Num0, @@ -933,7 +1012,13 @@ func TestProcessDeleveraging(t *testing.T) { "Liquidated: under-collateralized, TNC > 0, offsetting: under-collateralized, TNC < 0": { liquidatedSubaccount: constants.Carl_Num0_1BTC_Short_54999USD, offsettingSubaccount: constants.Dave_Num0_1BTC_Long_50001USD_Short, - deltaQuantums: big.NewInt(100_000_000), // 1 BTC + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: constants.ClobPair_Btc.Id, + BaseQuantumsDelta: big.NewInt(100_000_000), + }, + }, + deltaQuantums: big.NewInt(100_000_000), // 1 BTC expectedLiquidatedSubaccount: satypes.Subaccount{ Id: &constants.Carl_Num0, @@ -949,7 +1034,13 @@ func TestProcessDeleveraging(t *testing.T) { "Liquidated: under-collateralized, TNC == 0, offsetting: well-collateralized": { liquidatedSubaccount: constants.Carl_Num0_1BTC_Short_50000USD, offsettingSubaccount: constants.Dave_Num0_1BTC_Long_50000USD, - deltaQuantums: big.NewInt(100_000_000), // 1 BTC + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: constants.ClobPair_Btc.Id, + BaseQuantumsDelta: big.NewInt(100_000_000), + }, + }, + deltaQuantums: big.NewInt(100_000_000), // 1 BTC expectedLiquidatedSubaccount: satypes.Subaccount{ Id: &constants.Carl_Num0, @@ -966,7 +1057,13 @@ func TestProcessDeleveraging(t *testing.T) { "Liquidated: under-collateralized, TNC == 0, offsetting: under-collateralized, TNC > 0": { liquidatedSubaccount: constants.Carl_Num0_1BTC_Short_50000USD, offsettingSubaccount: constants.Dave_Num0_1BTC_Long_45001USD_Short, - deltaQuantums: big.NewInt(100_000_000), // 1 BTC + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: constants.ClobPair_Btc.Id, + BaseQuantumsDelta: big.NewInt(100_000_000), + }, + }, + deltaQuantums: big.NewInt(100_000_000), // 1 BTC expectedLiquidatedSubaccount: satypes.Subaccount{ Id: &constants.Carl_Num0, @@ -983,7 +1080,13 @@ func TestProcessDeleveraging(t *testing.T) { "Liquidated: under-collateralized, TNC == 0, offsetting: under-collateralized, TNC == 0": { liquidatedSubaccount: constants.Carl_Num0_1BTC_Short_50000USD, offsettingSubaccount: constants.Dave_Num0_1BTC_Long_50000USD_Short, - deltaQuantums: big.NewInt(100_000_000), // 1 BTC + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: constants.ClobPair_Btc.Id, + BaseQuantumsDelta: big.NewInt(100_000_000), + }, + }, + deltaQuantums: big.NewInt(100_000_000), // 1 BTC expectedLiquidatedSubaccount: satypes.Subaccount{ Id: &constants.Carl_Num0, @@ -998,7 +1101,13 @@ func TestProcessDeleveraging(t *testing.T) { "Liquidated: under-collateralized, TNC == 0, offsetting: under-collateralized, TNC < 0": { liquidatedSubaccount: constants.Carl_Num0_1BTC_Short_50000USD, offsettingSubaccount: constants.Dave_Num0_1BTC_Long_50001USD_Short, - deltaQuantums: big.NewInt(100_000_000), // 1 BTC + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: constants.ClobPair_Btc.Id, + BaseQuantumsDelta: big.NewInt(100_000_000), + }, + }, + deltaQuantums: big.NewInt(100_000_000), // 1 BTC // TNC of liquidated subaccount is $0, which means the bankruptcy price // to close 1 BTC short is $50,000. @@ -1012,7 +1121,13 @@ func TestProcessDeleveraging(t *testing.T) { "Liquidated: under-collateralized, TNC < 0, offsetting: well-collateralized": { liquidatedSubaccount: constants.Carl_Num0_1BTC_Short_49999USD, offsettingSubaccount: constants.Dave_Num0_1BTC_Long_50000USD, - deltaQuantums: big.NewInt(100_000_000), // 1 BTC + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: constants.ClobPair_Btc.Id, + BaseQuantumsDelta: big.NewInt(100_000_000), + }, + }, + deltaQuantums: big.NewInt(100_000_000), // 1 BTC expectedLiquidatedSubaccount: satypes.Subaccount{ Id: &constants.Carl_Num0, @@ -1029,7 +1144,13 @@ func TestProcessDeleveraging(t *testing.T) { "Liquidated: under-collateralized, TNC < 0, offsetting: under-collateralized, TNC > 0": { liquidatedSubaccount: constants.Carl_Num0_1BTC_Short_49999USD, offsettingSubaccount: constants.Dave_Num0_1BTC_Long_45001USD_Short, - deltaQuantums: big.NewInt(100_000_000), // 1 BTC + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: constants.ClobPair_Btc.Id, + BaseQuantumsDelta: big.NewInt(100_000_000), + }, + }, + deltaQuantums: big.NewInt(100_000_000), // 1 BTC expectedLiquidatedSubaccount: satypes.Subaccount{ Id: &constants.Carl_Num0, @@ -1046,7 +1167,13 @@ func TestProcessDeleveraging(t *testing.T) { "Liquidated: under-collateralized, TNC < 0, offsetting: under-collateralized, TNC == 0": { liquidatedSubaccount: constants.Carl_Num0_1BTC_Short_49999USD, offsettingSubaccount: constants.Dave_Num0_1BTC_Long_50000USD_Short, - deltaQuantums: big.NewInt(100_000_000), // 1 BTC + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: constants.ClobPair_Btc.Id, + BaseQuantumsDelta: big.NewInt(100_000_000), + }, + }, + deltaQuantums: big.NewInt(100_000_000), // 1 BTC // TNC of liquidated subaccount is $-1, which means the bankruptcy price // to close 1 BTC short is $49,999. @@ -1060,7 +1187,13 @@ func TestProcessDeleveraging(t *testing.T) { "Liquidated: under-collateralized, TNC < 0, offsetting: under-collateralized, TNC < 0": { liquidatedSubaccount: constants.Carl_Num0_1BTC_Short_49999USD, offsettingSubaccount: constants.Dave_Num0_1BTC_Long_50001USD_Short, - deltaQuantums: big.NewInt(100_000_000), // 1 BTC + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: constants.ClobPair_Btc.Id, + BaseQuantumsDelta: big.NewInt(100_000_000), + }, + }, + deltaQuantums: big.NewInt(100_000_000), // 1 BTC // TNC of liquidated subaccount is $-1, which means the bankruptcy price // to close 1 BTC short is $49,999. @@ -1074,7 +1207,13 @@ func TestProcessDeleveraging(t *testing.T) { can deleverage a partial position`: { liquidatedSubaccount: constants.Carl_Num0_1BTC_Short_54999USD, offsettingSubaccount: constants.Dave_Num0_1BTC_Long_50000USD, - deltaQuantums: big.NewInt(10_000_000), // 0.1 BTC + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: constants.ClobPair_Btc.Id, + BaseQuantumsDelta: big.NewInt(10_000_000), // 0.1 BTC + }, + }, + deltaQuantums: big.NewInt(10_000_000), // 0.1 BTC expectedLiquidatedSubaccount: satypes.Subaccount{ Id: &constants.Carl_Num0, @@ -1109,7 +1248,13 @@ func TestProcessDeleveraging(t *testing.T) { can not deleverage paritial positions`: { liquidatedSubaccount: constants.Carl_Num0_1BTC_Short_49999USD, offsettingSubaccount: constants.Dave_Num0_1BTC_Long_50001USD_Short, - deltaQuantums: big.NewInt(10_000_000), // 0.1 BTC + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: constants.ClobPair_Btc.Id, + BaseQuantumsDelta: big.NewInt(10_000_000), // 0.1 BTC + }, + }, + deltaQuantums: big.NewInt(10_000_000), // 0.1 BTC // TNC of liquidated subaccount is $-1, which means the bankruptcy price // to close 1 BTC short is $49,999. @@ -1142,7 +1287,13 @@ func TestProcessDeleveraging(t *testing.T) { }, }, offsettingSubaccount: constants.Dave_Num0_1BTC_Long_50000USD, - deltaQuantums: big.NewInt(100_000_000), // 1 BTC + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: constants.ClobPair_Btc.Id, + BaseQuantumsDelta: big.NewInt(100_000_000), + }, + }, + deltaQuantums: big.NewInt(100_000_000), // 1 BTC expectedLiquidatedSubaccount: satypes.Subaccount{ Id: &constants.Carl_Num0, @@ -1229,6 +1380,17 @@ func TestProcessDeleveraging(t *testing.T) { require.NoError(t, err) } + for _, perpOI := range tc.perpOIs { + require.NoError( + t, + ks.PerpetualsKeeper.ModifyOpenInterest( + ks.Ctx, + perpOI.PerpetualId, + perpOI.BaseQuantumsDelta, + ), + ) + } + ks.SubaccountsKeeper.SetSubaccount(ks.Ctx, tc.liquidatedSubaccount) ks.SubaccountsKeeper.SetSubaccount(ks.Ctx, tc.offsettingSubaccount) diff --git a/protocol/x/clob/keeper/liquidations_test.go b/protocol/x/clob/keeper/liquidations_test.go index ec916312e10..d0559297a98 100644 --- a/protocol/x/clob/keeper/liquidations_test.go +++ b/protocol/x/clob/keeper/liquidations_test.go @@ -37,6 +37,7 @@ func TestPlacePerpetualLiquidation(t *testing.T) { tests := map[string]struct { // Perpetuals state. perpetuals []perptypes.Perpetual + perpOIs []perptypes.OpenInterestDelta // Subaccount state. subaccounts []satypes.Subaccount // CLOB state. @@ -71,6 +72,12 @@ func TestPlacePerpetualLiquidation(t *testing.T) { perpetuals: []perptypes.Perpetual{ constants.BtcUsd_SmallMarginRequirement, }, + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: constants.ClobPair_Btc.Id, + BaseQuantumsDelta: big.NewInt(100_000_000), + }, + }, subaccounts: []satypes.Subaccount{ constants.Carl_Num0_1BTC_Short, constants.Dave_Num0_1BTC_Long_46000USD_Short, @@ -111,6 +118,12 @@ func TestPlacePerpetualLiquidation(t *testing.T) { perpetuals: []perptypes.Perpetual{ constants.BtcUsd_SmallMarginRequirement, }, + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: constants.ClobPair_Btc.Id, + BaseQuantumsDelta: big.NewInt(100_000_000), + }, + }, subaccounts: []satypes.Subaccount{ constants.Carl_Num0_1BTC_Short, constants.Dave_Num0_1BTC_Long_46000USD_Short, @@ -153,6 +166,12 @@ func TestPlacePerpetualLiquidation(t *testing.T) { perpetuals: []perptypes.Perpetual{ constants.BtcUsd_SmallMarginRequirement, }, + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: constants.ClobPair_Btc.Id, + BaseQuantumsDelta: big.NewInt(100_000_000), + }, + }, subaccounts: []satypes.Subaccount{ constants.Carl_Num0_1BTC_Short, constants.Dave_Num0_1BTC_Long_46000USD_Short, @@ -197,7 +216,13 @@ func TestPlacePerpetualLiquidation(t *testing.T) { constants.Carl_Num0_1BTC_Short, constants.Dave_Num0_1BTC_Long_46000USD_Short, }, - clobs: []types.ClobPair{constants.ClobPair_Btc}, + clobs: []types.ClobPair{constants.ClobPair_Btc}, + perpOIs: []perptypes.OpenInterestDelta{ + { + PerpetualId: constants.ClobPair_Btc.Id, + BaseQuantumsDelta: big.NewInt(100_000_000), + }, + }, feeParams: constants.PerpetualFeeParamsMakerRebate, existingOrders: []types.Order{ constants.Order_Carl_Num0_Id0_Clob0_Buy1BTC_Price50000_GTB10, @@ -299,6 +324,14 @@ func TestPlacePerpetualLiquidation(t *testing.T) { require.NoError(t, err) } + for _, perpOI := range tc.perpOIs { + require.NoError(t, ks.PerpetualsKeeper.ModifyOpenInterest( + ctx, + perpOI.PerpetualId, + perpOI.BaseQuantumsDelta, + )) + } + // Create all subaccounts. for _, subaccount := range tc.subaccounts { ks.SubaccountsKeeper.SetSubaccount(ctx, subaccount) @@ -318,6 +351,11 @@ func TestPlacePerpetualLiquidation(t *testing.T) { require.NoError(t, err) } + // ks.PerpetualsKeeper.ModifyOpenInterest( + // ctx, + + // ) + // Initialize the liquidations config. require.NoError( t, diff --git a/protocol/x/clob/keeper/orders.go b/protocol/x/clob/keeper/orders.go index f65aff2fce7..813b1d1f200 100644 --- a/protocol/x/clob/keeper/orders.go +++ b/protocol/x/clob/keeper/orders.go @@ -1098,7 +1098,7 @@ func (k Keeper) AddOrderToOrderbookCollatCheck( success, successPerSubaccountUpdate, err := k.subaccountsKeeper.CanUpdateSubaccounts( ctx, updates, - satypes.Match, + satypes.CollatCheck, ) // TODO(DEC-191): Remove the error case from `CanUpdateSubaccounts`, which can only occur on overflow and specifying // duplicate accounts. diff --git a/protocol/x/clob/memclob/memclob.go b/protocol/x/clob/memclob/memclob.go index 8f17780a854..207379907af 100644 --- a/protocol/x/clob/memclob/memclob.go +++ b/protocol/x/clob/memclob/memclob.go @@ -1712,6 +1712,7 @@ func (m *MemClobPriceTimePriority) mustPerformTakerOrderMatching( FillAmount: matchedAmount, } + fmt.Printf("!!! Processing matchWithOrders = %v\n", matchWithOrders) success, takerUpdateResult, makerUpdateResult, _, err := m.clobKeeper.ProcessSingleMatch(ctx, &matchWithOrders) if err != nil && !errors.Is(err, satypes.ErrFailedToUpdateSubaccounts) { if errors.Is(err, types.ErrLiquidationExceedsSubaccountMaxInsuranceLost) { diff --git a/protocol/x/perpetuals/types/types.go b/protocol/x/perpetuals/types/types.go index 84cfaacd6e7..e5a089094e5 100644 --- a/protocol/x/perpetuals/types/types.go +++ b/protocol/x/perpetuals/types/types.go @@ -112,3 +112,10 @@ type PerpetualsKeeper interface { ctx sdk.Context, ) []Perpetual } + +type OpenInterestDelta struct { + // The `Id` of the `Perpetual`. + PerpetualId uint32 + // Delta of open interest (in base quantums). + BaseQuantumsDelta *big.Int +} diff --git a/protocol/x/subaccounts/keeper/isolated_subaccount.go b/protocol/x/subaccounts/keeper/isolated_subaccount.go index e5547d3986c..bb664be6e8a 100644 --- a/protocol/x/subaccounts/keeper/isolated_subaccount.go +++ b/protocol/x/subaccounts/keeper/isolated_subaccount.go @@ -22,7 +22,7 @@ import ( // caused a failure, if any. func (k Keeper) checkIsolatedSubaccountConstraints( ctx sdk.Context, - settledUpdates []settledUpdate, + settledUpdates []SettledUpdate, perpetuals []perptypes.Perpetual, ) ( success bool, @@ -63,7 +63,7 @@ func (k Keeper) checkIsolatedSubaccountConstraints( // - a subaccount with no positions cannot be updated to have positions in multiple isolated // perpetuals or a combination of isolated and non-isolated perpetuals func isValidIsolatedPerpetualUpdates( - settledUpdate settledUpdate, + settledUpdate SettledUpdate, perpIdToMarketType map[uint32]perptypes.PerpetualMarketType, ) (types.UpdateResult, error) { // If there are no perpetual updates, then this update does not violate constraints for isolated diff --git a/protocol/x/subaccounts/keeper/oimf.go b/protocol/x/subaccounts/keeper/oimf.go new file mode 100644 index 00000000000..8fe7f18ca3e --- /dev/null +++ b/protocol/x/subaccounts/keeper/oimf.go @@ -0,0 +1,89 @@ +package keeper + +import ( + "fmt" + "math/big" + + "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" +) + +// Helper function to compute the delta long for a single settled update on a perpetual. +func getDeltaLongFromSettledUpdate( + u SettledUpdate, + updatedPerpId uint32, +) ( + deltaLong *big.Int, +) { + var perpPosition *types.PerpetualPosition + for _, p := range u.SettledSubaccount.PerpetualPositions { + // TODO use a pre-populated map + if p.PerpetualId == updatedPerpId { + perpPosition = p + } + } + + prevQuantums := perpPosition.GetBigQuantums() + afterQuantums := new(big.Int).Add( + prevQuantums, + u.PerpetualUpdates[0].GetBigQuantums(), + ) + + prevLong := prevQuantums // re-use pointer for efficiency + if prevLong.Sign() < 0 { + prevLong.SetUint64(0) + } + afterLong := afterQuantums // re-use pointer for efficiency + if afterLong.Sign() < 0 { + afterLong.SetUint64(0) + } + + return afterLong.Sub( + afterLong, + prevLong, + ) +} + +// Returns the delta open_interest for a pair of Match updates if they were applied. +func GetDeltaOpenInterestFromPerpMatchUpdates( + settledUpdates []SettledUpdate, +) ( + updatedPerpId uint32, + deltaOpenInterest *big.Int, +) { + if len(settledUpdates) != 2 { + panic( + fmt.Sprintf( + types.ErrMatchUpdatesMustHaveTwoUpdates, + settledUpdates, + ), + ) + } + + if len(settledUpdates[0].PerpetualUpdates) != 1 || len(settledUpdates[1].PerpetualUpdates) != 1 { + panic( + fmt.Sprintf( + types.ErrMatchUpdatesMustUpdateOnePerp, + settledUpdates, + ), + ) + } + + if settledUpdates[0].PerpetualUpdates[0].PerpetualId != settledUpdates[1].PerpetualUpdates[0].PerpetualId { + panic( + fmt.Sprintf( + types.ErrMatchUpdatesMustBeSamePerpId, + settledUpdates, + ), + ) + } else { + updatedPerpId = settledUpdates[0].PerpetualUpdates[0].PerpetualId + } + + deltaOpenInterest = big.NewInt(0) + for _, u := range settledUpdates { + deltaLong := getDeltaLongFromSettledUpdate(u, updatedPerpId) + deltaOpenInterest.Add(deltaOpenInterest, deltaLong) + } + + return updatedPerpId, deltaOpenInterest +} diff --git a/protocol/x/subaccounts/keeper/oimf_test.go b/protocol/x/subaccounts/keeper/oimf_test.go new file mode 100644 index 00000000000..27d4c8b56bb --- /dev/null +++ b/protocol/x/subaccounts/keeper/oimf_test.go @@ -0,0 +1,302 @@ +package keeper_test + +import ( + "fmt" + "math/big" + "testing" + + "github.com/dydxprotocol/v4-chain/protocol/dtypes" + keeper "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/keeper" + "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" + "github.com/stretchr/testify/require" +) + +var ( + aliceSubaccountId = &types.SubaccountId{ + Owner: "Alice", + } + bobSubaccountId = &types.SubaccountId{ + Owner: "Bob", + } +) + +func TestGetDeltaOpenInterestFromPerpMatchUpdates(t *testing.T) { + tests := map[string]struct { + settledUpdates []keeper.SettledUpdate + expectedDelta *big.Int + expectedUpdatedPerpId uint32 + panicErr string + }{ + "Invalid: 1 update": { + settledUpdates: []keeper.SettledUpdate{ + { + SettledSubaccount: types.Subaccount{}, + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: 0, + BigQuantumsDelta: big.NewInt(1_000), + }, + }, + }, + }, + panicErr: types.ErrMatchUpdatesMustHaveTwoUpdates, + }, + "Invalid: one of the updates contains no perp update": { + settledUpdates: []keeper.SettledUpdate{ + { + SettledSubaccount: types.Subaccount{ + Id: aliceSubaccountId, + }, + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: 0, + BigQuantumsDelta: big.NewInt(1_000), + }, + }, + }, + { + SettledSubaccount: types.Subaccount{ + Id: bobSubaccountId, + }, + }, + }, + panicErr: types.ErrMatchUpdatesMustUpdateOnePerp, + }, + "Invalid: updates are on different perpetuals": { + settledUpdates: []keeper.SettledUpdate{ + { + SettledSubaccount: types.Subaccount{ + Id: aliceSubaccountId, + }, + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: 0, + BigQuantumsDelta: big.NewInt(1_000), + }, + }, + }, + { + SettledSubaccount: types.Subaccount{ + Id: bobSubaccountId, + }, + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: 1, + BigQuantumsDelta: big.NewInt(1_000), + }, + }, + }, + }, + panicErr: types.ErrMatchUpdatesMustBeSamePerpId, + }, + "Valid: 0 -> -500, 0 -> 500, delta = 500": { + settledUpdates: []keeper.SettledUpdate{ + { + SettledSubaccount: types.Subaccount{ + Id: aliceSubaccountId, + }, + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: 1, + BigQuantumsDelta: big.NewInt(500), + }, + }, + }, + { + SettledSubaccount: types.Subaccount{ + Id: bobSubaccountId, + }, + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: 1, + BigQuantumsDelta: big.NewInt(-500), + }, + }, + }, + }, + expectedUpdatedPerpId: 1, + expectedDelta: big.NewInt(500), + }, + "Valid: 500 -> 0, 0 -> 500, delta = 0": { + settledUpdates: []keeper.SettledUpdate{ + { + SettledSubaccount: types.Subaccount{ + Id: aliceSubaccountId, + PerpetualPositions: []*types.PerpetualPosition{}, + }, + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: 1000, + BigQuantumsDelta: big.NewInt(500), + }, + }, + }, + { + SettledSubaccount: types.Subaccount{ + Id: bobSubaccountId, + PerpetualPositions: []*types.PerpetualPosition{ + { + PerpetualId: 1000, + Quantums: dtypes.NewInt(500), + }, + }, + }, + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: 1000, + BigQuantumsDelta: big.NewInt(-500), + }, + }, + }, + }, + expectedUpdatedPerpId: 1000, + expectedDelta: big.NewInt(0), + }, + "Valid: 500 -> 350, 0 -> 150, delta = 0": { + settledUpdates: []keeper.SettledUpdate{ + { + SettledSubaccount: types.Subaccount{ + Id: aliceSubaccountId, + PerpetualPositions: []*types.PerpetualPosition{ + { + PerpetualId: 1000, + Quantums: dtypes.NewInt(500), + }, + }, + }, + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: 1000, + BigQuantumsDelta: big.NewInt(-150), + }, + }, + }, + { + SettledSubaccount: types.Subaccount{ + Id: bobSubaccountId, + PerpetualPositions: []*types.PerpetualPosition{}, + }, + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: 1000, + BigQuantumsDelta: big.NewInt(150), + }, + }, + }, + }, + expectedUpdatedPerpId: 1000, + expectedDelta: big.NewInt(0), + }, + "Valid: -100 -> 200, 250 -> -50, delta = -50": { + settledUpdates: []keeper.SettledUpdate{ + { + SettledSubaccount: types.Subaccount{ + Id: aliceSubaccountId, + PerpetualPositions: []*types.PerpetualPosition{ + { + PerpetualId: 1000, + Quantums: dtypes.NewInt(-100), + }, + }, + }, + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: 1000, + BigQuantumsDelta: big.NewInt(300), + }, + }, + }, + { + SettledSubaccount: types.Subaccount{ + Id: bobSubaccountId, + PerpetualPositions: []*types.PerpetualPosition{ + { + PerpetualId: 1000, + Quantums: dtypes.NewInt(250), + }, + }, + }, + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: 1000, + BigQuantumsDelta: big.NewInt(-300), + }, + }, + }, + }, + expectedUpdatedPerpId: 1000, + expectedDelta: big.NewInt(-50), + }, + "Valid: -3100 -> -5000, 1000 -> 2900, delta = 1900": { + settledUpdates: []keeper.SettledUpdate{ + { + SettledSubaccount: types.Subaccount{ + Id: aliceSubaccountId, + PerpetualPositions: []*types.PerpetualPosition{ + { + PerpetualId: 1000, + Quantums: dtypes.NewInt(-3100), + }, + }, + }, + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: 1000, + BigQuantumsDelta: big.NewInt(-1900), + }, + }, + }, + { + SettledSubaccount: types.Subaccount{ + Id: bobSubaccountId, + PerpetualPositions: []*types.PerpetualPosition{ + { + PerpetualId: 1000, + Quantums: dtypes.NewInt(1000), + }, + }, + }, + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: 1000, + BigQuantumsDelta: big.NewInt(+1900), + }, + }, + }, + }, + expectedUpdatedPerpId: 1000, + expectedDelta: big.NewInt(1900), + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + if tc.panicErr != "" { + require.PanicsWithValue(t, + fmt.Sprintf( + tc.panicErr, + tc.settledUpdates, + ), func() { + keeper.GetDeltaOpenInterestFromPerpMatchUpdates(tc.settledUpdates) + }, + ) + return + } + + updatedPerpId, deltaOpenInterest := keeper.GetDeltaOpenInterestFromPerpMatchUpdates(tc.settledUpdates) + require.Equal( + t, + tc.expectedUpdatedPerpId, + updatedPerpId, + ) + fmt.Printf("deltaOpenInterest: %+v, tc.expectedDelta: %+v\n", deltaOpenInterest == nil, tc.expectedDelta == nil) + require.Zerof( + t, + tc.expectedDelta.Cmp(deltaOpenInterest), + "deltaOpenInterest: %v, tc.expectedDelta: %v", + deltaOpenInterest, + tc.expectedDelta, + ) + }) + } +} diff --git a/protocol/x/subaccounts/keeper/subaccount.go b/protocol/x/subaccounts/keeper/subaccount.go index ca9e978e80f..cbd96facfb7 100644 --- a/protocol/x/subaccounts/keeper/subaccount.go +++ b/protocol/x/subaccounts/keeper/subaccount.go @@ -206,12 +206,12 @@ func (k Keeper) getSettledUpdates( updates []types.Update, requireUniqueSubaccount bool, ) ( - settledUpdates []settledUpdate, + settledUpdates []SettledUpdate, subaccountIdToFundingPayments map[types.SubaccountId]map[uint32]dtypes.SerializableInt, err error, ) { var idToSettledSubaccount = make(map[types.SubaccountId]types.Subaccount) - settledUpdates = make([]settledUpdate, len(updates)) + settledUpdates = make([]SettledUpdate, len(updates)) subaccountIdToFundingPayments = make(map[types.SubaccountId]map[uint32]dtypes.SerializableInt) // Iterate over all updates and query the relevant `Subaccounts`. @@ -236,7 +236,7 @@ func (k Keeper) getSettledUpdates( subaccountIdToFundingPayments[u.SubaccountId] = fundingPayments } - settledUpdate := settledUpdate{ + settledUpdate := SettledUpdate{ SettledSubaccount: settledSubaccount, AssetUpdates: u.AssetUpdates, PerpetualUpdates: u.PerpetualUpdates, @@ -528,7 +528,7 @@ func checkPositionUpdatable( // caused a failure, if any. func (k Keeper) internalCanUpdateSubaccounts( ctx sdk.Context, - settledUpdates []settledUpdate, + settledUpdates []SettledUpdate, updateType types.UpdateType, perpetuals []perptypes.Perpetual, ) ( @@ -609,6 +609,17 @@ func (k Keeper) internalCanUpdateSubaccounts( } } + var perpOpenInterestDelta *perptypes.OpenInterestDelta + // If update type is `Match`, calculate the open interest delta for the updated perpetual. + // Note: this assumes that `Match` can only happen for perpetuals. + if updateType == types.Match { + updatedPerpId, deltaOpenInterest := GetDeltaOpenInterestFromPerpMatchUpdates(settledUpdates) + perpOpenInterestDelta = &perptypes.OpenInterestDelta{ + PerpetualId: updatedPerpId, + BaseQuantumsDelta: deltaOpenInterest, + } + } + bigCurNetCollateral := make(map[string]*big.Int) bigCurInitialMargin := make(map[string]*big.Int) bigCurMaintenanceMargin := make(map[string]*big.Int) @@ -631,11 +642,51 @@ func (k Keeper) internalCanUpdateSubaccounts( } } + // Temporily apply open interest delta to perpetuals, so IMF is calculated based on open interest after the update. + // `perpOpenInterestDeltas` is only present for `Match` update type. + if perpOpenInterestDelta != nil { + if err := k.perpetualsKeeper.ModifyOpenInterest( + ctx, + perpOpenInterestDelta.PerpetualId, + perpOpenInterestDelta.BaseQuantumsDelta, + ); err != nil { + return false, nil, errorsmod.Wrapf( + types.ErrCannotModifyPerpOpenInterestForOIMF, + "perpId = %v, delta = %v, settledUpdates = %+v", + perpOpenInterestDelta.PerpetualId, + perpOpenInterestDelta.BaseQuantumsDelta, + settledUpdates, + ) + } + } // Get the new collateralization and margin requirements with the update applied. bigNewNetCollateral, bigNewInitialMargin, bigNewMaintenanceMargin, - err := k.internalGetNetCollateralAndMarginRequirements(ctx, u) + err := k.internalGetNetCollateralAndMarginRequirements( + ctx, + u, + ) + + // Revert the temporary open interest delta to perpetuals. + if perpOpenInterestDelta != nil { + if err := k.perpetualsKeeper.ModifyOpenInterest( + ctx, + perpOpenInterestDelta.PerpetualId, + new(big.Int).Neg( + perpOpenInterestDelta.BaseQuantumsDelta, + ), + ); err != nil { + return false, nil, errorsmod.Wrapf( + types.ErrCannotRevertPerpOpenInterestForOIMF, + "perpId = %v, delta = %v, settledUpdates = %+v", + perpOpenInterestDelta.PerpetualId, + perpOpenInterestDelta.BaseQuantumsDelta, + settledUpdates, + ) + } + } + // if `internalGetNetCollateralAndMarginRequirements`, returns error. if err != nil { return false, nil, err } @@ -646,7 +697,7 @@ func (k Keeper) internalCanUpdateSubaccounts( // We must now check if the state transition is valid. if bigNewInitialMargin.Cmp(bigNewNetCollateral) > 0 { // Get the current collateralization and margin requirements without the update applied. - emptyUpdate := settledUpdate{ + emptyUpdate := SettledUpdate{ SettledSubaccount: u.SettledSubaccount, } @@ -785,7 +836,7 @@ func (k Keeper) GetNetCollateralAndMarginRequirements( return nil, nil, nil, err } - settledUpdate := settledUpdate{ + settledUpdate := SettledUpdate{ SettledSubaccount: settledSubaccount, AssetUpdates: update.AssetUpdates, PerpetualUpdates: update.PerpetualUpdates, @@ -810,7 +861,7 @@ func (k Keeper) GetNetCollateralAndMarginRequirements( // If two position updates reference the same position, an error is returned. func (k Keeper) internalGetNetCollateralAndMarginRequirements( ctx sdk.Context, - settledUpdate settledUpdate, + settledUpdate SettledUpdate, ) ( bigNetCollateral *big.Int, bigInitialMargin *big.Int, @@ -862,7 +913,11 @@ func (k Keeper) internalGetNetCollateralAndMarginRequirements( bigInitialMarginRequirements, bigMaintenanceMarginRequirements, - err := pk.GetMarginRequirements(ctx, id, bigQuantums) + err := pk.GetMarginRequirements( + ctx, + id, + bigQuantums, + ) if err != nil { return err } diff --git a/protocol/x/subaccounts/keeper/subaccount_helper.go b/protocol/x/subaccounts/keeper/subaccount_helper.go index 71721cb9a6d..79b14fbc2e6 100644 --- a/protocol/x/subaccounts/keeper/subaccount_helper.go +++ b/protocol/x/subaccounts/keeper/subaccount_helper.go @@ -12,7 +12,7 @@ import ( // been updated. This will include any asset postions that were closed due to an update. // TODO(DEC-1295): look into reducing code duplication here using Generics+Reflect. func getUpdatedAssetPositions( - update settledUpdate, + update SettledUpdate, ) []*types.AssetPosition { assetIdToPositionMap := make(map[uint32]*types.AssetPosition) for _, assetPosition := range update.SettledSubaccount.AssetPositions { @@ -53,7 +53,7 @@ func getUpdatedAssetPositions( // been updated. This will include any perpetual postions that were closed due to an update or that // received / paid out funding payments.. func getUpdatedPerpetualPositions( - update settledUpdate, + update SettledUpdate, fundingPayments map[uint32]dtypes.SerializableInt, ) []*types.PerpetualPosition { perpetualIdToPositionMap := make(map[uint32]*types.PerpetualPosition) @@ -102,7 +102,7 @@ func getUpdatedPerpetualPositions( // to reflect settledUpdate.PerpetualUpdates. // For newly created positions, use `perpIdToFundingIndex` map to populate the `FundingIndex` field. func UpdatePerpetualPositions( - settledUpdates []settledUpdate, + settledUpdates []SettledUpdate, perpIdToFundingIndex map[uint32]dtypes.SerializableInt, ) { // Apply the updates. @@ -166,7 +166,7 @@ func UpdatePerpetualPositions( // For each settledUpdate in settledUpdates, updates its SettledSubaccount.AssetPositions // to reflect settledUpdate.AssetUpdates. func UpdateAssetPositions( - settledUpdates []settledUpdate, + settledUpdates []SettledUpdate, ) { // Apply the updates. for i, u := range settledUpdates { diff --git a/protocol/x/subaccounts/keeper/subaccount_test.go b/protocol/x/subaccounts/keeper/subaccount_test.go index 49fcaab6855..ab583c775d3 100644 --- a/protocol/x/subaccounts/keeper/subaccount_test.go +++ b/protocol/x/subaccounts/keeper/subaccount_test.go @@ -300,7 +300,8 @@ func TestUpdateSubaccounts(t *testing.T) { assetPositions []*types.AssetPosition // updates - updates []types.Update + updates []types.Update + updateType types.UpdateType // expectations expectedQuoteBalance *big.Int @@ -325,6 +326,7 @@ func TestUpdateSubaccounts(t *testing.T) { AssetUpdates: testutil.CreateUsdcAssetUpdate(big.NewInt(100)), }, }, + updateType: types.Deposit, expectedAssetPositions: []*types.AssetPosition{ { AssetId: uint32(0), @@ -373,6 +375,7 @@ func TestUpdateSubaccounts(t *testing.T) { AssetUpdates: testutil.CreateUsdcAssetUpdate(big.NewInt(-100)), }, }, + updateType: types.Deposit, msgSenderEnabled: true, }, "one negative update to USDC asset position + persist unsettled negative funding": { @@ -430,6 +433,7 @@ func TestUpdateSubaccounts(t *testing.T) { AssetUpdates: testutil.CreateUsdcAssetUpdate(big.NewInt(-100)), }, }, + updateType: types.Deposit, msgSenderEnabled: true, }, "one negative update to USDC asset position + persist unsettled positive funding": { @@ -488,6 +492,7 @@ func TestUpdateSubaccounts(t *testing.T) { AssetUpdates: testutil.CreateUsdcAssetUpdate(big.NewInt(-100)), }, }, + updateType: types.Deposit, msgSenderEnabled: true, }, "multiple updates for same position not allowed": { @@ -518,6 +523,7 @@ func TestUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, msgSenderEnabled: true, }, "multiple updates to same account not allowed": { @@ -533,6 +539,7 @@ func TestUpdateSubaccounts(t *testing.T) { AssetUpdates: testutil.CreateUsdcAssetUpdate(big.NewInt(-100)), }, }, + updateType: types.Deposit, msgSenderEnabled: true, }, "update increases position size": { @@ -574,6 +581,7 @@ func TestUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, msgSenderEnabled: false, }, "update decreases position size": { @@ -628,6 +636,7 @@ func TestUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, msgSenderEnabled: false, }, "update closes long position": { @@ -677,6 +686,7 @@ func TestUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, msgSenderEnabled: true, }, "update closes short position": { @@ -730,6 +740,7 @@ func TestUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, msgSenderEnabled: true, }, "update closes 2nd position and updates 1st": { @@ -794,6 +805,7 @@ func TestUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, msgSenderEnabled: true, }, "update closes first asset position and updates 2nd": { @@ -843,6 +855,7 @@ func TestUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, msgSenderEnabled: true, }, "update closes first 1 positions and updates 2nd": { @@ -917,6 +930,7 @@ func TestUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, msgSenderEnabled: true, }, "update opens new long position, uses current perpetual funding index": { @@ -970,6 +984,7 @@ func TestUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, msgSenderEnabled: false, }, "update opens new short position": { @@ -1022,6 +1037,7 @@ func TestUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, msgSenderEnabled: false, }, "update opens new long eth position with existing btc position": { @@ -1069,6 +1085,7 @@ func TestUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, msgSenderEnabled: true, }, // TODO(DEC-581): add similar test case for multi-collateral asset support. @@ -1122,6 +1139,7 @@ func TestUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, msgSenderEnabled: true, }, "update opens new long eth position with existing btc and sol position": { @@ -1180,6 +1198,7 @@ func TestUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, msgSenderEnabled: true, }, "update opens new long btc position with existing eth and sol position": { @@ -1242,6 +1261,7 @@ func TestUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, msgSenderEnabled: true, }, "update opens new long eth position with existing unsettled sol position": { @@ -1313,6 +1333,7 @@ func TestUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, msgSenderEnabled: true, }, "provides out-of-order updates (not ordered by PerpetualId)": { @@ -1398,6 +1419,7 @@ func TestUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, msgSenderEnabled: true, }, "updates multiple subaccounts with new perpetual and asset positions": { @@ -1483,6 +1505,7 @@ func TestUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, msgSenderEnabled: true, }, "update would make account undercollateralized": { @@ -1509,6 +1532,7 @@ func TestUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, msgSenderEnabled: true, }, "updates new USDC asset position which exceeds max uint64": { @@ -1545,6 +1569,7 @@ func TestUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, msgSenderEnabled: true, }, "new USDC asset position (including unsettled funding) size exceeds max uint64": { @@ -1612,6 +1637,7 @@ func TestUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, msgSenderEnabled: true, }, "new position size exceeds max uint64": { @@ -1651,6 +1677,7 @@ func TestUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, msgSenderEnabled: true, }, "existing position size + update exceeds max uint64": { @@ -2326,7 +2353,7 @@ func TestUpdateSubaccounts(t *testing.T) { tc.updates[i] = u } - success, successPerUpdate, err := keeper.UpdateSubaccounts(ctx, tc.updates, types.Match) + success, successPerUpdate, err := keeper.UpdateSubaccounts(ctx, tc.updates, tc.updateType) if tc.expectedErr != nil { require.ErrorIs(t, tc.expectedErr, err) } else { @@ -2651,7 +2678,7 @@ func TestUpdateSubaccounts_WithdrawalsBlocked(t *testing.T) { assetPositions: testutil.CreateUsdcAssetPosition(big.NewInt(25_000_000_000)), // $25,000 expectedQuoteBalance: big.NewInt(0), expectedSuccess: true, - expectedSuccessPerUpdate: []types.UpdateResult{types.Success}, + expectedSuccessPerUpdate: []types.UpdateResult{types.Success, types.Success}, perpetuals: []perptypes.Perpetual{ constants.BtcUsd_NoMarginRequirement, }, @@ -2685,6 +2712,16 @@ func TestUpdateSubaccounts_WithdrawalsBlocked(t *testing.T) { }, }, }, + { + SubaccountId: secondSubaccountId, + AssetUpdates: testutil.CreateUsdcAssetUpdate(big.NewInt(25_000_000_000)), // $25,000 + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: uint32(0), + BigQuantumsDelta: big.NewInt(-50_000_000), // .5 BTC + }, + }, + }, }, msgSenderEnabled: false, @@ -2698,7 +2735,7 @@ func TestUpdateSubaccounts_WithdrawalsBlocked(t *testing.T) { assetPositions: testutil.CreateUsdcAssetPosition(big.NewInt(25_000_000_000)), // $25,000 expectedQuoteBalance: big.NewInt(0), expectedSuccess: true, - expectedSuccessPerUpdate: []types.UpdateResult{types.Success}, + expectedSuccessPerUpdate: []types.UpdateResult{types.Success, types.Success}, perpetuals: []perptypes.Perpetual{ constants.BtcUsd_NoMarginRequirement, }, @@ -2732,6 +2769,16 @@ func TestUpdateSubaccounts_WithdrawalsBlocked(t *testing.T) { }, }, }, + { + SubaccountId: secondSubaccountId, + AssetUpdates: testutil.CreateUsdcAssetUpdate(big.NewInt(25_000_000_000)), // $25,000 + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: uint32(0), + BigQuantumsDelta: big.NewInt(-50_000_000), // .5 BTC + }, + }, + }, }, msgSenderEnabled: false, @@ -2745,7 +2792,7 @@ func TestUpdateSubaccounts_WithdrawalsBlocked(t *testing.T) { assetPositions: testutil.CreateUsdcAssetPosition(big.NewInt(25_000_000_000)), // $25,000 expectedQuoteBalance: big.NewInt(0), expectedSuccess: true, - expectedSuccessPerUpdate: []types.UpdateResult{types.Success}, + expectedSuccessPerUpdate: []types.UpdateResult{types.Success, types.Success}, perpetuals: []perptypes.Perpetual{ constants.BtcUsd_NoMarginRequirement, }, @@ -2779,6 +2826,16 @@ func TestUpdateSubaccounts_WithdrawalsBlocked(t *testing.T) { }, }, }, + { + SubaccountId: secondSubaccountId, + AssetUpdates: testutil.CreateUsdcAssetUpdate(big.NewInt(25_000_000_000)), // $25,000 + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: uint32(0), + BigQuantumsDelta: big.NewInt(-50_000_000), // .5 BTC + }, + }, + }, }, msgSenderEnabled: false, @@ -2788,9 +2845,12 @@ func TestUpdateSubaccounts_WithdrawalsBlocked(t *testing.T) { updateType: types.Match, }, "undercollateralized matches are not blocked if negative TNC subaccount was seen at current block": { - expectedQuoteBalance: big.NewInt(0), - expectedSuccess: false, - expectedSuccessPerUpdate: []types.UpdateResult{types.NewlyUndercollateralized}, + expectedQuoteBalance: big.NewInt(0), + expectedSuccess: false, + expectedSuccessPerUpdate: []types.UpdateResult{ + types.NewlyUndercollateralized, + types.NewlyUndercollateralized, + }, perpetuals: []perptypes.Perpetual{ constants.BtcUsd_SmallMarginRequirement, }, @@ -2810,6 +2870,16 @@ func TestUpdateSubaccounts_WithdrawalsBlocked(t *testing.T) { }, }, }, + { + SubaccountId: secondSubaccountId, + AssetUpdates: testutil.CreateUsdcAssetUpdate(big.NewInt(50_000_000_000)), // $50,000 + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: uint32(0), + BigQuantumsDelta: big.NewInt(-100_000_000), // -1 BTC + }, + }, + }, }, msgSenderEnabled: true, @@ -2820,9 +2890,12 @@ func TestUpdateSubaccounts_WithdrawalsBlocked(t *testing.T) { }, `undercollateralized matches are not blocked if current block is within WITHDRAWAL_AND_TRANSFERS_BLOCKED_AFTER_NEGATIVE_TNC_SUBACCOUNT_SEEN_BLOCKS`: { - expectedQuoteBalance: big.NewInt(0), - expectedSuccess: false, - expectedSuccessPerUpdate: []types.UpdateResult{types.NewlyUndercollateralized}, + expectedQuoteBalance: big.NewInt(0), + expectedSuccess: false, + expectedSuccessPerUpdate: []types.UpdateResult{ + types.NewlyUndercollateralized, + types.NewlyUndercollateralized, + }, perpetuals: []perptypes.Perpetual{ constants.BtcUsd_SmallMarginRequirement, }, @@ -2842,6 +2915,16 @@ func TestUpdateSubaccounts_WithdrawalsBlocked(t *testing.T) { }, }, }, + { + SubaccountId: secondSubaccountId, + AssetUpdates: testutil.CreateUsdcAssetUpdate(big.NewInt(50_000_000_000)), // $50,000 + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: uint32(0), + BigQuantumsDelta: big.NewInt(-100_000_000), // 1 BTC + }, + }, + }, }, msgSenderEnabled: true, @@ -2852,9 +2935,12 @@ func TestUpdateSubaccounts_WithdrawalsBlocked(t *testing.T) { updateType: types.Match, }, "undercollateralized matches are not blocked if negative TNC subaccount was never seen": { - expectedQuoteBalance: big.NewInt(0), - expectedSuccess: false, - expectedSuccessPerUpdate: []types.UpdateResult{types.NewlyUndercollateralized}, + expectedQuoteBalance: big.NewInt(0), + expectedSuccess: false, + expectedSuccessPerUpdate: []types.UpdateResult{ + types.NewlyUndercollateralized, + types.NewlyUndercollateralized, + }, perpetuals: []perptypes.Perpetual{ constants.BtcUsd_SmallMarginRequirement, }, @@ -2874,6 +2960,16 @@ func TestUpdateSubaccounts_WithdrawalsBlocked(t *testing.T) { }, }, }, + { + SubaccountId: secondSubaccountId, + AssetUpdates: testutil.CreateUsdcAssetUpdate(big.NewInt(50_000_000_000)), // $50,000 + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: uint32(0), + BigQuantumsDelta: big.NewInt(-100_000_000), // -1 BTC + }, + }, + }, }, msgSenderEnabled: true, @@ -3146,20 +3242,386 @@ func TestCanUpdateSubaccounts(t *testing.T) { perpetuals []perptypes.Perpetual assets []*asstypes.Asset marketParamPrices []pricestypes.MarketParamPrice + openInterests []perptypes.OpenInterestDelta // Subaccount state. - useEmptySubaccount bool - perpetualPositions []*types.PerpetualPosition - assetPositions []*types.AssetPosition + useEmptySubaccount bool + perpetualPositions []*types.PerpetualPosition + assetPositions []*types.AssetPosition + additionalTestSubaccounts []types.Subaccount // Updates. - updates []types.Update + updates []types.Update + updateType types.UpdateType // Expectations. expectedSuccess bool expectedSuccessPerUpdate []types.UpdateResult expectedErr error }{ + "(OIMF) OI increased, still at base IMF, match is collateralized": { + expectedSuccess: true, + expectedSuccessPerUpdate: []types.UpdateResult{types.Success, types.Success}, + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance, + }, + assetPositions: []*types.AssetPosition{ + { + AssetId: uint32(0), + // 900_000 USDC (just enough to colalteralize 90 BTC at $50_000 and 20% IMF) + Quantums: dtypes.NewInt(900_000_000_000), + }, + }, + additionalTestSubaccounts: []types.Subaccount{ + { + Id: &types.SubaccountId{ + Owner: "Bob", + Number: 0, + }, + AssetPositions: []*types.AssetPosition{ + { + AssetId: uint32(0), + // 900_000 USDC (just enough to colalteralize 90 BTC at $50_000 and 20% IMF) + Quantums: dtypes.NewInt(900_000_000_000), + }, + }, + }, + }, + updates: []types.Update{ + { + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: uint32(0), + BigQuantumsDelta: big.NewInt(9_000_000_000), // 90 BTC + }, + }, + AssetUpdates: []types.AssetUpdate{ + { + AssetId: uint32(0), + BigQuantumsDelta: big.NewInt(-4_500_000_000_000), // -4,500,000 USDC + }, + }, + SubaccountId: types.SubaccountId{ + Owner: "Bob", + Number: 0, + }, + }, + { + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: uint32(0), + BigQuantumsDelta: big.NewInt(-9_000_000_000), // 9 BTC + }, + }, + AssetUpdates: []types.AssetUpdate{ + { + AssetId: uint32(0), + BigQuantumsDelta: big.NewInt(4_500_000_000_000), // 4,500,000 USDC + }, + }, + }, + }, + updateType: types.Match, + }, + "(OIMF) current OI soft lower cap, match collateralized at base IMF but not OIMF": { + expectedSuccess: false, + expectedSuccessPerUpdate: []types.UpdateResult{ + types.NewlyUndercollateralized, + types.NewlyUndercollateralized, + }, + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance_25mmLowerCap_50mmUpperCap, + }, + assetPositions: []*types.AssetPosition{ + { + AssetId: uint32(0), + // 900_000 USDC (just enough to colalteralize 90 BTC at $50_000 and 20% IMF) + Quantums: dtypes.NewInt(900_000_000_000), + }, + }, + additionalTestSubaccounts: []types.Subaccount{ + { + Id: &types.SubaccountId{ + Owner: "Bob", + Number: 0, + }, + AssetPositions: []*types.AssetPosition{ + { + AssetId: uint32(0), + // 900_000 USDC (just enough to colalteralize 90 BTC at $50_000 and 20% IMF) + Quantums: dtypes.NewInt(900_000_000_000), + }, + }, + }, + }, + openInterests: []perptypes.OpenInterestDelta{ + { + PerpetualId: uint32(0), + // 500 BTC. At $50,000, this is $25,000,000 of OI. + BaseQuantumsDelta: big.NewInt(50_000_000_000), + }, + }, + updates: []types.Update{ + { + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: uint32(0), + BigQuantumsDelta: big.NewInt(9_000_000_000), // 90 BTC + }, + }, + AssetUpdates: []types.AssetUpdate{ + { + AssetId: uint32(0), + BigQuantumsDelta: big.NewInt(-4_500_000_000_000), // -4,500,000 USDC + }, + }, + SubaccountId: types.SubaccountId{ + Owner: "Bob", + Number: 0, + }, + }, + { + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: uint32(0), + BigQuantumsDelta: big.NewInt(-9_000_000_000), // 9 BTC + }, + }, + AssetUpdates: []types.AssetUpdate{ + { + AssetId: uint32(0), + BigQuantumsDelta: big.NewInt(4_500_000_000_000), // 4,500,000 USDC + }, + }, + }, + }, + updateType: types.Match, + }, + "(OIMF) match collateralized at base IMF and just collateralized at OIMF": { + expectedSuccess: true, + expectedSuccessPerUpdate: []types.UpdateResult{ + types.Success, + types.Success, + }, + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance_25mmLowerCap_50mmUpperCap, + }, + assetPositions: []*types.AssetPosition{ + { + AssetId: uint32(0), + // 900_000 USDC (just enough to colalteralize 90 BTC at $50_000 and 20% IMF) + Quantums: dtypes.NewInt(900_000_000_000), + }, + }, + additionalTestSubaccounts: []types.Subaccount{ + { + Id: &types.SubaccountId{ + Owner: "Bob", + Number: 0, + }, + AssetPositions: []*types.AssetPosition{ + { + AssetId: uint32(0), + // 900_000 USDC (just enough to colalteralize 90 BTC at $50_000 and 20% IMF) + Quantums: dtypes.NewInt(900_000_000_000), + }, + }, + }, + }, + openInterests: []perptypes.OpenInterestDelta{ + { + PerpetualId: uint32(0), + // (Only difference from prevoius test case) + // 410 BTC. At $50,000, this is $20,500,000 of OI. + // OI would be $25,000,000 after the Match updates, so OIMF is still at base IMF. + BaseQuantumsDelta: big.NewInt(41_000_000_000), + }, + }, + updates: []types.Update{ + { + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: uint32(0), + BigQuantumsDelta: big.NewInt(9_000_000_000), // 90 BTC + }, + }, + AssetUpdates: []types.AssetUpdate{ + { + AssetId: uint32(0), + BigQuantumsDelta: big.NewInt(-4_500_000_000_000), // -4,500,000 USDC + }, + }, + SubaccountId: types.SubaccountId{ + Owner: "Bob", + Number: 0, + }, + }, + { + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: uint32(0), + BigQuantumsDelta: big.NewInt(-9_000_000_000), // 9 BTC + }, + }, + AssetUpdates: []types.AssetUpdate{ + { + AssetId: uint32(0), + BigQuantumsDelta: big.NewInt(4_500_000_000_000), // 4,500,000 USDC + }, + }, + }, + }, + updateType: types.Match, + }, + "(OIMF) match collateralized at base IMF and just failed collateralization at OIMF": { + expectedSuccess: false, + expectedSuccessPerUpdate: []types.UpdateResult{ + types.NewlyUndercollateralized, + types.NewlyUndercollateralized, + }, + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance_25mmLowerCap_50mmUpperCap, + }, + assetPositions: []*types.AssetPosition{ + { + AssetId: uint32(0), + // 900_000 USDC (just enough to colalteralize 90 BTC at $50_000 and 20% IMF) + Quantums: dtypes.NewInt(900_000_000_000), + }, + }, + additionalTestSubaccounts: []types.Subaccount{ + { + Id: &types.SubaccountId{ + Owner: "Bob", + Number: 0, + }, + AssetPositions: []*types.AssetPosition{ + { + AssetId: uint32(0), + // 900_000 USDC (just enough to colalteralize 90 BTC at $50_000 and 20% IMF) + Quantums: dtypes.NewInt(900_000_000_000), + }, + }, + }, + }, + openInterests: []perptypes.OpenInterestDelta{ + { + PerpetualId: uint32(0), + // (Only difference from prevoius test case) + // 410 BTC + 1 base quantum. At $50,000, this is > $20,500,000 of OI. + // OI would be just past $25,000,000 after the Match updates, so OIMF > IMF = 20% + BaseQuantumsDelta: big.NewInt(41_000_000_001), + }, + }, + updates: []types.Update{ + { + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: uint32(0), + BigQuantumsDelta: big.NewInt(9_000_000_000), // 90 BTC + }, + }, + AssetUpdates: []types.AssetUpdate{ + { + AssetId: uint32(0), + BigQuantumsDelta: big.NewInt(-4_500_000_000_000), // -4,500,000 USDC + }, + }, + SubaccountId: types.SubaccountId{ + Owner: "Bob", + Number: 0, + }, + }, + { + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: uint32(0), + BigQuantumsDelta: big.NewInt(-9_000_000_000), // 9 BTC + }, + }, + AssetUpdates: []types.AssetUpdate{ + { + AssetId: uint32(0), + BigQuantumsDelta: big.NewInt(4_500_000_000_000), // 4,500,000 USDC + }, + }, + }, + }, + updateType: types.Match, + }, + "(OIMF) OIMF caps at 100%, un-leveraged trade always succeeds": { + expectedSuccess: true, + expectedSuccessPerUpdate: []types.UpdateResult{ + types.Success, + types.Success, + }, + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance_25mmLowerCap_50mmUpperCap, + }, + assetPositions: []*types.AssetPosition{ + { + AssetId: uint32(0), + // 4_500_000 USDC (just enough to collateralize 90 BTC at $50_000 and 100% IMF) + Quantums: dtypes.NewInt(4_500_000_000_000)}, + }, + additionalTestSubaccounts: []types.Subaccount{ + { + Id: &types.SubaccountId{ + Owner: "Bob", + Number: 0, + }, + AssetPositions: []*types.AssetPosition{ + { + AssetId: uint32(0), + // 4_500_000 USDC (just enough to collateralize 90 BTC at $50_000 and 100% IMF) + Quantums: dtypes.NewInt(4_500_000_000_000), + }, + }, + }, + }, + openInterests: []perptypes.OpenInterestDelta{ + { + PerpetualId: uint32(0), + // 10_000 BTC. At $50,000, this is $500mm of OI which way past upper cap + BaseQuantumsDelta: big.NewInt(1_000_000_000_000), + }, + }, + updates: []types.Update{ + { + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: uint32(0), + BigQuantumsDelta: big.NewInt(9_000_000_000), // 90 BTC + }, + }, + AssetUpdates: []types.AssetUpdate{ + { + AssetId: uint32(0), + BigQuantumsDelta: big.NewInt(-4_500_000_000_000), // -4,500,000 USDC + }, + }, + SubaccountId: types.SubaccountId{ + Owner: "Bob", + Number: 0, + }, + }, + { + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: uint32(0), + BigQuantumsDelta: big.NewInt(-9_000_000_000), // 9 BTC + }, + }, + AssetUpdates: []types.AssetUpdate{ + { + AssetId: uint32(0), + BigQuantumsDelta: big.NewInt(4_500_000_000_000), // 4,500,000 USDC + }, + }, + }, + }, + updateType: types.Match, + }, "one update with no existing position and no margin requirements": { expectedSuccess: true, expectedSuccessPerUpdate: []types.UpdateResult{types.Success}, @@ -3176,6 +3638,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, }, "new USDC asset position exceeds max uint64": { assetPositions: testutil.CreateUsdcAssetPosition(new(big.Int).SetUint64(math.MaxUint64)), @@ -3184,6 +3647,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { AssetUpdates: testutil.CreateUsdcAssetUpdate(big.NewInt(1)), }, }, + updateType: types.Deposit, expectedSuccess: true, expectedSuccessPerUpdate: []types.UpdateResult{types.Success}, }, @@ -3199,6 +3663,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { updates: []types.Update{ {}, }, + updateType: types.Deposit, }, "new position quantums exceeds max uint64": { perpetuals: []perptypes.Perpetual{ @@ -3221,6 +3686,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, expectedSuccess: true, expectedSuccessPerUpdate: []types.UpdateResult{types.Success}, }, @@ -3244,6 +3710,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, }, "multiple updates are considered independently for same account": { expectedSuccess: false, @@ -3282,6 +3749,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, }, "Undercollateralized: " + "First update makes account less collateralized, " + @@ -3339,6 +3807,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, }, "USDC asset position is negative but increasing when no positions are open": { assetPositions: testutil.CreateUsdcAssetPosition(big.NewInt(-10)), @@ -3351,6 +3820,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { expectedSuccessPerUpdate: []types.UpdateResult{ types.Success, }, + updateType: types.Deposit, }, "USDC asset position is negative but unchanging when no positions are open": { assetPositions: testutil.CreateUsdcAssetPosition(big.NewInt(-10)), @@ -3363,6 +3833,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { expectedSuccessPerUpdate: []types.UpdateResult{ types.StillUndercollateralized, }, + updateType: types.Deposit, }, "USDC asset position decreases below zero when no positions are open": { updates: []types.Update{ @@ -3374,6 +3845,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { expectedSuccessPerUpdate: []types.UpdateResult{ types.NewlyUndercollateralized, }, + updateType: types.Deposit, }, "USDC asset position decreases further below zero when no positions are open": { assetPositions: testutil.CreateUsdcAssetPosition(big.NewInt(-1)), @@ -3386,6 +3858,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { expectedSuccessPerUpdate: []types.UpdateResult{ types.StillUndercollateralized, }, + updateType: types.Deposit, }, "two updates on different accounts, second account is new account": { assetPositions: testutil.CreateUsdcAssetPosition(big.NewInt(50_000_000_000)), // $50,000 @@ -3421,6 +3894,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, }, "unsettled funding reduces USDC asset position to 1; further decrease USDC asset position, still collateralized": { assetPositions: testutil.CreateUsdcAssetPosition(big.NewInt(100)), @@ -3443,6 +3917,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { expectedSuccessPerUpdate: []types.UpdateResult{ types.Success, }, + updateType: types.Deposit, }, "unsettled funding reduces USDC asset position to zero; further decrease USDC asset position, undercollateralized": { assetPositions: testutil.CreateUsdcAssetPosition(big.NewInt(100)), @@ -3465,6 +3940,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { expectedSuccessPerUpdate: []types.UpdateResult{ types.NewlyUndercollateralized, }, + updateType: types.Deposit, }, "unsettled funding makes position undercollateralized": { assetPositions: testutil.CreateUsdcAssetPosition(big.NewInt(200)), @@ -3487,6 +3963,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { expectedSuccessPerUpdate: []types.UpdateResult{ types.NewlyUndercollateralized, }, + updateType: types.Deposit, }, "position was undercollateralized before update due to funding and still undercollateralized" + "after due to funding": { @@ -3510,6 +3987,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { expectedSuccessPerUpdate: []types.UpdateResult{ types.StillUndercollateralized, }, + updateType: types.Deposit, }, "unsettled funding makes position with negative USDC asset position collateralized before update": { assetPositions: testutil.CreateUsdcAssetPosition(big.NewInt(-100)), @@ -3530,6 +4008,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { expectedSuccessPerUpdate: []types.UpdateResult{ types.Success, }, + updateType: types.Deposit, }, "adding unsettled funding to USDC asset position exceeds max uint64": { assetPositions: testutil.CreateUsdcAssetPosition(new(big.Int).SetUint64(math.MaxUint64 - 1)), @@ -3546,6 +4025,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { updates: []types.Update{ {}, }, + updateType: types.Deposit, expectedSuccess: true, expectedSuccessPerUpdate: []types.UpdateResult{types.Success}, }, @@ -3566,6 +4046,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { updates: []types.Update{ {}, }, + updateType: types.Deposit, expectedSuccess: true, expectedSuccessPerUpdate: []types.UpdateResult{types.Success}, }, @@ -3588,6 +4069,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { AssetUpdates: testutil.CreateUsdcAssetUpdate(big.NewInt(3)), // $3 }, }, + updateType: types.Deposit, expectedSuccess: true, expectedSuccessPerUpdate: []types.UpdateResult{types.Success}, }, @@ -3637,6 +4119,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { }, }, }, + updateType: types.Deposit, }, "Isolated subaccounts - has update for both an isolated perpetual and non-isolated perpetual": { assetPositions: testutil.CreateUsdcAssetPosition(big.NewInt(1_000_000_000_000)), @@ -3808,6 +4291,15 @@ func TestCanUpdateSubaccounts(t *testing.T) { require.NoError(t, err) } + for _, openInterest := range tc.openInterests { + // Update open interest for each perpetual from default `0`. + require.NoError(t, perpetualsKeeper.ModifyOpenInterest( + ctx, + openInterest.PerpetualId, + openInterest.BaseQuantumsDelta, + )) + } + subaccountId := types.SubaccountId{Owner: "foo", Number: 0} if !tc.useEmptySubaccount { subaccount := createNSubaccount(keeper, ctx, 1, big.NewInt(1_000))[0] @@ -3824,7 +4316,11 @@ func TestCanUpdateSubaccounts(t *testing.T) { tc.updates[i] = u } - success, successPerUpdate, err := keeper.CanUpdateSubaccounts(ctx, tc.updates, types.Match) + for _, sa := range tc.additionalTestSubaccounts { + keeper.SetSubaccount(ctx, sa) + } + + success, successPerUpdate, err := keeper.CanUpdateSubaccounts(ctx, tc.updates, tc.updateType) if tc.expectedErr != nil { require.ErrorIs(t, tc.expectedErr, err) } else { diff --git a/protocol/x/subaccounts/keeper/update.go b/protocol/x/subaccounts/keeper/update.go index 179139452a3..30e0ad58f7c 100644 --- a/protocol/x/subaccounts/keeper/update.go +++ b/protocol/x/subaccounts/keeper/update.go @@ -4,11 +4,11 @@ import ( "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" ) -// settledUpdate is used internally in the subaccounts keeper to +// SettledUpdate is used internally in the subaccounts keeper to // to specify changes to one or more `Subaccounts` (for example the // result of a trade, transfer, etc). // The subaccount is always in its settled state. -type settledUpdate struct { +type SettledUpdate struct { // The `Subaccount` for which this update applies to, in its settled form. SettledSubaccount types.Subaccount // A list of changes to make to any `AssetPositions` in the `Subaccount`. diff --git a/protocol/x/subaccounts/types/errors.go b/protocol/x/subaccounts/types/errors.go index 91b4da30d20..1b5315b187d 100644 --- a/protocol/x/subaccounts/types/errors.go +++ b/protocol/x/subaccounts/types/errors.go @@ -7,6 +7,16 @@ import ( "github.com/dydxprotocol/v4-chain/protocol/lib" ) +// Panic strings +const ( + ErrMatchUpdatesMustHaveTwoUpdates = "internalCanUpdateSubaccounts: MATCH subaccount updates must consist of " + + "exactly 2 updates, got settledUpdates: %+v" + ErrMatchUpdatesMustUpdateOnePerp = "internalCanUpdateSubaccounts: MATCH subaccount updates must each have " + + "exactly 1 PerpetualUpdate, got settledUpdates: %+v" + ErrMatchUpdatesMustBeSamePerpId = "internalCanUpdateSubaccounts: MATCH subaccount updates must consists two " + + "updates on same perpetual Id, got settledUpdates: %+v" +) + // x/subaccounts module sentinel errors var ( // 0 - 99: generic. @@ -38,7 +48,21 @@ var ( // 400 - 499: perpetual position related. ErrPerpPositionsOutOfOrder = errorsmod.Register(ModuleName, 400, "perpetual positions are out of order") - ErrPerpPositionZeroQuantum = errorsmod.Register(ModuleName, 401, "perpetual position's quantum cannot be zero") + ErrPerpPositionZeroQuantum = errorsmod.Register( + ModuleName, + 401, + "perpetual position's quantum cannot be zero", + ) + ErrCannotModifyPerpOpenInterestForOIMF = errorsmod.Register( + ModuleName, + 402, + "cannot temporarily modify perpetual open interest for OIMF calculation", + ) + ErrCannotRevertPerpOpenInterestForOIMF = errorsmod.Register( + ModuleName, + 403, + "cannot revert perpetual open interest for OIMF calculation", + ) // 500 - 599: transfer related. ErrAssetTransferQuantumsNotPositive = errorsmod.Register( diff --git a/protocol/x/subaccounts/types/expected_keepers.go b/protocol/x/subaccounts/types/expected_keepers.go index 4d30eb81747..8f0c9dd2172 100644 --- a/protocol/x/subaccounts/types/expected_keepers.go +++ b/protocol/x/subaccounts/types/expected_keepers.go @@ -74,6 +74,7 @@ type PerpetualsKeeper interface { GetAllPerpetuals(ctx sdk.Context) []perptypes.Perpetual GetInsuranceFundName(ctx sdk.Context, perpetualId uint32) (string, error) GetInsuranceFundModuleAddress(ctx sdk.Context, perpetualId uint32) (sdk.AccAddress, error) + ModifyOpenInterest(ctx sdk.Context, perpetualId uint32, bigQuantums *big.Int) error } // BankKeeper defines the expected interface needed to retrieve account balances. diff --git a/protocol/x/subaccounts/types/update.go b/protocol/x/subaccounts/types/update.go index 684b618b28e..aee752ce1e9 100644 --- a/protocol/x/subaccounts/types/update.go +++ b/protocol/x/subaccounts/types/update.go @@ -103,13 +103,15 @@ const ( Transfer Deposit Match + CollatCheck ) var updateTypeStringMap = map[UpdateType]string{ - Withdrawal: "Withdrawal", - Transfer: "Transfer", - Deposit: "Deposit", - Match: "Match", + Withdrawal: "Withdrawal", + Transfer: "Transfer", + Deposit: "Deposit", + Match: "Match", + CollatCheck: "CollatCheck", } func (u UpdateType) String() string {