Firma EIP-712
Este documento describe los tres dominios de firma EIP-712 utilizados para acciones on-chain: Agent Requests, Manager Actions y RSM Commands.
Resumen
La mayoría de las acciones del protocolo requieren firmas de datos tipados EIP-712. El firmante debe ser:
- Agent (API Wallet): Autorizado mediante
Exchange.addApiWallet- firma solicitudes de trading - Manager: El propietario de la cuenta - firma retiros y transferencias de activos
- RSM Signer: Controlado por el protocolo - firma comandos de liquidación/rebalanceo
El contrato Exchange verifica las firmas y reenvía las acciones al Processor, que las codifica como mensajes de ActionCaster.
Puntos de Entrada de Fondeo sin Firma
No todas las llamadas de fondeo son acciones EIP-712. Estos métodos son transacciones directas enviadas por la wallet pagadora o el router:
function depositUsdcFor(address account, uint256 amount) external;
function depositOption(address account, address token, uint256 amount) external;
depositUsdcFor es intencionalmente sin firma porque msg.sender es solo el pagador de USDC. La cuenta de Hypercall acreditada es el argumento explícito account y el campo UsdcDeposit.account del evento. Los routers y zaps pueden llamar a este método, por lo que los indexadores y servicios de backend no deben usar msg.sender para la atribución del crédito.
depositOption quema tokens de opción de msg.sender y emite Deposit(account, msg.sender, token, amount) para el indexador RSM. La ruta de acreditación de opciones está basada en eventos y no utiliza una firma de manager, agent o RSM del depositante.
Separadores de Dominio EIP-712
Los tres dominios usan la misma estructura pero nombres diferentes:
{
"name": "<DomainName>",
"version": "1",
"chainId": <chainId>,
"verifyingContract": "0x0000000000000000000000000000000000000000"
}
Chain IDs:
- Testnet:
998 - Mainnet:
999
Dominio 1: Agent Requests (HypercallAgentSign)
Nombre del Dominio: "HypercallAgentSign"
Separadores de Dominio Precalculados:
- Testnet:
0x8f0a44075cd4e0c79e5bd379a6fad5fa1329a4ea76d74e4edfa1138933d35e8a - Mainnet: use el chain ID
999y la configuración del verificador desplegado para el entorno activo.
Firmante: API Wallet (debe estar autorizada mediante Exchange.addApiWallet)
Nonce: Protección contra repetición por firmante. El motor almacena los 100 nonces más altos por firmante. Un nuevo nonce debe ser mayor que el más pequeño del conjunto y no haber sido usado. Los nonces deben estar dentro de (T - 2 días, T + 1 día) del timestamp del servidor. On-chain, Exchange.isNonceUsed(signer, nonce) rastrea el uso mediante un bitmap
HLRequestOrder
Coloca órdenes de perpetuos/spot en HyperLiquid.
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(...)")
Codificación:
- Aplique hash a cada
HLOrderusandostructHash(HLOrder) - Empaquete los hashes de las órdenes:
keccak256(abi.encodePacked(orderHashes)) - Aplique hash a la solicitud:
keccak256(abi.encode(HL_ORDER_REQUEST_TYPE_HASH, packedOrderHashes, nonce)) - Digest EIP-712:
MessageHashUtils.toTypedDataHash(domainSeparator, structHash)
Ejemplo (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);
Punto de Entrada On-Chain: Exchange.hlRequestOrder(HLRequestOrder memory request, bytes memory signature)
Salida del Processor: Codifica cada orden como ActionCasterEncoder.limitOrder(...) y devuelve acciones bytes[].
HLRequestCancel
Cancela órdenes por ID de orden.
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(...)")
Ejemplo:
const message = {
cancels: [{
asset: 0,
oid: 12345
}],
nonce: 2
};
const signature = await apiWalletSigner.signTypedData(domain, types, message);
Punto de Entrada On-Chain: Exchange.hlRequestCancel(HLRequestCancel memory request, bytes memory signature)
HLRequestCancelByCloid
Cancela órdenes por ID de orden del cliente.
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(...)")
Ejemplo:
const message = {
cancels: [{
asset: 0,
cloid: 9876543210
}],
nonce: 3
};
const signature = await apiWalletSigner.signTypedData(domain, types, message);
Punto de Entrada On-Chain: Exchange.hlRequestCancelByCloid(HLRequestCancelByCloid memory request, bytes memory signature)
Dominio 2: Manager Actions (HypercallManagerSign)
Nombre del Dominio: "HypercallManagerSign"
Separadores de Dominio Precalculados:
- Testnet:
0xd1f76b6138be892c14b71b0569bdb049cb44f239d34c78ef1ffaacd2466f9f18 - Mainnet: por definir
Firmante: Manager de la cuenta (la EOA que creó la cuenta)
Nonce: Protección contra repetición por manager. Mismo modelo de conjunto acotado que los nonces del Agent: se almacenan los 100 nonces más altos, el nuevo nonce debe superar el mínimo del conjunto y no ser un duplicado. On-chain se rastrea mediante Exchange.isNonceUsed(manager, nonce)
HLActionSendAsset
Envía activos desde la Cuenta a un destino a través de 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)")
Requisitos:
signer == managers[account](verificado on-chain)- Si
destination == Exchange, el token debe estar soportado (_checkExchangeToken)
Ejemplo:
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);
Punto de Entrada On-Chain: Exchange.hlActionSendAsset(HLActionSendAsset memory action, bytes memory signature)
Salida del Processor: Se codifica como ActionCasterEncoder.sendAsset(...).
HCActionWithdrawToken
Retira tokens del Exchange hacia la Cuenta.
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)")
Requisitos:
signer == managers[account]- El token debe estar soportado (
_checkExchangeToken- actualmente solo USDC spot) - La cuenta debe estar activada en HyperCore (
ActionCasterUtils.checkAccountActivated)
Comportamiento:
- El Exchange inicia las acciones de ActionCaster (no la Cuenta)
- Transfiere el token del Exchange a la Cuenta en HyperCore
Ejemplo:
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);
Punto de Entrada On-Chain: Exchange.hcActionWithdrawToken(HCActionWithdrawToken memory action, bytes memory signature)
HCActionWithdrawOption
Retira tokens de opción del Exchange hacia un destinatario en 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)")
Requisitos:
signer == managers[account]optiondebe estar soportada (optionRegistry.isSupportedOption(option))
Comportamiento:
- Sin acciones de ActionCaster (a diferencia de otros retiros)
- Acuña el token de opción para
recipientmedianteIOptionToken(option).mint(recipient, amountWei) - Emite
Withdraw(account, recipient, option, amountWei)
Ejemplo:
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);
Punto de Entrada On-Chain: Exchange.hcActionWithdrawOption(HCActionWithdrawOption memory action, bytes memory signature)
Dominio 3: RSM Commands (HypercallRsmSign)
Nombre del Dominio: "HypercallRsmSign"
Separadores de Dominio Precalculados:
- Testnet:
0x650b282053fb61d3fd477bdc28f6434311fe905e27cc4ca643e87e802c45938c - Mainnet: por definir
Firmante: RSM Signer (configurado mediante Exchange.setRsmSigner, verificado on-chain)
Nonce: Nonce por firmante RSM (rastreado por Exchange.nextNonce[rsmSigner])
Los comandos RSM solo pueden ser invocados por el SEQUENCER_ROLE; los market makers no los llaman directamente.
RsmCommandRebalance
Ejecuta una orden IOC reduce-only en HyperCore para rebalancear una posición.
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)")
Requisitos:
signer == rsmSigner(verificado on-chain)- El invocador debe tener el
SEQUENCER_ROLE
Comportamiento:
- Se codifica como
ActionCasterEncoder.limitOrderconreduceOnly: trueyencodedTif: 3(IOC) - Se ejecuta en la cuenta objetivo
Punto de Entrada On-Chain: Exchange.rsmCommandRebalance(RsmCommandRebalance memory cmd, bytes memory signature)
RsmCommandRepay
Deposita tokens en el Exchange en nombre de una cuenta (utilizado para repagos de liquidación).
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)")
Requisitos:
signer == rsmSigner- El invocador debe tener el
SEQUENCER_ROLE - El token debe estar soportado (
_checkExchangeToken)
Comportamiento:
- Se codifica como
ActionCasterEncoder.sendAssetcondestination: EXCHANGE - Se ejecuta en la cuenta objetivo
Punto de Entrada On-Chain: Exchange.rsmCommandRepay(RsmCommandRepay memory cmd, bytes memory signature)
Gestión de Nonces
Cada firmante (API wallet, manager, RSM signer) tiene un espacio de nonces independiente:
mapping(address signer => uint256 nonce) public nextNonce;
mapping(address signer => BitMaps.BitMap) private _nonces; // Tracks used nonces
Reglas:
- Los nonces deben ser estrictamente crecientes (no se requieren sin huecos, pero se mantiene
nextNonce) - Una vez usado, un nonce no puede reutilizarse (verificado mediante
isNonceUsed(signer, nonce)) nextNonce[signer]es el nonce mínimo garantizado como no usado (nonces inferiores pueden estar sin usar si fueron omitidos)
Consultar el Estado de un Nonce:
function isNonceUsed(address signer, uint256 nonce) external view returns (bool);
Buena Práctica: Rastree los nonces off-chain e increméntelos de forma atómica. Use nextNonce como verificación de consistencia.
Flujo de Verificación de Firma
- Off-Chain: El firmante crea el digest EIP-712 y firma con su clave privada
- On-Chain: El
Exchangerecibe el mensaje firmado y llama aProcessor.process* - Processor: Verifica la firma, recupera al firmante, codifica las acciones de ActionCaster
- Exchange: Verifica el nonce, verifica la autorización (manager/API wallet/RSM), ejecuta las acciones
Flujo de Ejemplo (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
Funciones Obsoletas
Las siguientes funciones están obsoletas pero aún existen por compatibilidad con versiones anteriores:
placeCoreOrders(usehlRequestOrder)cancelCoreOrders(usehlRequestCancel)cancelCoreOrdersByCloid(usehlRequestCancelByCloid)
Estas usan un esquema de codificación MsgPack heredado y el dominio CoreSignatures ("Exchange", chainId 1337). No las use para nuevas integraciones.
Consideraciones de Seguridad
-
Almacenamiento de Claves Privadas: Almacene de forma segura las claves de la API wallet y del manager (hardware wallet para el manager, almacenamiento cifrado para las API wallets).
-
Repetición de Nonces: Nunca reutilice nonces. Rastree los nonces off-chain e increméntelos de forma atómica.
-
Separador de Dominio: Use siempre el chain ID correcto (998 para testnet, mainnet por definir). Verifique que el separador de dominio coincida con las constantes del contrato.
-
Verificación de Firma: El contrato verifica las firmas on-chain. No confíe en la verificación de firmas off-chain para operaciones críticas.
-
Manager vs API Wallet: Los managers controlan la propiedad de la cuenta y los retiros. Las API wallets solo firman solicitudes de trading. Use claves separadas.
Referencias
- Onboarding para la creación de cuentas y la configuración de API wallets
- Autenticación de API para la autenticación de API off-chain