Allocated Balance Event Ledger

v0.8.6 makes PartyA and PartyB allocated balances reconstructible from exact storage-backed deltas while preserving liquidation, CVA, fee-return, and reimbursement context in dedicated events.

Overview

BalanceChangePartyA and BalanceChangePartyB now form a deterministic delta ledger for the core Diamond's allocated-balance mappings. Every non-zero PartyA or PartyB allocated-balance mutation goes through a LibAccount increase or decrease method. That method writes storage and emits the matching event with the caller's reason as one operation.

The ledger covers allocated balance only. It excludes free balance, reserved open fees, locked or pending collateral, deferred balance, reimbursement, and liquidation escrow. v0.8.6 keeps those movements observable without placing false entries in the allocated ledger.

Reconstruction contract

event BalanceChangePartyA(
    address indexed partyA,
    uint256 amount,
    BalanceChangeType _type
);

event BalanceChangePartyB(
    address indexed partyB,
    address indexed partyA,
    uint256 amount,
    BalanceChangeType _type
);

Despite its retained ABI name, BalanceChangePartyB.partyA is the exact second key of partyBAllocatedBalances. Treat it as allocationKey: it is the PartyA address for an isolated PartyB bucket and address(0) for the shared cross-margin bucket.

Mutation ownership

The storage write and its event cannot drift apart: all direct writes to the two allocated-balance mappings are confined to these four helpers, and every caller must supply the reason.

LibAccount helper Storage key Emission
increasePartyAAllocatedBalance allocatedBalances[partyA] Inbound BalanceChangePartyA
decreasePartyAAllocatedBalance allocatedBalances[partyA] Outbound BalanceChangePartyA
increasePartyBAllocatedBalance partyBAllocatedBalances[partyB][allocationKey] Inbound BalanceChangePartyB with allocationKey
decreasePartyBAllocatedBalance partyBAllocatedBalances[partyB][allocationKey] Outbound BalanceChangePartyB with allocationKey

Each helper calculates the post-write value once, stores it, emits only when amount > 0, and returns that calculated value for action events that expose newAllocatedBalance.

amount is always an absolute, non-negative delta. Direction comes from _type. For one PartyA address or one PartyB allocation key, the reconstruction equation is:

allocatedAfter = allocatedBefore + sum(inbound amounts) - sum(outbound amounts)
  • Process logs by blockNumber, transactionIndex, then logIndex.
  • Use only logs from the intended core Diamond deployment and the ABI active at that block.
  • Zero-value calls do not change storage and do not emit a balance-change event.
  • Reverted transactions contribute neither state changes nor surviving logs.
  • Several valid deltas may occur in one transaction. Apply all of them; do not net or deduplicate by transaction hash.

Balance-change types

Both existing event signatures remain unchanged. BalanceChangeType preserves ordinals 0 through 13 and appends six new reasons at 14 through 19. The table below is the v0.8.6 direction contract. "Inbound" adds amount. "Outbound" subtracts it.

Ordinal Type Direction v0.8.6 meaning
0 ALLOCATE Inbound Free collateral enters PartyA allocation or a PartyB allocation bucket.
1 DEALLOCATE Outbound Allocation returns to free balance, including suspended-user deallocation.
2 PLATFORM_FEE_IN Inbound A reserved open fee refund or positive explicit Clearing House platform-fee component enters PartyA allocation.
3 PLATFORM_FEE_OUT Outbound PartyA allocation pays or reserves a trading fee, or pays a negative explicit Clearing House platform-fee component.
4 REALIZED_PNL_IN Inbound Realized close, liquidation, or Clearing House value enters allocation. UPNL settlement uses ordinal 18.
5 REALIZED_PNL_OUT Outbound Realized close, liquidation, penalty, or Clearing House value leaves allocation. UPNL settlement uses ordinal 19.
6 CVA_IN Inbound Returned liquidation CVA enters the exact PartyB allocation bucket.
7 CVA_OUT Not emitted Retained for ABI compatibility. v0.8.6 PartyA settlement does not emit it where PartyA allocation does not change.
8 LF_IN Inbound A capped liquidation fee enters the liquidation starter or liquidator's PartyA allocation.
9 LF_OUT Outbound Liquidation fee value leaves a liquidated PartyA or PartyB allocation.
10 FUNDING_FEE_IN Inbound Funding settlement enters PartyA allocation or the exact PartyB bucket.
11 FUNDING_FEE_OUT Outbound Funding settlement leaves PartyA allocation or the exact PartyB bucket.
12 DEFERRED_BALANCE_IN Inbound Deferred PartyA value survives liquidation and re-enters allocation.
13 DEFERRED_BALANCE_OUT Outbound Available PartyA allocation moves into deferred balance at liquidation start.
14 REIMBURSEMENT_IN Inbound Reimbursement actually leaves its bucket and enters PartyA allocation.
15 OPERATIONAL_FEE_OUT Outbound Only the portion of an operational fee taken from PartyA allocation.
16 OPEN_SOLVER_FEE_OUT Outbound An open solver fee leaves PartyA allocation for PartyB free balance.
17 CLOSE_SOLVER_FEE_OUT Outbound A close solver fee leaves PartyA allocation for PartyB free balance.
18 SETTLEMENT_PNL_IN Inbound UPNL settlement value enters PartyA allocation or the exact PartyB bucket.
19 SETTLEMENT_PNL_OUT Outbound UPNL settlement value leaves PartyA allocation or the exact PartyB bucket.

An indexer should reject or quarantine an unknown ordinal instead of guessing its direction. Future releases may append new types, so direction belongs in versioned indexer configuration rather than a suffix-only parser.

PartyB allocation keys

For BalanceChangePartyB, the indexed field named partyA is the exact second key of partyBAllocatedBalances[partyB][allocationKey]. Treat the event's accounting key as:

partyBAllocationKey = (partyB, event.partyA)
PartyB mode event.partyA Meaning
Isolated The PartyA address One PartyB bucket dedicated to that PartyA.
Cross address(0) The shared cross-mode PartyB allocation bucket.

Why unified cross settlement writes credits before debits

All PartyAs share one cross-mode PartyB bucket. The contract credits that bucket for every PartyA loss before applying any PartyA gain, so an offsetting batch cannot fail from a temporary underflow caused only by input order. For example, from a starting bucket of 100, Alice paying the solver 30 and the solver paying Bob 50 emits SETTLEMENT_PNL_IN 30, then SETTLEMENT_PNL_OUT 50, both against (solver, address(0)); the final bucket is 80. Apply both logs by logIndex. Use SettleUpnlUnified for the affected PartyAs and final balance checkpoints, not to replace the zero-address ledger key.

Reimbursement ledger

While PartyA is liquidating, Clearing House distributions and returned pending-open fees can enter partyAReimbursement. That bucket has a separate exact event because it is not allocated balance.

event PartyAReimbursementChange(
    address indexed partyA,
    uint256 amount,
    uint256 newBalance,
    ReimbursementChangeType _type
);
Ordinal Type Direction Destination or source
0 CLEARING_HOUSE_IN_DEPRECATED Not emitted Reserved at its historical ordinal; explicit Clearing House credits use the typed ordinals below.
1 PLATFORM_FEE_IN Inbound A released pending-open fee or explicit positive platform-fee component enters reimbursement.
2 CLEARING_HOUSE_OUT_DEPRECATED Not emitted Reserved at its historical ordinal; explicit Clearing House debits use the typed ordinals below.
3 RELEASE_TO_ALLOCATED Outbound Reimbursement leaves this bucket before entering PartyA allocation.
4 MOVE_TO_LIQUIDATION_ESCROW Outbound Late or overdue reimbursement moves into liquidation escrow.
5 REALIZED_PNL_IN Inbound An explicit positive Clearing House realized-PnL component enters reimbursement.
6 REALIZED_PNL_OUT Outbound An explicit negative Clearing House realized-PnL component leaves reimbursement.
7 FUNDING_FEE_IN Inbound An explicit positive Clearing House funding component enters reimbursement.
8 FUNDING_FEE_OUT Outbound An explicit negative Clearing House funding component leaves reimbursement.
9 PLATFORM_FEE_OUT Outbound An explicit negative Clearing House platform-fee component leaves reimbursement.

For every reimbursement event, verify previousBalance ± amount == newBalance. A normal liquidation or Clearing House takeover emits RELEASE_TO_ALLOCATED, then a separate BalanceChangePartyA(REIMBURSEMENT_IN). A late or overdue liquidation emits MOVE_TO_LIQUIDATION_ESCROW and LiquidationEscrowCreated, but no allocated credit.

Liquidation and CVA

PartyA liquidation settlement keeps the original event and emits an overloaded enrichment for the same call:

event SettlePartyALiquidation(
    address partyA,
    address[] partyBs,
    int256[] amounts,
    bytes liquidationId
);

event SettlePartyALiquidation(
    address partyA,
    address[] partyBs,
    address[] allocationKeys,
    int256[] amounts,
    uint256[] cvaAmounts,
    bytes liquidationId
);
  • All arrays in the extended event have the same length and align by index.
  • allocationKeys[i] is the exact PartyB storage key: PartyA in isolated mode or address(0) in cross mode.
  • amounts[i] > 0 means PartyB paid PartyA; amounts[i] < 0 means PartyA paid PartyB.
  • cvaAmounts[i] is the CVA returned to that PartyB bucket: full in normal liquidation, haircut in late liquidation, and zero in overdue liquidation.
  • Paginated settlement emits one legacy event and one extended event for each processed batch.

Final PartyA settlement also replaces the pre-finalization allocation with the values that survive liquidation. The emitted breakdown debits the allocation-funded liquidation fee as LF_OUT, debits the remaining old allocation as REALIZED_PNL_OUT, then credits surviving deferred or normal reimbursement values. The signed sum equals the exact before/after PartyA allocated-balance delta.

v0.8.6 liquidation accounting

v0.8.6 exposes the exact amount on both sides whenever the protocol records CVA or LF as its own liquidation component. Keep economic attribution separate from allocated-balance replay: settlement events identify who paid and received, while BalanceChangePartyA and BalanceChangePartyB identify the storage delta that actually occurred.

PartyA liquidation

Value Economic record Allocated-balance record
CVA paid by the liquidated PartyA For every extended SettlePartyALiquidation, sum cvaAmounts[i] by liquidationId. partyA is the payer and partyBs[i] is the receiving solver. The same non-zero amount is emitted once as BalanceChangePartyB(CVA_IN) against (partyBs[i], allocationKeys[i]). There is no PartyA CVA_OUT delta.
CVA received by a PartyB cvaAmounts[i] is that solver's exact severity-adjusted receipt, including zero. Apply only the matching non-zero CVA_IN event. A zero metadata amount has no balance-change log and no storage effect.
Total LF received by the liquidation starter Use BalanceChangePartyA(recipient, amount, LF_IN) in the final settlement receipt. This is the whole capped liquidation fee; LF exists only for NORMAL liquidation, so LATE, OVERDUE, and takeover-cleared liquidations emit none. The event address and amount are the exact recipient allocation credit.
LF funded from the liquidated PartyA's allocation Use BalanceChangePartyA(partyA, amount, LF_OUT) in that same final receipt. This is only the part of the starter fee the live allocation could cover, not the original locked LF or liquidation-start fee calculation. The event amount is the exact debit from the liquidated user's allocated balance. Treat an absent LF_OUT as zero, not as missing data.
Starter-credit / live-debit gap Derive it as LF_IN - LF_OUT within the final receipt. It is the portion of the starter credit not represented by a PartyA allocated-balance debit in that receipt. No event identifies that gap as a separate transfer. No allocated-balance event carries this component, because no allocated balance moves for it in this receipt. Never replay it as a delta.
Original locked LF and liquidation-start disposition In a normal liquidation, rawRemainingLf = lockedLf - availableBalanceShortfall. The starter fee is capped at maxLiquidationProfitPerPosition * positionsCount; any excess is credited to the liquidation insurance vault. The allocated-balance ledger does not carry lockedLf, the signed-snapshot shortfall, or the insurance-vault credit. Do not infer any of them from LF_OUT or LF_IN alone.
partyAPaidCva(liquidationId) = sum(all extended cvaAmounts)
partyBReceivedCva(liquidationId, partyB) = sum(cvaAmounts[i] where partyBs[i] == partyB)
// The final settlement receipt is the settlement call that also emits FullyLiquidatedPartyA.
// Read LF_OUT and REALIZED_PNL_OUT on partyA, and LF_IN on the starter, from that receipt.
// A component with no event is zero, not unknown.
totalLf(liquidationId)            = LF_IN credited to the liquidation starter
lfFromPartyAAllocation(partyA)    = LF_OUT debited from partyA          // clamped to the live allocation
lfCreditDebitGap(partyA)          = totalLf - lfFromPartyAAllocation    // >= 0, no allocated-balance event
partyAAllocationBeforeFinalize    = LF_OUT + REALIZED_PNL_OUT on partyA // the allocation liquidation consumed
clampFired(partyA)                = totalLf > lfFromPartyAAllocation

// Liquidation-start classification, before the per-position profit cap:
rawRemainingLf                    = lockedLf - availableBalanceShortfall
totalLf                           = min(rawRemainingLf, maxLiquidationProfitPerPosition * positionsCount)
insuranceVaultCredit              = rawRemainingLf - totalLf
// LF_OUT is only the slice of totalLf still present in PartyA allocation at finalization.
// The allocated-balance events do not encode lockedLf or availableBalanceShortfall.

A settlement call emits its balance changes before the legacy and extended SettlePartyALiquidation events. Buffer the complete receipt, process it in logIndex order, and then attach the later extended event's liquidationId, partyBs, and allocationKeys. Paginated settlement contributes one extended event per processed batch; aggregate all batches sharing the liquidation ID.

PartyB liquidation

Value Exact v0.8.6 record
LF removed from the liquidated PartyB BalanceChangePartyB(partyB, partyA, remainingLf, LF_OUT) is the exact solver allocation debit.
LF received by liquidators Sum BalanceChangePartyA(recipient, amount, LF_IN) from the start call and every LiquidatePositionsPartyB batch. Each event is an exact recipient allocation credit.
Allocation transferred to PartyA The exact transfer is the matching REALIZED_PNL_OUT debit from PartyB and REALIZED_PNL_IN credit to PartyA.
CVA paid by PartyB The isolated PartyB liquidation flow does not expose a standalone realized CVA component. Do not label the quote CVA sum as CVA paid; it is locked exposure, while the actual combined transfer is recorded as realized PnL.

Event field corrections

Two events carried a wrong or path-dependent value in a single field. Both are corrected in v0.8.6 without any signature change, so an indexer will keep decoding them while silently starting to read a different value.

SettleUpnl reported the caller instead of PartyA

event SettleUpnl(
    QuoteSettlementData[] settlementData,
    uint256[] updatedPrices,
    address partyA,
    uint256 newPartyAAllocatedBalance,
    uint256[] newPartyBsAllocatedBalances
);

ForceActionsFacet.settleAndForceClosePosition passed msg.sender into the parameter declared as partyA. On that path the field held the keeper or liquidator who submitted the force close, not the PartyA being settled. SettlementFacet always passed the real PartyA, so one event signature meant two different things depending on which facet emitted it. v0.8.6 passes the quote's PartyA on both paths.

Any index keyed on that field from the force-close path has been attributing settlements to submitters. History can be corrected by re-deriving the PartyA from the quote IDs in settlementData.

LiquidatePartyB reported a post-payout balance on the force-close path

event LiquidatePartyB(address liquidator, address partyB, address partyA, uint256 partyBAllocatedBalance, int256 upnl);

PartyBLiquidationFacet.liquidatePartyB emits before it starts the liquidation, so its partyBAllocatedBalance is the value immediately before liquidation touches it. The force-close paths read the same field after liquidation had already run. Starting a PartyB liquidation debits that bucket twice, once for the realized settlement transfer and once for the remaining liquidation fee. The field therefore reported a smaller, post-payout number on those paths.

v0.8.6 captures the balance before liquidation on the force-close paths as well, so both entry points now report the pre-liquidation balance.

Indexer changelog

Before v0.8.6 v0.8.6 behavior Required migration
UPNL settlement reused REALIZED_PNL_IN / REALIZED_PNL_OUT. It emits SETTLEMENT_PNL_IN (18) and SETTLEMENT_PNL_OUT (19). Close, liquidation, and Clearing House keep the realized-PnL ordinals. Register the two appended ordinals with the same direction handling as realized PnL. Existing ordinals are unchanged, so no historical data moves.
Some allocation writes and events were separate operations. Four LibAccount methods own PartyA/PartyB increases and decreases plus non-zero emission. Use balance-change logs as the canonical post-upgrade delta ledger.
Suspended-user deallocation could miss its PartyA delta. It emits the exact DEALLOCATE amount. Do not add a path-specific workaround after activation.
PartyB events could use the economic PartyA instead of the mutated cross bucket. The indexed field is always the exact allocation key. Key cross balances by (partyB, address(0)).
Pending fee returns and Clearing House reimbursement could appear as PartyA allocated credits before allocation changed. They emit PartyAReimbursementChange. Move these values to the reimbursement ledger and wait for RELEASE_TO_ALLOCATED plus REIMBURSEMENT_IN.
Reimbursement reductions had no exact event stream. Every reduction emits amount, reason, and post-balance. Apply the typed REALIZED_PNL_OUT, FUNDING_FEE_OUT, or PLATFORM_FEE_OUT reason, or RELEASE_TO_ALLOCATED / MOVE_TO_LIQUIDATION_ESCROW as emitted.
PartyA CVA_OUT could be emitted when PartyA allocation did not change. CVA is exposed through the extended settlement event; the actual PartyB allocation credit remains CVA_IN. Stop applying PartyA CVA_OUT after activation and consume aligned cvaAmounts as metadata.
Final PartyA liquidation used whole-balance assignments with an incomplete event explanation. LF, realized PnL, deferred, and reimbursement components explain the complete allocation replacement. Apply every component in log order; do not infer one synthetic net event.
PartyA LF_OUT was read as the liquidated user's total LF payment. It is only the part funded from the live allocation, so it can be smaller than the starter's LF_IN, or absent entirely. Report received LF from LF_IN, the allocated debit from LF_OUT, and the receipt-local gap as LF_IN - LF_OUT. Obtain original locked LF, signed-snapshot shortfall, and any insurance-vault credit from their own state or events; the balance-change ledger does not carry them. Exempt the finalization receipt from any cross-account conservation check.
An operational-fee event reports the full charge. The balance event reports only allocated spillover. Use OperationalFeeCharged for fee reporting and OPERATIONAL_FEE_OUT for allocated accounting.
SettleUpnl carried msg.sender in its partyA field on the force-close path. Both emitting facets pass the settled PartyA. Stop treating that field as the submitter. Re-derive historical PartyAs from the quote IDs in settlementData if you need to correct the backfill.
LiquidatePartyB reported the post-payout allocated balance on force-close paths and the pre-liquidation balance on the direct path. Every path reports the balance immediately before liquidation debits the bucket. Read partyBAllocatedBalance as pre-liquidation everywhere. Pre-upgrade force-close records under-report it by the settlement transfer plus the remaining liquidation fee.
Force-close-related settlement and liquidation events could misidentify PartyA or report a post-liquidation PartyB balance. SettleUpnl carries the settled PartyA and returned PartyB balances; LiquidatePartyB carries the pre-debit balance. Adopt the corrected field semantics, but continue using balance-change events as the delta source.