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, thenlogIndex. - 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 oraddress(0)in cross mode. amounts[i] > 0means PartyB paid PartyA;amounts[i] < 0means 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. |