AccountLayer Behavior Changes

A new aggregate affiliate read API, a reordered sub-account emptiness check that changes which error callers see, and an open-position hook that handles partial-fill children according to account isolation.

Summary

Three AccountLayer behaviors now produce different caller-visible results. Existing entry points remain, but integrations may see a new aggregate read API, a different deletion error when a sub-account has multiple blockers, and partial-fill child handling in the open-position hook.

Aggregate affiliate views

Reading an affiliate's configuration previously meant stitching together getAffiliateState, getAffiliateAdmin, getAffiliateFeeDistributor, getAffiliateSymmioCores, getAffiliateStakeholders, and getAffiliateSymmioShare. Those six calls could observe different states if anything changed between them. v0.8.6 adds two aggregate reads.

function getAffiliate(address affiliate) external view returns (AffiliateDetail memory);

function getAffiliateSelectorConfigs(address affiliate, bytes4[] calldata selectors)
    external view returns (AffiliateSelectorConfig[] memory);

AffiliateDetail mirrors the stored AffiliateData with the fee details flattened, and adds the affiliate's own address and its pendingFeeUpdate. That field reports a requested fee split waiting for approval, including whether it exists, its timestamp, the proposed Symmio share, and proposed stakeholders. It previously had no aggregate reader. An unregistered affiliate reads back as an empty struct with state NONE rather than reverting.

Per-selector data is not included, because hooks and both allow-lists live in mappings and cannot be enumerated from on-chain state. getAffiliateSelectorConfigs takes the selectors you care about and returns the hook address, hookAllowed, and callAllowed for each, in the order requested. hookAllowed says whether the selector may trigger the configured affiliate hook. callAllowed says whether the affiliate may submit that selector through callAsAffiliate.

Sub-account deletion reports the real blocker

deleteSubAccount checks that an account is empty before removing it. The order of those checks changed, and with it the error a blocked caller receives.

Balances used to be checked first, so any account with an open position or a pending quote reported SubAccountNotEmpty; trading state is now checked first and reports the actual blocker.
Order Before v0.8.6 After
1 balance / allocated balance open positions
2 open positions pending quotes
3 pending quotes balance / allocated balance

With an open position or pending quote, the old order returned SubAccountNotEmpty before reaching the specific trading-state check. Each position or quote keeps its credit valuation adjustment (CVA) and liquidation fee (LF) locked as liquidation collateral in allocated balance, and core forbids deallocating below that floor, so such an account can never be emptied first. The error pointed the caller at a withdrawal that could not succeed instead of naming the position or quote that had to be closed.

Callers now receive OpenPositionsExist or PendingQuotesExist in those cases. SubAccountNotEmpty is still raised, but only when the account genuinely has nothing but a balance left. Error handling that branches on SubAccountNotEmpty to prompt a withdrawal needs to handle the other two.

The open-position hook now handles partial-fill children

SymmioHookFacet.onOpenPosition was a deliberate no-op. It existed so core would not revert when calling the hook. It now updates virtual accounts.

When a quote is partially filled, core opens the filled portion and creates a child quote carrying the remainder. That child was never registered against the virtual account, so a virtual account's quoteIds set silently lost track of the leftover inventory. MARKET and direction-isolated virtual accounts may track multiple compatible quotes, while a POSITION-isolated virtual account permits only one position. The hook now reads the newest quote and checks that it is the pending child of the quote just opened and belongs to the same PartyA. MARKET and direction-isolated accounts add the child to the set. POSITION-isolated accounts cancel it immediately so the same account cannot later open a second position.

Supporting the hook required three core interface changes. getNextQuoteId became a view function so it can be called from a hook, getQuote was added so the candidate child can be inspected before it is trusted, and requestToCancelQuote lets the POSITION path cancel the verified child as the virtual account.

See Position Isolation Partial Fill Remainder for the POSITION path, including its pause and instant-actions-mode behavior.

The hook returns early for anything that is not an existing virtual account, so sub-accounts and legacy accounts are unaffected.