Skip to content

Commit

Permalink
Merge branch 'jakob-dydx/final-settlement-endblocker' into jakob-dydx…
Browse files Browse the repository at this point in the history
…/final-settlement-deliver-tx
  • Loading branch information
jakob-dydx committed Dec 2, 2023
2 parents 7163576 + 57b747c commit fef60bf
Show file tree
Hide file tree
Showing 6 changed files with 311 additions and 17 deletions.
22 changes: 14 additions & 8 deletions protocol/x/clob/keeper/clob_pair.go
Original file line number Diff line number Diff line change
Expand Up @@ -468,13 +468,19 @@ func (k Keeper) UpdateClobPair(

oldStatus := oldClobPair.Status
newStatus := clobPair.Status
if oldStatus != newStatus && !types.IsSupportedClobPairStatusTransition(oldStatus, newStatus) {
return errorsmod.Wrapf(
types.ErrInvalidClobPairStatusTransition,
"Cannot transition from status %+v to status %+v",
oldStatus,
newStatus,
)
if oldStatus != newStatus {
if !types.IsSupportedClobPairStatusTransition(oldStatus, newStatus) {
return errorsmod.Wrapf(
types.ErrInvalidClobPairStatusTransition,
"Cannot transition from status %+v to status %+v",
oldStatus,
newStatus,
)
}

if newStatus == types.ClobPair_STATUS_FINAL_SETTLEMENT {
k.mustEnterFinalSettlement(ctx, clobPair.GetClobPairId())
}
}

if err := k.validateClobPair(ctx, &clobPair); err != nil {
Expand Down Expand Up @@ -624,7 +630,7 @@ func (k Keeper) IsPerpetualClobPairActive(
if !found {
return false, errorsmod.Wrapf(
types.ErrInvalidClob,
"GetPerpetualClobPairStatus: did not find clob pair with id = %d",
"IsPerpetualClobPairActive: did not find clob pair with id = %d",
clobPairId,
)
}
Expand Down
117 changes: 117 additions & 0 deletions protocol/x/clob/keeper/clob_pair_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

indexerevents "github.com/dydxprotocol/v4-chain/protocol/indexer/events"
"github.com/dydxprotocol/v4-chain/protocol/indexer/indexer_manager"
indexershared "github.com/dydxprotocol/v4-chain/protocol/indexer/shared"
"github.com/dydxprotocol/v4-chain/protocol/lib"

"github.com/cosmos/cosmos-sdk/codec"
Expand All @@ -19,6 +20,7 @@ import (
"github.com/dydxprotocol/v4-chain/protocol/testutil/nullify"
perptest "github.com/dydxprotocol/v4-chain/protocol/testutil/perpetuals"
pricestest "github.com/dydxprotocol/v4-chain/protocol/testutil/prices"
"github.com/dydxprotocol/v4-chain/protocol/x/clob/keeper"
"github.com/dydxprotocol/v4-chain/protocol/x/clob/memclob"
"github.com/dydxprotocol/v4-chain/protocol/x/clob/types"
"github.com/dydxprotocol/v4-chain/protocol/x/perpetuals"
Expand Down Expand Up @@ -597,6 +599,121 @@ func TestClobPairGetAll(t *testing.T) {
)
}

func TestUpdateClobPair_FinalSettlement(t *testing.T) {
memClob := memclob.NewMemClobPriceTimePriority(false)
mockIndexerEventManager := &mocks.IndexerEventManager{}
ks := keepertest.NewClobKeepersTestContext(t, memClob, &mocks.BankKeeper{}, mockIndexerEventManager)
prices.InitGenesis(ks.Ctx, *ks.PricesKeeper, constants.Prices_DefaultGenesisState)
perpetuals.InitGenesis(ks.Ctx, *ks.PerpetualsKeeper, constants.Perpetuals_DefaultGenesisState)

clobPair := constants.ClobPair_Btc
mockIndexerEventManager.On("AddTxnEvent",
ks.Ctx,
indexerevents.SubtypePerpetualMarket,
indexerevents.PerpetualMarketEventVersion,
indexer_manager.GetBytes(
indexerevents.NewPerpetualMarketCreateEvent(
0,
0,
constants.Perpetuals_DefaultGenesisState.Perpetuals[0].Params.Ticker,
constants.Perpetuals_DefaultGenesisState.Perpetuals[0].Params.MarketId,
types.ClobPair_STATUS_ACTIVE,
clobPair.QuantumConversionExponent,
constants.Perpetuals_DefaultGenesisState.Perpetuals[0].Params.AtomicResolution,
clobPair.SubticksPerTick,
clobPair.StepBaseQuantums,
constants.Perpetuals_DefaultGenesisState.Perpetuals[0].Params.LiquidityTier,
),
),
).Once().Return()

_, err := ks.ClobKeeper.CreatePerpetualClobPair(
ks.Ctx,
clobPair.Id,
clobtest.MustPerpetualId(clobPair),
satypes.BaseQuantums(clobPair.StepBaseQuantums),
clobPair.QuantumConversionExponent,
clobPair.SubticksPerTick,
types.ClobPair_STATUS_ACTIVE,
)
require.NoError(t, err)

mockIndexerEventManager.On("AddTxnEvent",
ks.Ctx,
indexerevents.SubtypeUpdateClobPair,
indexerevents.UpdateClobPairEventVersion,
indexer_manager.GetBytes(
indexerevents.NewUpdateClobPairEvent(
clobPair.GetClobPairId(),
types.ClobPair_STATUS_FINAL_SETTLEMENT,
clobPair.QuantumConversionExponent,
types.SubticksPerTick(clobPair.GetSubticksPerTick()),
satypes.BaseQuantums(clobPair.GetStepBaseQuantums()),
),
),
).Once().Return()

statefulOrders := []types.Order{
constants.LongTermOrder_Alice_Num0_Id0_Clob0_Buy100_Price10_GTBT15,
constants.LongTermOrder_Bob_Num0_Id0_Clob0_Buy25_Price30_GTBT10,
constants.LongTermOrder_Alice_Num0_Id0_Clob1_Buy5_Price10_GTBT5, // different clob pair
}
for _, order := range statefulOrders {
ks.ClobKeeper.SetLongTermOrderPlacement(ks.Ctx, order, 5)
ks.ClobKeeper.MustAddOrderToStatefulOrdersTimeSlice(
ks.Ctx,
order.MustGetUnixGoodTilBlockTime(),
order.GetOrderId(),
)

if order.OrderId.ClobPairId == 0 {
mockIndexerEventManager.On("AddTxnEvent",
ks.Ctx,
indexerevents.SubtypeStatefulOrder,
indexerevents.StatefulOrderEventVersion,
indexer_manager.GetBytes(
indexerevents.NewStatefulOrderRemovalEvent(
order.OrderId,
indexershared.OrderRemovalReason_ORDER_REMOVAL_REASON_FINAL_SETTLEMENT,
),
),
).Once().Return()
}
}

ks.ClobKeeper.UntriggeredConditionalOrders = map[types.ClobPairId]*keeper.UntriggeredConditionalOrders{
0: {}, // leaving blank, orders here don't matter in particular since we clear the whole key
}

clobPair.Status = types.ClobPair_STATUS_FINAL_SETTLEMENT
err = ks.ClobKeeper.UpdateClobPair(ks.Ctx, clobPair)
require.NoError(t, err)

// Verify indexer expectations.
mockIndexerEventManager.AssertExpectations(t)

// Verify stateful orders are removed from state.
for _, order := range statefulOrders {
_, found := ks.ClobKeeper.GetLongTermOrderPlacement(ks.Ctx, order.OrderId)
require.Equal(t, order.OrderId.ClobPairId != 0, found)
}

// Verify ProcessProposerMatchesEvents.RemovedStatefulOrderIds is populated correctly.
ppme := ks.ClobKeeper.GetProcessProposerMatchesEvents(ks.Ctx)
require.Equal(
t,
[]types.OrderId{
constants.LongTermOrder_Alice_Num0_Id0_Clob0_Buy100_Price10_GTBT15.OrderId,
constants.LongTermOrder_Bob_Num0_Id0_Clob0_Buy25_Price30_GTBT10.OrderId,
},
ppme.RemovedStatefulOrderIds,
)

// Verify UntriggeredConditionalOrders is cleared.
_, found := ks.ClobKeeper.UntriggeredConditionalOrders[0]
require.False(t, found)
}

func TestUpdateClobPair(t *testing.T) {
testCases := map[string]struct {
setup func(t *testing.T, ks keepertest.ClobKeepersTestContext, manager *mocks.IndexerEventManager)
Expand Down
70 changes: 70 additions & 0 deletions protocol/x/clob/keeper/final_settlement.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package keeper

import (
sdk "github.com/cosmos/cosmos-sdk/types"
indexerevents "github.com/dydxprotocol/v4-chain/protocol/indexer/events"
"github.com/dydxprotocol/v4-chain/protocol/indexer/indexer_manager"
indexershared "github.com/dydxprotocol/v4-chain/protocol/indexer/shared"
"github.com/dydxprotocol/v4-chain/protocol/x/clob/types"
)

// MustEnterFinalSettlement holds logic executed when a market transitions to FINAL_SETTLEMENT status.
// This function will forcefully cancel all stateful open orders for the clob pair.
func (k Keeper) mustEnterFinalSettlement(ctx sdk.Context, clobPairId types.ClobPairId) {
// Forcefully cancel all stateful orders from state for this clob pair
k.mustCancelStatefulOrdersForFinalSettlement(ctx, clobPairId)

// Delete untriggered conditional orders for this clob pair from memory
delete(k.UntriggeredConditionalOrders, clobPairId)
}

// mustCancelStatefulOrdersForFinalSettlement forcefully cancels all stateful orders
// for the provided ClobPair. These orders will be removed from the memclob in PrepareCheckState.
func (k Keeper) mustCancelStatefulOrdersForFinalSettlement(ctx sdk.Context, clobPairId types.ClobPairId) {
statefulOrders := k.GetAllStatefulOrders(ctx)
processProposerMatchesEvents := k.GetProcessProposerMatchesEvents(ctx)

// This logic is executed in EndBlocker and should not panic. This would be unexpected,
// but if it happens we would rather recover and continue if an order fails to be removed from state
// rather than halt the chain.
safelyRemoveStatefulOrder := func(ctx sdk.Context, orderId types.OrderId) {
defer func() {
if r := recover(); r != nil {
k.Logger(ctx).Error(
"mustCancelStatefulOrdersForFinalSettlement: Failed to remove stateful order with OrderId %+v: %v",
orderId,
r,
)
}
}()
k.MustRemoveStatefulOrder(ctx, orderId)
}

for _, order := range statefulOrders {
if order.GetClobPairId() == clobPairId {
// Remove from state, recovering from panic if necessary
safelyRemoveStatefulOrder(ctx, order.OrderId)

// Append to RemovedStatefulOrderIds so this order gets removed
// from the memclob in PrepareCheckState during the PurgeInvalidMemclobState step
processProposerMatchesEvents.RemovedStatefulOrderIds = append(
processProposerMatchesEvents.RemovedStatefulOrderIds,
order.OrderId,
)

k.GetIndexerEventManager().AddTxnEvent(
ctx,
indexerevents.SubtypeStatefulOrder,
indexerevents.StatefulOrderEventVersion,
indexer_manager.GetBytes(
indexerevents.NewStatefulOrderRemovalEvent(
order.OrderId,
indexershared.OrderRemovalReason_ORDER_REMOVAL_REASON_FINAL_SETTLEMENT,
),
),
)
}
}

k.MustSetProcessProposerMatchesEvents(ctx, processProposerMatchesEvents)
}
45 changes: 43 additions & 2 deletions protocol/x/clob/keeper/process_operations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1437,7 +1437,7 @@ func TestProcessProposerOperations(t *testing.T) {
rawOperations: []types.OperationRaw{
clobtest.NewOrderRemovalOperationRaw(
constants.LongTermOrder_Bob_Num0_Id0_Clob0_Buy25_Price30_GTBT10.OrderId,
types.OrderRemoval_REMOVAL_REASON_FULLY_FILLED,
types.OrderRemoval_REMOVAL_REASON_INVALID_SELF_TRADE,
),
},
expectedError: types.ErrOperationConflictsWithClobPairStatus,
Expand Down Expand Up @@ -1505,8 +1505,49 @@ func TestProcessProposerOperations(t *testing.T) {
},
expectedError: types.ErrOperationConflictsWithClobPairStatus,
},
"Succeeds with ClobMatch_MatchPerpetualDeleveraging, IsFinalSettlement is false for market in final settlement": {
perpetuals: []*perptypes.Perpetual{
&constants.BtcUsd_100PercentMarginRequirement,
},
perpetualFeeParams: &constants.PerpetualFeeParams,
clobPairs: []types.ClobPair{
constants.ClobPair_Btc_Final_Settlement,
},
subaccounts: []satypes.Subaccount{
// liquidatable: MMR = $5000, TNC = $499
constants.Carl_Num0_1BTC_Short_50499USD,
constants.Dave_Num0_1BTC_Long_50000USD,
},
marketIdToOraclePriceOverride: map[uint32]uint64{
constants.BtcUsd.MarketId: 5_050_000_000, // $50,500 / BTC
},
rawOperations: []types.OperationRaw{
clobtest.NewMatchOperationRawFromPerpetualDeleveragingLiquidation(
types.MatchPerpetualDeleveraging{
Liquidated: constants.Carl_Num0,
PerpetualId: 0,
Fills: []types.MatchPerpetualDeleveraging_Fill{
{
OffsettingSubaccountId: constants.Dave_Num0,
FillAmount: 100_000_000,
},
},
},
),
},
expectedProcessProposerMatchesEvents: types.ProcessProposerMatchesEvents{
BlockHeight: blockHeight,
},
expectedQuoteBalances: map[satypes.SubaccountId]int64{
constants.Carl_Num0: 0,
constants.Dave_Num0: constants.Usdc_Asset_100_499.GetBigQuantums().Int64(),
},
expectedPerpetualPositions: map[satypes.SubaccountId][]*satypes.PerpetualPosition{
constants.Carl_Num0: {},
constants.Dave_Num0: {},
},
},
// "Succeeds with ClobMatch_MatchPerpetualDeleveraging, IsFinalSettlement is true for market in final settlement": {},
// "Succeeds with ClobMatch_MatchPerpetualDeleveraging, IsFinalSettlement is false for market in final settlement": {},
}

for name, tc := range tests {
Expand Down
10 changes: 8 additions & 2 deletions protocol/x/clob/types/clob_pair.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@ import (
// may be transitioned to from this state. Note the keys of this map may be
// a subset of the types defined in the proto for ClobPair_Status.
var SupportedClobPairStatusTransitions = map[ClobPair_Status]map[ClobPair_Status]struct{}{
ClobPair_STATUS_ACTIVE: {},
ClobPair_STATUS_ACTIVE: {
ClobPair_STATUS_FINAL_SETTLEMENT: struct{}{},
},
ClobPair_STATUS_INITIALIZING: {
ClobPair_STATUS_ACTIVE: struct{}{},
ClobPair_STATUS_ACTIVE: struct{}{},
ClobPair_STATUS_FINAL_SETTLEMENT: struct{}{},
},
ClobPair_STATUS_FINAL_SETTLEMENT: {
ClobPair_STATUS_INITIALIZING: struct{}{},
},
}

Expand Down
Loading

0 comments on commit fef60bf

Please sign in to comment.