diff --git a/indexer/packages/compliance/src/constants.ts b/indexer/packages/compliance/src/constants.ts index a203b609f5..1d5556ec9c 100644 --- a/indexer/packages/compliance/src/constants.ts +++ b/indexer/packages/compliance/src/constants.ts @@ -1,2 +1,6 @@ -export const INDEXER_GEOBLOCKED_PAYLOAD = 'Because you appear to be a resident of, or trading from, a jurisdiction that violates our terms of use, or have engaged in activity that violates our terms of use, you have been blocked. You may withdraw your funds from the protocol at any time.'; -export const INDEXER_COMPLIANCE_BLOCKED_PAYLOAD = 'Because this address appears to be a resident of, or trading from, a jurisdiction that violates our terms of use, or has engaged in activity that violates our terms of use, this address has been blocked.'; +export const INDEXER_GEOBLOCKED_PAYLOAD: string = 'Because you appear to be a resident of, or trading from, a jurisdiction that violates our terms of use, or have engaged in activity that violates our terms of use, you have been blocked. You may withdraw your funds from the protocol at any time.'; +export const INDEXER_COMPLIANCE_BLOCKED_PAYLOAD: string = 'Because this address appears to be a resident of, or trading from, a jurisdiction that violates our terms of use, or has engaged in activity that violates our terms of use, this address has been blocked.'; + +// For use by other services packages, can't be used to index on the actual requests +// object as that needs to be a string literal. +export const COUNTRY_HEADER_KEY: string = 'cf-ipcountry'; diff --git a/indexer/packages/v4-protos/src/codegen/dydxprotocol/clob/query.rpc.Query.ts b/indexer/packages/v4-protos/src/codegen/dydxprotocol/clob/query.rpc.Query.ts index bdc2213141..643117c5ea 100644 --- a/indexer/packages/v4-protos/src/codegen/dydxprotocol/clob/query.rpc.Query.ts +++ b/indexer/packages/v4-protos/src/codegen/dydxprotocol/clob/query.rpc.Query.ts @@ -1,7 +1,7 @@ import { Rpc } from "../../helpers"; import * as _m0 from "protobufjs/minimal"; import { QueryClient, createProtobufRpcClient } from "@cosmjs/stargate"; -import { QueryGetClobPairRequest, QueryClobPairResponse, QueryAllClobPairRequest, QueryClobPairAllResponse, AreSubaccountsLiquidatableRequest, AreSubaccountsLiquidatableResponse, MevNodeToNodeCalculationRequest, MevNodeToNodeCalculationResponse, QueryEquityTierLimitConfigurationRequest, QueryEquityTierLimitConfigurationResponse, QueryBlockRateLimitConfigurationRequest, QueryBlockRateLimitConfigurationResponse, QueryLiquidationsConfigurationRequest, QueryLiquidationsConfigurationResponse } from "./query"; +import { QueryGetClobPairRequest, QueryClobPairResponse, QueryAllClobPairRequest, QueryClobPairAllResponse, MevNodeToNodeCalculationRequest, MevNodeToNodeCalculationResponse, QueryEquityTierLimitConfigurationRequest, QueryEquityTierLimitConfigurationResponse, QueryBlockRateLimitConfigurationRequest, QueryBlockRateLimitConfigurationResponse, QueryLiquidationsConfigurationRequest, QueryLiquidationsConfigurationResponse } from "./query"; /** Query defines the gRPC querier service. */ export interface Query { @@ -10,9 +10,6 @@ export interface Query { /** Queries a list of ClobPair items. */ clobPairAll(request?: QueryAllClobPairRequest): Promise; - /** Returns whether a subaccount is liquidatable. */ - - areSubaccountsLiquidatable(request: AreSubaccountsLiquidatableRequest): Promise; /** Runs the MEV node <> node calculation with the provided parameters. */ mevNodeToNodeCalculation(request: MevNodeToNodeCalculationRequest): Promise; @@ -33,7 +30,6 @@ export class QueryClientImpl implements Query { this.rpc = rpc; this.clobPair = this.clobPair.bind(this); this.clobPairAll = this.clobPairAll.bind(this); - this.areSubaccountsLiquidatable = this.areSubaccountsLiquidatable.bind(this); this.mevNodeToNodeCalculation = this.mevNodeToNodeCalculation.bind(this); this.equityTierLimitConfiguration = this.equityTierLimitConfiguration.bind(this); this.blockRateLimitConfiguration = this.blockRateLimitConfiguration.bind(this); @@ -54,12 +50,6 @@ export class QueryClientImpl implements Query { return promise.then(data => QueryClobPairAllResponse.decode(new _m0.Reader(data))); } - areSubaccountsLiquidatable(request: AreSubaccountsLiquidatableRequest): Promise { - const data = AreSubaccountsLiquidatableRequest.encode(request).finish(); - const promise = this.rpc.request("dydxprotocol.clob.Query", "AreSubaccountsLiquidatable", data); - return promise.then(data => AreSubaccountsLiquidatableResponse.decode(new _m0.Reader(data))); - } - mevNodeToNodeCalculation(request: MevNodeToNodeCalculationRequest): Promise { const data = MevNodeToNodeCalculationRequest.encode(request).finish(); const promise = this.rpc.request("dydxprotocol.clob.Query", "MevNodeToNodeCalculation", data); @@ -97,10 +87,6 @@ export const createRpcQueryExtension = (base: QueryClient) => { return queryService.clobPairAll(request); }, - areSubaccountsLiquidatable(request: AreSubaccountsLiquidatableRequest): Promise { - return queryService.areSubaccountsLiquidatable(request); - }, - mevNodeToNodeCalculation(request: MevNodeToNodeCalculationRequest): Promise { return queryService.mevNodeToNodeCalculation(request); }, diff --git a/indexer/packages/v4-protos/src/codegen/dydxprotocol/clob/query.ts b/indexer/packages/v4-protos/src/codegen/dydxprotocol/clob/query.ts index f2b798bc09..f0ef3987b8 100644 --- a/indexer/packages/v4-protos/src/codegen/dydxprotocol/clob/query.ts +++ b/indexer/packages/v4-protos/src/codegen/dydxprotocol/clob/query.ts @@ -1,5 +1,4 @@ import { PageRequest, PageRequestSDKType, PageResponse, PageResponseSDKType } from "../../cosmos/base/query/v1beta1/pagination"; -import { SubaccountId, SubaccountIdSDKType } from "../subaccounts/subaccount"; import { ValidatorMevMatches, ValidatorMevMatchesSDKType, MevNodeToNodeMetrics, MevNodeToNodeMetricsSDKType } from "./mev"; import { ClobPair, ClobPairSDKType } from "./clob_pair"; import { EquityTierLimitConfiguration, EquityTierLimitConfigurationSDKType } from "./equity_tier_limit_config"; @@ -51,52 +50,6 @@ export interface QueryClobPairAllResponseSDKType { clob_pair: ClobPairSDKType[]; pagination?: PageResponseSDKType; } -/** - * AreSubaccountsLiquidatableRequest is a request message used to check whether - * the given subaccounts are liquidatable. - * The subaccount ids should not contain duplicates. - */ - -export interface AreSubaccountsLiquidatableRequest { - subaccountIds: SubaccountId[]; -} -/** - * AreSubaccountsLiquidatableRequest is a request message used to check whether - * the given subaccounts are liquidatable. - * The subaccount ids should not contain duplicates. - */ - -export interface AreSubaccountsLiquidatableRequestSDKType { - subaccount_ids: SubaccountIdSDKType[]; -} -/** - * AreSubaccountsLiquidatableResponse is a response message that contains the - * liquidation status for each subaccount. - */ - -export interface AreSubaccountsLiquidatableResponse { - results: AreSubaccountsLiquidatableResponse_Result[]; -} -/** - * AreSubaccountsLiquidatableResponse is a response message that contains the - * liquidation status for each subaccount. - */ - -export interface AreSubaccountsLiquidatableResponseSDKType { - results: AreSubaccountsLiquidatableResponse_ResultSDKType[]; -} -/** Result returns whether a subaccount should be liquidated. */ - -export interface AreSubaccountsLiquidatableResponse_Result { - subaccountId?: SubaccountId; - isLiquidatable: boolean; -} -/** Result returns whether a subaccount should be liquidated. */ - -export interface AreSubaccountsLiquidatableResponse_ResultSDKType { - subaccount_id?: SubaccountIdSDKType; - is_liquidatable: boolean; -} /** * MevNodeToNodeCalculationRequest is a request message used to run the * MEV node <> node calculation. @@ -436,151 +389,6 @@ export const QueryClobPairAllResponse = { }; -function createBaseAreSubaccountsLiquidatableRequest(): AreSubaccountsLiquidatableRequest { - return { - subaccountIds: [] - }; -} - -export const AreSubaccountsLiquidatableRequest = { - encode(message: AreSubaccountsLiquidatableRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { - for (const v of message.subaccountIds) { - SubaccountId.encode(v!, writer.uint32(10).fork()).ldelim(); - } - - return writer; - }, - - decode(input: _m0.Reader | Uint8Array, length?: number): AreSubaccountsLiquidatableRequest { - const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input); - let end = length === undefined ? reader.len : reader.pos + length; - const message = createBaseAreSubaccountsLiquidatableRequest(); - - while (reader.pos < end) { - const tag = reader.uint32(); - - switch (tag >>> 3) { - case 1: - message.subaccountIds.push(SubaccountId.decode(reader, reader.uint32())); - break; - - default: - reader.skipType(tag & 7); - break; - } - } - - return message; - }, - - fromPartial(object: DeepPartial): AreSubaccountsLiquidatableRequest { - const message = createBaseAreSubaccountsLiquidatableRequest(); - message.subaccountIds = object.subaccountIds?.map(e => SubaccountId.fromPartial(e)) || []; - return message; - } - -}; - -function createBaseAreSubaccountsLiquidatableResponse(): AreSubaccountsLiquidatableResponse { - return { - results: [] - }; -} - -export const AreSubaccountsLiquidatableResponse = { - encode(message: AreSubaccountsLiquidatableResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { - for (const v of message.results) { - AreSubaccountsLiquidatableResponse_Result.encode(v!, writer.uint32(10).fork()).ldelim(); - } - - return writer; - }, - - decode(input: _m0.Reader | Uint8Array, length?: number): AreSubaccountsLiquidatableResponse { - const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input); - let end = length === undefined ? reader.len : reader.pos + length; - const message = createBaseAreSubaccountsLiquidatableResponse(); - - while (reader.pos < end) { - const tag = reader.uint32(); - - switch (tag >>> 3) { - case 1: - message.results.push(AreSubaccountsLiquidatableResponse_Result.decode(reader, reader.uint32())); - break; - - default: - reader.skipType(tag & 7); - break; - } - } - - return message; - }, - - fromPartial(object: DeepPartial): AreSubaccountsLiquidatableResponse { - const message = createBaseAreSubaccountsLiquidatableResponse(); - message.results = object.results?.map(e => AreSubaccountsLiquidatableResponse_Result.fromPartial(e)) || []; - return message; - } - -}; - -function createBaseAreSubaccountsLiquidatableResponse_Result(): AreSubaccountsLiquidatableResponse_Result { - return { - subaccountId: undefined, - isLiquidatable: false - }; -} - -export const AreSubaccountsLiquidatableResponse_Result = { - encode(message: AreSubaccountsLiquidatableResponse_Result, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { - if (message.subaccountId !== undefined) { - SubaccountId.encode(message.subaccountId, writer.uint32(10).fork()).ldelim(); - } - - if (message.isLiquidatable === true) { - writer.uint32(16).bool(message.isLiquidatable); - } - - return writer; - }, - - decode(input: _m0.Reader | Uint8Array, length?: number): AreSubaccountsLiquidatableResponse_Result { - const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input); - let end = length === undefined ? reader.len : reader.pos + length; - const message = createBaseAreSubaccountsLiquidatableResponse_Result(); - - while (reader.pos < end) { - const tag = reader.uint32(); - - switch (tag >>> 3) { - case 1: - message.subaccountId = SubaccountId.decode(reader, reader.uint32()); - break; - - case 2: - message.isLiquidatable = reader.bool(); - break; - - default: - reader.skipType(tag & 7); - break; - } - } - - return message; - }, - - fromPartial(object: DeepPartial): AreSubaccountsLiquidatableResponse_Result { - const message = createBaseAreSubaccountsLiquidatableResponse_Result(); - message.subaccountId = object.subaccountId !== undefined && object.subaccountId !== null ? SubaccountId.fromPartial(object.subaccountId) : undefined; - message.isLiquidatable = object.isLiquidatable ?? false; - return message; - } - -}; - function createBaseMevNodeToNodeCalculationRequest(): MevNodeToNodeCalculationRequest { return { blockProposerMatches: undefined, diff --git a/indexer/services/socks/__tests__/lib/subscriptions.test.ts b/indexer/services/socks/__tests__/lib/subscriptions.test.ts index 8cd3711ff1..04fdf84587 100644 --- a/indexer/services/socks/__tests__/lib/subscriptions.test.ts +++ b/indexer/services/socks/__tests__/lib/subscriptions.test.ts @@ -10,10 +10,12 @@ import { btcTicker, invalidChannel, invalidTicker } from '../constants'; import { axiosRequest } from '../../src/lib/axios'; import { AxiosSafeServerError, makeAxiosSafeServerError } from '@dydxprotocol-indexer/base'; import { BlockedError } from '../../src/lib/errors'; +import { isRestrictedCountry } from '@dydxprotocol-indexer/compliance'; jest.mock('ws'); jest.mock('../../src/helpers/wss'); jest.mock('../../src/lib/axios'); +jest.mock('@dydxprotocol-indexer/compliance'); describe('Subscriptions', () => { let subscriptions: Subscriptions; @@ -56,6 +58,8 @@ describe('Subscriptions', () => { [Channel.V4_TRADES]: ['/v4/trades/perpetualMarket/.+'], }; const initialMessage: Object = { a: 'b' }; + const restrictedCountry: string = 'US'; + const nonRestrictedCountry: string = 'AR'; beforeAll(async () => { await dbHelpers.migrate(); @@ -79,6 +83,9 @@ describe('Subscriptions', () => { axiosRequestMock = (axiosRequest as jest.Mock); axiosRequestMock.mockClear(); axiosRequestMock.mockImplementation(() => (JSON.stringify(initialMessage))); + (isRestrictedCountry as jest.Mock).mockImplementation((country: string): boolean => { + return country === restrictedCountry; + }); }); describe('subscribe', () => { @@ -98,6 +105,8 @@ describe('Subscriptions', () => { connectionId, initialMsgId, id, + false, + nonRestrictedCountry, ); expect(sendMessageStringMock).toHaveBeenCalledTimes(1); @@ -140,6 +149,8 @@ describe('Subscriptions', () => { connectionId, initialMsgId, id, + false, + nonRestrictedCountry, ); expect(sendMessageMock).toHaveBeenCalledTimes(1); @@ -167,6 +178,8 @@ describe('Subscriptions', () => { connectionId, initialMsgId, defaultId, + false, + nonRestrictedCountry, ); }, ).rejects.toEqual(new Error(`Invalid channel: ${invalidChannel}`)); @@ -180,6 +193,8 @@ describe('Subscriptions', () => { connectionId, initialMsgId, mockSubaccountId, + false, + nonRestrictedCountry, ); expect(sendMessageMock).toHaveBeenCalledTimes(1); @@ -201,6 +216,8 @@ describe('Subscriptions', () => { connectionId, initialMsgId, mockSubaccountId, + false, + nonRestrictedCountry, ); expect(sendMessageMock).toHaveBeenCalledTimes(1); @@ -235,6 +252,33 @@ describe('Subscriptions', () => { connectionId, initialMsgId, mockSubaccountId, + false, + nonRestrictedCountry, + ); + + expect(sendMessageMock).toHaveBeenCalledTimes(1); + expect(sendMessageMock).toHaveBeenCalledWith( + mockWs, + connectionId, + expect.objectContaining({ + connection_id: connectionId, + type: 'error', + message: expectedError.message, + })); + expect(subscriptions.subscriptions[Channel.V4_ACCOUNTS]).toBeUndefined(); + expect(subscriptions.subscriptionLists[connectionId]).toBeUndefined(); + }); + + it('sends blocked error if subscribing to subaccount from restricted country', async () => { + const expectedError: BlockedError = new BlockedError(); + await subscriptions.subscribe( + mockWs, + Channel.V4_ACCOUNTS, + connectionId, + initialMsgId, + mockSubaccountId, + false, + restrictedCountry, ); expect(sendMessageMock).toHaveBeenCalledTimes(1); @@ -260,6 +304,8 @@ describe('Subscriptions', () => { connectionId, initialMsgId, mockSubaccountId, + false, + nonRestrictedCountry, ); expect(sendMessageStringMock).toHaveBeenCalledTimes(1); @@ -295,6 +341,8 @@ describe('Subscriptions', () => { connectionId, initialMsgId, id, + false, + nonRestrictedCountry, ); subscriptions.unsubscribe( connectionId, @@ -313,6 +361,8 @@ describe('Subscriptions', () => { connectionId, initialMsgId, mockSubaccountId, + false, + nonRestrictedCountry, ); subscriptions.unsubscribe( connectionId, @@ -335,6 +385,8 @@ describe('Subscriptions', () => { connectionId, initialMsgId, validIds[channel], + false, + nonRestrictedCountry, ); })); diff --git a/indexer/services/socks/__tests__/websocket/index.test.ts b/indexer/services/socks/__tests__/websocket/index.test.ts index 4270f0939b..06dde78ca0 100644 --- a/indexer/services/socks/__tests__/websocket/index.test.ts +++ b/indexer/services/socks/__tests__/websocket/index.test.ts @@ -15,7 +15,7 @@ import { import { InvalidMessageHandler } from '../../src/lib/invalid-message'; import { PingHandler } from '../../src/lib/ping'; import config from '../../src/config'; -import { isRestrictedCountryHeaders } from '@dydxprotocol-indexer/compliance'; +import { isRestrictedCountryHeaders, COUNTRY_HEADER_KEY } from '@dydxprotocol-indexer/compliance'; jest.mock('uuid'); jest.mock('../../src/helpers/wss'); @@ -38,6 +38,7 @@ describe('Index', () => { const connectionId: string = 'conId'; const defaultGeoblockingEnabled: boolean = config.INDEXER_LEVEL_GEOBLOCKING_ENABLED; + const countryCode: string = 'AR'; beforeAll(() => { jest.useFakeTimers(); @@ -142,7 +143,9 @@ describe('Index', () => { beforeEach(() => { // Connect to the index before starting each test. (v4 as unknown as jest.Mock).mockReturnValueOnce(connectionId); - mockConnect(websocket, new IncomingMessage(new Socket())); + const incomingMessage: IncomingMessage = new IncomingMessage(new Socket()); + incomingMessage.headers[COUNTRY_HEADER_KEY] = countryCode; + mockConnect(websocket, incomingMessage); }); describe('message', () => { @@ -257,6 +260,7 @@ describe('Index', () => { index.connections[connectionId].messageId, id, isBatched, + countryCode, ); }); diff --git a/indexer/services/socks/src/lib/subscription.ts b/indexer/services/socks/src/lib/subscription.ts index 71a8a02ecb..02aaff590a 100644 --- a/indexer/services/socks/src/lib/subscription.ts +++ b/indexer/services/socks/src/lib/subscription.ts @@ -3,6 +3,7 @@ import { logger, stats, } from '@dydxprotocol-indexer/base'; +import { isRestrictedCountry } from '@dydxprotocol-indexer/compliance'; import { CandleResolution, perpetualMarketRefresher } from '@dydxprotocol-indexer/postgres'; import WebSocket from 'ws'; @@ -77,6 +78,7 @@ export class Subscriptions { messageId: number, id?: string, batched?: boolean, + country?: string, ): Promise { if (this.forwardMessage === undefined) { throw new Error('Unexpected error, subscription object is uninitialized.'); @@ -129,7 +131,7 @@ export class Subscriptions { let initialResponse: string; const startGetInitialResponse: number = Date.now(); try { - initialResponse = await this.getInitialResponsesForChannels(channel, id); + initialResponse = await this.getInitialResponsesForChannels(channel, id, country); } catch (error) { logger.info({ at: 'Subscription#subscribe', @@ -481,11 +483,21 @@ export class Subscriptions { } } - private async getInitialResponseForSubaccountSubscription(id?: string): Promise { + private async getInitialResponseForSubaccountSubscription( + id?: string, + country?: string, + ): Promise { if (id === undefined) { throw new Error('Invalid undefined id'); } + // TODO(IND-508): Change this to match technical spec for persistent geo-blocking. This may + // either have to replicate any blocking logic added on comlink, or re-direct to comlink to + // determine if subscribing to a specific subaccount is blocked. + if (country !== undefined && isRestrictedCountry(country)) { + throw new BlockedError(); + } + try { const { address, @@ -567,9 +579,13 @@ export class Subscriptions { * @param id Id fo the subscription to get the initial response for. * @returns The initial response for the channel. */ - private async getInitialResponsesForChannels(channel: Channel, id?: string): Promise { + private async getInitialResponsesForChannels( + channel: Channel, + id?: string, + country?: string, + ): Promise { if (channel === Channel.V4_ACCOUNTS) { - return this.getInitialResponseForSubaccountSubscription(id); + return this.getInitialResponseForSubaccountSubscription(id, country); } const endpoint: string | undefined = this.getInitialEndpointForSubscription(channel, id); // If no endpoint exists, return an empty initial response. diff --git a/indexer/services/socks/src/types.ts b/indexer/services/socks/src/types.ts index c4ca479268..bb49eda969 100644 --- a/indexer/services/socks/src/types.ts +++ b/indexer/services/socks/src/types.ts @@ -119,6 +119,7 @@ export interface Connection { messageId: number; heartbeat?: NodeJS.Timeout; disconnect?: NodeJS.Timeout; + countryCode?: string; } export interface MessageToForward { diff --git a/indexer/services/socks/src/websocket/index.ts b/indexer/services/socks/src/websocket/index.ts index 29f0fdd8b0..944e4243b9 100644 --- a/indexer/services/socks/src/websocket/index.ts +++ b/indexer/services/socks/src/websocket/index.ts @@ -109,6 +109,7 @@ export class Index { this.connections[connectionId] = { ws, messageId: 0, + countryCode: this.countryRestrictor.getCountry(req), }; const numConcurrentConnections: number = Object.keys(this.connections).length; @@ -287,6 +288,7 @@ export class Index { this.connections[connectionId].messageId, subscribeMessage.id, subscribeMessage.batched, + this.connections[connectionId].countryCode, ).catch((error: Error) => logger.error({ at: 'Subscription#subscribe', message: `Subscribing threw error: ${error.message}`, diff --git a/indexer/services/socks/src/websocket/restrict-countries.ts b/indexer/services/socks/src/websocket/restrict-countries.ts index e64d060d98..f079c90660 100644 --- a/indexer/services/socks/src/websocket/restrict-countries.ts +++ b/indexer/services/socks/src/websocket/restrict-countries.ts @@ -13,4 +13,9 @@ export class CountryRestrictor { return false; } + + public getCountry(req: IncomingMessage): string | undefined { + const countryHeaders: CountryHeaders = req.headers as CountryHeaders; + return countryHeaders['cf-ipcountry']; + } } diff --git a/proto/dydxprotocol/clob/query.proto b/proto/dydxprotocol/clob/query.proto index 9193122953..3fc37479c0 100644 --- a/proto/dydxprotocol/clob/query.proto +++ b/proto/dydxprotocol/clob/query.proto @@ -9,7 +9,6 @@ import "dydxprotocol/clob/clob_pair.proto"; import "dydxprotocol/clob/equity_tier_limit_config.proto"; import "dydxprotocol/clob/liquidations_config.proto"; import "dydxprotocol/clob/mev.proto"; -import "dydxprotocol/subaccounts/subaccount.proto"; option go_package = "github.com/dydxprotocol/v4-chain/protocol/x/clob/types"; @@ -25,10 +24,6 @@ service Query { option (google.api.http).get = "/dydxprotocol/clob/clob_pair"; } - // Returns whether a subaccount is liquidatable. - rpc AreSubaccountsLiquidatable(AreSubaccountsLiquidatableRequest) - returns (AreSubaccountsLiquidatableResponse); - // Runs the MEV node <> node calculation with the provided parameters. rpc MevNodeToNodeCalculation(MevNodeToNodeCalculationRequest) returns (MevNodeToNodeCalculationResponse) { @@ -76,26 +71,6 @@ message QueryClobPairAllResponse { cosmos.base.query.v1beta1.PageResponse pagination = 2; } -// AreSubaccountsLiquidatableRequest is a request message used to check whether -// the given subaccounts are liquidatable. -// The subaccount ids should not contain duplicates. -message AreSubaccountsLiquidatableRequest { - repeated dydxprotocol.subaccounts.SubaccountId subaccount_ids = 1 - [ (gogoproto.nullable) = false ]; -} - -// AreSubaccountsLiquidatableResponse is a response message that contains the -// liquidation status for each subaccount. -message AreSubaccountsLiquidatableResponse { - // Result returns whether a subaccount should be liquidated. - message Result { - dydxprotocol.subaccounts.SubaccountId subaccount_id = 1 - [ (gogoproto.nullable) = false ]; - bool is_liquidatable = 2; - } - repeated Result results = 1 [ (gogoproto.nullable) = false ]; -} - // MevNodeToNodeCalculationRequest is a request message used to run the // MEV node <> node calculation. message MevNodeToNodeCalculationRequest { diff --git a/protocol/app/app.go b/protocol/app/app.go index 42e448468b..0c414ae3ce 100644 --- a/protocol/app/app.go +++ b/protocol/app/app.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "errors" - "github.com/dydxprotocol/v4-chain/protocol/daemons/configs" "io" "math/big" "net/http" @@ -107,6 +106,7 @@ import ( // Daemons bridgeclient "github.com/dydxprotocol/v4-chain/protocol/daemons/bridge/client" + "github.com/dydxprotocol/v4-chain/protocol/daemons/configs" daemonflags "github.com/dydxprotocol/v4-chain/protocol/daemons/flags" liquidationclient "github.com/dydxprotocol/v4-chain/protocol/daemons/liquidation/client" metricsclient "github.com/dydxprotocol/v4-chain/protocol/daemons/metrics/client" diff --git a/protocol/app/flags/flags.go b/protocol/app/flags/flags.go index b7a7780cfe..6f67611a22 100644 --- a/protocol/app/flags/flags.go +++ b/protocol/app/flags/flags.go @@ -2,6 +2,7 @@ package flags import ( "fmt" + "github.com/cosmos/cosmos-sdk/server/config" servertypes "github.com/cosmos/cosmos-sdk/server/types" "github.com/spf13/cast" @@ -102,7 +103,7 @@ func GetFlagValuesFromOptions( } if option := appOpts.Get(DdAgentHost); option != nil { - if v, err := cast.ToStringE(option); err == nil { + if v, err := cast.ToStringE(option); err == nil && len(v) > 0 { result.DdAgentHost = v } } @@ -120,7 +121,7 @@ func GetFlagValuesFromOptions( } if option := appOpts.Get(GrpcAddress); option != nil { - if v, err := cast.ToStringE(option); err == nil { + if v, err := cast.ToStringE(option); err == nil && len(v) > 0 { result.GrpcAddress = v } } diff --git a/protocol/daemons/flags/flags.go b/protocol/daemons/flags/flags.go index e386f379ab..3bae6c44e6 100644 --- a/protocol/daemons/flags/flags.go +++ b/protocol/daemons/flags/flags.go @@ -20,10 +20,9 @@ const ( FlagBridgeDaemonLoopDelayMs = "bridge-daemon-loop-delay-ms" FlagBridgeDaemonEthRpcEndpoint = "bridge-daemon-eth-rpc-endpoint" - FlagLiquidationDaemonEnabled = "liquidation-daemon-enabled" - FlagLiquidationDaemonLoopDelayMs = "liquidation-daemon-loop-delay-ms" - FlagLiquidationDaemonSubaccountPageLimit = "liquidation-daemon-subaccount-page-limit" - FlagLiquidationDaemonRequestChunkSize = "liquidation-daemon-request-chunk-size" + FlagLiquidationDaemonEnabled = "liquidation-daemon-enabled" + FlagLiquidationDaemonLoopDelayMs = "liquidation-daemon-loop-delay-ms" + FlagLiquidationDaemonQueryPageLimit = "liquidation-daemon-query-page-limit" ) // Shared flags contains configuration flags shared by all daemons. @@ -52,9 +51,8 @@ type LiquidationFlags struct { Enabled bool // LoopDelayMs configures the update frequency of the liquidation daemon. LoopDelayMs uint32 - // SubaccountPageLimit configures the pagination limit for fetching subaccounts. - SubaccountPageLimit uint64 - RequestChunkSize uint64 + // QueryPageLimit configures the pagination limit for fetching subaccounts. + QueryPageLimit uint64 } // PriceFlags contains configuration flags for the Price Daemon. @@ -90,10 +88,9 @@ func GetDefaultDaemonFlags() DaemonFlags { EthRpcEndpoint: "", }, Liquidation: LiquidationFlags{ - Enabled: true, - LoopDelayMs: 1_600, - SubaccountPageLimit: 1_000, - RequestChunkSize: 50, + Enabled: true, + LoopDelayMs: 1_600, + QueryPageLimit: 1_000, }, Price: PriceFlags{ Enabled: true, @@ -160,14 +157,9 @@ func AddDaemonFlagsToCmd( "Delay in milliseconds between running the Liquidation Daemon task loop.", ) cmd.Flags().Uint64( - FlagLiquidationDaemonSubaccountPageLimit, - df.Liquidation.SubaccountPageLimit, - "Limit on the number of subaccounts to fetch per query in the Liquidation Daemon task loop.", - ) - cmd.Flags().Uint64( - FlagLiquidationDaemonRequestChunkSize, - df.Liquidation.RequestChunkSize, - "Limit on the number of subaccounts per collateralization check in the Liquidation Daemon task loop.", + FlagLiquidationDaemonQueryPageLimit, + df.Liquidation.QueryPageLimit, + "Limit on the number of items to fetch per query in the Liquidation Daemon task loop.", ) // Price Daemon. @@ -192,7 +184,7 @@ func GetDaemonFlagValuesFromOptions( // Shared Flags if option := appOpts.Get(FlagUnixSocketAddress); option != nil { - if v, err := cast.ToStringE(option); err == nil { + if v, err := cast.ToStringE(option); err == nil && len(v) > 0 { result.Shared.SocketAddress = v } } @@ -219,7 +211,7 @@ func GetDaemonFlagValuesFromOptions( } } if option := appOpts.Get(FlagBridgeDaemonEthRpcEndpoint); option != nil { - if v, err := cast.ToStringE(option); err == nil { + if v, err := cast.ToStringE(option); err == nil && len(v) > 0 { result.Bridge.EthRpcEndpoint = v } } @@ -235,14 +227,9 @@ func GetDaemonFlagValuesFromOptions( result.Liquidation.LoopDelayMs = v } } - if option := appOpts.Get(FlagLiquidationDaemonSubaccountPageLimit); option != nil { - if v, err := cast.ToUint64E(option); err == nil { - result.Liquidation.SubaccountPageLimit = v - } - } - if option := appOpts.Get(FlagLiquidationDaemonRequestChunkSize); option != nil { + if option := appOpts.Get(FlagLiquidationDaemonQueryPageLimit); option != nil { if v, err := cast.ToUint64E(option); err == nil { - result.Liquidation.RequestChunkSize = v + result.Liquidation.QueryPageLimit = v } } diff --git a/protocol/daemons/flags/flags_test.go b/protocol/daemons/flags/flags_test.go index 04191032f6..e94a055d45 100644 --- a/protocol/daemons/flags/flags_test.go +++ b/protocol/daemons/flags/flags_test.go @@ -25,7 +25,7 @@ func TestAddDaemonFlagsToCmd(t *testing.T) { flags.FlagLiquidationDaemonEnabled, flags.FlagLiquidationDaemonLoopDelayMs, - flags.FlagLiquidationDaemonSubaccountPageLimit, + flags.FlagLiquidationDaemonQueryPageLimit, flags.FlagPriceDaemonEnabled, flags.FlagPriceDaemonLoopDelayMs, @@ -52,8 +52,7 @@ func TestGetDaemonFlagValuesFromOptions_Custom(t *testing.T) { optsMap[flags.FlagLiquidationDaemonEnabled] = true optsMap[flags.FlagLiquidationDaemonLoopDelayMs] = uint32(2222) - optsMap[flags.FlagLiquidationDaemonSubaccountPageLimit] = uint64(3333) - optsMap[flags.FlagLiquidationDaemonRequestChunkSize] = uint64(4444) + optsMap[flags.FlagLiquidationDaemonQueryPageLimit] = uint64(3333) optsMap[flags.FlagPriceDaemonEnabled] = true optsMap[flags.FlagPriceDaemonLoopDelayMs] = uint32(4444) @@ -83,8 +82,7 @@ func TestGetDaemonFlagValuesFromOptions_Custom(t *testing.T) { // Liquidation Daemon. require.Equal(t, optsMap[flags.FlagLiquidationDaemonEnabled], r.Liquidation.Enabled) require.Equal(t, optsMap[flags.FlagLiquidationDaemonLoopDelayMs], r.Liquidation.LoopDelayMs) - require.Equal(t, optsMap[flags.FlagLiquidationDaemonSubaccountPageLimit], r.Liquidation.SubaccountPageLimit) - require.Equal(t, optsMap[flags.FlagLiquidationDaemonRequestChunkSize], r.Liquidation.RequestChunkSize) + require.Equal(t, optsMap[flags.FlagLiquidationDaemonQueryPageLimit], r.Liquidation.QueryPageLimit) // Price Daemon. require.Equal(t, optsMap[flags.FlagPriceDaemonEnabled], r.Price.Enabled) diff --git a/protocol/daemons/liquidation/client/client_test.go b/protocol/daemons/liquidation/client/client_test.go index 81b4943558..d925852287 100644 --- a/protocol/daemons/liquidation/client/client_test.go +++ b/protocol/daemons/liquidation/client/client_test.go @@ -7,20 +7,14 @@ import ( "testing" "github.com/cometbft/cometbft/libs/log" - "github.com/cosmos/cosmos-sdk/types/query" appflags "github.com/dydxprotocol/v4-chain/protocol/app/flags" d_constants "github.com/dydxprotocol/v4-chain/protocol/daemons/constants" "github.com/dydxprotocol/v4-chain/protocol/daemons/flags" - "github.com/dydxprotocol/v4-chain/protocol/daemons/liquidation/api" "github.com/dydxprotocol/v4-chain/protocol/daemons/liquidation/client" "github.com/dydxprotocol/v4-chain/protocol/mocks" "github.com/dydxprotocol/v4-chain/protocol/testutil/appoptions" - "github.com/dydxprotocol/v4-chain/protocol/testutil/constants" daemontestutils "github.com/dydxprotocol/v4-chain/protocol/testutil/daemons" "github.com/dydxprotocol/v4-chain/protocol/testutil/grpc" - clobtypes "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" - satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -70,183 +64,6 @@ func TestStart_UnixSocketConnectionFails(t *testing.T) { mockGrpcClient.AssertNumberOfCalls(t, "CloseConnection", 1) } -func TestRunLiquidationDaemonTaskLoop(t *testing.T) { - df := flags.GetDefaultDaemonFlags() - tests := map[string]struct { - // mocks - setupMocks func(ctx context.Context, mck *mocks.QueryClient) - - // expectations - expectedLiquidatableSubaccountIds []satypes.SubaccountId - expectedError error - }{ - "Success": { - setupMocks: func(ctx context.Context, mck *mocks.QueryClient) { - req := &satypes.QueryAllSubaccountRequest{ - Pagination: &query.PageRequest{ - Limit: df.Liquidation.SubaccountPageLimit, - }, - } - response := &satypes.QuerySubaccountAllResponse{ - Subaccount: []satypes.Subaccount{ - constants.Carl_Num0_1BTC_Short, - constants.Dave_Num0_1BTC_Long_50000USD, - }, - } - mck.On("SubaccountAll", ctx, req).Return(response, nil) - - req2 := &clobtypes.AreSubaccountsLiquidatableRequest{ - SubaccountIds: []satypes.SubaccountId{ - constants.Carl_Num0, - constants.Dave_Num0, - }, - } - response2 := &clobtypes.AreSubaccountsLiquidatableResponse{ - Results: []clobtypes.AreSubaccountsLiquidatableResponse_Result{ - { - SubaccountId: constants.Carl_Num0, - IsLiquidatable: true, - }, - { - SubaccountId: constants.Dave_Num0, - IsLiquidatable: false, - }, - }, - } - mck.On("AreSubaccountsLiquidatable", ctx, req2).Return(response2, nil) - - req3 := &api.LiquidateSubaccountsRequest{ - LiquidatableSubaccountIds: []satypes.SubaccountId{ - constants.Carl_Num0, - }, - } - response3 := &api.LiquidateSubaccountsResponse{} - mck.On("LiquidateSubaccounts", ctx, req3).Return(response3, nil) - }, - }, - "Success - no open position": { - setupMocks: func(ctx context.Context, mck *mocks.QueryClient) { - req := &satypes.QueryAllSubaccountRequest{ - Pagination: &query.PageRequest{ - Limit: df.Liquidation.SubaccountPageLimit, - }, - } - response := &satypes.QuerySubaccountAllResponse{ - Subaccount: []satypes.Subaccount{ - constants.Carl_Num0_599USD, // no open positions - constants.Dave_Num0_599USD, // no open positions - }, - } - mck.On("SubaccountAll", ctx, req).Return(response, nil) - req2 := &api.LiquidateSubaccountsRequest{ - LiquidatableSubaccountIds: []satypes.SubaccountId{}, - } - response2 := &api.LiquidateSubaccountsResponse{} - mck.On("LiquidateSubaccounts", ctx, req2).Return(response2, nil) - }, - }, - "Success - no liquidatable subaccounts": { - setupMocks: func(ctx context.Context, mck *mocks.QueryClient) { - req := &satypes.QueryAllSubaccountRequest{ - Pagination: &query.PageRequest{ - Limit: df.Liquidation.SubaccountPageLimit, - }, - } - response := &satypes.QuerySubaccountAllResponse{ - Subaccount: []satypes.Subaccount{ - constants.Carl_Num0_1BTC_Short, - constants.Dave_Num0_1BTC_Long_50000USD, - }, - } - mck.On("SubaccountAll", ctx, req).Return(response, nil) - - req2 := &clobtypes.AreSubaccountsLiquidatableRequest{ - SubaccountIds: []satypes.SubaccountId{ - constants.Carl_Num0, - constants.Dave_Num0, - }, - } - response2 := &clobtypes.AreSubaccountsLiquidatableResponse{ - Results: []clobtypes.AreSubaccountsLiquidatableResponse_Result{ - { - SubaccountId: constants.Carl_Num0, - IsLiquidatable: false, - }, - { - SubaccountId: constants.Dave_Num0, - IsLiquidatable: false, - }, - }, - } - mck.On("AreSubaccountsLiquidatable", ctx, req2).Return(response2, nil) - req3 := &api.LiquidateSubaccountsRequest{ - LiquidatableSubaccountIds: []satypes.SubaccountId{}, - } - response3 := &api.LiquidateSubaccountsResponse{} - mck.On("LiquidateSubaccounts", ctx, req3).Return(response3, nil) - }, - }, - "Panics on error - SubaccountAll": { - setupMocks: func(ctx context.Context, mck *mocks.QueryClient) { - mck.On("SubaccountAll", mock.Anything, mock.Anything).Return(nil, errors.New("test error")) - }, - expectedError: errors.New("test error"), - }, - "Panics on error - AreSubaccountsLiquidatable": { - setupMocks: func(ctx context.Context, mck *mocks.QueryClient) { - mck.On("SubaccountAll", mock.Anything, mock.Anything).Return(&satypes.QuerySubaccountAllResponse{ - Subaccount: []satypes.Subaccount{ - constants.Carl_Num0_1BTC_Short, - }, - }, nil) - mck.On("AreSubaccountsLiquidatable", mock.Anything, mock.Anything).Return(nil, errors.New("test error")) - }, - expectedError: errors.New("test error"), - }, - "Panics on error - LiquidateSubaccounts": { - setupMocks: func(ctx context.Context, mck *mocks.QueryClient) { - mck.On("SubaccountAll", mock.Anything, mock.Anything).Return(&satypes.QuerySubaccountAllResponse{ - Subaccount: []satypes.Subaccount{ - constants.Carl_Num0_1BTC_Short, - }, - }, nil, - ) - mck.On("AreSubaccountsLiquidatable", mock.Anything, mock.Anything).Return( - &clobtypes.AreSubaccountsLiquidatableResponse{}, - nil, - ) - mck.On("LiquidateSubaccounts", mock.Anything, mock.Anything).Return(nil, errors.New("test error")) - }, - expectedError: errors.New("test error"), - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - queryClientMock := &mocks.QueryClient{} - tc.setupMocks(grpc.Ctx, queryClientMock) - s := client.SubTaskRunnerImpl{} - - c := client.NewClient(log.NewNopLogger()) - c.SubaccountQueryClient = queryClientMock - c.ClobQueryClient = queryClientMock - c.LiquidationServiceClient = queryClientMock - - err := s.RunLiquidationDaemonTaskLoop( - grpc.Ctx, - c, - flags.GetDefaultDaemonFlags().Liquidation, - ) - if tc.expectedError != nil { - require.EqualError(t, err, tc.expectedError.Error()) - } else { - require.NoError(t, err) - queryClientMock.AssertExpectations(t) - } - }) - } -} - // FakeSubTaskRunner is a mock implementation of the SubTaskRunner interface for testing. type FakeSubTaskRunner struct { err error diff --git a/protocol/daemons/liquidation/client/grpc_helper.go b/protocol/daemons/liquidation/client/grpc_helper.go index 27a57b4c3a..92e49ba4bb 100644 --- a/protocol/daemons/liquidation/client/grpc_helper.go +++ b/protocol/daemons/liquidation/client/grpc_helper.go @@ -10,6 +10,7 @@ import ( "github.com/cosmos/cosmos-sdk/types/grpc" "github.com/cosmos/cosmos-sdk/types/query" "github.com/dydxprotocol/v4-chain/protocol/daemons/liquidation/api" + "github.com/dydxprotocol/v4-chain/protocol/lib" "github.com/dydxprotocol/v4-chain/protocol/lib/metrics" blocktimetypes "github.com/dydxprotocol/v4-chain/protocol/x/blocktime/types" clobtypes "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" @@ -45,7 +46,6 @@ func (c *Client) GetPreviousBlockInfo( // GetAllPerpetuals queries gRPC server and returns a list of perpetuals. func (c *Client) GetAllPerpetuals( ctx context.Context, - blockHeight uint32, pageLimit uint64, ) ( perpetuals []perptypes.Perpetual, @@ -85,7 +85,6 @@ func (c *Client) GetAllPerpetuals( // GetAllLiquidityTiers queries gRPC server and returns a list of liquidityTiers. func (c *Client) GetAllLiquidityTiers( ctx context.Context, - blockHeight uint32, pageLimit uint64, ) ( liquidityTiers []perptypes.LiquidityTier, @@ -125,7 +124,6 @@ func (c *Client) GetAllLiquidityTiers( // GetAllMarketPrices queries gRPC server and returns a list of market prices. func (c *Client) GetAllMarketPrices( ctx context.Context, - blockHeight uint32, pageLimit uint64, ) ( marketPrices []pricestypes.MarketPrice, @@ -205,38 +203,14 @@ func (c *Client) GetAllSubaccounts( return subaccounts, nil } -// CheckCollateralizationForSubaccounts queries a gRPC server using `AreSubaccountsLiquidatable` -// and returns a list of collateralization statuses for the given list of subaccount ids. -func (c *Client) CheckCollateralizationForSubaccounts( - ctx context.Context, - subaccountIds []satypes.SubaccountId, -) ( - results []clobtypes.AreSubaccountsLiquidatableResponse_Result, - err error, -) { - defer telemetry.ModuleMeasureSince( - metrics.LiquidationDaemon, - time.Now(), - metrics.CheckCollateralizationForSubaccounts, - metrics.Latency, - ) - - query := &clobtypes.AreSubaccountsLiquidatableRequest{ - SubaccountIds: subaccountIds, - } - response, err := c.ClobQueryClient.AreSubaccountsLiquidatable(ctx, query) - if err != nil { - return nil, err - } - - return response.Results, nil -} - // SendLiquidatableSubaccountIds sends a list of unique and potentially liquidatable // subaccount ids to a gRPC server via `LiquidateSubaccounts`. func (c *Client) SendLiquidatableSubaccountIds( ctx context.Context, - subaccountIds []satypes.SubaccountId, + blockHeight uint32, + liquidatableSubaccountIds []satypes.SubaccountId, + negativeTncSubaccountIds []satypes.SubaccountId, + openPositionInfoMap map[uint32]*clobtypes.SubaccountOpenPositionInfo, ) error { defer telemetry.ModuleMeasureSince( metrics.LiquidationDaemon, @@ -247,13 +221,31 @@ func (c *Client) SendLiquidatableSubaccountIds( telemetry.ModuleSetGauge( metrics.LiquidationDaemon, - float32(len(subaccountIds)), + float32(len(liquidatableSubaccountIds)), metrics.LiquidatableSubaccountIds, metrics.Count, ) + telemetry.ModuleSetGauge( + metrics.LiquidationDaemon, + float32(len(negativeTncSubaccountIds)), + metrics.NegativeTncSubaccountIds, + metrics.Count, + ) + + // Convert the map to a slice. + // Note that sorting here is not strictly necessary but is done for safety and to avoid making + // any assumptions on the server side. + sortedPerpetualIds := lib.GetSortedKeys[lib.Sortable[uint32]](openPositionInfoMap) + subaccountOpenPositionInfo := make([]clobtypes.SubaccountOpenPositionInfo, 0) + for _, perpetualId := range sortedPerpetualIds { + subaccountOpenPositionInfo = append(subaccountOpenPositionInfo, *openPositionInfoMap[perpetualId]) + } request := &api.LiquidateSubaccountsRequest{ - LiquidatableSubaccountIds: subaccountIds, + BlockHeight: blockHeight, + LiquidatableSubaccountIds: liquidatableSubaccountIds, + NegativeTncSubaccountIds: negativeTncSubaccountIds, + SubaccountOpenPositionInfo: subaccountOpenPositionInfo, } if _, err := c.LiquidationServiceClient.LiquidateSubaccounts(ctx, request); err != nil { @@ -262,7 +254,6 @@ func (c *Client) SendLiquidatableSubaccountIds( return nil } -// nolint:unused func newContextWithQueryBlockHeight( ctx context.Context, blockHeight uint32, diff --git a/protocol/daemons/liquidation/client/grpc_helper_test.go b/protocol/daemons/liquidation/client/grpc_helper_test.go index 3707c3f45b..2e6f0ab041 100644 --- a/protocol/daemons/liquidation/client/grpc_helper_test.go +++ b/protocol/daemons/liquidation/client/grpc_helper_test.go @@ -93,7 +93,7 @@ func TestGetAllSubaccounts(t *testing.T) { setupMocks: func(ctx context.Context, mck *mocks.QueryClient) { req := &satypes.QueryAllSubaccountRequest{ Pagination: &query.PageRequest{ - Limit: df.Liquidation.SubaccountPageLimit, + Limit: df.Liquidation.QueryPageLimit, }, } response := &satypes.QuerySubaccountAllResponse{ @@ -113,7 +113,7 @@ func TestGetAllSubaccounts(t *testing.T) { setupMocks: func(ctx context.Context, mck *mocks.QueryClient) { req := &satypes.QueryAllSubaccountRequest{ Pagination: &query.PageRequest{ - Limit: df.Liquidation.SubaccountPageLimit, + Limit: df.Liquidation.QueryPageLimit, }, } nextKey := []byte("next key") @@ -129,7 +129,7 @@ func TestGetAllSubaccounts(t *testing.T) { req2 := &satypes.QueryAllSubaccountRequest{ Pagination: &query.PageRequest{ Key: nextKey, - Limit: df.Liquidation.SubaccountPageLimit, + Limit: df.Liquidation.QueryPageLimit, }, } response2 := &satypes.QuerySubaccountAllResponse{ @@ -148,7 +148,7 @@ func TestGetAllSubaccounts(t *testing.T) { setupMocks: func(ctx context.Context, mck *mocks.QueryClient) { req := &satypes.QueryAllSubaccountRequest{ Pagination: &query.PageRequest{ - Limit: df.Liquidation.SubaccountPageLimit, + Limit: df.Liquidation.QueryPageLimit, }, } mck.On("SubaccountAll", ctx, req).Return(nil, errors.New("test error")) @@ -166,7 +166,7 @@ func TestGetAllSubaccounts(t *testing.T) { daemon.SubaccountQueryClient = queryClientMock actual, err := daemon.GetAllSubaccounts( grpc.Ctx, - df.Liquidation.SubaccountPageLimit, + df.Liquidation.QueryPageLimit, ) if err != nil { require.EqualError(t, err, tc.expectedError.Error()) @@ -258,7 +258,6 @@ func TestGetAllPerpetuals(t *testing.T) { daemon.PerpetualsQueryClient = queryClientMock actual, err := daemon.GetAllPerpetuals( grpc.Ctx, - uint32(50), tc.limit, ) if err != nil { @@ -347,7 +346,6 @@ func TestGetAllLiquidityTiers(t *testing.T) { daemon.PerpetualsQueryClient = queryClientMock actual, err := daemon.GetAllLiquidityTiers( grpc.Ctx, - uint32(50), tc.limit, ) if err != nil { @@ -441,7 +439,6 @@ func TestGetAllMarketPrices(t *testing.T) { daemon.PricesQueryClient = queryClientMock actual, err := daemon.GetAllMarketPrices( grpc.Ctx, - uint32(50), tc.limit, ) if err != nil { @@ -453,160 +450,109 @@ func TestGetAllMarketPrices(t *testing.T) { } } -func TestCheckCollateralizationForSubaccounts(t *testing.T) { - tests := map[string]struct { - // mocks - setupMocks func( - ctx context.Context, - mck *mocks.QueryClient, - results []clobtypes.AreSubaccountsLiquidatableResponse_Result, - ) - subaccountIds []satypes.SubaccountId - - // expectations - expectedResults []clobtypes.AreSubaccountsLiquidatableResponse_Result - expectedError error - }{ - "Success": { - setupMocks: func( - ctx context.Context, - mck *mocks.QueryClient, - results []clobtypes.AreSubaccountsLiquidatableResponse_Result, - ) { - query := &clobtypes.AreSubaccountsLiquidatableRequest{ - SubaccountIds: []satypes.SubaccountId{ - constants.Alice_Num0, - constants.Bob_Num0, - }, - } - response := &clobtypes.AreSubaccountsLiquidatableResponse{ - Results: results, - } - mck.On("AreSubaccountsLiquidatable", ctx, query).Return(response, nil) - }, - subaccountIds: []satypes.SubaccountId{ - constants.Alice_Num0, - constants.Bob_Num0, - }, - expectedResults: []clobtypes.AreSubaccountsLiquidatableResponse_Result{ - { - SubaccountId: constants.Alice_Num0, - IsLiquidatable: true, - }, - { - SubaccountId: constants.Bob_Num0, - IsLiquidatable: false, - }, - }, - }, - "Success - Empty": { - setupMocks: func( - ctx context.Context, - mck *mocks.QueryClient, - results []clobtypes.AreSubaccountsLiquidatableResponse_Result, - ) { - query := &clobtypes.AreSubaccountsLiquidatableRequest{ - SubaccountIds: []satypes.SubaccountId{}, - } - response := &clobtypes.AreSubaccountsLiquidatableResponse{ - Results: results, - } - mck.On("AreSubaccountsLiquidatable", ctx, query).Return(response, nil) - }, - subaccountIds: []satypes.SubaccountId{}, - expectedResults: []clobtypes.AreSubaccountsLiquidatableResponse_Result{}, - }, - "Errors are propagated": { - setupMocks: func( - ctx context.Context, - mck *mocks.QueryClient, - results []clobtypes.AreSubaccountsLiquidatableResponse_Result, - ) { - query := &clobtypes.AreSubaccountsLiquidatableRequest{ - SubaccountIds: []satypes.SubaccountId{}, - } - mck.On("AreSubaccountsLiquidatable", ctx, query).Return(nil, errors.New("test error")) - }, - subaccountIds: []satypes.SubaccountId{}, - expectedResults: []clobtypes.AreSubaccountsLiquidatableResponse_Result{}, - expectedError: errors.New("test error"), - }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - queryClientMock := &mocks.QueryClient{} - tc.setupMocks(grpc.Ctx, queryClientMock, tc.expectedResults) - - daemon := client.NewClient(log.NewNopLogger()) - daemon.ClobQueryClient = queryClientMock - actual, err := daemon.CheckCollateralizationForSubaccounts( - grpc.Ctx, - tc.subaccountIds, - ) - - if err != nil { - require.EqualError(t, err, tc.expectedError.Error()) - } else { - require.Equal(t, tc.expectedResults, actual) - } - }) - } -} - func TestSendLiquidatableSubaccountIds(t *testing.T) { tests := map[string]struct { // mocks - setupMocks func(ctx context.Context, mck *mocks.QueryClient, ids []satypes.SubaccountId) - subaccountIds []satypes.SubaccountId + setupMocks func(context.Context, *mocks.QueryClient) + liquidatableSubaccountIds []satypes.SubaccountId + negativeTncSubaccountIds []satypes.SubaccountId + subaccountOpenPositionInfo map[uint32]*clobtypes.SubaccountOpenPositionInfo // expectations expectedError error }{ "Success": { - setupMocks: func(ctx context.Context, mck *mocks.QueryClient, ids []satypes.SubaccountId) { + setupMocks: func(ctx context.Context, mck *mocks.QueryClient) { req := &api.LiquidateSubaccountsRequest{ - LiquidatableSubaccountIds: ids, + BlockHeight: uint32(50), + LiquidatableSubaccountIds: []satypes.SubaccountId{constants.Alice_Num0, constants.Bob_Num0}, + NegativeTncSubaccountIds: []satypes.SubaccountId{constants.Carl_Num0, constants.Dave_Num0}, + SubaccountOpenPositionInfo: []clobtypes.SubaccountOpenPositionInfo{ + { + PerpetualId: 0, + SubaccountsWithLongPosition: []satypes.SubaccountId{ + constants.Alice_Num0, + constants.Carl_Num0, + }, + SubaccountsWithShortPosition: []satypes.SubaccountId{ + constants.Bob_Num0, + constants.Dave_Num0, + }, + }, + }, } response := &api.LiquidateSubaccountsResponse{} mck.On("LiquidateSubaccounts", ctx, req).Return(response, nil) }, - subaccountIds: []satypes.SubaccountId{ + liquidatableSubaccountIds: []satypes.SubaccountId{ constants.Alice_Num0, constants.Bob_Num0, }, + negativeTncSubaccountIds: []satypes.SubaccountId{ + constants.Carl_Num0, + constants.Dave_Num0, + }, + subaccountOpenPositionInfo: map[uint32]*clobtypes.SubaccountOpenPositionInfo{ + 0: { + PerpetualId: 0, + SubaccountsWithLongPosition: []satypes.SubaccountId{ + constants.Alice_Num0, + constants.Carl_Num0, + }, + SubaccountsWithShortPosition: []satypes.SubaccountId{ + constants.Bob_Num0, + constants.Dave_Num0, + }, + }, + }, }, "Success Empty": { - setupMocks: func(ctx context.Context, mck *mocks.QueryClient, ids []satypes.SubaccountId) { + setupMocks: func(ctx context.Context, mck *mocks.QueryClient) { req := &api.LiquidateSubaccountsRequest{ - LiquidatableSubaccountIds: ids, + BlockHeight: uint32(50), + LiquidatableSubaccountIds: []satypes.SubaccountId{}, + NegativeTncSubaccountIds: []satypes.SubaccountId{}, + SubaccountOpenPositionInfo: []clobtypes.SubaccountOpenPositionInfo{}, } response := &api.LiquidateSubaccountsResponse{} mck.On("LiquidateSubaccounts", ctx, req).Return(response, nil) }, - subaccountIds: []satypes.SubaccountId{}, + liquidatableSubaccountIds: []satypes.SubaccountId{}, + negativeTncSubaccountIds: []satypes.SubaccountId{}, + subaccountOpenPositionInfo: map[uint32]*clobtypes.SubaccountOpenPositionInfo{}, }, "Errors are propagated": { - setupMocks: func(ctx context.Context, mck *mocks.QueryClient, ids []satypes.SubaccountId) { + setupMocks: func(ctx context.Context, mck *mocks.QueryClient) { req := &api.LiquidateSubaccountsRequest{ - LiquidatableSubaccountIds: ids, + BlockHeight: uint32(50), + LiquidatableSubaccountIds: []satypes.SubaccountId{}, + NegativeTncSubaccountIds: []satypes.SubaccountId{}, + SubaccountOpenPositionInfo: []clobtypes.SubaccountOpenPositionInfo{}, } mck.On("LiquidateSubaccounts", ctx, req).Return(nil, errors.New("test error")) }, - subaccountIds: []satypes.SubaccountId{}, - expectedError: errors.New("test error"), + liquidatableSubaccountIds: []satypes.SubaccountId{}, + negativeTncSubaccountIds: []satypes.SubaccountId{}, + subaccountOpenPositionInfo: map[uint32]*clobtypes.SubaccountOpenPositionInfo{}, + expectedError: errors.New("test error"), }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { queryClientMock := &mocks.QueryClient{} - tc.setupMocks(grpc.Ctx, queryClientMock, tc.subaccountIds) + tc.setupMocks(grpc.Ctx, queryClientMock) daemon := client.NewClient(log.NewNopLogger()) daemon.LiquidationServiceClient = queryClientMock - err := daemon.SendLiquidatableSubaccountIds(grpc.Ctx, tc.subaccountIds) + err := daemon.SendLiquidatableSubaccountIds( + grpc.Ctx, + uint32(50), + tc.liquidatableSubaccountIds, + tc.negativeTncSubaccountIds, + tc.subaccountOpenPositionInfo, + ) require.Equal(t, tc.expectedError, err) }) } diff --git a/protocol/daemons/liquidation/client/sub_task_runner.go b/protocol/daemons/liquidation/client/sub_task_runner.go index e7ae323f79..e768db2819 100644 --- a/protocol/daemons/liquidation/client/sub_task_runner.go +++ b/protocol/daemons/liquidation/client/sub_task_runner.go @@ -2,12 +2,21 @@ package client import ( "context" + "math/big" "time" + errorsmod "cosmossdk.io/errors" "github.com/cosmos/cosmos-sdk/telemetry" "github.com/dydxprotocol/v4-chain/protocol/daemons/flags" "github.com/dydxprotocol/v4-chain/protocol/lib" "github.com/dydxprotocol/v4-chain/protocol/lib/metrics" + assetstypes "github.com/dydxprotocol/v4-chain/protocol/x/assets/types" + clobkeeper "github.com/dydxprotocol/v4-chain/protocol/x/clob/keeper" + clobtypes "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" + perpkeeper "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/keeper" + perptypes "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types" + pricestypes "github.com/dydxprotocol/v4-chain/protocol/x/prices/types" + sakeeper "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/keeper" satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" ) @@ -40,24 +49,49 @@ func (s *SubTaskRunnerImpl) RunLiquidationDaemonTaskLoop( metrics.Latency, ) - // 1. Fetch all subaccounts from query service. - subaccounts, err := daemonClient.GetAllSubaccounts(ctx, liqFlags.SubaccountPageLimit) + lastCommittedBlockHeight, err := daemonClient.GetPreviousBlockInfo(ctx) if err != nil { return err } - // 2. Check collateralization statuses of subaccounts with at least one open position. - liquidatableSubaccountIds, err := daemonClient.GetLiquidatableSubaccountIds( + // 1. Fetch all information needed to calculate total net collateral and margin requirements. + subaccounts, + marketPrices, + perpetuals, + liquidityTiers, + err := daemonClient.FetchApplicationStateAtBlockHeight( ctx, + lastCommittedBlockHeight, liqFlags, + ) + if err != nil { + return err + } + + // 2. Check collateralization statuses of subaccounts with at least one open position. + liquidatableSubaccountIds, + negativeTncSubaccountIds, + err := daemonClient.GetLiquidatableSubaccountIds( subaccounts, + marketPrices, + perpetuals, + liquidityTiers, ) if err != nil { return err } + // Build a map of perpetual id to subaccounts with open positions in that perpetual. + subaccountOpenPositionInfo := daemonClient.GetSubaccountOpenPositionInfo(subaccounts) + // 3. Send the list of liquidatable subaccount ids to the daemon server. - err = daemonClient.SendLiquidatableSubaccountIds(ctx, liquidatableSubaccountIds) + err = daemonClient.SendLiquidatableSubaccountIds( + ctx, + lastCommittedBlockHeight, + liquidatableSubaccountIds, + negativeTncSubaccountIds, + subaccountOpenPositionInfo, + ) if err != nil { return err } @@ -65,14 +99,79 @@ func (s *SubTaskRunnerImpl) RunLiquidationDaemonTaskLoop( return nil } +// FetchApplicationStateAtBlockHeight queries a gRPC server and fetches the following information given a block height: +// - Last committed block height. +// - Subaccounts including their open positions. +// - Market prices. +// - Perpetuals. +// - Liquidity tiers. +func (c *Client) FetchApplicationStateAtBlockHeight( + ctx context.Context, + blockHeight uint32, + liqFlags flags.LiquidationFlags, +) ( + subaccounts []satypes.Subaccount, + marketPricesMap map[uint32]pricestypes.MarketPrice, + perpetualsMap map[uint32]perptypes.Perpetual, + liquidityTiersMap map[uint32]perptypes.LiquidityTier, + err error, +) { + defer telemetry.ModuleMeasureSince( + metrics.LiquidationDaemon, + time.Now(), + metrics.FetchApplicationStateAtBlockHeight, + metrics.Latency, + ) + + // Execute all queries at the given block height. + queryCtx := newContextWithQueryBlockHeight(ctx, blockHeight) + + // Subaccounts + subaccounts, err = c.GetAllSubaccounts(queryCtx, liqFlags.QueryPageLimit) + if err != nil { + return nil, nil, nil, nil, err + } + + // Market prices + marketPrices, err := c.GetAllMarketPrices(queryCtx, liqFlags.QueryPageLimit) + if err != nil { + return nil, nil, nil, nil, err + } + marketPricesMap = lib.UniqueSliceToMap(marketPrices, func(m pricestypes.MarketPrice) uint32 { + return m.Id + }) + + // Perpetuals + perpetuals, err := c.GetAllPerpetuals(queryCtx, liqFlags.QueryPageLimit) + if err != nil { + return nil, nil, nil, nil, err + } + perpetualsMap = lib.UniqueSliceToMap(perpetuals, func(p perptypes.Perpetual) uint32 { + return p.Params.Id + }) + + // Liquidity tiers + liquidityTiers, err := c.GetAllLiquidityTiers(queryCtx, liqFlags.QueryPageLimit) + if err != nil { + return nil, nil, nil, nil, err + } + liquidityTiersMap = lib.UniqueSliceToMap(liquidityTiers, func(l perptypes.LiquidityTier) uint32 { + return l.Id + }) + + return subaccounts, marketPricesMap, perpetualsMap, liquidityTiersMap, nil +} + // GetLiquidatableSubaccountIds verifies collateralization statuses of subaccounts with // at least one open position and returns a list of unique and potentially liquidatable subaccount ids. func (c *Client) GetLiquidatableSubaccountIds( - ctx context.Context, - liqFlags flags.LiquidationFlags, subaccounts []satypes.Subaccount, + marketPrices map[uint32]pricestypes.MarketPrice, + perpetuals map[uint32]perptypes.Perpetual, + liquidityTiers map[uint32]perptypes.LiquidityTier, ) ( liquidatableSubaccountIds []satypes.SubaccountId, + negativeTncSubaccountIds []satypes.SubaccountId, err error, ) { defer telemetry.ModuleMeasureSince( @@ -82,39 +181,192 @@ func (c *Client) GetLiquidatableSubaccountIds( metrics.Latency, ) - // Filter out subaccounts with no open positions. - subaccountsToCheck := make([]satypes.SubaccountId, 0) + liquidatableSubaccountIds = make([]satypes.SubaccountId, 0) + negativeTncSubaccountIds = make([]satypes.SubaccountId, 0) for _, subaccount := range subaccounts { - if len(subaccount.PerpetualPositions) > 0 { - subaccountsToCheck = append(subaccountsToCheck, *subaccount.Id) + // Skip subaccounts with no open positions. + if len(subaccount.PerpetualPositions) == 0 { + continue + } + + // Check if the subaccount is liquidatable. + isLiquidatable, hasNegativeTnc, err := c.CheckSubaccountCollateralization( + subaccount, + marketPrices, + perpetuals, + liquidityTiers, + ) + if err != nil { + c.logger.Error("Error checking collateralization status", "error", err) + return nil, nil, err + } + + if isLiquidatable { + liquidatableSubaccountIds = append(liquidatableSubaccountIds, *subaccount.Id) + } + if hasNegativeTnc { + negativeTncSubaccountIds = append(negativeTncSubaccountIds, *subaccount.Id) } } + return liquidatableSubaccountIds, negativeTncSubaccountIds, nil +} + +// GetSubaccountOpenPositionInfo iterates over the given subaccounts and returns a map of +// perpetual id to open position info. +func (c *Client) GetSubaccountOpenPositionInfo( + subaccounts []satypes.Subaccount, +) ( + subaccountOpenPositionInfo map[uint32]*clobtypes.SubaccountOpenPositionInfo, +) { + defer telemetry.ModuleMeasureSince( + metrics.LiquidationDaemon, + time.Now(), + metrics.GetSubaccountOpenPositionInfo, + metrics.Latency, + ) + + numSubaccountsWithOpenPositions := 0 + subaccountOpenPositionInfo = make(map[uint32]*clobtypes.SubaccountOpenPositionInfo) + for _, subaccount := range subaccounts { + // Skip subaccounts with no open positions. + if len(subaccount.PerpetualPositions) == 0 { + continue + } + + for _, perpetualPosition := range subaccount.PerpetualPositions { + openPositionInfo, ok := subaccountOpenPositionInfo[perpetualPosition.PerpetualId] + if !ok { + openPositionInfo = &clobtypes.SubaccountOpenPositionInfo{ + PerpetualId: perpetualPosition.PerpetualId, + SubaccountsWithLongPosition: make([]satypes.SubaccountId, 0), + SubaccountsWithShortPosition: make([]satypes.SubaccountId, 0), + } + subaccountOpenPositionInfo[perpetualPosition.PerpetualId] = openPositionInfo + } + + if perpetualPosition.GetIsLong() { + openPositionInfo.SubaccountsWithLongPosition = append( + openPositionInfo.SubaccountsWithLongPosition, + *subaccount.Id, + ) + } else { + openPositionInfo.SubaccountsWithShortPosition = append( + openPositionInfo.SubaccountsWithShortPosition, + *subaccount.Id, + ) + } + } + + numSubaccountsWithOpenPositions++ + } + telemetry.ModuleSetGauge( metrics.LiquidationDaemon, - float32(len(subaccountsToCheck)), + float32(numSubaccountsWithOpenPositions), metrics.SubaccountsWithOpenPositions, metrics.Count, ) - // Query the gRPC server in chunks of size `liqFlags.RequestChunkSize`. - liquidatableSubaccountIds = make([]satypes.SubaccountId, 0) - for start := 0; start < len(subaccountsToCheck); start += int(liqFlags.RequestChunkSize) { - end := lib.Min(start+int(liqFlags.RequestChunkSize), len(subaccountsToCheck)) + return subaccountOpenPositionInfo +} - results, err := c.CheckCollateralizationForSubaccounts( - ctx, - subaccountsToCheck[start:end], - ) - if err != nil { - return nil, err +// CheckSubaccountCollateralization performs the same collateralization check as the application +// using the provided market prices, perpetuals, and liquidity tiers. +// +// Note that current implementation assumes that the only asset is USDC and multi-collateral support +// is not yet implemented. +func (c *Client) CheckSubaccountCollateralization( + unsettledSubaccount satypes.Subaccount, + marketPrices map[uint32]pricestypes.MarketPrice, + perpetuals map[uint32]perptypes.Perpetual, + liquidityTiers map[uint32]perptypes.LiquidityTier, +) ( + isLiquidatable bool, + hasNegativeTnc bool, + err error, +) { + defer telemetry.ModuleMeasureSince( + metrics.LiquidationDaemon, + time.Now(), + metrics.CheckCollateralizationForSubaccounts, + metrics.Latency, + ) + + // Funding payments are lazily settled, so get the settled subaccount + // to ensure that the funding payments are included in the net collateral calculation. + settledSubaccount, _, err := sakeeper.GetSettledSubaccountWithPerpetuals( + unsettledSubaccount, + perpetuals, + ) + if err != nil { + return false, false, err + } + + bigTotalNetCollateral := big.NewInt(0) + bigTotalMaintenanceMargin := big.NewInt(0) + + // Calculate the net collateral and maintenance margin for each of the asset positions. + // Note that we only expect USDC before multi-collateral support is added. + for _, assetPosition := range settledSubaccount.AssetPositions { + if assetPosition.AssetId != assetstypes.AssetUsdc.Id { + return false, false, errorsmod.Wrapf( + assetstypes.ErrNotImplementedMulticollateral, + "Asset %d is not supported", + assetPosition.AssetId, + ) } + // Net collateral for USDC is the quantums of the position. + // Margin requirements for USDC are zero. + bigTotalNetCollateral.Add(bigTotalNetCollateral, assetPosition.GetBigQuantums()) + } - for _, result := range results { - if result.IsLiquidatable { - liquidatableSubaccountIds = append(liquidatableSubaccountIds, result.SubaccountId) - } + // Calculate the net collateral and maintenance margin for each of the perpetual positions. + for _, perpetualPosition := range settledSubaccount.PerpetualPositions { + perpetual, ok := perpetuals[perpetualPosition.PerpetualId] + if !ok { + return false, false, errorsmod.Wrapf( + perptypes.ErrPerpetualDoesNotExist, + "Perpetual not found for perpetual id %d", + perpetualPosition.PerpetualId, + ) + } + + marketPrice, ok := marketPrices[perpetual.Params.MarketId] + if !ok { + return false, false, errorsmod.Wrapf( + pricestypes.ErrMarketPriceDoesNotExist, + "MarketPrice not found for perpetual %+v", + perpetual, + ) + } + + bigQuantums := perpetualPosition.GetBigQuantums() + + // Get the net collateral for the position. + bigNetCollateralQuoteQuantums := perpkeeper.GetNetNotionalInQuoteQuantums(perpetual, marketPrice, bigQuantums) + bigTotalNetCollateral.Add(bigTotalNetCollateral, bigNetCollateralQuoteQuantums) + + liquidityTier, ok := liquidityTiers[perpetual.Params.LiquidityTier] + if !ok { + return false, false, errorsmod.Wrapf( + perptypes.ErrLiquidityTierDoesNotExist, + "LiquidityTier not found for perpetual %+v", + perpetual, + ) } + + // Get the maintenance margin requirement for the position. + _, bigMaintenanceMarginQuoteQuantums := perpkeeper.GetMarginRequirementsInQuoteQuantums( + perpetual, + marketPrice, + liquidityTier, + bigQuantums, + ) + bigTotalMaintenanceMargin.Add(bigTotalMaintenanceMargin, bigMaintenanceMarginQuoteQuantums) } - return liquidatableSubaccountIds, nil + + return clobkeeper.CanLiquidateSubaccount(bigTotalNetCollateral, bigTotalMaintenanceMargin), + bigTotalNetCollateral.Sign() == -1, + nil } diff --git a/protocol/daemons/liquidation/client/sub_task_runner_test.go b/protocol/daemons/liquidation/client/sub_task_runner_test.go new file mode 100644 index 0000000000..2914e3389d --- /dev/null +++ b/protocol/daemons/liquidation/client/sub_task_runner_test.go @@ -0,0 +1,735 @@ +package client_test + +import ( + "context" + "testing" + + "github.com/cometbft/cometbft/libs/log" + "github.com/dydxprotocol/v4-chain/protocol/daemons/flags" + "github.com/dydxprotocol/v4-chain/protocol/daemons/liquidation/api" + "github.com/dydxprotocol/v4-chain/protocol/daemons/liquidation/client" + "github.com/dydxprotocol/v4-chain/protocol/dtypes" + "github.com/dydxprotocol/v4-chain/protocol/mocks" + "github.com/dydxprotocol/v4-chain/protocol/testutil/constants" + "github.com/dydxprotocol/v4-chain/protocol/testutil/grpc" + blocktimetypes "github.com/dydxprotocol/v4-chain/protocol/x/blocktime/types" + clobtypes "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" + perptypes "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types" + pricestypes "github.com/dydxprotocol/v4-chain/protocol/x/prices/types" + satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestRunLiquidationDaemonTaskLoop(t *testing.T) { + tests := map[string]struct { + // mocks + setupMocks func(ctx context.Context, mck *mocks.QueryClient) + + // expectations + expectedLiquidatableSubaccountIds []satypes.SubaccountId + expectedError error + }{ + "Can get liquidatable subaccount with short position": { + setupMocks: func(ctx context.Context, mck *mocks.QueryClient) { + // Block height. + res := &blocktimetypes.QueryPreviousBlockInfoResponse{ + Info: &blocktimetypes.BlockInfo{ + Height: uint32(50), + Timestamp: constants.TimeTen, + }, + } + mck.On("PreviousBlockInfo", mock.Anything, mock.Anything).Return(res, nil) + + // Subaccount. + res2 := &satypes.QuerySubaccountAllResponse{ + Subaccount: []satypes.Subaccount{ + constants.Carl_Num0_1BTC_Short_54999USD, + }, + } + mck.On("SubaccountAll", mock.Anything, mock.Anything).Return(res2, nil) + + // Market prices. + res3 := &pricestypes.QueryAllMarketPricesResponse{ + MarketPrices: constants.TestMarketPrices, + } + mck.On("AllMarketPrices", mock.Anything, mock.Anything).Return(res3, nil) + + // Perpetuals. + res4 := &perptypes.QueryAllPerpetualsResponse{ + Perpetual: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance, + }, + } + mck.On("AllPerpetuals", mock.Anything, mock.Anything).Return(res4, nil) + + // Liquidity tiers. + res5 := &perptypes.QueryAllLiquidityTiersResponse{ + LiquidityTiers: constants.LiquidityTiers, + } + mck.On("AllLiquidityTiers", mock.Anything, mock.Anything).Return(res5, nil) + + // Sends liquidatable subaccount ids to the server. + req := &api.LiquidateSubaccountsRequest{ + BlockHeight: uint32(50), + LiquidatableSubaccountIds: []satypes.SubaccountId{ + constants.Carl_Num0, + }, + NegativeTncSubaccountIds: []satypes.SubaccountId{}, + SubaccountOpenPositionInfo: []clobtypes.SubaccountOpenPositionInfo{ + { + PerpetualId: 0, + SubaccountsWithLongPosition: []satypes.SubaccountId{}, + SubaccountsWithShortPosition: []satypes.SubaccountId{ + constants.Carl_Num0, + }, + }, + }, + } + response3 := &api.LiquidateSubaccountsResponse{} + mck.On("LiquidateSubaccounts", ctx, req).Return(response3, nil) + }, + }, + "Can get liquidatable subaccount with long position": { + setupMocks: func(ctx context.Context, mck *mocks.QueryClient) { + // Block height. + res := &blocktimetypes.QueryPreviousBlockInfoResponse{ + Info: &blocktimetypes.BlockInfo{ + Height: uint32(50), + Timestamp: constants.TimeTen, + }, + } + mck.On("PreviousBlockInfo", mock.Anything, mock.Anything).Return(res, nil) + + // Subaccount. + res2 := &satypes.QuerySubaccountAllResponse{ + Subaccount: []satypes.Subaccount{ + constants.Dave_Num0_1BTC_Long_45001USD_Short, + }, + } + mck.On("SubaccountAll", mock.Anything, mock.Anything).Return(res2, nil) + + // Market prices. + res3 := &pricestypes.QueryAllMarketPricesResponse{ + MarketPrices: constants.TestMarketPrices, + } + mck.On("AllMarketPrices", mock.Anything, mock.Anything).Return(res3, nil) + + // Perpetuals. + res4 := &perptypes.QueryAllPerpetualsResponse{ + Perpetual: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance, + }, + } + mck.On("AllPerpetuals", mock.Anything, mock.Anything).Return(res4, nil) + + // Liquidity tiers. + res5 := &perptypes.QueryAllLiquidityTiersResponse{ + LiquidityTiers: constants.LiquidityTiers, + } + mck.On("AllLiquidityTiers", mock.Anything, mock.Anything).Return(res5, nil) + + // Sends liquidatable subaccount ids to the server. + req := &api.LiquidateSubaccountsRequest{ + BlockHeight: uint32(50), + LiquidatableSubaccountIds: []satypes.SubaccountId{ + constants.Dave_Num0, + }, + NegativeTncSubaccountIds: []satypes.SubaccountId{}, + SubaccountOpenPositionInfo: []clobtypes.SubaccountOpenPositionInfo{ + { + PerpetualId: 0, + SubaccountsWithLongPosition: []satypes.SubaccountId{ + constants.Dave_Num0, + }, + SubaccountsWithShortPosition: []satypes.SubaccountId{}, + }, + }, + } + response3 := &api.LiquidateSubaccountsResponse{} + mck.On("LiquidateSubaccounts", ctx, req).Return(response3, nil) + }, + }, + "Skip well collateralized subaccounts": { + setupMocks: func(ctx context.Context, mck *mocks.QueryClient) { + // Block height. + res := &blocktimetypes.QueryPreviousBlockInfoResponse{ + Info: &blocktimetypes.BlockInfo{ + Height: uint32(50), + Timestamp: constants.TimeTen, + }, + } + mck.On("PreviousBlockInfo", mock.Anything, mock.Anything).Return(res, nil) + + // Subaccount. + res2 := &satypes.QuerySubaccountAllResponse{ + Subaccount: []satypes.Subaccount{ + constants.Carl_Num0_1BTC_Short_55000USD, + constants.Dave_Num0_1BTC_Long_45000USD_Short, + }, + } + mck.On("SubaccountAll", mock.Anything, mock.Anything).Return(res2, nil) + + // Market prices. + res3 := &pricestypes.QueryAllMarketPricesResponse{ + MarketPrices: constants.TestMarketPrices, + } + mck.On("AllMarketPrices", mock.Anything, mock.Anything).Return(res3, nil) + + // Perpetuals. + res4 := &perptypes.QueryAllPerpetualsResponse{ + Perpetual: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance, + }, + } + mck.On("AllPerpetuals", mock.Anything, mock.Anything).Return(res4, nil) + + // Liquidity tiers. + res5 := &perptypes.QueryAllLiquidityTiersResponse{ + LiquidityTiers: constants.LiquidityTiers, + } + mck.On("AllLiquidityTiers", mock.Anything, mock.Anything).Return(res5, nil) + + // Sends liquidatable subaccount ids to the server. + req := &api.LiquidateSubaccountsRequest{ + BlockHeight: uint32(50), + LiquidatableSubaccountIds: []satypes.SubaccountId{}, + NegativeTncSubaccountIds: []satypes.SubaccountId{}, + SubaccountOpenPositionInfo: []clobtypes.SubaccountOpenPositionInfo{ + { + PerpetualId: 0, + SubaccountsWithLongPosition: []satypes.SubaccountId{ + constants.Dave_Num0, + }, + SubaccountsWithShortPosition: []satypes.SubaccountId{ + constants.Carl_Num0, + }, + }, + }, + } + response3 := &api.LiquidateSubaccountsResponse{} + mck.On("LiquidateSubaccounts", ctx, req).Return(response3, nil) + }, + }, + "Skip subaccounts with no open positions": { + setupMocks: func(ctx context.Context, mck *mocks.QueryClient) { + // Block height. + res := &blocktimetypes.QueryPreviousBlockInfoResponse{ + Info: &blocktimetypes.BlockInfo{ + Height: uint32(50), + Timestamp: constants.TimeTen, + }, + } + mck.On("PreviousBlockInfo", mock.Anything, mock.Anything).Return(res, nil) + + // Subaccount. + res2 := &satypes.QuerySubaccountAllResponse{ + Subaccount: []satypes.Subaccount{ + constants.Alice_Num0_100_000USD, + }, + } + mck.On("SubaccountAll", mock.Anything, mock.Anything).Return(res2, nil) + + // Market prices. + res3 := &pricestypes.QueryAllMarketPricesResponse{ + MarketPrices: constants.TestMarketPrices, + } + mck.On("AllMarketPrices", mock.Anything, mock.Anything).Return(res3, nil) + + // Perpetuals. + res4 := &perptypes.QueryAllPerpetualsResponse{ + Perpetual: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance, + }, + } + mck.On("AllPerpetuals", mock.Anything, mock.Anything).Return(res4, nil) + + // Liquidity tiers. + res5 := &perptypes.QueryAllLiquidityTiersResponse{ + LiquidityTiers: constants.LiquidityTiers, + } + mck.On("AllLiquidityTiers", mock.Anything, mock.Anything).Return(res5, nil) + + // Sends liquidatable subaccount ids to the server. + req := &api.LiquidateSubaccountsRequest{ + BlockHeight: uint32(50), + LiquidatableSubaccountIds: []satypes.SubaccountId{}, + NegativeTncSubaccountIds: []satypes.SubaccountId{}, + SubaccountOpenPositionInfo: []clobtypes.SubaccountOpenPositionInfo{}, + } + response3 := &api.LiquidateSubaccountsResponse{} + mck.On("LiquidateSubaccounts", ctx, req).Return(response3, nil) + }, + }, + "Can get subaccount that become undercollateralized with funding payments (short)": { + setupMocks: func(ctx context.Context, mck *mocks.QueryClient) { + // Block height. + res := &blocktimetypes.QueryPreviousBlockInfoResponse{ + Info: &blocktimetypes.BlockInfo{ + Height: uint32(50), + Timestamp: constants.TimeTen, + }, + } + mck.On("PreviousBlockInfo", mock.Anything, mock.Anything).Return(res, nil) + + // Subaccount. + res2 := &satypes.QuerySubaccountAllResponse{ + Subaccount: []satypes.Subaccount{ + // Without funding, Carl has a TNC of $5,000, MMR of $5,000, and is + // well-collateralized. + // However, funding index for Carl's position is 10,000 and perpetual's funding index + // is 0. Index delta is -10,000, so Carl has to make a funding payment of $1 and + // become under-collateralized. + { + Id: &constants.Carl_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(55_000_000_000), // $55,000 + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(-100_000_000), // -1 BTC + FundingIndex: dtypes.NewInt(10_000), + }, + }, + }, + }, + } + mck.On("SubaccountAll", mock.Anything, mock.Anything).Return(res2, nil) + + // Market prices. + res3 := &pricestypes.QueryAllMarketPricesResponse{ + MarketPrices: constants.TestMarketPrices, + } + mck.On("AllMarketPrices", mock.Anything, mock.Anything).Return(res3, nil) + + // Perpetuals. + res4 := &perptypes.QueryAllPerpetualsResponse{ + Perpetual: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance, + }, + } + mck.On("AllPerpetuals", mock.Anything, mock.Anything).Return(res4, nil) + + // Liquidity tiers. + res5 := &perptypes.QueryAllLiquidityTiersResponse{ + LiquidityTiers: constants.LiquidityTiers, + } + mck.On("AllLiquidityTiers", mock.Anything, mock.Anything).Return(res5, nil) + + // Sends liquidatable subaccount ids to the server. + req := &api.LiquidateSubaccountsRequest{ + BlockHeight: uint32(50), + LiquidatableSubaccountIds: []satypes.SubaccountId{ + constants.Carl_Num0, + }, + NegativeTncSubaccountIds: []satypes.SubaccountId{}, + SubaccountOpenPositionInfo: []clobtypes.SubaccountOpenPositionInfo{ + { + PerpetualId: 0, + SubaccountsWithLongPosition: []satypes.SubaccountId{}, + SubaccountsWithShortPosition: []satypes.SubaccountId{ + constants.Carl_Num0, + }, + }, + }, + } + response3 := &api.LiquidateSubaccountsResponse{} + mck.On("LiquidateSubaccounts", ctx, req).Return(response3, nil) + }, + }, + "Can get subaccount that become liquidatable with funding payments (long)": { + setupMocks: func(ctx context.Context, mck *mocks.QueryClient) { + // Block height. + res := &blocktimetypes.QueryPreviousBlockInfoResponse{ + Info: &blocktimetypes.BlockInfo{ + Height: uint32(50), + Timestamp: constants.TimeTen, + }, + } + mck.On("PreviousBlockInfo", mock.Anything, mock.Anything).Return(res, nil) + + // Subaccount. + res2 := &satypes.QuerySubaccountAllResponse{ + Subaccount: []satypes.Subaccount{ + // Without funding, Dave has a TNC of $5,000, MMR of $5,000, and is + // well-collateralized. + // However, funding index for Dave's position is -10,000 and perpetual's funding index + // is 0. Index delta is 10,000, so Dave has to make a funding payment of $1 and + // become under-collateralized. + { + Id: &constants.Dave_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(-45_000_000_000), // -$45,000 + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(100_000_000), // 1 BTC + FundingIndex: dtypes.NewInt(-10_000), + }, + }, + }, + }, + } + mck.On("SubaccountAll", mock.Anything, mock.Anything).Return(res2, nil) + + // Market prices. + res3 := &pricestypes.QueryAllMarketPricesResponse{ + MarketPrices: constants.TestMarketPrices, + } + mck.On("AllMarketPrices", mock.Anything, mock.Anything).Return(res3, nil) + + // Perpetuals. + res4 := &perptypes.QueryAllPerpetualsResponse{ + Perpetual: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance, + }, + } + mck.On("AllPerpetuals", mock.Anything, mock.Anything).Return(res4, nil) + + // Liquidity tiers. + res5 := &perptypes.QueryAllLiquidityTiersResponse{ + LiquidityTiers: constants.LiquidityTiers, + } + mck.On("AllLiquidityTiers", mock.Anything, mock.Anything).Return(res5, nil) + + // Sends liquidatable subaccount ids to the server. + req := &api.LiquidateSubaccountsRequest{ + BlockHeight: uint32(50), + LiquidatableSubaccountIds: []satypes.SubaccountId{ + constants.Dave_Num0, + }, + NegativeTncSubaccountIds: []satypes.SubaccountId{}, + SubaccountOpenPositionInfo: []clobtypes.SubaccountOpenPositionInfo{ + { + PerpetualId: 0, + SubaccountsWithLongPosition: []satypes.SubaccountId{ + constants.Dave_Num0, + }, + SubaccountsWithShortPosition: []satypes.SubaccountId{}, + }, + }, + } + response3 := &api.LiquidateSubaccountsResponse{} + mck.On("LiquidateSubaccounts", ctx, req).Return(response3, nil) + }, + }, + "Skips subaccount that become well-collateralized with funding payments (short)": { + setupMocks: func(ctx context.Context, mck *mocks.QueryClient) { + // Block height. + res := &blocktimetypes.QueryPreviousBlockInfoResponse{ + Info: &blocktimetypes.BlockInfo{ + Height: uint32(50), + Timestamp: constants.TimeTen, + }, + } + mck.On("PreviousBlockInfo", mock.Anything, mock.Anything).Return(res, nil) + + // Subaccount. + res2 := &satypes.QuerySubaccountAllResponse{ + Subaccount: []satypes.Subaccount{ + // Without funding, Carl has a TNC of $4,999, MMR of $5,000, and is + // under-collateralized. + // However, funding index for Carl's position is -10,000 and perpetual's funding index + // is 0. Index delta is 10,000, so Carl would receive a funding payment of $1 and + // become well-collateralized. + { + Id: &constants.Carl_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(54_999_000_000), // $54,999 + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(-100_000_000), // -1 BTC + FundingIndex: dtypes.NewInt(-10_000), + }, + }, + }, + }, + } + mck.On("SubaccountAll", mock.Anything, mock.Anything).Return(res2, nil) + + // Market prices. + res3 := &pricestypes.QueryAllMarketPricesResponse{ + MarketPrices: constants.TestMarketPrices, + } + mck.On("AllMarketPrices", mock.Anything, mock.Anything).Return(res3, nil) + + // Perpetuals. + res4 := &perptypes.QueryAllPerpetualsResponse{ + Perpetual: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance, + }, + } + mck.On("AllPerpetuals", mock.Anything, mock.Anything).Return(res4, nil) + + // Liquidity tiers. + res5 := &perptypes.QueryAllLiquidityTiersResponse{ + LiquidityTiers: constants.LiquidityTiers, + } + mck.On("AllLiquidityTiers", mock.Anything, mock.Anything).Return(res5, nil) + + // Sends liquidatable subaccount ids to the server. + req := &api.LiquidateSubaccountsRequest{ + BlockHeight: uint32(50), + LiquidatableSubaccountIds: []satypes.SubaccountId{}, + NegativeTncSubaccountIds: []satypes.SubaccountId{}, + SubaccountOpenPositionInfo: []clobtypes.SubaccountOpenPositionInfo{ + { + PerpetualId: 0, + SubaccountsWithLongPosition: []satypes.SubaccountId{}, + SubaccountsWithShortPosition: []satypes.SubaccountId{ + constants.Carl_Num0, + }, + }, + }, + } + response3 := &api.LiquidateSubaccountsResponse{} + mck.On("LiquidateSubaccounts", ctx, req).Return(response3, nil) + }, + }, + "Skips subaccount that become well-collateralized with funding payments (long)": { + setupMocks: func(ctx context.Context, mck *mocks.QueryClient) { + // Block height. + res := &blocktimetypes.QueryPreviousBlockInfoResponse{ + Info: &blocktimetypes.BlockInfo{ + Height: uint32(50), + Timestamp: constants.TimeTen, + }, + } + mck.On("PreviousBlockInfo", mock.Anything, mock.Anything).Return(res, nil) + + // Subaccount. + res2 := &satypes.QuerySubaccountAllResponse{ + Subaccount: []satypes.Subaccount{ + // Without funding, Dave has a TNC of $4,999, MMR of $5,000, and is + // under-collateralized. + // However, funding index for Dave's position is 10,000 and perpetual's funding index + // is 0. Index delta is -10,000, so Dave would receive a funding payment of $1 and + // become well-collateralized. + { + Id: &constants.Dave_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(-44_999_000_000), // -$44,999 + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(100_000_000), // 1 BTC + FundingIndex: dtypes.NewInt(10_000), + }, + }, + }, + }, + } + mck.On("SubaccountAll", mock.Anything, mock.Anything).Return(res2, nil) + + // Market prices. + res3 := &pricestypes.QueryAllMarketPricesResponse{ + MarketPrices: constants.TestMarketPrices, + } + mck.On("AllMarketPrices", mock.Anything, mock.Anything).Return(res3, nil) + + // Perpetuals. + res4 := &perptypes.QueryAllPerpetualsResponse{ + Perpetual: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance, + }, + } + mck.On("AllPerpetuals", mock.Anything, mock.Anything).Return(res4, nil) + + // Liquidity tiers. + res5 := &perptypes.QueryAllLiquidityTiersResponse{ + LiquidityTiers: constants.LiquidityTiers, + } + mck.On("AllLiquidityTiers", mock.Anything, mock.Anything).Return(res5, nil) + + // Sends liquidatable subaccount ids to the server. + req := &api.LiquidateSubaccountsRequest{ + BlockHeight: uint32(50), + LiquidatableSubaccountIds: []satypes.SubaccountId{}, + NegativeTncSubaccountIds: []satypes.SubaccountId{}, + SubaccountOpenPositionInfo: []clobtypes.SubaccountOpenPositionInfo{ + { + PerpetualId: 0, + SubaccountsWithLongPosition: []satypes.SubaccountId{ + constants.Dave_Num0, + }, + SubaccountsWithShortPosition: []satypes.SubaccountId{}, + }, + }, + } + response3 := &api.LiquidateSubaccountsResponse{} + mck.On("LiquidateSubaccounts", ctx, req).Return(response3, nil) + }, + }, + "Can get negative tnc subaccount with short position": { + setupMocks: func(ctx context.Context, mck *mocks.QueryClient) { + // Block height. + res := &blocktimetypes.QueryPreviousBlockInfoResponse{ + Info: &blocktimetypes.BlockInfo{ + Height: uint32(50), + Timestamp: constants.TimeTen, + }, + } + mck.On("PreviousBlockInfo", mock.Anything, mock.Anything).Return(res, nil) + + // Subaccount. + res2 := &satypes.QuerySubaccountAllResponse{ + Subaccount: []satypes.Subaccount{ + // Carl has TNC of -$1. + constants.Carl_Num0_1BTC_Short_49999USD, + }, + } + mck.On("SubaccountAll", mock.Anything, mock.Anything).Return(res2, nil) + + // Market prices. + res3 := &pricestypes.QueryAllMarketPricesResponse{ + MarketPrices: constants.TestMarketPrices, + } + mck.On("AllMarketPrices", mock.Anything, mock.Anything).Return(res3, nil) + + // Perpetuals. + res4 := &perptypes.QueryAllPerpetualsResponse{ + Perpetual: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance, + }, + } + mck.On("AllPerpetuals", mock.Anything, mock.Anything).Return(res4, nil) + + // Liquidity tiers. + res5 := &perptypes.QueryAllLiquidityTiersResponse{ + LiquidityTiers: constants.LiquidityTiers, + } + mck.On("AllLiquidityTiers", mock.Anything, mock.Anything).Return(res5, nil) + + // Sends liquidatable subaccount ids to the server. + req := &api.LiquidateSubaccountsRequest{ + BlockHeight: uint32(50), + LiquidatableSubaccountIds: []satypes.SubaccountId{ + constants.Carl_Num0, + }, + NegativeTncSubaccountIds: []satypes.SubaccountId{ + constants.Carl_Num0, + }, + SubaccountOpenPositionInfo: []clobtypes.SubaccountOpenPositionInfo{ + { + PerpetualId: 0, + SubaccountsWithLongPosition: []satypes.SubaccountId{}, + SubaccountsWithShortPosition: []satypes.SubaccountId{ + constants.Carl_Num0, + }, + }, + }, + } + response3 := &api.LiquidateSubaccountsResponse{} + mck.On("LiquidateSubaccounts", ctx, req).Return(response3, nil) + }, + }, + "Can get negative tnc subaccount with long position": { + setupMocks: func(ctx context.Context, mck *mocks.QueryClient) { + // Block height. + res := &blocktimetypes.QueryPreviousBlockInfoResponse{ + Info: &blocktimetypes.BlockInfo{ + Height: uint32(50), + Timestamp: constants.TimeTen, + }, + } + mck.On("PreviousBlockInfo", mock.Anything, mock.Anything).Return(res, nil) + + // Subaccount. + res2 := &satypes.QuerySubaccountAllResponse{ + Subaccount: []satypes.Subaccount{ + // Dave has TNC of -$1. + constants.Dave_Num0_1BTC_Long_50001USD_Short, + }, + } + mck.On("SubaccountAll", mock.Anything, mock.Anything).Return(res2, nil) + + // Market prices. + res3 := &pricestypes.QueryAllMarketPricesResponse{ + MarketPrices: constants.TestMarketPrices, + } + mck.On("AllMarketPrices", mock.Anything, mock.Anything).Return(res3, nil) + + // Perpetuals. + res4 := &perptypes.QueryAllPerpetualsResponse{ + Perpetual: []perptypes.Perpetual{ + constants.BtcUsd_20PercentInitial_10PercentMaintenance, + }, + } + mck.On("AllPerpetuals", mock.Anything, mock.Anything).Return(res4, nil) + + // Liquidity tiers. + res5 := &perptypes.QueryAllLiquidityTiersResponse{ + LiquidityTiers: constants.LiquidityTiers, + } + mck.On("AllLiquidityTiers", mock.Anything, mock.Anything).Return(res5, nil) + + // Sends liquidatable subaccount ids to the server. + req := &api.LiquidateSubaccountsRequest{ + BlockHeight: uint32(50), + LiquidatableSubaccountIds: []satypes.SubaccountId{ + constants.Dave_Num0, + }, + NegativeTncSubaccountIds: []satypes.SubaccountId{ + constants.Dave_Num0, + }, + SubaccountOpenPositionInfo: []clobtypes.SubaccountOpenPositionInfo{ + { + PerpetualId: 0, + SubaccountsWithLongPosition: []satypes.SubaccountId{ + constants.Dave_Num0, + }, + SubaccountsWithShortPosition: []satypes.SubaccountId{}, + }, + }, + } + response3 := &api.LiquidateSubaccountsResponse{} + mck.On("LiquidateSubaccounts", ctx, req).Return(response3, nil) + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + queryClientMock := &mocks.QueryClient{} + tc.setupMocks(grpc.Ctx, queryClientMock) + s := client.SubTaskRunnerImpl{} + + c := client.NewClient(log.NewNopLogger()) + c.SubaccountQueryClient = queryClientMock + c.ClobQueryClient = queryClientMock + c.LiquidationServiceClient = queryClientMock + c.PerpetualsQueryClient = queryClientMock + c.PricesQueryClient = queryClientMock + c.BlocktimeQueryClient = queryClientMock + + err := s.RunLiquidationDaemonTaskLoop( + grpc.Ctx, + c, + flags.GetDefaultDaemonFlags().Liquidation, + ) + if tc.expectedError != nil { + require.EqualError(t, err, tc.expectedError.Error()) + } else { + require.NoError(t, err) + queryClientMock.AssertExpectations(t) + } + }) + } +} diff --git a/protocol/daemons/server/liquidation.go b/protocol/daemons/server/liquidation.go index 753609ccb8..573ede7eb7 100644 --- a/protocol/daemons/server/liquidation.go +++ b/protocol/daemons/server/liquidation.go @@ -42,7 +42,10 @@ func (s *Server) LiquidateSubaccounts( metrics.Count, ) + s.daemonLiquidationInfo.UpdateBlockHeight(req.BlockHeight) s.daemonLiquidationInfo.UpdateLiquidatableSubaccountIds(req.LiquidatableSubaccountIds) + s.daemonLiquidationInfo.UpdateNegativeTncSubaccountIds(req.NegativeTncSubaccountIds) + s.daemonLiquidationInfo.UpdateSubaccountsWithPositions(req.SubaccountOpenPositionInfo) // Capture valid responses in metrics. s.reportValidResponse(types.LiquidationsDaemonServiceName) diff --git a/protocol/daemons/server/liquidation_test.go b/protocol/daemons/server/liquidation_test.go index f6b3186294..7fca88b369 100644 --- a/protocol/daemons/server/liquidation_test.go +++ b/protocol/daemons/server/liquidation_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestLiquidateSubaccounts_Empty_Update(t *testing.T) { +func TestLiquidateSubaccounts_Empty_Update_Liquidatable_SubaccountIds(t *testing.T) { mockGrpcServer := &mocks.GrpcServer{} mockFileHandler := &mocks.FileHandler{} daemonLiquidationInfo := liquidationtypes.NewDaemonLiquidationInfo() @@ -31,7 +31,7 @@ func TestLiquidateSubaccounts_Empty_Update(t *testing.T) { require.Empty(t, daemonLiquidationInfo.GetLiquidatableSubaccountIds()) } -func TestLiquidateSubaccounts_Multiple_Subaccount_Ids(t *testing.T) { +func TestLiquidateSubaccounts_Multiple_Liquidatable_Subaccount_Ids(t *testing.T) { mockGrpcServer := &mocks.GrpcServer{} mockFileHandler := &mocks.FileHandler{} daemonLiquidationInfo := liquidationtypes.NewDaemonLiquidationInfo() @@ -57,3 +57,69 @@ func TestLiquidateSubaccounts_Multiple_Subaccount_Ids(t *testing.T) { actualSubaccountIds := daemonLiquidationInfo.GetLiquidatableSubaccountIds() require.Equal(t, expectedSubaccountIds, actualSubaccountIds) } + +func TestLiquidateSubaccounts_GetSetBlockHeight(t *testing.T) { + mockGrpcServer := &mocks.GrpcServer{} + mockFileHandler := &mocks.FileHandler{} + daemonLiquidationInfo := liquidationtypes.NewDaemonLiquidationInfo() + + s := createServerWithMocks( + t, + mockGrpcServer, + mockFileHandler, + ).WithDaemonLiquidationInfo( + daemonLiquidationInfo, + ) + _, err := s.LiquidateSubaccounts(grpc.Ctx, &api.LiquidateSubaccountsRequest{ + BlockHeight: uint32(123), + LiquidatableSubaccountIds: []satypes.SubaccountId{}, + }) + require.NoError(t, err) + require.Equal(t, uint32(123), daemonLiquidationInfo.GetBlockHeight()) +} + +func TestLiquidateSubaccounts_Empty_Update_Negative_TNC_SubaccountIds(t *testing.T) { + mockGrpcServer := &mocks.GrpcServer{} + mockFileHandler := &mocks.FileHandler{} + daemonLiquidationInfo := liquidationtypes.NewDaemonLiquidationInfo() + + s := createServerWithMocks( + t, + mockGrpcServer, + mockFileHandler, + ).WithDaemonLiquidationInfo( + daemonLiquidationInfo, + ) + _, err := s.LiquidateSubaccounts(grpc.Ctx, &api.LiquidateSubaccountsRequest{ + NegativeTncSubaccountIds: []satypes.SubaccountId{}, + }) + require.NoError(t, err) + require.Empty(t, daemonLiquidationInfo.GetNegativeTncSubaccountIds()) +} + +func TestLiquidateSubaccounts_Multiple_Negative_TNC_Subaccount_Ids(t *testing.T) { + mockGrpcServer := &mocks.GrpcServer{} + mockFileHandler := &mocks.FileHandler{} + daemonLiquidationInfo := liquidationtypes.NewDaemonLiquidationInfo() + + s := createServerWithMocks( + t, + mockGrpcServer, + mockFileHandler, + ).WithDaemonLiquidationInfo( + daemonLiquidationInfo, + ) + + expectedSubaccountIds := []satypes.SubaccountId{ + constants.Alice_Num1, + constants.Bob_Num0, + constants.Carl_Num0, + } + _, err := s.LiquidateSubaccounts(grpc.Ctx, &api.LiquidateSubaccountsRequest{ + NegativeTncSubaccountIds: expectedSubaccountIds, + }) + require.NoError(t, err) + + actualSubaccountIds := daemonLiquidationInfo.GetNegativeTncSubaccountIds() + require.Equal(t, expectedSubaccountIds, actualSubaccountIds) +} diff --git a/protocol/daemons/server/types/liquidations/daemon_liquidation_info.go b/protocol/daemons/server/types/liquidations/daemon_liquidation_info.go index a416bfb499..68f887abf7 100644 --- a/protocol/daemons/server/types/liquidations/daemon_liquidation_info.go +++ b/protocol/daemons/server/types/liquidations/daemon_liquidation_info.go @@ -80,20 +80,20 @@ func (ls *DaemonLiquidationInfo) GetNegativeTncSubaccountIds() []satypes.Subacco // UpdateSubaccountsWithPositions updates the struct with the given a list of subaccount ids with open positions. func (ls *DaemonLiquidationInfo) UpdateSubaccountsWithPositions( - subaccountsWithPositions map[uint32]*clobtypes.SubaccountOpenPositionInfo, + subaccountsWithPositions []clobtypes.SubaccountOpenPositionInfo, ) { ls.Lock() defer ls.Unlock() ls.subaccountsWithPositions = make(map[uint32]*clobtypes.SubaccountOpenPositionInfo) - for perpetualId, info := range subaccountsWithPositions { + for _, info := range subaccountsWithPositions { clone := &clobtypes.SubaccountOpenPositionInfo{ - PerpetualId: perpetualId, + PerpetualId: info.PerpetualId, SubaccountsWithLongPosition: make([]satypes.SubaccountId, len(info.SubaccountsWithLongPosition)), SubaccountsWithShortPosition: make([]satypes.SubaccountId, len(info.SubaccountsWithShortPosition)), } copy(clone.SubaccountsWithLongPosition, info.SubaccountsWithLongPosition) copy(clone.SubaccountsWithShortPosition, info.SubaccountsWithShortPosition) - ls.subaccountsWithPositions[perpetualId] = clone + ls.subaccountsWithPositions[info.PerpetualId] = clone } } diff --git a/protocol/daemons/server/types/liquidations/daemon_liquidation_info_test.go b/protocol/daemons/server/types/liquidations/daemon_liquidation_info_test.go index 87b6b275b3..e699d28473 100644 --- a/protocol/daemons/server/types/liquidations/daemon_liquidation_info_test.go +++ b/protocol/daemons/server/types/liquidations/daemon_liquidation_info_test.go @@ -49,18 +49,22 @@ func TestSubaccountsWithOpenPositions_Multiple_Reads(t *testing.T) { ls := liquidationstypes.NewDaemonLiquidationInfo() require.Empty(t, ls.GetNegativeTncSubaccountIds()) - expected := map[uint32]*clobtypes.SubaccountOpenPositionInfo{ - 0: { - PerpetualId: 0, - SubaccountsWithLongPosition: []satypes.SubaccountId{ - constants.Alice_Num1, - }, - SubaccountsWithShortPosition: []satypes.SubaccountId{ - constants.Bob_Num0, - }, + info := clobtypes.SubaccountOpenPositionInfo{ + PerpetualId: 0, + SubaccountsWithLongPosition: []satypes.SubaccountId{ + constants.Alice_Num1, + }, + SubaccountsWithShortPosition: []satypes.SubaccountId{ + constants.Bob_Num0, }, } - ls.UpdateSubaccountsWithPositions(expected) + + input := []clobtypes.SubaccountOpenPositionInfo{info} + ls.UpdateSubaccountsWithPositions(input) + + expected := map[uint32]*clobtypes.SubaccountOpenPositionInfo{ + 0: &info, + } require.Equal(t, expected, ls.GetSubaccountsWithPositions()) require.Equal(t, expected, ls.GetSubaccountsWithPositions()) require.Equal(t, expected, ls.GetSubaccountsWithPositions()) @@ -116,46 +120,55 @@ func TestSubaccountsWithOpenPositions_Multiple_Writes(t *testing.T) { ls := liquidationstypes.NewDaemonLiquidationInfo() require.Empty(t, ls.GetSubaccountsWithPositions()) - expected := map[uint32]*clobtypes.SubaccountOpenPositionInfo{ - 0: { - PerpetualId: 0, - SubaccountsWithLongPosition: []satypes.SubaccountId{ - constants.Alice_Num1, - }, - SubaccountsWithShortPosition: []satypes.SubaccountId{ - constants.Bob_Num0, - }, + info := clobtypes.SubaccountOpenPositionInfo{ + PerpetualId: 0, + SubaccountsWithLongPosition: []satypes.SubaccountId{ + constants.Alice_Num1, }, + SubaccountsWithShortPosition: []satypes.SubaccountId{ + constants.Bob_Num0, + }, + } + + input := []clobtypes.SubaccountOpenPositionInfo{info} + ls.UpdateSubaccountsWithPositions(input) + expected := map[uint32]*clobtypes.SubaccountOpenPositionInfo{ + 0: &info, } - ls.UpdateSubaccountsWithPositions(expected) require.Equal(t, expected, ls.GetSubaccountsWithPositions()) - expected = map[uint32]*clobtypes.SubaccountOpenPositionInfo{ - 0: { - PerpetualId: 0, - SubaccountsWithLongPosition: []satypes.SubaccountId{ - constants.Carl_Num0, - }, - SubaccountsWithShortPosition: []satypes.SubaccountId{ - constants.Dave_Num0, - }, + info2 := clobtypes.SubaccountOpenPositionInfo{ + PerpetualId: 0, + SubaccountsWithLongPosition: []satypes.SubaccountId{ + constants.Carl_Num0, + }, + SubaccountsWithShortPosition: []satypes.SubaccountId{ + constants.Dave_Num0, }, } - ls.UpdateSubaccountsWithPositions(expected) - require.Equal(t, expected, ls.GetSubaccountsWithPositions()) + input2 := []clobtypes.SubaccountOpenPositionInfo{info2} + ls.UpdateSubaccountsWithPositions(input2) expected = map[uint32]*clobtypes.SubaccountOpenPositionInfo{ - 0: { - PerpetualId: 0, - SubaccountsWithLongPosition: []satypes.SubaccountId{ - constants.Dave_Num1, - }, - SubaccountsWithShortPosition: []satypes.SubaccountId{ - constants.Alice_Num1, - }, + 0: &info2, + } + require.Equal(t, expected, ls.GetSubaccountsWithPositions()) + + info3 := clobtypes.SubaccountOpenPositionInfo{ + PerpetualId: 0, + SubaccountsWithLongPosition: []satypes.SubaccountId{ + constants.Dave_Num1, + }, + SubaccountsWithShortPosition: []satypes.SubaccountId{ + constants.Alice_Num1, }, } - ls.UpdateSubaccountsWithPositions(expected) + + input3 := []clobtypes.SubaccountOpenPositionInfo{info3} + ls.UpdateSubaccountsWithPositions(input3) + expected = map[uint32]*clobtypes.SubaccountOpenPositionInfo{ + 0: &info3, + } require.Equal(t, expected, ls.GetSubaccountsWithPositions()) } @@ -193,21 +206,23 @@ func TestSubaccountsWithOpenPosition_Empty_Update(t *testing.T) { ls := liquidationstypes.NewDaemonLiquidationInfo() require.Empty(t, ls.GetSubaccountsWithPositions()) - expected := map[uint32]*clobtypes.SubaccountOpenPositionInfo{ - 0: { - PerpetualId: 0, - SubaccountsWithLongPosition: []satypes.SubaccountId{ - constants.Alice_Num1, - }, - SubaccountsWithShortPosition: []satypes.SubaccountId{ - constants.Bob_Num0, - }, + info := clobtypes.SubaccountOpenPositionInfo{ + PerpetualId: 0, + SubaccountsWithLongPosition: []satypes.SubaccountId{ + constants.Alice_Num1, }, + SubaccountsWithShortPosition: []satypes.SubaccountId{ + constants.Bob_Num0, + }, + } + input := []clobtypes.SubaccountOpenPositionInfo{info} + ls.UpdateSubaccountsWithPositions(input) + expected := map[uint32]*clobtypes.SubaccountOpenPositionInfo{ + 0: &info, } - ls.UpdateSubaccountsWithPositions(expected) require.Equal(t, expected, ls.GetSubaccountsWithPositions()) - expected = map[uint32]*clobtypes.SubaccountOpenPositionInfo{} - ls.UpdateSubaccountsWithPositions(expected) + input2 := []clobtypes.SubaccountOpenPositionInfo{} + ls.UpdateSubaccountsWithPositions(input2) require.Empty(t, ls.GetSubaccountsWithPositions()) } diff --git a/protocol/lib/collections.go b/protocol/lib/collections.go index a00a0347df..5f57e7ff42 100644 --- a/protocol/lib/collections.go +++ b/protocol/lib/collections.go @@ -50,6 +50,24 @@ func UniqueSliceToSet[K comparable](values []K) map[K]struct{} { return set } +// UniqueSliceToMap converts a slice to a map using the provided keyFunc to generate the key. +func UniqueSliceToMap[K comparable, V any](slice []V, keyFunc func(V) K) map[K]V { + m := make(map[K]V) + for _, v := range slice { + k := keyFunc(v) + if _, exists := m[k]; exists { + panic( + fmt.Sprintf( + "UniqueSliceToMap: duplicate value: %+v", + v, + ), + ) + } + m[k] = v + } + return m +} + // MapSlice takes a function and executes that function on each element of a slice, returning the result. // Note the function must return one result for each element of the slice. func MapSlice[V any, E any](values []V, mapFunc func(V) E) []E { diff --git a/protocol/lib/collections_test.go b/protocol/lib/collections_test.go index 14878e8365..6b0f75443a 100644 --- a/protocol/lib/collections_test.go +++ b/protocol/lib/collections_test.go @@ -104,6 +104,58 @@ func TestUniqueSliceToSet(t *testing.T) { } } +func TestUniqueSliceToMap(t *testing.T) { + type testStruct struct { + Id uint32 + } + + tests := map[string]struct { + input []testStruct + expected map[uint32]testStruct + panicWith string + }{ + "Empty": { + input: []testStruct{}, + expected: map[uint32]testStruct{}, + }, + "Basic": { + input: []testStruct{ + {Id: 0}, {Id: 1}, {Id: 2}, + }, + expected: map[uint32]testStruct{ + 0: {Id: 0}, + 1: {Id: 1}, + 2: {Id: 2}, + }, + }, + "Duplicate": { + input: []testStruct{ + {Id: 0}, {Id: 0}, + }, + panicWith: "UniqueSliceToMap: duplicate value: {Id:0}", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + if tc.panicWith != "" { + require.PanicsWithValue( + t, + tc.panicWith, + func() { + lib.UniqueSliceToMap(tc.input, func(t testStruct) uint32 { return t.Id }) + }, + ) + } else { + require.Equal( + t, + tc.expected, + lib.UniqueSliceToMap(tc.input, func(t testStruct) uint32 { return t.Id }), + ) + } + }) + } +} + func TestMapSlice(t *testing.T) { // Can increment all numbers in a slice by 1, and change type to `uint64`. require.Equal( diff --git a/protocol/lib/metrics/constants.go b/protocol/lib/metrics/constants.go index 2b7c511198..ee3f61a02e 100644 --- a/protocol/lib/metrics/constants.go +++ b/protocol/lib/metrics/constants.go @@ -293,11 +293,14 @@ const ( // Liquidation Daemon. CheckCollateralizationForSubaccounts = "check_collateralization_for_subaccounts" + FetchApplicationStateAtBlockHeight = "fetch_application_state_at_block_height" GetAllSubaccounts = "get_all_subaccounts" GetLiquidatableSubaccountIds = "get_liquidatable_subaccount_ids" + GetSubaccountOpenPositionInfo = "get_subaccount_open_position_info" GetSubaccountsFromKey = "get_subaccounts_from_key" LiquidatableSubaccountIds = "liquidatable_subaccount_ids" LiquidationDaemon = "liquidation_daemon" + NegativeTncSubaccountIds = "negative_tnc_subaccount_ids" PageLimit = "page_limit" SendLiquidatableSubaccountIds = "send_liquidatable_subaccount_ids" SubaccountsWithOpenPositions = "subaccounts_with_open_positions" diff --git a/protocol/mocks/QueryClient.go b/protocol/mocks/QueryClient.go index 45a231ab3d..995b2ea62c 100644 --- a/protocol/mocks/QueryClient.go +++ b/protocol/mocks/QueryClient.go @@ -210,36 +210,6 @@ func (_m *QueryClient) AllPerpetuals(ctx context.Context, in *perpetualstypes.Qu return r0, r1 } -// AreSubaccountsLiquidatable provides a mock function with given fields: ctx, in, opts -func (_m *QueryClient) AreSubaccountsLiquidatable(ctx context.Context, in *clobtypes.AreSubaccountsLiquidatableRequest, opts ...grpc.CallOption) (*clobtypes.AreSubaccountsLiquidatableResponse, error) { - _va := make([]interface{}, len(opts)) - for _i := range opts { - _va[_i] = opts[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, in) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - var r0 *clobtypes.AreSubaccountsLiquidatableResponse - if rf, ok := ret.Get(0).(func(context.Context, *clobtypes.AreSubaccountsLiquidatableRequest, ...grpc.CallOption) *clobtypes.AreSubaccountsLiquidatableResponse); ok { - r0 = rf(ctx, in, opts...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*clobtypes.AreSubaccountsLiquidatableResponse) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *clobtypes.AreSubaccountsLiquidatableRequest, ...grpc.CallOption) error); ok { - r1 = rf(ctx, in, opts...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // BlockRateLimitConfiguration provides a mock function with given fields: ctx, in, opts func (_m *QueryClient) BlockRateLimitConfiguration(ctx context.Context, in *clobtypes.QueryBlockRateLimitConfigurationRequest, opts ...grpc.CallOption) (*clobtypes.QueryBlockRateLimitConfigurationResponse, error) { _va := make([]interface{}, len(opts)) diff --git a/protocol/testutil/constants/perpetuals.go b/protocol/testutil/constants/perpetuals.go index ccf11f1189..9f9739d64c 100644 --- a/protocol/testutil/constants/perpetuals.go +++ b/protocol/testutil/constants/perpetuals.go @@ -215,6 +215,7 @@ var ( DefaultFundingPpm: int32(0), LiquidityTier: uint32(3), }, + FundingIndex: dtypes.ZeroInt(), } BtcUsd_NoMarginRequirement = perptypes.Perpetual{ Params: perptypes.PerpetualParams{ diff --git a/protocol/testutil/constants/subaccounts.go b/protocol/testutil/constants/subaccounts.go index 28575e8c0d..832db4057d 100644 --- a/protocol/testutil/constants/subaccounts.go +++ b/protocol/testutil/constants/subaccounts.go @@ -141,8 +141,9 @@ var ( }, PerpetualPositions: []*satypes.PerpetualPosition{ { - PerpetualId: 0, - Quantums: dtypes.NewInt(-100_000_000), // -1 BTC + PerpetualId: 0, + Quantums: dtypes.NewInt(-100_000_000), // -1 BTC + FundingIndex: dtypes.NewInt(0), }, }, } @@ -171,8 +172,9 @@ var ( }, PerpetualPositions: []*satypes.PerpetualPosition{ { - PerpetualId: 0, - Quantums: dtypes.NewInt(-100_000_000), // -1 BTC + PerpetualId: 0, + Quantums: dtypes.NewInt(-100_000_000), // -1 BTC + FundingIndex: dtypes.NewInt(0), }, }, } @@ -328,6 +330,22 @@ var ( }, }, } + Dave_Num0_1BTC_Long_45000USD_Short = satypes.Subaccount{ + Id: &Dave_Num0, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(-45_000_000_000), // -$45,000 + }, + }, + PerpetualPositions: []*satypes.PerpetualPosition{ + { + PerpetualId: 0, + Quantums: dtypes.NewInt(100_000_000), // 1 BTC + FundingIndex: dtypes.NewInt(0), + }, + }, + } Dave_Num0_1BTC_Long_45001USD_Short = satypes.Subaccount{ Id: &Dave_Num0, AssetPositions: []*satypes.AssetPosition{ @@ -338,8 +356,9 @@ var ( }, PerpetualPositions: []*satypes.PerpetualPosition{ { - PerpetualId: 0, - Quantums: dtypes.NewInt(100_000_000), // 1 BTC + PerpetualId: 0, + Quantums: dtypes.NewInt(100_000_000), // 1 BTC + FundingIndex: dtypes.NewInt(0), }, }, } @@ -384,8 +403,9 @@ var ( }, PerpetualPositions: []*satypes.PerpetualPosition{ { - PerpetualId: 0, - Quantums: dtypes.NewInt(100_000_000), // 1 BTC + PerpetualId: 0, + Quantums: dtypes.NewInt(100_000_000), // 1 BTC + FundingIndex: dtypes.NewInt(0), }, }, } diff --git a/protocol/x/clob/flags/flags.go b/protocol/x/clob/flags/flags.go index 4564fa32b7..902d4eae63 100644 --- a/protocol/x/clob/flags/flags.go +++ b/protocol/x/clob/flags/flags.go @@ -119,13 +119,13 @@ func GetClobFlagValuesFromOptions( } if option := appOpts.Get(MevTelemetryHosts); option != nil { - if v, err := cast.ToStringE(option); err == nil { + if v, err := cast.ToStringE(option); err == nil && len(v) > 0 { result.MevTelemetryHosts = strings.Split(v, ",") } } if option := appOpts.Get(MevTelemetryIdentifier); option != nil { - if v, err := cast.ToStringE(option); err == nil { + if v, err := cast.ToStringE(option); err == nil && len(v) > 0 { result.MevTelemetryIdentifier = v } } diff --git a/protocol/x/clob/keeper/grpc_query_are_subaccounts_liquidatable.go b/protocol/x/clob/keeper/grpc_query_are_subaccounts_liquidatable.go deleted file mode 100644 index 7713c4dbe5..0000000000 --- a/protocol/x/clob/keeper/grpc_query_are_subaccounts_liquidatable.go +++ /dev/null @@ -1,40 +0,0 @@ -package keeper - -import ( - "context" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -func (k Keeper) AreSubaccountsLiquidatable( - c context.Context, - req *types.AreSubaccountsLiquidatableRequest, -) ( - *types.AreSubaccountsLiquidatableResponse, - error, -) { - if req == nil { - return nil, status.Error(codes.InvalidArgument, "invalid request") - } - - ctx := sdk.UnwrapSDKContext(c) - - results := make([]types.AreSubaccountsLiquidatableResponse_Result, len(req.SubaccountIds)) - for i, subaccountId := range req.SubaccountIds { - isLiquidatable, err := k.IsLiquidatable(ctx, subaccountId) - - if err != nil { - return nil, status.Error(codes.Internal, err.Error()) - } - results[i] = types.AreSubaccountsLiquidatableResponse_Result{ - SubaccountId: subaccountId, - IsLiquidatable: isLiquidatable, - } - } - return &types.AreSubaccountsLiquidatableResponse{ - Results: results, - }, nil -} diff --git a/protocol/x/clob/keeper/grpc_query_are_subaccounts_liquidatable_test.go b/protocol/x/clob/keeper/grpc_query_are_subaccounts_liquidatable_test.go deleted file mode 100644 index 1194fd7b30..0000000000 --- a/protocol/x/clob/keeper/grpc_query_are_subaccounts_liquidatable_test.go +++ /dev/null @@ -1,166 +0,0 @@ -package keeper_test - -import ( - "testing" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/require" - - "github.com/dydxprotocol/v4-chain/protocol/dtypes" - "github.com/dydxprotocol/v4-chain/protocol/mocks" - "github.com/dydxprotocol/v4-chain/protocol/testutil/constants" - keepertest "github.com/dydxprotocol/v4-chain/protocol/testutil/keeper" - "github.com/dydxprotocol/v4-chain/protocol/testutil/nullify" - "github.com/dydxprotocol/v4-chain/protocol/x/clob/memclob" - "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" - perptypes "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types" - satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" -) - -func TestAreSubaccountsLiquidatable(t *testing.T) { - for _, tc := range []struct { - desc string - subaccounts []satypes.Subaccount - perpetuals []perptypes.Perpetual - request *types.AreSubaccountsLiquidatableRequest - response *types.AreSubaccountsLiquidatableResponse - err error - }{ - { - desc: "No errors", - perpetuals: []perptypes.Perpetual{ - constants.BtcUsd_100PercentMarginRequirement, - }, - subaccounts: []satypes.Subaccount{ - { - Id: &constants.Alice_Num0, - AssetPositions: []*satypes.AssetPosition{ - &constants.Usdc_Asset_10_000, - }, - PerpetualPositions: []*satypes.PerpetualPosition{ - { - PerpetualId: 0, - Quantums: dtypes.NewInt(-1_000_000_000), // 1 BTC - }, - }, - }, - { - Id: &constants.Bob_Num0, - AssetPositions: []*satypes.AssetPosition{ - &constants.Usdc_Asset_10_000, - }, - PerpetualPositions: []*satypes.PerpetualPosition{ - { - PerpetualId: 0, - Quantums: dtypes.NewInt(100_000_000), // 1 BTC - }, - }, - }, - }, - request: &types.AreSubaccountsLiquidatableRequest{ - SubaccountIds: []satypes.SubaccountId{ - constants.Alice_Num0, - constants.Bob_Num0, - }, - }, - response: &types.AreSubaccountsLiquidatableResponse{ - Results: []types.AreSubaccountsLiquidatableResponse_Result{ - { - SubaccountId: constants.Alice_Num0, - IsLiquidatable: true, - }, - { - SubaccountId: constants.Bob_Num0, - IsLiquidatable: false, - }, - }, - }, - }, - { - desc: "Non-existent subaccount", - perpetuals: []perptypes.Perpetual{ - constants.BtcUsd_100PercentMarginRequirement, - }, - subaccounts: []satypes.Subaccount{}, - request: &types.AreSubaccountsLiquidatableRequest{ - SubaccountIds: []satypes.SubaccountId{ - constants.Alice_Num0, - }, - }, - response: &types.AreSubaccountsLiquidatableResponse{ - Results: []types.AreSubaccountsLiquidatableResponse_Result{ - { - SubaccountId: constants.Alice_Num0, - IsLiquidatable: false, - }, - }, - }, - }, - { - desc: "Errors are propagated", - subaccounts: []satypes.Subaccount{ - { - Id: &constants.Alice_Num0, - AssetPositions: []*satypes.AssetPosition{ - &constants.Usdc_Asset_10_000, - }, - PerpetualPositions: []*satypes.PerpetualPosition{ - { - PerpetualId: 0, - Quantums: dtypes.NewInt(-1_000_000_000), // 1 BTC - }, - }, - }, - }, - perpetuals: []perptypes.Perpetual{}, - request: &types.AreSubaccountsLiquidatableRequest{ - SubaccountIds: []satypes.SubaccountId{ - constants.Alice_Num0, - }, - }, - err: perptypes.ErrPerpetualDoesNotExist, - }, - } { - t.Run(tc.desc, func(t *testing.T) { - memClob := memclob.NewMemClobPriceTimePriority(false) - ks := keepertest.NewClobKeepersTestContext(t, memClob, &mocks.BankKeeper{}, &mocks.IndexerEventManager{}) - - // Create the default markets. - keepertest.CreateTestMarkets(t, ks.Ctx, ks.PricesKeeper) - - // Create liquidity tiers. - keepertest.CreateTestLiquidityTiers(t, ks.Ctx, ks.PerpetualsKeeper) - - // Create all perpetuals. - for _, p := range tc.perpetuals { - _, err := ks.PerpetualsKeeper.CreatePerpetual( - ks.Ctx, - p.Params.Id, - p.Params.Ticker, - p.Params.MarketId, - p.Params.AtomicResolution, - p.Params.DefaultFundingPpm, - p.Params.LiquidityTier, - ) - require.NoError(t, err) - } - - for _, subaccount := range tc.subaccounts { - ks.SubaccountsKeeper.SetSubaccount(ks.Ctx, subaccount) - } - - wctx := sdk.WrapSDKContext(ks.Ctx) - response, err := ks.ClobKeeper.AreSubaccountsLiquidatable(wctx, tc.request) - - if tc.err != nil { - require.ErrorContains(t, err, tc.err.Error()) - } else { - require.NoError(t, err) - require.Equal(t, - nullify.Fill(tc.response), //nolint:staticcheck - nullify.Fill(response), //nolint:staticcheck - ) - } - }) - } -} diff --git a/protocol/x/clob/keeper/liquidations.go b/protocol/x/clob/keeper/liquidations.go index 8017370cfa..6c2136c0fb 100644 --- a/protocol/x/clob/keeper/liquidations.go +++ b/protocol/x/clob/keeper/liquidations.go @@ -360,11 +360,22 @@ func (k Keeper) IsLiquidatable( return false, err } - // The subaccount is liquidatable if both of the following are true: - // - The maintenance margin requirements are greater than zero (note that they can never be negative). - // - The maintenance margin requirements are greater than the subaccount's net collateral. - isLiquidatable := bigMaintenanceMargin.Sign() > 0 && bigMaintenanceMargin.Cmp(bigNetCollateral) == 1 - return isLiquidatable, nil + return CanLiquidateSubaccount(bigNetCollateral, bigMaintenanceMargin), nil +} + +// CanLiquidateSubaccount returns true if a subaccount is liquidatable given its total net collateral and +// maintenance margin requirement. +// +// The subaccount is liquidatable if both of the following are true: +// - The maintenance margin requirements are greater than zero (note that they can never be negative). +// - The maintenance margin requirements are greater than the subaccount's net collateral. +// +// Note that this is a stateless function. +func CanLiquidateSubaccount( + bigNetCollateral *big.Int, + bigMaintenanceMargin *big.Int, +) bool { + return bigMaintenanceMargin.Sign() > 0 && bigMaintenanceMargin.Cmp(bigNetCollateral) == 1 } // EnsureIsLiquidatable returns an error if the subaccount is not liquidatable. diff --git a/protocol/x/clob/types/query.pb.go b/protocol/x/clob/types/query.pb.go index 40751a9ac1..78f490f800 100644 --- a/protocol/x/clob/types/query.pb.go +++ b/protocol/x/clob/types/query.pb.go @@ -11,7 +11,6 @@ import ( _ "github.com/cosmos/gogoproto/gogoproto" grpc1 "github.com/cosmos/gogoproto/grpc" proto "github.com/cosmos/gogoproto/proto" - types "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" _ "google.golang.org/genproto/googleapis/api/annotations" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" @@ -220,156 +219,6 @@ func (m *QueryClobPairAllResponse) GetPagination() *query.PageResponse { return nil } -// AreSubaccountsLiquidatableRequest is a request message used to check whether -// the given subaccounts are liquidatable. -// The subaccount ids should not contain duplicates. -type AreSubaccountsLiquidatableRequest struct { - SubaccountIds []types.SubaccountId `protobuf:"bytes,1,rep,name=subaccount_ids,json=subaccountIds,proto3" json:"subaccount_ids"` -} - -func (m *AreSubaccountsLiquidatableRequest) Reset() { *m = AreSubaccountsLiquidatableRequest{} } -func (m *AreSubaccountsLiquidatableRequest) String() string { return proto.CompactTextString(m) } -func (*AreSubaccountsLiquidatableRequest) ProtoMessage() {} -func (*AreSubaccountsLiquidatableRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_3365c195b25c5bc0, []int{4} -} -func (m *AreSubaccountsLiquidatableRequest) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *AreSubaccountsLiquidatableRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - if deterministic { - return xxx_messageInfo_AreSubaccountsLiquidatableRequest.Marshal(b, m, deterministic) - } else { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil - } -} -func (m *AreSubaccountsLiquidatableRequest) XXX_Merge(src proto.Message) { - xxx_messageInfo_AreSubaccountsLiquidatableRequest.Merge(m, src) -} -func (m *AreSubaccountsLiquidatableRequest) XXX_Size() int { - return m.Size() -} -func (m *AreSubaccountsLiquidatableRequest) XXX_DiscardUnknown() { - xxx_messageInfo_AreSubaccountsLiquidatableRequest.DiscardUnknown(m) -} - -var xxx_messageInfo_AreSubaccountsLiquidatableRequest proto.InternalMessageInfo - -func (m *AreSubaccountsLiquidatableRequest) GetSubaccountIds() []types.SubaccountId { - if m != nil { - return m.SubaccountIds - } - return nil -} - -// AreSubaccountsLiquidatableResponse is a response message that contains the -// liquidation status for each subaccount. -type AreSubaccountsLiquidatableResponse struct { - Results []AreSubaccountsLiquidatableResponse_Result `protobuf:"bytes,1,rep,name=results,proto3" json:"results"` -} - -func (m *AreSubaccountsLiquidatableResponse) Reset() { *m = AreSubaccountsLiquidatableResponse{} } -func (m *AreSubaccountsLiquidatableResponse) String() string { return proto.CompactTextString(m) } -func (*AreSubaccountsLiquidatableResponse) ProtoMessage() {} -func (*AreSubaccountsLiquidatableResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_3365c195b25c5bc0, []int{5} -} -func (m *AreSubaccountsLiquidatableResponse) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *AreSubaccountsLiquidatableResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - if deterministic { - return xxx_messageInfo_AreSubaccountsLiquidatableResponse.Marshal(b, m, deterministic) - } else { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil - } -} -func (m *AreSubaccountsLiquidatableResponse) XXX_Merge(src proto.Message) { - xxx_messageInfo_AreSubaccountsLiquidatableResponse.Merge(m, src) -} -func (m *AreSubaccountsLiquidatableResponse) XXX_Size() int { - return m.Size() -} -func (m *AreSubaccountsLiquidatableResponse) XXX_DiscardUnknown() { - xxx_messageInfo_AreSubaccountsLiquidatableResponse.DiscardUnknown(m) -} - -var xxx_messageInfo_AreSubaccountsLiquidatableResponse proto.InternalMessageInfo - -func (m *AreSubaccountsLiquidatableResponse) GetResults() []AreSubaccountsLiquidatableResponse_Result { - if m != nil { - return m.Results - } - return nil -} - -// Result returns whether a subaccount should be liquidated. -type AreSubaccountsLiquidatableResponse_Result struct { - SubaccountId types.SubaccountId `protobuf:"bytes,1,opt,name=subaccount_id,json=subaccountId,proto3" json:"subaccount_id"` - IsLiquidatable bool `protobuf:"varint,2,opt,name=is_liquidatable,json=isLiquidatable,proto3" json:"is_liquidatable,omitempty"` -} - -func (m *AreSubaccountsLiquidatableResponse_Result) Reset() { - *m = AreSubaccountsLiquidatableResponse_Result{} -} -func (m *AreSubaccountsLiquidatableResponse_Result) String() string { - return proto.CompactTextString(m) -} -func (*AreSubaccountsLiquidatableResponse_Result) ProtoMessage() {} -func (*AreSubaccountsLiquidatableResponse_Result) Descriptor() ([]byte, []int) { - return fileDescriptor_3365c195b25c5bc0, []int{5, 0} -} -func (m *AreSubaccountsLiquidatableResponse_Result) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *AreSubaccountsLiquidatableResponse_Result) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - if deterministic { - return xxx_messageInfo_AreSubaccountsLiquidatableResponse_Result.Marshal(b, m, deterministic) - } else { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil - } -} -func (m *AreSubaccountsLiquidatableResponse_Result) XXX_Merge(src proto.Message) { - xxx_messageInfo_AreSubaccountsLiquidatableResponse_Result.Merge(m, src) -} -func (m *AreSubaccountsLiquidatableResponse_Result) XXX_Size() int { - return m.Size() -} -func (m *AreSubaccountsLiquidatableResponse_Result) XXX_DiscardUnknown() { - xxx_messageInfo_AreSubaccountsLiquidatableResponse_Result.DiscardUnknown(m) -} - -var xxx_messageInfo_AreSubaccountsLiquidatableResponse_Result proto.InternalMessageInfo - -func (m *AreSubaccountsLiquidatableResponse_Result) GetSubaccountId() types.SubaccountId { - if m != nil { - return m.SubaccountId - } - return types.SubaccountId{} -} - -func (m *AreSubaccountsLiquidatableResponse_Result) GetIsLiquidatable() bool { - if m != nil { - return m.IsLiquidatable - } - return false -} - // MevNodeToNodeCalculationRequest is a request message used to run the // MEV node <> node calculation. type MevNodeToNodeCalculationRequest struct { @@ -386,7 +235,7 @@ func (m *MevNodeToNodeCalculationRequest) Reset() { *m = MevNodeToNodeCa func (m *MevNodeToNodeCalculationRequest) String() string { return proto.CompactTextString(m) } func (*MevNodeToNodeCalculationRequest) ProtoMessage() {} func (*MevNodeToNodeCalculationRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_3365c195b25c5bc0, []int{6} + return fileDescriptor_3365c195b25c5bc0, []int{4} } func (m *MevNodeToNodeCalculationRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -439,7 +288,7 @@ func (m *MevNodeToNodeCalculationResponse) Reset() { *m = MevNodeToNodeC func (m *MevNodeToNodeCalculationResponse) String() string { return proto.CompactTextString(m) } func (*MevNodeToNodeCalculationResponse) ProtoMessage() {} func (*MevNodeToNodeCalculationResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_3365c195b25c5bc0, []int{7} + return fileDescriptor_3365c195b25c5bc0, []int{5} } func (m *MevNodeToNodeCalculationResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -490,7 +339,7 @@ func (m *MevNodeToNodeCalculationResponse_MevAndVolumePerClob) String() string { } func (*MevNodeToNodeCalculationResponse_MevAndVolumePerClob) ProtoMessage() {} func (*MevNodeToNodeCalculationResponse_MevAndVolumePerClob) Descriptor() ([]byte, []int) { - return fileDescriptor_3365c195b25c5bc0, []int{7, 0} + return fileDescriptor_3365c195b25c5bc0, []int{5, 0} } func (m *MevNodeToNodeCalculationResponse_MevAndVolumePerClob) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -551,7 +400,7 @@ func (m *QueryEquityTierLimitConfigurationRequest) Reset() { func (m *QueryEquityTierLimitConfigurationRequest) String() string { return proto.CompactTextString(m) } func (*QueryEquityTierLimitConfigurationRequest) ProtoMessage() {} func (*QueryEquityTierLimitConfigurationRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_3365c195b25c5bc0, []int{8} + return fileDescriptor_3365c195b25c5bc0, []int{6} } func (m *QueryEquityTierLimitConfigurationRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -594,7 +443,7 @@ func (m *QueryEquityTierLimitConfigurationResponse) String() string { } func (*QueryEquityTierLimitConfigurationResponse) ProtoMessage() {} func (*QueryEquityTierLimitConfigurationResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_3365c195b25c5bc0, []int{9} + return fileDescriptor_3365c195b25c5bc0, []int{7} } func (m *QueryEquityTierLimitConfigurationResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -641,7 +490,7 @@ func (m *QueryBlockRateLimitConfigurationRequest) Reset() { func (m *QueryBlockRateLimitConfigurationRequest) String() string { return proto.CompactTextString(m) } func (*QueryBlockRateLimitConfigurationRequest) ProtoMessage() {} func (*QueryBlockRateLimitConfigurationRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_3365c195b25c5bc0, []int{10} + return fileDescriptor_3365c195b25c5bc0, []int{8} } func (m *QueryBlockRateLimitConfigurationRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -682,7 +531,7 @@ func (m *QueryBlockRateLimitConfigurationResponse) Reset() { func (m *QueryBlockRateLimitConfigurationResponse) String() string { return proto.CompactTextString(m) } func (*QueryBlockRateLimitConfigurationResponse) ProtoMessage() {} func (*QueryBlockRateLimitConfigurationResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_3365c195b25c5bc0, []int{11} + return fileDescriptor_3365c195b25c5bc0, []int{9} } func (m *QueryBlockRateLimitConfigurationResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -727,7 +576,7 @@ func (m *QueryLiquidationsConfigurationRequest) Reset() { *m = QueryLiqu func (m *QueryLiquidationsConfigurationRequest) String() string { return proto.CompactTextString(m) } func (*QueryLiquidationsConfigurationRequest) ProtoMessage() {} func (*QueryLiquidationsConfigurationRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_3365c195b25c5bc0, []int{12} + return fileDescriptor_3365c195b25c5bc0, []int{10} } func (m *QueryLiquidationsConfigurationRequest) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -768,7 +617,7 @@ func (m *QueryLiquidationsConfigurationResponse) Reset() { func (m *QueryLiquidationsConfigurationResponse) String() string { return proto.CompactTextString(m) } func (*QueryLiquidationsConfigurationResponse) ProtoMessage() {} func (*QueryLiquidationsConfigurationResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_3365c195b25c5bc0, []int{13} + return fileDescriptor_3365c195b25c5bc0, []int{11} } func (m *QueryLiquidationsConfigurationResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -809,9 +658,6 @@ func init() { proto.RegisterType((*QueryClobPairResponse)(nil), "dydxprotocol.clob.QueryClobPairResponse") proto.RegisterType((*QueryAllClobPairRequest)(nil), "dydxprotocol.clob.QueryAllClobPairRequest") proto.RegisterType((*QueryClobPairAllResponse)(nil), "dydxprotocol.clob.QueryClobPairAllResponse") - proto.RegisterType((*AreSubaccountsLiquidatableRequest)(nil), "dydxprotocol.clob.AreSubaccountsLiquidatableRequest") - proto.RegisterType((*AreSubaccountsLiquidatableResponse)(nil), "dydxprotocol.clob.AreSubaccountsLiquidatableResponse") - proto.RegisterType((*AreSubaccountsLiquidatableResponse_Result)(nil), "dydxprotocol.clob.AreSubaccountsLiquidatableResponse.Result") proto.RegisterType((*MevNodeToNodeCalculationRequest)(nil), "dydxprotocol.clob.MevNodeToNodeCalculationRequest") proto.RegisterType((*MevNodeToNodeCalculationResponse)(nil), "dydxprotocol.clob.MevNodeToNodeCalculationResponse") proto.RegisterType((*MevNodeToNodeCalculationResponse_MevAndVolumePerClob)(nil), "dydxprotocol.clob.MevNodeToNodeCalculationResponse.MevAndVolumePerClob") @@ -826,75 +672,67 @@ func init() { func init() { proto.RegisterFile("dydxprotocol/clob/query.proto", fileDescriptor_3365c195b25c5bc0) } var fileDescriptor_3365c195b25c5bc0 = []byte{ - // 1084 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x57, 0xcf, 0x6f, 0x1b, 0xc5, - 0x1f, 0xcd, 0x38, 0xfd, 0xe6, 0x9b, 0x7e, 0xda, 0x04, 0x98, 0x34, 0x6d, 0x70, 0x52, 0xc7, 0x59, - 0x88, 0x63, 0xa7, 0x62, 0x97, 0xa6, 0x01, 0x41, 0x5a, 0x21, 0x25, 0x11, 0x44, 0x95, 0x12, 0xe4, - 0x6e, 0xab, 0x22, 0x41, 0xa5, 0xd5, 0x7a, 0x77, 0x70, 0x46, 0x5d, 0xef, 0x38, 0xfb, 0x4b, 0x89, - 0x10, 0x17, 0x84, 0x90, 0x2a, 0x38, 0x20, 0x71, 0xe0, 0xc0, 0x91, 0xbf, 0x82, 0x03, 0x02, 0x6e, - 0x3d, 0x56, 0xe2, 0xc2, 0x01, 0x21, 0x94, 0x70, 0xe6, 0x6f, 0x40, 0x3b, 0x3b, 0xb6, 0x77, 0xbd, - 0x3f, 0x9c, 0xf8, 0xe2, 0xd8, 0x33, 0x6f, 0xde, 0xbc, 0x37, 0x6f, 0xf6, 0xf3, 0xd9, 0xc0, 0x4d, - 0xf3, 0xc4, 0x3c, 0xee, 0x3a, 0xcc, 0x63, 0x06, 0xb3, 0x14, 0xc3, 0x62, 0x2d, 0xe5, 0xc8, 0x27, - 0xce, 0x89, 0xcc, 0xc7, 0xf0, 0x2b, 0xf1, 0x69, 0x39, 0x9c, 0x2e, 0x5f, 0x6b, 0xb3, 0x36, 0xe3, - 0x43, 0x4a, 0xf8, 0x2d, 0x02, 0x96, 0x97, 0xda, 0x8c, 0xb5, 0x2d, 0xa2, 0xe8, 0x5d, 0xaa, 0xe8, - 0xb6, 0xcd, 0x3c, 0xdd, 0xa3, 0xcc, 0x76, 0xc5, 0xec, 0xba, 0xc1, 0xdc, 0x0e, 0x73, 0x95, 0x96, - 0xee, 0x92, 0x88, 0x5f, 0x09, 0x6e, 0xb7, 0x88, 0xa7, 0xdf, 0x56, 0xba, 0x7a, 0x9b, 0xda, 0x1c, - 0x2c, 0xb0, 0x4a, 0x5a, 0x51, 0xcb, 0x62, 0xc6, 0x53, 0xcd, 0xd1, 0x3d, 0xa2, 0x59, 0xb4, 0x43, - 0x3d, 0xcd, 0x60, 0xf6, 0xa7, 0xb4, 0x2d, 0x16, 0xac, 0xa4, 0x17, 0x84, 0x1f, 0x5a, 0x57, 0xa7, - 0x8e, 0x80, 0xbc, 0x99, 0x86, 0x90, 0x23, 0x9f, 0x7a, 0x27, 0x9a, 0x47, 0x89, 0x93, 0x45, 0x7a, - 0x2b, 0xbd, 0xc2, 0xa2, 0x47, 0x3e, 0x35, 0x23, 0x5f, 0x49, 0xf0, 0x62, 0x1a, 0xdc, 0x21, 0x81, - 0x98, 0x6c, 0x24, 0x26, 0x5d, 0xbf, 0xa5, 0x1b, 0x06, 0xf3, 0x6d, 0xcf, 0x8d, 0x7d, 0x8f, 0xa0, - 0x52, 0x03, 0x6e, 0x3c, 0x08, 0x0f, 0x67, 0x8f, 0x78, 0xbb, 0x16, 0x6b, 0x35, 0x75, 0xea, 0xa8, - 0xe4, 0xc8, 0x27, 0xae, 0x87, 0x67, 0xa1, 0x44, 0xcd, 0x05, 0x54, 0x45, 0xf5, 0x19, 0xb5, 0x44, - 0x4d, 0xe9, 0x23, 0x98, 0xe7, 0xd0, 0x01, 0xce, 0xed, 0x32, 0xdb, 0x25, 0xf8, 0x3d, 0xb8, 0xdc, - 0x77, 0xcf, 0xf1, 0x57, 0x36, 0x16, 0xe5, 0x54, 0x8a, 0x72, 0x6f, 0xdd, 0xce, 0xa5, 0xe7, 0x7f, - 0x2d, 0x4f, 0xa8, 0xd3, 0x86, 0xf8, 0x2d, 0xe9, 0x42, 0xc3, 0xb6, 0x65, 0x0d, 0x6b, 0xf8, 0x00, - 0x60, 0x90, 0x96, 0xe0, 0xae, 0xc9, 0x51, 0xb4, 0x72, 0x18, 0xad, 0x1c, 0x5d, 0x1d, 0x11, 0xad, - 0xdc, 0xd4, 0xdb, 0x44, 0xac, 0x55, 0x63, 0x2b, 0xa5, 0x1f, 0x11, 0x2c, 0x24, 0xc4, 0x6f, 0x5b, - 0x56, 0x9e, 0xfe, 0xc9, 0x0b, 0xea, 0xc7, 0x7b, 0x09, 0x91, 0x25, 0x2e, 0x72, 0x6d, 0xa4, 0xc8, - 0x68, 0xf3, 0x84, 0xca, 0x63, 0x58, 0xd9, 0x76, 0xc8, 0xc3, 0x41, 0x5e, 0xfb, 0x22, 0x7f, 0xbd, - 0x65, 0xf5, 0x6c, 0xe1, 0x87, 0x30, 0x3b, 0x48, 0x51, 0xa3, 0xa6, 0x2b, 0x24, 0xd7, 0x92, 0x92, - 0x63, 0xa9, 0xcb, 0x03, 0xc6, 0xfb, 0xa6, 0x50, 0x3f, 0xe3, 0xc6, 0xc6, 0x5c, 0xe9, 0x59, 0x09, - 0xa4, 0xa2, 0xad, 0xc5, 0x49, 0x3d, 0x81, 0xff, 0x3b, 0xc4, 0xf5, 0x2d, 0xaf, 0xb7, 0xe9, 0xbd, - 0x8c, 0x73, 0x1a, 0xcd, 0x23, 0xab, 0x9c, 0x44, 0x48, 0xe9, 0x51, 0x96, 0xbf, 0x44, 0x30, 0x15, - 0xcd, 0xe0, 0x07, 0x30, 0x93, 0x30, 0xd9, 0x8f, 0xfe, 0x22, 0x1e, 0xaf, 0xc6, 0x3d, 0xe2, 0x35, - 0x78, 0x89, 0xba, 0x9a, 0x15, 0x93, 0xc3, 0xa3, 0x9a, 0x56, 0x67, 0x69, 0x42, 0xa4, 0xf4, 0x27, - 0x82, 0xe5, 0x03, 0x12, 0x7c, 0xc8, 0x4c, 0xf2, 0x88, 0x85, 0x9f, 0xbb, 0xba, 0x65, 0xf8, 0x16, - 0x8f, 0xa8, 0x17, 0xc2, 0x13, 0xb8, 0x1e, 0x55, 0x88, 0xae, 0xc3, 0xba, 0xcc, 0x25, 0x8e, 0xd6, - 0xd1, 0x3d, 0xe3, 0x90, 0xb8, 0xd9, 0x42, 0xf9, 0xb9, 0x3c, 0xd6, 0xad, 0x70, 0x0f, 0xe6, 0x1c, - 0x90, 0xe0, 0x20, 0x42, 0xab, 0xd7, 0x38, 0x4b, 0x53, 0x90, 0x88, 0x51, 0xfc, 0x09, 0xcc, 0x07, - 0x3d, 0xb0, 0xd6, 0x21, 0x81, 0xd6, 0x21, 0x9e, 0x43, 0x0d, 0xb7, 0x7f, 0xb7, 0xd2, 0xe4, 0x09, - 0xc1, 0x07, 0x11, 0x5c, 0x9d, 0x0b, 0xe2, 0x5b, 0x46, 0x83, 0xd2, 0xbf, 0x08, 0xaa, 0xf9, 0xf6, - 0x44, 0xd0, 0xed, 0xe1, 0xa0, 0xf7, 0x46, 0xed, 0x99, 0xc1, 0x12, 0x02, 0xb6, 0x6d, 0xf3, 0x31, - 0xb3, 0xfc, 0x0e, 0x69, 0x12, 0x27, 0x7c, 0x80, 0x86, 0x33, 0xd7, 0x61, 0x2e, 0x03, 0x85, 0xab, - 0x70, 0xb5, 0xff, 0x48, 0x6a, 0xfd, 0x2a, 0x04, 0xbd, 0x47, 0xee, 0xbe, 0x89, 0x5f, 0x86, 0xc9, - 0x0e, 0x09, 0xf8, 0x89, 0x94, 0xd4, 0xf0, 0x2b, 0xbe, 0x0e, 0x53, 0x01, 0x27, 0x59, 0x98, 0xac, - 0xa2, 0xfa, 0x25, 0x55, 0xfc, 0x92, 0xd6, 0xa1, 0xce, 0x1f, 0xfd, 0xf7, 0x79, 0xf9, 0x7d, 0x44, - 0x89, 0xb3, 0x1f, 0x16, 0xdf, 0x5d, 0x5e, 0x4e, 0x7d, 0x27, 0x9e, 0xab, 0xf4, 0x03, 0x82, 0xc6, - 0x39, 0xc0, 0xe2, 0x94, 0x6c, 0x58, 0xc8, 0xab, 0xe9, 0xe2, 0x1e, 0x28, 0x19, 0xc7, 0x56, 0x44, - 0x2d, 0x8e, 0x67, 0x9e, 0x64, 0x61, 0xa4, 0x06, 0xac, 0x71, 0x71, 0x3b, 0xe1, 0xa5, 0x51, 0x75, - 0x8f, 0xe4, 0x1b, 0xf9, 0x1e, 0x09, 0xd7, 0x85, 0x58, 0xe1, 0xe3, 0x29, 0xdc, 0xc8, 0xe9, 0x77, - 0xc2, 0x86, 0x9c, 0x61, 0xa3, 0x80, 0x58, 0xb8, 0x88, 0x2e, 0xf7, 0x10, 0x44, 0x5a, 0x83, 0x55, - 0x2e, 0x6c, 0x3f, 0xd6, 0xdb, 0x32, 0x2d, 0x7c, 0x85, 0xa0, 0x36, 0x0a, 0xd9, 0xaf, 0x4b, 0x73, - 0x19, 0xad, 0x52, 0x88, 0x5f, 0xcd, 0x10, 0x9f, 0xa6, 0x14, 0x9a, 0xb1, 0x95, 0x9a, 0xd9, 0xf8, - 0xe9, 0x32, 0xfc, 0x8f, 0x0b, 0xc1, 0x5f, 0x23, 0x98, 0xee, 0xb5, 0x01, 0xbc, 0x9e, 0xc1, 0x9b, - 0xd3, 0x4b, 0xcb, 0xf5, 0x3c, 0xec, 0x70, 0x33, 0x95, 0x1a, 0x5f, 0xfc, 0xfe, 0xcf, 0x77, 0xa5, - 0xd7, 0xf0, 0x8a, 0x52, 0xf0, 0x8e, 0xa1, 0x7c, 0x46, 0xcd, 0xcf, 0xf1, 0x37, 0x08, 0xae, 0xc4, - 0xfa, 0x59, 0xbe, 0xa0, 0x74, 0x63, 0x2d, 0xdf, 0x1a, 0x25, 0x28, 0xd6, 0x20, 0xa5, 0xd7, 0xb9, - 0xa6, 0x0a, 0x5e, 0x2a, 0xd2, 0x84, 0x9f, 0x21, 0x28, 0xe7, 0xd7, 0x7e, 0xbc, 0x79, 0xc1, 0x56, - 0x11, 0xe9, 0x7c, 0x6b, 0xac, 0x06, 0x83, 0x7f, 0x41, 0xb0, 0x90, 0x57, 0x9e, 0xf0, 0xc6, 0x85, - 0x6a, 0x59, 0xa4, 0xe3, 0xce, 0x18, 0xf5, 0x4f, 0xda, 0xe2, 0xe7, 0xb6, 0xb9, 0x85, 0xd6, 0x25, - 0x45, 0xc9, 0x7c, 0x61, 0xd3, 0x6c, 0x66, 0x12, 0xcd, 0x63, 0xd1, 0x5f, 0x23, 0x26, 0xf2, 0x37, - 0x04, 0x4b, 0x45, 0x95, 0x02, 0xdf, 0xcd, 0x4b, 0xf0, 0x1c, 0x75, 0xae, 0x7c, 0x6f, 0xbc, 0xc5, - 0xc2, 0x57, 0x8d, 0xfb, 0xaa, 0xe2, 0x8a, 0x52, 0xf8, 0x92, 0x8b, 0x7f, 0x46, 0xb0, 0x58, 0x50, - 0x26, 0xf0, 0x56, 0x9e, 0x8a, 0xd1, 0x05, 0xae, 0x7c, 0x77, 0xac, 0xb5, 0xc2, 0xc0, 0x2a, 0x37, - 0xb0, 0x8c, 0x6f, 0x16, 0xbe, 0xf9, 0xe3, 0x5f, 0x11, 0xbc, 0x9a, 0x5b, 0x7c, 0xf0, 0x3b, 0x79, - 0x0a, 0x46, 0x55, 0xb6, 0xf2, 0xbb, 0x63, 0xac, 0x14, 0xca, 0x65, 0xae, 0xbc, 0x8e, 0x6b, 0xca, - 0xb9, 0xfe, 0x5b, 0xd8, 0x69, 0x3e, 0x3f, 0xad, 0xa0, 0x17, 0xa7, 0x15, 0xf4, 0xf7, 0x69, 0x05, - 0x7d, 0x7b, 0x56, 0x99, 0x78, 0x71, 0x56, 0x99, 0xf8, 0xe3, 0xac, 0x32, 0xf1, 0xf1, 0xdb, 0x6d, - 0xea, 0x1d, 0xfa, 0x2d, 0xd9, 0x60, 0x9d, 0x24, 0x57, 0xb0, 0xf9, 0x86, 0x71, 0xa8, 0x53, 0x5b, - 0xe9, 0x8f, 0x1c, 0x47, 0xfc, 0xde, 0x49, 0x97, 0xb8, 0xad, 0x29, 0x3e, 0x7c, 0xe7, 0xbf, 0x00, - 0x00, 0x00, 0xff, 0xff, 0xd2, 0x5a, 0xb5, 0xd4, 0xc7, 0x0d, 0x00, 0x00, + // 946 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x56, 0xcf, 0x6f, 0xdc, 0x44, + 0x14, 0xce, 0x6c, 0x4a, 0x09, 0x53, 0x40, 0x30, 0x69, 0xda, 0x65, 0x93, 0x6e, 0xb6, 0x86, 0x24, + 0x9b, 0x54, 0x78, 0x68, 0x5a, 0x21, 0x48, 0x11, 0x52, 0x12, 0x41, 0x84, 0xd4, 0xa0, 0xc5, 0xaa, + 0x8a, 0x04, 0x95, 0xac, 0x59, 0x7b, 0x70, 0x46, 0x1d, 0x7b, 0x36, 0xf6, 0xd8, 0x6a, 0x84, 0xb8, + 0x70, 0xe0, 0x02, 0x07, 0x24, 0x0e, 0x1c, 0x38, 0x72, 0xe6, 0x4f, 0x40, 0xc0, 0xad, 0xc7, 0x4a, + 0x5c, 0x38, 0x20, 0x84, 0x12, 0xce, 0xfc, 0x0d, 0x95, 0xc7, 0xb3, 0x5b, 0x6f, 0xfc, 0x63, 0x93, + 0xbd, 0xec, 0xda, 0x33, 0xdf, 0x7b, 0xfe, 0xbe, 0xf7, 0xde, 0x7c, 0x36, 0xbc, 0xe6, 0x1e, 0xb9, + 0x8f, 0x06, 0xa1, 0x90, 0xc2, 0x11, 0x1c, 0x3b, 0x5c, 0xf4, 0xf1, 0x61, 0x4c, 0xc3, 0x23, 0x53, + 0xad, 0xa1, 0x57, 0xf3, 0xdb, 0x66, 0xba, 0xdd, 0xba, 0xec, 0x09, 0x4f, 0xa8, 0x25, 0x9c, 0x5e, + 0x65, 0xc0, 0xd6, 0x92, 0x27, 0x84, 0xc7, 0x29, 0x26, 0x03, 0x86, 0x49, 0x10, 0x08, 0x49, 0x24, + 0x13, 0x41, 0xa4, 0x77, 0x37, 0x1c, 0x11, 0xf9, 0x22, 0xc2, 0x7d, 0x12, 0xd1, 0x2c, 0x3f, 0x4e, + 0x6e, 0xf6, 0xa9, 0x24, 0x37, 0xf1, 0x80, 0x78, 0x2c, 0x50, 0x60, 0x8d, 0xc5, 0x45, 0x46, 0x7d, + 0x2e, 0x9c, 0x87, 0x76, 0x48, 0x24, 0xb5, 0x39, 0xf3, 0x99, 0xb4, 0x1d, 0x11, 0x7c, 0xc1, 0x3c, + 0x1d, 0x70, 0xbd, 0x18, 0x90, 0xfe, 0xd8, 0x03, 0xc2, 0x42, 0x0d, 0x79, 0xab, 0x08, 0xa1, 0x87, + 0x31, 0x93, 0x47, 0xb6, 0x64, 0x34, 0x2c, 0x4b, 0x7a, 0xa3, 0x18, 0xc1, 0xd9, 0x61, 0xcc, 0xdc, + 0x4c, 0xd7, 0x38, 0x78, 0xb1, 0x08, 0xf6, 0x69, 0x92, 0x6d, 0x1a, 0xeb, 0xf0, 0xea, 0x27, 0xa9, + 0xe2, 0x3d, 0x2a, 0x77, 0xb9, 0xe8, 0xf7, 0x08, 0x0b, 0x2d, 0x7a, 0x18, 0xd3, 0x48, 0xa2, 0x97, + 0x61, 0x83, 0xb9, 0x4d, 0xd0, 0x01, 0xdd, 0x97, 0xac, 0x06, 0x73, 0x8d, 0x4f, 0xe1, 0x82, 0x82, + 0x3e, 0xc3, 0x45, 0x03, 0x11, 0x44, 0x14, 0xbd, 0x0f, 0x5f, 0x18, 0x49, 0x52, 0xf8, 0x4b, 0x9b, + 0x8b, 0x66, 0xa1, 0x35, 0xe6, 0x30, 0x6e, 0xe7, 0xc2, 0xe3, 0x7f, 0x96, 0x67, 0xac, 0x39, 0x47, + 0xdf, 0x1b, 0x44, 0x73, 0xd8, 0xe6, 0xfc, 0x34, 0x87, 0x0f, 0x21, 0x7c, 0xd6, 0x02, 0x9d, 0x7b, + 0xd5, 0xcc, 0xfa, 0x65, 0xa6, 0xfd, 0x32, 0xb3, 0x79, 0xd0, 0xfd, 0x32, 0x7b, 0xc4, 0xa3, 0x3a, + 0xd6, 0xca, 0x45, 0x1a, 0x3f, 0x03, 0xd8, 0x1c, 0x23, 0xbf, 0xcd, 0x79, 0x15, 0xff, 0xd9, 0x73, + 0xf2, 0x47, 0x7b, 0x63, 0x24, 0x1b, 0x8a, 0xe4, 0xda, 0x44, 0x92, 0xd9, 0xc3, 0xc7, 0x58, 0xfe, + 0x0d, 0xe0, 0xf2, 0x3e, 0x4d, 0x3e, 0x16, 0x2e, 0xbd, 0x27, 0xd2, 0xdf, 0x5d, 0xc2, 0x9d, 0x98, + 0xab, 0xcd, 0x61, 0x45, 0x1e, 0xc0, 0x2b, 0xd9, 0xc0, 0x0d, 0x42, 0x31, 0x10, 0x11, 0x0d, 0x6d, + 0x9f, 0x48, 0xe7, 0x80, 0x46, 0xa3, 0xea, 0x14, 0x99, 0xdf, 0x27, 0x3c, 0x1d, 0x0d, 0x11, 0xee, + 0xd3, 0x64, 0x3f, 0x43, 0x5b, 0x97, 0x55, 0x96, 0x9e, 0x4e, 0xa2, 0x57, 0xd1, 0xe7, 0x70, 0x21, + 0x19, 0x82, 0x6d, 0x9f, 0x26, 0xb6, 0x4f, 0x65, 0xc8, 0x9c, 0x68, 0xa4, 0xaa, 0x98, 0x7c, 0x8c, + 0xf0, 0x7e, 0x06, 0xb7, 0xe6, 0x93, 0xfc, 0x23, 0xb3, 0x45, 0xe3, 0x7f, 0x00, 0x3b, 0xd5, 0xf2, + 0x74, 0x33, 0x3c, 0xf8, 0x7c, 0x48, 0xa3, 0x98, 0xcb, 0x48, 0xb7, 0x62, 0x6f, 0xd2, 0x33, 0x4b, + 0xb2, 0xa4, 0x80, 0xed, 0xc0, 0xbd, 0x2f, 0x78, 0xec, 0xd3, 0x1e, 0x0d, 0xd3, 0xd6, 0xe9, 0xb6, + 0x0d, 0xb3, 0xb7, 0x08, 0x9c, 0x2f, 0x41, 0xa1, 0x0e, 0x7c, 0x71, 0x34, 0x0c, 0xf6, 0x68, 0xfe, + 0xe1, 0xb0, 0xd9, 0x1f, 0xb9, 0xe8, 0x15, 0x38, 0xeb, 0xd3, 0x44, 0x55, 0xa4, 0x61, 0xa5, 0x97, + 0xe8, 0x0a, 0xbc, 0x98, 0xa8, 0x24, 0xcd, 0xd9, 0x0e, 0xe8, 0x5e, 0xb0, 0xf4, 0x9d, 0xb1, 0x01, + 0xbb, 0x6a, 0xe8, 0x3e, 0x50, 0xa7, 0xf9, 0x1e, 0xa3, 0xe1, 0xdd, 0xf4, 0x2c, 0xef, 0xaa, 0xd3, + 0x19, 0x87, 0xf9, 0xbe, 0x1a, 0x3f, 0x01, 0xb8, 0x7e, 0x06, 0xb0, 0xae, 0x52, 0x00, 0x9b, 0x55, + 0x16, 0xa1, 0xe7, 0x00, 0x97, 0x94, 0xad, 0x2e, 0xb5, 0x2e, 0xcf, 0x02, 0x2d, 0xc3, 0x18, 0xeb, + 0x70, 0x4d, 0x91, 0xdb, 0x49, 0x87, 0xc6, 0x22, 0x92, 0x56, 0x0b, 0xf9, 0x11, 0x68, 0xd5, 0xb5, + 0x58, 0xad, 0xe3, 0x21, 0xbc, 0x5a, 0x61, 0x9f, 0x5a, 0x86, 0x59, 0x22, 0xa3, 0x26, 0xb1, 0x56, + 0x91, 0x0d, 0xf7, 0x29, 0x88, 0xb1, 0x06, 0x57, 0x14, 0xb1, 0xbb, 0x39, 0xab, 0x2c, 0x95, 0xf0, + 0x0d, 0x80, 0xab, 0x93, 0x90, 0x5a, 0xc0, 0x03, 0x38, 0x5f, 0xe2, 0xbc, 0x9a, 0xfc, 0x4a, 0x09, + 0xf9, 0x62, 0x4a, 0xcd, 0x19, 0xf1, 0xc2, 0xce, 0xe6, 0x2f, 0x73, 0xf0, 0x39, 0x45, 0x04, 0x7d, + 0x0b, 0xe0, 0xdc, 0xd0, 0x80, 0xd0, 0x46, 0x49, 0xde, 0x0a, 0x17, 0x6f, 0x75, 0xab, 0xb0, 0xa7, + 0x6d, 0xdc, 0x58, 0xff, 0xfa, 0xcf, 0xff, 0x7e, 0x68, 0xbc, 0x8e, 0xae, 0xe3, 0x9a, 0x57, 0x16, + 0xfe, 0x92, 0xb9, 0x5f, 0xa1, 0xef, 0x00, 0xbc, 0x94, 0x73, 0xd2, 0x6a, 0x42, 0x45, 0x4b, 0x6f, + 0xdd, 0x98, 0x44, 0x28, 0x67, 0xcd, 0xc6, 0x1b, 0x8a, 0x53, 0x1b, 0x2d, 0xd5, 0x71, 0x42, 0xbf, + 0x01, 0xd8, 0xac, 0xb2, 0x04, 0xb4, 0x79, 0x2e, 0xff, 0xc8, 0x38, 0xde, 0x9a, 0xc2, 0x73, 0x8c, + 0x2d, 0xc5, 0xf5, 0xf6, 0x16, 0xd8, 0x30, 0x30, 0x2e, 0x7d, 0xe7, 0xda, 0x81, 0x70, 0xa9, 0x2d, + 0x45, 0xf6, 0xef, 0xe4, 0x48, 0xfe, 0x01, 0xe0, 0x52, 0xdd, 0xe9, 0x44, 0x77, 0xaa, 0xaa, 0x76, + 0x06, 0x6f, 0x69, 0xbd, 0x37, 0x5d, 0xb0, 0xd6, 0xb5, 0xaa, 0x74, 0x75, 0x50, 0x1b, 0xd7, 0x7e, + 0xa7, 0xa0, 0x5f, 0x01, 0x5c, 0xac, 0x39, 0x9a, 0x68, 0xab, 0x8a, 0xc5, 0x64, 0x53, 0x69, 0xdd, + 0x99, 0x2a, 0x56, 0x0b, 0x58, 0x51, 0x02, 0x96, 0xd1, 0xb5, 0xda, 0x8f, 0x37, 0xf4, 0x3b, 0x80, + 0xaf, 0x55, 0x1e, 0x78, 0xf4, 0x4e, 0x15, 0x83, 0x49, 0x6e, 0xd2, 0x7a, 0x77, 0x8a, 0x48, 0xcd, + 0xdc, 0x54, 0xcc, 0xbb, 0x68, 0x15, 0x9f, 0xe9, 0x83, 0x6f, 0xa7, 0xf7, 0xf8, 0xb8, 0x0d, 0x9e, + 0x1c, 0xb7, 0xc1, 0xbf, 0xc7, 0x6d, 0xf0, 0xfd, 0x49, 0x7b, 0xe6, 0xc9, 0x49, 0x7b, 0xe6, 0xaf, + 0x93, 0xf6, 0xcc, 0x67, 0x6f, 0x7b, 0x4c, 0x1e, 0xc4, 0x7d, 0xd3, 0x11, 0xfe, 0x78, 0xae, 0xe4, + 0xf6, 0x9b, 0xce, 0x01, 0x61, 0x01, 0x1e, 0xad, 0x3c, 0xca, 0xf2, 0xcb, 0xa3, 0x01, 0x8d, 0xfa, + 0x17, 0xd5, 0xf2, 0xad, 0xa7, 0x01, 0x00, 0x00, 0xff, 0xff, 0x75, 0xf9, 0x20, 0xb1, 0x8a, 0x0b, + 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -913,8 +751,6 @@ type QueryClient interface { ClobPair(ctx context.Context, in *QueryGetClobPairRequest, opts ...grpc.CallOption) (*QueryClobPairResponse, error) // Queries a list of ClobPair items. ClobPairAll(ctx context.Context, in *QueryAllClobPairRequest, opts ...grpc.CallOption) (*QueryClobPairAllResponse, error) - // Returns whether a subaccount is liquidatable. - AreSubaccountsLiquidatable(ctx context.Context, in *AreSubaccountsLiquidatableRequest, opts ...grpc.CallOption) (*AreSubaccountsLiquidatableResponse, error) // Runs the MEV node <> node calculation with the provided parameters. MevNodeToNodeCalculation(ctx context.Context, in *MevNodeToNodeCalculationRequest, opts ...grpc.CallOption) (*MevNodeToNodeCalculationResponse, error) // Queries EquityTierLimitConfiguration. @@ -951,15 +787,6 @@ func (c *queryClient) ClobPairAll(ctx context.Context, in *QueryAllClobPairReque return out, nil } -func (c *queryClient) AreSubaccountsLiquidatable(ctx context.Context, in *AreSubaccountsLiquidatableRequest, opts ...grpc.CallOption) (*AreSubaccountsLiquidatableResponse, error) { - out := new(AreSubaccountsLiquidatableResponse) - err := c.cc.Invoke(ctx, "/dydxprotocol.clob.Query/AreSubaccountsLiquidatable", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - func (c *queryClient) MevNodeToNodeCalculation(ctx context.Context, in *MevNodeToNodeCalculationRequest, opts ...grpc.CallOption) (*MevNodeToNodeCalculationResponse, error) { out := new(MevNodeToNodeCalculationResponse) err := c.cc.Invoke(ctx, "/dydxprotocol.clob.Query/MevNodeToNodeCalculation", in, out, opts...) @@ -1002,8 +829,6 @@ type QueryServer interface { ClobPair(context.Context, *QueryGetClobPairRequest) (*QueryClobPairResponse, error) // Queries a list of ClobPair items. ClobPairAll(context.Context, *QueryAllClobPairRequest) (*QueryClobPairAllResponse, error) - // Returns whether a subaccount is liquidatable. - AreSubaccountsLiquidatable(context.Context, *AreSubaccountsLiquidatableRequest) (*AreSubaccountsLiquidatableResponse, error) // Runs the MEV node <> node calculation with the provided parameters. MevNodeToNodeCalculation(context.Context, *MevNodeToNodeCalculationRequest) (*MevNodeToNodeCalculationResponse, error) // Queries EquityTierLimitConfiguration. @@ -1024,9 +849,6 @@ func (*UnimplementedQueryServer) ClobPair(ctx context.Context, req *QueryGetClob func (*UnimplementedQueryServer) ClobPairAll(ctx context.Context, req *QueryAllClobPairRequest) (*QueryClobPairAllResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ClobPairAll not implemented") } -func (*UnimplementedQueryServer) AreSubaccountsLiquidatable(ctx context.Context, req *AreSubaccountsLiquidatableRequest) (*AreSubaccountsLiquidatableResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method AreSubaccountsLiquidatable not implemented") -} func (*UnimplementedQueryServer) MevNodeToNodeCalculation(ctx context.Context, req *MevNodeToNodeCalculationRequest) (*MevNodeToNodeCalculationResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method MevNodeToNodeCalculation not implemented") } @@ -1080,24 +902,6 @@ func _Query_ClobPairAll_Handler(srv interface{}, ctx context.Context, dec func(i return interceptor(ctx, in, info, handler) } -func _Query_AreSubaccountsLiquidatable_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(AreSubaccountsLiquidatableRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(QueryServer).AreSubaccountsLiquidatable(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/dydxprotocol.clob.Query/AreSubaccountsLiquidatable", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(QueryServer).AreSubaccountsLiquidatable(ctx, req.(*AreSubaccountsLiquidatableRequest)) - } - return interceptor(ctx, in, info, handler) -} - func _Query_MevNodeToNodeCalculation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(MevNodeToNodeCalculationRequest) if err := dec(in); err != nil { @@ -1182,10 +986,6 @@ var _Query_serviceDesc = grpc.ServiceDesc{ MethodName: "ClobPairAll", Handler: _Query_ClobPairAll_Handler, }, - { - MethodName: "AreSubaccountsLiquidatable", - Handler: _Query_AreSubaccountsLiquidatable_Handler, - }, { MethodName: "MevNodeToNodeCalculation", Handler: _Query_MevNodeToNodeCalculation_Handler, @@ -1352,123 +1152,6 @@ func (m *QueryClobPairAllResponse) MarshalToSizedBuffer(dAtA []byte) (int, error return len(dAtA) - i, nil } -func (m *AreSubaccountsLiquidatableRequest) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBuffer(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *AreSubaccountsLiquidatableRequest) MarshalTo(dAtA []byte) (int, error) { - size := m.Size() - return m.MarshalToSizedBuffer(dAtA[:size]) -} - -func (m *AreSubaccountsLiquidatableRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { - i := len(dAtA) - _ = i - var l int - _ = l - if len(m.SubaccountIds) > 0 { - for iNdEx := len(m.SubaccountIds) - 1; iNdEx >= 0; iNdEx-- { - { - size, err := m.SubaccountIds[iNdEx].MarshalToSizedBuffer(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarintQuery(dAtA, i, uint64(size)) - } - i-- - dAtA[i] = 0xa - } - } - return len(dAtA) - i, nil -} - -func (m *AreSubaccountsLiquidatableResponse) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBuffer(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *AreSubaccountsLiquidatableResponse) MarshalTo(dAtA []byte) (int, error) { - size := m.Size() - return m.MarshalToSizedBuffer(dAtA[:size]) -} - -func (m *AreSubaccountsLiquidatableResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { - i := len(dAtA) - _ = i - var l int - _ = l - if len(m.Results) > 0 { - for iNdEx := len(m.Results) - 1; iNdEx >= 0; iNdEx-- { - { - size, err := m.Results[iNdEx].MarshalToSizedBuffer(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarintQuery(dAtA, i, uint64(size)) - } - i-- - dAtA[i] = 0xa - } - } - return len(dAtA) - i, nil -} - -func (m *AreSubaccountsLiquidatableResponse_Result) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBuffer(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *AreSubaccountsLiquidatableResponse_Result) MarshalTo(dAtA []byte) (int, error) { - size := m.Size() - return m.MarshalToSizedBuffer(dAtA[:size]) -} - -func (m *AreSubaccountsLiquidatableResponse_Result) MarshalToSizedBuffer(dAtA []byte) (int, error) { - i := len(dAtA) - _ = i - var l int - _ = l - if m.IsLiquidatable { - i-- - if m.IsLiquidatable { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i-- - dAtA[i] = 0x10 - } - { - size, err := m.SubaccountId.MarshalToSizedBuffer(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarintQuery(dAtA, i, uint64(size)) - } - i-- - dAtA[i] = 0xa - return len(dAtA) - i, nil -} - func (m *MevNodeToNodeCalculationRequest) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -1826,50 +1509,6 @@ func (m *QueryClobPairAllResponse) Size() (n int) { return n } -func (m *AreSubaccountsLiquidatableRequest) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if len(m.SubaccountIds) > 0 { - for _, e := range m.SubaccountIds { - l = e.Size() - n += 1 + l + sovQuery(uint64(l)) - } - } - return n -} - -func (m *AreSubaccountsLiquidatableResponse) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if len(m.Results) > 0 { - for _, e := range m.Results { - l = e.Size() - n += 1 + l + sovQuery(uint64(l)) - } - } - return n -} - -func (m *AreSubaccountsLiquidatableResponse_Result) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = m.SubaccountId.Size() - n += 1 + l + sovQuery(uint64(l)) - if m.IsLiquidatable { - n += 2 - } - return n -} - func (m *MevNodeToNodeCalculationRequest) Size() (n int) { if m == nil { return 0 @@ -2344,277 +1983,6 @@ func (m *QueryClobPairAllResponse) Unmarshal(dAtA []byte) error { } return nil } -func (m *AreSubaccountsLiquidatableRequest) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowQuery - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: AreSubaccountsLiquidatableRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: AreSubaccountsLiquidatableRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field SubaccountIds", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowQuery - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLengthQuery - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLengthQuery - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.SubaccountIds = append(m.SubaccountIds, types.SubaccountId{}) - if err := m.SubaccountIds[len(m.SubaccountIds)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skipQuery(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLengthQuery - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *AreSubaccountsLiquidatableResponse) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowQuery - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: AreSubaccountsLiquidatableResponse: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: AreSubaccountsLiquidatableResponse: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Results", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowQuery - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLengthQuery - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLengthQuery - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Results = append(m.Results, AreSubaccountsLiquidatableResponse_Result{}) - if err := m.Results[len(m.Results)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skipQuery(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLengthQuery - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *AreSubaccountsLiquidatableResponse_Result) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowQuery - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: Result: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: Result: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field SubaccountId", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowQuery - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLengthQuery - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLengthQuery - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - if err := m.SubaccountId.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - case 2: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field IsLiquidatable", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowQuery - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - m.IsLiquidatable = bool(v != 0) - default: - iNdEx = preIndex - skippy, err := skipQuery(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLengthQuery - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} func (m *MevNodeToNodeCalculationRequest) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 diff --git a/protocol/x/perpetuals/keeper/perpetual.go b/protocol/x/perpetuals/keeper/perpetual.go index 9d03408c48..c170c6c31b 100644 --- a/protocol/x/perpetuals/keeper/perpetual.go +++ b/protocol/x/perpetuals/keeper/perpetual.go @@ -768,6 +768,23 @@ func (k Keeper) GetNetNotional( return new(big.Int), err } + return GetNetNotionalInQuoteQuantums(perpetual, marketPrice, bigQuantums), nil +} + +// GetNetNotionalInQuoteQuantums returns the net notional in quote quantums, which can be +// represented by the following equation: +// +// `quantums / 10^baseAtomicResolution * marketPrice * 10^marketExponent * 10^quoteAtomicResolution`. +// Note that longs are positive, and shorts are negative. +// +// Also note that this is a stateless function. +func GetNetNotionalInQuoteQuantums( + perpetual types.Perpetual, + marketPrice pricestypes.MarketPrice, + bigQuantums *big.Int, +) ( + bigNetNotionalQuoteQuantums *big.Int, +) { bigQuoteQuantums := lib.BaseToQuoteQuantums( bigQuantums, perpetual.Params.AtomicResolution, @@ -775,7 +792,7 @@ func (k Keeper) GetNetNotional( marketPrice.Exponent, ) - return bigQuoteQuantums, nil + return bigQuoteQuantums } // GetNotionalInBaseQuantums returns the net notional in base quantums, which can be represented @@ -879,6 +896,29 @@ func (k Keeper) GetMarginRequirements( return nil, nil, err } + bigInitialMarginQuoteQuantums, + bigMaintenanceMarginQuoteQuantums = GetMarginRequirementsInQuoteQuantums( + perpetual, + marketPrice, + liquidityTier, + bigQuantums, + ) + return bigInitialMarginQuoteQuantums, bigMaintenanceMarginQuoteQuantums, nil +} + +// GetMarginRequirementsInQuoteQuantums returns initial and maintenance margin requirements +// in quote quantums, given the position size in base quantums. +// +// Note that this is a stateless function. +func GetMarginRequirementsInQuoteQuantums( + perpetual types.Perpetual, + marketPrice pricestypes.MarketPrice, + liquidityTier types.LiquidityTier, + bigQuantums *big.Int, +) ( + bigInitialMarginQuoteQuantums *big.Int, + bigMaintenanceMarginQuoteQuantums *big.Int, +) { // Always consider the magnitude of the position regardless of whether it is long/short. bigAbsQuantums := new(big.Int).Set(bigQuantums).Abs(bigQuantums) @@ -900,8 +940,7 @@ func (k Keeper) GetMarginRequirements( ), true, ) - - return bigInitialMarginQuoteQuantums, bigMaintenanceMarginQuoteQuantums, nil + return bigInitialMarginQuoteQuantums, bigMaintenanceMarginQuoteQuantums } // GetSettlementPpm returns the net settlement amount ppm (in quote quantums) given @@ -930,11 +969,31 @@ func (k Keeper) GetSettlementPpm( return big.NewInt(0), big.NewInt(0), err } + bigNetSettlementPpm, newFundingIndex = GetSettlementPpmWithPerpetual( + perpetual, + quantums, + index, + ) + return bigNetSettlementPpm, newFundingIndex, nil +} + +// GetSettlementPpm returns the net settlement amount ppm (in quote quantums) given +// the perpetual and position size (in base quantums). +// +// Note that this function is a stateless utility function. +func GetSettlementPpmWithPerpetual( + perpetual types.Perpetual, + quantums *big.Int, + index *big.Int, +) ( + bigNetSettlementPpm *big.Int, + newFundingIndex *big.Int, +) { indexDelta := new(big.Int).Sub(perpetual.FundingIndex.BigInt(), index) // if indexDelta is zero, then net settlement is zero. if indexDelta.Sign() == 0 { - return big.NewInt(0), perpetual.FundingIndex.BigInt(), nil + return big.NewInt(0), perpetual.FundingIndex.BigInt() } bigNetSettlementPpm = new(big.Int).Mul(indexDelta, quantums) @@ -944,7 +1003,7 @@ func (k Keeper) GetSettlementPpm( // Thus, always negate `bigNetSettlementPpm` here. bigNetSettlementPpm = bigNetSettlementPpm.Neg(bigNetSettlementPpm) - return bigNetSettlementPpm, perpetual.FundingIndex.BigInt(), nil + return bigNetSettlementPpm, perpetual.FundingIndex.BigInt() } // GetPremiumSamples reads premium samples from the current `funding-tick` epoch, diff --git a/protocol/x/subaccounts/keeper/subaccount.go b/protocol/x/subaccounts/keeper/subaccount.go index f387649059..4716ad3072 100644 --- a/protocol/x/subaccounts/keeper/subaccount.go +++ b/protocol/x/subaccounts/keeper/subaccount.go @@ -17,6 +17,8 @@ import ( indexer_manager "github.com/dydxprotocol/v4-chain/protocol/indexer/indexer_manager" "github.com/dydxprotocol/v4-chain/protocol/lib" "github.com/dydxprotocol/v4-chain/protocol/lib/metrics" + perpkeeper "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/keeper" + perptypes "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types" "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" ) @@ -376,6 +378,33 @@ func (k Keeper) getSettledSubaccount( settledSubaccount types.Subaccount, fundingPayments map[uint32]dtypes.SerializableInt, err error, +) { + // Fetch all relevant perpetuals. + perpetuals := make(map[uint32]perptypes.Perpetual) + for _, p := range subaccount.PerpetualPositions { + perpetual, err := k.perpetualsKeeper.GetPerpetual(ctx, p.PerpetualId) + if err != nil { + return types.Subaccount{}, nil, err + } + perpetuals[p.PerpetualId] = perpetual + } + + return GetSettledSubaccountWithPerpetuals(subaccount, perpetuals) +} + +// GetSettledSubaccountWithPerpetuals returns 1. a new settled subaccount given an unsettled subaccount, +// updating the USDC AssetPosition, FundingIndex, and LastFundingPayment fields accordingly +// (does not persist any changes) and 2. a map with perpetual ID as key and last funding +// payment as value (for emitting funding payments to indexer). +// +// Note that this is a stateless utility function. +func GetSettledSubaccountWithPerpetuals( + subaccount types.Subaccount, + perpetuals map[uint32]perptypes.Perpetual, +) ( + settledSubaccount types.Subaccount, + fundingPayments map[uint32]dtypes.SerializableInt, + err error, ) { totalNetSettlementPpm := big.NewInt(0) @@ -384,15 +413,21 @@ func (k Keeper) getSettledSubaccount( // Iterate through and settle all perpetual positions. for _, p := range subaccount.PerpetualPositions { - bigNetSettlementPpm, newFundingIndex, err := k.perpetualsKeeper.GetSettlementPpm( - ctx, - p.PerpetualId, + perpetual, found := perpetuals[p.PerpetualId] + if !found { + return types.Subaccount{}, + nil, + errorsmod.Wrap( + perptypes.ErrPerpetualDoesNotExist, lib.UintToString(p.PerpetualId), + ) + } + + // Call the stateless utility function to get the net settlement and new funding index. + bigNetSettlementPpm, newFundingIndex := perpkeeper.GetSettlementPpmWithPerpetual( + perpetual, p.GetBigQuantums(), p.FundingIndex.BigInt(), ) - if err != nil { - return types.Subaccount{}, nil, err - } // Record non-zero funding payment (to be later emitted in SubaccountUpdateEvent to indexer). // Note: Funding payment is the negative of settlement, i.e. positive settlement is equivalent // to a negative funding payment (position received funding payment) and vice versa. diff --git a/protocol/x/subaccounts/types/expected_keepers.go b/protocol/x/subaccounts/types/expected_keepers.go index a8890d649e..56d2c3e25e 100644 --- a/protocol/x/subaccounts/types/expected_keepers.go +++ b/protocol/x/subaccounts/types/expected_keepers.go @@ -62,6 +62,13 @@ type PerpetualsKeeper interface { newFundingIndex *big.Int, err error, ) + GetPerpetual( + ctx sdk.Context, + perpetualId uint32, + ) ( + perpetual perptypes.Perpetual, + err error, + ) GetAllPerpetuals(ctx sdk.Context) []perptypes.Perpetual }