For a high-level architectural overview, see the InstantLayer Overview.
Overview
InstantLayer allows PartyB to sign operations off-chain that can be executed by anyone on-chain. This enables batched execution and delegated transaction submission.
New: Self-Execution Mode - When PartyB is the transaction sender (msg.sender), signature
verification is skipped for their own operations. This means PartyB can execute their operations directly without signing,
while still providing user signatures for user operations in the same batch.
What PartyB Needs to Sign
PartyB signs a SignedOperation struct using EIP-712 typed data signing.
SignedOperation Structure
interface SignedOperation {
signer: string; // PartyB contract address
target: string; // Symmio contract address
callData: string; // Encoded function call (e.g., lockQuote, openPosition)
signerAccount: {
addr: string; // PartyB contract address (must match signer)
isPartyB: boolean; // Must be true for PartyB
};
flexFields: FlexField[]; // Modifiable calldata regions (empty for standard ops)
maxUses: number; // Max execution count (1 for standard ops)
replayAttackHeader: {
nonce: bigint; // Sequential nonce (0 for salt-only mode)
deadline: bigint; // Unix timestamp expiry (0 for no deadline)
salt: string; // Unique 32-byte hex string
};
}
interface FlexField {
offset: number; // Byte offset after 4-byte selector
length: number; // Number of bytes (typically 32)
authorizedFlexFiller: string; // Address authorized to fill this field
}
For standard PartyB operations, flexFields is always [] and maxUses is 1.
See the InstantLayer Overview for flex field
details.
EIP-712 Configuration
Domain
const domain = {
name: "SymmioInstantLayer",
version: "1",
chainId: <network_chain_id>,
verifyingContract: <instant_layer_address>
};
HyperEVM v0.8.5 Deployment
For the current HyperEVM v0.8.5 deployment, use the Instant Layer address as the EIP-712 verifyingContract and
the Symmio address as the target for Symmio core calls:
| Contract | Address |
|---|---|
| Symmio | 0x57331038c21982116EE9b0906E4a5c5cB52dcE2e |
| Collateral | 0xb88339CB7199b77E23DB6E890353E22632Ba630f |
| Account Layer | 0x46493c376758Da47823D7E3Ae5d417eA6546eEB3 |
| Instant Layer | 0x72DBF07457b2712b160F67A85D338F860c1CA620 |
| Muon Signature Verifier | 0xcFEedF3C16f59d19eEE5F18C708cf8e84A9340b0 |
| Create2 Factory | 0x93a457232a3376E1F94f368A85D0b8BC469ead00 |
| Symbol Manager | 0xe877d634D5Fe44172B983cBd5900E30169a7A6Ce |
| Main MultiSig | 0x5146C35725d9b8F11A84ebD4a3abe9845698Ada9 |
| Signature Verifier | 0xaD97EdE10D84BD797a3240864Ca7898cf666A784 |
| Instant Withdraw | 0xb1b12E91E456D02184E4B934Bd8dD0c443962015 |
| Liquidator | 0x97EDFA4Be38Bfd934E08D4D1222c2Cf976b2969a |
const HYPEREVM_CHAIN_ID = 999n;
const INSTANT_LAYER_ADDRESS = "0x72DBF07457b2712b160F67A85D338F860c1CA620";
const SYMMIO_ADDRESS = "0x57331038c21982116EE9b0906E4a5c5cB52dcE2e";
const ACCOUNT_LAYER_ADDRESS = "0x46493c376758Da47823D7E3Ae5d417eA6546eEB3";
Types
const types = {
Account: [
{ name: "addr", type: "address" },
{ name: "isPartyB", type: "bool" },
],
FlexField: [
{ name: "offset", type: "uint256" },
{ name: "length", type: "uint256" },
{ name: "authorizedFlexFiller", type: "address" },
],
ReplayAttackHeader: [
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
{ name: "salt", type: "bytes32" },
],
SignedOperation: [
{ name: "signer", type: "address" },
{ name: "target", type: "address" },
{ name: "callData", type: "bytes" },
{ name: "signerAccount", type: "Account" },
{ name: "flexFields", type: "FlexField[]" },
{ name: "maxUses", type: "uint256" },
{ name: "replayAttackHeader", type: "ReplayAttackHeader" },
],
};
Code Examples
Complete Signing Example (ethers.js v6)
import { ethers } from "ethers";
import { randomBytes } from "crypto";
// Configuration
const INSTANT_LAYER_ADDRESS = "0x..."; // InstantLayer contract address
const SYMMIO_ADDRESS = "0x..."; // Symmio contract address
const PARTY_B_ADDRESS = "0x..."; // Your PartyB contract address
// EIP-712 Domain
async function getDomain(provider: ethers.Provider) {
const network = await provider.getNetwork();
return {
name: "SymmioInstantLayer",
version: "1",
chainId: network.chainId,
verifyingContract: INSTANT_LAYER_ADDRESS,
};
}
// EIP-712 Types
const types = {
Account: [
{ name: "addr", type: "address" },
{ name: "isPartyB", type: "bool" },
],
FlexField: [
{ name: "offset", type: "uint256" },
{ name: "length", type: "uint256" },
{ name: "authorizedFlexFiller", type: "address" },
],
ReplayAttackHeader: [
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
{ name: "salt", type: "bytes32" },
],
SignedOperation: [
{ name: "signer", type: "address" },
{ name: "target", type: "address" },
{ name: "callData", type: "bytes" },
{ name: "signerAccount", type: "Account" },
{ name: "flexFields", type: "FlexField[]" },
{ name: "maxUses", type: "uint256" },
{ name: "replayAttackHeader", type: "ReplayAttackHeader" },
],
};
// Generate unique salt
function generateSalt(): string {
return "0x" + randomBytes(32).toString("hex");
}
// Create and sign an operation
async function signPartyBOperation(
signer: ethers.Wallet,
callData: string,
nonce: bigint = 0n,
deadlineSeconds: number = 300 // 5 minutes default
) {
const provider = signer.provider!;
const domain = await getDomain(provider);
const block = await provider.getBlock("latest");
const deadline = deadlineSeconds > 0
? BigInt(block!.timestamp) + BigInt(deadlineSeconds)
: 0n;
const operation = {
signer: PARTY_B_ADDRESS,
target: SYMMIO_ADDRESS,
callData: callData,
signerAccount: {
addr: PARTY_B_ADDRESS,
isPartyB: true,
},
flexFields: [],
maxUses: 1,
replayAttackHeader: {
nonce: nonce,
deadline: deadline,
salt: generateSalt(),
},
};
const signature = await signer.signTypedData(domain, types, operation);
return { operation, signature };
}
Example: Sign a lockQuote Operation
// Encode the lockQuote function call
const symmioInterface = new ethers.Interface([
"function lockQuote(uint256 quoteId, uint256 upnl, int256 price, bytes memory upnlSig, bytes memory priceSig)"
]);
const callData = symmioInterface.encodeFunctionData("lockQuote", [
quoteId,
upnl,
price,
upnlSig,
priceSig
]);
// Sign the operation
const { operation, signature } = await signPartyBOperation(
hedgerSigner, // The signer configured in your PartyB contract
callData,
0n, // nonce: 0 for salt-only mode
300 // deadline: 5 minutes
);
// Submit to InstantLayer (can be done by anyone)
await instantLayer.executeBatch([operation], [signature], [[]], [[]]);
Example: Sign an openPosition Operation
const symmioInterface = new ethers.Interface([
"function openPosition(uint256 quoteId, uint256 filledAmount, uint256 openedPrice, uint256 upnl, int256 price, bytes memory upnlSig, bytes memory priceSig)"
]);
const callData = symmioInterface.encodeFunctionData("openPosition", [
quoteId,
filledAmount,
openedPrice,
upnl,
price,
upnlSig,
priceSig
]);
const { operation, signature } = await signPartyBOperation(
hedgerSigner,
callData
);
Example: Batch Multiple Operations
// Create multiple operations
const lockOp = await signPartyBOperation(hedgerSigner, lockQuoteCallData, 1n, 300);
const openOp = await signPartyBOperation(hedgerSigner, openPositionCallData, 2n, 300);
// Execute as batch
await instantLayer.executeBatch(
[lockOp.operation, openOp.operation],
[lockOp.signature, openOp.signature],
[[], []],
[[], []]
);
Template Execution
Use executeBatch when every operation can be encoded independently. Use executeTemplate when a later
operation needs a value returned by an earlier operation, such as a quoteId returned by a user quote operation
and then consumed by PartyB lockQuote and openPosition.
The signature still covers the original SignedOperation.callData. For templated PartyB calls, encode placeholder
32-byte values in the positions that the template will replace. InstantLayer verifies the operation signature first, then
applies flex fills, then injects template results into the calldata.
function executeTemplate(
uint256 templateId,
SignedOperation[] calldata signedOps,
bytes[] calldata signatures,
bytes[][] calldata fills,
bytes[][] calldata flexFillerSignatures
) external returns (bytes[] memory results);
The array rules are the same as executeBatch:
signedOps.length == signatures.length == fills.length == flexFillerSignatures.lengthsignedOps.lengthmust also match the number of operations in the selected template-
pass
[]forfills[i]andflexFillerSignatures[i]when the operation has no flex fields - pass
"0x"for a PartyB signature only when PartyB is the transaction sender for its own operation
Template injection uses three arrays per operation:
| Field | Meaning |
|---|---|
insertionPoints |
ABI argument offsets after the 4-byte function selector where a 32-byte result should be inserted |
sourceIndices |
Previous operation indexes whose return data should be read |
sourceOffsets |
32-byte slot offsets inside the source return data |
For example, if op 1 returns a quoteId as its first return value, an operation with
insertionPoints: [0], sourceIndices: [1], and sourceOffsets: [0] replaces its first ABI
argument with that quoteId.
HyperEVM v0.8.5 Templates
The current HyperEVM v0.8.5 Instant Layer has the standard template set registered in this order:
| Template ID | Name | Operations | PartyB relevance |
|---|---|---|---|
0 |
InstantOpen |
4 |
User quote flow followed by PartyB lockQuote and openPosition; the quote ID returned by
the quote operation is injected into both PartyB calls. instantOpenMode is enabled for this template.
|
1 |
InstantClose |
2 | User close request followed by PartyB close fill. No result injection is configured. |
2 |
InstantCloseWithAllocation |
3 | Close flow with an additional allocation/deallocation step. No result injection is configured. |
Admins can add, disable, or update template modes, so production clients should read the current state before relying on hardcoded IDs:
const nextTemplateId = await instantLayer.getNextTemplateId();
const instantOpen = await instantLayer.getTemplate(0);
const instantOpenMode = await instantLayer.templateInstantOpenMode(0);
Example: Execute The HyperEVM InstantOpen Template
For InstantOpen, the PartyB operations should be signed with placeholder quoteId values. The
template injects the quote ID returned by the quote operation before executing lockQuote and
openPosition.
const templateId = 0n; // InstantOpen on the HyperEVM v0.8.5 deployment
const lockQuoteCallData = symmioInterface.encodeFunctionData("lockQuote", [
0n, // quoteId placeholder; replaced by the template
upnlSig,
]);
const openPositionCallData = symmioInterface.encodeFunctionData("openPosition", [
0n, // quoteId placeholder; replaced by the template
filledAmount,
openedPrice,
upnlSig,
]);
const lockOp = await signPartyBOperation(hedgerSigner, lockQuoteCallData, 1n, 300);
const openOp = await signPartyBOperation(hedgerSigner, openPositionCallData, 2n, 300);
await instantLayer.executeTemplate(
templateId,
[userMarginOrQuoteOp, userQuoteOp, lockOp.operation, openOp.operation],
[userMarginOrQuoteSig, userQuoteSig, lockOp.signature, openOp.signature],
[[], [], [], []],
[[], [], [], []]
);
If PartyB is submitting the transaction itself and its registered PartyB contract has OPERATOR_ROLE, the PartyB
entries can use empty signatures:
await instantLayer.connect(partyB).executeTemplate(
0n,
[userMarginOrQuoteOp, userQuoteOp, lockOp.operation, openOp.operation],
[userMarginOrQuoteSig, userQuoteSig, "0x", "0x"],
[[], [], [], []],
[[], [], [], []]
);
Self-Execution Mode (No Signature Required)
When PartyB is the transaction sender (msg.sender), signature verification is automatically skipped for
operations where signer == msg.sender. This allows PartyB to execute operations directly without signing them.
How It Works
- The contract verifies that the signer is a registered PartyB
- If
signer == msg.sender, signature verification is bypassed - An empty signature (
"0x") can be provided for self-executed operations - All other security checks (replay protection, nonce, deadline) still apply
Example: PartyB Self-Execution
// PartyB executing their own operation - no signature needed
function createSelfOperation(callData: string): SignedOperation {
return {
signer: PARTY_B_ADDRESS, // Same as msg.sender when PartyB calls
target: SYMMIO_ADDRESS,
callData: callData,
signerAccount: {
addr: PARTY_B_ADDRESS,
isPartyB: true,
},
flexFields: [],
maxUses: 1,
replayAttackHeader: {
nonce: 0n,
deadline: 0n, // No deadline needed
salt: generateSalt(),
},
};
}
// Execute directly as PartyB - empty signature
const operation = createSelfOperation(lockQuoteCallData);
await instantLayer.executeBatch([operation], ["0x"], [[]], [[]]);
Example: Mixed Batch (User Signed + PartyB Self-Execution)
This is the most common pattern - combining user-signed operations with PartyB self-executed operations:
// User's sendQuote operation - requires signature
const userOperation = {
signer: userAddress,
target: SYMMIO_ADDRESS,
callData: encodedSendQuote,
signerAccount: { addr: userAccountAddress, isPartyB: false },
flexFields: [],
maxUses: 1,
replayAttackHeader: { nonce: 0n, deadline: deadline, salt: generateSalt() }
};
const userSignature = await userSigner.signTypedData(domain, types, userOperation);
// PartyB's lockQuote operation - no signature needed when PartyB is msg.sender
const partyBOperation = {
signer: PARTY_B_ADDRESS,
target: SYMMIO_ADDRESS,
callData: encodedLockQuote,
signerAccount: { addr: PARTY_B_ADDRESS, isPartyB: true },
flexFields: [],
maxUses: 1,
replayAttackHeader: { nonce: 0n, deadline: 0n, salt: generateSalt() }
};
// PartyB calls executeBatch directly
await instantLayer.executeBatch(
[userOperation, partyBOperation],
[userSignature, "0x"], // User signature + empty for PartyB
[[], []], // No flex fills
[[], []] // No flex filler signatures
);
When to Use Self-Execution
| Scenario | Signature Required? |
|---|---|
| PartyB calls directly, executing own operation | No (empty "0x") |
| PartyB calls directly, executing user operation | Yes (user signature) |
| Operator calls, executing PartyB operation | Yes (PartyB signature) |
| Operator calls, executing user operation | Yes (user signature) |
Nonce Modes
Salt-Only Mode (nonce = 0)
- Operations can be executed in any order
- Each operation must have a unique salt
- Recommended for independent operations
Sequential Mode (nonce > 0)
- Operations must be executed in order
- Next nonce must be
currentNonce + 1 - Query current nonce:
instantLayer.nonces(partyBAddress)
// Get current nonce for your PartyB
const currentNonce = await instantLayer.nonces(PARTY_B_ADDRESS);
const nextNonce = currentNonce + 1n;
PartyB Contract Setup
Your PartyB contract must:
- Be registered with InstantLayer:
// Called by InstantLayer admin
instantLayer.registerPartyBs([partyBAddress]);
- Have a signer configured (for ERC-1271 signature verification):
// In your PartyB contract
function setSigner(address _signer) external onlyRole(SETTER_ROLE) {
signer = _signer;
}
- Implement ERC-1271 (already implemented in SymmioPartyB):
function isValidSignature(bytes32 hash, bytes memory signature)
external view returns (bytes4)
{
return SignatureChecker.isValidSignatureNow(signer, hash, signature)
? bytes4(0x1626ba7e)
: bytes4(0xffffffff);
}
Validation Rules
Your operations must satisfy:
| Rule | Description |
|---|---|
signer == signerAccount.addr |
Signer must match account address |
signerAccount.isPartyB == true |
Must be true for PartyB operations |
deadline == 0 OR deadline > block.timestamp |
Valid deadline |
nonce == 0 OR nonce == currentNonce + 1 |
Valid nonce |
callData.length >= 4 |
Must include function selector |
| Unique salt | Salt must not have been used before |
| Valid signature OR self-execution | Signature required unless signer == msg.sender |
Error Reference
| Error | Cause |
|---|---|
InstantLayerInvalidCallDataLength |
callData is less than 4 bytes |
InstantLayerOperationExpired |
Deadline has passed |
InstantLayerTargetNotWhitelisted |
Target contract not whitelisted |
InstantLayerInvalidOperationSignature |
Signature verification failed (skipped if signer == msg.sender) |
InstantLayerInvalidNonce |
Nonce is not sequential (when > 0) |
InstantLayerOperationAlreadyUsed |
Salt/operation already executed |
InstantLayerSignerMismatch |
Signer doesn't match signerAccount.addr |
InstantLayerInvalidPartyB |
PartyB is not registered |
Quick Reference
// Option 1: Signed operation (when executed by a third-party operator)
const operation = {
signer: PARTY_B_ADDRESS,
target: SYMMIO_ADDRESS,
callData: encodedFunctionCall,
signerAccount: { addr: PARTY_B_ADDRESS, isPartyB: true },
flexFields: [],
maxUses: 1,
replayAttackHeader: { nonce: 0n, deadline: 0n, salt: generateSalt() }
};
const signature = await signer.signTypedData(domain, types, operation);
await instantLayer.connect(operator).executeBatch(
[operation], [signature], [[]], [[]]
);
// Option 2: Self-execution (when PartyB calls directly)
const operation = {
signer: PARTY_B_ADDRESS,
target: SYMMIO_ADDRESS,
callData: encodedFunctionCall,
signerAccount: { addr: PARTY_B_ADDRESS, isPartyB: true },
flexFields: [],
maxUses: 1,
replayAttackHeader: { nonce: 0n, deadline: 0n, salt: generateSalt() }
};
await instantLayer.connect(partyB).executeBatch(
[operation], ["0x"], [[]], [[]]
); // Empty signature
Related Documentation
- InstantLayer Overview - High-level architecture and concepts