diff --git a/protocol/testutil/keeper/listing.go b/protocol/testutil/keeper/listing.go index dbf11239f8..78a72d13f3 100644 --- a/protocol/testutil/keeper/listing.go +++ b/protocol/testutil/keeper/listing.go @@ -48,6 +48,7 @@ func ListingKeepers( // Define necessary keepers here for unit tests memClob := &mocks.MemClob{} memClob.On("SetClobKeeper", mock.Anything).Return() + memClob.On("CreateOrderbook", mock.Anything, mock.Anything).Return(nil) epochsKeeper, _ := createEpochsKeeper(stateStore, db, cdc) accountsKeeper, _ := createAccountKeeper( diff --git a/protocol/x/clob/keeper/clob_pair.go b/protocol/x/clob/keeper/clob_pair.go index 13183bdad6..c03ebdfb6f 100644 --- a/protocol/x/clob/keeper/clob_pair.go +++ b/protocol/x/clob/keeper/clob_pair.go @@ -36,7 +36,7 @@ func clobPairKey( // CreatePerpetualClobPair creates a new perpetual CLOB pair in the store. // Additionally, it creates an order book matching the ID of the newly created CLOB pair. // -// An error will occur if any of the fields fail validation (see validateClobPair for details), +// An error will occur if any of the fields fail validation (see ValidateClobPair for details), // or if the `perpetualId` cannot be found. // In the event of an error, the store will not be updated nor will a matching order book be created. // @@ -50,26 +50,6 @@ func (k Keeper) CreatePerpetualClobPair( subticksPerTick uint32, status types.ClobPair_Status, ) (types.ClobPair, error) { - // If the desired CLOB pair ID is already in use, return an error. - if clobPair, exists := k.GetClobPair(ctx, types.ClobPairId(clobPairId)); exists { - return types.ClobPair{}, errorsmod.Wrapf( - types.ErrClobPairAlreadyExists, - "id=%v, existing clob pair=%v", - clobPairId, - clobPair, - ) - } - - // Verify the perpetual ID is not already associated with an existing CLOB pair. - if clobPairId, found := k.PerpetualIdToClobPairId[perpetualId]; found { - return types.ClobPair{}, errorsmod.Wrapf( - types.ErrPerpetualAssociatedWithExistingClobPair, - "perpetual id=%v, existing clob pair id=%v", - perpetualId, - clobPairId, - ) - } - clobPair := types.ClobPair{ Metadata: &types.ClobPair_PerpetualClobMetadata{ PerpetualClobMetadata: &types.PerpetualClobMetadata{ @@ -82,39 +62,52 @@ func (k Keeper) CreatePerpetualClobPair( SubticksPerTick: subticksPerTick, Status: status, } - if err := k.validateClobPair(ctx, &clobPair); err != nil { + if err := k.ValidateClobPairCreation(ctx, &clobPair); err != nil { return clobPair, err } - perpetual, err := k.perpetualsKeeper.GetPerpetual(ctx, perpetualId) + + err := k.CreateClobPair(ctx, clobPair) if err != nil { return clobPair, err } - k.createClobPair(ctx, clobPair) - k.GetIndexerEventManager().AddTxnEvent( - ctx, - indexerevents.SubtypePerpetualMarket, - indexerevents.PerpetualMarketEventVersion, - indexer_manager.GetBytes( - indexerevents.NewPerpetualMarketCreateEvent( - perpetualId, - clobPairId, - perpetual.Params.Ticker, - perpetual.Params.MarketId, - status, - quantumConversionExponent, - perpetual.Params.AtomicResolution, - subticksPerTick, - stepSizeBaseQuantums.ToUint64(), - perpetual.Params.LiquidityTier, - perpetual.Params.MarketType, - ), - ), - ) - return clobPair, nil } +// ValidateClobPairCreation validates a CLOB pair's fields are suitable for CLOB pair creation +// and that the perpetual ID is associated with an existing perpetual. +func (k Keeper) ValidateClobPairCreation(ctx sdk.Context, clobPair *types.ClobPair) error { + // If the desired CLOB pair ID is already in use, return an error. + if clobPair, exists := k.GetClobPair(ctx, clobPair.GetClobPairId()); exists { + return errorsmod.Wrapf( + types.ErrClobPairAlreadyExists, + "id=%v, existing clob pair=%v", + clobPair.Id, + clobPair, + ) + } + + perpetualId, err := clobPair.GetPerpetualId() + if err != nil { + return errorsmod.Wrap( + types.ErrInvalidClobPairParameter, + err.Error(), + ) + } + + // Verify the perpetual ID is not already associated with an existing CLOB pair. + if clobPairId, found := k.PerpetualIdToClobPairId[perpetualId]; found { + return errorsmod.Wrapf( + types.ErrPerpetualAssociatedWithExistingClobPair, + "perpetual id=%v, existing clob pair id=%v", + perpetualId, + clobPairId, + ) + } + + return k.validateClobPair(ctx, clobPair) +} + // validateClobPair validates a CLOB pair's fields are suitable for CLOB pair creation. // // Stateful Validation: @@ -169,9 +162,9 @@ func (k Keeper) createOrderbook(ctx sdk.Context, clobPair types.ClobPair) { k.MemClob.CreateOrderbook(clobPair) } -// createClobPair creates a new `ClobPair` in the store and creates the corresponding orderbook in the memclob. +// CreateClobPair creates a new `ClobPair` in the store and creates the corresponding orderbook in the memclob. // This function returns an error if a value for the ClobPair's id already exists in state. -func (k Keeper) createClobPair(ctx sdk.Context, clobPair types.ClobPair) { +func (k Keeper) CreateClobPair(ctx sdk.Context, clobPair types.ClobPair) error { // Validate the given clob pair id is not already in use. if _, exists := k.GetClobPair(ctx, clobPair.GetClobPairId()); exists { panic( @@ -190,6 +183,38 @@ func (k Keeper) createClobPair(ctx sdk.Context, clobPair types.ClobPair) { // Create the mapping between clob pair and perpetual. k.SetClobPairIdForPerpetual(ctx, clobPair) + + perpetualId, err := clobPair.GetPerpetualId() + if err != nil { + panic(err) + } + perpetual, err := k.perpetualsKeeper.GetPerpetual(ctx, perpetualId) + if err != nil { + return err + } + + k.GetIndexerEventManager().AddTxnEvent( + ctx, + indexerevents.SubtypePerpetualMarket, + indexerevents.PerpetualMarketEventVersion, + indexer_manager.GetBytes( + indexerevents.NewPerpetualMarketCreateEvent( + perpetualId, + clobPair.Id, + perpetual.Params.Ticker, + perpetual.Params.MarketId, + clobPair.Status, + clobPair.QuantumConversionExponent, + perpetual.Params.AtomicResolution, + clobPair.SubticksPerTick, + clobPair.StepBaseQuantums, + perpetual.Params.LiquidityTier, + perpetual.Params.MarketType, + ), + ), + ) + + return nil } // setClobPair sets a specific `ClobPair` in the store from its index. diff --git a/protocol/x/listing/client/cli/tx.go b/protocol/x/listing/client/cli/tx.go index d959097e87..a766edc1f0 100644 --- a/protocol/x/listing/client/cli/tx.go +++ b/protocol/x/listing/client/cli/tx.go @@ -3,6 +3,10 @@ package cli import ( "fmt" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/tx" + satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" + "github.com/spf13/cobra" "github.com/cosmos/cosmos-sdk/client" @@ -19,5 +23,38 @@ func GetTxCmd() *cobra.Command { RunE: client.ValidateCmd, } + cmd.AddCommand(CmdCreateMarketPermissionless()) + + return cmd +} + +// CmdCreateMarketPermissionless is the CLI command for creating a permissionless market. +func CmdCreateMarketPermissionless() *cobra.Command { + cmd := &cobra.Command{ + Use: "create-market [ticker] [address]", + Short: "Create new market with permissionless access", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) (err error) { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + ticker, address := args[0], args[1] + + // Create MsgCreateMarketPermissionless. + msg := &types.MsgCreateMarketPermissionless{ + Ticker: ticker, + SubaccountId: &satypes.SubaccountId{ + Owner: address, + Number: 0, + }, + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + return cmd } diff --git a/protocol/x/listing/keeper/listing.go b/protocol/x/listing/keeper/listing.go index f716428c1d..53ca3e8ca0 100644 --- a/protocol/x/listing/keeper/listing.go +++ b/protocol/x/listing/keeper/listing.go @@ -3,6 +3,8 @@ package keeper import ( "math" + "github.com/dydxprotocol/v4-chain/protocol/lib" + "github.com/dydxprotocol/v4-chain/protocol/lib/slinky" sdk "github.com/cosmos/cosmos-sdk/types" @@ -11,7 +13,6 @@ import ( "github.com/dydxprotocol/v4-chain/protocol/x/listing/types" perpetualtypes "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types" pricestypes "github.com/dydxprotocol/v4-chain/protocol/x/prices/types" - satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" "github.com/skip-mev/slinky/x/marketmap/types/tickermetadata" ) @@ -84,20 +85,31 @@ func (k Keeper) CreateClobPair( ) (clobPairId uint32, err error) { clobPairId = k.ClobKeeper.AcquireNextClobPairID(ctx) - // Create a new clob pair - clobPair, err := k.ClobKeeper.CreatePerpetualClobPair( - ctx, - clobPairId, - perpetualId, - satypes.BaseQuantums(types.DefaultStepBaseQuantums), - types.DefaultQuantumConversionExponent, - types.SubticksPerTick_LongTail, - clobtypes.ClobPair_STATUS_ACTIVE, - ) - if err != nil { + clobPair := clobtypes.ClobPair{ + Metadata: &clobtypes.ClobPair_PerpetualClobMetadata{ + PerpetualClobMetadata: &clobtypes.PerpetualClobMetadata{ + PerpetualId: perpetualId, + }, + }, + Id: clobPairId, + StepBaseQuantums: types.DefaultStepBaseQuantums, + QuantumConversionExponent: types.DefaultQuantumConversionExponent, + SubticksPerTick: types.SubticksPerTick_LongTail, + Status: clobtypes.ClobPair_STATUS_ACTIVE, + } + if err := k.ClobKeeper.ValidateClobPairCreation(ctx, &clobPair); err != nil { return 0, err } + // Only create the clob pair if we are in deliver tx mode. This is to prevent populating + // in memory data structures in the CLOB during simulation mode. + if lib.IsDeliverTxMode(ctx) { + err := k.ClobKeeper.CreateClobPair(ctx, clobPair) + if err != nil { + return 0, err + } + } + return clobPair.Id, nil } @@ -118,11 +130,11 @@ func (k Keeper) CreatePerpetual( } marketMapDetails, err := k.MarketMapKeeper.GetMarket(ctx, marketMapPair.String()) if err != nil { - return 0, err + return 0, types.ErrMarketNotFound } metadata, err := tickermetadata.DyDxFromJSONString(marketMapDetails.Ticker.Metadata_JSON) if err != nil { - return 0, err + return 0, types.ErrInvalidMarketMapTickerMetadata } if metadata.ReferencePrice == 0 { return 0, types.ErrReferencePriceZero diff --git a/protocol/x/listing/keeper/listing_test.go b/protocol/x/listing/keeper/listing_test.go index 8943e21bd3..3e37a9ec09 100644 --- a/protocol/x/listing/keeper/listing_test.go +++ b/protocol/x/listing/keeper/listing_test.go @@ -4,6 +4,11 @@ import ( "errors" "testing" + "github.com/stretchr/testify/mock" + + "github.com/dydxprotocol/v4-chain/protocol/lib" + clobtypes "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" + perpetualtypes "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types" oracletypes "github.com/skip-mev/slinky/pkg/types" marketmaptypes "github.com/skip-mev/slinky/x/marketmap/types" @@ -180,3 +185,107 @@ func TestCreatePerpetual(t *testing.T) { ) } } + +func TestCreateClobPair(t *testing.T) { + tests := map[string]struct { + ticker string + isDeliverTx bool + }{ + "deliverTx - true": { + ticker: "TEST-USD", + isDeliverTx: true, + }, + "deliverTx - false": { + ticker: "TEST-USD", + isDeliverTx: false, + }, + } + + for name, tc := range tests { + t.Run( + name, func(t *testing.T) { + mockIndexerEventManager := &mocks.IndexerEventManager{} + ctx, keeper, _, _, pricesKeeper, perpetualsKeeper, clobKeeper, marketMapKeeper := keepertest.ListingKeepers( + t, + &mocks.BankKeeper{}, + mockIndexerEventManager, + ) + mockIndexerEventManager.On( + "AddTxnEvent", + mock.Anything, + mock.Anything, + mock.Anything, + mock.Anything, + ).Return() + keepertest.CreateLiquidityTiersAndNPerpetuals(t, ctx, perpetualsKeeper, pricesKeeper, 10) + + // Set deliverTx mode + if tc.isDeliverTx { + ctx = ctx.WithIsCheckTx(false).WithIsReCheckTx(false) + lib.AssertDeliverTxMode(ctx) + } else { + ctx = ctx.WithIsCheckTx(true) + lib.AssertCheckTxMode(ctx) + } + + // Create a marketmap with a single market + dydxMetadata, err := tickermetadata.MarshalDyDx( + tickermetadata.DyDx{ + ReferencePrice: 1000000000, + Liquidity: 0, + AggregateIDs: nil, + }, + ) + require.NoError(t, err) + + market := marketmaptypes.Market{ + Ticker: marketmaptypes.Ticker{ + CurrencyPair: oracletypes.CurrencyPair{Base: "TEST", Quote: "USD"}, + Decimals: 6, + MinProviderCount: 2, + Enabled: false, + Metadata_JSON: string(dydxMetadata), + }, + ProviderConfigs: []marketmaptypes.ProviderConfig{ + { + Name: "binance_ws", + OffChainTicker: "TESTUSDT", + }, + }, + } + err = marketMapKeeper.CreateMarket(ctx, market) + require.NoError(t, err) + + marketId, err := keeper.CreateMarket(ctx, tc.ticker) + require.NoError(t, err) + + perpetualId, err := keeper.CreatePerpetual(ctx, marketId, tc.ticker) + require.NoError(t, err) + + clobPairId, err := keeper.CreateClobPair(ctx, perpetualId) + require.NoError(t, err) + + // Check if the clob pair was created only if we are in deliverTx mode + if tc.isDeliverTx { + clobPair, found := clobKeeper.GetClobPair(ctx, clobtypes.ClobPairId(clobPairId)) + require.True(t, found) + require.Equal(t, clobtypes.ClobPair_STATUS_ACTIVE, clobPair.Status) + require.Equal( + t, + clobtypes.SubticksPerTick(types.SubticksPerTick_LongTail), + clobPair.GetClobPairSubticksPerTick(), + ) + require.Equal( + t, + types.DefaultStepBaseQuantums, + clobPair.GetClobPairMinOrderBaseQuantums().ToUint64(), + ) + require.Equal(t, perpetualId, clobPair.MustGetPerpetualId()) + } else { + _, found := clobKeeper.GetClobPair(ctx, clobtypes.ClobPairId(clobPairId)) + require.False(t, found) + } + }, + ) + } +} diff --git a/protocol/x/listing/keeper/msg_create_market_permissionless.go b/protocol/x/listing/keeper/msg_create_market_permissionless.go index 96d4d5a176..194ff01b08 100644 --- a/protocol/x/listing/keeper/msg_create_market_permissionless.go +++ b/protocol/x/listing/keeper/msg_create_market_permissionless.go @@ -21,16 +21,19 @@ func (k msgServer) CreateMarketPermissionless( marketId, err := k.Keeper.CreateMarket(ctx, msg.Ticker) if err != nil { + k.Logger(ctx).Error("failed to create PML market", "error", err) return nil, err } perpetualId, err := k.Keeper.CreatePerpetual(ctx, marketId, msg.Ticker) if err != nil { + k.Logger(ctx).Error("failed to create perpetual for PML market", "error", err) return nil, err } _, err = k.Keeper.CreateClobPair(ctx, perpetualId) if err != nil { + k.Logger(ctx).Error("failed to create clob pair for PML market", "error", err) return nil, err } diff --git a/protocol/x/listing/types/errors.go b/protocol/x/listing/types/errors.go index 920b6a3854..8213fdd738 100644 --- a/protocol/x/listing/types/errors.go +++ b/protocol/x/listing/types/errors.go @@ -33,4 +33,10 @@ var ( 5, "invalid number of blocks to lock shares", ) + + ErrInvalidMarketMapTickerMetadata = errorsmod.Register( + ModuleName, + 6, + "invalid market map ticker metadata", + ) ) diff --git a/protocol/x/listing/types/expected_keepers.go b/protocol/x/listing/types/expected_keepers.go index 2e071d7ed4..5201a05b5d 100644 --- a/protocol/x/listing/types/expected_keepers.go +++ b/protocol/x/listing/types/expected_keepers.go @@ -29,6 +29,8 @@ type ClobKeeper interface { status clobtypes.ClobPair_Status, ) (clobtypes.ClobPair, error) AcquireNextClobPairID(ctx sdk.Context) uint32 + ValidateClobPairCreation(ctx sdk.Context, clobPair *clobtypes.ClobPair) error + CreateClobPair(ctx sdk.Context, clobPair clobtypes.ClobPair) error } type MarketMapKeeper interface {