Summary
A delegation names one account and one set of selectors. Before v0.8.6 it was enforced as one owner and one set of selectors, which is a wider grant than the user agreed to.
A delegate approved for SubAccount A could run the delegated selectors against any other SubAccount held by the
same owner. Approving a bot to trade one account effectively approved it across every account that user held.
An account family is one named SubAccount together with its Virtual Accounts. v0.8.6 confines a delegated operation to the
family the delegation was granted over. Anything else is rejected with AccountOutOfScope and the whole operation
reverts, so no balance or ownership change lands.
What changed
Before v0.8.6, InstantLayer executed a delegated operation as the account owner. The AccountLayer then checked
ownership of whichever account the call named, and an owner owns all of their SubAccounts, so that check passed for every one
of them. The delegation was checked against A while the call was allowed against B.
After v0.8.6, the session carries the delegated account family alongside the signer. Ownership is checked as before, and a second check rejects any account outside that family.
Nothing about granting or revoking a delegation changed. Existing delegations keep working, and they now reach only the account they already named.
What a delegation covers
The grant covers the named SubAccount together with its Virtual Accounts.
scope = virtualAccount.isExists ? virtualAccount.parentAccount : account
allowed(target) = owner(target) == signer AND canonical(target) == scope
Virtual Accounts are inside the grant because a SubAccount and its VAs are one trading unit. Margin moves between them in
normal use: addMargin is called on a Virtual Account but draws the collateral from its parent. A delegate that
could not reach Virtual Accounts could not trade the account it was approved for.
The grant stops there. A second SubAccount of the same owner, and any Virtual Account beneath it, is outside the delegation even though one person owns both.
The scope resolves through the same rule the delegation itself is keyed by, so the enforced family and the granted family are the same set. A delegation recorded against a Virtual Account and a delegation recorded against its parent are one grant, not two.
How the scope is set
InstantLayer is the only caller that opens a confined session. When it executes an operation whose signer is not the account owner, it resolves the delegation key for the named account and installs it alongside the owner.
An owner-signed operation needs no confinement and uses a zero scope. A delegated operation resolves the named account to its
canonical SubAccount family. The administrator-controlled transientContextEnabled flag selects either the native
begin/end selector protocol and setTransientSignerScoped, or the historical setter-compatible protocol and
setSignerScoped. This is a configured protocol choice, not automatic capability detection. Both routes require
the upgraded AccountLayer scoped-signer methods and install the same owner and scope for this transaction.
address scope = signedOp.signer == owner
? address(0)
: _canonicalDelegator(signedOp.signerAccount.addr);
if (usesNativeContextSelectors) {
IAccountLayerDiamond(accountLayer).setTransientSignerScoped(owner, scope);
} else {
IAccountLayerDiamond(accountLayer).setSignerScoped(owner, scope);
}
The scope applies to both routes InstantLayer uses: operations targeting Symmio, which are wrapped through
_call, and direct AccountLayer calls such as addMargin or transferSubAccountOwnership.
The session is cleared with the signer when the operation finishes. Both setSigner and
setTransientSigner install a zero scope, so either matching clear path removes the confinement before the next
operation. Callers that do not execute through an InstantLayer delegation never open a scoped session and are unaffected. This
includes AccountManager, direct users, and protocol operations.
ViewFacet.getSignerScope reports the active scope, or address(0) when the session is unconfined.
Because the scope is opened and cleared inside one transaction, it reads zero between transactions and is only meaningful to
an observer inside the call.
AccountOutOfScope is raised inside the AccountLayer, so a caller going through InstantLayer receives it wrapped
as OperationFailed(index, reason) with the scope rejection as the inner reason, rather than as a top-level
revert.