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:
TradesPackedis emitted beforeBookUpdatesPackedwhenever both exist.SpotSwapis emitted beforeTradesPackedandBookUpdatesPacked.- 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.pushPriceAndRequotecalls the orderbook first. The orderbookTradesPacked/BookUpdatesPackedprecede the hook'sSlotSyncevents, andPriceAccepted/Requotedfollow that internal sync.ExamplePostFillHook.postFillemits 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
PerpMarginTransferredis an audit delta and is immediately followed by the authoritativePerpMarginUpdatedsnapshot for that namespace. Apply only the snapshot to margin state. - A Spot-to-Perp move emits
PerpMarginTransferredthenPerpMarginUpdatedbefore AccountCoreSpotReserveUpdated. - A Perp-to-Spot move emits
PerpMarginTransferredthenPerpMarginUpdatedbefore the AccountCoreSpotReserveUpdatedfree-balance snapshot. - Moving margin between cross/isolated namespaces invokes debit then credit: debit audit/snapshot precedes credit audit/snapshot.
- Explicit
settlePerpFundingcan emit a margin snapshot first (when funding is nonzero), then an isolated-position snapshot if applicable, thenPerpFundingSettled. ItsmarginBalanceand 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/Withdrawalare activity records paired with an earlierSpotReserveUpdated; do not apply their amount to balances.InternalAccountTransferhas no paired snapshot; apply its free-balance delta once.PerpMarginTransferredis paired with a margin snapshot; do not apply both.- AccountCore
PerpMarketRiskParamsUpdateRequested,PerpFundingIntervalUpdateRequested,PerpPricesUpdateRequested, andPerpFundingUpdateRequestedrepeat the preceding PerpEngine mutation named in Market configuration forwarding. - A full fill with
TradesPacked.updatedSize == 0deletes 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. PerpFeeChargedandPerpProtocolFeesAccruedoverlap in fee attribution; only the latter plusSpotReserveUpdatedmutates the collector balance projection.- Router and AccountCore discovery events with identical signatures describe different registries; never deduplicate them by topic/data alone.