From be4ba5475a868b4b1c11fbab9eeb4f05482eb8cf Mon Sep 17 00:00:00 2001 From: Jonathan Fung <121899091+jonfung-dydx@users.noreply.github.com> Date: Tue, 19 Mar 2024 11:27:50 -0400 Subject: [PATCH] [CT-515] Combine place, cancel, batch cancel rate limiters (#1165) * combine place and cancel rate limiters, add batch cancel * fixups * remove cancel rate limit genesis * fix json files for genesis * update sample pregenesis * get tests to work * update protos to deprecate instead of remove * lint fix * empty commit * fix tests * lint * basic tests with only batch cancel orders submitted * fix cli rate limit test * e2e tests * rate limit 2 -> 3 * more explicit replay test * todo comment * linty lint * constants for the tests * rate limit proto deprecate => reserved * sample pregenesis update * indexer proto regen * fix tests * fix test * upgrade handler for batch cancel configs * upgrade handler * proto swap reserved back to deprecated * more test fix * try upgrade * fix test * regen indexer protos * proto lint * test fix * upgrade file comment --- .../clob/block_rate_limit_config.ts | 26 +- .../clob/block_rate_limit_config.proto | 16 +- protocol/app/app.go | 3 +- .../app/testdata/default_genesis_state.json | 5 +- protocol/app/upgrades.go | 1 + protocol/app/upgrades/v5.0.0/upgrade.go | 57 ++++ protocol/lib/metrics/constants.go | 1 + protocol/mocks/ClobKeeper.go | 36 +++ .../scripts/genesis/sample_pregenesis.json | 14 +- protocol/testing/genesis.sh | 12 +- protocol/testutil/constants/genesis.go | 10 +- protocol/testutil/keeper/clob.go | 3 +- protocol/x/clob/ante/rate_limit.go | 9 +- ...ery_block_rate_limit_configuration_test.go | 6 +- protocol/x/clob/e2e/app_test.go | 87 ++++++ protocol/x/clob/e2e/rate_limit_test.go | 258 ++++++++++++++++-- protocol/x/clob/genesis_test.go | 15 +- .../x/clob/keeper/block_rate_limit_config.go | 6 +- ...ery_block_rate_limit_configuration_test.go | 13 +- protocol/x/clob/keeper/keeper.go | 13 +- ...ver_update_block_rate_limit_config_test.go | 19 +- protocol/x/clob/keeper/rate_limit.go | 36 ++- protocol/x/clob/module_test.go | 25 +- .../rate_limit/multi_block_rate_limiter.go | 11 +- .../x/clob/rate_limit/noop_rate_limiter.go | 4 + .../x/clob/rate_limit/order_rate_limiter.go | 178 ++++++------ .../x/clob/rate_limit/panic_rate_limiter.go | 4 + protocol/x/clob/rate_limit/rate_limit.go | 3 + .../rate_limit/single_block_rate_limiter.go | 9 +- .../x/clob/types/block_rate_limit_config.go | 38 +-- .../clob/types/block_rate_limit_config.pb.go | 127 +++++++-- protocol/x/clob/types/clob_keeper.go | 4 + protocol/x/clob/types/genesis_test.go | 113 ++------ 33 files changed, 786 insertions(+), 376 deletions(-) diff --git a/indexer/packages/v4-protos/src/codegen/dydxprotocol/clob/block_rate_limit_config.ts b/indexer/packages/v4-protos/src/codegen/dydxprotocol/clob/block_rate_limit_config.ts index 4db5bf31a3..7da63ae44f 100644 --- a/indexer/packages/v4-protos/src/codegen/dydxprotocol/clob/block_rate_limit_config.ts +++ b/indexer/packages/v4-protos/src/codegen/dydxprotocol/clob/block_rate_limit_config.ts @@ -10,7 +10,11 @@ export interface BlockRateLimitConfiguration { * configurations. * * Specifying 0 values disables this rate limit. + * Deprecated in favor of `max_short_term_orders_and_cancels_per_n_blocks` + * for v5.x onwards. */ + + /** @deprecated */ maxShortTermOrdersPerNBlocks: MaxPerNBlocksRateLimit[]; /** * How many stateful order attempts (successful and failed) are allowed for @@ -22,7 +26,10 @@ export interface BlockRateLimitConfiguration { */ maxStatefulOrdersPerNBlocks: MaxPerNBlocksRateLimit[]; + /** @deprecated */ + maxShortTermOrderCancellationsPerNBlocks: MaxPerNBlocksRateLimit[]; + maxShortTermOrdersAndCancelsPerNBlocks: MaxPerNBlocksRateLimit[]; } /** Defines the block rate limits for CLOB specific operations. */ @@ -34,7 +41,11 @@ export interface BlockRateLimitConfigurationSDKType { * configurations. * * Specifying 0 values disables this rate limit. + * Deprecated in favor of `max_short_term_orders_and_cancels_per_n_blocks` + * for v5.x onwards. */ + + /** @deprecated */ max_short_term_orders_per_n_blocks: MaxPerNBlocksRateLimitSDKType[]; /** * How many stateful order attempts (successful and failed) are allowed for @@ -46,7 +57,10 @@ export interface BlockRateLimitConfigurationSDKType { */ max_stateful_orders_per_n_blocks: MaxPerNBlocksRateLimitSDKType[]; + /** @deprecated */ + max_short_term_order_cancellations_per_n_blocks: MaxPerNBlocksRateLimitSDKType[]; + max_short_term_orders_and_cancels_per_n_blocks: MaxPerNBlocksRateLimitSDKType[]; } /** Defines a rate limit over a specific number of blocks. */ @@ -83,7 +97,8 @@ function createBaseBlockRateLimitConfiguration(): BlockRateLimitConfiguration { return { maxShortTermOrdersPerNBlocks: [], maxStatefulOrdersPerNBlocks: [], - maxShortTermOrderCancellationsPerNBlocks: [] + maxShortTermOrderCancellationsPerNBlocks: [], + maxShortTermOrdersAndCancelsPerNBlocks: [] }; } @@ -101,6 +116,10 @@ export const BlockRateLimitConfiguration = { MaxPerNBlocksRateLimit.encode(v!, writer.uint32(26).fork()).ldelim(); } + for (const v of message.maxShortTermOrdersAndCancelsPerNBlocks) { + MaxPerNBlocksRateLimit.encode(v!, writer.uint32(34).fork()).ldelim(); + } + return writer; }, @@ -125,6 +144,10 @@ export const BlockRateLimitConfiguration = { message.maxShortTermOrderCancellationsPerNBlocks.push(MaxPerNBlocksRateLimit.decode(reader, reader.uint32())); break; + case 4: + message.maxShortTermOrdersAndCancelsPerNBlocks.push(MaxPerNBlocksRateLimit.decode(reader, reader.uint32())); + break; + default: reader.skipType(tag & 7); break; @@ -139,6 +162,7 @@ export const BlockRateLimitConfiguration = { message.maxShortTermOrdersPerNBlocks = object.maxShortTermOrdersPerNBlocks?.map(e => MaxPerNBlocksRateLimit.fromPartial(e)) || []; message.maxStatefulOrdersPerNBlocks = object.maxStatefulOrdersPerNBlocks?.map(e => MaxPerNBlocksRateLimit.fromPartial(e)) || []; message.maxShortTermOrderCancellationsPerNBlocks = object.maxShortTermOrderCancellationsPerNBlocks?.map(e => MaxPerNBlocksRateLimit.fromPartial(e)) || []; + message.maxShortTermOrdersAndCancelsPerNBlocks = object.maxShortTermOrdersAndCancelsPerNBlocks?.map(e => MaxPerNBlocksRateLimit.fromPartial(e)) || []; return message; } diff --git a/proto/dydxprotocol/clob/block_rate_limit_config.proto b/proto/dydxprotocol/clob/block_rate_limit_config.proto index 8f212132e7..273c2d536e 100644 --- a/proto/dydxprotocol/clob/block_rate_limit_config.proto +++ b/proto/dydxprotocol/clob/block_rate_limit_config.proto @@ -13,8 +13,10 @@ message BlockRateLimitConfiguration { // configurations. // // Specifying 0 values disables this rate limit. + // Deprecated in favor of `max_short_term_orders_and_cancels_per_n_blocks` + // for v5.x onwards. repeated MaxPerNBlocksRateLimit max_short_term_orders_per_n_blocks = 1 - [ (gogoproto.nullable) = false ]; + [ (gogoproto.nullable) = false, deprecated = true ]; // How many stateful order attempts (successful and failed) are allowed for // an account per N blocks. Note that the rate limits are applied @@ -31,8 +33,20 @@ message BlockRateLimitConfiguration { // rate limit configurations. // // Specifying 0 values disables this rate limit. + // Deprecated in favor of `max_short_term_orders_and_cancels_per_n_blocks` + // for v5.x onwards. repeated MaxPerNBlocksRateLimit max_short_term_order_cancellations_per_n_blocks = 3 + [ (gogoproto.nullable) = false, deprecated = true ]; + + // How many short term order place and cancel attempts (successful and failed) + // are allowed for an account per N blocks. Note that the rate limits are + // applied in an AND fashion such that an order placement must pass all rate + // limit configurations. + // + // Specifying 0 values disables this rate limit. + repeated MaxPerNBlocksRateLimit + max_short_term_orders_and_cancels_per_n_blocks = 4 [ (gogoproto.nullable) = false ]; } diff --git a/protocol/app/app.go b/protocol/app/app.go index 5f4ad9bce2..ae84165c57 100644 --- a/protocol/app/app.go +++ b/protocol/app/app.go @@ -1022,8 +1022,7 @@ func New( app.GrpcStreamingManager, txConfig.TxDecoder(), clobFlags, - rate_limit.NewPanicRateLimiter[*clobmoduletypes.MsgPlaceOrder](), - rate_limit.NewPanicRateLimiter[*clobmoduletypes.MsgCancelOrder](), + rate_limit.NewPanicRateLimiter[sdk.Msg](), daemonLiquidationInfo, ) clobModule := clobmodule.NewAppModule( diff --git a/protocol/app/testdata/default_genesis_state.json b/protocol/app/testdata/default_genesis_state.json index 1b30955b02..92e61f4219 100644 --- a/protocol/app/testdata/default_genesis_state.json +++ b/protocol/app/testdata/default_genesis_state.json @@ -86,9 +86,10 @@ } }, "block_rate_limit_config": { + "max_short_term_order_cancellations_per_n_blocks": [], "max_short_term_orders_per_n_blocks": [], - "max_stateful_orders_per_n_blocks": [], - "max_short_term_order_cancellations_per_n_blocks": [] + "max_short_term_orders_and_cancels_per_n_blocks": [], + "max_stateful_orders_per_n_blocks": [] }, "equity_tier_limit_config": { "short_term_order_equity_tiers": [], diff --git a/protocol/app/upgrades.go b/protocol/app/upgrades.go index 51a6dc9e39..99c6c7b0c1 100644 --- a/protocol/app/upgrades.go +++ b/protocol/app/upgrades.go @@ -31,6 +31,7 @@ func (app *App) setupUpgradeHandlers() { app.ModuleManager, app.configurator, app.PerpetualsKeeper, + app.ClobKeeper, ), ) } diff --git a/protocol/app/upgrades/v5.0.0/upgrade.go b/protocol/app/upgrades/v5.0.0/upgrade.go index 56f78e72c5..02ac57375d 100644 --- a/protocol/app/upgrades/v5.0.0/upgrade.go +++ b/protocol/app/upgrades/v5.0.0/upgrade.go @@ -6,6 +6,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" + clobtypes "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" perptypes "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types" upgradetypes "cosmossdk.io/x/upgrade/types" @@ -30,10 +31,63 @@ func perpetualsUpgrade( } } +// blockRateLimitConfigUpdate upgrades the block rate limit. It searches for the +// 1-block window limit for short term and cancellations, sums them, and creates a new +// combined rate limit. +func blockRateLimitConfigUpdate( + ctx sdk.Context, + clobKeeper clobtypes.ClobKeeper, +) { + oldBlockRateLimitConfig := clobKeeper.GetBlockRateLimitConfiguration(ctx) + numAllowedShortTermOrderPlacementsInOneBlock := 0 + numAllowedShortTermOrderCancellationsInOneBlock := 0 + oldShortTermOrderRateLimits := oldBlockRateLimitConfig.MaxShortTermOrdersPerNBlocks + for _, limit := range oldShortTermOrderRateLimits { + if limit.NumBlocks == 1 { + numAllowedShortTermOrderPlacementsInOneBlock += int(limit.NumBlocks) + break + } + } + if numAllowedShortTermOrderPlacementsInOneBlock == 0 { + panic("Failed to find MaxShortTermOrdersPerNBlocks with window 1.") + } + + oldShortTermOrderCancellationRateLimits := oldBlockRateLimitConfig.MaxShortTermOrderCancellationsPerNBlocks + for _, limit := range oldShortTermOrderCancellationRateLimits { + if limit.NumBlocks == 1 { + numAllowedShortTermOrderCancellationsInOneBlock += int(limit.NumBlocks) + break + } + } + if numAllowedShortTermOrderCancellationsInOneBlock == 0 { + panic("Failed to find MaxShortTermOrdersPerNBlocks with window 1.") + } + + allowedNumShortTermPlaceAndCancelInFiveBlocks := + (numAllowedShortTermOrderPlacementsInOneBlock + numAllowedShortTermOrderCancellationsInOneBlock) * 5 + + blockRateLimitConfig := clobtypes.BlockRateLimitConfiguration{ + // Kept the same + MaxStatefulOrdersPerNBlocks: oldBlockRateLimitConfig.MaxStatefulOrdersPerNBlocks, + // Combine place and cancel, gate over 5 blocks to allow burst + MaxShortTermOrdersAndCancelsPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ + { + NumBlocks: 5, + Limit: uint32(allowedNumShortTermPlaceAndCancelInFiveBlocks), + }, + }, + } + + if err := clobKeeper.InitializeBlockRateLimit(ctx, blockRateLimitConfig); err != nil { + panic(fmt.Sprintf("failed to update the block rate limit configuration: %s", err)) + } +} + func CreateUpgradeHandler( mm *module.Manager, configurator module.Configurator, perpetualsKeeper perptypes.PerpetualsKeeper, + clobKeeper clobtypes.ClobKeeper, ) upgradetypes.UpgradeHandler { return func(ctx context.Context, plan upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { sdkCtx := lib.UnwrapSDKContext(ctx, "app/upgrades") @@ -42,6 +96,9 @@ func CreateUpgradeHandler( // Set all perpetuals to cross market type perpetualsUpgrade(sdkCtx, perpetualsKeeper) + // Set block rate limit configuration + blockRateLimitConfigUpdate(sdkCtx, clobKeeper) + // TODO(TRA-93): Initialize `x/vault` module. return mm.RunMigrations(ctx, configurator, vm) diff --git a/protocol/lib/metrics/constants.go b/protocol/lib/metrics/constants.go index 3e2644d7a4..f1d25fc3bd 100644 --- a/protocol/lib/metrics/constants.go +++ b/protocol/lib/metrics/constants.go @@ -92,6 +92,7 @@ const ( // CLOB. AddPerpetualFillAmount = "add_perpetual_fill_amount" BaseQuantums = "base_quantums" + BatchCancel = "batch_cancel" BestAsk = "best_ask" BestAskClobPair = "best_ask_clob_pair" BestBid = "best_bid" diff --git a/protocol/mocks/ClobKeeper.go b/protocol/mocks/ClobKeeper.go index c9991c4dc9..fc5a177f70 100644 --- a/protocol/mocks/ClobKeeper.go +++ b/protocol/mocks/ClobKeeper.go @@ -230,6 +230,24 @@ func (_m *ClobKeeper) GetBankruptcyPriceInQuoteQuantums(ctx types.Context, subac return r0, r1 } +// GetBlockRateLimitConfiguration provides a mock function with given fields: ctx +func (_m *ClobKeeper) GetBlockRateLimitConfiguration(ctx types.Context) clobtypes.BlockRateLimitConfiguration { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetBlockRateLimitConfiguration") + } + + var r0 clobtypes.BlockRateLimitConfiguration + if rf, ok := ret.Get(0).(func(types.Context) clobtypes.BlockRateLimitConfiguration); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(clobtypes.BlockRateLimitConfiguration) + } + + return r0 +} + // GetClobPair provides a mock function with given fields: ctx, id func (_m *ClobKeeper) GetClobPair(ctx types.Context, id clobtypes.ClobPairId) (clobtypes.ClobPair, bool) { ret := _m.Called(ctx, id) @@ -1028,6 +1046,24 @@ func (_m *ClobKeeper) PruneStateFillAmountsForShortTermOrders(ctx types.Context) _m.Called(ctx) } +// RateLimitBatchCancel provides a mock function with given fields: ctx, order +func (_m *ClobKeeper) RateLimitBatchCancel(ctx types.Context, order *clobtypes.MsgBatchCancel) error { + ret := _m.Called(ctx, order) + + if len(ret) == 0 { + panic("no return value specified for RateLimitBatchCancel") + } + + var r0 error + if rf, ok := ret.Get(0).(func(types.Context, *clobtypes.MsgBatchCancel) error); ok { + r0 = rf(ctx, order) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // RateLimitCancelOrder provides a mock function with given fields: ctx, order func (_m *ClobKeeper) RateLimitCancelOrder(ctx types.Context, order *clobtypes.MsgCancelOrder) error { ret := _m.Called(ctx, order) diff --git a/protocol/scripts/genesis/sample_pregenesis.json b/protocol/scripts/genesis/sample_pregenesis.json index 5cc71f0073..887ad44c85 100644 --- a/protocol/scripts/genesis/sample_pregenesis.json +++ b/protocol/scripts/genesis/sample_pregenesis.json @@ -100,18 +100,14 @@ }, "clob": { "block_rate_limit_config": { - "max_short_term_order_cancellations_per_n_blocks": [ + "max_short_term_order_cancellations_per_n_blocks": [], + "max_short_term_orders_and_cancels_per_n_blocks": [ { - "limit": 200, - "num_blocks": 1 - } - ], - "max_short_term_orders_per_n_blocks": [ - { - "limit": 200, + "limit": 400, "num_blocks": 1 } ], + "max_short_term_orders_per_n_blocks": [], "max_stateful_orders_per_n_blocks": [ { "limit": 2, @@ -1830,7 +1826,7 @@ ] } }, - "app_version": "4.0.0-dev0-101-g18028fd0", + "app_version": "4.0.0-dev0-134-gc65a2bfe", "chain_id": "dydx-sample-1", "consensus": { "params": { diff --git a/protocol/testing/genesis.sh b/protocol/testing/genesis.sh index 2c9b065f98..f1e342af9f 100755 --- a/protocol/testing/genesis.sh +++ b/protocol/testing/genesis.sh @@ -1404,14 +1404,10 @@ function edit_genesis() { dasel put -t int -f "$GENESIS" '.app_state.clob.liquidations_config.fillable_price_config.spread_to_maintenance_margin_ratio_ppm' -v '1500000' # 150% # Block Rate Limit - # Max 50 short term orders per block - dasel put -t json -f "$GENESIS" '.app_state.clob.block_rate_limit_config.max_short_term_orders_per_n_blocks.[]' -v "{}" - dasel put -t int -f "$GENESIS" '.app_state.clob.block_rate_limit_config.max_short_term_orders_per_n_blocks.[0].limit' -v '200' - dasel put -t int -f "$GENESIS" '.app_state.clob.block_rate_limit_config.max_short_term_orders_per_n_blocks.[0].num_blocks' -v '1' - # Max 50 short term order cancellations per block - dasel put -t json -f "$GENESIS" '.app_state.clob.block_rate_limit_config.max_short_term_order_cancellations_per_n_blocks.[]' -v "{}" - dasel put -t int -f "$GENESIS" '.app_state.clob.block_rate_limit_config.max_short_term_order_cancellations_per_n_blocks.[0].limit' -v '200' - dasel put -t int -f "$GENESIS" '.app_state.clob.block_rate_limit_config.max_short_term_order_cancellations_per_n_blocks.[0].num_blocks' -v '1' + # Max 400 short term orders/cancels per block + dasel put -t json -f "$GENESIS" '.app_state.clob.block_rate_limit_config.max_short_term_orders_and_cancels_per_n_blocks.[]' -v "{}" + dasel put -t int -f "$GENESIS" '.app_state.clob.block_rate_limit_config.max_short_term_orders_and_cancels_per_n_blocks.[0].limit' -v '400' + dasel put -t int -f "$GENESIS" '.app_state.clob.block_rate_limit_config.max_short_term_orders_and_cancels_per_n_blocks.[0].num_blocks' -v '1' # Max 2 stateful orders per block dasel put -t json -f "$GENESIS" '.app_state.clob.block_rate_limit_config.max_stateful_orders_per_n_blocks.[]' -v "{}" dasel put -t int -f "$GENESIS" '.app_state.clob.block_rate_limit_config.max_stateful_orders_per_n_blocks.[0].limit' -v '2' diff --git a/protocol/testutil/constants/genesis.go b/protocol/testutil/constants/genesis.go index d8b1ca8209..dc56ee3590 100644 --- a/protocol/testutil/constants/genesis.go +++ b/protocol/testutil/constants/genesis.go @@ -218,10 +218,10 @@ const GenesisState = `{ }, "clob": { "block_rate_limit_config": { - "max_short_term_orders_per_n_blocks": [ + "max_short_term_orders_and_cancels_per_n_blocks": [ { "num_blocks": 1, - "limit": 200 + "limit": 400 } ], "max_stateful_orders_per_n_blocks": [ @@ -233,12 +233,6 @@ const GenesisState = `{ "num_blocks": 100, "limit": 20 } - ], - "max_short_term_order_cancellations_per_n_blocks": [ - { - "num_blocks": 1, - "limit": 200 - } ] }, "clob_pairs": [ diff --git a/protocol/testutil/keeper/clob.go b/protocol/testutil/keeper/clob.go index 506ebe114a..cad8a0eed7 100644 --- a/protocol/testutil/keeper/clob.go +++ b/protocol/testutil/keeper/clob.go @@ -218,8 +218,7 @@ func createClobKeeper( streaming.NewNoopGrpcStreamingManager(), constants.TestEncodingCfg.TxConfig.TxDecoder(), flags.GetDefaultClobFlags(), - rate_limit.NewNoOpRateLimiter[*types.MsgPlaceOrder](), - rate_limit.NewNoOpRateLimiter[*types.MsgCancelOrder](), + rate_limit.NewNoOpRateLimiter[sdk.Msg](), liquidationtypes.NewDaemonLiquidationInfo(), ) k.SetAnteHandler(constants.EmptyAnteHandler) diff --git a/protocol/x/clob/ante/rate_limit.go b/protocol/x/clob/ante/rate_limit.go index 3be0da2180..a6ba7c89e9 100644 --- a/protocol/x/clob/ante/rate_limit.go +++ b/protocol/x/clob/ante/rate_limit.go @@ -8,14 +8,15 @@ import ( var _ sdktypes.AnteDecorator = (*ClobRateLimitDecorator)(nil) // ClobRateLimitDecorator is an AnteDecorator which is responsible for rate limiting MsgCancelOrder and MsgPlaceOrder -// requests. +// and MsgBatchCancel requests. // // This AnteDecorator is a no-op if: -// - No messages in the transaction are `MsgCancelOrder` or `MsgPlaceOrder`. +// - No messages in the transaction are `MsgCancelOrder` or `MsgPlaceOrder` or `MsgBatchCancel` // // This AnteDecorator returns an error if: // - The rate limit is exceeded for any `MsgCancelOrder` messages. // - The rate limit is exceeded for any `MsgPlaceOrder` messages. +// - The rate limit is exceeded for any `MsgBatchCancel` messages. // // TODO(CLOB-721): Rate limit short term order cancellations. type ClobRateLimitDecorator struct { @@ -43,6 +44,10 @@ func (r ClobRateLimitDecorator) AnteHandle( if err = r.clobKeeper.RateLimitPlaceOrder(ctx, msg); err != nil { return ctx, err } + case *types.MsgBatchCancel: + if err = r.clobKeeper.RateLimitBatchCancel(ctx, msg); err != nil { + return ctx, err + } } } return next(ctx, tx, simulate) diff --git a/protocol/x/clob/client/cli/query_block_rate_limit_configuration_test.go b/protocol/x/clob/client/cli/query_block_rate_limit_configuration_test.go index 8faf3b636a..4aeabb226f 100644 --- a/protocol/x/clob/client/cli/query_block_rate_limit_configuration_test.go +++ b/protocol/x/clob/client/cli/query_block_rate_limit_configuration_test.go @@ -4,19 +4,21 @@ package cli_test import ( "fmt" + "testing" + tmcli "github.com/cometbft/cometbft/libs/cli" clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli" "github.com/dydxprotocol/v4-chain/protocol/x/clob/client/cli" "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" "github.com/stretchr/testify/require" - "testing" ) var ( emptyConfig = types.BlockRateLimitConfiguration{ MaxShortTermOrdersPerNBlocks: []types.MaxPerNBlocksRateLimit{}, - MaxStatefulOrdersPerNBlocks: []types.MaxPerNBlocksRateLimit{}, MaxShortTermOrderCancellationsPerNBlocks: []types.MaxPerNBlocksRateLimit{}, + MaxStatefulOrdersPerNBlocks: []types.MaxPerNBlocksRateLimit{}, + MaxShortTermOrdersAndCancelsPerNBlocks: []types.MaxPerNBlocksRateLimit{}, } ) diff --git a/protocol/x/clob/e2e/app_test.go b/protocol/x/clob/e2e/app_test.go index 33037954fb..e2ed8f1952 100644 --- a/protocol/x/clob/e2e/app_test.go +++ b/protocol/x/clob/e2e/app_test.go @@ -53,6 +53,26 @@ var ( }, testapp.DefaultGenesis(), )) + PlaceOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTB23 = *clobtypes.NewMsgPlaceOrder(testapp.MustScaleOrder( + clobtypes.Order{ + OrderId: clobtypes.OrderId{SubaccountId: constants.Alice_Num0, ClientId: 0, ClobPairId: 0}, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 5, + Subticks: 10, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 23}, + }, + testapp.DefaultGenesis(), + )) + PlaceOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTB24 = *clobtypes.NewMsgPlaceOrder(testapp.MustScaleOrder( + clobtypes.Order{ + OrderId: clobtypes.OrderId{SubaccountId: constants.Alice_Num0, ClientId: 0, ClobPairId: 0}, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 5, + Subticks: 10, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 24}, + }, + testapp.DefaultGenesis(), + )) PlaceOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTB27 = *clobtypes.NewMsgPlaceOrder(testapp.MustScaleOrder( clobtypes.Order{ OrderId: clobtypes.OrderId{SubaccountId: constants.Alice_Num0, ClientId: 0, ClobPairId: 0}, @@ -158,6 +178,14 @@ var ( }, 27, ) + CancelOrder_Alice_Num0_Id0_Clob0_GTB23 = *clobtypes.NewMsgCancelOrderShortTerm( + clobtypes.OrderId{ + SubaccountId: constants.Alice_Num0, + ClientId: 0, + ClobPairId: 0, + }, + 23, + ) CancelOrder_Alice_Num1_Id0_Clob0_GTB20 = *clobtypes.NewMsgCancelOrderShortTerm( clobtypes.OrderId{ SubaccountId: constants.Alice_Num1, @@ -166,6 +194,14 @@ var ( }, 20, ) + CancelOrder_Alice_Num0_Id1_Clob0_GTB20 = *clobtypes.NewMsgCancelOrderShortTerm( + clobtypes.OrderId{ + SubaccountId: constants.Alice_Num0, + ClientId: 0, + ClobPairId: 1, + }, + 20, + ) PlaceOrder_Bob_Num0_Id0_Clob0_Sell5_Price10_GTB20 = *clobtypes.NewMsgPlaceOrder(testapp.MustScaleOrder( clobtypes.Order{ OrderId: clobtypes.OrderId{SubaccountId: constants.Bob_Num0, ClientId: 0, ClobPairId: 0}, @@ -223,6 +259,57 @@ var ( constants.ConditionalOrder_Alice_Num1_Id0_Clob0_Sell5_Price10_GTB15, testapp.DefaultGenesis(), )) + + BatchCancel_Alice_Num0_Clob0_1_2_3_GTB5 = *clobtypes.NewMsgBatchCancel( + constants.Alice_Num0, + []clobtypes.OrderBatch{ + { + ClobPairId: 0, + ClientIds: []uint32{1, 2, 3}, + }, + }, + 5, + ) + BatchCancel_Alice_Num0_Clob0_1_2_3_GTB27 = *clobtypes.NewMsgBatchCancel( + constants.Alice_Num0, + []clobtypes.OrderBatch{ + { + ClobPairId: 0, + ClientIds: []uint32{1, 2, 3}, + }, + }, + 27, + ) + BatchCancel_Alice_Num0_Clob0_1_2_3_GTB20 = *clobtypes.NewMsgBatchCancel( + constants.Alice_Num0, + []clobtypes.OrderBatch{ + { + ClobPairId: 0, + ClientIds: []uint32{1, 2, 3}, + }, + }, + 20, + ) + BatchCancel_Alice_Num0_Clob1_1_2_3_GTB20 = *clobtypes.NewMsgBatchCancel( + constants.Alice_Num0, + []clobtypes.OrderBatch{ + { + ClobPairId: 1, + ClientIds: []uint32{1, 2, 3}, + }, + }, + 20, + ) + BatchCancel_Alice_Num1_Clob0_1_2_3_GTB20 = *clobtypes.NewMsgBatchCancel( + constants.Alice_Num1, + []clobtypes.OrderBatch{ + { + ClobPairId: 0, + ClientIds: []uint32{1, 2, 3}, + }, + }, + 20, + ) ) // We place 300 orders that match and 700 orders followed by their cancellations concurrently. diff --git a/protocol/x/clob/e2e/rate_limit_test.go b/protocol/x/clob/e2e/rate_limit_test.go index 8ad5a348a9..f88e1a1e51 100644 --- a/protocol/x/clob/e2e/rate_limit_test.go +++ b/protocol/x/clob/e2e/rate_limit_test.go @@ -1,9 +1,10 @@ package clob_test import ( - satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" "testing" + satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" + abcitypes "github.com/cometbft/cometbft/abci/types" sdktypes "github.com/cosmos/cosmos-sdk/types" @@ -18,13 +19,13 @@ import ( func TestRateLimitingOrders_RateLimitsAreEnforced(t *testing.T) { tests := map[string]struct { - blockRateLimitConifg clobtypes.BlockRateLimitConfiguration + blockRateLimitConfig clobtypes.BlockRateLimitConfiguration firstMsg sdktypes.Msg secondMsg sdktypes.Msg }{ "Short term orders with same subaccounts": { - blockRateLimitConifg: clobtypes.BlockRateLimitConfiguration{ - MaxShortTermOrdersPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ + blockRateLimitConfig: clobtypes.BlockRateLimitConfiguration{ + MaxShortTermOrdersAndCancelsPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ { NumBlocks: 2, Limit: 1, @@ -35,8 +36,8 @@ func TestRateLimitingOrders_RateLimitsAreEnforced(t *testing.T) { secondMsg: &PlaceOrder_Alice_Num0_Id0_Clob1_Buy5_Price10_GTB20, }, "Short term orders with different subaccounts": { - blockRateLimitConifg: clobtypes.BlockRateLimitConfiguration{ - MaxShortTermOrdersPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ + blockRateLimitConfig: clobtypes.BlockRateLimitConfiguration{ + MaxShortTermOrdersAndCancelsPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ { NumBlocks: 2, Limit: 1, @@ -47,7 +48,7 @@ func TestRateLimitingOrders_RateLimitsAreEnforced(t *testing.T) { secondMsg: &PlaceOrder_Alice_Num1_Id0_Clob0_Buy5_Price10_GTB20, }, "Stateful orders with same subaccounts": { - blockRateLimitConifg: clobtypes.BlockRateLimitConfiguration{ + blockRateLimitConfig: clobtypes.BlockRateLimitConfiguration{ MaxStatefulOrdersPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ { NumBlocks: 2, @@ -59,7 +60,7 @@ func TestRateLimitingOrders_RateLimitsAreEnforced(t *testing.T) { secondMsg: &LongTermPlaceOrder_Alice_Num0_Id0_Clob1_Buy5_Price10_GTBT5, }, "Stateful orders with different subaccounts": { - blockRateLimitConifg: clobtypes.BlockRateLimitConfiguration{ + blockRateLimitConfig: clobtypes.BlockRateLimitConfiguration{ MaxStatefulOrdersPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ { NumBlocks: 2, @@ -71,8 +72,8 @@ func TestRateLimitingOrders_RateLimitsAreEnforced(t *testing.T) { secondMsg: &LongTermPlaceOrder_Alice_Num1_Id0_Clob0_Buy5_Price10_GTBT5, }, "Short term order cancellations with same subaccounts": { - blockRateLimitConifg: clobtypes.BlockRateLimitConfiguration{ - MaxShortTermOrderCancellationsPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ + blockRateLimitConfig: clobtypes.BlockRateLimitConfiguration{ + MaxShortTermOrdersAndCancelsPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ { NumBlocks: 2, Limit: 1, @@ -83,8 +84,8 @@ func TestRateLimitingOrders_RateLimitsAreEnforced(t *testing.T) { secondMsg: &CancelOrder_Alice_Num0_Id0_Clob0_GTB20, }, "Short term order cancellations with different subaccounts": { - blockRateLimitConifg: clobtypes.BlockRateLimitConfiguration{ - MaxShortTermOrderCancellationsPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ + blockRateLimitConfig: clobtypes.BlockRateLimitConfiguration{ + MaxShortTermOrdersAndCancelsPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ { NumBlocks: 2, Limit: 1, @@ -94,6 +95,30 @@ func TestRateLimitingOrders_RateLimitsAreEnforced(t *testing.T) { firstMsg: &CancelOrder_Alice_Num0_Id0_Clob1_GTB5, secondMsg: &CancelOrder_Alice_Num1_Id0_Clob0_GTB20, }, + "Batch cancellations with same subaccounts": { + blockRateLimitConfig: clobtypes.BlockRateLimitConfiguration{ + MaxShortTermOrdersAndCancelsPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ + { + NumBlocks: 2, + Limit: 2, + }, + }, + }, + firstMsg: &BatchCancel_Alice_Num0_Clob0_1_2_3_GTB5, + secondMsg: &BatchCancel_Alice_Num0_Clob0_1_2_3_GTB20, + }, + "Batch cancellations with different subaccounts": { + blockRateLimitConfig: clobtypes.BlockRateLimitConfiguration{ + MaxShortTermOrdersAndCancelsPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ + { + NumBlocks: 2, + Limit: 2, + }, + }, + }, + firstMsg: &BatchCancel_Alice_Num0_Clob0_1_2_3_GTB5, + secondMsg: &BatchCancel_Alice_Num1_Clob0_1_2_3_GTB20, + }, } for name, tc := range tests { @@ -106,7 +131,7 @@ func TestRateLimitingOrders_RateLimitsAreEnforced(t *testing.T) { testapp.UpdateGenesisDocWithAppStateForModule( &genesis, func(genesisState *clobtypes.GenesisState) { - genesisState.BlockRateLimitConfig = tc.blockRateLimitConifg + genesisState.BlockRateLimitConfig = tc.blockRateLimitConfig }, ) testapp.UpdateGenesisDocWithAppStateForModule( @@ -146,21 +171,21 @@ func TestRateLimitingOrders_RateLimitsAreEnforced(t *testing.T) { resp = tApp.CheckTx(secondCheckTx) require.Conditionf(t, resp.IsErr, "Expected CheckTx to error. Response: %+v", resp) require.Equal(t, clobtypes.ErrBlockRateLimitExceeded.ABCICode(), resp.Code) - require.Contains(t, resp.Log, "Rate of 2 exceeds configured block rate limit") + require.Contains(t, resp.Log, "exceeds configured block rate limit") // Rate limit of 1 over two blocks should still apply, total should be 3 now (2 in block 2, 1 in block 3). tApp.AdvanceToBlock(3, testapp.AdvanceToBlockOptions{}) resp = tApp.CheckTx(secondCheckTx) require.Conditionf(t, resp.IsErr, "Expected CheckTx to error. Response: %+v", resp) require.Equal(t, clobtypes.ErrBlockRateLimitExceeded.ABCICode(), resp.Code) - require.Contains(t, resp.Log, "Rate of 3 exceeds configured block rate limit") + require.Contains(t, resp.Log, "exceeds configured block rate limit") // Rate limit of 1 over two blocks should still apply, total should be 2 now (1 in block 3, 1 in block 4). tApp.AdvanceToBlock(4, testapp.AdvanceToBlockOptions{}) resp = tApp.CheckTx(secondCheckTx) require.Conditionf(t, resp.IsErr, "Expected CheckTx to error. Response: %+v", resp) require.Equal(t, clobtypes.ErrBlockRateLimitExceeded.ABCICode(), resp.Code) - require.Contains(t, resp.Log, "Rate of 2 exceeds configured block rate limit") + require.Contains(t, resp.Log, "exceeds configured block rate limit") // Advancing two blocks should make the total count 0 now and the msg should be accepted. tApp.AdvanceToBlock(6, testapp.AdvanceToBlockOptions{}) @@ -170,6 +195,180 @@ func TestRateLimitingOrders_RateLimitsAreEnforced(t *testing.T) { } } +func TestCombinedPlaceCancelBatchCancel_RateLimitsAreEnforced(t *testing.T) { + tests := map[string]struct { + blockRateLimitConfig clobtypes.BlockRateLimitConfiguration + firstBatch []sdktypes.Msg + secondBatch []sdktypes.Msg + thirdBatch []sdktypes.Msg + firstBatchSuccess []bool + secondBatchSuccess []bool + thirdBatchSuccess []bool + lastOrder sdktypes.Msg + }{ + "Combination Place, Cancel, BatchCancel orders": { + blockRateLimitConfig: clobtypes.BlockRateLimitConfiguration{ + MaxShortTermOrdersAndCancelsPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ + { + NumBlocks: 2, + Limit: 6, // TODO FIX THIS AFTER SETTLE ON A NUM + }, + }, + }, + firstBatch: []sdktypes.Msg{ + &PlaceOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTB20, // 1-weight success @ 1 + &PlaceOrder_Alice_Num0_Id0_Clob1_Buy5_Price10_GTB20, // 1-weight success @ 2 + &CancelOrder_Alice_Num0_Id0_Clob0_GTB20, // 1-weight success @ 3 + }, + firstBatchSuccess: []bool{ + true, + true, + true, + }, + secondBatch: []sdktypes.Msg{ + &PlaceOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTB23, // 1-weight success @ 4 + &CancelOrder_Alice_Num1_Id0_Clob0_GTB20, // 1-weight success @ 5 + &BatchCancel_Alice_Num0_Clob0_1_2_3_GTB20, // 2-weight failure @ 7 + &CancelOrder_Alice_Num0_Id0_Clob0_GTB23, // 1-weight failure @ 8 + }, + secondBatchSuccess: []bool{ + true, + true, + false, + false, + }, + // advance one block, subtract 3 for a count of 5 + thirdBatch: []sdktypes.Msg{ + &PlaceOrder_Alice_Num0_Id0_Clob0_Buy5_Price10_GTB24, // 1-weight success @ 6 + &BatchCancel_Alice_Num0_Clob0_1_2_3_GTB20, // 2-weight failure @ 8 + &CancelOrder_Alice_Num0_Id0_Clob0_GTB20, // 1-weight failure @ 9 + }, + thirdBatchSuccess: []bool{ + true, + false, + false, + }, + // advance one block, subtract 5 for a count of 4 + lastOrder: &BatchCancel_Alice_Num1_Clob0_1_2_3_GTB20, // 2-weight pass @ 6 + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + tApp := testapp.NewTestAppBuilder(t). + // Disable non-determinism checks since we mutate keeper state directly. + WithNonDeterminismChecksEnabled(false). + WithGenesisDocFn(func() (genesis types.GenesisDoc) { + genesis = testapp.DefaultGenesis() + testapp.UpdateGenesisDocWithAppStateForModule( + &genesis, + func(genesisState *clobtypes.GenesisState) { + genesisState.BlockRateLimitConfig = tc.blockRateLimitConfig + }, + ) + testapp.UpdateGenesisDocWithAppStateForModule( + &genesis, + func(genesisState *satypes.GenesisState) { + genesisState.Subaccounts = []satypes.Subaccount{ + constants.Alice_Num0_10_000USD, + constants.Alice_Num1_10_000USD, + } + }) + return genesis + }).Build() + ctx := tApp.InitChain() + + firstCheckTxArray := []abcitypes.RequestCheckTx{} + for _, msg := range tc.firstBatch { + checkTx := testapp.MustMakeCheckTx( + ctx, + tApp.App, + testapp.MustMakeCheckTxOptions{ + AccAddressForSigning: testtx.MustGetOnlySignerAddress(tApp.App.AppCodec(), msg), + }, + msg, + ) + firstCheckTxArray = append(firstCheckTxArray, checkTx) + } + secondCheckTxArray := []abcitypes.RequestCheckTx{} + for _, msg := range tc.secondBatch { + checkTx := testapp.MustMakeCheckTx( + ctx, + tApp.App, + testapp.MustMakeCheckTxOptions{ + AccAddressForSigning: testtx.MustGetOnlySignerAddress(tApp.App.AppCodec(), msg), + }, + msg, + ) + secondCheckTxArray = append(secondCheckTxArray, checkTx) + } + thirdCheckTxArray := []abcitypes.RequestCheckTx{} + for _, msg := range tc.thirdBatch { + checkTx := testapp.MustMakeCheckTx( + ctx, + tApp.App, + testapp.MustMakeCheckTxOptions{ + AccAddressForSigning: testtx.MustGetOnlySignerAddress(tApp.App.AppCodec(), msg), + }, + msg, + ) + thirdCheckTxArray = append(thirdCheckTxArray, checkTx) + } + + tApp.AdvanceToBlock(2, testapp.AdvanceToBlockOptions{}) + // First batch of transactions. + for idx, checkTx := range firstCheckTxArray { + resp := tApp.CheckTx(checkTx) + shouldSucceed := tc.firstBatchSuccess[idx] + if shouldSucceed { + require.Conditionf(t, resp.IsOK, "Expected CheckTx to succeed. Response: %+v", resp) + } else { + require.Equal(t, clobtypes.ErrBlockRateLimitExceeded.ABCICode(), resp.Code) + require.Contains(t, resp.Log, "exceeds configured block rate limit") + } + } + // Advance one block + tApp.AdvanceToBlock(3, testapp.AdvanceToBlockOptions{}) + // Second batch of transactions. + for idx, checkTx := range secondCheckTxArray { + resp := tApp.CheckTx(checkTx) + shouldSucceed := tc.secondBatchSuccess[idx] + if shouldSucceed { + require.Conditionf(t, resp.IsOK, "Expected CheckTx to succeed. Response: %+v", resp) + } else { + require.Equal(t, clobtypes.ErrBlockRateLimitExceeded.ABCICode(), resp.Code) + require.Contains(t, resp.Log, "exceeds configured block rate limit") + } + } + // Advance one block + tApp.AdvanceToBlock(4, testapp.AdvanceToBlockOptions{}) + // Third batch of transactions. + for idx, checkTx := range thirdCheckTxArray { + resp := tApp.CheckTx(checkTx) + shouldSucceed := tc.thirdBatchSuccess[idx] + if shouldSucceed { + require.Conditionf(t, resp.IsOK, "Expected CheckTx to succeed. Response: %+v", resp) + } else { + require.Equal(t, clobtypes.ErrBlockRateLimitExceeded.ABCICode(), resp.Code) + require.Contains(t, resp.Log, "exceeds configured block rate limit") + } + } + // Advance one block + tApp.AdvanceToBlock(5, testapp.AdvanceToBlockOptions{}) + lastCheckTx := testapp.MustMakeCheckTx( + ctx, + tApp.App, + testapp.MustMakeCheckTxOptions{ + AccAddressForSigning: testtx.MustGetOnlySignerAddress(tApp.App.AppCodec(), tc.lastOrder), + }, + tc.lastOrder, + ) + resp := tApp.CheckTx(lastCheckTx) + require.Conditionf(t, resp.IsOK, "Expected CheckTx to succeed. Response: %+v", resp) + }) + } +} + func TestCancellationAndMatchInTheSameBlock_Regression(t *testing.T) { tApp := testapp.NewTestAppBuilder(t).Build() @@ -487,7 +686,7 @@ func TestRateLimitingShortTermOrders_GuardedAgainstReplayAttacks(t *testing.T) { }{ "Short term order placements": { blockRateLimitConfig: clobtypes.BlockRateLimitConfiguration{ - MaxShortTermOrdersPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ + MaxShortTermOrdersAndCancelsPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ { NumBlocks: 1, Limit: 1, @@ -501,7 +700,7 @@ func TestRateLimitingShortTermOrders_GuardedAgainstReplayAttacks(t *testing.T) { }, "Short term order cancellations": { blockRateLimitConfig: clobtypes.BlockRateLimitConfiguration{ - MaxShortTermOrderCancellationsPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ + MaxShortTermOrdersAndCancelsPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ { NumBlocks: 1, Limit: 1, @@ -511,7 +710,21 @@ func TestRateLimitingShortTermOrders_GuardedAgainstReplayAttacks(t *testing.T) { replayLessGTB: &CancelOrder_Alice_Num0_Id0_Clob0_GTB5, replayGreaterGTB: &CancelOrder_Alice_Num0_Id0_Clob0_GTB27, firstValidGTB: &CancelOrder_Alice_Num0_Id0_Clob0_GTB20, - secondValidGTB: &CancelOrder_Alice_Num1_Id0_Clob0_GTB20, + secondValidGTB: &CancelOrder_Alice_Num0_Id1_Clob0_GTB20, + }, + "Batch cancellations": { + blockRateLimitConfig: clobtypes.BlockRateLimitConfiguration{ + MaxShortTermOrdersAndCancelsPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ + { + NumBlocks: 1, + Limit: 2, + }, + }, + }, + replayLessGTB: &BatchCancel_Alice_Num0_Clob0_1_2_3_GTB5, + replayGreaterGTB: &BatchCancel_Alice_Num0_Clob0_1_2_3_GTB27, + firstValidGTB: &BatchCancel_Alice_Num0_Clob0_1_2_3_GTB20, + secondValidGTB: &BatchCancel_Alice_Num0_Clob1_1_2_3_GTB20, }, } @@ -537,6 +750,7 @@ func TestRateLimitingShortTermOrders_GuardedAgainstReplayAttacks(t *testing.T) { }).Build() ctx := tApp.AdvanceToBlock(5, testapp.AdvanceToBlockOptions{}) + // First tx fails due to GTB being too low. replayLessGTBTx := testapp.MustMakeCheckTx( ctx, tApp.App, @@ -549,6 +763,7 @@ func TestRateLimitingShortTermOrders_GuardedAgainstReplayAttacks(t *testing.T) { require.Conditionf(t, resp.IsErr, "Expected CheckTx to error. Response: %+v", resp) require.Equal(t, clobtypes.ErrHeightExceedsGoodTilBlock.ABCICode(), resp.Code) + // Second tx fails due to GTB being too high. replayGreaterGTBTx := testapp.MustMakeCheckTx( ctx, tApp.App, @@ -569,7 +784,8 @@ func TestRateLimitingShortTermOrders_GuardedAgainstReplayAttacks(t *testing.T) { }, tc.firstValidGTB, ) - // First transaction should be allowed. + // First transaction should be allowed due to GTB being valid. The first two tx do not count towards + // the rate limit. resp = tApp.CheckTx(firstCheckTx) require.Conditionf(t, resp.IsOK, "Expected CheckTx to succeed. Response: %+v", resp) @@ -585,7 +801,7 @@ func TestRateLimitingShortTermOrders_GuardedAgainstReplayAttacks(t *testing.T) { resp = tApp.CheckTx(secondCheckTx) require.Conditionf(t, resp.IsErr, "Expected CheckTx to error. Response: %+v", resp) require.Equal(t, clobtypes.ErrBlockRateLimitExceeded.ABCICode(), resp.Code) - require.Contains(t, resp.Log, "Rate of 2 exceeds configured block rate limit") + require.Contains(t, resp.Log, "exceeds configured block rate limit") }) } } diff --git a/protocol/x/clob/genesis_test.go b/protocol/x/clob/genesis_test.go index 852a32a562..928b8276d1 100644 --- a/protocol/x/clob/genesis_test.go +++ b/protocol/x/clob/genesis_test.go @@ -1,9 +1,10 @@ package clob_test import ( - errorsmod "cosmossdk.io/errors" "testing" + errorsmod "cosmossdk.io/errors" + indexerevents "github.com/dydxprotocol/v4-chain/protocol/indexer/events" "github.com/dydxprotocol/v4-chain/protocol/indexer/indexer_manager" clobtest "github.com/dydxprotocol/v4-chain/protocol/testutil/clob" @@ -34,13 +35,7 @@ func TestGenesis(t *testing.T) { "Genesis state is valid": { genesis: types.GenesisState{ BlockRateLimitConfig: types.BlockRateLimitConfiguration{ - MaxShortTermOrdersPerNBlocks: []types.MaxPerNBlocksRateLimit{ - { - Limit: 200, - NumBlocks: 1, - }, - }, - MaxShortTermOrderCancellationsPerNBlocks: []types.MaxPerNBlocksRateLimit{ + MaxShortTermOrdersAndCancelsPerNBlocks: []types.MaxPerNBlocksRateLimit{ { Limit: 200, NumBlocks: 1, @@ -377,7 +372,7 @@ func TestGenesis(t *testing.T) { "Genesis state is invalid when BlockRateLimitConfiguration is invalid": { genesis: types.GenesisState{ BlockRateLimitConfig: types.BlockRateLimitConfiguration{ - MaxShortTermOrdersPerNBlocks: []types.MaxPerNBlocksRateLimit{ + MaxShortTermOrdersAndCancelsPerNBlocks: []types.MaxPerNBlocksRateLimit{ { Limit: 1, NumBlocks: 0, @@ -394,7 +389,7 @@ func TestGenesis(t *testing.T) { SubaccountBlockLimits: constants.SubaccountBlockLimits_Default, }, }, - expectedErr: "0 is not a valid NumBlocks for MaxShortTermOrdersPerNBlocks rate limit " + + expectedErr: "0 is not a valid NumBlocks for MaxShortTermOrdersAndCancelsPerNBlocks rate limit " + "{NumBlocks:0 Limit:1}", expectedErrType: types.ErrInvalidBlockRateLimitConfig, }, diff --git a/protocol/x/clob/keeper/block_rate_limit_config.go b/protocol/x/clob/keeper/block_rate_limit_config.go index 205694d9a3..035ffa875f 100644 --- a/protocol/x/clob/keeper/block_rate_limit_config.go +++ b/protocol/x/clob/keeper/block_rate_limit_config.go @@ -37,8 +37,7 @@ func (k *Keeper) InitalizeBlockRateLimitFromStateIfExists(ctx sdk.Context) { var config types.BlockRateLimitConfiguration k.cdc.MustUnmarshal(b, &config) - k.placeOrderRateLimiter = rate_limit.NewPlaceOrderRateLimiter(config) - k.cancelOrderRateLimiter = rate_limit.NewCancelOrderRateLimiter(config) + k.placeCancelOrderRateLimiter = rate_limit.NewPlaceCancelOrderRateLimiter(config) } // InitializeBlockRateLimit initializes the block rate limit configuration in state and uses @@ -61,8 +60,7 @@ func (k *Keeper) InitializeBlockRateLimit( b := k.cdc.MustMarshal(&config) store.Set([]byte(types.BlockRateLimitConfigKey), b) - k.placeOrderRateLimiter = rate_limit.NewPlaceOrderRateLimiter(config) - k.cancelOrderRateLimiter = rate_limit.NewCancelOrderRateLimiter(config) + k.placeCancelOrderRateLimiter = rate_limit.NewPlaceCancelOrderRateLimiter(config) return nil } diff --git a/protocol/x/clob/keeper/grpc_query_block_rate_limit_configuration_test.go b/protocol/x/clob/keeper/grpc_query_block_rate_limit_configuration_test.go index 1d489dfefa..56ffcb8531 100644 --- a/protocol/x/clob/keeper/grpc_query_block_rate_limit_configuration_test.go +++ b/protocol/x/clob/keeper/grpc_query_block_rate_limit_configuration_test.go @@ -1,12 +1,13 @@ package keeper_test import ( + "testing" + testApp "github.com/dydxprotocol/v4-chain/protocol/testutil/app" "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "testing" ) func TestGetBlockRateLimitConfiguration(t *testing.T) { @@ -19,10 +20,10 @@ func TestGetBlockRateLimitConfiguration(t *testing.T) { req: &types.QueryBlockRateLimitConfigurationRequest{}, res: &types.QueryBlockRateLimitConfigurationResponse{ BlockRateLimitConfig: types.BlockRateLimitConfiguration{ - MaxShortTermOrdersPerNBlocks: []types.MaxPerNBlocksRateLimit{ + MaxShortTermOrdersAndCancelsPerNBlocks: []types.MaxPerNBlocksRateLimit{ { NumBlocks: 1, - Limit: 200, + Limit: 400, }, }, MaxStatefulOrdersPerNBlocks: []types.MaxPerNBlocksRateLimit{ @@ -35,12 +36,6 @@ func TestGetBlockRateLimitConfiguration(t *testing.T) { Limit: 20, }, }, - MaxShortTermOrderCancellationsPerNBlocks: []types.MaxPerNBlocksRateLimit{ - { - NumBlocks: 1, - Limit: 200, - }, - }, }, }, }, diff --git a/protocol/x/clob/keeper/keeper.go b/protocol/x/clob/keeper/keeper.go index 370ef8d3ac..300cc2a066 100644 --- a/protocol/x/clob/keeper/keeper.go +++ b/protocol/x/clob/keeper/keeper.go @@ -56,8 +56,7 @@ type ( // Note that the antehandler is not set until after the BaseApp antehandler is also set. antehandler sdk.AnteHandler - placeOrderRateLimiter rate_limit.RateLimiter[*types.MsgPlaceOrder] - cancelOrderRateLimiter rate_limit.RateLimiter[*types.MsgCancelOrder] + placeCancelOrderRateLimiter rate_limit.RateLimiter[sdk.Msg] DaemonLiquidationInfo *liquidationtypes.DaemonLiquidationInfo } @@ -88,8 +87,7 @@ func NewKeeper( grpcStreamingManager streamingtypes.GrpcStreamingManager, txDecoder sdk.TxDecoder, clobFlags flags.ClobFlags, - placeOrderRateLimiter rate_limit.RateLimiter[*types.MsgPlaceOrder], - cancelOrderRateLimiter rate_limit.RateLimiter[*types.MsgCancelOrder], + placeCancelOrderRateLimiter rate_limit.RateLimiter[sdk.Msg], daemonLiquidationInfo *liquidationtypes.DaemonLiquidationInfo, ) *Keeper { keeper := &Keeper{ @@ -119,10 +117,9 @@ func NewKeeper( Hosts: clobFlags.MevTelemetryHosts, Identifier: clobFlags.MevTelemetryIdentifier, }, - Flags: clobFlags, - placeOrderRateLimiter: placeOrderRateLimiter, - cancelOrderRateLimiter: cancelOrderRateLimiter, - DaemonLiquidationInfo: daemonLiquidationInfo, + Flags: clobFlags, + placeCancelOrderRateLimiter: placeCancelOrderRateLimiter, + DaemonLiquidationInfo: daemonLiquidationInfo, } // Provide the keeper to the MemClob. diff --git a/protocol/x/clob/keeper/msg_server_update_block_rate_limit_config_test.go b/protocol/x/clob/keeper/msg_server_update_block_rate_limit_config_test.go index e5ec7bf9b2..04bd635e78 100644 --- a/protocol/x/clob/keeper/msg_server_update_block_rate_limit_config_test.go +++ b/protocol/x/clob/keeper/msg_server_update_block_rate_limit_config_test.go @@ -1,6 +1,8 @@ package keeper_test import ( + "testing" + "github.com/cometbft/cometbft/types" "github.com/dydxprotocol/v4-chain/protocol/lib" testapp "github.com/dydxprotocol/v4-chain/protocol/testutil/app" @@ -8,7 +10,6 @@ import ( clobtypes "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" "github.com/stretchr/testify/require" - "testing" ) func TestUpdateBlockRateLimitConfig(t *testing.T) { @@ -22,7 +23,7 @@ func TestUpdateBlockRateLimitConfig(t *testing.T) { }) testapp.UpdateGenesisDocWithAppStateForModule(&genesis, func(state *clobtypes.GenesisState) { state.BlockRateLimitConfig = clobtypes.BlockRateLimitConfiguration{ - MaxShortTermOrdersPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ + MaxShortTermOrdersAndCancelsPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ { NumBlocks: 1, Limit: 2, @@ -34,19 +35,13 @@ func TestUpdateBlockRateLimitConfig(t *testing.T) { Limit: 4, }, }, - MaxShortTermOrderCancellationsPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ - { - NumBlocks: 5, - Limit: 6, - }, - }, } }) return genesis }).Build() expectedConfig := clobtypes.BlockRateLimitConfiguration{ - MaxShortTermOrdersPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ + MaxShortTermOrdersAndCancelsPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ { NumBlocks: 7, Limit: 8, @@ -58,12 +53,6 @@ func TestUpdateBlockRateLimitConfig(t *testing.T) { Limit: 10, }, }, - MaxShortTermOrderCancellationsPerNBlocks: []clobtypes.MaxPerNBlocksRateLimit{ - { - NumBlocks: 11, - Limit: 12, - }, - }, } ctx := tApp.InitChain() diff --git a/protocol/x/clob/keeper/rate_limit.go b/protocol/x/clob/keeper/rate_limit.go index 6079b82987..3be72e2d87 100644 --- a/protocol/x/clob/keeper/rate_limit.go +++ b/protocol/x/clob/keeper/rate_limit.go @@ -31,8 +31,7 @@ func (k *Keeper) RateLimitCancelOrder(ctx sdk.Context, msg *types.MsgCancelOrder return err } } - - return k.cancelOrderRateLimiter.RateLimit(ctx, msg) + return k.placeCancelOrderRateLimiter.RateLimit(ctx, msg) } // RateLimitPlaceOrder passes orders with valid clob pairs to `placeOrderRateLimiter`. @@ -60,10 +59,37 @@ func (k *Keeper) RateLimitPlaceOrder(ctx sdk.Context, msg *types.MsgPlaceOrder) } } - return k.placeOrderRateLimiter.RateLimit(ctx, msg) + return k.placeCancelOrderRateLimiter.RateLimit(ctx, msg) +} + +// RateLimitBatchCancel passes orders with valid clob pairs to `placeOrderRateLimiter`. +// The rate limiting is only performed during `CheckTx` and `ReCheckTx`. +func (k *Keeper) RateLimitBatchCancel(ctx sdk.Context, msg *types.MsgBatchCancel) error { + // Only rate limit during `CheckTx` and `ReCheckTx`. + if lib.IsDeliverTxMode(ctx) { + return nil + } + + for _, batch := range msg.ShortTermCancels { + _, found := k.GetClobPair(ctx, types.ClobPairId(batch.GetClobPairId())) + // If the clob pair isn't found then we expect order validation to fail the order as being invalid. + if !found { + return nil + } + } + + // Ensure that the GTB is valid before we attempt to rate limit. This is to prevent a replay attack + // where short-term order placements with GTBs in the past or the far future could be replayed by an adversary. + // Normally transaction replay attacks rely on sequence numbers being part of the signature and being incremented + // for each transaction but sequence number verification is skipped for short-term orders. + nextBlockHeight := lib.MustConvertIntegerToUint32(ctx.BlockHeight() + 1) + if err := k.validateGoodTilBlock(msg.GetGoodTilBlock(), nextBlockHeight); err != nil { + return err + } + + return k.placeCancelOrderRateLimiter.RateLimit(ctx, msg) } func (k *Keeper) PruneRateLimits(ctx sdk.Context) { - k.placeOrderRateLimiter.PruneRateLimits(ctx) - k.cancelOrderRateLimiter.PruneRateLimits(ctx) + k.placeCancelOrderRateLimiter.PruneRateLimits(ctx) } diff --git a/protocol/x/clob/module_test.go b/protocol/x/clob/module_test.go index 45c349e212..df3e7e95f1 100644 --- a/protocol/x/clob/module_test.go +++ b/protocol/x/clob/module_test.go @@ -46,9 +46,8 @@ func getValidGenesisStr() string { gs += `{"max_notional_liquidated":"100000000000000","max_quantums_insurance_lost":"100000000000000"},` gs += `"fillable_price_config":{"bankruptcy_adjustment_ppm":1000000,` gs += `"spread_to_maintenance_margin_ratio_ppm":100000}},"block_rate_limit_config":` - gs += `{"max_short_term_orders_per_n_blocks":[{"limit": 200,"num_blocks":1}],` - gs += `"max_stateful_orders_per_n_blocks":[{"limit": 2,"num_blocks":1},{"limit": 20,"num_blocks":100}],` - gs += `"max_short_term_order_cancellations_per_n_blocks":[{"limit": 200,"num_blocks":1}]},` + gs += `{"max_short_term_orders_and_cancels_per_n_blocks":[{"limit": 400,"num_blocks":1}],` + gs += `"max_stateful_orders_per_n_blocks":[{"limit": 2,"num_blocks":1},{"limit": 20,"num_blocks":100}]},` gs += `"equity_tier_limit_config":{"short_term_order_equity_tiers":[{"limit":0,"usd_tnc_required":"0"},` gs += `{"limit":1,"usd_tnc_required":"20"},{"limit":5,"usd_tnc_required":"100"},` gs += `{"limit":10,"usd_tnc_required":"1000"},{"limit":100,"usd_tnc_required":"10000"},` @@ -167,8 +166,8 @@ func TestAppModuleBasic_DefaultGenesis(t *testing.T) { expected += `{"max_notional_liquidated":"100000000000000","max_quantums_insurance_lost":"100000000000000"},` expected += `"fillable_price_config":{"bankruptcy_adjustment_ppm":1000000,` expected += `"spread_to_maintenance_margin_ratio_ppm":100000}},"block_rate_limit_config":` - expected += `{"max_short_term_orders_per_n_blocks":[],"max_stateful_orders_per_n_blocks":[],` - expected += `"max_short_term_order_cancellations_per_n_blocks":[]},` + expected += `{"max_short_term_orders_and_cancels_per_n_blocks":[],"max_stateful_orders_per_n_blocks":[],` + expected += `"max_short_term_order_cancellations_per_n_blocks":[],"max_short_term_orders_per_n_blocks":[]},` expected += `"equity_tier_limit_config":{"short_term_order_equity_tiers":[], "stateful_order_equity_tiers":[]}}` require.JSONEq(t, expected, string(json)) @@ -337,9 +336,9 @@ func TestAppModule_InitExportGenesis(t *testing.T) { require.Equal( t, clob_types.BlockRateLimitConfiguration{ - MaxShortTermOrdersPerNBlocks: []clob_types.MaxPerNBlocksRateLimit{ + MaxShortTermOrdersAndCancelsPerNBlocks: []clob_types.MaxPerNBlocksRateLimit{ { - Limit: 200, + Limit: 400, NumBlocks: 1, }, }, @@ -353,12 +352,6 @@ func TestAppModule_InitExportGenesis(t *testing.T) { NumBlocks: 100, }, }, - MaxShortTermOrderCancellationsPerNBlocks: []clob_types.MaxPerNBlocksRateLimit{ - { - Limit: 200, - NumBlocks: 1, - }, - }, }, blockRateLimitConfig, ) @@ -433,10 +426,10 @@ func TestAppModule_InitExportGenesis(t *testing.T) { expected += `{"max_notional_liquidated":"100000000000000","max_quantums_insurance_lost":"100000000000000"},` expected += `"fillable_price_config":{"bankruptcy_adjustment_ppm":1000000,` expected += `"spread_to_maintenance_margin_ratio_ppm":100000}},"block_rate_limit_config":` - expected += `{"max_short_term_orders_per_n_blocks":[{"limit": 200,"num_blocks":1}],` + expected += `{"max_short_term_orders_and_cancels_per_n_blocks":[{"limit": 400,"num_blocks":1}],` expected += `"max_stateful_orders_per_n_blocks":[{"limit": 2,"num_blocks":1},` - expected += `{"limit": 20,"num_blocks":100}],"max_short_term_order_cancellations_per_n_blocks":` - expected += `[{"limit": 200,"num_blocks":1}]},` + expected += `{"limit": 20,"num_blocks":100}],"max_short_term_order_cancellations_per_n_blocks":[],` + expected += `"max_short_term_orders_per_n_blocks":[]},` expected += `"equity_tier_limit_config":{"short_term_order_equity_tiers":[{"limit":0,"usd_tnc_required":"0"},` expected += `{"limit":1,"usd_tnc_required":"20"},{"limit":5,"usd_tnc_required":"100"},` expected += `{"limit":10,"usd_tnc_required":"1000"},{"limit":100,"usd_tnc_required":"10000"},` diff --git a/protocol/x/clob/rate_limit/multi_block_rate_limiter.go b/protocol/x/clob/rate_limit/multi_block_rate_limiter.go index a5f3e3638b..74173228be 100644 --- a/protocol/x/clob/rate_limit/multi_block_rate_limiter.go +++ b/protocol/x/clob/rate_limit/multi_block_rate_limiter.go @@ -1,10 +1,11 @@ package rate_limit import ( - errorsmod "cosmossdk.io/errors" "fmt" "sort" + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/dydxprotocol/v4-chain/protocol/lib" "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" @@ -66,6 +67,10 @@ func NewMultiBlockRateLimiter[K comparable](context string, config []types.MaxPe } func (r *multiBlockRateLimiter[K]) RateLimit(ctx sdk.Context, key K) error { + return r.RateLimitIncrBy(ctx, key, 1) +} + +func (r *multiBlockRateLimiter[K]) RateLimitIncrBy(ctx sdk.Context, key K, incrBy uint32) error { blockHeight := lib.MustConvertIntegerToUint32(ctx.BlockHeight()) offset := blockHeight % r.maxNumBlocks @@ -83,7 +88,7 @@ func (r *multiBlockRateLimiter[K]) RateLimit(ctx sdk.Context, key K) error { perBlockCounts = make(map[uint32]uint32) r.perKeyBlockCounts[key] = perBlockCounts } - count := perBlockCounts[blockHeight] + 1 + count := perBlockCounts[blockHeight] + incrBy perBlockCounts[blockHeight] = count // Update the per rate limit count. @@ -93,7 +98,7 @@ func (r *multiBlockRateLimiter[K]) RateLimit(ctx sdk.Context, key K) error { r.perKeyRateLimitCounts[key] = perRateLimitCounts } for i := range perRateLimitCounts { - perRateLimitCounts[i] += 1 + perRateLimitCounts[i] += incrBy } // Check the accumulated rate limit count to see if any rate limit has been exceeded. diff --git a/protocol/x/clob/rate_limit/noop_rate_limiter.go b/protocol/x/clob/rate_limit/noop_rate_limiter.go index 824020a80d..391251beec 100644 --- a/protocol/x/clob/rate_limit/noop_rate_limiter.go +++ b/protocol/x/clob/rate_limit/noop_rate_limiter.go @@ -19,5 +19,9 @@ func (n noOpRateLimiter[K]) RateLimit(ctx sdk.Context, key K) error { return nil } +func (n noOpRateLimiter[K]) RateLimitIncrBy(ctx sdk.Context, key K, incrBy uint32) error { + return nil +} + func (n noOpRateLimiter[K]) PruneRateLimits(ctx sdk.Context) { } diff --git a/protocol/x/clob/rate_limit/order_rate_limiter.go b/protocol/x/clob/rate_limit/order_rate_limiter.go index 19e3d03631..911e927424 100644 --- a/protocol/x/clob/rate_limit/order_rate_limiter.go +++ b/protocol/x/clob/rate_limit/order_rate_limiter.go @@ -8,22 +8,28 @@ import ( "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" ) -// A RateLimiter which rate limits types.MsgPlaceOrder. +var ( + BATCH_CANCEL_RATE_LIMIT_WEIGHT = uint32(2) +) + +// A RateLimiter which rate limits types.MsgPlaceOrder, types.MsgCancelOrder, and +// types.MsgBatchCancel. // // The rate limiting keeps track of short term and stateful orders placed during // CheckTx. -type placeOrderRateLimiter struct { - checkStateShortTermOrderRateLimiter RateLimiter[string] - checkStateStatefulOrderRateLimiter RateLimiter[string] +type placeAndCancelOrderRateLimiter struct { + checkStateShortTermOrderPlaceCancelRateLimiter RateLimiter[string] + checkStateStatefulOrderRateLimiter RateLimiter[string] // The set of rate limited accounts is only stored for telemetry purposes. rateLimitedAccounts map[string]bool } -var _ RateLimiter[*types.MsgPlaceOrder] = (*placeOrderRateLimiter)(nil) +var _ RateLimiter[sdk.Msg] = (*placeAndCancelOrderRateLimiter)(nil) -// NewPlaceOrderRateLimiter returns a RateLimiter which rate limits types.MsgPlaceOrder based upon the provided -// types.BlockRateLimitConfiguration. The rate limiter currently supports limiting based upon: -// - how many short term orders per account (by using string). +// NewPlaceCancelOrderRateLimiter returns a RateLimiter which rate limits types.MsgPlaceOrder, types.MsgCancelOrder, +// types.MsgBatchCancel based upon the provided types.BlockRateLimitConfiguration. The rate limiter currently +// supports limiting based upon: +// - how many short term place/cancel orders per account (by using string). // - how many stateful order per account (by using string). // // The rate limiting must only be used during `CheckTx` because the rate limiting information is not recovered @@ -34,31 +40,31 @@ var _ RateLimiter[*types.MsgPlaceOrder] = (*placeOrderRateLimiter)(nil) // - `ctx.BlockHeight()` in PruneRateLimits and should be invoked during `EndBlocker`. If invoked // during `PrepareCheckState` one must supply a `ctx` with the previous block height via // `ctx.WithBlockHeight(ctx.BlockHeight()-1)`. -func NewPlaceOrderRateLimiter(config types.BlockRateLimitConfiguration) RateLimiter[*types.MsgPlaceOrder] { +func NewPlaceCancelOrderRateLimiter(config types.BlockRateLimitConfiguration) RateLimiter[sdk.Msg] { if err := config.Validate(); err != nil { panic(err) } // Return the no-op rate limiter if the configuration is empty. - if len(config.MaxShortTermOrdersPerNBlocks)+len(config.MaxStatefulOrdersPerNBlocks) == 0 { - return noOpRateLimiter[*types.MsgPlaceOrder]{} + if len(config.MaxShortTermOrdersAndCancelsPerNBlocks)+len(config.MaxStatefulOrdersPerNBlocks) == 0 { + return noOpRateLimiter[sdk.Msg]{} } - r := placeOrderRateLimiter{ + r := placeAndCancelOrderRateLimiter{ rateLimitedAccounts: make(map[string]bool, 0), } - if len(config.MaxShortTermOrdersPerNBlocks) == 0 { - r.checkStateShortTermOrderRateLimiter = NewNoOpRateLimiter[string]() - } else if len(config.MaxShortTermOrdersPerNBlocks) == 1 && - config.MaxShortTermOrdersPerNBlocks[0].NumBlocks == 1 { - r.checkStateShortTermOrderRateLimiter = NewSingleBlockRateLimiter[string]( - "MaxShortTermOrdersPerNBlocks", - config.MaxShortTermOrdersPerNBlocks[0], + if len(config.MaxShortTermOrdersAndCancelsPerNBlocks) == 0 { + r.checkStateShortTermOrderPlaceCancelRateLimiter = NewNoOpRateLimiter[string]() + } else if len(config.MaxShortTermOrdersAndCancelsPerNBlocks) == 1 && + config.MaxShortTermOrdersAndCancelsPerNBlocks[0].NumBlocks == 1 { + r.checkStateShortTermOrderPlaceCancelRateLimiter = NewSingleBlockRateLimiter[string]( + "MaxShortTermOrdersAndCancelsPerNBlocks", + config.MaxShortTermOrdersAndCancelsPerNBlocks[0], ) } else { - r.checkStateShortTermOrderRateLimiter = NewMultiBlockRateLimiter[string]( - "MaxShortTermOrdersPerNBlocks", - config.MaxShortTermOrdersPerNBlocks, + r.checkStateShortTermOrderPlaceCancelRateLimiter = NewMultiBlockRateLimiter[string]( + "MaxShortTermOrdersAndCancelsPerNBlocks", + config.MaxShortTermOrdersAndCancelsPerNBlocks, ) } if len(config.MaxStatefulOrdersPerNBlocks) == 0 { @@ -79,11 +85,27 @@ func NewPlaceOrderRateLimiter(config types.BlockRateLimitConfiguration) RateLimi return &r } -func (r *placeOrderRateLimiter) RateLimit(ctx sdk.Context, msg *types.MsgPlaceOrder) (err error) { +func (r *placeAndCancelOrderRateLimiter) RateLimit(ctx sdk.Context, msg sdk.Msg) (err error) { lib.AssertCheckTxMode(ctx) + switch castedMsg := (msg).(type) { + case *types.MsgCancelOrder: + err = r.RateLimitCancelOrder(ctx, *castedMsg) + case *types.MsgPlaceOrder: + err = r.RateLimitPlaceOrder(ctx, *castedMsg) + case *types.MsgBatchCancel: + err = r.RateLimitBatchCancelOrder(ctx, *castedMsg) + } + return err +} +func (r *placeAndCancelOrderRateLimiter) RateLimitIncrBy(ctx sdk.Context, msg sdk.Msg, incrBy uint32) (err error) { + panic("PlaceAndCancelOrderRateLimiter is a top-level rate limiter. It should not use IncrBy.") +} + +func (r *placeAndCancelOrderRateLimiter) RateLimitPlaceOrder(ctx sdk.Context, msg types.MsgPlaceOrder) (err error) { + lib.AssertCheckTxMode(ctx) if msg.Order.IsShortTermOrder() { - err = r.checkStateShortTermOrderRateLimiter.RateLimit( + err = r.checkStateShortTermOrderPlaceCancelRateLimiter.RateLimit( ctx, msg.Order.OrderId.SubaccountId.Owner, ) @@ -103,81 +125,14 @@ func (r *placeOrderRateLimiter) RateLimit(ctx sdk.Context, msg *types.MsgPlaceOr return err } -func (r *placeOrderRateLimiter) PruneRateLimits(ctx sdk.Context) { - telemetry.IncrCounter( - float32(len(r.rateLimitedAccounts)), - types.ModuleName, - metrics.RateLimit, - metrics.PlaceOrderAccounts, - metrics.Count, - ) - // Note that this method for clearing the map is optimized by the go compiler significantly - // and will leave the relative size of the map the same so that it doesn't need to be resized - // often. - for key := range r.rateLimitedAccounts { - delete(r.rateLimitedAccounts, key) - } - r.checkStateShortTermOrderRateLimiter.PruneRateLimits(ctx) - r.checkStateStatefulOrderRateLimiter.PruneRateLimits(ctx) -} - -// A RateLimiter which rate limits types.MsgCancelOrder. -// -// The rate limiting keeps track of short term order cancellations during CheckTx. -type cancelOrderRateLimiter struct { - checkStateShortTermRateLimiter RateLimiter[string] - // The set of rate limited accounts is only stored for telemetry purposes. - rateLimitedAccounts map[string]bool -} - -var _ RateLimiter[*types.MsgCancelOrder] = (*cancelOrderRateLimiter)(nil) - -// NewCancelOrderRateLimiter returns a RateLimiter which rate limits types.MsgCancelOrder based upon the provided -// types.BlockRateLimitConfiguration. The rate limiter currently supports limiting based upon: -// - how many short term order cancellations per account (by using string). -// -// The rate limiting must only be used during `CheckTx` because the rate limiting information is not recovered -// on application restart preventing it from being deterministic during `DeliverTx`. -// -// Depending upon the provided types.BlockRateLimitConfiguration, the returned RateLimiter may rely on: -// - `ctx.BlockHeight()` in RateLimit to track which block the rate limit should apply to. -// - `ctx.BlockHeight()` in PruneRateLimits and should be invoked during `EndBlocker`. If invoked -// during `PrepareCheckState` one must supply a `ctx` with the previous block height via -// `ctx.WithBlockHeight(ctx.BlockHeight()-1)`. -func NewCancelOrderRateLimiter(config types.BlockRateLimitConfiguration) RateLimiter[*types.MsgCancelOrder] { - if err := config.Validate(); err != nil { - panic(err) - } - - // Return the no-op rate limiter if the configuration is empty. - if len(config.MaxShortTermOrderCancellationsPerNBlocks) == 0 { - return noOpRateLimiter[*types.MsgCancelOrder]{} - } - - rateLimiter := cancelOrderRateLimiter{ - rateLimitedAccounts: make(map[string]bool, 0), - } - if len(config.MaxShortTermOrderCancellationsPerNBlocks) == 1 && - config.MaxShortTermOrderCancellationsPerNBlocks[0].NumBlocks == 1 { - rateLimiter.checkStateShortTermRateLimiter = NewSingleBlockRateLimiter[string]( - "MaxShortTermOrdersPerNBlocks", - config.MaxShortTermOrderCancellationsPerNBlocks[0], - ) - return &rateLimiter - } else { - rateLimiter.checkStateShortTermRateLimiter = NewMultiBlockRateLimiter[string]( - "MaxShortTermOrdersPerNBlocks", - config.MaxShortTermOrderCancellationsPerNBlocks, - ) - return &rateLimiter - } -} - -func (r *cancelOrderRateLimiter) RateLimit(ctx sdk.Context, msg *types.MsgCancelOrder) (err error) { +func (r *placeAndCancelOrderRateLimiter) RateLimitCancelOrder( + ctx sdk.Context, + msg types.MsgCancelOrder, +) (err error) { lib.AssertCheckTxMode(ctx) if msg.OrderId.IsShortTermOrder() { - err = r.checkStateShortTermRateLimiter.RateLimit( + err = r.checkStateShortTermOrderPlaceCancelRateLimiter.RateLimit( ctx, msg.OrderId.SubaccountId.Owner, ) @@ -193,12 +148,36 @@ func (r *cancelOrderRateLimiter) RateLimit(ctx sdk.Context, msg *types.MsgCancel return err } -func (r *cancelOrderRateLimiter) PruneRateLimits(ctx sdk.Context) { +func (r *placeAndCancelOrderRateLimiter) RateLimitBatchCancelOrder( + ctx sdk.Context, + msg types.MsgBatchCancel, +) (err error) { + lib.AssertCheckTxMode(ctx) + + // TODO(CT-688) Use a scaling function such as (1 + ceil(0.1 * #cancels)) to calculate batch + // cancel rate limit weights. + err = r.checkStateShortTermOrderPlaceCancelRateLimiter.RateLimitIncrBy( + ctx, + msg.SubaccountId.Owner, + BATCH_CANCEL_RATE_LIMIT_WEIGHT, + ) + if err != nil { + telemetry.IncrCounterWithLabels( + []string{types.ModuleName, metrics.RateLimit, metrics.PlaceOrder, metrics.Count}, + 1, + []metrics.Label{}, + ) + r.rateLimitedAccounts[msg.SubaccountId.Owner] = true + } + return err +} + +func (r *placeAndCancelOrderRateLimiter) PruneRateLimits(ctx sdk.Context) { telemetry.IncrCounter( float32(len(r.rateLimitedAccounts)), types.ModuleName, metrics.RateLimit, - metrics.CancelOrderAccounts, + metrics.PlaceOrderAccounts, metrics.Count, ) // Note that this method for clearing the map is optimized by the go compiler significantly @@ -207,5 +186,6 @@ func (r *cancelOrderRateLimiter) PruneRateLimits(ctx sdk.Context) { for key := range r.rateLimitedAccounts { delete(r.rateLimitedAccounts, key) } - r.checkStateShortTermRateLimiter.PruneRateLimits(ctx) + r.checkStateShortTermOrderPlaceCancelRateLimiter.PruneRateLimits(ctx) + r.checkStateStatefulOrderRateLimiter.PruneRateLimits(ctx) } diff --git a/protocol/x/clob/rate_limit/panic_rate_limiter.go b/protocol/x/clob/rate_limit/panic_rate_limiter.go index 310cd04dcc..3fd024ce1e 100644 --- a/protocol/x/clob/rate_limit/panic_rate_limiter.go +++ b/protocol/x/clob/rate_limit/panic_rate_limiter.go @@ -19,6 +19,10 @@ func (n panicRateLimiter[K]) RateLimit(ctx sdk.Context, key K) error { panic("Unexpected invocation of RateLimit") } +func (n panicRateLimiter[K]) RateLimitIncrBy(ctx sdk.Context, key K, incrBy uint32) error { + panic("Unexpected invocation of RateLimitIncrBy") +} + func (n panicRateLimiter[K]) PruneRateLimits(ctx sdk.Context) { panic("Unexpected invocation of PruneRateLimits") } diff --git a/protocol/x/clob/rate_limit/rate_limit.go b/protocol/x/clob/rate_limit/rate_limit.go index 32f673b62a..d0a5ae305a 100644 --- a/protocol/x/clob/rate_limit/rate_limit.go +++ b/protocol/x/clob/rate_limit/rate_limit.go @@ -8,6 +8,9 @@ import ( type RateLimiter[K any] interface { // Returns an error if the RateLimiter exceeds any configured rate limits for the key K and context state. RateLimit(ctx sdk.Context, key K) error + // Returns an error if the RateLimiter exceeds any configured rate limits for the key K and context state + // Increments the rate limit counter by incrBy. + RateLimitIncrBy(ctx sdk.Context, key K, incrBy uint32) error // Prunes rate limits for the provided context. PruneRateLimits(ctx sdk.Context) } diff --git a/protocol/x/clob/rate_limit/single_block_rate_limiter.go b/protocol/x/clob/rate_limit/single_block_rate_limiter.go index 8ba6f670c5..07da1cd5fe 100644 --- a/protocol/x/clob/rate_limit/single_block_rate_limiter.go +++ b/protocol/x/clob/rate_limit/single_block_rate_limiter.go @@ -1,9 +1,10 @@ package rate_limit import ( - errorsmod "cosmossdk.io/errors" "fmt" + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" ) @@ -33,7 +34,11 @@ func NewSingleBlockRateLimiter[K comparable](context string, config types.MaxPer } func (r *singleBlockRateLimiter[K]) RateLimit(ctx sdk.Context, key K) error { - count := r.perKeyCounts[key] + 1 + return r.RateLimitIncrBy(ctx, key, 1) +} + +func (r *singleBlockRateLimiter[K]) RateLimitIncrBy(ctx sdk.Context, key K, incrBy uint32) error { + count := r.perKeyCounts[key] + incrBy r.perKeyCounts[key] = count if count > r.config.Limit { return errorsmod.Wrapf( diff --git a/protocol/x/clob/types/block_rate_limit_config.go b/protocol/x/clob/types/block_rate_limit_config.go index 7970be3353..9d784eb9c6 100644 --- a/protocol/x/clob/types/block_rate_limit_config.go +++ b/protocol/x/clob/types/block_rate_limit_config.go @@ -5,32 +5,27 @@ import ( ) const ( - MaxShortTermOrdersPerNBlocksNumBlocks = 1_000 - MaxShortTermOrdersPerNBlocksLimit = 10_000_000 - MaxShortTermOrderCancellationsPerNBlocksNumBlocks = 1_000 - MaxShortTermOrderCancellationsPerNBlocksLimit = 10_000_000 - MaxStatefulOrdersPerNBlocksNumBlocks = 10_000 - MaxStatefulOrdersPerNBlocksLimit = 1_000_000 + MaxShortTermOrdersAndCancelsPerNBlocksNumBlocks = 2_000 + MaxShortTermOrdersAndCancelsPerNBlocksLimit = 10_000_000 + MaxStatefulOrdersPerNBlocksNumBlocks = 10_000 + MaxStatefulOrdersPerNBlocksLimit = 1_000_000 ) // Validate validates each individual MaxPerNBlocksRateLimit. // It returns an error if any of the rate limits fail the following validations: -// - `Limit == 0` || `Limit > MaxShortTermOrdersPerNBlocksLimit` for short term order rate limits. -// - `NumBlocks == 0` || `NumBlocks > MaxShortTermOrdersPerNBlocksNumBlocks` for short term order rate +// - `Limit == 0` || `Limit > MaxShortTermOrdersAndCancelsPerNBlocksLimit` for short term order/cancel rate limits. +// - `NumBlocks == 0` || `NumBlocks > MaxShortTermOrdersAndCancelsPerNBlocksNumBlocks` +// for short term order/cancel rate // limits. // - `Limit == 0` || `Limit > MaxStatefulOrdersPerNBlocksLimit` for stateful order rate limits. // - `NumBlocks == 0` || `NumBlocks > MaxStatefulOrdersPerNBlocksNumBlocks` for stateful order rate limits. -// - `Limit == 0` || `Limit > MaxShortTermOrderCancellationsPerNBlocksNumBlocks` for short term order -// cancellation rate limits. -// - `NumBlocks == 0` || `NumBlocks > MaxShortTermOrderCancellationsPerNBlocksLimit` for short term order -// cancellation rate limits. -// - There are multiple rate limits for the same `NumBlocks` in `MaxShortTermOrdersPerNBlocks`, -// `MaxStatefulOrdersPerNBlocks`, or `MaxShortTermOrderCancellationsPerNBlocks`. +// - There are multiple rate limits for the same `NumBlocks` in `MaxShortTermOrdersAndCancelsPerNBlocks`, +// `MaxStatefulOrdersPerNBlocks`. func (lc BlockRateLimitConfiguration) Validate() error { - if err := (maxPerNBlocksRateLimits)(lc.MaxShortTermOrdersPerNBlocks).validate( - "MaxShortTermOrdersPerNBlocks", - MaxShortTermOrdersPerNBlocksNumBlocks, - MaxShortTermOrdersPerNBlocksLimit, + if err := (maxPerNBlocksRateLimits)(lc.MaxShortTermOrdersAndCancelsPerNBlocks).validate( + "MaxShortTermOrdersAndCancelsPerNBlocks", + MaxShortTermOrdersAndCancelsPerNBlocksNumBlocks, + MaxShortTermOrdersAndCancelsPerNBlocksLimit, ); err != nil { return err } @@ -41,13 +36,6 @@ func (lc BlockRateLimitConfiguration) Validate() error { ); err != nil { return err } - if err := (maxPerNBlocksRateLimits)(lc.MaxShortTermOrderCancellationsPerNBlocks).validate( - "MaxShortTermOrderCancellationsPerNBlocks", - MaxShortTermOrderCancellationsPerNBlocksNumBlocks, - MaxShortTermOrderCancellationsPerNBlocksLimit, - ); err != nil { - return err - } return nil } diff --git a/protocol/x/clob/types/block_rate_limit_config.pb.go b/protocol/x/clob/types/block_rate_limit_config.pb.go index b1f0be069e..13cf4cf5b7 100644 --- a/protocol/x/clob/types/block_rate_limit_config.pb.go +++ b/protocol/x/clob/types/block_rate_limit_config.pb.go @@ -31,7 +31,9 @@ type BlockRateLimitConfiguration struct { // configurations. // // Specifying 0 values disables this rate limit. - MaxShortTermOrdersPerNBlocks []MaxPerNBlocksRateLimit `protobuf:"bytes,1,rep,name=max_short_term_orders_per_n_blocks,json=maxShortTermOrdersPerNBlocks,proto3" json:"max_short_term_orders_per_n_blocks"` + // Deprecated in favor of `max_short_term_orders_and_cancels_per_n_blocks` + // for v5.x onwards. + MaxShortTermOrdersPerNBlocks []MaxPerNBlocksRateLimit `protobuf:"bytes,1,rep,name=max_short_term_orders_per_n_blocks,json=maxShortTermOrdersPerNBlocks,proto3" json:"max_short_term_orders_per_n_blocks"` // Deprecated: Do not use. // How many stateful order attempts (successful and failed) are allowed for // an account per N blocks. Note that the rate limits are applied // in an AND fashion such that an order placement must pass all rate limit @@ -45,7 +47,16 @@ type BlockRateLimitConfiguration struct { // rate limit configurations. // // Specifying 0 values disables this rate limit. - MaxShortTermOrderCancellationsPerNBlocks []MaxPerNBlocksRateLimit `protobuf:"bytes,3,rep,name=max_short_term_order_cancellations_per_n_blocks,json=maxShortTermOrderCancellationsPerNBlocks,proto3" json:"max_short_term_order_cancellations_per_n_blocks"` + // Deprecated in favor of `max_short_term_orders_and_cancels_per_n_blocks` + // for v5.x onwards. + MaxShortTermOrderCancellationsPerNBlocks []MaxPerNBlocksRateLimit `protobuf:"bytes,3,rep,name=max_short_term_order_cancellations_per_n_blocks,json=maxShortTermOrderCancellationsPerNBlocks,proto3" json:"max_short_term_order_cancellations_per_n_blocks"` // Deprecated: Do not use. + // How many short term order place and cancel attempts (successful and failed) + // are allowed for an account per N blocks. Note that the rate limits are + // applied in an AND fashion such that an order placement must pass all rate + // limit configurations. + // + // Specifying 0 values disables this rate limit. + MaxShortTermOrdersAndCancelsPerNBlocks []MaxPerNBlocksRateLimit `protobuf:"bytes,4,rep,name=max_short_term_orders_and_cancels_per_n_blocks,json=maxShortTermOrdersAndCancelsPerNBlocks,proto3" json:"max_short_term_orders_and_cancels_per_n_blocks"` } func (m *BlockRateLimitConfiguration) Reset() { *m = BlockRateLimitConfiguration{} } @@ -81,6 +92,7 @@ func (m *BlockRateLimitConfiguration) XXX_DiscardUnknown() { var xxx_messageInfo_BlockRateLimitConfiguration proto.InternalMessageInfo +// Deprecated: Do not use. func (m *BlockRateLimitConfiguration) GetMaxShortTermOrdersPerNBlocks() []MaxPerNBlocksRateLimit { if m != nil { return m.MaxShortTermOrdersPerNBlocks @@ -95,6 +107,7 @@ func (m *BlockRateLimitConfiguration) GetMaxStatefulOrdersPerNBlocks() []MaxPerN return nil } +// Deprecated: Do not use. func (m *BlockRateLimitConfiguration) GetMaxShortTermOrderCancellationsPerNBlocks() []MaxPerNBlocksRateLimit { if m != nil { return m.MaxShortTermOrderCancellationsPerNBlocks @@ -102,6 +115,13 @@ func (m *BlockRateLimitConfiguration) GetMaxShortTermOrderCancellationsPerNBlock return nil } +func (m *BlockRateLimitConfiguration) GetMaxShortTermOrdersAndCancelsPerNBlocks() []MaxPerNBlocksRateLimit { + if m != nil { + return m.MaxShortTermOrdersAndCancelsPerNBlocks + } + return nil +} + // Defines a rate limit over a specific number of blocks. type MaxPerNBlocksRateLimit struct { // How many blocks the rate limit is over. @@ -169,29 +189,32 @@ func init() { } var fileDescriptor_0b7d196450032f13 = []byte{ - // 351 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x92, 0xc1, 0x4e, 0xc2, 0x40, - 0x10, 0x40, 0x5b, 0x50, 0x13, 0xd7, 0x78, 0xb0, 0x21, 0x86, 0x88, 0x56, 0xc2, 0x09, 0x0f, 0xb6, - 0x89, 0x1a, 0x3f, 0x00, 0xae, 0xa2, 0x04, 0x3d, 0x79, 0xd9, 0x6c, 0x97, 0xa5, 0x34, 0xee, 0xee, - 0x90, 0xed, 0x96, 0x94, 0xbf, 0x30, 0xfe, 0x83, 0xff, 0xc2, 0x91, 0xa3, 0x27, 0x63, 0xe0, 0x47, - 0xcc, 0x2e, 0x04, 0x50, 0x38, 0x71, 0x6b, 0xa7, 0x33, 0xef, 0xcd, 0x4c, 0x07, 0x85, 0xdd, 0x51, - 0x37, 0x1f, 0x28, 0xd0, 0x40, 0x81, 0x87, 0x94, 0x43, 0x14, 0x46, 0x1c, 0xe8, 0x1b, 0x56, 0x44, - 0x33, 0xcc, 0x13, 0x91, 0x68, 0x4c, 0x41, 0xf6, 0x92, 0x38, 0xb0, 0x59, 0xde, 0xc9, 0x7a, 0x41, - 0x60, 0x0a, 0xce, 0x4a, 0x31, 0xc4, 0x60, 0x43, 0xa1, 0x79, 0x9a, 0x27, 0xd6, 0x3e, 0x8b, 0xa8, - 0xd2, 0x30, 0xa8, 0x0e, 0xd1, 0xec, 0xc1, 0x80, 0x9a, 0x96, 0x93, 0x29, 0xa2, 0x13, 0x90, 0xde, - 0x08, 0xd5, 0x04, 0xc9, 0x71, 0xda, 0x07, 0xa5, 0xb1, 0x66, 0x4a, 0x60, 0x50, 0x5d, 0xa6, 0x52, - 0x3c, 0x60, 0x0a, 0x4b, 0x6c, 0xbb, 0x48, 0xcb, 0x6e, 0xb5, 0x58, 0x3f, 0xba, 0xb9, 0x0a, 0x36, - 0xac, 0x41, 0x8b, 0xe4, 0x6d, 0xa6, 0x1e, 0xad, 0x22, 0x5d, 0x3a, 0x1a, 0x7b, 0xe3, 0xef, 0x4b, - 0xa7, 0x73, 0x2e, 0x48, 0xfe, 0x6c, 0xc8, 0x2f, 0x4c, 0x89, 0x27, 0xcb, 0x5d, 0x25, 0x7b, 0x43, - 0x54, 0xb5, 0x6a, 0x4d, 0x34, 0xeb, 0x65, 0x7c, 0xab, 0xb8, 0xb0, 0x9b, 0xb8, 0x62, 0xc4, 0x0b, - 0xee, 0x86, 0xf7, 0xc3, 0x45, 0xe1, 0xb6, 0x99, 0x31, 0x25, 0x92, 0x32, 0xce, 0xed, 0x62, 0xfe, - 0xf5, 0x51, 0xdc, 0xad, 0x8f, 0xfa, 0xc6, 0x02, 0x9a, 0xeb, 0x8e, 0x55, 0x61, 0xad, 0x85, 0x4e, - 0xb7, 0x93, 0xbc, 0x0b, 0x84, 0x64, 0x26, 0x56, 0x7f, 0xc2, 0xad, 0x1f, 0x77, 0x0e, 0x65, 0x26, - 0x16, 0xd3, 0x94, 0xd0, 0xbe, 0xbd, 0x8f, 0x72, 0xc1, 0x7e, 0x99, 0xbf, 0x34, 0xda, 0xe3, 0xa9, - 0xef, 0x4e, 0xa6, 0xbe, 0xfb, 0x33, 0xf5, 0xdd, 0xf7, 0x99, 0xef, 0x4c, 0x66, 0xbe, 0xf3, 0x35, - 0xf3, 0x9d, 0xd7, 0xfb, 0x38, 0xd1, 0xfd, 0x2c, 0x0a, 0x28, 0x88, 0xbf, 0x57, 0x37, 0xbc, 0xbb, - 0xa6, 0x7d, 0x92, 0xc8, 0x70, 0x19, 0xc9, 0xe7, 0x97, 0xa8, 0x47, 0x03, 0x96, 0x46, 0x07, 0x36, - 0x7c, 0xfb, 0x1b, 0x00, 0x00, 0xff, 0xff, 0x43, 0x4b, 0x98, 0xcb, 0xab, 0x02, 0x00, 0x00, + // 391 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x93, 0xc1, 0x6e, 0xda, 0x30, + 0x18, 0xc7, 0x63, 0x60, 0x48, 0xf3, 0xb4, 0xc3, 0x22, 0x34, 0xa1, 0xb1, 0x65, 0x88, 0xc3, 0xc4, + 0x0e, 0x4b, 0xa4, 0x6d, 0xea, 0xbd, 0x70, 0x2d, 0x2d, 0x4a, 0x7b, 0xea, 0xc5, 0x72, 0x1c, 0x13, + 0xa2, 0xc6, 0x36, 0x72, 0x1c, 0x14, 0xd4, 0x87, 0x68, 0x0f, 0x7d, 0x9b, 0xbe, 0x00, 0x47, 0x8e, + 0x3d, 0x55, 0x15, 0xbc, 0x48, 0x15, 0x07, 0x41, 0x4a, 0x72, 0x29, 0xb7, 0xe4, 0xf3, 0xe7, 0xff, + 0xef, 0xef, 0xbf, 0xfd, 0x41, 0xc7, 0x5f, 0xf8, 0xe9, 0x4c, 0x0a, 0x25, 0x88, 0x88, 0x1c, 0x12, + 0x09, 0xcf, 0xf1, 0x22, 0x41, 0x6e, 0x90, 0xc4, 0x8a, 0xa2, 0x28, 0x64, 0xa1, 0x42, 0x44, 0xf0, + 0x49, 0x18, 0xd8, 0xba, 0xcb, 0xfc, 0x52, 0xdc, 0x60, 0x67, 0x1b, 0xbe, 0xb5, 0x02, 0x11, 0x08, + 0x5d, 0x72, 0xb2, 0xaf, 0xbc, 0xb1, 0xf7, 0xd8, 0x80, 0x9d, 0x41, 0x26, 0xe5, 0x62, 0x45, 0xcf, + 0x32, 0xa1, 0xa1, 0xd6, 0x49, 0x24, 0x56, 0xa1, 0xe0, 0xe6, 0x2d, 0xec, 0x31, 0x9c, 0xa2, 0x78, + 0x2a, 0xa4, 0x42, 0x8a, 0x4a, 0x86, 0x84, 0xf4, 0xa9, 0x8c, 0xd1, 0x8c, 0x4a, 0xc4, 0x91, 0x76, + 0x11, 0xb7, 0x41, 0xb7, 0xde, 0xff, 0xf4, 0xf7, 0xb7, 0x5d, 0xa2, 0xda, 0x23, 0x9c, 0x8e, 0xa9, + 0x3c, 0xd7, 0x88, 0x78, 0xc7, 0x18, 0x34, 0x97, 0xcf, 0x3f, 0x8d, 0x36, 0x70, 0xbf, 0x33, 0x9c, + 0x5e, 0x66, 0xda, 0x57, 0x54, 0xb2, 0x0b, 0xad, 0xbc, 0x6f, 0x37, 0xe7, 0xb0, 0xab, 0xe1, 0x0a, + 0x2b, 0x3a, 0x49, 0xa2, 0x4a, 0x74, 0xed, 0xbd, 0xe8, 0x46, 0x86, 0x76, 0x3b, 0x19, 0x78, 0xab, + 0x5b, 0xe2, 0x3e, 0x00, 0xe8, 0x54, 0x9d, 0x1a, 0x11, 0xcc, 0x09, 0x8d, 0x22, 0x1d, 0xcd, 0x81, + 0x8f, 0xfa, 0xb1, 0x11, 0xf4, 0x4b, 0x11, 0x0c, 0x8b, 0x94, 0x82, 0xad, 0x3b, 0x00, 0xed, 0xea, + 0xcb, 0xc0, 0xdc, 0xdf, 0x7a, 0x3b, 0x70, 0xd5, 0x38, 0x2e, 0x9d, 0x5f, 0xe5, 0x6b, 0x39, 0xe5, + 0x7e, 0xee, 0xab, 0xe0, 0xa8, 0x37, 0x82, 0x5f, 0xab, 0x75, 0xcc, 0x1f, 0x10, 0xf2, 0x84, 0xed, + 0xdf, 0x07, 0xe8, 0x7f, 0x76, 0x3f, 0xf2, 0x84, 0x6d, 0x8f, 0xd2, 0x82, 0x1f, 0xf4, 0xab, 0x6d, + 0xd7, 0xf4, 0x4a, 0xfe, 0x33, 0x18, 0x2f, 0xd7, 0x16, 0x58, 0xad, 0x2d, 0xf0, 0xb2, 0xb6, 0xc0, + 0xfd, 0xc6, 0x32, 0x56, 0x1b, 0xcb, 0x78, 0xda, 0x58, 0xc6, 0xf5, 0x49, 0x10, 0xaa, 0x69, 0xe2, + 0xd9, 0x44, 0xb0, 0xb7, 0xb3, 0x30, 0xff, 0xff, 0x87, 0x4c, 0x71, 0xc8, 0x9d, 0x5d, 0x25, 0xcd, + 0xe7, 0x43, 0x2d, 0x66, 0x34, 0xf6, 0x9a, 0xba, 0xfc, 0xef, 0x35, 0x00, 0x00, 0xff, 0xff, 0x04, + 0x33, 0x7f, 0xa5, 0x41, 0x03, 0x00, 0x00, } func (m *BlockRateLimitConfiguration) Marshal() (dAtA []byte, err error) { @@ -214,6 +237,20 @@ func (m *BlockRateLimitConfiguration) MarshalToSizedBuffer(dAtA []byte) (int, er _ = i var l int _ = l + if len(m.MaxShortTermOrdersAndCancelsPerNBlocks) > 0 { + for iNdEx := len(m.MaxShortTermOrdersAndCancelsPerNBlocks) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.MaxShortTermOrdersAndCancelsPerNBlocks[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintBlockRateLimitConfig(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x22 + } + } if len(m.MaxShortTermOrderCancellationsPerNBlocks) > 0 { for iNdEx := len(m.MaxShortTermOrderCancellationsPerNBlocks) - 1; iNdEx >= 0; iNdEx-- { { @@ -327,6 +364,12 @@ func (m *BlockRateLimitConfiguration) Size() (n int) { n += 1 + l + sovBlockRateLimitConfig(uint64(l)) } } + if len(m.MaxShortTermOrdersAndCancelsPerNBlocks) > 0 { + for _, e := range m.MaxShortTermOrdersAndCancelsPerNBlocks { + l = e.Size() + n += 1 + l + sovBlockRateLimitConfig(uint64(l)) + } + } return n } @@ -482,6 +525,40 @@ func (m *BlockRateLimitConfiguration) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MaxShortTermOrdersAndCancelsPerNBlocks", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowBlockRateLimitConfig + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthBlockRateLimitConfig + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthBlockRateLimitConfig + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.MaxShortTermOrdersAndCancelsPerNBlocks = append(m.MaxShortTermOrdersAndCancelsPerNBlocks, MaxPerNBlocksRateLimit{}) + if err := m.MaxShortTermOrdersAndCancelsPerNBlocks[len(m.MaxShortTermOrdersAndCancelsPerNBlocks)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipBlockRateLimitConfig(dAtA[iNdEx:]) diff --git a/protocol/x/clob/types/clob_keeper.go b/protocol/x/clob/types/clob_keeper.go index 319e1bb80c..474d799b03 100644 --- a/protocol/x/clob/types/clob_keeper.go +++ b/protocol/x/clob/types/clob_keeper.go @@ -132,7 +132,11 @@ type ClobKeeper interface { GetIndexerEventManager() indexer_manager.IndexerEventManager RateLimitCancelOrder(ctx sdk.Context, order *MsgCancelOrder) error RateLimitPlaceOrder(ctx sdk.Context, order *MsgPlaceOrder) error + RateLimitBatchCancel(ctx sdk.Context, order *MsgBatchCancel) error InitializeBlockRateLimit(ctx sdk.Context, config BlockRateLimitConfiguration) error + GetBlockRateLimitConfiguration( + ctx sdk.Context, + ) (config BlockRateLimitConfiguration) InitializeEquityTierLimit(ctx sdk.Context, config EquityTierLimitConfiguration) error Logger(ctx sdk.Context) log.Logger UpdateClobPair( diff --git a/protocol/x/clob/types/genesis_test.go b/protocol/x/clob/types/genesis_test.go index 8cededd9cc..2bb266fd95 100644 --- a/protocol/x/clob/types/genesis_test.go +++ b/protocol/x/clob/types/genesis_test.go @@ -25,14 +25,14 @@ func TestGenesisState_Validate(t *testing.T) { "valid genesis state": { genState: &types.GenesisState{ BlockRateLimitConfig: types.BlockRateLimitConfiguration{ - MaxShortTermOrdersPerNBlocks: []types.MaxPerNBlocksRateLimit{ + MaxShortTermOrdersAndCancelsPerNBlocks: []types.MaxPerNBlocksRateLimit{ { NumBlocks: 1, Limit: 1, }, { - NumBlocks: types.MaxShortTermOrdersPerNBlocksNumBlocks, - Limit: types.MaxShortTermOrdersPerNBlocksLimit, + NumBlocks: types.MaxShortTermOrdersAndCancelsPerNBlocksNumBlocks, + Limit: types.MaxShortTermOrdersAndCancelsPerNBlocksLimit, }, }, MaxStatefulOrdersPerNBlocks: []types.MaxPerNBlocksRateLimit{ @@ -45,16 +45,6 @@ func TestGenesisState_Validate(t *testing.T) { Limit: types.MaxStatefulOrdersPerNBlocksLimit, }, }, - MaxShortTermOrderCancellationsPerNBlocks: []types.MaxPerNBlocksRateLimit{ - { - NumBlocks: 1, - Limit: 1, - }, - { - NumBlocks: types.MaxShortTermOrderCancellationsPerNBlocksNumBlocks, - Limit: types.MaxShortTermOrderCancellationsPerNBlocksLimit, - }, - }, }, ClobPairs: []types.ClobPair{ { @@ -269,7 +259,7 @@ func TestGenesisState_Validate(t *testing.T) { "max num blocks for short term order rate limit is zero": { genState: &types.GenesisState{ BlockRateLimitConfig: types.BlockRateLimitConfiguration{ - MaxShortTermOrdersPerNBlocks: []types.MaxPerNBlocksRateLimit{ + MaxShortTermOrdersAndCancelsPerNBlocks: []types.MaxPerNBlocksRateLimit{ { NumBlocks: 0, Limit: 1, @@ -277,12 +267,12 @@ func TestGenesisState_Validate(t *testing.T) { }, }, }, - expectedError: errors.New("0 is not a valid NumBlocks for MaxShortTermOrdersPerNBlocks"), + expectedError: errors.New("0 is not a valid NumBlocks for MaxShortTermOrdersAndCancelsPerNBlocks"), }, "max limit for short term order rate limit is zero": { genState: &types.GenesisState{ BlockRateLimitConfig: types.BlockRateLimitConfiguration{ - MaxShortTermOrdersPerNBlocks: []types.MaxPerNBlocksRateLimit{ + MaxShortTermOrdersAndCancelsPerNBlocks: []types.MaxPerNBlocksRateLimit{ { NumBlocks: 1, Limit: 0, @@ -290,7 +280,7 @@ func TestGenesisState_Validate(t *testing.T) { }, }, }, - expectedError: errors.New("0 is not a valid Limit for MaxShortTermOrdersPerNBlocks"), + expectedError: errors.New("0 is not a valid Limit for MaxShortTermOrdersAndCancelsPerNBlocks"), }, "max num blocks for stateful order rate limit is zero": { genState: &types.GenesisState{ @@ -318,59 +308,33 @@ func TestGenesisState_Validate(t *testing.T) { }, expectedError: errors.New("0 is not a valid Limit for MaxStatefulOrdersPerNBlocks"), }, - "max num blocks for short term order cancellation rate limit is zero": { - genState: &types.GenesisState{ - BlockRateLimitConfig: types.BlockRateLimitConfiguration{ - MaxShortTermOrderCancellationsPerNBlocks: []types.MaxPerNBlocksRateLimit{ - { - NumBlocks: 0, - Limit: 1, - }, - }, - }, - }, - expectedError: errors.New("0 is not a valid NumBlocks for MaxShortTermOrderCancellationsPerNBlocks"), - }, - "max limit for short term order cancellation rate limit is zero": { - genState: &types.GenesisState{ - BlockRateLimitConfig: types.BlockRateLimitConfiguration{ - MaxShortTermOrderCancellationsPerNBlocks: []types.MaxPerNBlocksRateLimit{ - { - NumBlocks: 1, - Limit: 0, - }, - }, - }, - }, - expectedError: errors.New("0 is not a valid Limit for MaxShortTermOrderCancellationsPerNBlocks"), - }, "max num blocks for short term order rate limit is greater than max": { genState: &types.GenesisState{ BlockRateLimitConfig: types.BlockRateLimitConfiguration{ - MaxShortTermOrdersPerNBlocks: []types.MaxPerNBlocksRateLimit{ + MaxShortTermOrdersAndCancelsPerNBlocks: []types.MaxPerNBlocksRateLimit{ { - NumBlocks: types.MaxShortTermOrdersPerNBlocksNumBlocks + 1, + NumBlocks: types.MaxShortTermOrdersAndCancelsPerNBlocksNumBlocks + 1, Limit: 1, }, }, }, }, - expectedError: fmt.Errorf("%d is not a valid NumBlocks for MaxShortTermOrdersPerNBlocks", - types.MaxShortTermOrdersPerNBlocksNumBlocks+1), + expectedError: fmt.Errorf("%d is not a valid NumBlocks for MaxShortTermOrdersAndCancelsPerNBlocks", + types.MaxShortTermOrdersAndCancelsPerNBlocksNumBlocks+1), }, "max limit for short term order rate limit is greater than max": { genState: &types.GenesisState{ BlockRateLimitConfig: types.BlockRateLimitConfiguration{ - MaxShortTermOrdersPerNBlocks: []types.MaxPerNBlocksRateLimit{ + MaxShortTermOrdersAndCancelsPerNBlocks: []types.MaxPerNBlocksRateLimit{ { NumBlocks: 1, - Limit: types.MaxShortTermOrdersPerNBlocksLimit + 1, + Limit: types.MaxShortTermOrdersAndCancelsPerNBlocksLimit + 1, }, }, }, }, - expectedError: fmt.Errorf("%d is not a valid Limit for MaxShortTermOrdersPerNBlocks", - types.MaxShortTermOrdersPerNBlocksLimit+1), + expectedError: fmt.Errorf("%d is not a valid Limit for MaxShortTermOrdersAndCancelsPerNBlocks", + types.MaxShortTermOrdersAndCancelsPerNBlocksLimit+1), }, "max num blocks for stateful order rate limit is greater than max": { genState: &types.GenesisState{ @@ -400,38 +364,10 @@ func TestGenesisState_Validate(t *testing.T) { expectedError: fmt.Errorf("%d is not a valid Limit for MaxStatefulOrdersPerNBlocks", types.MaxStatefulOrdersPerNBlocksLimit+1), }, - "max num blocks for short term order cancellation rate limit is greater than max": { - genState: &types.GenesisState{ - BlockRateLimitConfig: types.BlockRateLimitConfiguration{ - MaxShortTermOrderCancellationsPerNBlocks: []types.MaxPerNBlocksRateLimit{ - { - NumBlocks: types.MaxShortTermOrderCancellationsPerNBlocksNumBlocks + 1, - Limit: 1, - }, - }, - }, - }, - expectedError: fmt.Errorf("%d is not a valid NumBlocks for MaxShortTermOrderCancellationsPerNBlocks", - types.MaxShortTermOrdersPerNBlocksNumBlocks+1), - }, - "max limit for short term order cancellation rate limit is greater than max": { - genState: &types.GenesisState{ - BlockRateLimitConfig: types.BlockRateLimitConfiguration{ - MaxShortTermOrderCancellationsPerNBlocks: []types.MaxPerNBlocksRateLimit{ - { - NumBlocks: 1, - Limit: types.MaxShortTermOrderCancellationsPerNBlocksLimit + 1, - }, - }, - }, - }, - expectedError: fmt.Errorf("%d is not a valid Limit for MaxShortTermOrderCancellationsPerNBlocks", - types.MaxShortTermOrdersPerNBlocksLimit+1), - }, "duplicate short term order rate limit NumBlocks not allowed": { genState: &types.GenesisState{ BlockRateLimitConfig: types.BlockRateLimitConfiguration{ - MaxShortTermOrdersPerNBlocks: []types.MaxPerNBlocksRateLimit{ + MaxShortTermOrdersAndCancelsPerNBlocks: []types.MaxPerNBlocksRateLimit{ { NumBlocks: 1, Limit: 1, @@ -462,23 +398,6 @@ func TestGenesisState_Validate(t *testing.T) { }, expectedError: fmt.Errorf("Multiple rate limits"), }, - "duplicate short term order cancellation rate limit NumBlocks not allowed": { - genState: &types.GenesisState{ - BlockRateLimitConfig: types.BlockRateLimitConfiguration{ - MaxShortTermOrderCancellationsPerNBlocks: []types.MaxPerNBlocksRateLimit{ - { - NumBlocks: 1, - Limit: 1, - }, - { - NumBlocks: 1, - Limit: 2, - }, - }, - }, - }, - expectedError: fmt.Errorf("Multiple rate limits"), - }, "out of order short term order equity tier limit UsdTncRequired not allowed": { genState: &types.GenesisState{ EquityTierLimitConfig: types.EquityTierLimitConfiguration{