Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions protocol/app/upgrades/v9.6/upgrade_container_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build all || container_test

package v_9_6_test

import (
Expand Down
5 changes: 5 additions & 0 deletions protocol/mocks/MemClob.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions protocol/x/clob/keeper/clob_pair.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -606,6 +607,7 @@ func (k Keeper) UpdateClobPair(
newSBQ,
)
}
shouldSyncMemClobState = true
}

// Only allow decreasing SubticksPerTick; it must remain positive.
Expand All @@ -620,6 +622,7 @@ func (k Keeper) UpdateClobPair(
newSPT,
)
}
shouldSyncMemClobState = true
}

if clobPair.QuantumConversionExponent != oldClobPair.QuantumConversionExponent {
Expand All @@ -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,
Expand Down
47 changes: 47 additions & 0 deletions protocol/x/clob/keeper/clob_pair_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
2 changes: 2 additions & 0 deletions protocol/x/clob/keeper/msg_server_update_clob_pair_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions protocol/x/clob/memclob/memclob.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
116 changes: 116 additions & 0 deletions protocol/x/clob/memclob/memclob_sync_orderbook_state_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
3 changes: 3 additions & 0 deletions protocol/x/clob/types/memclob.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,7 @@ type MemClob interface {
takerOrder MatchableOrder,
makerOrders []Order,
) StreamOrderbookFill
SyncOrderbookState(
clobPair ClobPair,
)
}
Loading