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
Most protocol actions require EIP-712 typed data signatures. The signer must be:
- Agent (API Wallet): Authorized via
Exchange.addApiWallet- signs trading requests - Manager: The account owner - signs withdrawals and asset transfers
- 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.
Unsigned Funding Entrypoints
Not every funding call is an EIP-712 action. These methods are direct transactions sent by the paying wallet or router:
function depositUsdcFor(address account, uint256 amount) external;
function depositOption(address account, address token, uint256 amount) external;
depositUsdcFor is intentionally unsigned because msg.sender is only the USDC payer. The credited Hypercall account is the explicit account argument and the UsdcDeposit.account event field. Routers and zaps may call this method, so indexers and backend services must not use msg.sender for credit attribution.
depositOption burns option tokens from msg.sender and emits Deposit(account, msg.sender, token, amount) for the RSM indexer. The option credit path is event-driven and does not use a manager, agent, or RSM signature from the depositor.
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:
999
Domain 1: Agent Requests (HypercallAgentSign)
Domain Name: "HypercallAgentSign"
Precomputed Domain Separators:
- Testnet:
0x8f0a44075cd4e0c79e5bd379a6fad5fa1329a4ea76d74e4edfa1138933d35e8a - Mainnet: use chain ID
999and the deployed verifier configuration for the active environment.
Signer: API Wallet (must be authorized via Exchange.addApiWallet)
Nonce: Per-signer replay protection. The engine stores the 100 highest nonces per signer. A new nonce must be greater than the smallest in the set and not already used. Nonces must be within (T - 2 days, T + 1 day) of the server timestamp. On-chain, Exchange.isNonceUsed(signer, nonce) tracks usage via a bitmap
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:
- Hash each
HLOrderusingstructHash(HLOrder) - Pack order hashes:
keccak256(abi.encodePacked(orderHashes)) - Hash request:
keccak256(abi.encode(HL_ORDER_REQUEST_TYPE_HASH, packedOrderHashes, nonce)) - 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 replay protection. Same bounded-set model as Agent nonces: the 100 highest nonces are stored, new nonce must exceed the set minimum and not be a duplicate. On-chain tracked via Exchange.isNonceUsed(manager, nonce)
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]optionmust be supported (optionRegistry.isSupportedOption(option))
Behavior:
- No ActionCaster actions (unlike other withdrawals)
- Mints option token to
recipientviaIOptionToken(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.limitOrderwithreduceOnly: trueandencodedTif: 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.sendAssetwithdestination: 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:
- Nonces must be strictly increasing (no gaps required, but
nextNonceis maintained) - Once used, a nonce cannot be reused (checked via
isNonceUsed(signer, nonce)) 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
- Off-Chain: Signer creates EIP-712 digest and signs with private key
- On-Chain:
Exchangereceives signed message and callsProcessor.process* - Processor: Verifies signature, recovers signer, encodes ActionCaster actions
- 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(usehlRequestOrder)cancelCoreOrders(usehlRequestCancel)cancelCoreOrdersByCloid(usehlRequestCancelByCloid)
These use a legacy MsgPack encoding scheme and the CoreSignatures domain ("Exchange", chainId 1337). Do not use for new integrations.
Security Considerations
-
Private Key Storage: Store API wallet and manager keys securely (hardware wallet for manager, encrypted storage for API wallets).
-
Nonce Replay: Never reuse nonces. Track nonces off-chain and increment atomically.
-
Domain Separator: Always use the correct chain ID (998 for testnet, mainnet TBD). Verify domain separator matches contract constants.
-
Signature Verification: The contract verifies signatures on-chain. Do not trust off-chain signature verification for critical operations.
-
Manager vs API Wallet: Managers control account ownership and withdrawals. API wallets only sign trading requests. Use separate keys.
References
- Onboarding for account creation and API wallet setup
- API Authentication for off-chain API authentication