Summary
Before v0.8.6, granting a delegation was its own transaction: the owner called grantDelegation directly or a
relayer submitted a grantBatchDelegationBySig payload, and only afterwards could an operation signed by the
delegate execute. A flow such as “authorize this session key, then let it configure my account” therefore took at
least two transactions, and the second could not be prepared until the first had landed.
In v0.8.6 the grant itself is a SignedOperation: an operation that targets the InstantLayer contract itself and
carries grantDelegation(DelegationInfo) as its calldata. The InstantLayer recognizes the self-target, verifies
the operation through the normal EIP-712 pipeline, and applies the grant in place instead of making an external call. Because
both executeBatch and executeTemplate verify each operation immediately before executing it, an
operation at index 1 can already use a delegation granted at index 0 — within one atomic transaction.
No entry point changed shape. There are no new parameters, no new signing scheme, and no new EIP-712 types: the owner signs
the grant with the same SignedOperation typehash used for every other operation.
A grant can also name several delegates at once. grantDelegations(DelegationInfo[]) takes a list of entries for
the same account, each with its own delegate, selectors, and expiry, and is accepted as a self-targeted operation exactly like
the single form. This is what keeps setup at one wallet signature when an account needs more than one delegate — a
browser session key plus a server-side signer, for example.
The grant operation
A grant operation is an ordinary SignedOperation with two distinguishing properties:
targetis the InstantLayer contract itself, and-
callDatais an ABI-encoded call tograntDelegation(DelegationInfo)orgrantDelegations(DelegationInfo[]).
DelegationInfo {
Account account; // the account granting delegation (must match signerAccount)
address delegatedSigner; // the session key / delegate
bytes4[] selectors; // functions the delegate may sign for
uint256 expiryTimestamp; // when the delegation lapses
}
Execution decodes the DelegationInfo, normalizes the granting account to its canonical account family (a Virtual
Account resolves to its parent SubAccount), writes each selector permission, clears any pending revocation for those
selectors, and emits DelegationGranted per selector — the same shared logic behind the direct and
by-signature grant paths.
The multi-delegate form runs that same logic once per entry. Every entry must name the operation's
signerAccount; an empty list, or an entry for another account, reverts with InvalidDelegation.
Verification rules
Self-targeted operations are valid only as delegation grants, and grants carry stricter shape rules than ordinary operations:
-
Only the
grantDelegationandgrantDelegationsselectors may target the InstantLayer. Any other self-targeted calldata reverts withInvalidGrantOperation, so the target whitelist never opens other InstantLayer functions to signed operations. -
The operation signer must be the account owner. A delegate can never sign a grant — not even one holding a
delegation over the
grantDelegationorgrantDelegationsselector — so a session key cannot mint further delegations for itself or for anyone else. -
maxUsesmust be exactly1. Granting clears pending revocation timestamps, so a replayable grant operation could silently cancel a revocation the owner had already scheduled. -
flexFieldsmust be empty, and the grant decodes from the original signed calldata rather than the fill-and-injection-modified copy. Neither flex fills nor template result injection can rewrite the delegate, the selectors, or the expiry after the owner signed. -
DelegationInfo.accountmust equal the operation'ssignerAccount— for every entry of a multi-delegate grant — and PartyB accounts cannot grant.
Everything else is the standard operation machinery: the EIP-712 signature check, the salt-based usage count, the optional
ordered nonce, and the deadline all apply unchanged. Replaying a grant operation fails with MaxUsesExceeded. The
shared grant validation also enforces SelfDelegation, DelegationExpired, and non-empty selectors.
Usage
Build the grant like any other operation; only the target and calldata differ:
const grantOp = {
signer: owner.address, // the account owner signs
target: instantLayerAddress, // self-targeted
callData: instantLayer.interface.encodeFunctionData("grantDelegation", [{
account: { addr: subAccount, isPartyB: false },
delegatedSigner: sessionKey.address,
selectors: [bindToPartyBSelector, approveOperationalFeeSelector],
expiryTimestamp: now + 3600n,
}]),
signerAccount: { addr: subAccount, isPartyB: false },
flexFields: [],
maxUses: 1, // required for grant operations
replayAttackHeader: { nonce: 0n, deadline, salt },
}
const grantSig = await owner.signTypedData(domain, signedOperationTypes, grantOp)
To authorize more than one delegate under the same signature, encode grantDelegations instead. Each entry is a
full DelegationInfo, so the delegates can differ in both scope and lifetime:
callData: instantLayer.interface.encodeFunctionData("grantDelegations", [[
{ // short-lived browser key, only what it needs for trading
account: { addr: subAccount, isPartyB: false },
delegatedSigner: sessionKey.address,
selectors: [sendQuoteSelector, approveOperationalFeeSelector],
expiryTimestamp: now + 3600n,
},
{ // longer-lived server-side signer, its own selector set
account: { addr: subAccount, isPartyB: false },
delegatedSigner: serverSigner.address,
selectors: [requestToClosePositionSelector],
expiryTimestamp: now + 86400n,
},
]])
Then submit it in front of the operations that depend on it. Operations signed by the session key list the granted selectors in their calldata and verify against the fresh delegation:
await instantLayer.executeBatch(
[grantOp, bindOp, approveFeeOp],
[grantSig, bindSig, approveFeeSig],
[[], [], []],
[[], [], []],
)
The GaslessLayer needs no dedicated surface for this: relayInstantBatch forwards a grant operation like any other
instant operation. Pass 0 for its entry in the required wallet ID array. Billing prices it by its selector (grantDelegation
or grantDelegations) through the normal fee schedule (setSelectorFeeConfig), including the daily
free-operation quota and Virtual Account → parent payer resolution.
Scenario: one-signature onboarding
The motivating flow is gasless user onboarding, end to end:
-
Show the deposit address. The service reads
getGaslessWalletAddress(user, walletId)— a deterministic CREATE2 address — and shows it to the user. No transaction, no gas. - The user deposits. Collateral is bridged to that address from anywhere.
-
The relayer settles.
settleDepositToNewAccountsweeps the wallet, takes the flat deposit fee, creates a SubAccount owned by the user through the AccountLayer, and deposits the remainder into core. -
One relayed batch finishes setup. The batch carries the user's single signature and the session key's
signatures:
The grant lands first, so the session-key operations verify against it inside the same transaction. Operational fees are collected after the batch executes, so the in-batch[ grantOp, // signed by the user: delegate bindToPartyB + approveOperationalFee // to the session key bindOp, // signed by the session key: bindToPartyB(solver) approveFeeOp, // signed by the session key: approveOperationalFee([gaslessLayer], budget) ]approveOperationalFeefunds the billing of the very batch that carried it. The whole sequence is atomic: if any step fails, no delegation, binding, or approval lands.
From the user's perspective: one deposit and one signature. Every subsequent action — and everything in the setup batch beyond the grant itself — is signed by the session key and relayed gaslessly.
The same holds when 1-click trading needs a second delegate next to the session key. The grant operation then encodes
grantDelegations with one entry per delegate, the wallet still signs exactly once, and operations signed by
either delegate ride the same batch. Two things would quietly add a prompt back: deriving the session key from a wallet
signature (the grant must already contain the key's address), and learning the second delegate's address only after the grant
was signed.
Existing grant paths
The operation form is additive. grantDelegation and grantDelegations as direct owner calls and
grantBatchDelegationBySig with its dedicated EIP-712 payload and delegation nonce all keep working unchanged;
every path shares the same validation and writes the same delegation storage. For new integrations the operation form is
preferred, since it composes with other operations in a batch and rides the standard operation replay machinery instead of a
separate nonce track.