Skip to content

Event ordering and atomicity

All state changes and logs in a successful transaction are atomic. The order below is nevertheless essential because many events are post-state snapshots and because a single action can reuse a slot or mutate the same balance several times.

Global order

Use this total order:

(blockNumber, transactionIndex, logIndex, packedRecordIndex)

packedRecordIndex is the zero-based record position inside TradesPacked.packedTrades or BookUpdatesPacked.packedUpdates. Never sort packed records by account, price, slot, or trade ID. logIndex is the block-global RPC value, not a transaction-local counter: the first log in a receipt can be nonzero, and a filtered in-scope list can contain gaps. Preserve it; do not rebase it to the receipt-array index.

Within one transaction, apply logs immediately in log order. If a later snapshot overwrites an earlier value, retain both raw observations but publish only the later value as transaction-final state.

Ownership transfer ambiguity

transferOwnership, renounceOwnership, initialization, and completeOwnershipHandover(pendingOwner) all converge on the same OwnershipTransferred(previousOwner,newOwner) event. Event order and fields cannot distinguish them. Apply the owner snapshot immediately, but do not mutate any pending-candidate row unless transaction calldata or a trace identifies completeOwnershipHandover and its argument. If only a block-pinned getter is available, reconcile each previously known candidate independently and mark same-block intermediate history unknown when later transactions could have changed the result.

Proxy construction and discovery

An ERC-1967 proxy can emit the common Upgraded(implementation) and initializer ownership/ Initialized logs before the protocol's market-discovery event.

Spot deployment

The current call sequence is:

Spot orderbook proxy: Upgraded / OwnershipTransferred / Initialized
Spot orderbook:       AuthorityUpdated             (only when Router authority is nonzero)
SpotEngine:           SpotMarketAdded
AccountCore:          SpotMarketRegistered
SpotRouter:           SpotMarketRegistered         (discovery event is last)

Buffer unknown-emitter logs per transaction. When the Router discovery event arrives, classify the earlier proxy logs and replay them with the Spot ABI. The full numeric config is in Router calldata and getters, not in the three discovery events.

Perp deployment

The current call sequence is:

PerpEngine:           PerpMarketUpdated             (market registered first)
Perp orderbook proxy: Upgraded / OwnershipTransferred / Initialized
Perp orderbook:       AuthorityUpdated              (optional)
Perp orderbook:       MarketStateUpdated            (only if launched paused)
AccountCore:          PerpMarketEnabled(true)
AccountCore:          PerpOrderBookUpdated(0 -> book)
PerpRouter:           PerpMarketDeployed             (discovery event is last)

Initial deployment does not emit PerpOrderBookRouteUpdated. That Router event is reserved for later replacement/revocation.

Engine registration

PerpRouter.addPerpEngine calls AccountCore first, so AccountCore PerpEngineAdded precedes the Router event with the same signature. Store both as provenance; one relation row is enough.

Spot order action

For a normal Spot batch/replace/swap, AccountCore settlement and reserve logs occur before the orderbook flushes packed market data.

The fixed outer ordering is:

AccountCore AccountRegistered                       (only if direct account auto-registration occurs)
OrderBook MakerRegistered                           (only on first maker-page allocation)
matching-time settlement families                   (zero or many; may interleave as below)
OrderBook SpotSwap                                  (swap entrypoint only)
OrderBook TradesPacked                              (all fills, record order = match order)
OrderBook BookUpdatesPacked                         (all live/delete changes)
KuruTradingWallet IntentExecuted or TriggerFired
                                                    (successful forwarding; after orderbook call)

There is no total ordering that places every AccountCore settlement before every passive-band snapshot. Each passive fill calls AccountCore immediately, so its SpotReserveUpdated/conditional BuilderFeeAccrued logs are followed by that fill's PassiveBandUpdated. Active maker fills remain buffered until _settleMatch; the resulting AccountCore snapshots/conditional builder accrual can therefore appear after one or more earlier passive-band updates. A packed batch can start another match and repeat either family before the final SpotSwap/packed events.

Important variations:

  • TradesPacked is emitted before BookUpdatesPacked whenever both exist.
  • SpotSwap is emitted before TradesPacked and BookUpdatesPacked.
  • Active maker/taker and fee-collector balance snapshots may repeat in one action. Each is a full post-state replacement.
  • A passive fill emits AccountCore balance snapshots and any builder accrual, then PassiveBandUpdated, while its packed trade record is emitted only at the final action flush.
  • Hook-generated replenishment/replacement is synchronous and is included in the same final TradesPacked/BookUpdatesPacked. It can itself be consumed before the action ends.
  • ExamplePostFillHook.pushPriceAndRequote calls the orderbook first. The orderbook TradesPacked/BookUpdatesPacked precede the hook's SlotSync events, and PriceAccepted / Requoted follow that internal sync.
  • ExamplePostFillHook.postFill emits no hook event; the orderbook's packed records are the observable state transition.

Normal Perp order action

The orderbook passes its captured journal to AccountCore and PerpEngine before flushing packed market-data events:

PerpEngine PerpPositionModeUpdated / PerpCrossPositionSlotUpdated /
           PerpIsolatedPositionUpdated             (as needed)
PerpEngine PerpMarginUpdated / PerpMakerFeeReserveUpdated
PerpEngine PerpOrderExposureUpdated
PerpEngine funding-related margin snapshots
PerpEngine PerpFillSettled                          (maker then taker, per fill)
AccountCore PerpFeeCharged(maker=true)              (once per nonzero maker fee)
AccountCore PerpProtocolFeesAccrued                 (when aggregate protocol fee is nonzero)
AccountCore SpotReserveUpdated                      (collector snapshot, immediately after accrual)
AccountCore PerpFeeCharged(maker=false)             (when taker fee is nonzero)
AccountCore BuilderFeeAccrued                       (when a builder is configured)
PerpOrderBook TradesPacked
PerpOrderBook BookUpdatesPacked

There can be more than one PerpMarginUpdated, PerpMakerFeeReserveUpdated, PerpIsolatedPositionUpdated, or PerpOrderExposureUpdated between two fills. Consume every PerpEngine log in canonical log order; use TradesPacked for market/order attribution, not as a second position mutation.

For each maker fill, Engine emits the maker PerpFillSettled before the taker PerpFillSettled. Multiple makers therefore appear as (maker1,taker,maker2,taker,...), while packed TradesPacked has one record per maker fill.

Forced Perp liquidation

Forced cancellation/execution uses a different return-journal path. The orderbook flushes packed events before the Engine consumes the returned journal.

Start liquidation

Cross liquidation has no event for the initial cross-lock write. For each requested market in strictly increasing market-ID order:

PerpOrderBook BookUpdatesPacked                     (forced deletes; clientOrderId = 0)
PerpEngine margin/reserve/exposure snapshots        (journal consumption)
PerpEngine isolated/cross mode cleanup              (if cancellation makes it flat)

After all markets are processed, Engine emits PerpLiquidationStarted. Its recovered flag means cancellation alone restored maintenance health and the lock was cleared before emission. Do not assume PerpLiquidationCleared also appears in this case; it does not.

Isolated start first emits PerpIsolatedPositionUpdated with position flag bit 2 set, then the market's BookUpdatesPacked, PerpMarginUpdated / PerpMakerFeeReserveUpdated / PerpOrderExposureUpdated, and a final isolated snapshot. If cancellation recovers maintenance health, another PerpIsolatedPositionUpdated clears bit 2 before PerpLiquidationStarted(recovered=true).

Partial liquidation execution

PerpOrderBook TradesPacked                          (forced taker account; clientOrderId = 0)
PerpOrderBook BookUpdatesPacked                     (maker residual deletes/updates, if any)
PerpEngine maker and target settlement events
PerpEngine PerpLiquidationCleared                   (only if maintenance health recovered)
AccountCore PerpProtocolFeesAccrued / SpotReserveUpdated
AccountCore PerpFeeCharged                          (nonzero target taker fee)
AccountCore PerpPositionLiquidated                  (action summary; last)

Do not write a reducer that assumes Engine settlement always precedes packed market data; normal and forced paths intentionally differ.

Takeover

Cross takeover emits, for every transferred market, target/destination mode updates followed by target/destination cross-slot updates. Margin snapshots follow all positions; one final PerpNamespaceTakenOver closes the sequence.

Isolated takeover emits target/destination mode updates, target clear, destination full isolated snapshot, target/destination margin snapshots, then PerpNamespaceTakenOver.

Margin transfers and funding

  • Engine PerpMarginTransferred is an audit delta and is immediately followed by the authoritative PerpMarginUpdated snapshot for that namespace. Apply only the snapshot to margin state.
  • A Spot-to-Perp move emits PerpMarginTransferred then PerpMarginUpdated before AccountCore SpotReserveUpdated.
  • A Perp-to-Spot move emits PerpMarginTransferred then PerpMarginUpdated before the AccountCore SpotReserveUpdated free-balance snapshot.
  • Moving margin between cross/isolated namespaces invokes debit then credit: debit audit/snapshot precedes credit audit/snapshot.
  • Explicit settlePerpFunding can emit a margin snapshot first (when funding is nonzero), then an isolated-position snapshot if applicable, then PerpFundingSettled. Its marginBalance and signed checkpoint are authoritative action-final values.

Market configuration forwarding

AccountCore forwarding functions call the Engine first and then emit a ...Requested audit event:

PerpEngine PerpMarketRiskParamsUpdated
AccountCore PerpMarketRiskParamsUpdateRequested

PerpEngine PerpFundingIntervalUpdated
AccountCore PerpFundingIntervalUpdateRequested

PerpEngine PerpPricesUpdated
AccountCore PerpPricesUpdateRequested

PerpEngine PerpFundingUpdated
AccountCore PerpFundingUpdateRequested

Only the Engine event mutates the Engine projection. Retain the AccountCore event for caller-path and operational attribution; do not apply the values twice.

Route replacement and upgrades

Perp route replacement calls AccountCore before updating Router mappings:

new PerpOrderBook AuthorityUpdated                  (optional, before route activation)
AccountCore PerpOrderBookUpdated
PerpRouter PerpOrderBookRouteUpdated

The AccountCore event is the settlement-authority boundary. Router state and its event are the discovery mirror. A replacement orderbook may have initialization logs from an earlier transaction; do not assign its activation block from its deployment alone.

Router-mediated UUPS upgrades produce:

target proxy Upgraded(newImplementation)
target migration/initializer events                 (if upgrade data emits any)
Router ...Upgraded(target,newImplementation)

Use the proxy Upgraded log as the decoder boundary. The Router event is confirmation/audit.

Identity checks and double-application guards

  • Deposit/Withdrawal are activity records paired with an earlier SpotReserveUpdated; do not apply their amount to balances.
  • InternalAccountTransfer has no paired snapshot; apply its free-balance delta once.
  • PerpMarginTransferred is paired with a margin snapshot; do not apply both.
  • AccountCore PerpMarketRiskParamsUpdateRequested, PerpFundingIntervalUpdateRequested, PerpPricesUpdateRequested, and PerpFundingUpdateRequested repeat the preceding PerpEngine mutation named in Market configuration forwarding.
  • A full fill with TradesPacked.updatedSize == 0 deletes the slot. If a later non-live book record references the same order ID, identity-check and treat it as already deleted.
  • A non-live book update deletes only when (accountId, slot, orderId) equals the current slot. This prevents a stale delete from removing a replacement placed later in the same action.
  • PerpFeeCharged and PerpProtocolFeesAccrued overlap in fee attribution; only the latter plus SpotReserveUpdated mutates the collector balance projection.
  • Router and AccountCore discovery events with identical signatures describe different registries; never deduplicate them by topic/data alone.