Cette page a été traduite automatiquement. La version anglaise fait référence. Lire en anglais
Aller au contenu principal

Signature EIP-712

Ce document décrit les trois domaines de signature EIP-712 utilisés pour les actions on-chain : Requêtes Agent, Actions Manager et Commandes RSM.

Vue d'ensemble

La plupart des actions du protocole nécessitent des signatures de données typées EIP-712. Le signataire doit être :

  1. Agent (portefeuille API) : autorisé via Exchange.addApiWallet - signe les requêtes de trading
  2. Manager : le propriétaire du compte - signe les retraits et les transferts d'actifs
  3. Signataire RSM : contrôlé par le protocole - signe les commandes de liquidation/rééquilibrage

Le contrat Exchange vérifie les signatures et transmet les actions au Processor, qui les encode sous forme de messages ActionCaster.

Points d'entrée de financement non signés

Tous les appels de financement ne sont pas des actions EIP-712. Ces méthodes sont des transactions directes envoyées par le portefeuille payeur ou le routeur :

function depositUsdcFor(address account, uint256 amount) external;
function depositOption(address account, address token, uint256 amount) external;

depositUsdcFor est volontairement non signé car msg.sender n'est que le payeur en USDC. Le compte Hypercall crédité est l'argument explicite account et le champ d'événement UsdcDeposit.account. Les routeurs et les zaps peuvent appeler cette méthode, donc les indexeurs et les services backend ne doivent pas utiliser msg.sender pour l'attribution du crédit.

depositOption brûle les jetons d'option de msg.sender et émet Deposit(account, msg.sender, token, amount) pour l'indexeur RSM. Le chemin de crédit des options est piloté par les événements et n'utilise pas de signature manager, agent ou RSM de la part du déposant.

Séparateurs de domaine EIP-712

Les trois domaines utilisent la même structure mais des noms différents :

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

Chain IDs :

  • Testnet : 998
  • Réseau principal : 999

Domaine 1 : Requêtes Agent (HypercallAgentSign)

Nom de domaine : "HypercallAgentSign"

Séparateurs de domaine précalculés :

  • Testnet : 0x8f0a44075cd4e0c79e5bd379a6fad5fa1329a4ea76d74e4edfa1138933d35e8a
  • Réseau principal : utilisez le chain ID 999 et la configuration du vérificateur déployé pour l'environnement actif.

Signataire : portefeuille API (doit être autorisé via Exchange.addApiWallet)

Nonce : protection contre le rejeu par signataire. Le moteur stocke les 100 nonces les plus élevés par signataire. Un nouveau nonce doit être supérieur au plus petit de l'ensemble et ne pas avoir déjà été utilisé. Les nonces doivent être compris dans l'intervalle (T - 2 jours, T + 1 jour) par rapport à l'horodatage du serveur. On-chain, Exchange.isNonceUsed(signer, nonce) suit l'utilisation via un bitmap

HLRequestOrder

Place des ordres perp/spot 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(...)")

Encodage :

  1. Hachez chaque HLOrder avec structHash(HLOrder)
  2. Concaténez les hachages des ordres : keccak256(abi.encodePacked(orderHashes))
  3. Hachez la requête : keccak256(abi.encode(HL_ORDER_REQUEST_TYPE_HASH, packedOrderHashes, nonce))
  4. Digest EIP-712 : MessageHashUtils.toTypedDataHash(domainSeparator, structHash)

Exemple (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);

Point d'entrée on-chain : Exchange.hlRequestOrder(HLRequestOrder memory request, bytes memory signature)

Sortie du Processor : encode chaque ordre en ActionCasterEncoder.limitOrder(...) et retourne des actions bytes[].

HLRequestCancel

Annule des ordres par identifiant d'ordre.

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(...)")

Exemple :

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

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

Point d'entrée on-chain : Exchange.hlRequestCancel(HLRequestCancel memory request, bytes memory signature)

HLRequestCancelByCloid

Annule des ordres par identifiant d'ordre client.

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(...)")

Exemple :

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

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

Point d'entrée on-chain : Exchange.hlRequestCancelByCloid(HLRequestCancelByCloid memory request, bytes memory signature)

Domaine 2 : Actions Manager (HypercallManagerSign)

Nom de domaine : "HypercallManagerSign"

Séparateurs de domaine précalculés :

  • Testnet : 0xd1f76b6138be892c14b71b0569bdb049cb44f239d34c78ef1ffaacd2466f9f18
  • Réseau principal : à déterminer

Signataire : Manager du compte (l'EOA qui a créé le compte)

Nonce : protection contre le rejeu par manager. Même modèle d'ensemble borné que les nonces Agent : les 100 nonces les plus élevés sont stockés, le nouveau nonce doit dépasser le minimum de l'ensemble et ne pas être un doublon. Suivi on-chain via Exchange.isNonceUsed(manager, nonce)

HLActionSendAsset

Envoie des actifs depuis le compte vers une 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)")

Exigences :

  • signer == managers[account] (vérifié on-chain)
  • Si destination == Exchange, le jeton doit être pris en charge (_checkExchangeToken)

Exemple :

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);

Point d'entrée on-chain : Exchange.hlActionSendAsset(HLActionSendAsset memory action, bytes memory signature)

Sortie du Processor : encodé en ActionCasterEncoder.sendAsset(...).

HCActionWithdrawToken

Retire des jetons de l'Exchange vers le compte.

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)")

Exigences :

  • signer == managers[account]
  • Le jeton doit être pris en charge (_checkExchangeToken - actuellement uniquement l'USDC spot)
  • Le compte doit être activé sur HyperCore (ActionCasterUtils.checkAccountActivated)

Comportement :

  • L'Exchange initie les actions ActionCaster (et non le compte)
  • Transfère le jeton de l'Exchange vers le compte sur HyperCore

Exemple :

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);

Point d'entrée on-chain : Exchange.hcActionWithdrawToken(HCActionWithdrawToken memory action, bytes memory signature)

HCActionWithdrawOption

Retire des jetons d'option de l'Exchange vers un destinataire sur 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)")

Exigences :

  • signer == managers[account]
  • option doit être pris en charge (optionRegistry.isSupportedOption(option))

Comportement :

  • Aucune action ActionCaster (contrairement aux autres retraits)
  • Émet (mint) le jeton d'option vers recipient via IOptionToken(option).mint(recipient, amountWei)
  • Émet l'événement Withdraw(account, recipient, option, amountWei)

Exemple :

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);

Point d'entrée on-chain : Exchange.hcActionWithdrawOption(HCActionWithdrawOption memory action, bytes memory signature)

Domaine 3 : Commandes RSM (HypercallRsmSign)

Nom de domaine : "HypercallRsmSign"

Séparateurs de domaine précalculés :

  • Testnet : 0x650b282053fb61d3fd477bdc28f6434311fe905e27cc4ca643e87e802c45938c
  • Réseau principal : à déterminer

Signataire : signataire RSM (défini via Exchange.setRsmSigner, vérifié on-chain)

Nonce : nonce par signataire RSM (suivi par Exchange.nextNonce[rsmSigner])

Les commandes RSM peuvent être appelées par le SEQUENCER_ROLE ; les market makers ne les appellent pas directement.

RsmCommandRebalance

Exécute un ordre IOC reduce-only sur HyperCore pour rééquilibrer une 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)")

Exigences :

  • signer == rsmSigner (vérifié on-chain)
  • L'appelant doit avoir le SEQUENCER_ROLE

Comportement :

  • Encodé en ActionCasterEncoder.limitOrder avec reduceOnly: true et encodedTif: 3 (IOC)
  • S'exécute sur le compte cible

Point d'entrée on-chain : Exchange.rsmCommandRebalance(RsmCommandRebalance memory cmd, bytes memory signature)

RsmCommandRepay

Dépose des jetons dans l'Exchange au nom d'un compte (utilisé pour les remboursements de liquidation).

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)")

Exigences :

  • signer == rsmSigner
  • L'appelant doit avoir le SEQUENCER_ROLE
  • Le jeton doit être pris en charge (_checkExchangeToken)

Comportement :

  • Encodé en ActionCasterEncoder.sendAsset avec destination: EXCHANGE
  • S'exécute sur le compte cible

Point d'entrée on-chain : Exchange.rsmCommandRepay(RsmCommandRepay memory cmd, bytes memory signature)

Gestion des nonces

Chaque signataire (portefeuille API, manager, signataire RSM) dispose d'un espace de nonces indépendant :

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

Règles :

  1. Les nonces doivent être strictement croissants (les écarts sont autorisés, mais nextNonce est maintenu)
  2. Une fois utilisé, un nonce ne peut pas être réutilisé (vérifié via isNonceUsed(signer, nonce))
  3. nextNonce[signer] est le nonce minimum garanti non utilisé (des nonces inférieurs peuvent être inutilisés s'ils ont été sautés)

Interroger l'état d'un nonce :

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

Bonne pratique : suivez les nonces hors chaîne et incrémentez-les de manière atomique. Utilisez nextNonce comme vérification de cohérence.

Flux de vérification des signatures

  1. Hors chaîne : le signataire crée le digest EIP-712 et le signe avec sa clé privée
  2. On-chain : l'Exchange reçoit le message signé et appelle Processor.process*
  3. Processor : vérifie la signature, récupère le signataire, encode les actions ActionCaster
  4. Exchange : vérifie le nonce, vérifie l'autorisation (manager/portefeuille API/RSM), exécute les actions

Exemple de flux (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

Fonctions dépréciées

Les fonctions suivantes sont dépréciées mais existent encore pour la rétrocompatibilité :

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

Elles utilisent un schéma d'encodage MsgPack hérité et le domaine CoreSignatures ("Exchange", chainId 1337). Ne les utilisez pas pour de nouvelles intégrations.

Considérations de sécurité

  1. Stockage des clés privées : stockez les clés du portefeuille API et du manager de manière sécurisée (portefeuille matériel pour le manager, stockage chiffré pour les portefeuilles API).

  2. Rejeu de nonce : ne réutilisez jamais les nonces. Suivez les nonces hors chaîne et incrémentez-les de manière atomique.

  3. Séparateur de domaine : utilisez toujours le bon chain ID (998 pour le testnet, réseau principal à déterminer). Vérifiez que le séparateur de domaine correspond aux constantes du contrat.

  4. Vérification des signatures : le contrat vérifie les signatures on-chain. Ne faites pas confiance à la vérification de signature hors chaîne pour les opérations critiques.

  5. Manager vs portefeuille API : les managers contrôlent la propriété du compte et les retraits. Les portefeuilles API ne signent que les requêtes de trading. Utilisez des clés distinctes.

Références