EIP-712署名
このドキュメントでは、オンチェーンアクションに使用される3つのEIP-712署名ドメイン、Agent Requests、Manager Actions、RSM Commandsについて説明します。
概要
ほとんどのプロトコルアクションには、EIP-712型付きデータ署名が必要です。署名者は以下のいずれかである必要があります。
- Agent (APIウォレット):
Exchange.addApiWalletにより認可され、取引リクエストに署名します - Manager: アカウント所有者であり、出金および資産移転に署名します
- RSM Signer: プロトコルが管理し、清算/リバランスコマンドに署名します
Exchangeコントラクトは署名を検証し、アクションをProcessorに転送します。ProcessorはそれらをActionCasterメッセージとしてエンコードします。
署名不要の資金移動エントリーポイント
すべての資金移動呼び出しがEIP-712アクションというわけではありません。以下のメソッドは、支払いを行うウォレットまたはルーターが直接送信するトランザクションです。
function depositUsdcFor(address account, uint256 amount) external;
function depositOption(address account, address token, uint256 amount) external;
depositUsdcForは意図的に署名不要となっています。これは、msg.senderが単にUSDCの支払者にすぎないためです。入金先となるHypercallアカウントは、明示的なaccount引数およびUsdcDeposit.accountイベントフィールドで指定されます。ルーターやZapがこのメソッドを呼び出す可能性があるため、インデクサーやバックエンドサービスは、入金の帰属判定にmsg.senderを使用してはいけません。
depositOptionはmsg.senderからオプショントークンをバーンし、RSMインデクサー向けにDeposit(account, msg.sender, token, amount)を発行します。オプションの入金経路はイベント駆動であり、入金者のマネージャー、エージェント、またはRSMの署名を使用しません。
EIP-712ドメインセパレーター
3つのドメインはすべて同じ構造を使用しますが、名前が異なります。
{
"name": "<DomainName>",
"version": "1",
"chainId": <chainId>,
"verifyingContract": "0x0000000000000000000000000000000000000000"
}
チェーンID:
- テストネット:
998 - メインネット:
999
ドメイン1: Agent Requests (HypercallAgentSign)
ドメイン名: "HypercallAgentSign"
事前計算済みドメインセパレーター:
- テストネット:
0x8f0a44075cd4e0c79e5bd379a6fad5fa1329a4ea76d74e4edfa1138933d35e8a - メインネット: チェーンID
999と、稼働中の環境にデプロイされた検証コントラクトの設定を使用してください。
署名者: APIウォレット (Exchange.addApiWalletにより認可されている必要があります)
Nonce: 署名者ごとのリプレイ防止です。エンジンは署名者ごとに上位100個のnonceを保存します。新しいnonceはセット内の最小値より大きく、かつ未使用である必要があります。nonceはサーバータイムスタンプに対して (T - 2日, T + 1日) の範囲内でなければなりません。オンチェーンでは、Exchange.isNonceUsed(signer, nonce)がビットマップにより使用状況を追跡します。
HLRequestOrder
HyperLiquidのパーペチュアル/スポット注文を発注します。
構造体:
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;
}
タイプハッシュ:
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(...)")
エンコーディング:
- 各
HLOrderをstructHash(HLOrder)でハッシュ化します - 注文ハッシュをパックします:
keccak256(abi.encodePacked(orderHashes)) - リクエストをハッシュ化します:
keccak256(abi.encode(HL_ORDER_REQUEST_TYPE_HASH, packedOrderHashes, nonce)) - EIP-712ダイジェスト:
MessageHashUtils.toTypedDataHash(domainSeparator, structHash)
例 (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);
オンチェーンエントリーポイント: Exchange.hlRequestOrder(HLRequestOrder memory request, bytes memory signature)
Processorの出力: 各注文をActionCasterEncoder.limitOrder(...)としてエンコードし、bytes[]アクションを返します。
HLRequestCancel
注文IDで注文をキャンセルします。
構造体:
struct HLCancel {
uint32 asset;
uint64 oid; // Order ID from HyperLiquid
}
struct HLRequestCancel {
HLCancel[] cancels;
uint64 nonce;
}
タイプハッシュ:
HL_CANCEL_TYPE_HASH:keccak256("HLCancel(uint32 asset,uint64 oid)")HL_CANCEL_REQUEST_TYPE_HASH:keccak256("HLRequestCancel(HLCancel[] cancels,uint64 nonce)HLCancel(...)")
例:
const message = {
cancels: [{
asset: 0,
oid: 12345
}],
nonce: 2
};
const signature = await apiWalletSigner.signTypedData(domain, types, message);
オンチェーンエントリーポイント: Exchange.hlRequestCancel(HLRequestCancel memory request, bytes memory signature)
HLRequestCancelByCloid
クライアント注文IDで注文をキャンセルします。
構造体:
struct HLCancelByCloid {
uint32 asset;
uint128 cloid; // Client order ID
}
struct HLRequestCancelByCloid {
HLCancelByCloid[] cancels;
uint64 nonce;
}
タイプハッシュ:
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(...)")
例:
const message = {
cancels: [{
asset: 0,
cloid: 9876543210
}],
nonce: 3
};
const signature = await apiWalletSigner.signTypedData(domain, types, message);
オンチェーンエントリーポイント: Exchange.hlRequestCancelByCloid(HLRequestCancelByCloid memory request, bytes memory signature)
ドメイン2: Manager Actions (HypercallManagerSign)
ドメイン名: "HypercallManagerSign"
事前計算済みドメインセパレーター:
- テストネット:
0xd1f76b6138be892c14b71b0569bdb049cb44f239d34c78ef1ffaacd2466f9f18 - メインネット: 未定
署名者: アカウントマネージャー (アカウントを作成したEOA)
Nonce: マネージャーごとのリプレイ防止です。Agentのnonceと同じ有界セットモデルを採用しており、上位100個のnonceが保存され、新しいnonceはセットの最小値を超えかつ重複していない必要があります。オンチェーンではExchange.isNonceUsed(manager, nonce)により追跡されます。
HLActionSendAsset
ActionCasterを介して、Accountから送金先へ資産を送信します。
構造体:
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
}
タイプハッシュ: keccak256("HLActionSendAsset(address account,uint64 nonce,address destination,uint32 srcDex,uint32 dstDex,uint64 token,uint64 amountWei)")
要件:
signer == managers[account](オンチェーンで検証されます)destination == Exchangeの場合、トークンはサポートされている必要があります (_checkExchangeToken)
例:
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);
オンチェーンエントリーポイント: Exchange.hlActionSendAsset(HLActionSendAsset memory action, bytes memory signature)
Processorの出力: ActionCasterEncoder.sendAsset(...)としてエンコードします。
HCActionWithdrawToken
ExchangeからAccountへトークンを引き出します。
構造体:
struct HCActionWithdrawToken {
address account;
uint64 nonce;
uint32 srcDex;
uint32 dstDex;
uint64 token;
uint64 amountWei;
}
タイプハッシュ: keccak256("HCActionWithdrawToken(address account,uint64 nonce,uint32 srcDex,uint32 dstDex,uint64 token,uint64 amountWei)")
要件:
signer == managers[account]- トークンはサポートされている必要があります (
_checkExchangeToken- 現在はスポットUSDCのみ) - アカウントはHyperCore上でアクティベート済みである必要があります (
ActionCasterUtils.checkAccountActivated)
動作:
- ExchangeがActionCasterアクションを開始します (Accountではありません)
- HyperCore上でExchangeからAccountへトークンを転送します
例:
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);
オンチェーンエントリーポイント: Exchange.hcActionWithdrawToken(HCActionWithdrawToken memory action, bytes memory signature)
HCActionWithdrawOption
ExchangeからHyperEVM上の受取人へオプショントークンを引き出します。
構造体:
struct HCActionWithdrawOption {
address account;
uint64 nonce;
address recipient;
address option; // Option token address
uint256 amountWei; // Amount in wei
}
タイプハッシュ: keccak256("HCActionWithdrawOption(address account,uint64 nonce,address recipient,address option,uint256 amountWei)")
要件:
signer == managers[account]optionはサポートされている必要があります (optionRegistry.isSupportedOption(option))
動作:
- ActionCasterアクションは発生しません (他の引き出しとは異なります)
IOptionToken(option).mint(recipient, amountWei)により、オプショントークンをrecipientにミントしますWithdraw(account, recipient, option, amountWei)を発行します
例:
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);
オンチェーンエントリーポイント: Exchange.hcActionWithdrawOption(HCActionWithdrawOption memory action, bytes memory signature)
ドメイン3: RSM Commands (HypercallRsmSign)
ドメイン名: "HypercallRsmSign"
事前計算済みドメインセパレーター:
- テストネット:
0x650b282053fb61d3fd477bdc28f6434311fe905e27cc4ca643e87e802c45938c - メインネット: 未定
署名者: RSM Signer (Exchange.setRsmSignerで設定され、オンチェーンで検証されます)
Nonce: RSM署名者ごとのnonce (Exchange.nextNonce[rsmSigner]により追跡されます)
RSMコマンドはSEQUENCER_ROLEによって呼び出し可能です。マーケットメイカーがこれらを直接呼び出すことはありません。
RsmCommandRebalance
ポジションをリバランスするために、HyperCore上でリデュースオンリーのIOC注文を実行します。
構造体:
struct RsmCommandRebalance {
address target; // Account to rebalance
uint64 nonce;
uint32 asset;
bool isBuy;
uint64 limitPx;
uint64 sz;
}
タイプハッシュ: keccak256("RsmCommandRebalance(address target,uint64 nonce,uint32 asset,bool isBuy,uint64 limitPx,uint64 sz)")
要件:
signer == rsmSigner(オンチェーンで検証されます)- 呼び出し元は
SEQUENCER_ROLEを持っている必要があります
動作:
reduceOnly: trueおよびencodedTif: 3(IOC) を指定したActionCasterEncoder.limitOrderとしてエンコードします- 対象アカウント上で実行されます
オンチェーンエントリーポイント: Exchange.rsmCommandRebalance(RsmCommandRebalance memory cmd, bytes memory signature)
RsmCommandRepay
アカウントに代わってExchangeにトークンを入金します (清算時の返済に使用されます)。
構造体:
struct RsmCommandRepay {
address target;
uint64 nonce;
uint32 srcDex;
uint32 dstDex;
uint64 token;
uint64 amountWei;
}
タイプハッシュ: keccak256("RsmCommandRepay(address target,uint64 nonce,uint32 srcDex,uint32 dstDex,uint64 token,uint64 amountWei)")
要件:
signer == rsmSigner- 呼び出し元は
SEQUENCER_ROLEを持っている必要があります - トークンはサポートされている必要があります (
_checkExchangeToken)
動作:
destination: EXCHANGEを指定したActionCasterEncoder.sendAssetとしてエンコードします- 対象アカウント上で実行されます
オンチェーンエントリーポイント: Exchange.rsmCommandRepay(RsmCommandRepay memory cmd, bytes memory signature)
Nonce管理
各署名者 (APIウォレット、マネージャー、RSM署名者) は、それぞれ独立したnonce空間を持ちます。
mapping(address signer => uint256 nonce) public nextNonce;
mapping(address signer => BitMaps.BitMap) private _nonces; // Tracks used nonces
ルール:
- nonceは厳密に増加する必要があります (欠番は許容されますが、
nextNonceは維持されます) - 一度使用されたnonceは再利用できません (
isNonceUsed(signer, nonce)で確認されます) nextNonce[signer]は未使用が保証される最小のnonceです (スキップされた場合、それより小さいnonceが未使用のままの可能性があります)
Nonce状態の照会:
function isNonceUsed(address signer, uint256 nonce) external view returns (bool);
ベストプラクティス: nonceをオフチェーンで管理し、アトミックにインクリメントしてください。nextNonceはサニティチェックとして使用してください。
署名検証フロー
- オフチェーン: 署名者がEIP-712ダイジェストを作成し、秘密鍵で署名します
- オンチェーン:
Exchangeが署名済みメッセージを受け取り、Processor.process*を呼び出します - Processor: 署名を検証し、署名者を復元し、ActionCasterアクションをエンコードします
- Exchange: nonceを確認し、認可 (マネージャー/APIウォレット/RSM) を検証し、アクションを実行します
フロー例 (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
非推奨の関数
以下の関数は非推奨ですが、後方互換性のために引き続き存在します。
placeCoreOrders(代わりにhlRequestOrderを使用してください)cancelCoreOrders(代わりにhlRequestCancelを使用してください)cancelCoreOrdersByCloid(代わりにhlRequestCancelByCloidを使用してください)
これらはレガシーなMsgPackエンコーディング方式とCoreSignaturesドメイン ("Exchange"、chainId 1337) を使用しています。新規の統合には使用しないでください。
セキュリティに関する考慮事項
-
秘密鍵の保管: APIウォレットおよびマネージャーの鍵を安全に保管してください (マネージャーにはハードウェアウォレット、APIウォレットには暗号化ストレージを推奨します)。
-
Nonceのリプレイ: nonceを決して再利用しないでください。nonceをオフチェーンで管理し、アトミックにインクリメントしてください。
-
ドメインセパレーター: 常に正しいチェーンID (テストネットは998、メインネットは未定) を使用してください。ドメインセパレーターがコントラクトの定数と一致することを確認してください。
-
署名検証: コントラクトはオンチェーンで署名を検証します。重要な操作については、オフチェーンでの署名検証を信頼しないでください。
-
マネージャーとAPIウォレットの違い: マネージャーはアカウントの所有権と出金を管理します。APIウォレットは取引リクエストの署名のみを行います。別々の鍵を使用してください。
参考資料
- アカウント作成とAPIウォレットの設定についてはOnboardingを参照してください
- オフチェーンAPI認証についてはAPI Authenticationを参照してください