Skip to main content

EIP-712 Signing

This document describes the three EIP-712 signing domains used for on-chain actions: Agent Requests, Manager Actions, and RSM Commands.

Overview

All on-chain actions require EIP-712 typed data signatures. The signer must be:

  1. Agent (API Wallet): Authorized via Exchange.addApiWallet - signs trading requests
  2. Manager: The account owner - signs withdrawals and asset transfers
  3. RSM Signer: Protocol-controlled - signs liquidation/rebalance commands

The Exchange contract verifies signatures and forwards actions to the Processor, which encodes them as ActionCaster messages.

EIP-712 Domain Separators

All three domains use the same structure but different names:

{
"name": "<DomainName>",
"version": "1",
"chainId": <chainId>,
"verifyingContract": "0x0000000000000000000000000000000000000000"
}

Chain IDs:

  • Testnet: 998
  • Mainnet: TBD

Note: The Rust client currently hardcodes chainId: 1337 for some legacy signing paths; align before mainnet.

Domain 1: Agent Requests (HypercallAgentSign)

Domain Name: "HypercallAgentSign"

Precomputed Domain Separators:

  • Testnet: 0x8f0a44075cd4e0c79e5bd379a6fad5fa1329a4ea76d74e4edfa1138933d35e8a
  • Mainnet: TBD

Signer: API Wallet (must be authorized via Exchange.addApiWallet)

Nonce: Per-API-wallet nonce (tracked by Exchange.nextNonce[apiWallet])

HLRequestOrder

Places HyperLiquid perp/spot orders.

Struct:

struct HLOrder {
uint32 asset; // HyperLiquid asset ID
bool isBuy; // true = buy, false = sell
uint64 limitPx; // Limit price (fixed-point)
uint64 sz; // Size (fixed-point)
bool reduceOnly; // true = reduce-only order
uint8 encodedTif; // Time-in-force encoding
uint128 cloid; // Client order ID (0 = auto-generate)
}

struct HLRequestOrder {
HLOrder[] orders;
uint64 nonce;
}

Type Hash:

  • HL_ORDER_TYPE_HASH: keccak256("HLOrder(uint32 asset,bool isBuy,uint64 limitPx,uint64 sz,bool reduceOnly,uint8 encodedTif,uint128 cloid)")
  • HL_ORDER_REQUEST_TYPE_HASH: keccak256("HLRequestOrder(HLOrder[] orders,uint64 nonce)HLOrder(...)")

Encoding:

  1. Hash each HLOrder using structHash(HLOrder)
  2. Pack order hashes: keccak256(abi.encodePacked(orderHashes))
  3. Hash request: keccak256(abi.encode(HL_ORDER_REQUEST_TYPE_HASH, packedOrderHashes, nonce))
  4. EIP-712 digest: MessageHashUtils.toTypedDataHash(domainSeparator, structHash)

Example (ethers.js):

const domain = {
name: "HypercallAgentSign",
version: "1",
chainId: 998, // testnet
verifyingContract: ethers.ZeroAddress
};

const types = {
HLOrder: [
{ name: "asset", type: "uint32" },
{ name: "isBuy", type: "bool" },
{ name: "limitPx", type: "uint64" },
{ name: "sz", type: "uint64" },
{ name: "reduceOnly", type: "bool" },
{ name: "encodedTif", type: "uint8" },
{ name: "cloid", type: "uint128" }
],
HLRequestOrder: [
{ name: "orders", type: "HLOrder[]" },
{ name: "nonce", type: "uint64" }
]
};

const message = {
orders: [{
asset: 0, // BTC perp
isBuy: true,
limitPx: 50000000000, // $50,000 (fixed-point)
sz: 1000000, // 0.001 BTC (fixed-point)
reduceOnly: false,
encodedTif: 0, // GTC
cloid: 0 // auto-generate
}],
nonce: 1
};

const signature = await apiWalletSigner.signTypedData(domain, types, message);

On-Chain Entrypoint: Exchange.hlRequestOrder(HLRequestOrder memory request, bytes memory signature)

Processor Output: Encodes each order as ActionCasterEncoder.limitOrder(...) and returns bytes[] actions.

HLRequestCancel

Cancels orders by order ID.

Struct:

struct HLCancel {
uint32 asset;
uint64 oid; // Order ID from HyperLiquid
}

struct HLRequestCancel {
HLCancel[] cancels;
uint64 nonce;
}

Type Hash:

  • HL_CANCEL_TYPE_HASH: keccak256("HLCancel(uint32 asset,uint64 oid)")
  • HL_CANCEL_REQUEST_TYPE_HASH: keccak256("HLRequestCancel(HLCancel[] cancels,uint64 nonce)HLCancel(...)")

Example:

const message = {
cancels: [{
asset: 0,
oid: 12345
}],
nonce: 2
};

const signature = await apiWalletSigner.signTypedData(domain, types, message);

On-Chain Entrypoint: Exchange.hlRequestCancel(HLRequestCancel memory request, bytes memory signature)

HLRequestCancelByCloid

Cancels orders by client order ID.

Struct:

struct HLCancelByCloid {
uint32 asset;
uint128 cloid; // Client order ID
}

struct HLRequestCancelByCloid {
HLCancelByCloid[] cancels;
uint64 nonce;
}

Type Hash:

  • HL_CANCEL_BY_CLOID_TYPE_HASH: keccak256("HLCancelByCloid(uint32 asset,uint128 cloid)")
  • HL_CANCEL_BY_CLOID_REQUEST_TYPE_HASH: keccak256("HLRequestCancelByCloid(HLCancelByCloid[] cancels,uint64 nonce)HLCancelByCloid(...)")

Example:

const message = {
cancels: [{
asset: 0,
cloid: 9876543210
}],
nonce: 3
};

const signature = await apiWalletSigner.signTypedData(domain, types, message);

On-Chain Entrypoint: Exchange.hlRequestCancelByCloid(HLRequestCancelByCloid memory request, bytes memory signature)

Domain 2: Manager Actions (HypercallManagerSign)

Domain Name: "HypercallManagerSign"

Precomputed Domain Separators:

  • Testnet: 0xd1f76b6138be892c14b71b0569bdb049cb44f239d34c78ef1ffaacd2466f9f18
  • Mainnet: TBD

Signer: Account Manager (the EOA that created the account)

Nonce: Per-manager nonce (tracked by Exchange.nextNonce[manager])

HLActionSendAsset

Sends assets from the Account to a destination via ActionCaster.

Struct:

struct HLActionSendAsset {
address account;
uint64 nonce;
address destination;
uint32 srcDex; // Source DEX (type(uint32).max = HyperCore)
uint32 dstDex; // Destination DEX (type(uint32).max = HyperCore)
uint64 token; // Token ID
uint64 amountWei; // Amount in wei
}

Type Hash: keccak256("HLActionSendAsset(address account,uint64 nonce,address destination,uint32 srcDex,uint32 dstDex,uint64 token,uint64 amountWei)")

Requirements:

  • signer == managers[account] (verified on-chain)
  • If destination == Exchange, token must be supported (_checkExchangeToken)

Example:

const domain = {
name: "HypercallManagerSign",
version: "1",
chainId: 998,
verifyingContract: ethers.ZeroAddress
};

const types = {
HLActionSendAsset: [
{ name: "account", type: "address" },
{ name: "nonce", type: "uint64" },
{ name: "destination", type: "address" },
{ name: "srcDex", type: "uint32" },
{ name: "dstDex", type: "uint32" },
{ name: "token", type: "uint64" },
{ name: "amountWei", type: "uint64" }
]
};

const message = {
account: accountAddress,
nonce: 1,
destination: recipientAddress,
srcDex: 0xFFFFFFFF, // HyperCore
dstDex: 0xFFFFFFFF, // HyperCore
token: 0, // USDC
amountWei: 1000000 // 1 USDC (6 decimals)
};

const signature = await managerSigner.signTypedData(domain, types, message);

On-Chain Entrypoint: Exchange.hlActionSendAsset(HLActionSendAsset memory action, bytes memory signature)

Processor Output: Encodes as ActionCasterEncoder.sendAsset(...).

HCActionWithdrawToken

Withdraws tokens from the Exchange into the Account.

Struct:

struct HCActionWithdrawToken {
address account;
uint64 nonce;
uint32 srcDex;
uint32 dstDex;
uint64 token;
uint64 amountWei;
}

Type Hash: keccak256("HCActionWithdrawToken(address account,uint64 nonce,uint32 srcDex,uint32 dstDex,uint64 token,uint64 amountWei)")

Requirements:

  • signer == managers[account]
  • Token must be supported (_checkExchangeToken - currently only spot USDC)
  • Account must be activated on HyperCore (ActionCasterUtils.checkAccountActivated)

Behavior:

  • Exchange initiates ActionCaster actions (not the Account)
  • Transfers token from Exchange to Account on HyperCore

Example:

const message = {
account: accountAddress,
nonce: 2,
srcDex: 0xFFFFFFFF, // Exchange
dstDex: 0xFFFFFFFF, // HyperCore
token: 0, // USDC
amountWei: 5000000 // 5 USDC
};

const signature = await managerSigner.signTypedData(domain, types, message);

On-Chain Entrypoint: Exchange.hcActionWithdrawToken(HCActionWithdrawToken memory action, bytes memory signature)

HCActionWithdrawOption

Withdraws option tokens from the Exchange to a recipient on HyperEVM.

Struct:

struct HCActionWithdrawOption {
address account;
uint64 nonce;
address recipient;
address option; // Option token address
uint256 amountWei; // Amount in wei
}

Type Hash: keccak256("HCActionWithdrawOption(address account,uint64 nonce,address recipient,address option,uint256 amountWei)")

Requirements:

  • signer == managers[account]
  • option must be supported (optionRegistry.isSupportedOption(option))

Behavior:

  • No ActionCaster actions (unlike other withdrawals)
  • Mints option token to recipient via IOptionToken(option).mint(recipient, amountWei)
  • Emits Withdraw(account, recipient, option, amountWei)

Example:

const message = {
account: accountAddress,
nonce: 3,
recipient: recipientAddress,
option: optionTokenAddress,
amountWei: ethers.parseEther("1.0") // 1 option token
};

const signature = await managerSigner.signTypedData(domain, types, message);

On-Chain Entrypoint: Exchange.hcActionWithdrawOption(HCActionWithdrawOption memory action, bytes memory signature)

Domain 3: RSM Commands (HypercallRsmSign)

Domain Name: "HypercallRsmSign"

Precomputed Domain Separators:

  • Testnet: 0x650b282053fb61d3fd477bdc28f6434311fe905e27cc4ca643e87e802c45938c
  • Mainnet: TBD

Signer: RSM Signer (set via Exchange.setRsmSigner, verified on-chain)

Nonce: Per-RSM-signer nonce (tracked by Exchange.nextNonce[rsmSigner])

RSM commands are callable by the SEQUENCER_ROLE; market makers do not call these directly.

RsmCommandRebalance

Executes a reduce-only IOC order on HyperCore to rebalance a position.

Struct:

struct RsmCommandRebalance {
address target; // Account to rebalance
uint64 nonce;
uint32 asset;
bool isBuy;
uint64 limitPx;
uint64 sz;
}

Type Hash: keccak256("RsmCommandRebalance(address target,uint64 nonce,uint32 asset,bool isBuy,uint64 limitPx,uint64 sz)")

Requirements:

  • signer == rsmSigner (verified on-chain)
  • Caller must have SEQUENCER_ROLE

Behavior:

  • Encodes as ActionCasterEncoder.limitOrder with reduceOnly: true and encodedTif: 3 (IOC)
  • Executes on the target account

On-Chain Entrypoint: Exchange.rsmCommandRebalance(RsmCommandRebalance memory cmd, bytes memory signature)

RsmCommandRepay

Deposits tokens into the Exchange on behalf of an account (used for liquidation repayments).

Struct:

struct RsmCommandRepay {
address target;
uint64 nonce;
uint32 srcDex;
uint32 dstDex;
uint64 token;
uint64 amountWei;
}

Type Hash: keccak256("RsmCommandRepay(address target,uint64 nonce,uint32 srcDex,uint32 dstDex,uint64 token,uint64 amountWei)")

Requirements:

  • signer == rsmSigner
  • Caller must have SEQUENCER_ROLE
  • Token must be supported (_checkExchangeToken)

Behavior:

  • Encodes as ActionCasterEncoder.sendAsset with destination: EXCHANGE
  • Executes on the target account

On-Chain Entrypoint: Exchange.rsmCommandRepay(RsmCommandRepay memory cmd, bytes memory signature)

Nonce Management

Each signer (API wallet, manager, RSM signer) has an independent nonce space:

mapping(address signer => uint256 nonce) public nextNonce;
mapping(address signer => BitMaps.BitMap) private _nonces; // Tracks used nonces

Rules:

  1. Nonces must be strictly increasing (no gaps required, but nextNonce is maintained)
  2. Once used, a nonce cannot be reused (checked via isNonceUsed(signer, nonce))
  3. nextNonce[signer] is the minimum guaranteed unused nonce (lower nonces may be unused if skipped)

Query Nonce Status:

function isNonceUsed(address signer, uint256 nonce) external view returns (bool);

Best Practice: Track nonces off-chain and increment atomically. Use nextNonce as a sanity check.

Signature Verification Flow

  1. Off-Chain: Signer creates EIP-712 digest and signs with private key
  2. On-Chain: Exchange receives signed message and calls Processor.process*
  3. Processor: Verifies signature, recovers signer, encodes ActionCaster actions
  4. Exchange: Checks nonce, verifies authorization (manager/API wallet/RSM), executes actions

Example Flow (HLRequestOrder):

1. API Wallet signs HLRequestOrder with nonce=1
2. RSM Sequencer calls Exchange.hlRequestOrder(request, signature)
3. Processor.hlRequestOrder verifies signature, recovers API wallet
4. Exchange._useNonce(apiWallet, 1) checks and marks nonce as used
5. Exchange._getAccountByApiWallet(apiWallet) returns Account
6. Account.performCoreActions(orderActions) executes ActionCaster calls

Deprecated Functions

The following functions are deprecated but still exist for backward compatibility:

  • placeCoreOrders (use hlRequestOrder)
  • cancelCoreOrders (use hlRequestCancel)
  • cancelCoreOrdersByCloid (use hlRequestCancelByCloid)

These use a legacy MsgPack encoding scheme and the CoreSignatures domain ("Exchange", chainId 1337). Do not use for new integrations.

Security Considerations

  1. Private Key Storage: Store API wallet and manager keys securely (hardware wallet for manager, encrypted storage for API wallets).

  2. Nonce Replay: Never reuse nonces. Track nonces off-chain and increment atomically.

  3. Domain Separator: Always use the correct chain ID (998 for testnet, mainnet TBD). Verify domain separator matches contract constants.

  4. Signature Verification: The contract verifies signatures on-chain. Do not trust off-chain signature verification for critical operations.

  5. Manager vs API Wallet: Managers control account ownership and withdrawals. API wallets only sign trading requests. Use separate keys.

References