Diese Seite wurde maschinell übersetzt. Das englische Original ist maßgeblich. Auf Englisch lesen
Zum Hauptinhalt springen

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:

  1. Agent (API-Wallet): Autorisiert über Exchange.addApiWallet – signiert Handelsanfragen
  2. Manager: Der Kontoinhaber – signiert Auszahlungen und Asset-Transfers
  3. 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 (HypercallApiSign)

Domain-Name: "HypercallApiSign"

Vorberechnete Domain-Separatoren:

  • Testnet: 0x339bee4c00da9a5361f687797205c8d03f323659f5d9dc9e0505b9437ddee25e
  • Mainnet: Verwenden Sie Chain-ID 999 und 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

HLOrder

Places one managed-account HyperLiquid perp order through Hypercall portfolio-margin admission.

Struct:

struct LimitOrder {
uint32 asset;
bool isBuy;
uint64 limitPx;
uint64 sz;
bool reduceOnly;
uint8 encodedTif;
uint128 cloid;
}

struct HLOrder {
address account;
uint64 nonce;
LimitOrder action;
}

Canonical EIP-712 type:

HLOrder(address account,uint64 nonce,LimitOrder action)LimitOrder(uint32 asset,bool isBuy,uint64 limitPx,uint64 sz,bool reduceOnly,uint8 encodedTif,uint128 cloid)

The signed primary message binds the managed Account.sol address, the API-wallet nonce, and one LimitOrder action. Price and size use exact 1e8 units. Time in force is encoded as ALO = 1, GTC = 2, or IOC = 3.

Example (ethers.js):

const domain = {
name: "HypercallApiSign",
version: "1",
chainId: 998,
verifyingContract: ethers.ZeroAddress
};

const types = {
LimitOrder: [
{ 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" }
],
HLOrder: [
{ name: "account", type: "address" },
{ name: "nonce", type: "uint64" },
{ name: "action", type: "LimitOrder" }
]
};

const message = {
account: managedAccount,
nonce: selectedNonce,
action: {
asset: 0,
isBuy: true,
limitPx: 5000000000000n,
sz: 100000n,
reduceOnly: false,
encodedTif: 2,
cloid: selectedNonce
}
};

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

Hypercall API endpoint: POST /v1/actions/hl_limit_order

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]
  • option muss unterstützt werden (optionRegistry.isSupportedOption(option))

Verhalten:

  • Keine ActionCaster-Aktionen (im Gegensatz zu anderen Auszahlungen)
  • Prägt den Optionstoken an recipient über IOptionToken(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_ROLE besitzen

Verhalten:

  • Kodiert als ActionCasterEncoder.limitOrder mit reduceOnly: true und encodedTif: 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_ROLE besitzen
  • Der Token muss unterstützt werden (_checkExchangeToken)

Verhalten:

  • Kodiert als ActionCasterEncoder.sendAsset mit destination: 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:

  1. Nonces müssen streng aufsteigend sein (keine Lücken erforderlich, aber nextNonce wird gepflegt)
  2. Einmal verwendet, kann eine Nonce nicht wiederverwendet werden (geprüft über isNonceUsed(signer, nonce))
  3. 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

  1. Off-Chain: Der Signierer erstellt den EIP-712-Digest und signiert mit dem privaten Schlüssel
  2. On-Chain: Exchange empfängt die signierte Nachricht und ruft Processor.process* auf
  3. Processor: Verifiziert die Signatur, ermittelt den Signierer, kodiert ActionCaster-Aktionen
  4. Exchange: Prüft die Nonce, verifiziert die Autorisierung (Manager/API-Wallet/RSM), führt die Aktionen aus

Example Flow (HLOrder):

1. API wallet signs HLOrder with the managed account, a unique nonce, and one LimitOrder action
2. Client posts account, nonce, action, and signature to /v1/actions/hl_limit_order
3. Hypercall recovers the API wallet and applies portfolio-margin admission
4. The response reports the honest directive stage, rejection details, and transaction hash
5. Client polls /v1/directives/{directive_id} for subsequent status

Veraltete Funktionen

Die folgenden Funktionen sind veraltet, existieren aber weiterhin aus Gründen der Abwärtskompatibilität:

  • placeCoreOrders (verwenden Sie hlRequestOrder)
  • cancelCoreOrders (verwenden Sie hlRequestCancel)
  • cancelCoreOrdersByCloid (verwenden Sie hlRequestCancelByCloid)

Diese verwenden ein Legacy-MsgPack-Kodierungsschema und die CoreSignatures-Domain ("Exchange", chainId 1337). Verwenden Sie sie nicht für neue Integrationen.

Sicherheitsüberlegungen

  1. 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).

  2. Nonce-Replay: Verwenden Sie Nonces niemals wieder. Verfolgen Sie Nonces off-chain und inkrementieren Sie sie atomar.

  3. 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.

  4. Signaturverifizierung: Der Vertrag verifiziert Signaturen on-chain. Vertrauen Sie bei kritischen Operationen nicht auf Off-Chain-Signaturverifizierung.

  5. Manager vs. API-Wallet: Manager kontrollieren Kontoinhaberschaft und Auszahlungen. API-Wallets signieren nur Handelsanfragen. Verwenden Sie getrennte Schlüssel.

Referenzen