Summary
When a solver partially fills a LIMIT quote, the core creates a new PENDING quote for the unfilled remainder under the same
partyA. On a POSITION-isolated virtual account that remainder would be a path to a second position on an account
whose isolation rule permits exactly one position, sharing margin and liquidating together with the first.
The AccountLayer therefore cancels the remainder during the solver's openPosition transaction. The
onOpenPosition hook detects the child quote and calls requestToCancelQuote on it as the virtual
account. Because the child is still PENDING with no PartyB, the cancel is immediate: the reserved open trading fee is refunded
and the pending locked balance is released back to the virtual account in the same transaction.
Virtual accounts with MARKET, MARKET_LONG, or MARKET_SHORT isolation keep the remainder: the child quote has the same symbol and direction as its parent, so tracking it on the same account preserves those isolation contracts.
Partial-fill flow
Core fires lifecycle hooks after all openPosition state is final, which is what makes the nested cancel safe: the
filled part of the quote is already OPENED and the remainder is a fully-formed PENDING quote.
// SymmioHookFacet.onOpenPosition
uint256 childQuoteId = symmio.getNextQuoteId();
ISymmio.Quote memory childQuote = symmio.getQuote(childQuoteId);
if (childQuote.parentId == quoteId && childQuote.partyA == partyA && childQuote.quoteStatus == ISymmio.QuoteStatus.PENDING) {
if (vData.isolationType == VirtualAccountIsolationType.POSITION) {
LibAccountLayerUtils.executeWithSigner(partyA, abi.encodeWithSelector(ISymmio.requestToCancelQuote.selector, childQuoteId));
} else {
vData.quoteIds.add(childQuoteId);
}
}
The cancel path inside the core does four things atomically:
- Sets the child quote's status to
CANCELED. - Refunds the child's reserved open trading fee to the virtual account.
- Releases the child's
pendingLockedBalancesreservation. - Removes the child from the account's pending quote list.
The nested cancel triggers the onCancelQuote hook back into the AccountLayer while onOpenPosition is
still executing. The child was never added to the virtual account's tracked quote IDs, and the account still holds the opened
position, so no deletion is attempted. onCancelQuote uses the callback-aware reentrancy guard: it admits this
expected callback from the exact core that AccountLayer is already calling while still rejecting unrelated or nested protected
callbacks.
Order semantics
On POSITION-isolated accounts, a LIMIT order is effectively partial-fill-and-kill: the solver keeps whatever it filled, and the unfilled remainder does not rest on the book. MARKET orders are unaffected because the core requires them to fill completely, and instant opens are unaffected for the same reason.
The refunded fee and released reservation stay on the virtual account as free allocated margin, where they buffer the position that just opened. Moving them off the account follows the normal rules for an account with an open position: a Muon-attested deallocation, or the automatic sweep to the parent SubAccount when the position closes and the virtual account is deleted.
A client that wants the remainder to keep working the book re-quotes it through the SubAccount. Because the existing virtual account already holds a quote, routing assigns the new quote to a fresh virtual account, which needs its own margin.
Integration notes
Indexers and frontends should expect a RequestToCancelQuote(partyA, address(0), CANCELED, childQuoteId) event
emitted by the nested cancel inside the solver's openPosition transaction. It occurs during the
onOpenPosition hook, before the outer facet emits its open-position events. After the hook returns, the outer
facet also emits AcceptCancelRequest(childQuoteId, CANCELED) for the already-cancelled remainder. The child
quote's parentId points to the filled quote, which is how clients distinguish this pair from a user-initiated
cancel.
Two operational couplings follow from routing the cancel through the normal PartyA path:
- While PartyA actions are paused, the nested cancel reverts, so a partial fill on a POSITION-isolated account reverts with it. Solvers can still fill such quotes completely.
-
If an account has activated instant actions mode,
requestToCancelQuoteis blocked outside the InstantLayer, so a partial fill of a resting quote on such an account reverts. Quotes routed through the InstantLayer are unaffected because instant opens always fill completely.