Assinatura EIP-712
Este documento descreve os três domínios de assinatura EIP-712 usados para ações on-chain: Agent Requests, Manager Actions e RSM Commands.
Visão Geral
A maioria das ações do protocolo requer assinaturas de dados tipados EIP-712. O signatário deve ser:
- Agent (Carteira de API): Autorizado via
Exchange.addApiWallet- assina requisições de trading - Manager: O proprietário da conta - assina saques e transferências de ativos
- RSM Signer: Controlado pelo protocolo - assina comandos de liquidação/rebalanceamento
O contrato Exchange verifica as assinaturas e encaminha as ações para o Processor, que as codifica como mensagens ActionCaster.
Pontos de Entrada de Funding Sem Assinatura
Nem toda chamada de funding é uma ação EIP-712. Estes métodos são transações diretas enviadas pela carteira pagadora ou pelo router:
function depositUsdcFor(address account, uint256 amount) external;
function depositOption(address account, address token, uint256 amount) external;
O depositUsdcFor é intencionalmente não assinado porque msg.sender é apenas o pagador do USDC. A conta Hypercall creditada é o argumento explícito account e o campo UsdcDeposit.account do evento. Routers e zaps podem chamar este método, então indexadores e serviços de backend não devem usar msg.sender para atribuição de crédito.
O depositOption queima tokens de opção de msg.sender e emite Deposit(account, msg.sender, token, amount) para o indexador RSM. O caminho de crédito de opções é orientado a eventos e não usa assinatura de manager, agent ou RSM do depositante.
Separadores de Domínio EIP-712
Os três domínios usam a mesma estrutura, mas nomes diferentes:
{
"name": "<DomainName>",
"version": "1",
"chainId": <chainId>,
"verifyingContract": "0x0000000000000000000000000000000000000000"
}
Chain IDs:
- Testnet:
998 - Mainnet:
999
Domínio 1: Agent Requests (HypercallAgentSign)
Nome do Domínio: "HypercallAgentSign"
Separadores de Domínio Pré-computados:
- Testnet:
0x8f0a44075cd4e0c79e5bd379a6fad5fa1329a4ea76d74e4edfa1138933d35e8a - Mainnet: use o chain ID
999e a configuração do verificador implantado para o ambiente ativo.
Signatário: Carteira de API (deve estar autorizada via Exchange.addApiWallet)
Nonce: Proteção contra replay por signatário. O engine armazena os 100 maiores nonces por signatário. Um novo nonce deve ser maior que o menor do conjunto e não pode já ter sido usado. Os nonces devem estar dentro de (T - 2 dias, T + 1 dia) do timestamp do servidor. On-chain, Exchange.isNonceUsed(signer, nonce) rastreia o uso via bitmap
HLRequestOrder
Envia ordens de perp/spot na 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(...)")
Codificação:
- Faça o hash de cada
HLOrderusandostructHash(HLOrder) - Empacote os hashes das ordens:
keccak256(abi.encodePacked(orderHashes)) - Faça o hash da requisição:
keccak256(abi.encode(HL_ORDER_REQUEST_TYPE_HASH, packedOrderHashes, nonce)) - Digest EIP-712:
MessageHashUtils.toTypedDataHash(domainSeparator, structHash)
Exemplo (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);
Ponto de Entrada On-Chain: Exchange.hlRequestOrder(HLRequestOrder memory request, bytes memory signature)
Saída do Processor: Codifica cada ordem como ActionCasterEncoder.limitOrder(...) e retorna ações bytes[].
HLRequestCancel
Cancela ordens pelo ID da ordem.
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(...)")
Exemplo:
const message = {
cancels: [{
asset: 0,
oid: 12345
}],
nonce: 2
};
const signature = await apiWalletSigner.signTypedData(domain, types, message);
Ponto de Entrada On-Chain: Exchange.hlRequestCancel(HLRequestCancel memory request, bytes memory signature)
HLRequestCancelByCloid
Cancela ordens pelo ID de ordem do 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(...)")
Exemplo:
const message = {
cancels: [{
asset: 0,
cloid: 9876543210
}],
nonce: 3
};
const signature = await apiWalletSigner.signTypedData(domain, types, message);
Ponto de Entrada On-Chain: Exchange.hlRequestCancelByCloid(HLRequestCancelByCloid memory request, bytes memory signature)
Domínio 2: Manager Actions (HypercallManagerSign)
Nome do Domínio: "HypercallManagerSign"
Separadores de Domínio Pré-computados:
- Testnet:
0xd1f76b6138be892c14b71b0569bdb049cb44f239d34c78ef1ffaacd2466f9f18 - Mainnet: a definir
Signatário: Manager da Conta (o EOA que criou a conta)
Nonce: Proteção contra replay por manager. Mesmo modelo de conjunto limitado dos nonces de Agent: os 100 maiores nonces são armazenados, o novo nonce deve exceder o mínimo do conjunto e não pode ser duplicado. Rastreado on-chain via Exchange.isNonceUsed(manager, nonce)
HLActionSendAsset
Envia ativos da Account para um destino 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)")
Requisitos:
signer == managers[account](verificado on-chain)- Se
destination == Exchange, o token deve ser suportado (_checkExchangeToken)
Exemplo:
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);
Ponto de Entrada On-Chain: Exchange.hlActionSendAsset(HLActionSendAsset memory action, bytes memory signature)
Saída do Processor: Codifica como ActionCasterEncoder.sendAsset(...).
HCActionWithdrawToken
Saca tokens da Exchange para a 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)")
Requisitos:
signer == managers[account]- O token deve ser suportado (
_checkExchangeToken- atualmente apenas USDC spot) - A conta deve estar ativada na HyperCore (
ActionCasterUtils.checkAccountActivated)
Comportamento:
- A Exchange inicia ações ActionCaster (não a Account)
- Transfere o token da Exchange para a Account na HyperCore
Exemplo:
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);
Ponto de Entrada On-Chain: Exchange.hcActionWithdrawToken(HCActionWithdrawToken memory action, bytes memory signature)
HCActionWithdrawOption
Saca tokens de opção da Exchange para um destinatário na 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]optiondeve ser suportado (optionRegistry.isSupportedOption(option))
Comportamento:
- Sem ações ActionCaster (diferente dos outros saques)
- Faz mint do token de opção para o
recipientviaIOptionToken(option).mint(recipient, amountWei) - Emite
Withdraw(account, recipient, option, amountWei)
Exemplo:
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);
Ponto de Entrada On-Chain: Exchange.hcActionWithdrawOption(HCActionWithdrawOption memory action, bytes memory signature)
Domínio 3: RSM Commands (HypercallRsmSign)
Nome do Domínio: "HypercallRsmSign"
Separadores de Domínio Pré-computados:
- Testnet:
0x650b282053fb61d3fd477bdc28f6434311fe905e27cc4ca643e87e802c45938c - Mainnet: a definir
Signatário: RSM Signer (definido via Exchange.setRsmSigner, verificado on-chain)
Nonce: Nonce por signatário RSM (rastreado por Exchange.nextNonce[rsmSigner])
Os comandos RSM podem ser chamados pelo SEQUENCER_ROLE; market makers não os chamam diretamente.
RsmCommandRebalance
Executa uma ordem IOC reduce-only na HyperCore para rebalancear uma posição.
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)- O chamador deve ter
SEQUENCER_ROLE
Comportamento:
- Codifica como
ActionCasterEncoder.limitOrdercomreduceOnly: trueeencodedTif: 3(IOC) - Executa na conta alvo
Ponto de Entrada On-Chain: Exchange.rsmCommandRebalance(RsmCommandRebalance memory cmd, bytes memory signature)
RsmCommandRepay
Deposita tokens na Exchange em nome de uma conta (usado para reembolsos de liquidação).
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- O chamador deve ter
SEQUENCER_ROLE - O token deve ser suportado (
_checkExchangeToken)
Comportamento:
- Codifica como
ActionCasterEncoder.sendAssetcomdestination: EXCHANGE - Executa na conta alvo
Ponto de Entrada On-Chain: Exchange.rsmCommandRepay(RsmCommandRepay memory cmd, bytes memory signature)
Gerenciamento de Nonces
Cada signatário (carteira de API, manager, RSM signer) tem um espaço de nonces independente:
mapping(address signer => uint256 nonce) public nextNonce;
mapping(address signer => BitMaps.BitMap) private _nonces; // Tracks used nonces
Regras:
- Os nonces devem ser estritamente crescentes (lacunas não são obrigatórias, mas
nextNonceé mantido) - Uma vez usado, um nonce não pode ser reutilizado (verificado via
isNonceUsed(signer, nonce)) nextNonce[signer]é o nonce mínimo garantido como não usado (nonces menores podem estar não usados se foram pulados)
Consultar Status do Nonce:
function isNonceUsed(address signer, uint256 nonce) external view returns (bool);
Boa Prática: Rastreie os nonces off-chain e incremente de forma atômica. Use nextNonce como verificação de sanidade.
Fluxo de Verificação de Assinatura
- Off-Chain: O signatário cria o digest EIP-712 e assina com a chave privada
- On-Chain: A
Exchangerecebe a mensagem assinada e chamaProcessor.process* - Processor: Verifica a assinatura, recupera o signatário, codifica as ações ActionCaster
- Exchange: Verifica o nonce, verifica a autorização (manager/carteira de API/RSM), executa as ações
Exemplo de Fluxo (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
Funções Descontinuadas
As seguintes funções estão descontinuadas, mas ainda existem por compatibilidade retroativa:
placeCoreOrders(usehlRequestOrder)cancelCoreOrders(usehlRequestCancel)cancelCoreOrdersByCloid(usehlRequestCancelByCloid)
Elas usam um esquema legado de codificação MsgPack e o domínio CoreSignatures ("Exchange", chainId 1337). Não use em novas integrações.
Considerações de Segurança
-
Armazenamento de Chaves Privadas: Armazene as chaves da carteira de API e do manager com segurança (hardware wallet para o manager, armazenamento criptografado para carteiras de API).
-
Replay de Nonce: Nunca reutilize nonces. Rastreie os nonces off-chain e incremente de forma atômica.
-
Separador de Domínio: Sempre use o chain ID correto (998 para testnet, mainnet a definir). Verifique se o separador de domínio corresponde às constantes do contrato.
-
Verificação de Assinatura: O contrato verifica as assinaturas on-chain. Não confie em verificação de assinatura off-chain para operações críticas.
-
Manager vs Carteira de API: Managers controlam a propriedade da conta e os saques. Carteiras de API assinam apenas requisições de trading. Use chaves separadas.
Referências
- Onboarding para criação de conta e configuração de carteira de API
- Autenticação da API para autenticação off-chain da API