Hook Entry Points
The SymmioHookFacet on the AccountLayer diamond implements these hooks:
| Hook | Fired When | Action |
|---|---|---|
onOpenPosition |
Position opened | No-op |
onClosePosition |
Position closed (full or partial) | Remove quoteId from VA if full close; attempt deletion |
onCancelQuote |
Quote cancelled/expired/liquidated | Remove quoteId from VA; attempt deletion |
onFeeCharged |
Fee charged | No-op |
onLiquidationSettled |
Liquidation fully settled | Attempt deferred VA deletion |
Core Logic
Partial Close Detection
_removeQuoteFromAccount(quoteId, partyA, partyB) compares core state with VA tracked state:
coreTotal = partyAPositionsCount(partyA) + getPartyAPendingQuotes(partyA).length
-
If
coreTotal < vData.quoteIds.length()— a quote was fully resolved (closed/cancelled/liquidated), so the quoteId is removed from the VA's tracking set. -
If
coreTotal >= vData.quoteIds.length()— this is a partial close; the position is still open, so the quoteId is kept.
Deletion Deferral
_tryDeleteVirtualAccount(partyA, partyB) gates deletion with three checks:
| Check | Protects Against | Cleared By | Recovery Hook |
|---|---|---|---|
isPartyALiquidated(partyA) |
Premature deletion during partyA liquidation (reimbursement not yet credited) | settlePartyALiquidation clears liquidationStatus |
onLiquidationSettled |
isPartyATakeoverInProgress(partyA) |
Premature deletion during CH takeover (funds not yet distributed) | settlePartyATakeover clears inProgress |
onLiquidationSettled |
getPartyBCrossLiquidationStatus(partyB) |
Premature deletion during cross partyB liq (funds not yet distributed) | settleCrossPartyBLiquidation clears inProgress |
onLiquidationSettled (passes address(0), skipping this check) |
The partyB parameter comes directly from the hook call (onClosePosition /
onCancelQuote). For onLiquidationSettled, address(0) is passed because settlement is
already complete — no cross partyB check is needed.
Normal Flows
Full Close (fillCloseRequest)
-
LibQuoteClose.closeQuote()removes position frompartyAPositionsCountbefore firing hook onClosePositionfires —coreTotal < quoteIds.length()— quoteId removed- If last quoteId:
_tryDeleteVirtualAccount— no liquidation active — VA deleted
Partial Close (fillCloseRequest with partial fill)
- Position stays open;
partyAPositionsCountunchanged onClosePositionfires —coreTotal >= quoteIds.length()— quoteId kept- VA persists until full close
Cancel Quote (requestToCancelQuote, acceptCancelRequest, expireQuote)
- Quote removed from
partyAPendingQuotesbefore hook fires onCancelQuotefires —coreTotal < quoteIds.length()— quoteId removed- If last quoteId: VA deleted
Force Cancel Quote (forceCancelQuote)
- Quote removed from pending via
removeFromPendingQuotesbefore hook fires onCancelQuotefires — quoteId removed- If last quoteId: VA deleted
Force Close (finalizeForceClose)
Uses shared LibQuoteClose.closeQuote() path — same as full close above.
Liquidation Flows
PartyA Normal Liquidation
Trigger: VA becomes insolvent.
-
liquidatePartyA(partyA)— setsliquidationStatus[partyA] = true -
setSymbolsPrice(partyA)— sets liquidation prices per symbol -
liquidatePendingPositionsPartyA(partyA)— cancels all pending quotes - Collects quoteIds during loop -delete partyAPendingQuotes[partyA]- FiresonCancelQuoteper quote after delete — quoteIds removed from VA -_tryDeleteVirtualAccount--isPartyALiquidated = true— deferred -
liquidatePositionsPartyA(partyA, quoteIds)— liquidates open positions (paginated) - Removes from open positions, decrementspartyAPositionsCount- FiresonClosePositionper quote after state update — quoteIds removed from VA -_tryDeleteVirtualAccount--isPartyALiquidated = true— deferred -
settlePartyALiquidation(partyA, partyBs)— settles funds (paginated across partyBs) - When fully settled:liquidationStatus[partyA] = false- FiresonLiquidationSettled(partyA)--isPartyALiquidated = false— VA deleted
Fund flow: Reimbursement credited to allocatedBalances[VA] during settlement.
_deleteVirtualAccount calls zeroUpnlDeallocate to sweep to parent.
ClearingHouse PartyA Takeover
Trigger: ClearingHouse takes over a partyA's liquidation.
-
initiatePartyATakeover(partyA)— setspartyATakeoverDetails[partyA].inProgress = true -
liquidatePendingPositionsForClearingHouse(partyA, [])— takeover path - Collects quoteIds,delete partyAPendingQuotes[partyA]- FiresonCancelQuoteper quote after delete -- quoteIds removed -_tryDeleteVirtualAccount—isPartyATakeoverInProgress = true-- deferred -
liquidatePositionsForClearingHouse(partyA, quoteIds, prices)— takeover path - FiresonClosePositionper quote after state update — quoteIds removed -_tryDeleteVirtualAccount--isPartyATakeoverInProgress = true— deferred -
distributeForClearingHouse(partyA, ...)— distributes funds - CreditsallocatedBalances[VA]with reimbursement -
settlePartyATakeover(partyA, settledPartyBs)— clears takeover state -inProgress = false- FiresonLiquidationSettled(partyA)— VA deleted
ClearingHouse Cross PartyB Liquidation
Trigger: A cross partyB becomes insolvent across all its partyAs.
-
liquidateCrossPartyB(partyB)— setscrossLiquidationDetails[partyB].inProgress = true -
liquidatePendingPositionsForClearingHouse(partyB, [va1, va2, ...])— cross path - Per partyA: swap-and-pop loop, firesonCancelQuoteafter pop — quoteIds removed -_tryDeleteVirtualAccount(partyA, partyB)—getPartyBCrossLiquidationStatus(partyB) = true-- deferred -
liquidatePositionsForClearingHouse(partyB, quoteIds, prices)— cross path - FiresonClosePositionper quote after state update — quoteIds removed -_tryDeleteVirtualAccount(partyA, partyB)— cross liq still active — deferred -
distributeForClearingHouse(partyB, [va1, ...], ...)— distributes fromdeallocatedPool- CreditsallocatedBalances[VA]with reimbursement -
settleCrossPartyBLiquidation(partyB, [va1], false)— partial settlement (pagination) - FiresonLiquidationSettled(va1)withaddress(0)— skips cross check -- VA1 deleted -inProgressstilltrue(no finalize) -
settleCrossPartyBLiquidation(partyB, [va2], true)— final batch - FiresonLiquidationSettled(va2)— VA2 deleted -inProgress = false(finalize clears it)
Note: onLiquidationSettled passes address(0) as partyB, which skips
the cross check. This is correct because the hook only fires for partyAs that have already been distributed to — their funds
are safe.
Isolated PartyB Liquidation
Trigger: PartyB becomes insolvent against a specific partyA (VA).
-
liquidatePartyB(partyB, partyA)—LibLiquidation.liquidatePartyB- Cancels pending quotes (swap-and-pop), collects quoteIds - Transfers partyB's allocated balance to partyA:allocatedBalances[VA] += value-delete partyBPendingQuotes, zeroes partyB balances, increments nonce - FiresonCancelQuoteper quote after all state changes — quoteIds removed -_tryDeleteVirtualAccount(partyA, partyB)— partyA NOT liquidated, partyB NOT cross — checks pass - If VA has open positions:quoteIds.length() != 0— not deleted yet -
liquidatePositionsPartyB(partyB, partyA, priceSig)— closes open positions - Per quote: settles PnL, removes from open positions - FiresonClosePositionper quote after state update — quoteIds removed - When last quoteId removed:_tryDeleteVirtualAccount— VA deleted immediately
No deferral needed: PartyA is not liquidated. PartyB is not in cross mode. VA is deleted as soon as all quotes are resolved.
No settlement hook needed: Funds are settled inline during liquidatePartyB (pending) and
liquidatePositionsPartyB (positions). No deferred distribution step exists.
Hook Firing Order
All hooks must fire after core state changes are complete so that _removeQuoteFromAccount can
correctly compare coreTotal with vData.quoteIds.length().
Places where this was specifically addressed: - liquidatePendingPositionsPartyA — hooks fire after
delete partyAPendingQuotes - liquidatePendingPositionsForClearingHouse (both paths) — hooks fire
after swap-pop loop / delete - LibLiquidation.liquidatePartyB — hooks fire after all balance updates and
delete partyBPendingQuotes