EIP-712-Signierung
Dieses Dokument beschreibt die drei EIP-712-Signierungsdomains, die für On-Chain-Aktionen verwendet werden: Agent Requests, Manager Actions und RSM Commands.
Überblick
Die meisten Protokollaktionen erfordern typisierte EIP-712-Datensignaturen. Der Signierer muss einer der folgenden sein:
- Agent (API-Wallet): Autorisiert über
Exchange.addApiWallet– signiert Handelsanfragen - Manager: Der Kontoinhaber – signiert Auszahlungen und Asset-Transfers
- RSM-Signierer: Protokollgesteuert – signiert Liquidations-/Rebalance-Befehle
Der Exchange-Vertrag verifiziert Signaturen und leitet Aktionen an den Processor weiter, der sie als ActionCaster-Nachrichten kodiert.
Unsignierte Funding-Einstiegspunkte
Nicht jeder Funding-Aufruf ist eine EIP-712-Aktion. Diese Methoden sind direkte Transaktionen, die von der zahlenden Wallet oder dem Router gesendet werden:
function depositUsdcFor(address account, uint256 amount) external;
function depositOption(address account, address token, uint256 amount) external;
depositUsdcFor ist absichtlich unsigniert, da msg.sender lediglich der USDC-Zahler ist. Das gutgeschriebene Hypercall-Konto ist das explizite account-Argument sowie das Event-Feld UsdcDeposit.account. Router und Zaps können diese Methode aufrufen, daher dürfen Indexer und Backend-Dienste msg.sender nicht für die Gutschriftszuordnung verwenden.
depositOption verbrennt Optionstoken von msg.sender und emittiert Deposit(account, msg.sender, token, amount) für den RSM-Indexer. Der Optionsgutschriftspfad ist event-gesteuert und verwendet keine Manager-, Agent- oder RSM-Signatur des Einzahlers.
EIP-712-Domain-Separatoren
Alle drei Domains verwenden dieselbe Struktur, jedoch unterschiedliche Namen:
{
"name": "<DomainName>",
"version": "1",
"chainId": <chainId>,
"verifyingContract": "0x0000000000000000000000000000000000000000"
}
Chain-IDs:
- Testnet:
998 - Mainnet:
999
Domain 1: Agent Requests (HypercallAgentSign)
Domain-Name: "HypercallAgentSign"
Vorberechnete Domain-Separatoren:
- Testnet:
0x8f0a44075cd4e0c79e5bd379a6fad5fa1329a4ea76d74e4edfa1138933d35e8a - Mainnet: Verwenden Sie Chain-ID
999und die bereitgestellte Verifier-Konfiguration für die aktive Umgebung.
Signierer: API-Wallet (muss über Exchange.addApiWallet autorisiert sein)
Nonce: Replay-Schutz pro Signierer. Die Engine speichert die 100 höchsten Nonces pro Signierer. Eine neue Nonce muss größer sein als die kleinste in der Menge und darf nicht bereits verwendet worden sein. Nonces müssen innerhalb von (T - 2 Tage, T + 1 Tag) des Server-Zeitstempels liegen. On-Chain verfolgt Exchange.isNonceUsed(signer, nonce) die Verwendung über eine Bitmap
HLRequestOrder
Platziert 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(...)")
Kodierung:
- Hashen Sie jede
HLOrdermitstructHash(HLOrder) - Packen Sie die Order-Hashes:
keccak256(abi.encodePacked(orderHashes)) - Hashen Sie die Anfrage:
keccak256(abi.encode(HL_ORDER_REQUEST_TYPE_HASH, packedOrderHashes, nonce)) - EIP-712-Digest:
MessageHashUtils.toTypedDataHash(domainSeparator, structHash)
Beispiel (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-Einstiegspunkt: Exchange.hlRequestOrder(HLRequestOrder memory request, bytes memory signature)
Processor-Ausgabe: Kodiert jede Order als ActionCasterEncoder.limitOrder(...) und gibt bytes[]-Aktionen zurück.
HLRequestCancel
Storniert Orders anhand der 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(...)")
Beispiel:
const message = {
cancels: [{
asset: 0,
oid: 12345
}],
nonce: 2
};
const signature = await apiWalletSigner.signTypedData(domain, types, message);
On-Chain-Einstiegspunkt: Exchange.hlRequestCancel(HLRequestCancel memory request, bytes memory signature)
HLRequestCancelByCloid
Storniert Orders anhand der 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(...)")
Beispiel:
const message = {
cancels: [{
asset: 0,
cloid: 9876543210
}],
nonce: 3
};
const signature = await apiWalletSigner.signTypedData(domain, types, message);
On-Chain-Einstiegspunkt: Exchange.hlRequestCancelByCloid(HLRequestCancelByCloid memory request, bytes memory signature)
Domain 2: Manager Actions (HypercallManagerSign)
Domain-Name: "HypercallManagerSign"
Vorberechnete Domain-Separatoren:
- Testnet:
0xd1f76b6138be892c14b71b0569bdb049cb44f239d34c78ef1ffaacd2466f9f18 - Mainnet: TBD
Signierer: Konto-Manager (die EOA, die das Konto erstellt hat)
Nonce: Replay-Schutz pro Manager. Dasselbe Modell einer begrenzten Menge wie bei Agent-Nonces: Die 100 höchsten Nonces werden gespeichert, eine neue Nonce muss das Minimum der Menge überschreiten und darf kein Duplikat sein. On-Chain verfolgt über Exchange.isNonceUsed(manager, nonce)
HLActionSendAsset
Sendet Assets vom Konto an ein Ziel über 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)")
Anforderungen:
signer == managers[account](wird on-chain verifiziert)- Wenn
destination == Exchange, muss der Token unterstützt werden (_checkExchangeToken)
Beispiel:
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-Einstiegspunkt: Exchange.hlActionSendAsset(HLActionSendAsset memory action, bytes memory signature)
Processor-Ausgabe: Kodiert als ActionCasterEncoder.sendAsset(...).
HCActionWithdrawToken
Zieht Token vom Exchange in das Konto ab.
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)")
Anforderungen:
signer == managers[account]- Der Token muss unterstützt werden (
_checkExchangeToken– derzeit nur Spot-USDC) - Das Konto muss auf HyperCore aktiviert sein (
ActionCasterUtils.checkAccountActivated)
Verhalten:
- Der Exchange initiiert ActionCaster-Aktionen (nicht das Konto)
- Überträgt den Token vom Exchange an das Konto auf HyperCore
Beispiel:
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-Einstiegspunkt: Exchange.hcActionWithdrawToken(HCActionWithdrawToken memory action, bytes memory signature)
HCActionWithdrawOption
Zieht Optionstoken vom Exchange an einen Empfänger auf HyperEVM ab.
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)")
Anforderungen:
signer == managers[account]optionmuss unterstützt werden (optionRegistry.isSupportedOption(option))
Verhalten:
- Keine ActionCaster-Aktionen (im Gegensatz zu anderen Auszahlungen)
- Prägt den Optionstoken an
recipientüberIOptionToken(option).mint(recipient, amountWei) - Emittiert
Withdraw(account, recipient, option, amountWei)
Beispiel:
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-Einstiegspunkt: Exchange.hcActionWithdrawOption(HCActionWithdrawOption memory action, bytes memory signature)
Domain 3: RSM Commands (HypercallRsmSign)
Domain-Name: "HypercallRsmSign"
Vorberechnete Domain-Separatoren:
- Testnet:
0x650b282053fb61d3fd477bdc28f6434311fe905e27cc4ca643e87e802c45938c - Mainnet: TBD
Signierer: RSM-Signierer (gesetzt über Exchange.setRsmSigner, wird on-chain verifiziert)
Nonce: Nonce pro RSM-Signierer (verfolgt durch Exchange.nextNonce[rsmSigner])
RSM-Befehle können von der SEQUENCER_ROLE aufgerufen werden; Market Maker rufen diese nicht direkt auf.
RsmCommandRebalance
Führt eine Reduce-only-IOC-Order auf HyperCore aus, um eine Position zu rebalancieren.
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)")
Anforderungen:
signer == rsmSigner(wird on-chain verifiziert)- Der Aufrufer muss die
SEQUENCER_ROLEbesitzen
Verhalten:
- Kodiert als
ActionCasterEncoder.limitOrdermitreduceOnly: trueundencodedTif: 3(IOC) - Wird auf dem Zielkonto ausgeführt
On-Chain-Einstiegspunkt: Exchange.rsmCommandRebalance(RsmCommandRebalance memory cmd, bytes memory signature)
RsmCommandRepay
Zahlt Token im Namen eines Kontos in den Exchange ein (wird für Liquidationsrückzahlungen verwendet).
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)")
Anforderungen:
signer == rsmSigner- Der Aufrufer muss die
SEQUENCER_ROLEbesitzen - Der Token muss unterstützt werden (
_checkExchangeToken)
Verhalten:
- Kodiert als
ActionCasterEncoder.sendAssetmitdestination: EXCHANGE - Wird auf dem Zielkonto ausgeführt
On-Chain-Einstiegspunkt: Exchange.rsmCommandRepay(RsmCommandRepay memory cmd, bytes memory signature)
Nonce-Verwaltung
Jeder Signierer (API-Wallet, Manager, RSM-Signierer) hat einen unabhängigen Nonce-Raum:
mapping(address signer => uint256 nonce) public nextNonce;
mapping(address signer => BitMaps.BitMap) private _nonces; // Tracks used nonces
Regeln:
- Nonces müssen streng aufsteigend sein (keine Lücken erforderlich, aber
nextNoncewird gepflegt) - Einmal verwendet, kann eine Nonce nicht wiederverwendet werden (geprüft über
isNonceUsed(signer, nonce)) nextNonce[signer]ist die minimal garantiert unbenutzte Nonce (niedrigere Nonces können unbenutzt sein, falls übersprungen)
Nonce-Status abfragen:
function isNonceUsed(address signer, uint256 nonce) external view returns (bool);
Best Practice: Verfolgen Sie Nonces off-chain und inkrementieren Sie sie atomar. Verwenden Sie nextNonce als Plausibilitätsprüfung.
Ablauf der Signaturverifizierung
- Off-Chain: Der Signierer erstellt den EIP-712-Digest und signiert mit dem privaten Schlüssel
- On-Chain:
Exchangeempfängt die signierte Nachricht und ruftProcessor.process*auf - Processor: Verifiziert die Signatur, ermittelt den Signierer, kodiert ActionCaster-Aktionen
- Exchange: Prüft die Nonce, verifiziert die Autorisierung (Manager/API-Wallet/RSM), führt die Aktionen aus
Beispielablauf (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
Veraltete Funktionen
Die folgenden Funktionen sind veraltet, existieren aber weiterhin aus Gründen der Abwärtskompatibilität:
placeCoreOrders(verwenden SiehlRequestOrder)cancelCoreOrders(verwenden SiehlRequestCancel)cancelCoreOrdersByCloid(verwenden SiehlRequestCancelByCloid)
Diese verwenden ein Legacy-MsgPack-Kodierungsschema und die CoreSignatures-Domain ("Exchange", chainId 1337). Verwenden Sie sie nicht für neue Integrationen.
Sicherheitsüberlegungen
-
Aufbewahrung privater Schlüssel: Bewahren Sie API-Wallet- und Manager-Schlüssel sicher auf (Hardware-Wallet für den Manager, verschlüsselte Speicherung für API-Wallets).
-
Nonce-Replay: Verwenden Sie Nonces niemals wieder. Verfolgen Sie Nonces off-chain und inkrementieren Sie sie atomar.
-
Domain-Separator: Verwenden Sie immer die korrekte Chain-ID (998 für Testnet, Mainnet TBD). Prüfen Sie, ob der Domain-Separator mit den Vertragskonstanten übereinstimmt.
-
Signaturverifizierung: Der Vertrag verifiziert Signaturen on-chain. Vertrauen Sie bei kritischen Operationen nicht auf Off-Chain-Signaturverifizierung.
-
Manager vs. API-Wallet: Manager kontrollieren Kontoinhaberschaft und Auszahlungen. API-Wallets signieren nur Handelsanfragen. Verwenden Sie getrennte Schlüssel.
Referenzen
- Onboarding für Kontoerstellung und API-Wallet-Einrichtung
- API-Authentifizierung für Off-Chain-API-Authentifizierung