From a6cf63c03e002e20b679a858e6c3f1f90d907cca Mon Sep 17 00:00:00 2001
From: Teddy Ding <teddy@dydx.exchange>
Date: Thu, 3 Oct 2024 16:28:48 -0400
Subject: [PATCH] feat: make PML compatible with OE by staging in-memory CLOB
 side effects (#2447)

---
 .../src/codegen/dydxprotocol/bundle.ts        | 516 +++++++++---------
 .../dydxprotocol/clob/finalize_block.ts       |  66 +++
 .../v4-protos/src/codegen/gogoproto/bundle.ts |   4 +-
 .../v4-protos/src/codegen/google/bundle.ts    |  22 +-
 proto/dydxprotocol/clob/finalize_block.proto  |  16 +
 protocol/mocks/MemClob.go                     |  17 +-
 protocol/testutil/keeper/clob.go              |   4 +-
 protocol/x/clob/abci.go                       |   6 +
 protocol/x/clob/abci_test.go                  |   6 +-
 protocol/x/clob/genesis.go                    |   2 +-
 protocol/x/clob/keeper/clob_pair.go           | 174 ++++--
 protocol/x/clob/keeper/clob_pair_test.go      |  32 +-
 protocol/x/clob/keeper/deleveraging_test.go   |   4 +-
 .../x/clob/keeper/get_price_premium_test.go   |   2 +-
 protocol/x/clob/keeper/keeper.go              |  54 +-
 protocol/x/clob/keeper/liquidations_test.go   |  22 +-
 protocol/x/clob/keeper/mev_test.go            |   4 +-
 .../keeper/msg_server_place_order_test.go     |   4 +-
 protocol/x/clob/keeper/orders_test.go         |  18 +-
 .../x/clob/keeper/process_operations_test.go  |   2 +-
 protocol/x/clob/memclob/memclob.go            |   5 +-
 .../memclob_get_impact_price_subticks_test.go |   2 +-
 protocol/x/clob/types/finalize_block.pb.go    | 390 +++++++++++++
 protocol/x/clob/types/keys.go                 |   6 +
 protocol/x/clob/types/memclob.go              |   2 +-
 protocol/x/listing/keeper/listing.go          |  36 +-
 protocol/x/listing/keeper/listing_test.go     |  19 +-
 protocol/x/listing/types/expected_keepers.go  |   3 +-
 28 files changed, 1024 insertions(+), 414 deletions(-)
 create mode 100644 indexer/packages/v4-protos/src/codegen/dydxprotocol/clob/finalize_block.ts
 create mode 100644 proto/dydxprotocol/clob/finalize_block.proto
 create mode 100644 protocol/x/clob/types/finalize_block.pb.go

diff --git a/indexer/packages/v4-protos/src/codegen/dydxprotocol/bundle.ts b/indexer/packages/v4-protos/src/codegen/dydxprotocol/bundle.ts
index 3324f5cd90..3550eca53c 100644
--- a/indexer/packages/v4-protos/src/codegen/dydxprotocol/bundle.ts
+++ b/indexer/packages/v4-protos/src/codegen/dydxprotocol/bundle.ts
@@ -26,161 +26,162 @@ import * as _29 from "./bridge/tx";
 import * as _30 from "./clob/block_rate_limit_config";
 import * as _31 from "./clob/clob_pair";
 import * as _32 from "./clob/equity_tier_limit_config";
-import * as _33 from "./clob/genesis";
-import * as _34 from "./clob/liquidations_config";
-import * as _35 from "./clob/liquidations";
-import * as _36 from "./clob/matches";
-import * as _37 from "./clob/mev";
-import * as _38 from "./clob/operation";
-import * as _39 from "./clob/order_removals";
-import * as _40 from "./clob/order";
-import * as _41 from "./clob/process_proposer_matches_events";
-import * as _42 from "./clob/query";
-import * as _43 from "./clob/streaming";
-import * as _44 from "./clob/tx";
-import * as _45 from "./daemons/bridge/bridge";
-import * as _46 from "./daemons/liquidation/liquidation";
-import * as _47 from "./daemons/pricefeed/price_feed";
-import * as _48 from "./delaymsg/block_message_ids";
-import * as _49 from "./delaymsg/delayed_message";
-import * as _50 from "./delaymsg/genesis";
-import * as _51 from "./delaymsg/query";
-import * as _52 from "./delaymsg/tx";
-import * as _53 from "./epochs/epoch_info";
-import * as _54 from "./epochs/genesis";
-import * as _55 from "./epochs/query";
-import * as _56 from "./feetiers/genesis";
-import * as _57 from "./feetiers/params";
-import * as _58 from "./feetiers/query";
-import * as _59 from "./feetiers/tx";
-import * as _60 from "./govplus/genesis";
-import * as _61 from "./govplus/query";
-import * as _62 from "./govplus/tx";
-import * as _63 from "./indexer/events/events";
-import * as _64 from "./indexer/indexer_manager/event";
-import * as _65 from "./indexer/off_chain_updates/off_chain_updates";
-import * as _66 from "./indexer/protocol/v1/clob";
-import * as _67 from "./indexer/protocol/v1/perpetual";
-import * as _68 from "./indexer/protocol/v1/subaccount";
-import * as _69 from "./indexer/protocol/v1/vault";
-import * as _70 from "./indexer/redis/redis_order";
-import * as _71 from "./indexer/shared/removal_reason";
-import * as _72 from "./indexer/socks/messages";
-import * as _73 from "./listing/genesis";
-import * as _74 from "./listing/params";
-import * as _75 from "./listing/query";
-import * as _76 from "./listing/tx";
-import * as _77 from "./perpetuals/genesis";
-import * as _78 from "./perpetuals/params";
-import * as _79 from "./perpetuals/perpetual";
-import * as _80 from "./perpetuals/query";
-import * as _81 from "./perpetuals/tx";
-import * as _82 from "./prices/genesis";
-import * as _83 from "./prices/market_param";
-import * as _84 from "./prices/market_price";
-import * as _85 from "./prices/query";
-import * as _86 from "./prices/tx";
-import * as _87 from "./ratelimit/capacity";
-import * as _88 from "./ratelimit/genesis";
-import * as _89 from "./ratelimit/limit_params";
-import * as _90 from "./ratelimit/pending_send_packet";
-import * as _91 from "./ratelimit/query";
-import * as _92 from "./ratelimit/tx";
-import * as _93 from "./revshare/genesis";
-import * as _94 from "./revshare/params";
-import * as _95 from "./revshare/query";
-import * as _96 from "./revshare/revshare";
-import * as _97 from "./revshare/tx";
-import * as _98 from "./rewards/genesis";
-import * as _99 from "./rewards/params";
-import * as _100 from "./rewards/query";
-import * as _101 from "./rewards/reward_share";
-import * as _102 from "./rewards/tx";
-import * as _103 from "./sending/genesis";
-import * as _104 from "./sending/query";
-import * as _105 from "./sending/transfer";
-import * as _106 from "./sending/tx";
-import * as _107 from "./stats/genesis";
-import * as _108 from "./stats/params";
-import * as _109 from "./stats/query";
-import * as _110 from "./stats/stats";
-import * as _111 from "./stats/tx";
-import * as _112 from "./subaccounts/asset_position";
-import * as _113 from "./subaccounts/genesis";
-import * as _114 from "./subaccounts/perpetual_position";
-import * as _115 from "./subaccounts/query";
-import * as _116 from "./subaccounts/streaming";
-import * as _117 from "./subaccounts/subaccount";
-import * as _118 from "./vault/genesis";
-import * as _119 from "./vault/params";
-import * as _120 from "./vault/query";
-import * as _121 from "./vault/share";
-import * as _122 from "./vault/tx";
-import * as _123 from "./vault/vault";
-import * as _124 from "./vest/genesis";
-import * as _125 from "./vest/query";
-import * as _126 from "./vest/tx";
-import * as _127 from "./vest/vest_entry";
-import * as _135 from "./accountplus/query.lcd";
-import * as _136 from "./assets/query.lcd";
-import * as _137 from "./blocktime/query.lcd";
-import * as _138 from "./bridge/query.lcd";
-import * as _139 from "./clob/query.lcd";
-import * as _140 from "./delaymsg/query.lcd";
-import * as _141 from "./epochs/query.lcd";
-import * as _142 from "./feetiers/query.lcd";
-import * as _143 from "./listing/query.lcd";
-import * as _144 from "./perpetuals/query.lcd";
-import * as _145 from "./prices/query.lcd";
-import * as _146 from "./ratelimit/query.lcd";
-import * as _147 from "./revshare/query.lcd";
-import * as _148 from "./rewards/query.lcd";
-import * as _149 from "./stats/query.lcd";
-import * as _150 from "./subaccounts/query.lcd";
-import * as _151 from "./vault/query.lcd";
-import * as _152 from "./vest/query.lcd";
-import * as _153 from "./accountplus/query.rpc.Query";
-import * as _154 from "./affiliates/query.rpc.Query";
-import * as _155 from "./assets/query.rpc.Query";
-import * as _156 from "./blocktime/query.rpc.Query";
-import * as _157 from "./bridge/query.rpc.Query";
-import * as _158 from "./clob/query.rpc.Query";
-import * as _159 from "./delaymsg/query.rpc.Query";
-import * as _160 from "./epochs/query.rpc.Query";
-import * as _161 from "./feetiers/query.rpc.Query";
-import * as _162 from "./govplus/query.rpc.Query";
-import * as _163 from "./listing/query.rpc.Query";
-import * as _164 from "./perpetuals/query.rpc.Query";
-import * as _165 from "./prices/query.rpc.Query";
-import * as _166 from "./ratelimit/query.rpc.Query";
-import * as _167 from "./revshare/query.rpc.Query";
-import * as _168 from "./rewards/query.rpc.Query";
-import * as _169 from "./sending/query.rpc.Query";
-import * as _170 from "./stats/query.rpc.Query";
-import * as _171 from "./subaccounts/query.rpc.Query";
-import * as _172 from "./vault/query.rpc.Query";
-import * as _173 from "./vest/query.rpc.Query";
-import * as _174 from "./accountplus/tx.rpc.msg";
-import * as _175 from "./affiliates/tx.rpc.msg";
-import * as _176 from "./blocktime/tx.rpc.msg";
-import * as _177 from "./bridge/tx.rpc.msg";
-import * as _178 from "./clob/tx.rpc.msg";
-import * as _179 from "./delaymsg/tx.rpc.msg";
-import * as _180 from "./feetiers/tx.rpc.msg";
-import * as _181 from "./govplus/tx.rpc.msg";
-import * as _182 from "./listing/tx.rpc.msg";
-import * as _183 from "./perpetuals/tx.rpc.msg";
-import * as _184 from "./prices/tx.rpc.msg";
-import * as _185 from "./ratelimit/tx.rpc.msg";
-import * as _186 from "./revshare/tx.rpc.msg";
-import * as _187 from "./rewards/tx.rpc.msg";
-import * as _188 from "./sending/tx.rpc.msg";
-import * as _189 from "./stats/tx.rpc.msg";
-import * as _190 from "./vault/tx.rpc.msg";
-import * as _191 from "./vest/tx.rpc.msg";
-import * as _192 from "./lcd";
-import * as _193 from "./rpc.query";
-import * as _194 from "./rpc.tx";
+import * as _33 from "./clob/finalize_block";
+import * as _34 from "./clob/genesis";
+import * as _35 from "./clob/liquidations_config";
+import * as _36 from "./clob/liquidations";
+import * as _37 from "./clob/matches";
+import * as _38 from "./clob/mev";
+import * as _39 from "./clob/operation";
+import * as _40 from "./clob/order_removals";
+import * as _41 from "./clob/order";
+import * as _42 from "./clob/process_proposer_matches_events";
+import * as _43 from "./clob/query";
+import * as _44 from "./clob/streaming";
+import * as _45 from "./clob/tx";
+import * as _46 from "./daemons/bridge/bridge";
+import * as _47 from "./daemons/liquidation/liquidation";
+import * as _48 from "./daemons/pricefeed/price_feed";
+import * as _49 from "./delaymsg/block_message_ids";
+import * as _50 from "./delaymsg/delayed_message";
+import * as _51 from "./delaymsg/genesis";
+import * as _52 from "./delaymsg/query";
+import * as _53 from "./delaymsg/tx";
+import * as _54 from "./epochs/epoch_info";
+import * as _55 from "./epochs/genesis";
+import * as _56 from "./epochs/query";
+import * as _57 from "./feetiers/genesis";
+import * as _58 from "./feetiers/params";
+import * as _59 from "./feetiers/query";
+import * as _60 from "./feetiers/tx";
+import * as _61 from "./govplus/genesis";
+import * as _62 from "./govplus/query";
+import * as _63 from "./govplus/tx";
+import * as _64 from "./indexer/events/events";
+import * as _65 from "./indexer/indexer_manager/event";
+import * as _66 from "./indexer/off_chain_updates/off_chain_updates";
+import * as _67 from "./indexer/protocol/v1/clob";
+import * as _68 from "./indexer/protocol/v1/perpetual";
+import * as _69 from "./indexer/protocol/v1/subaccount";
+import * as _70 from "./indexer/protocol/v1/vault";
+import * as _71 from "./indexer/redis/redis_order";
+import * as _72 from "./indexer/shared/removal_reason";
+import * as _73 from "./indexer/socks/messages";
+import * as _74 from "./listing/genesis";
+import * as _75 from "./listing/params";
+import * as _76 from "./listing/query";
+import * as _77 from "./listing/tx";
+import * as _78 from "./perpetuals/genesis";
+import * as _79 from "./perpetuals/params";
+import * as _80 from "./perpetuals/perpetual";
+import * as _81 from "./perpetuals/query";
+import * as _82 from "./perpetuals/tx";
+import * as _83 from "./prices/genesis";
+import * as _84 from "./prices/market_param";
+import * as _85 from "./prices/market_price";
+import * as _86 from "./prices/query";
+import * as _87 from "./prices/tx";
+import * as _88 from "./ratelimit/capacity";
+import * as _89 from "./ratelimit/genesis";
+import * as _90 from "./ratelimit/limit_params";
+import * as _91 from "./ratelimit/pending_send_packet";
+import * as _92 from "./ratelimit/query";
+import * as _93 from "./ratelimit/tx";
+import * as _94 from "./revshare/genesis";
+import * as _95 from "./revshare/params";
+import * as _96 from "./revshare/query";
+import * as _97 from "./revshare/revshare";
+import * as _98 from "./revshare/tx";
+import * as _99 from "./rewards/genesis";
+import * as _100 from "./rewards/params";
+import * as _101 from "./rewards/query";
+import * as _102 from "./rewards/reward_share";
+import * as _103 from "./rewards/tx";
+import * as _104 from "./sending/genesis";
+import * as _105 from "./sending/query";
+import * as _106 from "./sending/transfer";
+import * as _107 from "./sending/tx";
+import * as _108 from "./stats/genesis";
+import * as _109 from "./stats/params";
+import * as _110 from "./stats/query";
+import * as _111 from "./stats/stats";
+import * as _112 from "./stats/tx";
+import * as _113 from "./subaccounts/asset_position";
+import * as _114 from "./subaccounts/genesis";
+import * as _115 from "./subaccounts/perpetual_position";
+import * as _116 from "./subaccounts/query";
+import * as _117 from "./subaccounts/streaming";
+import * as _118 from "./subaccounts/subaccount";
+import * as _119 from "./vault/genesis";
+import * as _120 from "./vault/params";
+import * as _121 from "./vault/query";
+import * as _122 from "./vault/share";
+import * as _123 from "./vault/tx";
+import * as _124 from "./vault/vault";
+import * as _125 from "./vest/genesis";
+import * as _126 from "./vest/query";
+import * as _127 from "./vest/tx";
+import * as _128 from "./vest/vest_entry";
+import * as _136 from "./accountplus/query.lcd";
+import * as _137 from "./assets/query.lcd";
+import * as _138 from "./blocktime/query.lcd";
+import * as _139 from "./bridge/query.lcd";
+import * as _140 from "./clob/query.lcd";
+import * as _141 from "./delaymsg/query.lcd";
+import * as _142 from "./epochs/query.lcd";
+import * as _143 from "./feetiers/query.lcd";
+import * as _144 from "./listing/query.lcd";
+import * as _145 from "./perpetuals/query.lcd";
+import * as _146 from "./prices/query.lcd";
+import * as _147 from "./ratelimit/query.lcd";
+import * as _148 from "./revshare/query.lcd";
+import * as _149 from "./rewards/query.lcd";
+import * as _150 from "./stats/query.lcd";
+import * as _151 from "./subaccounts/query.lcd";
+import * as _152 from "./vault/query.lcd";
+import * as _153 from "./vest/query.lcd";
+import * as _154 from "./accountplus/query.rpc.Query";
+import * as _155 from "./affiliates/query.rpc.Query";
+import * as _156 from "./assets/query.rpc.Query";
+import * as _157 from "./blocktime/query.rpc.Query";
+import * as _158 from "./bridge/query.rpc.Query";
+import * as _159 from "./clob/query.rpc.Query";
+import * as _160 from "./delaymsg/query.rpc.Query";
+import * as _161 from "./epochs/query.rpc.Query";
+import * as _162 from "./feetiers/query.rpc.Query";
+import * as _163 from "./govplus/query.rpc.Query";
+import * as _164 from "./listing/query.rpc.Query";
+import * as _165 from "./perpetuals/query.rpc.Query";
+import * as _166 from "./prices/query.rpc.Query";
+import * as _167 from "./ratelimit/query.rpc.Query";
+import * as _168 from "./revshare/query.rpc.Query";
+import * as _169 from "./rewards/query.rpc.Query";
+import * as _170 from "./sending/query.rpc.Query";
+import * as _171 from "./stats/query.rpc.Query";
+import * as _172 from "./subaccounts/query.rpc.Query";
+import * as _173 from "./vault/query.rpc.Query";
+import * as _174 from "./vest/query.rpc.Query";
+import * as _175 from "./accountplus/tx.rpc.msg";
+import * as _176 from "./affiliates/tx.rpc.msg";
+import * as _177 from "./blocktime/tx.rpc.msg";
+import * as _178 from "./bridge/tx.rpc.msg";
+import * as _179 from "./clob/tx.rpc.msg";
+import * as _180 from "./delaymsg/tx.rpc.msg";
+import * as _181 from "./feetiers/tx.rpc.msg";
+import * as _182 from "./govplus/tx.rpc.msg";
+import * as _183 from "./listing/tx.rpc.msg";
+import * as _184 from "./perpetuals/tx.rpc.msg";
+import * as _185 from "./prices/tx.rpc.msg";
+import * as _186 from "./ratelimit/tx.rpc.msg";
+import * as _187 from "./revshare/tx.rpc.msg";
+import * as _188 from "./rewards/tx.rpc.msg";
+import * as _189 from "./sending/tx.rpc.msg";
+import * as _190 from "./stats/tx.rpc.msg";
+import * as _191 from "./vault/tx.rpc.msg";
+import * as _192 from "./vest/tx.rpc.msg";
+import * as _193 from "./lcd";
+import * as _194 from "./rpc.query";
+import * as _195 from "./rpc.tx";
 export namespace dydxprotocol {
   export const accountplus = { ..._5,
     ..._6,
@@ -188,32 +189,32 @@ export namespace dydxprotocol {
     ..._8,
     ..._9,
     ..._10,
-    ..._135,
-    ..._153,
-    ..._174
+    ..._136,
+    ..._154,
+    ..._175
   };
   export const affiliates = { ..._11,
     ..._12,
     ..._13,
     ..._14,
-    ..._154,
-    ..._175
+    ..._155,
+    ..._176
   };
   export const assets = { ..._15,
     ..._16,
     ..._17,
     ..._18,
-    ..._136,
-    ..._155
+    ..._137,
+    ..._156
   };
   export const blocktime = { ..._19,
     ..._20,
     ..._21,
     ..._22,
     ..._23,
-    ..._137,
-    ..._156,
-    ..._176
+    ..._138,
+    ..._157,
+    ..._177
   };
   export const bridge = { ..._24,
     ..._25,
@@ -221,9 +222,9 @@ export namespace dydxprotocol {
     ..._27,
     ..._28,
     ..._29,
-    ..._138,
-    ..._157,
-    ..._177
+    ..._139,
+    ..._158,
+    ..._178
   };
   export const clob = { ..._30,
     ..._31,
@@ -240,167 +241,168 @@ export namespace dydxprotocol {
     ..._42,
     ..._43,
     ..._44,
-    ..._139,
-    ..._158,
-    ..._178
+    ..._45,
+    ..._140,
+    ..._159,
+    ..._179
   };
   export namespace daemons {
-    export const bridge = { ..._45
+    export const bridge = { ..._46
     };
-    export const liquidation = { ..._46
+    export const liquidation = { ..._47
     };
-    export const pricefeed = { ..._47
+    export const pricefeed = { ..._48
     };
   }
-  export const delaymsg = { ..._48,
-    ..._49,
+  export const delaymsg = { ..._49,
     ..._50,
     ..._51,
     ..._52,
-    ..._140,
-    ..._159,
-    ..._179
+    ..._53,
+    ..._141,
+    ..._160,
+    ..._180
   };
-  export const epochs = { ..._53,
-    ..._54,
+  export const epochs = { ..._54,
     ..._55,
-    ..._141,
-    ..._160
+    ..._56,
+    ..._142,
+    ..._161
   };
-  export const feetiers = { ..._56,
-    ..._57,
+  export const feetiers = { ..._57,
     ..._58,
     ..._59,
-    ..._142,
-    ..._161,
-    ..._180
-  };
-  export const govplus = { ..._60,
-    ..._61,
-    ..._62,
+    ..._60,
+    ..._143,
     ..._162,
     ..._181
   };
+  export const govplus = { ..._61,
+    ..._62,
+    ..._63,
+    ..._163,
+    ..._182
+  };
   export namespace indexer {
-    export const events = { ..._63
+    export const events = { ..._64
     };
-    export const indexer_manager = { ..._64
+    export const indexer_manager = { ..._65
     };
-    export const off_chain_updates = { ..._65
+    export const off_chain_updates = { ..._66
     };
     export namespace protocol {
-      export const v1 = { ..._66,
-        ..._67,
+      export const v1 = { ..._67,
         ..._68,
-        ..._69
+        ..._69,
+        ..._70
       };
     }
-    export const redis = { ..._70
+    export const redis = { ..._71
     };
-    export const shared = { ..._71
+    export const shared = { ..._72
     };
-    export const socks = { ..._72
+    export const socks = { ..._73
     };
   }
-  export const listing = { ..._73,
-    ..._74,
+  export const listing = { ..._74,
     ..._75,
     ..._76,
-    ..._143,
-    ..._163,
-    ..._182
+    ..._77,
+    ..._144,
+    ..._164,
+    ..._183
   };
-  export const perpetuals = { ..._77,
-    ..._78,
+  export const perpetuals = { ..._78,
     ..._79,
     ..._80,
     ..._81,
-    ..._144,
-    ..._164,
-    ..._183
+    ..._82,
+    ..._145,
+    ..._165,
+    ..._184
   };
-  export const prices = { ..._82,
-    ..._83,
+  export const prices = { ..._83,
     ..._84,
     ..._85,
     ..._86,
-    ..._145,
-    ..._165,
-    ..._184
+    ..._87,
+    ..._146,
+    ..._166,
+    ..._185
   };
-  export const ratelimit = { ..._87,
-    ..._88,
+  export const ratelimit = { ..._88,
     ..._89,
     ..._90,
     ..._91,
     ..._92,
-    ..._146,
-    ..._166,
-    ..._185
+    ..._93,
+    ..._147,
+    ..._167,
+    ..._186
   };
-  export const revshare = { ..._93,
-    ..._94,
+  export const revshare = { ..._94,
     ..._95,
     ..._96,
     ..._97,
-    ..._147,
-    ..._167,
-    ..._186
+    ..._98,
+    ..._148,
+    ..._168,
+    ..._187
   };
-  export const rewards = { ..._98,
-    ..._99,
+  export const rewards = { ..._99,
     ..._100,
     ..._101,
     ..._102,
-    ..._148,
-    ..._168,
-    ..._187
+    ..._103,
+    ..._149,
+    ..._169,
+    ..._188
   };
-  export const sending = { ..._103,
-    ..._104,
+  export const sending = { ..._104,
     ..._105,
     ..._106,
-    ..._169,
-    ..._188
+    ..._107,
+    ..._170,
+    ..._189
   };
-  export const stats = { ..._107,
-    ..._108,
+  export const stats = { ..._108,
     ..._109,
     ..._110,
     ..._111,
-    ..._149,
-    ..._170,
-    ..._189
+    ..._112,
+    ..._150,
+    ..._171,
+    ..._190
   };
-  export const subaccounts = { ..._112,
-    ..._113,
+  export const subaccounts = { ..._113,
     ..._114,
     ..._115,
     ..._116,
     ..._117,
-    ..._150,
-    ..._171
+    ..._118,
+    ..._151,
+    ..._172
   };
-  export const vault = { ..._118,
-    ..._119,
+  export const vault = { ..._119,
     ..._120,
     ..._121,
     ..._122,
     ..._123,
-    ..._151,
-    ..._172,
-    ..._190
-  };
-  export const vest = { ..._124,
-    ..._125,
-    ..._126,
-    ..._127,
+    ..._124,
     ..._152,
     ..._173,
     ..._191
   };
-  export const ClientFactory = { ..._192,
-    ..._193,
-    ..._194
+  export const vest = { ..._125,
+    ..._126,
+    ..._127,
+    ..._128,
+    ..._153,
+    ..._174,
+    ..._192
+  };
+  export const ClientFactory = { ..._193,
+    ..._194,
+    ..._195
   };
 }
\ No newline at end of file
diff --git a/indexer/packages/v4-protos/src/codegen/dydxprotocol/clob/finalize_block.ts b/indexer/packages/v4-protos/src/codegen/dydxprotocol/clob/finalize_block.ts
new file mode 100644
index 0000000000..ac6382d2c9
--- /dev/null
+++ b/indexer/packages/v4-protos/src/codegen/dydxprotocol/clob/finalize_block.ts
@@ -0,0 +1,66 @@
+import { ClobPair, ClobPairSDKType } from "./clob_pair";
+import * as _m0 from "protobufjs/minimal";
+import { DeepPartial } from "../../helpers";
+/**
+ * ClobStagedFinalizeBlockEvent defines a CLOB event staged during
+ * FinalizeBlock.
+ */
+
+export interface ClobStagedFinalizeBlockEvent {
+  /** create_clob_pair indicates a new CLOB pair creation. */
+  createClobPair?: ClobPair;
+}
+/**
+ * ClobStagedFinalizeBlockEvent defines a CLOB event staged during
+ * FinalizeBlock.
+ */
+
+export interface ClobStagedFinalizeBlockEventSDKType {
+  /** create_clob_pair indicates a new CLOB pair creation. */
+  create_clob_pair?: ClobPairSDKType;
+}
+
+function createBaseClobStagedFinalizeBlockEvent(): ClobStagedFinalizeBlockEvent {
+  return {
+    createClobPair: undefined
+  };
+}
+
+export const ClobStagedFinalizeBlockEvent = {
+  encode(message: ClobStagedFinalizeBlockEvent, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
+    if (message.createClobPair !== undefined) {
+      ClobPair.encode(message.createClobPair, writer.uint32(10).fork()).ldelim();
+    }
+
+    return writer;
+  },
+
+  decode(input: _m0.Reader | Uint8Array, length?: number): ClobStagedFinalizeBlockEvent {
+    const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input);
+    let end = length === undefined ? reader.len : reader.pos + length;
+    const message = createBaseClobStagedFinalizeBlockEvent();
+
+    while (reader.pos < end) {
+      const tag = reader.uint32();
+
+      switch (tag >>> 3) {
+        case 1:
+          message.createClobPair = ClobPair.decode(reader, reader.uint32());
+          break;
+
+        default:
+          reader.skipType(tag & 7);
+          break;
+      }
+    }
+
+    return message;
+  },
+
+  fromPartial(object: DeepPartial<ClobStagedFinalizeBlockEvent>): ClobStagedFinalizeBlockEvent {
+    const message = createBaseClobStagedFinalizeBlockEvent();
+    message.createClobPair = object.createClobPair !== undefined && object.createClobPair !== null ? ClobPair.fromPartial(object.createClobPair) : undefined;
+    return message;
+  }
+
+};
\ No newline at end of file
diff --git a/indexer/packages/v4-protos/src/codegen/gogoproto/bundle.ts b/indexer/packages/v4-protos/src/codegen/gogoproto/bundle.ts
index 00375897ff..675007986c 100644
--- a/indexer/packages/v4-protos/src/codegen/gogoproto/bundle.ts
+++ b/indexer/packages/v4-protos/src/codegen/gogoproto/bundle.ts
@@ -1,3 +1,3 @@
-import * as _128 from "./gogo";
-export const gogoproto = { ..._128
+import * as _129 from "./gogo";
+export const gogoproto = { ..._129
 };
\ No newline at end of file
diff --git a/indexer/packages/v4-protos/src/codegen/google/bundle.ts b/indexer/packages/v4-protos/src/codegen/google/bundle.ts
index b2f572879c..4617ba1a6e 100644
--- a/indexer/packages/v4-protos/src/codegen/google/bundle.ts
+++ b/indexer/packages/v4-protos/src/codegen/google/bundle.ts
@@ -1,16 +1,16 @@
-import * as _129 from "./api/annotations";
-import * as _130 from "./api/http";
-import * as _131 from "./protobuf/descriptor";
-import * as _132 from "./protobuf/duration";
-import * as _133 from "./protobuf/timestamp";
-import * as _134 from "./protobuf/any";
+import * as _130 from "./api/annotations";
+import * as _131 from "./api/http";
+import * as _132 from "./protobuf/descriptor";
+import * as _133 from "./protobuf/duration";
+import * as _134 from "./protobuf/timestamp";
+import * as _135 from "./protobuf/any";
 export namespace google {
-  export const api = { ..._129,
-    ..._130
+  export const api = { ..._130,
+    ..._131
   };
-  export const protobuf = { ..._131,
-    ..._132,
+  export const protobuf = { ..._132,
     ..._133,
-    ..._134
+    ..._134,
+    ..._135
   };
 }
\ No newline at end of file
diff --git a/proto/dydxprotocol/clob/finalize_block.proto b/proto/dydxprotocol/clob/finalize_block.proto
new file mode 100644
index 0000000000..635e59e5c4
--- /dev/null
+++ b/proto/dydxprotocol/clob/finalize_block.proto
@@ -0,0 +1,16 @@
+syntax = "proto3";
+package dydxprotocol.clob;
+
+import "dydxprotocol/clob/clob_pair.proto";
+
+option go_package = "github.com/dydxprotocol/v4-chain/protocol/x/clob/types";
+
+// ClobStagedFinalizeBlockEvent defines a CLOB event staged during
+// FinalizeBlock.
+message ClobStagedFinalizeBlockEvent {
+  // event is the staged event.
+  oneof event {
+    // create_clob_pair indicates a new CLOB pair creation.
+    ClobPair create_clob_pair = 1;
+  }
+}
diff --git a/protocol/mocks/MemClob.go b/protocol/mocks/MemClob.go
index e5f0aa26bf..9d02b77580 100644
--- a/protocol/mocks/MemClob.go
+++ b/protocol/mocks/MemClob.go
@@ -443,8 +443,21 @@ func (_m *MemClob) InsertZeroFillDeleveragingIntoOperationsQueue(subaccountId su
 }
 
 // MaybeCreateOrderbook provides a mock function with given fields: clobPair
-func (_m *MemClob) MaybeCreateOrderbook(clobPair clobtypes.ClobPair) {
-	_m.Called(clobPair)
+func (_m *MemClob) MaybeCreateOrderbook(clobPair clobtypes.ClobPair) bool {
+	ret := _m.Called(clobPair)
+
+	if len(ret) == 0 {
+		panic("no return value specified for MaybeCreateOrderbook")
+	}
+
+	var r0 bool
+	if rf, ok := ret.Get(0).(func(clobtypes.ClobPair) bool); ok {
+		r0 = rf(clobPair)
+	} else {
+		r0 = ret.Get(0).(bool)
+	}
+
+	return r0
 }
 
 // PlaceOrder provides a mock function with given fields: ctx, order
diff --git a/protocol/testutil/keeper/clob.go b/protocol/testutil/keeper/clob.go
index eddbabd9bf..d5bbe04df6 100644
--- a/protocol/testutil/keeper/clob.go
+++ b/protocol/testutil/keeper/clob.go
@@ -282,7 +282,7 @@ func CreateTestClobPairs(
 	clobPairs []types.ClobPair,
 ) {
 	for _, clobPair := range clobPairs {
-		_, err := clobKeeper.CreatePerpetualClobPair(
+		_, err := clobKeeper.CreatePerpetualClobPairAndMemStructs(
 			ctx,
 			clobPair.Id,
 			clobPair.MustGetPerpetualId(),
@@ -341,7 +341,7 @@ func CreateNClobPair(
 			),
 		).Return()
 
-		_, err := keeper.CreatePerpetualClobPair(
+		_, err := keeper.CreatePerpetualClobPairAndMemStructs(
 			ctx,
 			items[i].Id,
 			clobtest.MustPerpetualId(items[i]),
diff --git a/protocol/x/clob/abci.go b/protocol/x/clob/abci.go
index 9fd5f4a0b5..11ac034253 100644
--- a/protocol/x/clob/abci.go
+++ b/protocol/x/clob/abci.go
@@ -51,6 +51,12 @@ func Precommit(
 	ctx sdk.Context,
 	keeper keeper.Keeper,
 ) {
+	// Process all staged finalize block events, and apply necessary side effects
+	// (e.g. MemClob orderbook creation) that could not be done during FinalizeBlock.
+	// Note: this must be done in `Precommit` which is prior to `PrepareCheckState`, when
+	// MemClob could access the new orderbooks.
+	keeper.ProcessStagedFinalizeBlockEvents(ctx)
+
 	if streamingManager := keeper.GetFullNodeStreamingManager(); !streamingManager.Enabled() {
 		return
 	}
diff --git a/protocol/x/clob/abci_test.go b/protocol/x/clob/abci_test.go
index 661b8bd4e0..1997bc9d9d 100644
--- a/protocol/x/clob/abci_test.go
+++ b/protocol/x/clob/abci_test.go
@@ -530,7 +530,7 @@ func TestEndBlocker_Success(t *testing.T) {
 					),
 				),
 			).Once().Return()
-			_, err = ks.ClobKeeper.CreatePerpetualClobPair(
+			_, err = ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 				ctx,
 				constants.ClobPair_Btc.Id,
 				clobtest.MustPerpetualId(constants.ClobPair_Btc),
@@ -563,7 +563,7 @@ func TestEndBlocker_Success(t *testing.T) {
 					),
 				),
 			).Once().Return()
-			_, err = ks.ClobKeeper.CreatePerpetualClobPair(
+			_, err = ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 				ctx,
 				constants.ClobPair_Eth.Id,
 				clobtest.MustPerpetualId(constants.ClobPair_Eth),
@@ -1170,7 +1170,7 @@ func TestPrepareCheckState(t *testing.T) {
 
 			// Create all CLOBs.
 			for _, clobPair := range tc.clobs {
-				_, err = ks.ClobKeeper.CreatePerpetualClobPair(
+				_, err = ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 					ctx,
 					clobPair.Id,
 					clobtest.MustPerpetualId(clobPair),
diff --git a/protocol/x/clob/genesis.go b/protocol/x/clob/genesis.go
index 3880475451..e0fac3d766 100644
--- a/protocol/x/clob/genesis.go
+++ b/protocol/x/clob/genesis.go
@@ -19,7 +19,7 @@ func InitGenesis(ctx sdk.Context, k *keeper.Keeper, genState types.GenesisState)
 		if err != nil {
 			panic(errorsmod.Wrap(types.ErrInvalidClobPairParameter, err.Error()))
 		}
-		_, err = k.CreatePerpetualClobPair(
+		_, err = k.CreatePerpetualClobPairAndMemStructs(
 			ctx,
 			elem.Id,
 			perpetualId,
diff --git a/protocol/x/clob/keeper/clob_pair.go b/protocol/x/clob/keeper/clob_pair.go
index 3b49d7c246..cc1cd449d6 100644
--- a/protocol/x/clob/keeper/clob_pair.go
+++ b/protocol/x/clob/keeper/clob_pair.go
@@ -15,6 +15,7 @@ import (
 	indexerevents "github.com/dydxprotocol/v4-chain/protocol/indexer/events"
 	"github.com/dydxprotocol/v4-chain/protocol/indexer/indexer_manager"
 	"github.com/dydxprotocol/v4-chain/protocol/lib"
+	dydxlog "github.com/dydxprotocol/v4-chain/protocol/lib/log"
 	"github.com/dydxprotocol/v4-chain/protocol/x/clob/types"
 	satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types"
 )
@@ -33,6 +34,39 @@ func clobPairKey(
 	return lib.Uint32ToKey(id.ToUint32())
 }
 
+// (Same behavior as old `CreatePerpetualClobPair`) creates the
+// objects in state and in-memory.
+// This function should ONLY be used to initialize genesis state
+// or test setups.
+// Regular CLOB logic should use `CreatePerpetualClobPair` instead.
+func (k Keeper) CreatePerpetualClobPairAndMemStructs(
+	ctx sdk.Context,
+	clobPairId uint32,
+	perpetualId uint32,
+	stepSizeBaseQuantums satypes.BaseQuantums,
+	quantumConversionExponent int32,
+	subticksPerTick uint32,
+	status types.ClobPair_Status,
+) (types.ClobPair, error) {
+	clobPair, err := k.createPerpetualClobPair(
+		ctx,
+		clobPairId,
+		perpetualId,
+		stepSizeBaseQuantums,
+		quantumConversionExponent,
+		subticksPerTick,
+		status,
+	)
+	if err != nil {
+		return types.ClobPair{}, err
+	}
+
+	k.MemClob.CreateOrderbook(clobPair)
+	k.SetClobPairIdForPerpetual(clobPair)
+
+	return clobPair, nil
+}
+
 // CreatePerpetualClobPair creates a new perpetual CLOB pair in the store.
 // Additionally, it creates an order book matching the ID of the newly created CLOB pair.
 //
@@ -49,6 +83,38 @@ func (k Keeper) CreatePerpetualClobPair(
 	quantumConversionExponent int32,
 	subticksPerTick uint32,
 	status types.ClobPair_Status,
+) (types.ClobPair, error) {
+	clobPair, err := k.createPerpetualClobPair(
+		ctx,
+		clobPairId,
+		perpetualId,
+		stepSizeBaseQuantums,
+		quantumConversionExponent,
+		subticksPerTick,
+		status,
+	)
+	if err != nil {
+		return types.ClobPair{}, err
+	}
+
+	// Don't stage events for the genesis block.
+	if lib.IsDeliverTxMode(ctx) {
+		if err := k.StageNewClobPairSideEffects(ctx, clobPair); err != nil {
+			return clobPair, err
+		}
+	}
+
+	return clobPair, nil
+}
+
+func (k Keeper) createPerpetualClobPair(
+	ctx sdk.Context,
+	clobPairId uint32,
+	perpetualId uint32,
+	stepSizeBaseQuantums satypes.BaseQuantums,
+	quantumConversionExponent int32,
+	subticksPerTick uint32,
+	status types.ClobPair_Status,
 ) (types.ClobPair, error) {
 	clobPair := types.ClobPair{
 		Metadata: &types.ClobPair_PerpetualClobMetadata{
@@ -69,11 +135,36 @@ func (k Keeper) CreatePerpetualClobPair(
 	// Write the `ClobPair` to state.
 	k.SetClobPair(ctx, clobPair)
 
-	err := k.CreateClobPairStructures(ctx, clobPair)
+	perpetualId, err := clobPair.GetPerpetualId()
 	if err != nil {
-		return clobPair, err
+		panic(err)
+	}
+	perpetual, err := k.perpetualsKeeper.GetPerpetual(ctx, perpetualId)
+	if err != nil {
+		return types.ClobPair{}, err
 	}
 
+	k.GetIndexerEventManager().AddTxnEvent(
+		ctx,
+		indexerevents.SubtypePerpetualMarket,
+		indexerevents.PerpetualMarketEventVersion,
+		indexer_manager.GetBytes(
+			indexerevents.NewPerpetualMarketCreateEvent(
+				perpetualId,
+				clobPair.Id,
+				perpetual.Params.Ticker,
+				perpetual.Params.MarketId,
+				clobPair.Status,
+				clobPair.QuantumConversionExponent,
+				perpetual.Params.AtomicResolution,
+				clobPair.SubticksPerTick,
+				clobPair.StepBaseQuantums,
+				perpetual.Params.LiquidityTier,
+				perpetual.Params.MarketType,
+			),
+		),
+	)
+
 	return clobPair, nil
 }
 
@@ -153,57 +244,36 @@ func (k Keeper) validateClobPair(ctx sdk.Context, clobPair *types.ClobPair) erro
 	return nil
 }
 
-// maybeCreateOrderbook creates a new orderbook in the memclob.
-func (k Keeper) maybeCreateOrderbook(ctx sdk.Context, clobPair types.ClobPair) {
-	// Create the corresponding orderbook in the memclob.
-	k.MemClob.MaybeCreateOrderbook(clobPair)
-}
-
-// createOrderbook creates a new orderbook in the memclob.
-func (k Keeper) createOrderbook(ctx sdk.Context, clobPair types.ClobPair) {
-	// Create the corresponding orderbook in the memclob.
-	k.MemClob.CreateOrderbook(clobPair)
+func (k Keeper) ApplySideEffectsForNewClobPair(
+	ctx sdk.Context,
+	clobPair types.ClobPair,
+) {
+	if created := k.MemClob.MaybeCreateOrderbook(clobPair); !created {
+		dydxlog.ErrorLog(
+			ctx,
+			"ApplySideEffectsForNewClobPair: Orderbook already exists for CLOB pair",
+			"clob_pair", clobPair,
+		)
+		return
+	}
+	k.SetClobPairIdForPerpetual(clobPair)
 }
 
-// CreateClobPair performs all non stateful operations to create a CLOB pair.
-// These include creating the corresponding orderbook in the memclob, the mapping between
-// the CLOB pair and the perpetual and the indexer event.
-// This function returns an error if a value for the ClobPair's id already exists in state.
-func (k Keeper) CreateClobPairStructures(ctx sdk.Context, clobPair types.ClobPair) error {
-	// Create the corresponding orderbook in the memclob.
-	k.createOrderbook(ctx, clobPair)
-
-	// Create the mapping between clob pair and perpetual.
-	k.SetClobPairIdForPerpetual(ctx, clobPair)
-
-	perpetualId, err := clobPair.GetPerpetualId()
-	if err != nil {
-		panic(err)
-	}
-	perpetual, err := k.perpetualsKeeper.GetPerpetual(ctx, perpetualId)
-	if err != nil {
-		return err
-	}
+// StageNewClobPairSideEffects stages a ClobPair creation event, so that any in-memory side effects
+// can happen later when the transaction and block is committed.
+// Note the staged event will be processed only if below are both true:
+//   - The current transaction is committed.
+//   - The current block is agreed upon and committed by consensus.
+func (k Keeper) StageNewClobPairSideEffects(ctx sdk.Context, clobPair types.ClobPair) error {
+	lib.AssertDeliverTxMode(ctx)
 
-	k.GetIndexerEventManager().AddTxnEvent(
+	k.finalizeBlockEventStager.StageFinalizeBlockEvent(
 		ctx,
-		indexerevents.SubtypePerpetualMarket,
-		indexerevents.PerpetualMarketEventVersion,
-		indexer_manager.GetBytes(
-			indexerevents.NewPerpetualMarketCreateEvent(
-				perpetualId,
-				clobPair.Id,
-				perpetual.Params.Ticker,
-				perpetual.Params.MarketId,
-				clobPair.Status,
-				clobPair.QuantumConversionExponent,
-				perpetual.Params.AtomicResolution,
-				clobPair.SubticksPerTick,
-				clobPair.StepBaseQuantums,
-				perpetual.Params.LiquidityTier,
-				perpetual.Params.MarketType,
-			),
-		),
+		&types.ClobStagedFinalizeBlockEvent{
+			Event: &types.ClobStagedFinalizeBlockEvent_CreateClobPair{
+				CreateClobPair: &clobPair,
+			},
+		},
 	)
 
 	return nil
@@ -221,8 +291,7 @@ func (k Keeper) InitMemClobOrderbooks(ctx sdk.Context) {
 	clobPairs := k.GetAllClobPairs(ctx)
 	for _, clobPair := range clobPairs {
 		// Create the corresponding orderbook in the memclob.
-		k.maybeCreateOrderbook(
-			ctx,
+		k.MemClob.MaybeCreateOrderbook(
 			clobPair,
 		)
 	}
@@ -234,14 +303,13 @@ func (k Keeper) HydrateClobPairAndPerpetualMapping(ctx sdk.Context) {
 	for _, clobPair := range clobPairs {
 		// Create the corresponding mapping between clob pair and perpetual.
 		k.SetClobPairIdForPerpetual(
-			ctx,
 			clobPair,
 		)
 	}
 }
 
 // SetClobPairIdForPerpetual sets the mapping between clob pair and perpetual.
-func (k Keeper) SetClobPairIdForPerpetual(ctx sdk.Context, clobPair types.ClobPair) {
+func (k Keeper) SetClobPairIdForPerpetual(clobPair types.ClobPair) {
 	// If this `ClobPair` is for a perpetual, add the `clobPairId` to the list of CLOB pair IDs
 	// that facilitate trading of this perpetual.
 	if perpetualClobMetadata := clobPair.GetPerpetualClobMetadata(); perpetualClobMetadata != nil {
diff --git a/protocol/x/clob/keeper/clob_pair_test.go b/protocol/x/clob/keeper/clob_pair_test.go
index 76a56e0794..cff3f32be5 100644
--- a/protocol/x/clob/keeper/clob_pair_test.go
+++ b/protocol/x/clob/keeper/clob_pair_test.go
@@ -35,7 +35,7 @@ import (
 // Prevent strconv unused error
 var _ = strconv.IntSize
 
-func TestCreatePerpetualClobPair_MultiplePerpetual(t *testing.T) {
+func TestCreatePerpetualClobPairAndMemStructs_MultiplePerpetual(t *testing.T) {
 	memClob := memclob.NewMemClobPriceTimePriority(false)
 	mockIndexerEventManager := &mocks.IndexerEventManager{}
 	ks := keepertest.NewClobKeepersTestContext(t, memClob, &mocks.BankKeeper{}, mockIndexerEventManager)
@@ -71,7 +71,7 @@ func TestCreatePerpetualClobPair_MultiplePerpetual(t *testing.T) {
 			),
 		).Once().Return()
 		//nolint: errcheck
-		ks.ClobKeeper.CreatePerpetualClobPair(
+		ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 			ks.Ctx,
 			clobPair.Id,
 			clobtest.MustPerpetualId(clobPair),
@@ -92,7 +92,7 @@ func TestCreatePerpetualClobPair_MultiplePerpetual(t *testing.T) {
 	)
 }
 
-func TestCreatePerpetualClobPair_FailsWithPerpetualAssociatedWithExistingClobPair(t *testing.T) {
+func TestCreatePerpetualClobPairAndMemStructs_FailsWithPerpetualAssociatedWithExistingClobPair(t *testing.T) {
 	memClob := memclob.NewMemClobPriceTimePriority(false)
 	// Set up mock indexer event manager that accepts anything.
 	mockIndexerEventManager := &mocks.IndexerEventManager{}
@@ -130,7 +130,7 @@ func TestCreatePerpetualClobPair_FailsWithPerpetualAssociatedWithExistingClobPai
 
 	// Create test clob pair id 1, associated with perpetual id 0.
 	newClobPair := clobtest.GenerateClobPair(clobtest.WithId(1), clobtest.WithPerpetualId(0))
-	_, err := ks.ClobKeeper.CreatePerpetualClobPair(
+	_, err := ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 		ks.Ctx,
 		newClobPair.Id,
 		newClobPair.MustGetPerpetualId(),
@@ -146,7 +146,7 @@ func TestCreatePerpetualClobPair_FailsWithPerpetualAssociatedWithExistingClobPai
 	)
 }
 
-func TestCreatePerpetualClobPair_FailsWithDuplicateClobPairId(t *testing.T) {
+func TestCreatePerpetualClobPairAndMemStructs_FailsWithDuplicateClobPairId(t *testing.T) {
 	memClob := memclob.NewMemClobPriceTimePriority(false)
 	mockIndexerEventManager := &mocks.IndexerEventManager{}
 	ks := keepertest.NewClobKeepersTestContext(
@@ -194,7 +194,7 @@ func TestCreatePerpetualClobPair_FailsWithDuplicateClobPairId(t *testing.T) {
 		),
 	).Once().Return()
 
-	_, err = ks.ClobKeeper.CreatePerpetualClobPair(
+	_, err = ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 		ks.Ctx,
 		clobPair.Id,
 		clobtest.MustPerpetualId(clobPair),
@@ -211,7 +211,7 @@ func TestCreatePerpetualClobPair_FailsWithDuplicateClobPairId(t *testing.T) {
 	)
 }
 
-func TestCreatePerpetualClobPair(t *testing.T) {
+func TestCreatePerpetualClobPairAndMemStructs(t *testing.T) {
 	tests := map[string]struct {
 		// CLOB pair.
 		clobPair types.ClobPair
@@ -289,7 +289,7 @@ func TestCreatePerpetualClobPair(t *testing.T) {
 			}
 
 			// Perform the method under test.
-			createdClobPair, actualErr := ks.ClobKeeper.CreatePerpetualClobPair(
+			createdClobPair, actualErr := ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 				ks.Ctx,
 				tc.clobPair.Id,
 				clobtest.MustPerpetualId(tc.clobPair),
@@ -442,7 +442,7 @@ func TestCreateMultipleClobPairs(t *testing.T) {
 					).Return()
 				}
 
-				_, err := ks.ClobKeeper.CreatePerpetualClobPair(
+				_, err := ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 					ks.Ctx,
 					make.clobPair.Id,
 					clobtest.MustPerpetualId(make.clobPair),
@@ -635,7 +635,7 @@ func TestUpdateClobPair_FinalSettlement(t *testing.T) {
 		),
 	).Once().Return()
 
-	_, err := ks.ClobKeeper.CreatePerpetualClobPair(
+	_, err := ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 		ks.Ctx,
 		clobPair.Id,
 		clobtest.MustPerpetualId(clobPair),
@@ -744,7 +744,7 @@ func TestUpdateClobPair(t *testing.T) {
 					),
 				).Once().Return()
 
-				_, err := ks.ClobKeeper.CreatePerpetualClobPair(
+				_, err := ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 					ks.Ctx,
 					clobPair.Id,
 					clobtest.MustPerpetualId(clobPair),
@@ -802,7 +802,7 @@ func TestUpdateClobPair(t *testing.T) {
 					),
 				).Once().Return()
 
-				_, err := ks.ClobKeeper.CreatePerpetualClobPair(
+				_, err := ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 					ks.Ctx,
 					clobPair.Id,
 					clobtest.MustPerpetualId(clobPair),
@@ -840,7 +840,7 @@ func TestUpdateClobPair(t *testing.T) {
 					),
 				).Once().Return()
 
-				_, err := ks.ClobKeeper.CreatePerpetualClobPair(
+				_, err := ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 					ks.Ctx,
 					clobPair.Id,
 					clobtest.MustPerpetualId(clobPair),
@@ -923,7 +923,7 @@ func TestGetAllClobPairs_Sorted(t *testing.T) {
 	).Return().Times(len(clobPairs))
 
 	for _, clobPair := range clobPairs {
-		_, err := ks.ClobKeeper.CreatePerpetualClobPair(
+		_, err := ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 			ks.Ctx,
 			clobPair.Id,
 			clobtest.MustPerpetualId(clobPair),
@@ -1048,7 +1048,7 @@ func TestIsPerpetualClobPairActive(t *testing.T) {
 			perpetuals.InitGenesis(ks.Ctx, *ks.PerpetualsKeeper, constants.Perpetuals_DefaultGenesisState)
 
 			if tc.clobPair != nil {
-				// allows us to circumvent CreatePerpetualClobPair and write unsupported statuses to state to
+				// allows us to circumvent CreatePerpetualClobPairAndMemStructs and write unsupported statuses to state to
 				// test this function with unsupported statuses.
 				cdc := codec.NewProtoCodec(module.InterfaceRegistry)
 				store := prefix.NewStore(ks.Ctx.KVStore(ks.StoreKey), []byte(types.ClobPairKeyPrefix))
@@ -1183,7 +1183,7 @@ func TestAcquireNextClobPairID(t *testing.T) {
 		clobtest.WithPerpetualId(perp.Params.Id),
 	)
 
-	_, err = ks.ClobKeeper.CreatePerpetualClobPair(
+	_, err = ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 		ks.Ctx,
 		clobPair.Id,
 		perp.GetId(),
diff --git a/protocol/x/clob/keeper/deleveraging_test.go b/protocol/x/clob/keeper/deleveraging_test.go
index 6b0084e064..d3e6cb1ff0 100644
--- a/protocol/x/clob/keeper/deleveraging_test.go
+++ b/protocol/x/clob/keeper/deleveraging_test.go
@@ -419,7 +419,7 @@ func TestCanDeleverageSubaccount(t *testing.T) {
 					),
 				).Once().Return()
 
-				_, err = ks.ClobKeeper.CreatePerpetualClobPair(
+				_, err = ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 					ks.Ctx,
 					clobPair.Id,
 					clobPair.MustGetPerpetualId(),
@@ -816,7 +816,7 @@ func TestOffsetSubaccountPerpetualPosition(t *testing.T) {
 					),
 				).Once().Return()
 
-				_, err = ks.ClobKeeper.CreatePerpetualClobPair(
+				_, err = ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 					ks.Ctx,
 					clobPair.Id,
 					clobPair.MustGetPerpetualId(),
diff --git a/protocol/x/clob/keeper/get_price_premium_test.go b/protocol/x/clob/keeper/get_price_premium_test.go
index a54f805054..f75e1c8c15 100644
--- a/protocol/x/clob/keeper/get_price_premium_test.go
+++ b/protocol/x/clob/keeper/get_price_premium_test.go
@@ -188,7 +188,7 @@ func TestGetPricePremiumForPerpetual(t *testing.T) {
 					),
 				),
 			).Return()
-			_, err := ks.ClobKeeper.CreatePerpetualClobPair(
+			_, err := ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 				ks.Ctx,
 				tc.args.clobPair.Id,
 				clobtest.MustPerpetualId(tc.args.clobPair),
diff --git a/protocol/x/clob/keeper/keeper.go b/protocol/x/clob/keeper/keeper.go
index f49eb61271..1ed0e9c934 100644
--- a/protocol/x/clob/keeper/keeper.go
+++ b/protocol/x/clob/keeper/keeper.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"sync/atomic"
 
+	"github.com/dydxprotocol/v4-chain/protocol/finalizeblock"
 	satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types"
 
 	"cosmossdk.io/log"
@@ -15,6 +16,7 @@ import (
 	liquidationtypes "github.com/dydxprotocol/v4-chain/protocol/daemons/server/types/liquidations"
 	"github.com/dydxprotocol/v4-chain/protocol/indexer/indexer_manager"
 	"github.com/dydxprotocol/v4-chain/protocol/lib"
+	dydxlog "github.com/dydxprotocol/v4-chain/protocol/lib/log"
 	"github.com/dydxprotocol/v4-chain/protocol/lib/metrics"
 	streamingtypes "github.com/dydxprotocol/v4-chain/protocol/streaming/types"
 	flags "github.com/dydxprotocol/v4-chain/protocol/x/clob/flags"
@@ -45,8 +47,9 @@ type (
 		affiliatesKeeper  types.AffiliatesKeeper
 		revshareKeeper    types.RevShareKeeper
 
-		indexerEventManager indexer_manager.IndexerEventManager
-		streamingManager    streamingtypes.FullNodeStreamingManager
+		indexerEventManager      indexer_manager.IndexerEventManager
+		streamingManager         streamingtypes.FullNodeStreamingManager
+		finalizeBlockEventStager finalizeblock.EventStager[*types.ClobStagedFinalizeBlockEvent]
 
 		initialized         *atomic.Bool
 		memStoreInitialized *atomic.Bool
@@ -75,7 +78,7 @@ func NewKeeper(
 	cdc codec.BinaryCodec,
 	storeKey storetypes.StoreKey,
 	memKey storetypes.StoreKey,
-	liquidationsStoreKey storetypes.StoreKey,
+	transientStoreKey storetypes.StoreKey,
 	authorities []string,
 	memClob types.MemClob,
 	subaccountsKeeper types.SubaccountsKeeper,
@@ -100,7 +103,7 @@ func NewKeeper(
 		cdc:                     cdc,
 		storeKey:                storeKey,
 		memKey:                  memKey,
-		transientStoreKey:       liquidationsStoreKey,
+		transientStoreKey:       transientStoreKey,
 		authorities:             lib.UniqueSliceToSet(authorities),
 		MemClob:                 memClob,
 		PerpetualIdToClobPairId: make(map[uint32][]types.ClobPairId),
@@ -128,6 +131,12 @@ func NewKeeper(
 		placeCancelOrderRateLimiter: placeCancelOrderRateLimiter,
 		DaemonLiquidationInfo:       daemonLiquidationInfo,
 		revshareKeeper:              revshareKeeper,
+		finalizeBlockEventStager: finalizeblock.NewEventStager[*types.ClobStagedFinalizeBlockEvent](
+			transientStoreKey,
+			cdc,
+			types.StagedEventsCountKey,
+			types.StagedEventsKeyPrefix,
+		),
 	}
 
 	// Provide the keeper to the MemClob.
@@ -196,6 +205,43 @@ func (k Keeper) Initialize(ctx sdk.Context) {
 	k.HydrateClobPairAndPerpetualMapping(checkCtx)
 }
 
+func (k Keeper) GetStagedClobFinalizeBlockEvents(ctx sdk.Context) []*types.ClobStagedFinalizeBlockEvent {
+	return k.finalizeBlockEventStager.GetStagedFinalizeBlockEvents(
+		ctx,
+		func() *types.ClobStagedFinalizeBlockEvent {
+			return &types.ClobStagedFinalizeBlockEvent{}
+		},
+	)
+}
+
+func (k Keeper) ProcessStagedFinalizeBlockEvents(ctx sdk.Context) {
+	stagedEvents := k.GetStagedClobFinalizeBlockEvents(ctx)
+	for _, stagedEvent := range stagedEvents {
+		if stagedEvent == nil {
+			// We don't ever expect this. However, should not panic since we are in Precommit.
+			dydxlog.ErrorLog(
+				ctx,
+				"got nil ClobStagedFinalizeBlockEvent, skipping",
+				"staged_events",
+				stagedEvents,
+			)
+			continue
+		}
+
+		switch event := stagedEvent.Event.(type) {
+		case *types.ClobStagedFinalizeBlockEvent_CreateClobPair:
+			k.ApplySideEffectsForNewClobPair(ctx, *event.CreateClobPair)
+		default:
+			dydxlog.ErrorLog(
+				ctx,
+				"got unknown ClobStagedFinalizeBlockEvent",
+				"event",
+				event,
+			)
+		}
+	}
+}
+
 // InitMemStore initializes the memstore of the `clob` keeper.
 // This is called during app initialization in `app.go`, before any ABCI calls are received.
 func (k Keeper) InitMemStore(ctx sdk.Context) {
diff --git a/protocol/x/clob/keeper/liquidations_test.go b/protocol/x/clob/keeper/liquidations_test.go
index 89809421b6..1e17e1e24a 100644
--- a/protocol/x/clob/keeper/liquidations_test.go
+++ b/protocol/x/clob/keeper/liquidations_test.go
@@ -332,7 +332,7 @@ func TestPlacePerpetualLiquidation(t *testing.T) {
 
 			// Create all CLOBs.
 			for _, clobPair := range tc.clobs {
-				_, err = ks.ClobKeeper.CreatePerpetualClobPair(
+				_, err = ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 					ctx,
 					clobPair.Id,
 					clobtest.MustPerpetualId(clobPair),
@@ -438,7 +438,7 @@ func TestPlacePerpetualLiquidation_validateLiquidationAgainstClobPairStatus(t *t
 			}
 
 			clobPair := constants.ClobPair_Btc
-			_, err = ks.ClobKeeper.CreatePerpetualClobPair(
+			_, err = ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 				ctx,
 				clobPair.Id,
 				clobtest.MustPerpetualId(clobPair),
@@ -1244,7 +1244,7 @@ func TestPlacePerpetualLiquidation_PreexistingLiquidation(t *testing.T) {
 					),
 				),
 			).Once().Return()
-			_, err = ks.ClobKeeper.CreatePerpetualClobPair(
+			_, err = ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 				ctx,
 				constants.ClobPair_Btc.Id,
 				clobtest.MustPerpetualId(constants.ClobPair_Btc),
@@ -1274,7 +1274,7 @@ func TestPlacePerpetualLiquidation_PreexistingLiquidation(t *testing.T) {
 					),
 				),
 			).Once().Return()
-			_, err = ks.ClobKeeper.CreatePerpetualClobPair(
+			_, err = ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 				ctx,
 				constants.ClobPair_Eth.Id,
 				clobtest.MustPerpetualId(constants.ClobPair_Eth),
@@ -2175,7 +2175,7 @@ func TestPlacePerpetualLiquidation_Deleveraging(t *testing.T) {
 						),
 					),
 				).Once().Return()
-				_, err = ks.ClobKeeper.CreatePerpetualClobPair(
+				_, err = ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 					ctx,
 					clobPair.Id,
 					clobtest.MustPerpetualId(clobPair),
@@ -2295,7 +2295,7 @@ func TestPlacePerpetualLiquidation_SendOffchainMessages(t *testing.T) {
 			),
 		),
 	).Once().Return()
-	_, err := ks.ClobKeeper.CreatePerpetualClobPair(
+	_, err := ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 		ctx,
 		constants.ClobPair_Btc.Id,
 		clobtest.MustPerpetualId(constants.ClobPair_Btc),
@@ -3834,7 +3834,7 @@ func TestGetLiquidationInsuranceFundDelta(t *testing.T) {
 					),
 				),
 			).Once().Return()
-			_, err := ks.ClobKeeper.CreatePerpetualClobPair(
+			_, err := ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 				ks.Ctx,
 				constants.ClobPair_Btc.Id,
 				clobtest.MustPerpetualId(constants.ClobPair_Btc),
@@ -4582,7 +4582,7 @@ func TestGetPerpetualPositionToLiquidate(t *testing.T) {
 						),
 					),
 				).Once().Return()
-				_, err := ks.ClobKeeper.CreatePerpetualClobPair(
+				_, err := ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 					ks.Ctx,
 					clobPair.Id,
 					clobtest.MustPerpetualId(clobPair),
@@ -4818,7 +4818,7 @@ func TestMaybeGetLiquidationOrder(t *testing.T) {
 
 			// Create all CLOBs.
 			for _, clobPair := range tc.clobs {
-				_, err = ks.ClobKeeper.CreatePerpetualClobPair(
+				_, err = ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 					ctx,
 					clobPair.Id,
 					clobtest.MustPerpetualId(clobPair),
@@ -5163,7 +5163,7 @@ func TestGetMaxAndMinPositionNotionalLiquidatable(t *testing.T) {
 					),
 				),
 			).Once().Return()
-			_, err = ks.ClobKeeper.CreatePerpetualClobPair(
+			_, err = ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 				ks.Ctx,
 				constants.ClobPair_Btc.Id,
 				clobtest.MustPerpetualId(constants.ClobPair_Btc),
@@ -5318,7 +5318,7 @@ func TestSortLiquidationOrders(t *testing.T) {
 					),
 				),
 			).Once().Return()
-			_, err = ks.ClobKeeper.CreatePerpetualClobPair(
+			_, err = ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 				ks.Ctx,
 				constants.ClobPair_Btc.Id,
 				clobtest.MustPerpetualId(constants.ClobPair_Btc),
diff --git a/protocol/x/clob/keeper/mev_test.go b/protocol/x/clob/keeper/mev_test.go
index 6f0cd44d16..97921ebc53 100644
--- a/protocol/x/clob/keeper/mev_test.go
+++ b/protocol/x/clob/keeper/mev_test.go
@@ -904,7 +904,7 @@ func TestRecordMevMetrics(t *testing.T) {
 
 			// Create all CLOBs.
 			for _, clobPair := range tc.clobPairs {
-				_, err := ks.ClobKeeper.CreatePerpetualClobPair(
+				_, err := ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 					ctx,
 					clobPair.Id,
 					clobtest.MustPerpetualId(clobPair),
@@ -1289,7 +1289,7 @@ func TestGetMidPrices(t *testing.T) {
 
 			// Create all CLOBs.
 			for _, clobPair := range tc.clobPairs {
-				_, err = ks.ClobKeeper.CreatePerpetualClobPair(
+				_, err = ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 					ctx,
 					clobPair.Id,
 					clobtest.MustPerpetualId(clobPair),
diff --git a/protocol/x/clob/keeper/msg_server_place_order_test.go b/protocol/x/clob/keeper/msg_server_place_order_test.go
index 5754c06b07..f7f0785c41 100644
--- a/protocol/x/clob/keeper/msg_server_place_order_test.go
+++ b/protocol/x/clob/keeper/msg_server_place_order_test.go
@@ -186,7 +186,7 @@ func TestPlaceOrder_Error(t *testing.T) {
 					),
 				),
 			).Once().Return()
-			_, err = ks.ClobKeeper.CreatePerpetualClobPair(
+			_, err = ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 				ks.Ctx,
 				clobPair.Id,
 				clobtest.MustPerpetualId(clobPair),
@@ -337,7 +337,7 @@ func TestPlaceOrder_Success(t *testing.T) {
 					),
 				),
 			).Once().Return()
-			_, err = ks.ClobKeeper.CreatePerpetualClobPair(
+			_, err = ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 				ctx,
 				clobPair.Id,
 				clobtest.MustPerpetualId(clobPair),
diff --git a/protocol/x/clob/keeper/orders_test.go b/protocol/x/clob/keeper/orders_test.go
index b7138d71ab..a60dc9f12b 100644
--- a/protocol/x/clob/keeper/orders_test.go
+++ b/protocol/x/clob/keeper/orders_test.go
@@ -590,7 +590,7 @@ func TestPlaceShortTermOrder(t *testing.T) {
 
 			// Create all CLOBs.
 			for _, clobPair := range tc.clobs {
-				_, err = ks.ClobKeeper.CreatePerpetualClobPair(
+				_, err = ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 					ctx,
 					clobPair.Id,
 					clobtest.MustPerpetualId(clobPair),
@@ -834,7 +834,7 @@ func TestAddPreexistingStatefulOrder(t *testing.T) {
 
 			// Create all CLOBs.
 			for _, clobPair := range tc.clobs {
-				_, err := ks.ClobKeeper.CreatePerpetualClobPair(
+				_, err := ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 					ctx,
 					clobPair.Id,
 					clobPair.GetPerpetualClobMetadata().PerpetualId,
@@ -965,7 +965,7 @@ func TestPlaceOrder_SendOffchainMessages(t *testing.T) {
 			),
 		),
 	).Once().Return()
-	_, err := ks.ClobKeeper.CreatePerpetualClobPair(
+	_, err := ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 		ctx,
 		constants.ClobPair_Btc.Id,
 		clobtest.MustPerpetualId(constants.ClobPair_Btc),
@@ -1022,7 +1022,7 @@ func TestPerformStatefulOrderValidation_PreExistingStatefulOrder(t *testing.T) {
 			),
 		),
 	).Once().Return()
-	_, err := ks.ClobKeeper.CreatePerpetualClobPair(
+	_, err := ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 		ks.Ctx,
 		constants.ClobPair_Btc.Id,
 		clobtest.MustPerpetualId(constants.ClobPair_Btc),
@@ -1779,7 +1779,7 @@ func TestGetStatePosition_Success(t *testing.T) {
 						),
 					),
 				).Once().Return()
-				_, err := ks.ClobKeeper.CreatePerpetualClobPair(
+				_, err := ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 					ks.Ctx,
 					cp.Id,
 					perpetualId,
@@ -1836,7 +1836,7 @@ func TestGetStatePosition_PanicsOnInvalidClob(t *testing.T) {
 
 // 	// Create CLOB pair.
 // 	clobPair := constants.ClobPair_Asset
-// 	clobKeeper.CreatePerpetualClobPair(
+// 	clobKeeper.CreatePerpetualClobPairAndMemStructs(
 // 		ctx,
 // 		clobPair.Metadata.(*types.ClobPair_PerpetualClobMetadata),
 // 		satypes.BaseQuantums(clobPair.StepBaseQuantums),
@@ -1997,7 +1997,7 @@ func TestInitStatefulOrders(t *testing.T) {
 					),
 				),
 			).Once().Return()
-			_, err := ks.ClobKeeper.CreatePerpetualClobPair(
+			_, err := ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 				ks.Ctx,
 				constants.ClobPair_Btc.Id,
 				clobtest.MustPerpetualId(constants.ClobPair_Btc),
@@ -2167,7 +2167,7 @@ func TestPlaceStatefulOrdersFromLastBlock(t *testing.T) {
 
 			// Create CLOB pair.
 			memClob.On("CreateOrderbook", constants.ClobPair_Btc).Return()
-			_, err := ks.ClobKeeper.CreatePerpetualClobPair(
+			_, err := ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 				ctx,
 				constants.ClobPair_Btc.Id,
 				clobtest.MustPerpetualId(constants.ClobPair_Btc),
@@ -2297,7 +2297,7 @@ func TestPlaceConditionalOrdersTriggeredInLastBlock(t *testing.T) {
 
 			// Create CLOB pair.
 			memClob.On("CreateOrderbook", constants.ClobPair_Btc).Return()
-			_, err := ks.ClobKeeper.CreatePerpetualClobPair(
+			_, err := ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 				ctx,
 				constants.ClobPair_Btc.Id,
 				clobtest.MustPerpetualId(constants.ClobPair_Btc),
diff --git a/protocol/x/clob/keeper/process_operations_test.go b/protocol/x/clob/keeper/process_operations_test.go
index 178c737dd6..7ea9f4cca4 100644
--- a/protocol/x/clob/keeper/process_operations_test.go
+++ b/protocol/x/clob/keeper/process_operations_test.go
@@ -2461,7 +2461,7 @@ func setupProcessProposerOperationsTestCase(
 			).Once().Return()
 		}
 
-		_, err = ks.ClobKeeper.CreatePerpetualClobPair(
+		_, err = ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 			ctx,
 			clobPair.Id,
 			clobtest.MustPerpetualId(clobPair),
diff --git a/protocol/x/clob/memclob/memclob.go b/protocol/x/clob/memclob/memclob.go
index db541650c3..50ead5254c 100644
--- a/protocol/x/clob/memclob/memclob.go
+++ b/protocol/x/clob/memclob/memclob.go
@@ -158,11 +158,12 @@ func (m *MemClobPriceTimePriority) CancelOrder(
 // MaybeCreateOrderbook is used for updating memclob internal data structures to mark an orderbook as created.
 func (m *MemClobPriceTimePriority) MaybeCreateOrderbook(
 	clobPair types.ClobPair,
-) {
+) (created bool) {
 	if _, exists := m.orderbooks[clobPair.GetClobPairId()]; exists {
-		return
+		return false
 	}
 	m.CreateOrderbook(clobPair)
+	return true
 }
 
 // CreateOrderbook is used for updating memclob internal data structures to mark an orderbook as created.
diff --git a/protocol/x/clob/memclob/memclob_get_impact_price_subticks_test.go b/protocol/x/clob/memclob/memclob_get_impact_price_subticks_test.go
index 413c6fcbcb..d59d7c1f60 100644
--- a/protocol/x/clob/memclob/memclob_get_impact_price_subticks_test.go
+++ b/protocol/x/clob/memclob/memclob_get_impact_price_subticks_test.go
@@ -338,7 +338,7 @@ func initializeMemclobForTest(
 	}
 
 	// Create CLOB
-	_, err = ks.ClobKeeper.CreatePerpetualClobPair(
+	_, err = ks.ClobKeeper.CreatePerpetualClobPairAndMemStructs(
 		ctx,
 		clobPair.Id,
 		clobtest.MustPerpetualId(clobPair),
diff --git a/protocol/x/clob/types/finalize_block.pb.go b/protocol/x/clob/types/finalize_block.pb.go
new file mode 100644
index 0000000000..11610e4a25
--- /dev/null
+++ b/protocol/x/clob/types/finalize_block.pb.go
@@ -0,0 +1,390 @@
+// Code generated by protoc-gen-gogo. DO NOT EDIT.
+// source: dydxprotocol/clob/finalize_block.proto
+
+package types
+
+import (
+	fmt "fmt"
+	proto "github.com/cosmos/gogoproto/proto"
+	io "io"
+	math "math"
+	math_bits "math/bits"
+)
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the proto package it is being compiled against.
+// A compilation error at this line likely means your copy of the
+// proto package needs to be updated.
+const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
+
+// ClobStagedFinalizeBlockEvent defines a CLOB event staged during
+// FinalizeBlock.
+type ClobStagedFinalizeBlockEvent struct {
+	// event is the staged event.
+	//
+	// Types that are valid to be assigned to Event:
+	//	*ClobStagedFinalizeBlockEvent_CreateClobPair
+	Event isClobStagedFinalizeBlockEvent_Event `protobuf_oneof:"event"`
+}
+
+func (m *ClobStagedFinalizeBlockEvent) Reset()         { *m = ClobStagedFinalizeBlockEvent{} }
+func (m *ClobStagedFinalizeBlockEvent) String() string { return proto.CompactTextString(m) }
+func (*ClobStagedFinalizeBlockEvent) ProtoMessage()    {}
+func (*ClobStagedFinalizeBlockEvent) Descriptor() ([]byte, []int) {
+	return fileDescriptor_ce1d49660993e938, []int{0}
+}
+func (m *ClobStagedFinalizeBlockEvent) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *ClobStagedFinalizeBlockEvent) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_ClobStagedFinalizeBlockEvent.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 *ClobStagedFinalizeBlockEvent) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_ClobStagedFinalizeBlockEvent.Merge(m, src)
+}
+func (m *ClobStagedFinalizeBlockEvent) XXX_Size() int {
+	return m.Size()
+}
+func (m *ClobStagedFinalizeBlockEvent) XXX_DiscardUnknown() {
+	xxx_messageInfo_ClobStagedFinalizeBlockEvent.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_ClobStagedFinalizeBlockEvent proto.InternalMessageInfo
+
+type isClobStagedFinalizeBlockEvent_Event interface {
+	isClobStagedFinalizeBlockEvent_Event()
+	MarshalTo([]byte) (int, error)
+	Size() int
+}
+
+type ClobStagedFinalizeBlockEvent_CreateClobPair struct {
+	CreateClobPair *ClobPair `protobuf:"bytes,1,opt,name=create_clob_pair,json=createClobPair,proto3,oneof" json:"create_clob_pair,omitempty"`
+}
+
+func (*ClobStagedFinalizeBlockEvent_CreateClobPair) isClobStagedFinalizeBlockEvent_Event() {}
+
+func (m *ClobStagedFinalizeBlockEvent) GetEvent() isClobStagedFinalizeBlockEvent_Event {
+	if m != nil {
+		return m.Event
+	}
+	return nil
+}
+
+func (m *ClobStagedFinalizeBlockEvent) GetCreateClobPair() *ClobPair {
+	if x, ok := m.GetEvent().(*ClobStagedFinalizeBlockEvent_CreateClobPair); ok {
+		return x.CreateClobPair
+	}
+	return nil
+}
+
+// XXX_OneofWrappers is for the internal use of the proto package.
+func (*ClobStagedFinalizeBlockEvent) XXX_OneofWrappers() []interface{} {
+	return []interface{}{
+		(*ClobStagedFinalizeBlockEvent_CreateClobPair)(nil),
+	}
+}
+
+func init() {
+	proto.RegisterType((*ClobStagedFinalizeBlockEvent)(nil), "dydxprotocol.clob.ClobStagedFinalizeBlockEvent")
+}
+
+func init() {
+	proto.RegisterFile("dydxprotocol/clob/finalize_block.proto", fileDescriptor_ce1d49660993e938)
+}
+
+var fileDescriptor_ce1d49660993e938 = []byte{
+	// 219 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0x4b, 0xa9, 0x4c, 0xa9,
+	0x28, 0x28, 0xca, 0x2f, 0xc9, 0x4f, 0xce, 0xcf, 0xd1, 0x4f, 0xce, 0xc9, 0x4f, 0xd2, 0x4f, 0xcb,
+	0xcc, 0x4b, 0xcc, 0xc9, 0xac, 0x4a, 0x8d, 0x4f, 0xca, 0xc9, 0x4f, 0xce, 0xd6, 0x03, 0x4b, 0x0a,
+	0x09, 0x22, 0xab, 0xd3, 0x03, 0xa9, 0x93, 0x52, 0xc4, 0xd4, 0x0a, 0x22, 0xe2, 0x0b, 0x12, 0x33,
+	0x8b, 0x20, 0xba, 0x94, 0x0a, 0xb8, 0x64, 0x9c, 0x73, 0xf2, 0x93, 0x82, 0x4b, 0x12, 0xd3, 0x53,
+	0x53, 0xdc, 0xa0, 0xe6, 0x3a, 0x81, 0x8c, 0x75, 0x2d, 0x4b, 0xcd, 0x2b, 0x11, 0x72, 0xe7, 0x12,
+	0x48, 0x2e, 0x4a, 0x4d, 0x2c, 0x49, 0x8d, 0x87, 0xeb, 0x94, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x36,
+	0x92, 0xd6, 0xc3, 0xb0, 0x50, 0x0f, 0x64, 0x54, 0x40, 0x62, 0x66, 0x91, 0x07, 0x43, 0x10, 0x1f,
+	0x44, 0x1b, 0x4c, 0xc4, 0x89, 0x9d, 0x8b, 0x35, 0x15, 0x64, 0xa2, 0x53, 0xc0, 0x89, 0x47, 0x72,
+	0x8c, 0x17, 0x1e, 0xc9, 0x31, 0x3e, 0x78, 0x24, 0xc7, 0x38, 0xe1, 0xb1, 0x1c, 0xc3, 0x85, 0xc7,
+	0x72, 0x0c, 0x37, 0x1e, 0xcb, 0x31, 0x44, 0x99, 0xa5, 0x67, 0x96, 0x64, 0x94, 0x26, 0xe9, 0x25,
+	0xe7, 0xe7, 0xea, 0xa3, 0xb8, 0xbc, 0xcc, 0x44, 0x37, 0x39, 0x23, 0x31, 0x33, 0x4f, 0x1f, 0x2e,
+	0x52, 0x01, 0xf1, 0x4d, 0x49, 0x65, 0x41, 0x6a, 0x71, 0x12, 0x1b, 0x58, 0xd8, 0x18, 0x10, 0x00,
+	0x00, 0xff, 0xff, 0xc0, 0x74, 0xef, 0x17, 0x2a, 0x01, 0x00, 0x00,
+}
+
+func (m *ClobStagedFinalizeBlockEvent) 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 *ClobStagedFinalizeBlockEvent) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *ClobStagedFinalizeBlockEvent) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if m.Event != nil {
+		{
+			size := m.Event.Size()
+			i -= size
+			if _, err := m.Event.MarshalTo(dAtA[i:]); err != nil {
+				return 0, err
+			}
+		}
+	}
+	return len(dAtA) - i, nil
+}
+
+func (m *ClobStagedFinalizeBlockEvent_CreateClobPair) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *ClobStagedFinalizeBlockEvent_CreateClobPair) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	if m.CreateClobPair != nil {
+		{
+			size, err := m.CreateClobPair.MarshalToSizedBuffer(dAtA[:i])
+			if err != nil {
+				return 0, err
+			}
+			i -= size
+			i = encodeVarintFinalizeBlock(dAtA, i, uint64(size))
+		}
+		i--
+		dAtA[i] = 0xa
+	}
+	return len(dAtA) - i, nil
+}
+func encodeVarintFinalizeBlock(dAtA []byte, offset int, v uint64) int {
+	offset -= sovFinalizeBlock(v)
+	base := offset
+	for v >= 1<<7 {
+		dAtA[offset] = uint8(v&0x7f | 0x80)
+		v >>= 7
+		offset++
+	}
+	dAtA[offset] = uint8(v)
+	return base
+}
+func (m *ClobStagedFinalizeBlockEvent) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	if m.Event != nil {
+		n += m.Event.Size()
+	}
+	return n
+}
+
+func (m *ClobStagedFinalizeBlockEvent_CreateClobPair) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	if m.CreateClobPair != nil {
+		l = m.CreateClobPair.Size()
+		n += 1 + l + sovFinalizeBlock(uint64(l))
+	}
+	return n
+}
+
+func sovFinalizeBlock(x uint64) (n int) {
+	return (math_bits.Len64(x|1) + 6) / 7
+}
+func sozFinalizeBlock(x uint64) (n int) {
+	return sovFinalizeBlock(uint64((x << 1) ^ uint64((int64(x) >> 63))))
+}
+func (m *ClobStagedFinalizeBlockEvent) 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 ErrIntOverflowFinalizeBlock
+			}
+			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: ClobStagedFinalizeBlockEvent: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: ClobStagedFinalizeBlockEvent: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field CreateClobPair", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowFinalizeBlock
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthFinalizeBlock
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthFinalizeBlock
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			v := &ClobPair{}
+			if err := v.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			m.Event = &ClobStagedFinalizeBlockEvent_CreateClobPair{v}
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := skipFinalizeBlock(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return ErrInvalidLengthFinalizeBlock
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
+func skipFinalizeBlock(dAtA []byte) (n int, err error) {
+	l := len(dAtA)
+	iNdEx := 0
+	depth := 0
+	for iNdEx < l {
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return 0, ErrIntOverflowFinalizeBlock
+			}
+			if iNdEx >= l {
+				return 0, io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= (uint64(b) & 0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		wireType := int(wire & 0x7)
+		switch wireType {
+		case 0:
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return 0, ErrIntOverflowFinalizeBlock
+				}
+				if iNdEx >= l {
+					return 0, io.ErrUnexpectedEOF
+				}
+				iNdEx++
+				if dAtA[iNdEx-1] < 0x80 {
+					break
+				}
+			}
+		case 1:
+			iNdEx += 8
+		case 2:
+			var length int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return 0, ErrIntOverflowFinalizeBlock
+				}
+				if iNdEx >= l {
+					return 0, io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				length |= (int(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if length < 0 {
+				return 0, ErrInvalidLengthFinalizeBlock
+			}
+			iNdEx += length
+		case 3:
+			depth++
+		case 4:
+			if depth == 0 {
+				return 0, ErrUnexpectedEndOfGroupFinalizeBlock
+			}
+			depth--
+		case 5:
+			iNdEx += 4
+		default:
+			return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
+		}
+		if iNdEx < 0 {
+			return 0, ErrInvalidLengthFinalizeBlock
+		}
+		if depth == 0 {
+			return iNdEx, nil
+		}
+	}
+	return 0, io.ErrUnexpectedEOF
+}
+
+var (
+	ErrInvalidLengthFinalizeBlock        = fmt.Errorf("proto: negative length found during unmarshaling")
+	ErrIntOverflowFinalizeBlock          = fmt.Errorf("proto: integer overflow")
+	ErrUnexpectedEndOfGroupFinalizeBlock = fmt.Errorf("proto: unexpected end of group")
+)
diff --git a/protocol/x/clob/types/keys.go b/protocol/x/clob/types/keys.go
index b0b3691612..a78fbea45e 100644
--- a/protocol/x/clob/types/keys.go
+++ b/protocol/x/clob/types/keys.go
@@ -139,3 +139,9 @@ const (
 	// This is meant to be used for improved conditional order triggering.
 	MaxTradePricePrefix = "MaxTrade:"
 )
+
+// FinalizeBlock event staging
+const (
+	StagedEventsCountKey  = "StgEvtCnt"
+	StagedEventsKeyPrefix = "StgEvt:"
+)
diff --git a/protocol/x/clob/types/memclob.go b/protocol/x/clob/types/memclob.go
index 17cbb37601..86191376f7 100644
--- a/protocol/x/clob/types/memclob.go
+++ b/protocol/x/clob/types/memclob.go
@@ -23,7 +23,7 @@ type MemClob interface {
 	)
 	MaybeCreateOrderbook(
 		clobPair ClobPair,
-	)
+	) bool
 	GetOperationsToReplay(
 		ctx sdk.Context,
 	) (
diff --git a/protocol/x/listing/keeper/listing.go b/protocol/x/listing/keeper/listing.go
index 82d190620d..2304062e70 100644
--- a/protocol/x/listing/keeper/listing.go
+++ b/protocol/x/listing/keeper/listing.go
@@ -8,8 +8,6 @@ import (
 
 	satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types"
 
-	"github.com/dydxprotocol/v4-chain/protocol/lib"
-
 	"github.com/dydxprotocol/v4-chain/protocol/lib/slinky"
 
 	sdk "github.com/cosmos/cosmos-sdk/types"
@@ -90,33 +88,19 @@ func (k Keeper) CreateClobPair(
 ) (clobPairId uint32, err error) {
 	clobPairId = k.ClobKeeper.AcquireNextClobPairID(ctx)
 
-	clobPair := clobtypes.ClobPair{
-		Metadata: &clobtypes.ClobPair_PerpetualClobMetadata{
-			PerpetualClobMetadata: &clobtypes.PerpetualClobMetadata{
-				PerpetualId: perpetualId,
-			},
-		},
-		Id:                        clobPairId,
-		StepBaseQuantums:          types.DefaultStepBaseQuantums,
-		QuantumConversionExponent: types.DefaultQuantumConversionExponent,
-		SubticksPerTick:           types.SubticksPerTick_LongTail,
-		Status:                    clobtypes.ClobPair_STATUS_ACTIVE,
-	}
-	if err := k.ClobKeeper.ValidateClobPairCreation(ctx, &clobPair); err != nil {
+	clobPair, err := k.ClobKeeper.CreatePerpetualClobPair(
+		ctx,
+		clobPairId,
+		perpetualId,
+		satypes.BaseQuantums(types.DefaultStepBaseQuantums),
+		types.DefaultQuantumConversionExponent,
+		types.SubticksPerTick_LongTail,
+		clobtypes.ClobPair_STATUS_ACTIVE,
+	)
+	if err != nil {
 		return 0, err
 	}
 
-	k.ClobKeeper.SetClobPair(ctx, clobPair)
-
-	// Only create the clob pair if we are in deliver tx mode. This is to prevent populating
-	// in memory data structures in the CLOB during simulation mode.
-	if lib.IsDeliverTxMode(ctx) {
-		err := k.ClobKeeper.CreateClobPairStructures(ctx, clobPair)
-		if err != nil {
-			return 0, err
-		}
-	}
-
 	return clobPair.Id, nil
 }
 
diff --git a/protocol/x/listing/keeper/listing_test.go b/protocol/x/listing/keeper/listing_test.go
index 052172c0f0..06de01a6c0 100644
--- a/protocol/x/listing/keeper/listing_test.go
+++ b/protocol/x/listing/keeper/listing_test.go
@@ -12,6 +12,7 @@ import (
 
 	asstypes "github.com/dydxprotocol/v4-chain/protocol/x/assets/types"
 
+	sdk "github.com/cosmos/cosmos-sdk/types"
 	satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types"
 
 	"github.com/dydxprotocol/v4-chain/protocol/testutil/constants"
@@ -234,7 +235,7 @@ func TestCreateClobPair(t *testing.T) {
 
 				// Set deliverTx mode
 				if tc.isDeliverTx {
-					ctx = ctx.WithIsCheckTx(false).WithIsReCheckTx(false)
+					ctx = ctx.WithIsCheckTx(false).WithIsReCheckTx(false).WithExecMode(sdk.ExecModeFinalize)
 					lib.AssertDeliverTxMode(ctx)
 				} else {
 					ctx = ctx.WithIsCheckTx(true)
@@ -293,12 +294,21 @@ func TestCreateClobPair(t *testing.T) {
 				)
 				require.Equal(t, perpetualId, clobPair.MustGetPerpetualId())
 
-				// Check if the clob pair was created only if we are in deliverTx mode
+				// Should not modify in-memory object right away
 				_, found = clobKeeper.PerpetualIdToClobPairId[perpetualId]
+				require.False(t, found)
+
+				// Check the corresponding ClobPair creation was staged.
+				stagedEvents := clobKeeper.GetStagedClobFinalizeBlockEvents(ctx)
+
 				if tc.isDeliverTx {
-					require.True(t, found)
+					require.Equal(t, 1, len(stagedEvents))
+					require.Equal(t,
+						stagedEvents[0].GetCreateClobPair().GetPerpetualClobMetadata().PerpetualId,
+						perpetualId,
+					)
 				} else {
-					require.False(t, found)
+					require.Equal(t, 0, len(stagedEvents))
 				}
 			},
 		)
@@ -378,6 +388,7 @@ func TestDepositToMegavaultforPML(t *testing.T) {
 						return genesis
 					},
 				).Build()
+
 				ctx := tApp.InitChain()
 
 				// Set existing total shares.
diff --git a/protocol/x/listing/types/expected_keepers.go b/protocol/x/listing/types/expected_keepers.go
index d6b72ac3f9..b13f91fe2f 100644
--- a/protocol/x/listing/types/expected_keepers.go
+++ b/protocol/x/listing/types/expected_keepers.go
@@ -34,8 +34,9 @@ type ClobKeeper interface {
 	) (clobtypes.ClobPair, error)
 	AcquireNextClobPairID(ctx sdk.Context) uint32
 	ValidateClobPairCreation(ctx sdk.Context, clobPair *clobtypes.ClobPair) error
-	CreateClobPairStructures(ctx sdk.Context, clobPair clobtypes.ClobPair) error
+	StageNewClobPairSideEffects(ctx sdk.Context, clobPair clobtypes.ClobPair) error
 	SetClobPair(ctx sdk.Context, clobPair clobtypes.ClobPair)
+	GetStagedClobFinalizeBlockEvents(ctx sdk.Context) []*clobtypes.ClobStagedFinalizeBlockEvent
 }
 
 type MarketMapKeeper interface {