diff --git a/protocol/testutil/constants/perpetuals.go b/protocol/testutil/constants/perpetuals.go index 3c6db1e6fa..a95623be3f 100644 --- a/protocol/testutil/constants/perpetuals.go +++ b/protocol/testutil/constants/perpetuals.go @@ -104,12 +104,12 @@ var LiquidityTiers = []perptypes.LiquidityTier{ // Perpetual OI setup in tests var ( BtcUsd_OpenInterest1_AtomicRes8 = perptypes.OpenInterestDelta{ - PerpetualId: 0, - BaseQuantumsDelta: big.NewInt(100_000_000), + PerpetualId: 0, + BaseQuantums: big.NewInt(100_000_000), } EthUsd_OpenInterest1_AtomicRes9 = perptypes.OpenInterestDelta{ - PerpetualId: 1, - BaseQuantumsDelta: big.NewInt(1_000_000_000), + PerpetualId: 1, + BaseQuantums: big.NewInt(1_000_000_000), } DefaultTestPerpOIs = []perptypes.OpenInterestDelta{ BtcUsd_OpenInterest1_AtomicRes8, @@ -277,6 +277,19 @@ var ( FundingIndex: dtypes.ZeroInt(), OpenInterest: dtypes.NewInt(100_000_000), } + BtcUsd_20PercentInitial_10PercentMaintenance_OpenInterest2 = 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(3), + MarketType: perptypes.PerpetualMarketType_PERPETUAL_MARKET_TYPE_CROSS, + }, + FundingIndex: dtypes.ZeroInt(), + OpenInterest: dtypes.NewInt(200_000_000), + } BtcUsd_20PercentInitial_10PercentMaintenance_25mmLowerCap_50mmUpperCap = perptypes.Perpetual{ Params: perptypes.PerpetualParams{ Id: 0, @@ -379,6 +392,7 @@ var ( MarketType: perptypes.PerpetualMarketType_PERPETUAL_MARKET_TYPE_ISOLATED, }, FundingIndex: dtypes.ZeroInt(), + OpenInterest: dtypes.ZeroInt(), } Iso2Usd_IsolatedMarket = perptypes.Perpetual{ Params: perptypes.PerpetualParams{ @@ -391,6 +405,7 @@ var ( MarketType: perptypes.PerpetualMarketType_PERPETUAL_MARKET_TYPE_ISOLATED, }, FundingIndex: dtypes.ZeroInt(), + OpenInterest: dtypes.ZeroInt(), } ) diff --git a/protocol/testutil/perpetuals/perpetuals.go b/protocol/testutil/perpetuals/perpetuals.go index bde3204aff..deb06ae378 100644 --- a/protocol/testutil/perpetuals/perpetuals.go +++ b/protocol/testutil/perpetuals/perpetuals.go @@ -137,7 +137,7 @@ func SetUpDefaultPerpOIsForTest( k.ModifyOpenInterest( ctx, perp.Params.Id, - perpOI.BaseQuantumsDelta, + perpOI.BaseQuantums, ), ) } diff --git a/protocol/x/clob/keeper/deleveraging_test.go b/protocol/x/clob/keeper/deleveraging_test.go index 1b28708043..3f54a3cd19 100644 --- a/protocol/x/clob/keeper/deleveraging_test.go +++ b/protocol/x/clob/keeper/deleveraging_test.go @@ -467,6 +467,9 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { expectedSubaccounts []satypes.Subaccount expectedFills []types.MatchPerpetualDeleveraging_Fill expectedQuantumsRemaining *big.Int + // Expected remaining OI after test. + // The test initializes each perp with default open interest of 1 full coin. + expectedOpenInterest *big.Int }{ "Can get one offsetting subaccount for deleveraged short": { subaccounts: []satypes.Subaccount{ @@ -496,6 +499,7 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { }, }, expectedQuantumsRemaining: new(big.Int), + expectedOpenInterest: new(big.Int), // fully deleveraged }, "Can get one offsetting subaccount for deleveraged long": { subaccounts: []satypes.Subaccount{ @@ -523,6 +527,7 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { }, }, expectedQuantumsRemaining: new(big.Int), + expectedOpenInterest: new(big.Int), // fully deleveraged }, "Can get multiple offsetting subaccounts": { subaccounts: []satypes.Subaccount{ @@ -587,6 +592,7 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { }, }, expectedQuantumsRemaining: new(big.Int), + expectedOpenInterest: new(big.Int), // fully deleveraged }, "Skips subaccounts with positions on the same side": { subaccounts: []satypes.Subaccount{ @@ -618,6 +624,7 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { }, }, expectedQuantumsRemaining: new(big.Int), + expectedOpenInterest: new(big.Int), // fully deleveraged }, "Skips subaccounts with no open position for the given perpetual": { subaccounts: []satypes.Subaccount{ @@ -649,6 +656,7 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { }, }, expectedQuantumsRemaining: new(big.Int), + expectedOpenInterest: new(big.Int), // fully deleveraged }, "Skips subaccounts with non-overlapping bankruptcy prices": { subaccounts: []satypes.Subaccount{ @@ -680,6 +688,7 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { }, }, expectedQuantumsRemaining: new(big.Int), + expectedOpenInterest: new(big.Int), // fully deleveraged }, "Returns an error if not enough subaccounts to fully deleverage liquidated subaccount's position": { subaccounts: []satypes.Subaccount{ @@ -692,6 +701,7 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { expectedSubaccounts: nil, expectedFills: []types.MatchPerpetualDeleveraging_Fill{}, expectedQuantumsRemaining: big.NewInt(100_000_000), + expectedOpenInterest: big.NewInt(100_000_000), }, "Can offset subaccount with multiple positions, first position is offset leaving TNC constant": { subaccounts: []satypes.Subaccount{ @@ -731,6 +741,7 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { }, }, expectedQuantumsRemaining: big.NewInt(0), + expectedOpenInterest: new(big.Int), // fully deleveraged }, } @@ -865,6 +876,17 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) { for _, subaccount := range tc.expectedSubaccounts { require.Equal(t, subaccount, ks.SubaccountsKeeper.GetSubaccount(ks.Ctx, *subaccount.Id)) } + + if tc.expectedOpenInterest != nil { + gotPerp, err := ks.PerpetualsKeeper.GetPerpetual(ks.Ctx, tc.perpetualId) + require.NoError(t, err) + require.Zero(t, + tc.expectedOpenInterest.Cmp(gotPerp.OpenInterest.BigInt()), + "expected open interest %s, got %s", + tc.expectedOpenInterest.String(), + gotPerp.OpenInterest.String(), + ) + } }) } } diff --git a/protocol/x/clob/keeper/liquidations_test.go b/protocol/x/clob/keeper/liquidations_test.go index 8d89d59975..c0cc7b00bf 100644 --- a/protocol/x/clob/keeper/liquidations_test.go +++ b/protocol/x/clob/keeper/liquidations_test.go @@ -52,6 +52,9 @@ func TestPlacePerpetualLiquidation(t *testing.T) { // Expectations. expectedPlacedOrders []*types.MsgPlaceOrder expectedMatchedOrders []*types.ClobMatch + // Expected remaining OI after test. + // The test initializes each perp with default open interest of 1 full coin. + expectedOpenInterests map[uint32]*big.Int }{ `Can place a liquidation that doesn't match any maker orders`: { perpetuals: []perptypes.Perpetual{ @@ -67,6 +70,9 @@ func TestPlacePerpetualLiquidation(t *testing.T) { expectedPlacedOrders: []*types.MsgPlaceOrder{}, expectedMatchedOrders: []*types.ClobMatch{}, + expectedOpenInterests: map[uint32]*big.Int{ + constants.BtcUsd_SmallMarginRequirement.Params.Id: big.NewInt(100_000_000), // unchanged + }, }, `Can place a liquidation that matches maker orders`: { perpetuals: []perptypes.Perpetual{ @@ -107,6 +113,9 @@ func TestPlacePerpetualLiquidation(t *testing.T) { }, ), }, + expectedOpenInterests: map[uint32]*big.Int{ + constants.BtcUsd_SmallMarginRequirement.Params.Id: new(big.Int), // fully liquidated + }, }, `Can place a liquidation that matches maker orders and removes undercollateralized ones`: { perpetuals: []perptypes.Perpetual{ @@ -149,6 +158,9 @@ func TestPlacePerpetualLiquidation(t *testing.T) { }, ), }, + expectedOpenInterests: map[uint32]*big.Int{ + constants.BtcUsd_SmallMarginRequirement.Params.Id: new(big.Int), // fully liquidated + }, }, `Can place a liquidation that matches maker orders with maker rebates and empty fee collector`: { perpetuals: []perptypes.Perpetual{ @@ -189,6 +201,9 @@ func TestPlacePerpetualLiquidation(t *testing.T) { }, ), }, + expectedOpenInterests: map[uint32]*big.Int{ + constants.BtcUsd_SmallMarginRequirement.Params.Id: new(big.Int), // fully liquidated + }, }, `Can place a liquidation that matches maker orders with maker rebates`: { perpetuals: []perptypes.Perpetual{ @@ -228,6 +243,9 @@ func TestPlacePerpetualLiquidation(t *testing.T) { }, ), }, + expectedOpenInterests: map[uint32]*big.Int{ + constants.BtcUsd_SmallMarginRequirement.Params.Id: new(big.Int), // fully liquidated + }, }, } for name, tc := range tests { @@ -342,6 +360,19 @@ func TestPlacePerpetualLiquidation(t *testing.T) { _, _, err = ks.ClobKeeper.PlacePerpetualLiquidation(ctx, tc.order) require.NoError(t, err) + for _, perp := range tc.perpetuals { + if expectedOI, exists := tc.expectedOpenInterests[perp.Params.Id]; exists { + gotPerp, err := ks.PerpetualsKeeper.GetPerpetual(ks.Ctx, perp.Params.Id) + require.NoError(t, err) + require.Zero(t, + expectedOI.Cmp(gotPerp.OpenInterest.BigInt()), + "expected open interest %s, got %s", + expectedOI.String(), + gotPerp.OpenInterest.String(), + ) + } + } + // Verify test expectations. // TODO(DEC-1979): Refactor these tests to support the operations queue refactor. // placedOrders, matchedOrders := memClob.GetPendingFills(ctx) diff --git a/protocol/x/clob/keeper/orders_test.go b/protocol/x/clob/keeper/orders_test.go index 0912d2be24..521e0b8f85 100644 --- a/protocol/x/clob/keeper/orders_test.go +++ b/protocol/x/clob/keeper/orders_test.go @@ -57,7 +57,10 @@ func TestPlaceShortTermOrder(t *testing.T) { expectedMultiStoreWrites []string expectedOrderStatus types.OrderStatus expectedFilledSize satypes.BaseQuantums - expectedErr error + // Expected remaining OI after test. + // The test initializes each perp with default open interest of 1 full coin. + expectedOpenInterests map[uint32]*big.Int + expectedErr error }{ "Can place an order on the orderbook closing a position": { perpetuals: []perptypes.Perpetual{ @@ -73,6 +76,10 @@ func TestPlaceShortTermOrder(t *testing.T) { expectedOrderStatus: types.Success, expectedFilledSize: 0, + expectedOpenInterests: map[uint32]*big.Int{ + // unchanged, no match happened + constants.BtcUsd_SmallMarginRequirement.Params.Id: big.NewInt(100_000_000), + }, }, "Can place an order on the orderbook in a different market than their current perpetual position": { perpetuals: []perptypes.Perpetual{ @@ -92,6 +99,10 @@ func TestPlaceShortTermOrder(t *testing.T) { expectedOrderStatus: types.Success, expectedFilledSize: 0, + expectedOpenInterests: map[uint32]*big.Int{ + // unchanged, no match happened + constants.BtcUsd_SmallMarginRequirement.Params.Id: big.NewInt(100_000_000), + }, }, "Can place an order and the order is fully matched": { perpetuals: []perptypes.Perpetual{ @@ -142,6 +153,10 @@ func TestPlaceShortTermOrder(t *testing.T) { // Update maker order fill amount in memStore types.OrderAmountFilledKeyPrefix, }, + expectedOpenInterests: map[uint32]*big.Int{ + // positions fully closed + constants.BtcUsd_SmallMarginRequirement.Params.Id: big.NewInt(0), + }, }, "Cannot place an order on the orderbook if the account would be undercollateralized": { perpetuals: []perptypes.Perpetual{ @@ -161,6 +176,10 @@ func TestPlaceShortTermOrder(t *testing.T) { expectedOrderStatus: types.Undercollateralized, expectedFilledSize: 0, + expectedOpenInterests: map[uint32]*big.Int{ + // unchanged, no match happened + constants.BtcUsd_SmallMarginRequirement.Params.Id: big.NewInt(100_000_000), + }, }, "Can place an order on the orderbook if the subaccount is right at the initial margin ratio": { perpetuals: []perptypes.Perpetual{ @@ -178,6 +197,10 @@ func TestPlaceShortTermOrder(t *testing.T) { expectedOrderStatus: types.Success, expectedFilledSize: 0, + expectedOpenInterests: map[uint32]*big.Int{ + // unchanged, no match happened + constants.BtcUsd_SmallMarginRequirement.Params.Id: big.NewInt(100_000_000), + }, }, "Cannot place an order on the orderbook if the account would be undercollateralized due to fees paid": { perpetuals: []perptypes.Perpetual{ @@ -196,6 +219,10 @@ func TestPlaceShortTermOrder(t *testing.T) { expectedOrderStatus: types.Undercollateralized, expectedFilledSize: 0, + expectedOpenInterests: map[uint32]*big.Int{ + // unchanged, no match happened + constants.BtcUsd_SmallMarginRequirement.Params.Id: big.NewInt(100_000_000), + }, }, "Can place an order on the orderbook if the account would be collateralized due to rebate": { perpetuals: []perptypes.Perpetual{ @@ -215,6 +242,10 @@ func TestPlaceShortTermOrder(t *testing.T) { expectedOrderStatus: types.Success, expectedFilledSize: 0, + expectedOpenInterests: map[uint32]*big.Int{ + // unchanged, no match happened + constants.BtcUsd_SmallMarginRequirement.Params.Id: big.NewInt(100_000_000), + }, }, "Cannot open an order if it doesn't reference a valid CLOB": { perpetuals: []perptypes.Perpetual{ @@ -232,6 +263,10 @@ func TestPlaceShortTermOrder(t *testing.T) { order: constants.Order_Carl_Num0_Id3_Clob1_Buy1ETH_Price3000, expectedErr: types.ErrInvalidClob, + expectedOpenInterests: map[uint32]*big.Int{ + // unchanged, no match happened + constants.BtcUsd_SmallMarginRequirement.Params.Id: big.NewInt(100_000_000), + }, }, "Cannot open an order if the subticks are invalid": { perpetuals: []perptypes.Perpetual{ @@ -254,6 +289,10 @@ func TestPlaceShortTermOrder(t *testing.T) { }, expectedErr: types.ErrInvalidPlaceOrder, + expectedOpenInterests: map[uint32]*big.Int{ + // unchanged, no match happened + constants.BtcUsd_SmallMarginRequirement.Params.Id: big.NewInt(100_000_000), + }, }, "Cannot open an order that is smaller than the minimum base quantums": { perpetuals: []perptypes.Perpetual{ @@ -276,6 +315,10 @@ func TestPlaceShortTermOrder(t *testing.T) { }, expectedErr: types.ErrInvalidPlaceOrder, + expectedOpenInterests: map[uint32]*big.Int{ + // unchanged, no match happened + constants.BtcUsd_SmallMarginRequirement.Params.Id: big.NewInt(100_000_000), + }, }, "Cannot open an order that is not divisible by the step size base quantums": { perpetuals: []perptypes.Perpetual{ @@ -296,6 +339,10 @@ func TestPlaceShortTermOrder(t *testing.T) { }, expectedErr: types.ErrInvalidPlaceOrder, + expectedOpenInterests: map[uint32]*big.Int{ + // unchanged, no match happened + constants.BtcUsd_SmallMarginRequirement.Params.Id: big.NewInt(100_000_000), + }, }, "Cannot open an order with a GoodTilBlock in the past": { perpetuals: []perptypes.Perpetual{ @@ -317,6 +364,10 @@ func TestPlaceShortTermOrder(t *testing.T) { }, expectedErr: types.ErrHeightExceedsGoodTilBlock, + expectedOpenInterests: map[uint32]*big.Int{ + // unchanged, no match happened + constants.BtcUsd_SmallMarginRequirement.Params.Id: big.NewInt(100_000_000), + }, }, "Cannot open an order with a GoodTilBlock greater than ShortBlockWindow blocks in the future": { perpetuals: []perptypes.Perpetual{ @@ -338,6 +389,10 @@ func TestPlaceShortTermOrder(t *testing.T) { }, expectedErr: types.ErrGoodTilBlockExceedsShortBlockWindow, + expectedOpenInterests: map[uint32]*big.Int{ + // unchanged, no match happened + constants.BtcUsd_SmallMarginRequirement.Params.Id: big.NewInt(100_000_000), + }, }, // This is a regression test for an issue whereby orders that had been previously matched were being checked for // collateralization as if the subticks of the order were `0`. This resulted in always using `0` @@ -401,6 +456,10 @@ func TestPlaceShortTermOrder(t *testing.T) { // and a perpetual of size 0.01 BTC ($500), and the perpetual has a 100% margin requirement. order: constants.Order_Carl_Num1_Id1_Clob0_Buy1kQtBTC_Price50000, expectedOrderStatus: types.Undercollateralized, + expectedOpenInterests: map[uint32]*big.Int{ + // 1 BTC + 0.01 BTC filled + constants.BtcUsd_100PercentMarginRequirement.Params.Id: big.NewInt(101_000_000), + }, }, `New order should be undercollateralized when matching when previous fills make it undercollateralized when using maker orders subticks, but would be collateralized if using taker order subticks`: { @@ -458,6 +517,10 @@ func TestPlaceShortTermOrder(t *testing.T) { // Update maker order fill amount in memStore types.OrderAmountFilledKeyPrefix, }, + expectedOpenInterests: map[uint32]*big.Int{ + // 1 BTC + 0.01 BTC + 0.01 BTC filled + constants.BtcUsd_50PercentInitial_40PercentMaintenance.Params.Id: big.NewInt(102_000_000), + }, }, // This is a regression test for an issue whereby orders that had been previously matched were being checked for // collateralization as if the CLOB pair ID of the order was `0`. This resulted in always using `0` @@ -508,6 +571,12 @@ func TestPlaceShortTermOrder(t *testing.T) { // quantums required to open the previous buy order and fail. order: constants.Order_Carl_Num0_Id4_Clob1_Buy01ETH_Price3000, expectedOrderStatus: types.Success, + expectedOpenInterests: map[uint32]*big.Int{ + // Unchanged, no BTC match happened + constants.BtcUsd_NoMarginRequirement.Params.Id: big.NewInt(100_000_000), + // 1 ETH + 1 ETH filled + constants.EthUsd_20PercentInitial_10PercentMaintenance.Params.Id: big.NewInt(2_000_000_000), + }, }, `Subaccount cannot place maker buy order for 1 BTC at 5 subticks with 0 collateral`: { perpetuals: []perptypes.Perpetual{ @@ -753,6 +822,19 @@ func TestPlaceShortTermOrder(t *testing.T) { } traceDecoder.RequireKeyPrefixesWritten(t, tc.expectedMultiStoreWrites) + + for _, perp := range tc.perpetuals { + if expectedOI, exists := tc.expectedOpenInterests[perp.Params.Id]; exists { + gotPerp, err := ks.PerpetualsKeeper.GetPerpetual(ks.Ctx, perp.Params.Id) + require.NoError(t, err) + require.Zero(t, + expectedOI.Cmp(gotPerp.OpenInterest.BigInt()), + "expected open interest %s, got %s", + expectedOI.String(), + gotPerp.OpenInterest.String(), + ) + } + } }) } } diff --git a/protocol/x/perpetuals/keeper/perpetual.go b/protocol/x/perpetuals/keeper/perpetual.go index 651111ea5c..502bb74523 100644 --- a/protocol/x/perpetuals/keeper/perpetual.go +++ b/protocol/x/perpetuals/keeper/perpetual.go @@ -1247,6 +1247,11 @@ func (k Keeper) ModifyOpenInterest( ) ( err error, ) { + // No-op if delta is zero. + if openInterestDeltaBaseQuantums.Sign() == 0 { + return nil + } + // Get perpetual. perpetual, err := k.GetPerpetual(ctx, perpetualId) if err != nil { @@ -1271,6 +1276,8 @@ func (k Keeper) ModifyOpenInterest( perpetual.OpenInterest = dtypes.NewIntFromBigInt(bigOpenInterest) k.SetPerpetual(ctx, perpetual) + + // TODO(OTE-247): add indexer update logic for open interest change. return nil } diff --git a/protocol/x/perpetuals/keeper/perpetual_test.go b/protocol/x/perpetuals/keeper/perpetual_test.go index fcc005db5e..a88ee0039b 100644 --- a/protocol/x/perpetuals/keeper/perpetual_test.go +++ b/protocol/x/perpetuals/keeper/perpetual_test.go @@ -568,7 +568,7 @@ func TestModifyOpenInterest_Failure(t *testing.T) { "Non-existent perp Id": { id: 1111, initOpenInterest: big.NewInt(1_000), - openInterestDelta: big.NewInt(0), + openInterestDelta: big.NewInt(100), err: types.ErrPerpetualDoesNotExist, }, } diff --git a/protocol/x/perpetuals/types/types.go b/protocol/x/perpetuals/types/types.go index 7fc9ebdb9b..86c96c31fa 100644 --- a/protocol/x/perpetuals/types/types.go +++ b/protocol/x/perpetuals/types/types.go @@ -121,9 +121,10 @@ type PerpetualsKeeper interface { GetAllLiquidityTiers(ctx sdk.Context) (list []LiquidityTier) } +// OpenInterestDelta represents a (perpId, openInterestDelta) tuple. type OpenInterestDelta struct { // The `Id` of the `Perpetual`. PerpetualId uint32 // Delta of open interest (in base quantums). - BaseQuantumsDelta *big.Int + BaseQuantums *big.Int } diff --git a/protocol/x/subaccounts/keeper/oimf.go b/protocol/x/subaccounts/keeper/oimf.go index 2f08cece71..691dd941a0 100644 --- a/protocol/x/subaccounts/keeper/oimf.go +++ b/protocol/x/subaccounts/keeper/oimf.go @@ -45,7 +45,7 @@ func getDeltaLongFromSettledUpdate( } // For `Match` updates: -// - returns a struct `OpenInterestDelta` if input updates results in OI delta. +// - returns a struct `OpenInterest` if input updates results in OI delta. // - returns nil if OI delta is zero. // - panics if update format is invalid. // @@ -114,7 +114,7 @@ func GetDeltaOpenInterestFromUpdates( } return &perptypes.OpenInterestDelta{ - PerpetualId: updatedPerpId, - BaseQuantumsDelta: baseQuantumsDelta, + PerpetualId: updatedPerpId, + BaseQuantums: baseQuantumsDelta, } } diff --git a/protocol/x/subaccounts/keeper/oimf_test.go b/protocol/x/subaccounts/keeper/oimf_test.go index e91e986d73..46cb310769 100644 --- a/protocol/x/subaccounts/keeper/oimf_test.go +++ b/protocol/x/subaccounts/keeper/oimf_test.go @@ -176,8 +176,8 @@ func TestGetDeltaOpenInterestFromUpdates(t *testing.T) { }, }, expectedVal: &perptypes.OpenInterestDelta{ - PerpetualId: 1, - BaseQuantumsDelta: big.NewInt(500), + PerpetualId: 1, + BaseQuantums: big.NewInt(500), }, }, "Valid: 500 -> 0, 0 -> 500, delta = 0": { @@ -307,8 +307,8 @@ func TestGetDeltaOpenInterestFromUpdates(t *testing.T) { }, }, expectedVal: &perptypes.OpenInterestDelta{ - PerpetualId: 1000, - BaseQuantumsDelta: big.NewInt(-50), + PerpetualId: 1000, + BaseQuantums: big.NewInt(-50), }, }, "Valid: -3100 -> -5000, 1000 -> 2900, delta = 1900": { @@ -350,8 +350,8 @@ func TestGetDeltaOpenInterestFromUpdates(t *testing.T) { }, }, expectedVal: &perptypes.OpenInterestDelta{ - PerpetualId: 1000, - BaseQuantumsDelta: big.NewInt(1900), + PerpetualId: 1000, + BaseQuantums: big.NewInt(1900), }, }, } diff --git a/protocol/x/subaccounts/keeper/subaccount.go b/protocol/x/subaccounts/keeper/subaccount.go index defce49e3f..3a213710b4 100644 --- a/protocol/x/subaccounts/keeper/subaccount.go +++ b/protocol/x/subaccounts/keeper/subaccount.go @@ -308,6 +308,25 @@ func (k Keeper) UpdateSubaccounts( perpIdToFundingIndex[perp.Params.Id] = perp.FundingIndex } + // Get OpenInterestDelta from the updates, and persist the OI change if any. + perpOpenInterestDelta := GetDeltaOpenInterestFromUpdates(settledUpdates, updateType) + if perpOpenInterestDelta != nil { + if err := k.perpetualsKeeper.ModifyOpenInterest( + ctx, + perpOpenInterestDelta.PerpetualId, + perpOpenInterestDelta.BaseQuantums, + ); err != nil { + return false, nil, errorsmod.Wrapf( + types.ErrCannotModifyPerpOpenInterestForOIMF, + "perpId = %v, delta = %v, settledUpdates = %+v, err = %v", + perpOpenInterestDelta.PerpetualId, + perpOpenInterestDelta.BaseQuantums, + settledUpdates, + err, + ) + } + } + // Apply the updates to perpetual positions. UpdatePerpetualPositions( settledUpdates, @@ -679,13 +698,13 @@ func (k Keeper) internalCanUpdateSubaccounts( if err := k.perpetualsKeeper.ModifyOpenInterest( branchedContext, perpOpenInterestDelta.PerpetualId, - perpOpenInterestDelta.BaseQuantumsDelta, + perpOpenInterestDelta.BaseQuantums, ); err != nil { return false, nil, errorsmod.Wrapf( types.ErrCannotModifyPerpOpenInterestForOIMF, "perpId = %v, delta = %v, settledUpdates = %+v, err = %v", perpOpenInterestDelta.PerpetualId, - perpOpenInterestDelta.BaseQuantumsDelta, + perpOpenInterestDelta.BaseQuantums, settledUpdates, err, ) diff --git a/protocol/x/subaccounts/keeper/subaccount_test.go b/protocol/x/subaccounts/keeper/subaccount_test.go index 1999d6b6f0..90501a68c7 100644 --- a/protocol/x/subaccounts/keeper/subaccount_test.go +++ b/protocol/x/subaccounts/keeper/subaccount_test.go @@ -95,13 +95,22 @@ func assertSubaccountUpdateEventsInIndexerBlock( // For each update, verify that the expected SubaccountUpdateEvent is emitted. for _, update := range updates { - expecetedSubaccountUpdateEvent := indexerevents.NewSubaccountUpdateEvent( + expectedSubaccountUpdateEvent := indexerevents.NewSubaccountUpdateEvent( &update.SubaccountId, expectedUpdatedPerpetualPositions[update.SubaccountId], expectedUpdatedAssetPositions[update.SubaccountId], expectedSubaccoundIdToFundingPayments[update.SubaccountId], ) - require.Contains(t, subaccountUpdates, expecetedSubaccountUpdateEvent) + + for _, gotUpdate := range subaccountUpdates { + if gotUpdate.SubaccountId.Owner == expectedSubaccountUpdateEvent.SubaccountId.Owner && + gotUpdate.SubaccountId.Number == expectedSubaccountUpdateEvent.SubaccountId.Number { + require.Equal(t, + expectedSubaccountUpdateEvent, + gotUpdate, + ) + } + } } } @@ -297,6 +306,9 @@ func TestUpdateSubaccounts(t *testing.T) { newFundingIndices []*big.Int // 1:1 mapped to perpetuals list assets []*asstypes.Asset marketParamPrices []pricestypes.MarketParamPrice + // If not specified, default to `CollatCheck` + updateType types.UpdateType + additionalTestSubaccounts []types.Subaccount // subaccount state perpetualPositions []*types.PerpetualPosition @@ -316,6 +328,9 @@ func TestUpdateSubaccounts(t *testing.T) { expectedSuccess bool expectedSuccessPerUpdate []types.UpdateResult expectedErr error + // List of expected open interest. + // If not specified, this means OI is default value. + expectedOpenInterest map[uint32]*big.Int // Only contains the updated perpetual positions, to assert against the events included. expectedUpdatedPerpetualPositions map[types.SubaccountId][]*types.PerpetualPosition @@ -2461,6 +2476,313 @@ func TestUpdateSubaccounts(t *testing.T) { expectedErr: sdkerrors.ErrInsufficientFunds, msgSenderEnabled: true, }, + "Match updates increase OI: 0 -> 0.9, 0 -> -0.9": { + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance_OpenInterest1, + }, + 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: constants.Bob_Num0, + }, + { + 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 + }, + }, + }, + }, + assetPositions: testutil.CreateUsdcAssetPosition(big.NewInt(900_000_000_000)), // 900_000 USDC + additionalTestSubaccounts: []types.Subaccount{ + { + Id: &constants.Bob_Num0, + AssetPositions: testutil.CreateUsdcAssetPosition(big.NewInt( + 900_000_000_000, + )), // 900_000 USDC + }, + }, + updateType: types.Match, + expectedAssetPositions: []*types.AssetPosition{ + { + AssetId: uint32(0), + Quantums: dtypes.NewInt(5_400_000_000_000), + }, + }, + expectedUpdatedAssetPositions: map[types.SubaccountId][]*types.AssetPosition{ + defaultSubaccountId: { + { + AssetId: uint32(0), + Quantums: dtypes.NewInt(5_400_000_000_000), + }, + }, + constants.Bob_Num0: { + { + AssetId: uint32(0), + Quantums: dtypes.NewInt(-3_600_000_000_000), + }, + }, + }, + expectedPerpetualPositions: []*types.PerpetualPosition{ + { + PerpetualId: uint32(0), + Quantums: dtypes.NewInt(-9_000_000_000), + FundingIndex: dtypes.NewInt(0), + }, + }, + expectedUpdatedPerpetualPositions: map[types.SubaccountId][]*types.PerpetualPosition{ + defaultSubaccountId: { + { + PerpetualId: uint32(0), + Quantums: dtypes.NewInt(-9_000_000_000), + FundingIndex: dtypes.NewInt(0), + }, + }, + constants.Bob_Num0: { + { + PerpetualId: uint32(0), + Quantums: dtypes.NewInt(9_000_000_000), + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + expectedSuccess: true, + expectedSuccessPerUpdate: []types.UpdateResult{types.Success, types.Success}, + expectedOpenInterest: map[uint32]*big.Int{ + 0: big.NewInt(9_100_000_000), // 1 + 90 = 91 BTC + }, + msgSenderEnabled: true, + }, + "Match updates decreases OI: 1 -> 0.1, -2 -> -1.1": { + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance_OpenInterest2, + }, + perpetualPositions: []*types.PerpetualPosition{ + { + PerpetualId: uint32(0), + Quantums: dtypes.NewInt(100_000_000), // 1 BTC + }, + }, + assetPositions: testutil.CreateUsdcAssetPosition(big.NewInt(-40_000_000_000)), // -40_000 USDC + updates: []types.Update{ + { + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: uint32(0), + BigQuantumsDelta: big.NewInt(90_000_000), // 0.9 BTC + }, + }, + AssetUpdates: []types.AssetUpdate{ + { + AssetId: uint32(0), + BigQuantumsDelta: big.NewInt(-45_000_000_000), // -45,000 USDC + }, + }, + SubaccountId: constants.Bob_Num0, + }, + { + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: uint32(0), + BigQuantumsDelta: big.NewInt(-90_000_000), // -0.9 BTC + }, + }, + AssetUpdates: []types.AssetUpdate{ + { + AssetId: uint32(0), + BigQuantumsDelta: big.NewInt(45_000_000_000), // 45,000 USDC + }, + }, + }, + }, + additionalTestSubaccounts: []types.Subaccount{ + { + Id: &constants.Bob_Num0, + AssetPositions: testutil.CreateUsdcAssetPosition(big.NewInt( + 120_000_000_000, + )), // 120_000 USDC + PerpetualPositions: []*types.PerpetualPosition{ + { + PerpetualId: uint32(0), + Quantums: dtypes.NewInt(-200_000_000), // -2 BTC + }, + }, + }, + }, + updateType: types.Match, + expectedAssetPositions: []*types.AssetPosition{ + { + AssetId: uint32(0), + Quantums: dtypes.NewInt(5_000_000_000), // 5_000 USDC + }, + }, + expectedUpdatedAssetPositions: map[types.SubaccountId][]*types.AssetPosition{ + defaultSubaccountId: { + { + AssetId: uint32(0), + Quantums: dtypes.NewInt(5_000_000_000), // 5_000 USDC + }, + }, + constants.Bob_Num0: { + { + AssetId: uint32(0), + Quantums: dtypes.NewInt(75_000_000_000), // 75_000 USDC + }, + }, + }, + expectedPerpetualPositions: []*types.PerpetualPosition{ + { + PerpetualId: uint32(0), + Quantums: dtypes.NewInt(10_000_000), // 0.1 BTC + FundingIndex: dtypes.NewInt(0), + }, + }, + expectedUpdatedPerpetualPositions: map[types.SubaccountId][]*types.PerpetualPosition{ + defaultSubaccountId: { + { + PerpetualId: uint32(0), + Quantums: dtypes.NewInt(10_000_000), // 0.1 BTC + FundingIndex: dtypes.NewInt(0), + }, + }, + constants.Bob_Num0: { + { + PerpetualId: uint32(0), + Quantums: dtypes.NewInt(-110_000_000), // -1.1 BTC + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + expectedSuccess: true, + expectedSuccessPerUpdate: []types.UpdateResult{types.Success, types.Success}, + expectedOpenInterest: map[uint32]*big.Int{ + 0: big.NewInt(110_000_000), // 2 - 0.9 = 1.1 BTC + }, + msgSenderEnabled: true, + }, + "Match updates does not change OI: 1 -> 0.1, 0.1 -> 1": { + perpetuals: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance_OpenInterest1, + }, + perpetualPositions: []*types.PerpetualPosition{ + { + PerpetualId: uint32(0), + Quantums: dtypes.NewInt(100_000_000), // 1 BTC + }, + }, + assetPositions: testutil.CreateUsdcAssetPosition(big.NewInt(-40_000_000_000)), // -40_000 USDC + updates: []types.Update{ + { + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: uint32(0), + BigQuantumsDelta: big.NewInt(90_000_000), // 0.9 BTC + }, + }, + AssetUpdates: []types.AssetUpdate{ + { + AssetId: uint32(0), + BigQuantumsDelta: big.NewInt(-45_000_000_000), // -45,000 USDC + }, + }, + SubaccountId: constants.Bob_Num0, + }, + { + PerpetualUpdates: []types.PerpetualUpdate{ + { + PerpetualId: uint32(0), + BigQuantumsDelta: big.NewInt(-90_000_000), // -0.9 BTC + }, + }, + AssetUpdates: []types.AssetUpdate{ + { + AssetId: uint32(0), + BigQuantumsDelta: big.NewInt(45_000_000_000), // 45,000 USDC + }, + }, + }, + }, + additionalTestSubaccounts: []types.Subaccount{ + { + Id: &constants.Bob_Num0, + AssetPositions: testutil.CreateUsdcAssetPosition(big.NewInt(5_000_000_000)), // 5000 USDC + PerpetualPositions: []*types.PerpetualPosition{ + { + PerpetualId: uint32(0), + Quantums: dtypes.NewInt(10_000_000), // 0.1 BTC + }, + }, + }, + }, + updateType: types.Match, + expectedAssetPositions: []*types.AssetPosition{ + { + AssetId: uint32(0), + Quantums: dtypes.NewInt(5_000_000_000), // 5_000 USDC + }, + }, + expectedUpdatedAssetPositions: map[types.SubaccountId][]*types.AssetPosition{ + defaultSubaccountId: { + { + AssetId: uint32(0), + Quantums: dtypes.NewInt(5_000_000_000), // 5_000 USDC + }, + }, + constants.Bob_Num0: { + { + AssetId: uint32(0), + Quantums: dtypes.NewInt(-40_000_000_000), // -40_000 USDC + }, + }, + }, + expectedPerpetualPositions: []*types.PerpetualPosition{ + { + PerpetualId: uint32(0), + Quantums: dtypes.NewInt(10_000_000), // 0.1 BTC + FundingIndex: dtypes.NewInt(0), + }, + }, + expectedUpdatedPerpetualPositions: map[types.SubaccountId][]*types.PerpetualPosition{ + defaultSubaccountId: { + { + PerpetualId: uint32(0), + Quantums: dtypes.NewInt(10_000_000), // 0.1 BTC + FundingIndex: dtypes.NewInt(0), + }, + }, + constants.Bob_Num0: { + { + PerpetualId: uint32(0), + Quantums: dtypes.NewInt(100_000_000), // 1 BTC + FundingIndex: dtypes.NewInt(0), + }, + }, + }, + expectedSuccess: true, + expectedSuccessPerUpdate: []types.UpdateResult{types.Success, types.Success}, + expectedOpenInterest: map[uint32]*big.Int{ + 0: big.NewInt(100_000_000), // 1 BTC + }, + msgSenderEnabled: true, + }, } for name, tc := range tests { @@ -2499,23 +2821,16 @@ func TestUpdateSubaccounts(t *testing.T) { } for i, p := range tc.perpetuals { - perp, err := perpetualsKeeper.CreatePerpetual( + perpetualsKeeper.SetPerpetual( ctx, - p.Params.Id, - p.Params.Ticker, - p.Params.MarketId, - p.Params.AtomicResolution, - p.Params.DefaultFundingPpm, - p.Params.LiquidityTier, - p.Params.MarketType, + p, ) - require.NoError(t, err) // Update FundingIndex for testing settlements. if i < len(tc.newFundingIndices) { - err = perpetualsKeeper.ModifyFundingIndex( + err := perpetualsKeeper.ModifyFundingIndex( ctx, - perp.Params.Id, + p.Params.Id, tc.newFundingIndices[i], ) require.NoError(t, err) @@ -2540,6 +2855,10 @@ func TestUpdateSubaccounts(t *testing.T) { keeper.SetSubaccount(ctx, subaccount) subaccountId := *subaccount.Id + for _, sa := range tc.additionalTestSubaccounts { + keeper.SetSubaccount(ctx, sa) + } + for i, u := range tc.updates { if u.SubaccountId == (types.SubaccountId{}) { u.SubaccountId = subaccountId @@ -2547,7 +2866,11 @@ func TestUpdateSubaccounts(t *testing.T) { tc.updates[i] = u } - success, successPerUpdate, err := keeper.UpdateSubaccounts(ctx, tc.updates, types.CollatCheck) + updateType := types.CollatCheck + if tc.updateType != types.UpdateTypeUnspecified { + updateType = tc.updateType + } + success, successPerUpdate, err := keeper.UpdateSubaccounts(ctx, tc.updates, updateType) if tc.expectedErr != nil { require.ErrorIs(t, tc.expectedErr, err) } else { @@ -2598,6 +2921,20 @@ func TestUpdateSubaccounts(t *testing.T) { usdcBal, ) } + + for _, perp := range tc.perpetuals { + gotPerp, err := perpetualsKeeper.GetPerpetual(ctx, perp.GetId()) + require.NoError(t, err) + + if expectedOI, exists := tc.expectedOpenInterest[perp.GetId()]; exists { + require.Equal(t, expectedOI, gotPerp.OpenInterest.BigInt()) + } else { + // If no specified expected OI, then check OI is unchanged. + require.Zero(t, perp.OpenInterest.BigInt().Cmp( + gotPerp.OpenInterest.BigInt(), + )) + } + } }) } } @@ -4150,7 +4487,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { { PerpetualId: uint32(0), // 500 BTC. At $50,000, this is $25,000,000 of OI. - BaseQuantumsDelta: big.NewInt(50_000_000_000), + BaseQuantums: big.NewInt(50_000_000_000), }, }, updates: []types.Update{ @@ -4220,7 +4557,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { // (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), + BaseQuantums: big.NewInt(41_000_000_000), }, }, updates: []types.Update{ @@ -4290,7 +4627,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { // (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), + BaseQuantums: big.NewInt(41_000_000_001), }, }, updates: []types.Update{ @@ -4357,7 +4694,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { { 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), + BaseQuantums: big.NewInt(1_000_000_000_000), }, }, updates: []types.Update{ @@ -5052,7 +5389,7 @@ func TestCanUpdateSubaccounts(t *testing.T) { require.NoError(t, perpetualsKeeper.ModifyOpenInterest( ctx, openInterest.PerpetualId, - openInterest.BaseQuantumsDelta, + openInterest.BaseQuantums, )) } @@ -5095,9 +5432,9 @@ func TestCanUpdateSubaccounts(t *testing.T) { perp, err := perpetualsKeeper.GetPerpetual(ctx, openInterest.PerpetualId) require.NoError(t, err) require.Zerof(t, - openInterest.BaseQuantumsDelta.Cmp(perp.OpenInterest.BigInt()), + openInterest.BaseQuantums.Cmp(perp.OpenInterest.BigInt()), "expected: %s, got: %s", - openInterest.BaseQuantumsDelta.String(), + openInterest.BaseQuantums.String(), perp.OpenInterest.String(), ) } diff --git a/protocol/x/subaccounts/types/errors.go b/protocol/x/subaccounts/types/errors.go index d27fb280fb..2113575cfe 100644 --- a/protocol/x/subaccounts/types/errors.go +++ b/protocol/x/subaccounts/types/errors.go @@ -58,7 +58,7 @@ var ( ErrCannotModifyPerpOpenInterestForOIMF = errorsmod.Register( ModuleName, 402, - "cannot temporarily modify perpetual open interest for OIMF calculation", + "cannot modify perpetual open interest for OIMF calculation", ) ErrCannotRevertPerpOpenInterestForOIMF = errorsmod.Register( ModuleName,