diff --git a/protocol/app/upgrades/v9.6/upgrade_container_test.go b/protocol/app/upgrades/v9.6/upgrade_container_test.go index 64b943c61b..ab2b64c402 100644 --- a/protocol/app/upgrades/v9.6/upgrade_container_test.go +++ b/protocol/app/upgrades/v9.6/upgrade_container_test.go @@ -1,3 +1,5 @@ +//go:build all || container_test + package v_9_6_test import ( diff --git a/protocol/mocks/MemClob.go b/protocol/mocks/MemClob.go index 60cb6aca69..a340e0145e 100644 --- a/protocol/mocks/MemClob.go +++ b/protocol/mocks/MemClob.go @@ -608,6 +608,11 @@ func (_m *MemClob) SetMemclobGauges(ctx types.Context) { _m.Called(ctx) } +// SyncOrderbookState provides a mock function with given fields: clobPair +func (_m *MemClob) SyncOrderbookState(clobPair clobtypes.ClobPair) { + _m.Called(clobPair) +} + // NewMemClob creates a new instance of MemClob. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMemClob(t interface { diff --git a/protocol/x/clob/keeper/clob_pair.go b/protocol/x/clob/keeper/clob_pair.go index 6391bee34c..811212e3b6 100644 --- a/protocol/x/clob/keeper/clob_pair.go +++ b/protocol/x/clob/keeper/clob_pair.go @@ -563,6 +563,7 @@ func (k Keeper) UpdateClobPair( ctx sdk.Context, clobPair types.ClobPair, ) error { + shouldSyncMemClobState := false oldClobPair, found := k.GetClobPair(ctx, types.ClobPairId(clobPair.Id)) if !found { return errorsmod.Wrapf( @@ -606,6 +607,7 @@ func (k Keeper) UpdateClobPair( newSBQ, ) } + shouldSyncMemClobState = true } // Only allow decreasing SubticksPerTick; it must remain positive. @@ -620,6 +622,7 @@ func (k Keeper) UpdateClobPair( newSPT, ) } + shouldSyncMemClobState = true } if clobPair.QuantumConversionExponent != oldClobPair.QuantumConversionExponent { @@ -646,6 +649,10 @@ func (k Keeper) UpdateClobPair( k.SetClobPair(ctx, clobPair) + if shouldSyncMemClobState { + k.MemClob.SyncOrderbookState(clobPair) + } + // Send UpdateClobPair to indexer. k.GetIndexerEventManager().AddTxnEvent( ctx, diff --git a/protocol/x/clob/keeper/clob_pair_test.go b/protocol/x/clob/keeper/clob_pair_test.go index 8f8955f443..30b211b69c 100644 --- a/protocol/x/clob/keeper/clob_pair_test.go +++ b/protocol/x/clob/keeper/clob_pair_test.go @@ -1206,3 +1206,50 @@ func TestAcquireNextClobPairID(t *testing.T) { nextClobPairID = ks.ClobKeeper.AcquireNextClobPairID(ks.Ctx) require.Equal(t, nextClobPairIDFromStore+1, nextClobPairID) } + +func TestSyncOrderbookState_SucceedsAndUpdatesValues(t *testing.T) { + // Use keeper to verify memclob is synced by behavior after update. + memClob := memclob.NewMemClobPriceTimePriority(false) + ks := keepertest.NewClobKeepersTestContext(t, memClob, nil, indexer_manager.NewIndexerEventManagerNoop()) + + ks.MarketMapKeeper.InitGenesis(ks.Ctx, constants.MarketMap_DefaultGenesisState) + prices.InitGenesis(ks.Ctx, *ks.PricesKeeper, constants.Prices_DefaultGenesisState) + perpetuals.InitGenesis(ks.Ctx, *ks.PerpetualsKeeper, constants.Perpetuals_DefaultGenesisState) + + // Create initial clob pair (defaults to 5/5 for BTC). + clobPair := constants.ClobPair_Btc + _, err := ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs( + ks.Ctx, + clobPair.Id, + clobPair.MustGetPerpetualId(), + clobPair.GetClobPairMinOrderBaseQuantums(), + clobPair.QuantumConversionExponent, + uint32(clobPair.GetClobPairSubticksPerTick()), + clobPair.Status, + ) + require.NoError(t, err) + + // Update to valid divisors and smaller values. + clobPair.StepBaseQuantums = 1 + clobPair.SubticksPerTick = 1 + err = ks.ClobKeeper.UpdateClobPair(ks.Ctx, clobPair) + require.NoError(t, err) + + // Place a short-term order that would fail under old memclob params but passes after sync. + nextBlock := uint32(ks.Ctx.BlockHeight() + int64(types.ShortBlockWindow)) + order := types.Order{ + OrderId: types.OrderId{ + SubaccountId: constants.Alice_Num0, + ClientId: 7, + ClobPairId: clobPair.Id, + }, + Side: types.Order_SIDE_BUY, + Quantums: 1, // valid after MinOrderBaseQuantums=1 + Subticks: 1, // valid after SubticksPerTick=1 + GoodTilOneof: &types.Order_GoodTilBlock{GoodTilBlock: nextBlock}, + } + ctx := ks.Ctx.WithIsCheckTx(true) + _, status, err := ks.ClobKeeper.PlaceShortTermOrder(ctx, types.NewMsgPlaceOrder(order)) + require.NoError(t, err) + require.Equal(t, types.Success, status) +} diff --git a/protocol/x/clob/keeper/msg_server_update_clob_pair_test.go b/protocol/x/clob/keeper/msg_server_update_clob_pair_test.go index 13cb8fc3f5..63aca7477b 100644 --- a/protocol/x/clob/keeper/msg_server_update_clob_pair_test.go +++ b/protocol/x/clob/keeper/msg_server_update_clob_pair_test.go @@ -89,6 +89,7 @@ func TestMsgServerUpdateClobPair(t *testing.T) { }, }, setup: func(ks keepertest.ClobKeepersTestContext, mockIndexerEventManager *mocks.IndexerEventManager) { + ks.ClobKeeper.MemClob.CreateOrderbook(constants.ClobPair_Btc) cdc := codec.NewProtoCodec(module.InterfaceRegistry) store := prefix.NewStore(ks.Ctx.KVStore(ks.StoreKey), []byte(types.ClobPairKeyPrefix)) // Existing clob pair with StepBaseQuantums = 5 and status initializing. @@ -256,6 +257,7 @@ func TestMsgServerUpdateClobPair(t *testing.T) { }, setup: func(ks keepertest.ClobKeepersTestContext, mockIndexerEventManager *mocks.IndexerEventManager) { // write default btc clob pair to state (initializing, SPT=5) + ks.ClobKeeper.MemClob.CreateOrderbook(constants.ClobPair_Btc) cdc := codec.NewProtoCodec(module.InterfaceRegistry) store := prefix.NewStore(ks.Ctx.KVStore(ks.StoreKey), []byte(types.ClobPairKeyPrefix)) clobPair := constants.ClobPair_Btc diff --git a/protocol/x/clob/memclob/memclob.go b/protocol/x/clob/memclob/memclob.go index cd7cbb607f..2080a3d414 100644 --- a/protocol/x/clob/memclob/memclob.go +++ b/protocol/x/clob/memclob/memclob.go @@ -167,6 +167,46 @@ func (m *MemClobPriceTimePriority) MaybeCreateOrderbook( return true } +// SyncOrderbookState syncs the subticks per tick and step base quantums for the memclob with the ClobPair state. +// This is used to ensure the existing memclob has the up to date params after modifying the ClobPair +// subticks per tick/step base quantums in state. +func (m *MemClobPriceTimePriority) SyncOrderbookState( + clobPair types.ClobPair, +) { + clobPairId := clobPair.GetClobPairId() + orderbook, exists := m.orderbooks[clobPairId] + if !exists { + panic(fmt.Sprintf("SyncOrderbookState: Orderbook for ClobPair ID %d does not exist", clobPairId)) + } + + subticksPerTick := clobPair.GetClobPairSubticksPerTick() + if subticksPerTick == 0 { + panic("subticksPerTick must be greater than zero") + } + // if subticksPerTick is increased or not a divisor of the previous subticksPerTick, then we're screwed + // since this is registered in state. + if subticksPerTick > orderbook.SubticksPerTick || + orderbook.SubticksPerTick%subticksPerTick != 0 { + panic(fmt.Sprintf("clob %d SubticksPerTick increased and/or not a divisor of the previous", + clobPairId)) + } + + minOrderBaseQuantums := clobPair.GetClobPairMinOrderBaseQuantums() + if minOrderBaseQuantums == 0 { + panic("minOrderBaseQuantums must be greater than zero") + } + // if minOrderBaseQuantums is increased or not a divisor of the previous minOrderBaseQuantums, then we're screwed + // since this is registered in state. + if minOrderBaseQuantums > orderbook.MinOrderBaseQuantums || + orderbook.MinOrderBaseQuantums%minOrderBaseQuantums != 0 { + panic(fmt.Sprintf("clob %d stepBaseQuantums increased and/or not a divisor of the previous", + clobPairId)) + } + + orderbook.SubticksPerTick = subticksPerTick + orderbook.MinOrderBaseQuantums = minOrderBaseQuantums +} + // CreateOrderbook is used for updating memclob internal data structures to mark an orderbook as created. // This function will panic if `clobPairId` already exists in any of the memclob's internal data structures. func (m *MemClobPriceTimePriority) CreateOrderbook( diff --git a/protocol/x/clob/memclob/memclob_sync_orderbook_state_test.go b/protocol/x/clob/memclob/memclob_sync_orderbook_state_test.go new file mode 100644 index 0000000000..e20075630b --- /dev/null +++ b/protocol/x/clob/memclob/memclob_sync_orderbook_state_test.go @@ -0,0 +1,116 @@ +package memclob + +import ( + "testing" + + "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" + "github.com/stretchr/testify/require" +) + +// helper to create a minimal perpetual ClobPair with desired params. +func newPerpClobPair(id uint32, subticksPerTick uint32, stepBaseQuantums uint64) types.ClobPair { + return types.ClobPair{ + Id: id, + SubticksPerTick: subticksPerTick, + StepBaseQuantums: stepBaseQuantums, + Metadata: &types.ClobPair_PerpetualClobMetadata{ + PerpetualClobMetadata: &types.PerpetualClobMetadata{ + PerpetualId: id, + }, + }, + } +} + +func TestSyncOrderbookState_PanicsWhenOrderbookMissing(t *testing.T) { + mem := NewMemClobPriceTimePriority(false) + clobPair := newPerpClobPair(1, 100, 1000) + require.Panics(t, func() { + mem.SyncOrderbookState(clobPair) + }) +} + +func TestSyncOrderbookState_PanicsWhenSubticksPerTickZero(t *testing.T) { + mem := NewMemClobPriceTimePriority(false) + initial := newPerpClobPair(1, 100, 1000) + mem.CreateOrderbook(initial) + + update := newPerpClobPair(1, 0, 1000) + require.Panics(t, func() { + mem.SyncOrderbookState(update) + }) +} + +func TestSyncOrderbookState_PanicsWhenSubticksPerTickIncreased(t *testing.T) { + mem := NewMemClobPriceTimePriority(false) + initial := newPerpClobPair(1, 100, 1000) + mem.CreateOrderbook(initial) + + // Increase from 100 -> 200 should panic. + update := newPerpClobPair(1, 200, 1000) + require.Panics(t, func() { + mem.SyncOrderbookState(update) + }) +} + +func TestSyncOrderbookState_PanicsWhenSubticksPerTickNotDivisor(t *testing.T) { + mem := NewMemClobPriceTimePriority(false) + initial := newPerpClobPair(1, 100, 1000) + mem.CreateOrderbook(initial) + + // 100 % 30 != 0 should panic even though decreased. + update := newPerpClobPair(1, 30, 1000) + require.Panics(t, func() { + mem.SyncOrderbookState(update) + }) +} + +func TestSyncOrderbookState_PanicsWhenMinOrderBaseQuantumsZero(t *testing.T) { + mem := NewMemClobPriceTimePriority(false) + initial := newPerpClobPair(1, 100, 1000) + mem.CreateOrderbook(initial) + + update := newPerpClobPair(1, 100, 0) + require.Panics(t, func() { + mem.SyncOrderbookState(update) + }) +} + +func TestSyncOrderbookState_PanicsWhenMinOrderBaseQuantumsIncreased(t *testing.T) { + mem := NewMemClobPriceTimePriority(false) + initial := newPerpClobPair(1, 100, 1000) + mem.CreateOrderbook(initial) + + // Increase from 1000 -> 2000 should panic. + update := newPerpClobPair(1, 100, 2000) + require.Panics(t, func() { + mem.SyncOrderbookState(update) + }) +} + +func TestSyncOrderbookState_PanicsWhenMinOrderBaseQuantumsNotDivisor(t *testing.T) { + mem := NewMemClobPriceTimePriority(false) + initial := newPerpClobPair(1, 100, 1000) + mem.CreateOrderbook(initial) + + // 1000 % 300 != 0 should panic even though decreased. + update := newPerpClobPair(1, 100, 300) + require.Panics(t, func() { + mem.SyncOrderbookState(update) + }) +} + +func TestSyncOrderbookState_SucceedsAndUpdatesValues(t *testing.T) { + mem := NewMemClobPriceTimePriority(false) + initial := newPerpClobPair(1, 100, 1000) + mem.CreateOrderbook(initial) + + // Valid decreases to positive divisors. + update := newPerpClobPair(1, 50, 200) + require.NotPanics(t, func() { + mem.SyncOrderbookState(update) + }) + + ob := mem.orderbooks[update.GetClobPairId()] + require.Equal(t, types.SubticksPerTick(50), ob.SubticksPerTick) + require.Equal(t, ob.MinOrderBaseQuantums, update.GetClobPairMinOrderBaseQuantums()) +} diff --git a/protocol/x/clob/types/memclob.go b/protocol/x/clob/types/memclob.go index 2c55eba839..eebecde456 100644 --- a/protocol/x/clob/types/memclob.go +++ b/protocol/x/clob/types/memclob.go @@ -150,4 +150,7 @@ type MemClob interface { takerOrder MatchableOrder, makerOrders []Order, ) StreamOrderbookFill + SyncOrderbookState( + clobPair ClobPair, + ) }