diff --git a/protocol/app/app.go b/protocol/app/app.go index 8c9846de11..e6f30878f7 100644 --- a/protocol/app/app.go +++ b/protocol/app/app.go @@ -1165,7 +1165,14 @@ func New( &app.MarketMapKeeper, app.PerpetualsKeeper, ) - listingModule := listingmodule.NewAppModule(appCodec, app.ListingKeeper) + listingModule := listingmodule.NewAppModule( + appCodec, + app.ListingKeeper, + app.PricesKeeper, + app.ClobKeeper, + &app.MarketMapKeeper, + app.PerpetualsKeeper, + ) app.AccountPlusKeeper = *accountplusmodulekeeper.NewKeeper( appCodec, diff --git a/protocol/testutil/app/app.go b/protocol/testutil/app/app.go index 72a9dfeb2a..44f34b4b70 100644 --- a/protocol/testutil/app/app.go +++ b/protocol/testutil/app/app.go @@ -15,6 +15,8 @@ import ( "testing" "time" + listingtypes "github.com/dydxprotocol/v4-chain/protocol/x/listing/types" + "cosmossdk.io/log" "cosmossdk.io/store/rootmulti" storetypes "cosmossdk.io/store/types" @@ -265,6 +267,8 @@ func UpdateGenesisDocWithAppStateForModule[T GenesisStates](genesisDoc *types.Ge moduleName = revsharetypes.ModuleName case marketmapmoduletypes.GenesisState: moduleName = marketmapmoduletypes.ModuleName + case listingtypes.GenesisState: + moduleName = listingtypes.ModuleName default: panic(fmt.Errorf("Unsupported type %T", t)) } diff --git a/protocol/testutil/keeper/listing.go b/protocol/testutil/keeper/listing.go new file mode 100644 index 0000000000..e36116b34f --- /dev/null +++ b/protocol/testutil/keeper/listing.go @@ -0,0 +1,187 @@ +package keeper + +import ( + storetypes "cosmossdk.io/store/types" + dbm "github.com/cosmos/cosmos-db" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + "github.com/dydxprotocol/v4-chain/protocol/indexer/indexer_manager" + "github.com/dydxprotocol/v4-chain/protocol/lib" + "github.com/dydxprotocol/v4-chain/protocol/mocks" + "github.com/dydxprotocol/v4-chain/protocol/testutil/constants" + clobkeeper "github.com/dydxprotocol/v4-chain/protocol/x/clob/keeper" + perpetualskeeper "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/keeper" + priceskeeper "github.com/dydxprotocol/v4-chain/protocol/x/prices/keeper" + marketmapkeeper "github.com/skip-mev/slinky/x/marketmap/keeper" + "github.com/stretchr/testify/mock" + + "testing" + + "github.com/dydxprotocol/v4-chain/protocol/x/listing/keeper" + "github.com/dydxprotocol/v4-chain/protocol/x/listing/types" +) + +func ListingKeepers( + t testing.TB, + bankKeeper bankkeeper.Keeper, + indexerEventManager indexer_manager.IndexerEventManager, +) ( + ctx sdk.Context, + keeper *keeper.Keeper, + storeKey storetypes.StoreKey, + mockTimeProvider *mocks.TimeProvider, + pricesKeeper *priceskeeper.Keeper, + perpetualsKeeper *perpetualskeeper.Keeper, + clobKeeper *clobkeeper.Keeper, + marketMapKeeper *marketmapkeeper.Keeper, +) { + ctx = initKeepers( + t, func( + db *dbm.MemDB, + registry codectypes.InterfaceRegistry, + cdc *codec.ProtoCodec, + stateStore storetypes.CommitMultiStore, + transientStoreKey storetypes.StoreKey, + ) []GenesisInitializer { + // Define necessary keepers here for unit tests + memClob := &mocks.MemClob{} + memClob.On("SetClobKeeper", mock.Anything).Return() + revShareKeeper, _, _ := createRevShareKeeper(stateStore, db, cdc) + marketMapKeeper, _ = createMarketMapKeeper(stateStore, db, cdc) + pricesKeeper, _, _, mockTimeProvider = createPricesKeeper( + stateStore, + db, + cdc, + transientStoreKey, + revShareKeeper, + marketMapKeeper, + ) + // Mock time provider response for market creation. + mockTimeProvider.On("Now").Return(constants.TimeT) + epochsKeeper, _ := createEpochsKeeper(stateStore, db, cdc) + perpetualsKeeper, _ = createPerpetualsKeeper( + stateStore, + db, + cdc, + pricesKeeper, + epochsKeeper, + transientStoreKey, + ) + assetsKeeper, _ := createAssetsKeeper( + stateStore, + db, + cdc, + pricesKeeper, + transientStoreKey, + true, + ) + blockTimeKeeper, _ := createBlockTimeKeeper(stateStore, db, cdc) + statsKeeper, _ := createStatsKeeper( + stateStore, + epochsKeeper, + db, + cdc, + ) + vaultKeeper, _ := createVaultKeeper( + stateStore, + db, + cdc, + transientStoreKey, + ) + feeTiersKeeper, _ := createFeeTiersKeeper( + stateStore, + statsKeeper, + vaultKeeper, + db, + cdc, + ) + rewardsKeeper, _ := createRewardsKeeper( + stateStore, + assetsKeeper, + bankKeeper, + feeTiersKeeper, + pricesKeeper, + indexerEventManager, + db, + cdc, + ) + subaccountsKeeper, _ := createSubaccountsKeeper( + stateStore, + db, + cdc, + assetsKeeper, + bankKeeper, + perpetualsKeeper, + blockTimeKeeper, + revShareKeeper, + transientStoreKey, + true, + ) + clobKeeper, _, _ = createClobKeeper( + stateStore, + db, + cdc, + memClob, + assetsKeeper, + blockTimeKeeper, + bankKeeper, + feeTiersKeeper, + perpetualsKeeper, + pricesKeeper, + statsKeeper, + rewardsKeeper, + subaccountsKeeper, + indexerEventManager, + transientStoreKey, + ) + // Create the listing keeper + keeper, storeKey, _ = createListingKeeper( + stateStore, + db, + cdc, + pricesKeeper, + perpetualsKeeper, + clobKeeper, + marketMapKeeper, + ) + + return []GenesisInitializer{keeper} + }, + ) + + return ctx, keeper, storeKey, mockTimeProvider, pricesKeeper, perpetualsKeeper, clobKeeper, marketMapKeeper +} + +func createListingKeeper( + stateStore storetypes.CommitMultiStore, + db *dbm.MemDB, + cdc *codec.ProtoCodec, + pricesKeeper *priceskeeper.Keeper, + perpetualsKeeper *perpetualskeeper.Keeper, + clobKeeper *clobkeeper.Keeper, + marketMapKeeper *marketmapkeeper.Keeper, +) ( + *keeper.Keeper, + storetypes.StoreKey, + *mocks.TimeProvider, +) { + storeKey := storetypes.NewKVStoreKey(types.StoreKey) + stateStore.MountStoreWithDB(storeKey, storetypes.StoreTypeIAVL, db) + mockTimeProvider := &mocks.TimeProvider{} + + k := keeper.NewKeeper( + cdc, + storeKey, + []string{ + lib.GovModuleAddress.String(), + }, + pricesKeeper, + clobKeeper, + marketMapKeeper, + perpetualsKeeper, + ) + + return k, storeKey, mockTimeProvider +} diff --git a/protocol/x/listing/keeper/listing.go b/protocol/x/listing/keeper/listing.go index 1a5c5c3789..bc10bed17a 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/slinky" + sdk "github.com/cosmos/cosmos-sdk/types" gogotypes "github.com/cosmos/gogoproto/types" clobtypes "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" @@ -32,7 +34,6 @@ func (k Keeper) GetMarketsHardCap(ctx sdk.Context) (hardCap uint32) { // Function to wrap the creation of a new market // Note: This will only list long-tail/isolated markets -// TODO (TRA-505): Add tests once market mapper testutils become available func (k Keeper) CreateMarket( ctx sdk.Context, ticker string, @@ -40,10 +41,15 @@ func (k Keeper) CreateMarket( marketId = k.PricesKeeper.AcquireNextMarketID(ctx) // Get market details from marketmap - marketMapDetails, err := k.MarketMapKeeper.GetMarket(ctx, ticker) + // TODO: change to use util from marketmap when available + marketMapPair, err := slinky.MarketPairToCurrencyPair(ticker) if err != nil { return 0, err } + marketMapDetails, err := k.MarketMapKeeper.GetMarket(ctx, marketMapPair.String()) + if err != nil { + return 0, types.ErrMarketNotFound + } // Create a new market market, err := k.PricesKeeper.CreateMarket( @@ -52,9 +58,10 @@ func (k Keeper) CreateMarket( Id: marketId, Pair: ticker, // Set the price exponent to the negative of the number of decimals - Exponent: int32(marketMapDetails.Ticker.Decimals) * -1, - MinExchanges: uint32(marketMapDetails.Ticker.MinProviderCount), - MinPriceChangePpm: types.MinPriceChangePpm_LongTail, + Exponent: int32(marketMapDetails.Ticker.Decimals) * -1, + MinExchanges: uint32(marketMapDetails.Ticker.MinProviderCount), + MinPriceChangePpm: types.MinPriceChangePpm_LongTail, + ExchangeConfigJson: "{}", // Placeholder. TODO (TRA-513): Deprecate this field }, pricestypes.MarketPrice{ Id: marketId, @@ -96,7 +103,6 @@ func (k Keeper) CreateClobPair( // Function to wrap the creation of a new perpetual // Note: This will only list long-tail/isolated markets -// TODO: Add tests pending marketmap testutils func (k Keeper) CreatePerpetual( ctx sdk.Context, marketId uint32, @@ -105,7 +111,12 @@ func (k Keeper) CreatePerpetual( perpetualId = k.PerpetualsKeeper.AcquireNextPerpetualID(ctx) // Get reference price from market map - marketMapDetails, err := k.MarketMapKeeper.GetMarket(ctx, ticker) + // TODO: change to use util from marketmap when available + marketMapPair, err := slinky.MarketPairToCurrencyPair(ticker) + if err != nil { + return 0, err + } + marketMapDetails, err := k.MarketMapKeeper.GetMarket(ctx, marketMapPair.String()) if err != nil { return 0, err } diff --git a/protocol/x/listing/keeper/listing_test.go b/protocol/x/listing/keeper/listing_test.go new file mode 100644 index 0000000000..2679aa68c9 --- /dev/null +++ b/protocol/x/listing/keeper/listing_test.go @@ -0,0 +1,182 @@ +package keeper_test + +import ( + "errors" + "testing" + + 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" + "github.com/skip-mev/slinky/x/marketmap/types/tickermetadata" + + pricestypes "github.com/dydxprotocol/v4-chain/protocol/x/prices/types" + + "github.com/dydxprotocol/v4-chain/protocol/x/listing/types" + + "github.com/dydxprotocol/v4-chain/protocol/mocks" + keepertest "github.com/dydxprotocol/v4-chain/protocol/testutil/keeper" + "github.com/stretchr/testify/require" +) + +func TestCreateMarket(t *testing.T) { + tests := map[string]struct { + ticker string + duplicateMarket bool + + expectedErr error + }{ + "success": { + ticker: "TEST-USD", + expectedErr: nil, + }, + "failure - market not found": { + ticker: "INVALID-USD", + expectedErr: types.ErrMarketNotFound, + }, + "failure - duplicate market": { + ticker: "BTC-USD", + expectedErr: pricestypes.ErrMarketParamPairAlreadyExists, + }, + } + + for name, tc := range tests { + t.Run( + name, func(t *testing.T) { + mockIndexerEventManager := &mocks.IndexerEventManager{} + ctx, keeper, _, _, pricesKeeper, _, _, marketMapKeeper := keepertest.ListingKeepers( + t, + &mocks.BankKeeper{}, + mockIndexerEventManager, + ) + + keepertest.CreateTestMarkets(t, ctx, pricesKeeper) + + testMarketParams := pricestypes.MarketParam{ + Pair: "TEST-USD", + Exponent: int32(-6), + ExchangeConfigJson: `{"test_config_placeholder":{}}`, + MinExchanges: 2, + MinPriceChangePpm: uint32(800), + } + + keepertest.CreateMarketsInMarketMapFromParams( + t, + ctx, + marketMapKeeper, + []pricestypes.MarketParam{ + testMarketParams, + }, + ) + + marketId, err := keeper.CreateMarket(ctx, tc.ticker) + if tc.expectedErr != nil { + require.ErrorContains(t, err, tc.expectedErr.Error()) + } else { + require.NoError(t, err) + + // Check if the market was created + market, exists := pricesKeeper.GetMarketParam(ctx, marketId) + require.True(t, exists) + require.Equal(t, testMarketParams.Pair, market.Pair) + require.Equal(t, testMarketParams.Exponent, market.Exponent) + require.Equal(t, testMarketParams.MinExchanges, market.MinExchanges) + require.Equal(t, testMarketParams.MinPriceChangePpm, types.MinPriceChangePpm_LongTail) + } + }, + ) + } +} + +func TestCreatePerpetual(t *testing.T) { + tests := map[string]struct { + ticker string + referencePrice uint64 + + expectedErr error + }{ + "success": { + ticker: "TEST-USD", + referencePrice: 1000000000, // $1000 + expectedErr: nil, + }, + "failure - reference price 0": { + ticker: "TEST-USD", + referencePrice: 0, + expectedErr: types.ErrReferencePriceZero, + }, + "failure - market not found": { + ticker: "INVALID-USD", + expectedErr: types.ErrMarketNotFound, + }, + } + + for name, tc := range tests { + t.Run( + name, func(t *testing.T) { + mockIndexerEventManager := &mocks.IndexerEventManager{} + ctx, keeper, _, _, pricesKeeper, perpetualsKeeper, _, marketMapKeeper := keepertest.ListingKeepers( + t, + &mocks.BankKeeper{}, + mockIndexerEventManager, + ) + keepertest.CreateLiquidityTiersAndNPerpetuals(t, ctx, perpetualsKeeper, pricesKeeper, 10) + + // Create a marketmap with a single market + dydxMetadata, err := tickermetadata.MarshalDyDx( + tickermetadata.DyDx{ + ReferencePrice: tc.referencePrice, + 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) + if errors.Is(tc.expectedErr, types.ErrMarketNotFound) { + require.ErrorContains(t, err, tc.expectedErr.Error()) + return + } + + perpetualId, err := keeper.CreatePerpetual(ctx, marketId, tc.ticker) + if tc.expectedErr != nil { + require.Error(t, err) + } else { + require.NoError(t, err) + + // Check if the perpetual was created + perpetual, err := perpetualsKeeper.GetPerpetual(ctx, perpetualId) + require.NoError(t, err) + require.Equal(t, uint32(10), perpetual.GetId()) + require.Equal(t, marketId, perpetual.Params.MarketId) + require.Equal(t, tc.ticker, perpetual.Params.Ticker) + // Expected resolution = -6 - Floor(log10(1000000000)) = -15 + require.Equal(t, int32(-15), perpetual.Params.AtomicResolution) + require.Equal(t, int32(types.DefaultFundingPpm), perpetual.Params.DefaultFundingPpm) + require.Equal(t, uint32(types.LiquidityTier_LongTail), perpetual.Params.LiquidityTier) + require.Equal( + t, perpetualtypes.PerpetualMarketType_PERPETUAL_MARKET_TYPE_ISOLATED, + perpetual.Params.MarketType, + ) + } + }, + ) + } +} diff --git a/protocol/x/listing/module.go b/protocol/x/listing/module.go index 79d2a6c3a4..07f2fdef9a 100644 --- a/protocol/x/listing/module.go +++ b/protocol/x/listing/module.go @@ -98,16 +98,28 @@ func (AppModuleBasic) GetQueryCmd() *cobra.Command { type AppModule struct { AppModuleBasic - keeper keeper.Keeper + keeper keeper.Keeper + pricesKeeper types.PricesKeeper + clobKeeper types.ClobKeeper + marketMapKeeper types.MarketMapKeeper + perpetualsKeeper types.PerpetualsKeeper } func NewAppModule( cdc codec.Codec, keeper keeper.Keeper, + pricesKeeper types.PricesKeeper, + clobKeeper types.ClobKeeper, + marketMapKeeper types.MarketMapKeeper, + perpetualsKeeper types.PerpetualsKeeper, ) AppModule { return AppModule{ - AppModuleBasic: NewAppModuleBasic(cdc), - keeper: keeper, + AppModuleBasic: NewAppModuleBasic(cdc), + keeper: keeper, + pricesKeeper: pricesKeeper, + clobKeeper: clobKeeper, + marketMapKeeper: marketMapKeeper, + perpetualsKeeper: perpetualsKeeper, } } diff --git a/protocol/x/listing/types/errors.go b/protocol/x/listing/types/errors.go index 4476f39da7..3bad6b07b5 100644 --- a/protocol/x/listing/types/errors.go +++ b/protocol/x/listing/types/errors.go @@ -4,9 +4,15 @@ import errorsmod "cosmossdk.io/errors" var ( // Add x/listing specific errors here - ErrReferencePriceZero = errorsmod.Register( + ErrMarketNotFound = errorsmod.Register( ModuleName, 1, + "market not found", + ) + + ErrReferencePriceZero = errorsmod.Register( + ModuleName, + 2, "reference price is zero", ) )