From cafe79f59f4b7508980684d43dfc8bba743c5ffa Mon Sep 17 00:00:00 2001 From: Brendan Chou <3680392+BrendanChou@users.noreply.github.com> Date: Fri, 14 Jun 2024 11:55:09 -0400 Subject: [PATCH] [Performance] Optimize state-reads of perpetual information when doing collateral checks (#1691) --- .../liquidation/client/sub_task_runner.go | 104 +++++---- protocol/x/perpetuals/types/perpinfo.go | 12 ++ .../subaccounts/keeper/isolated_subaccount.go | 53 ++--- .../keeper/isolated_subaccount_test.go | 9 +- protocol/x/subaccounts/keeper/subaccount.go | 197 ++++++++++-------- .../x/subaccounts/keeper/subaccount_helper.go | 16 +- protocol/x/subaccounts/types/errors.go | 5 + 7 files changed, 215 insertions(+), 181 deletions(-) create mode 100644 protocol/x/perpetuals/types/perpinfo.go diff --git a/protocol/daemons/liquidation/client/sub_task_runner.go b/protocol/daemons/liquidation/client/sub_task_runner.go index 7918e9db39..3d4130cd9f 100644 --- a/protocol/daemons/liquidation/client/sub_task_runner.go +++ b/protocol/daemons/liquidation/client/sub_task_runner.go @@ -56,9 +56,7 @@ func (s *SubTaskRunnerImpl) RunLiquidationDaemonTaskLoop( // 1. Fetch all information needed to calculate total net collateral and margin requirements. subaccounts, - marketPrices, - perpetuals, - liquidityTiers, + perpInfos, err := daemonClient.FetchApplicationStateAtBlockHeight( ctx, lastCommittedBlockHeight, @@ -73,9 +71,7 @@ func (s *SubTaskRunnerImpl) RunLiquidationDaemonTaskLoop( negativeTncSubaccountIds, err := daemonClient.GetLiquidatableSubaccountIds( subaccounts, - marketPrices, - perpetuals, - liquidityTiers, + perpInfos, ) if err != nil { return err @@ -111,9 +107,7 @@ func (c *Client) FetchApplicationStateAtBlockHeight( liqFlags flags.LiquidationFlags, ) ( subaccounts []satypes.Subaccount, - marketPricesMap map[uint32]pricestypes.MarketPrice, - perpetualsMap map[uint32]perptypes.Perpetual, - liquidityTiersMap map[uint32]perptypes.LiquidityTier, + perpInfos map[uint32]perptypes.PerpInfo, err error, ) { defer telemetry.ModuleMeasureSince( @@ -129,46 +123,66 @@ func (c *Client) FetchApplicationStateAtBlockHeight( // Subaccounts subaccounts, err = c.GetAllSubaccounts(queryCtx, liqFlags.QueryPageLimit) if err != nil { - return nil, nil, nil, nil, err + return nil, nil, err } // Market prices marketPrices, err := c.GetAllMarketPrices(queryCtx, liqFlags.QueryPageLimit) if err != nil { - return nil, nil, nil, nil, err + return nil, nil, err } - marketPricesMap = lib.UniqueSliceToMap(marketPrices, func(m pricestypes.MarketPrice) uint32 { + marketPricesMap := lib.UniqueSliceToMap(marketPrices, func(m pricestypes.MarketPrice) uint32 { return m.Id }) // Perpetuals perpetuals, err := c.GetAllPerpetuals(queryCtx, liqFlags.QueryPageLimit) if err != nil { - return nil, nil, nil, nil, err + return nil, nil, err } - perpetualsMap = lib.UniqueSliceToMap(perpetuals, func(p perptypes.Perpetual) uint32 { - return p.Params.Id - }) // Liquidity tiers liquidityTiers, err := c.GetAllLiquidityTiers(queryCtx, liqFlags.QueryPageLimit) if err != nil { - return nil, nil, nil, nil, err + return nil, nil, err } - liquidityTiersMap = lib.UniqueSliceToMap(liquidityTiers, func(l perptypes.LiquidityTier) uint32 { + liquidityTiersMap := lib.UniqueSliceToMap(liquidityTiers, func(l perptypes.LiquidityTier) uint32 { return l.Id }) - return subaccounts, marketPricesMap, perpetualsMap, liquidityTiersMap, nil + perpInfos = make(map[uint32]perptypes.PerpInfo, len(perpetuals)) + for _, perp := range perpetuals { + price, ok := marketPricesMap[perp.Params.MarketId] + if !ok { + return nil, nil, errorsmod.Wrapf( + pricestypes.ErrMarketPriceDoesNotExist, + "%d", + perp.Params.MarketId, + ) + } + liquidityTier, ok := liquidityTiersMap[perp.Params.LiquidityTier] + if !ok { + return nil, nil, errorsmod.Wrapf( + perptypes.ErrLiquidityTierDoesNotExist, + "%d", + perp.Params.LiquidityTier, + ) + } + perpInfos[perp.Params.Id] = perptypes.PerpInfo{ + Perpetual: perp, + Price: price, + LiquidityTier: liquidityTier, + } + } + + return subaccounts, perpInfos, nil } // GetLiquidatableSubaccountIds verifies collateralization statuses of subaccounts with // at least one open position and returns a list of unique and potentially liquidatable subaccount ids. func (c *Client) GetLiquidatableSubaccountIds( subaccounts []satypes.Subaccount, - marketPrices map[uint32]pricestypes.MarketPrice, - perpetuals map[uint32]perptypes.Perpetual, - liquidityTiers map[uint32]perptypes.LiquidityTier, + perpInfos map[uint32]perptypes.PerpInfo, ) ( liquidatableSubaccountIds []satypes.SubaccountId, negativeTncSubaccountIds []satypes.SubaccountId, @@ -192,9 +206,7 @@ func (c *Client) GetLiquidatableSubaccountIds( // Check if the subaccount is liquidatable. isLiquidatable, hasNegativeTnc, err := c.CheckSubaccountCollateralization( subaccount, - marketPrices, - perpetuals, - liquidityTiers, + perpInfos, ) if err != nil { c.logger.Error("Error checking collateralization status", "error", err) @@ -278,9 +290,7 @@ func (c *Client) GetSubaccountOpenPositionInfo( // is not yet implemented. func (c *Client) CheckSubaccountCollateralization( unsettledSubaccount satypes.Subaccount, - marketPrices map[uint32]pricestypes.MarketPrice, - perpetuals map[uint32]perptypes.Perpetual, - liquidityTiers map[uint32]perptypes.LiquidityTier, + perpInfos map[uint32]perptypes.PerpInfo, ) ( isLiquidatable bool, hasNegativeTnc bool, @@ -297,7 +307,7 @@ func (c *Client) CheckSubaccountCollateralization( // to ensure that the funding payments are included in the net collateral calculation. settledSubaccount, _, err := sakeeper.GetSettledSubaccountWithPerpetuals( unsettledSubaccount, - perpetuals, + perpInfos, ) if err != nil { return false, false, err @@ -323,44 +333,30 @@ func (c *Client) CheckSubaccountCollateralization( // Calculate the net collateral and maintenance margin for each of the perpetual positions. for _, perpetualPosition := range settledSubaccount.PerpetualPositions { - perpetual, ok := perpetuals[perpetualPosition.PerpetualId] + perpInfo, ok := perpInfos[perpetualPosition.PerpetualId] if !ok { return false, false, errorsmod.Wrapf( - perptypes.ErrPerpetualDoesNotExist, - "Perpetual not found for perpetual id %d", + satypes.ErrPerpetualInfoDoesNotExist, + "%d", perpetualPosition.PerpetualId, ) } - marketPrice, ok := marketPrices[perpetual.Params.MarketId] - if !ok { - return false, false, errorsmod.Wrapf( - pricestypes.ErrMarketPriceDoesNotExist, - "MarketPrice not found for perpetual %+v", - perpetual, - ) - } - bigQuantums := perpetualPosition.GetBigQuantums() // Get the net collateral for the position. - bigNetCollateralQuoteQuantums := perplib.GetNetNotionalInQuoteQuantums(perpetual, marketPrice, bigQuantums) + bigNetCollateralQuoteQuantums := perplib.GetNetNotionalInQuoteQuantums( + perpInfo.Perpetual, + perpInfo.Price, + bigQuantums, + ) bigTotalNetCollateral.Add(bigTotalNetCollateral, bigNetCollateralQuoteQuantums) - liquidityTier, ok := liquidityTiers[perpetual.Params.LiquidityTier] - if !ok { - return false, false, errorsmod.Wrapf( - perptypes.ErrLiquidityTierDoesNotExist, - "LiquidityTier not found for perpetual %+v", - perpetual, - ) - } - // Get the maintenance margin requirement for the position. _, bigMaintenanceMarginQuoteQuantums := perplib.GetMarginRequirementsInQuoteQuantums( - perpetual, - marketPrice, - liquidityTier, + perpInfo.Perpetual, + perpInfo.Price, + perpInfo.LiquidityTier, bigQuantums, ) bigTotalMaintenanceMargin.Add(bigTotalMaintenanceMargin, bigMaintenanceMarginQuoteQuantums) diff --git a/protocol/x/perpetuals/types/perpinfo.go b/protocol/x/perpetuals/types/perpinfo.go new file mode 100644 index 0000000000..788143aa82 --- /dev/null +++ b/protocol/x/perpetuals/types/perpinfo.go @@ -0,0 +1,12 @@ +package types + +import ( + pricestypes "github.com/dydxprotocol/v4-chain/protocol/x/prices/types" +) + +// PerpInfo contains all information needed to calculate margin requirements for a perpetual. +type PerpInfo struct { + Perpetual Perpetual + Price pricestypes.MarketPrice + LiquidityTier LiquidityTier +} diff --git a/protocol/x/subaccounts/keeper/isolated_subaccount.go b/protocol/x/subaccounts/keeper/isolated_subaccount.go index 6866bc22cb..9d4f4cebb5 100644 --- a/protocol/x/subaccounts/keeper/isolated_subaccount.go +++ b/protocol/x/subaccounts/keeper/isolated_subaccount.go @@ -6,7 +6,6 @@ import ( errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/dydxprotocol/v4-chain/protocol/lib" assettypes "github.com/dydxprotocol/v4-chain/protocol/x/assets/types" perptypes "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types" "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" @@ -25,7 +24,7 @@ import ( func (k Keeper) checkIsolatedSubaccountConstraints( ctx sdk.Context, settledUpdates []SettledUpdate, - perpetuals []perptypes.Perpetual, + perpInfos map[uint32]perptypes.PerpInfo, ) ( success bool, successPerUpdate []types.UpdateResult, @@ -33,10 +32,9 @@ func (k Keeper) checkIsolatedSubaccountConstraints( ) { success = true successPerUpdate = make([]types.UpdateResult, len(settledUpdates)) - perpIdToMarketType := getPerpIdToMarketTypeMap(perpetuals) for i, u := range settledUpdates { - result, err := isValidIsolatedPerpetualUpdates(u, perpIdToMarketType) + result, err := isValidIsolatedPerpetualUpdates(u, perpInfos) if err != nil { return false, nil, err } @@ -62,7 +60,7 @@ func (k Keeper) checkIsolatedSubaccountConstraints( // perpetuals or a combination of isolated and non-isolated perpetuals func isValidIsolatedPerpetualUpdates( settledUpdate SettledUpdate, - perpIdToMarketType map[uint32]perptypes.PerpetualMarketType, + perpInfos map[uint32]perptypes.PerpInfo, ) (types.UpdateResult, error) { // If there are no perpetual updates, then this update does not violate constraints for isolated // markets. @@ -74,14 +72,13 @@ func isValidIsolatedPerpetualUpdates( hasIsolatedUpdate := false isolatedUpdatePerpetualId := uint32(math.MaxUint32) for _, perpetualUpdate := range settledUpdate.PerpetualUpdates { - marketType, exists := perpIdToMarketType[perpetualUpdate.PerpetualId] + perpInfo, exists := perpInfos[perpetualUpdate.PerpetualId] if !exists { - return types.UpdateCausedError, errorsmod.Wrap( - perptypes.ErrPerpetualDoesNotExist, lib.UintToString(perpetualUpdate.PerpetualId), - ) + return types.UpdateCausedError, + errorsmod.Wrapf(types.ErrPerpetualInfoDoesNotExist, "%d", perpetualUpdate.PerpetualId) } - if marketType == perptypes.PerpetualMarketType_PERPETUAL_MARKET_TYPE_ISOLATED { + if perpInfo.Perpetual.Params.MarketType == perptypes.PerpetualMarketType_PERPETUAL_MARKET_TYPE_ISOLATED { hasIsolatedUpdate = true isolatedUpdatePerpetualId = perpetualUpdate.PerpetualId break @@ -94,14 +91,13 @@ func isValidIsolatedPerpetualUpdates( isolatedPositionPerpetualId := uint32(math.MaxUint32) hasPerpetualPositions := len(settledUpdate.SettledSubaccount.PerpetualPositions) > 0 for _, perpetualPosition := range settledUpdate.SettledSubaccount.PerpetualPositions { - marketType, exists := perpIdToMarketType[perpetualPosition.PerpetualId] + perpInfo, exists := perpInfos[perpetualPosition.PerpetualId] if !exists { - return types.UpdateCausedError, errorsmod.Wrap( - perptypes.ErrPerpetualDoesNotExist, lib.UintToString(perpetualPosition.PerpetualId), - ) + return types.UpdateCausedError, + errorsmod.Wrapf(types.ErrPerpetualInfoDoesNotExist, "%d", perpetualPosition.PerpetualId) } - if marketType == perptypes.PerpetualMarketType_PERPETUAL_MARKET_TYPE_ISOLATED { + if perpInfo.Perpetual.Params.MarketType == perptypes.PerpetualMarketType_PERPETUAL_MARKET_TYPE_ISOLATED { isIsolatedSubaccount = true isolatedPositionPerpetualId = perpetualPosition.PerpetualId break @@ -146,9 +142,8 @@ func isValidIsolatedPerpetualUpdates( // so all the updates must have been applied already to the subaccount. func GetIsolatedPerpetualStateTransition( settledUpdateWithUpdatedSubaccount SettledUpdate, - perpetuals []perptypes.Perpetual, + perpInfos map[uint32]perptypes.PerpInfo, ) (*types.IsolatedPerpetualPositionStateTransition, error) { - perpIdToMarketType := getPerpIdToMarketTypeMap(perpetuals) // This subaccount needs to have had the updates in the `settledUpdate` already applied to it. updatedSubaccount := settledUpdateWithUpdatedSubaccount.SettledSubaccount // If there are no perpetual updates, then no perpetual position could have been opened or closed @@ -168,15 +163,13 @@ func GetIsolatedPerpetualStateTransition( // Now, from the above checks, we know there is only a single perpetual update and 0 or 1 perpetual // positions. perpetualUpdate := settledUpdateWithUpdatedSubaccount.PerpetualUpdates[0] - marketType, exists := perpIdToMarketType[perpetualUpdate.PerpetualId] + perpInfo, exists := perpInfos[perpetualUpdate.PerpetualId] if !exists { - return nil, errorsmod.Wrap( - perptypes.ErrPerpetualDoesNotExist, lib.UintToString(perpetualUpdate.PerpetualId), - ) + return nil, errorsmod.Wrapf(types.ErrPerpetualInfoDoesNotExist, "%d", perpetualUpdate.PerpetualId) } // If the perpetual update is not for an isolated perpetual, no isolated perpetual position is // being opened or closed. - if marketType != perptypes.PerpetualMarketType_PERPETUAL_MARKET_TYPE_ISOLATED { + if perpInfo.Perpetual.Params.MarketType != perptypes.PerpetualMarketType_PERPETUAL_MARKET_TYPE_ISOLATED { return nil, nil } @@ -325,13 +318,13 @@ func (k *Keeper) transferCollateralForIsolatedPerpetual( func (k *Keeper) computeAndExecuteCollateralTransfer( ctx sdk.Context, settledUpdateWithUpdatedSubaccount SettledUpdate, - perpetuals []perptypes.Perpetual, + perpInfos map[uint32]perptypes.PerpInfo, ) error { // The subaccount in `settledUpdateWithUpdatedSubaccount` already has the perpetual updates // and asset updates applied to it. stateTransition, err := GetIsolatedPerpetualStateTransition( settledUpdateWithUpdatedSubaccount, - perpetuals, + perpInfos, ) if err != nil { return err @@ -345,15 +338,3 @@ func (k *Keeper) computeAndExecuteCollateralTransfer( return nil } - -func getPerpIdToMarketTypeMap( - perpetuals []perptypes.Perpetual, -) map[uint32]perptypes.PerpetualMarketType { - var perpIdToMarketType = make(map[uint32]perptypes.PerpetualMarketType, len(perpetuals)) - - for _, perpetual := range perpetuals { - perpIdToMarketType[perpetual.GetId()] = perpetual.Params.MarketType - } - - return perpIdToMarketType -} diff --git a/protocol/x/subaccounts/keeper/isolated_subaccount_test.go b/protocol/x/subaccounts/keeper/isolated_subaccount_test.go index 25073a920e..5b226cdd0b 100644 --- a/protocol/x/subaccounts/keeper/isolated_subaccount_test.go +++ b/protocol/x/subaccounts/keeper/isolated_subaccount_test.go @@ -319,9 +319,16 @@ func TestGetIsolatedPerpetualStateTransition(t *testing.T) { for name, tc := range tests { t.Run( name, func(t *testing.T) { + perpInfos := make(map[uint32]perptypes.PerpInfo, len(tc.perpetuals)) + for _, perp := range tc.perpetuals { + perpInfos[perp.Params.Id] = perptypes.PerpInfo{ + Perpetual: perp, + } + } + stateTransition, err := keeper.GetIsolatedPerpetualStateTransition( tc.settledUpdateWithUpdatedSubaccount, - tc.perpetuals, + perpInfos, ) if tc.expectedErr != nil { require.Error(t, tc.expectedErr, err) diff --git a/protocol/x/subaccounts/keeper/subaccount.go b/protocol/x/subaccounts/keeper/subaccount.go index 69b623efc5..fe39182c05 100644 --- a/protocol/x/subaccounts/keeper/subaccount.go +++ b/protocol/x/subaccounts/keeper/subaccount.go @@ -211,6 +211,7 @@ func (k Keeper) getRandomBytes(ctx sdk.Context, rand *rand.Rand) ([]byte, error) func (k Keeper) getSettledUpdates( ctx sdk.Context, updates []types.Update, + perpInfos map[uint32]perptypes.PerpInfo, requireUniqueSubaccount bool, ) ( settledUpdates []SettledUpdate, @@ -234,7 +235,7 @@ func (k Keeper) getSettledUpdates( // idToSettledSubaccount map. if !exists { subaccount := k.GetSubaccount(ctx, u.SubaccountId) - settledSubaccount, fundingPayments, err = k.getSettledSubaccount(ctx, subaccount) + settledSubaccount, fundingPayments, err = GetSettledSubaccountWithPerpetuals(subaccount, perpInfos) if err != nil { return nil, nil, err } @@ -285,29 +286,27 @@ func (k Keeper) UpdateSubaccounts( }, ) - settledUpdates, subaccountIdToFundingPayments, err := k.getSettledUpdates(ctx, updates, true) + perpInfos, err := k.GetAllRelevantPerpetuals(ctx, updates) + if err != nil { + return false, nil, err + } + + settledUpdates, subaccountIdToFundingPayments, err := k.getSettledUpdates(ctx, updates, perpInfos, true) if err != nil { return false, nil, err } - allPerps := k.perpetualsKeeper.GetAllPerpetuals(ctx) success, successPerUpdate, err = k.internalCanUpdateSubaccounts( ctx, settledUpdates, updateType, - allPerps, + perpInfos, ) if !success || err != nil { return success, successPerUpdate, err } - // Get a mapping from perpetual Id to current perpetual funding index. - perpIdToFundingIndex := make(map[uint32]dtypes.SerializableInt, len(allPerps)) - for _, perp := range allPerps { - 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 { @@ -330,7 +329,7 @@ func (k Keeper) UpdateSubaccounts( // Apply the updates to perpetual positions. UpdatePerpetualPositions( settledUpdates, - perpIdToFundingIndex, + perpInfos, ) // Apply the updates to asset positions. @@ -344,7 +343,7 @@ func (k Keeper) UpdateSubaccounts( // The subaccount in `settledUpdateWithUpdatedSubaccount` already has the perpetual updates // and asset updates applied to it. settledUpdateWithUpdatedSubaccount, - allPerps, + perpInfos, ); err != nil { return false, nil, err } @@ -355,7 +354,7 @@ func (k Keeper) UpdateSubaccounts( for _, u := range settledUpdates { k.SetSubaccount(ctx, u.SettledSubaccount) // Below access is safe because for all updated subaccounts' IDs, this map - // is populated as getSettledSubaccount() is called in getSettledUpdates(). + // is populated as GetSettledSubaccountWithPerpetuals() is called in getSettledUpdates(). fundingPayments := subaccountIdToFundingPayments[*u.SettledSubaccount.Id] k.GetIndexerEventManager().AddTxnEvent( ctx, @@ -422,39 +421,18 @@ func (k Keeper) CanUpdateSubaccounts( }, ) - settledUpdates, _, err := k.getSettledUpdates(ctx, updates, false) + perpInfos, err := k.GetAllRelevantPerpetuals(ctx, updates) if err != nil { return false, nil, err } - allPerps := k.perpetualsKeeper.GetAllPerpetuals(ctx) - success, successPerUpdate, err = k.internalCanUpdateSubaccounts(ctx, settledUpdates, updateType, allPerps) - return success, successPerUpdate, err -} - -// getSettledSubaccount returns 1. a new settled subaccount given an unsettled subaccount, -// updating the USDC AssetPosition, FundingIndex, and LastFundingPayment fields accordingly -// (does not persist any changes) and 2. a map with perpetual ID as key and last funding -// payment as value (for emitting funding payments to indexer). -func (k Keeper) getSettledSubaccount( - ctx sdk.Context, - subaccount types.Subaccount, -) ( - settledSubaccount types.Subaccount, - fundingPayments map[uint32]dtypes.SerializableInt, - err error, -) { - // Fetch all relevant perpetuals. - perpetuals := make(map[uint32]perptypes.Perpetual, len(subaccount.PerpetualPositions)) - for _, p := range subaccount.PerpetualPositions { - perpetual, err := k.perpetualsKeeper.GetPerpetual(ctx, p.PerpetualId) - if err != nil { - return types.Subaccount{}, nil, err - } - perpetuals[p.PerpetualId] = perpetual + settledUpdates, _, err := k.getSettledUpdates(ctx, updates, perpInfos, false) + if err != nil { + return false, nil, err } - return GetSettledSubaccountWithPerpetuals(subaccount, perpetuals) + success, successPerUpdate, err = k.internalCanUpdateSubaccounts(ctx, settledUpdates, updateType, perpInfos) + return success, successPerUpdate, err } // GetSettledSubaccountWithPerpetuals returns 1. a new settled subaccount given an unsettled subaccount, @@ -465,7 +443,7 @@ func (k Keeper) getSettledSubaccount( // Note that this is a stateless utility function. func GetSettledSubaccountWithPerpetuals( subaccount types.Subaccount, - perpetuals map[uint32]perptypes.Perpetual, + perpInfos map[uint32]perptypes.PerpInfo, ) ( settledSubaccount types.Subaccount, fundingPayments map[uint32]dtypes.SerializableInt, @@ -478,18 +456,14 @@ func GetSettledSubaccountWithPerpetuals( // Iterate through and settle all perpetual positions. for _, p := range subaccount.PerpetualPositions { - perpetual, found := perpetuals[p.PerpetualId] + perpInfo, found := perpInfos[p.PerpetualId] if !found { - return types.Subaccount{}, - nil, - errorsmod.Wrap( - perptypes.ErrPerpetualDoesNotExist, lib.UintToString(p.PerpetualId), - ) + return types.Subaccount{}, nil, errorsmod.Wrapf(types.ErrPerpetualInfoDoesNotExist, "%d", p.PerpetualId) } // Call the stateless utility function to get the net settlement and new funding index. bigNetSettlementPpm, newFundingIndex := perplib.GetSettlementPpmWithPerpetual( - perpetual, + perpInfo.Perpetual, p.GetBigQuantums(), p.FundingIndex.BigInt(), ) @@ -576,7 +550,7 @@ func (k Keeper) internalCanUpdateSubaccounts( ctx sdk.Context, settledUpdates []SettledUpdate, updateType types.UpdateType, - perpetuals []perptypes.Perpetual, + perpInfos map[uint32]perptypes.PerpInfo, ) ( success bool, successPerUpdate []types.UpdateResult, @@ -588,7 +562,7 @@ func (k Keeper) internalCanUpdateSubaccounts( success, successPerUpdate, err = k.checkIsolatedSubaccountConstraints( ctx, settledUpdates, - perpetuals, + perpInfos, ) if err != nil { return false, nil, err @@ -666,6 +640,29 @@ func (k Keeper) internalCanUpdateSubaccounts( // do not result in OI changes. perpOpenInterestDelta := GetDeltaOpenInterestFromUpdates(settledUpdates, updateType) + // 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 { + perpInfo, ok := perpInfos[perpOpenInterestDelta.PerpetualId] + if !ok { + return false, nil, errorsmod.Wrapf(types.ErrPerpetualInfoDoesNotExist, "%d", perpOpenInterestDelta.PerpetualId) + } + existingValue := big.NewInt(0) + if !perpInfo.Perpetual.OpenInterest.IsNil() { + existingValue.Set(perpInfo.Perpetual.OpenInterest.BigInt()) + } + perpInfo.Perpetual.OpenInterest = dtypes.NewIntFromBigInt( + new(big.Int).Add(existingValue, perpOpenInterestDelta.BaseQuantums), + ) + perpInfos[perpOpenInterestDelta.PerpetualId] = perpInfo + + // Reset the OpenInterest to the original value. + defer func() { + perpInfo.Perpetual.OpenInterest = dtypes.NewIntFromBigInt(existingValue) + perpInfos[perpOpenInterestDelta.PerpetualId] = perpInfo + }() + } + bigCurNetCollateral := make(map[string]*big.Int) bigCurInitialMargin := make(map[string]*big.Int) bigCurMaintenanceMargin := make(map[string]*big.Int) @@ -688,35 +685,14 @@ func (k Keeper) internalCanUpdateSubaccounts( } } - // Branch the state to calculate the new OIMF after OI increase. - // The branched state is only needed for this purpose and is always discarded. - branchedContext, _ := ctx.CacheContext() - - // 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( - branchedContext, - 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, - ) - } - } // Get the new collateralization and margin requirements with the update applied. bigNewNetCollateral, bigNewInitialMargin, bigNewMaintenanceMargin, err := k.internalGetNetCollateralAndMarginRequirements( - branchedContext, + ctx, u, + perpInfos, ) // if `internalGetNetCollateralAndMarginRequirements`, returns error. @@ -748,6 +724,7 @@ func (k Keeper) internalCanUpdateSubaccounts( err = k.internalGetNetCollateralAndMarginRequirements( ctx, emptyUpdate, + perpInfos, ) if err != nil { return false, nil, err @@ -864,7 +841,11 @@ func (k Keeper) GetNetCollateralAndMarginRequirements( ) { subaccount := k.GetSubaccount(ctx, update.SubaccountId) - settledSubaccount, _, err := k.getSettledSubaccount(ctx, subaccount) + perpInfos, err := k.GetAllRelevantPerpetuals(ctx, []types.Update{update}) + if err != nil { + return nil, nil, nil, err + } + settledSubaccount, _, err := GetSettledSubaccountWithPerpetuals(subaccount, perpInfos) if err != nil { return nil, nil, nil, err } @@ -878,6 +859,7 @@ func (k Keeper) GetNetCollateralAndMarginRequirements( return k.internalGetNetCollateralAndMarginRequirements( ctx, settledUpdate, + perpInfos, ) } @@ -895,6 +877,7 @@ func (k Keeper) GetNetCollateralAndMarginRequirements( func (k Keeper) internalGetNetCollateralAndMarginRequirements( ctx sdk.Context, settledUpdate SettledUpdate, + perpInfos map[uint32]perptypes.PerpInfo, ) ( bigNetCollateral *big.Int, bigInitialMargin *big.Int, @@ -956,17 +939,15 @@ func (k Keeper) internalGetNetCollateralAndMarginRequirements( // Iterate over all perpetuals and updates and calculate change to net collateral and margin requirements. for _, size := range perpetualSizes { - perpetual, - marketPrice, - liquidityTier, - err := k.perpetualsKeeper.GetPerpetualAndMarketPriceAndLiquidityTier(ctx, size.GetId()) - if err != nil { - return big.NewInt(0), big.NewInt(0), big.NewInt(0), err + perpInfo, found := perpInfos[size.GetId()] + if !found { + return big.NewInt(0), big.NewInt(0), big.NewInt(0), + errorsmod.Wrapf(types.ErrPerpetualInfoDoesNotExist, "%d", size.GetId()) } nc, imr, mmr := perplib.GetNetCollateralAndMarginRequirements( - perpetual, - marketPrice, - liquidityTier, + perpInfo.Perpetual, + perpInfo.Price, + perpInfo.LiquidityTier, size.GetBigQuantums(), ) bigNetCollateral.Add(bigNetCollateral, nc) @@ -1035,3 +1016,53 @@ func applyUpdatesToPositions[ return result, nil } + +// GetAllRelevantPerpetuals returns all relevant perpetual information for a given set of updates. +// This includes all perpetuals that exist on the accounts already and all perpetuals that are +// being updated in the input updates. +func (k Keeper) GetAllRelevantPerpetuals( + ctx sdk.Context, + updates []types.Update, +) ( + map[uint32]perptypes.PerpInfo, + error, +) { + subaccountIds := make(map[types.SubaccountId]struct{}) + perpIds := make(map[uint32]struct{}) + + // Add all relevant perpetuals in every update. + for _, update := range updates { + // If this subaccount has not been processed already, get all of its existing perpetuals. + if _, exists := subaccountIds[update.SubaccountId]; !exists { + sa := k.GetSubaccount(ctx, update.SubaccountId) + for _, postition := range sa.PerpetualPositions { + perpIds[postition.PerpetualId] = struct{}{} + } + subaccountIds[update.SubaccountId] = struct{}{} + } + + // Add all perpetuals in the update. + for _, perpUpdate := range update.PerpetualUpdates { + perpIds[perpUpdate.GetId()] = struct{}{} + } + } + + // Get all perpetual information from state. + perpetuals := make(map[uint32]perptypes.PerpInfo, len(perpIds)) + for perpId := range perpIds { + perpetual, + price, + liquidityTier, + err := k.perpetualsKeeper.GetPerpetualAndMarketPriceAndLiquidityTier(ctx, perpId) + if err != nil { + return nil, err + } + perpetuals[perpId] = perptypes.PerpInfo{ + Perpetual: perpetual, + Price: price, + LiquidityTier: liquidityTier, + } + } + + return perpetuals, nil +} diff --git a/protocol/x/subaccounts/keeper/subaccount_helper.go b/protocol/x/subaccounts/keeper/subaccount_helper.go index 799704184a..9fc97011d6 100644 --- a/protocol/x/subaccounts/keeper/subaccount_helper.go +++ b/protocol/x/subaccounts/keeper/subaccount_helper.go @@ -1,10 +1,12 @@ package keeper import ( - "fmt" "sort" + errorsmod "cosmossdk.io/errors" + "github.com/dydxprotocol/v4-chain/protocol/dtypes" + perptypes "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types" "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" ) @@ -103,7 +105,7 @@ func getUpdatedPerpetualPositions( // For newly created positions, use `perpIdToFundingIndex` map to populate the `FundingIndex` field. func UpdatePerpetualPositions( settledUpdates []SettledUpdate, - perpIdToFundingIndex map[uint32]dtypes.SerializableInt, + perpInfos map[uint32]perptypes.PerpInfo, ) { // Apply the updates. for i, u := range settledUpdates { @@ -131,16 +133,16 @@ func UpdatePerpetualPositions( } else { // This subaccount does not have a matching position for this update. // Create the new position. - fundingIndex, exists := perpIdToFundingIndex[pu.PerpetualId] + perpInfo, exists := perpInfos[pu.PerpetualId] if !exists { - // Invariant: `perpIdToFundingIndex` contains all existing perpetauls, - // and perpetual position update must refer to an existing perpetual. - panic(fmt.Sprintf("perpetual id %d not found in perpIdToFundingIndex", pu.PerpetualId)) + // Invariant: `perpInfos` should all relevant perpetuals, which includes all + // perpetuals that are updated. + panic(errorsmod.Wrapf(types.ErrPerpetualInfoDoesNotExist, "%d", pu.PerpetualId)) } perpetualPosition := &types.PerpetualPosition{ PerpetualId: pu.PerpetualId, Quantums: dtypes.NewIntFromBigInt(pu.GetBigQuantums()), - FundingIndex: fundingIndex, + FundingIndex: perpInfo.Perpetual.FundingIndex, } // Add the new position to the map. diff --git a/protocol/x/subaccounts/types/errors.go b/protocol/x/subaccounts/types/errors.go index 2113575cfe..e36de86d00 100644 --- a/protocol/x/subaccounts/types/errors.go +++ b/protocol/x/subaccounts/types/errors.go @@ -65,6 +65,11 @@ var ( 403, "cannot revert perpetual open interest for OIMF calculation", ) + ErrPerpetualInfoDoesNotExist = errorsmod.Register( + ModuleName, + 404, + "PerpetualInfo does not exist in map", + ) // 500 - 599: transfer related. ErrAssetTransferQuantumsNotPositive = errorsmod.Register(