From ece92b4216ce61c6e28090124aae1c4f1144ec2f Mon Sep 17 00:00:00 2001
From: jayy04 <103467857+jayy04@users.noreply.github.com>
Date: Tue, 19 Dec 2023 13:29:05 -0500
Subject: [PATCH] [CLOB-1043] replicate IsLiquidatable logic on daemon (#873)

* [CLOB-1043] replicate IsLiquidatable logic on daemon

* comments

* comments

* update to return error

* [CLOB-1044] replicate funding settlement on liquidation daemon (#882)

* [CLOB-1043] deprecate unused grpc query (#889)

* [CLOB-1043] deprecate unused grpc query

* fix lint

* [CLOB-1046] populate negative tnc subaccounts in grpc request (#890)

* [CLOB-1046] populate negative tnc subaccounts in grpc request

* [CLOB-1047] populate subaccounts with open positions in grpc request (#892)

* [CLOB-1047] populate subaccounts with open positions in grpc request

* comments
---
 .../dydxprotocol/clob/query.rpc.Query.ts      |  16 +-
 .../src/codegen/dydxprotocol/clob/query.ts    | 192 -----
 proto/dydxprotocol/clob/query.proto           |  25 -
 protocol/daemons/flags/flags.go               |  39 +-
 protocol/daemons/flags/flags_test.go          |   8 +-
 .../daemons/liquidation/client/client_test.go | 183 -----
 .../daemons/liquidation/client/grpc_helper.go |  59 +-
 .../liquidation/client/grpc_helper_test.go    | 192 ++---
 .../liquidation/client/sub_task_runner.go     | 306 ++++++-
 .../client/sub_task_runner_test.go            | 735 +++++++++++++++++
 protocol/daemons/server/liquidation.go        |   3 +
 protocol/daemons/server/liquidation_test.go   |  70 +-
 .../liquidations/daemon_liquidation_info.go   |   8 +-
 .../daemon_liquidation_info_test.go           | 119 +--
 protocol/lib/collections.go                   |  18 +
 protocol/lib/collections_test.go              |  52 ++
 protocol/lib/metrics/constants.go             |   3 +
 protocol/mocks/QueryClient.go                 |  30 -
 protocol/testutil/constants/perpetuals.go     |   1 +
 protocol/testutil/constants/subaccounts.go    |  36 +-
 ...grpc_query_are_subaccounts_liquidatable.go |  40 -
 ...query_are_subaccounts_liquidatable_test.go | 166 ----
 protocol/x/clob/keeper/liquidations.go        |  21 +-
 protocol/x/clob/types/query.pb.go             | 772 ++----------------
 protocol/x/perpetuals/keeper/perpetual.go     |  69 +-
 protocol/x/subaccounts/keeper/subaccount.go   |  47 +-
 .../x/subaccounts/types/expected_keepers.go   |   7 +
 27 files changed, 1567 insertions(+), 1650 deletions(-)
 create mode 100644 protocol/daemons/liquidation/client/sub_task_runner_test.go
 delete mode 100644 protocol/x/clob/keeper/grpc_query_are_subaccounts_liquidatable.go
 delete mode 100644 protocol/x/clob/keeper/grpc_query_are_subaccounts_liquidatable_test.go

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<QueryClobPairAllResponse>;
-  /** Returns whether a subaccount is liquidatable. */
-
-  areSubaccountsLiquidatable(request: AreSubaccountsLiquidatableRequest): Promise<AreSubaccountsLiquidatableResponse>;
   /** Runs the MEV node <> node calculation with the provided parameters. */
 
   mevNodeToNodeCalculation(request: MevNodeToNodeCalculationRequest): Promise<MevNodeToNodeCalculationResponse>;
@@ -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<AreSubaccountsLiquidatableResponse> {
-    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<MevNodeToNodeCalculationResponse> {
     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<AreSubaccountsLiquidatableResponse> {
-      return queryService.areSubaccountsLiquidatable(request);
-    },
-
     mevNodeToNodeCalculation(request: MevNodeToNodeCalculationRequest): Promise<MevNodeToNodeCalculationResponse> {
       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>): 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>): 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>): 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/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/daemons/flags/flags.go b/protocol/daemons/flags/flags.go
index 6fbbcd7c3c..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.
@@ -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/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
 }