Trang này được dịch tự động. Bản gốc tiếng Anh là phiên bản chính thức. Đọc bằng tiếng Anh
Chuyển đến nội dung chính

Ký EIP-712

Tài liệu này mô tả ba domain ký EIP-712 được sử dụng cho các hành động on-chain: Yêu cầu Agent (Agent Requests), Hành động Manager (Manager Actions), và Lệnh RSM (RSM Commands).

Tổng quan

Hầu hết các hành động của giao thức đều yêu cầu chữ ký dữ liệu có kiểu EIP-712 (typed data signature). Người ký phải là:

  1. Agent (Ví API): Được ủy quyền thông qua Exchange.addApiWallet - ký các yêu cầu giao dịch
  2. Manager: Chủ sở hữu tài khoản - ký các lệnh rút tiền và chuyển tài sản
  3. RSM Signer: Do giao thức kiểm soát - ký các lệnh thanh lý/tái cân bằng

Hợp đồng Exchange xác minh chữ ký và chuyển tiếp các hành động đến Processor, nơi mã hóa chúng thành các thông điệp ActionCaster.

Các Entrypoint nạp tiền không cần ký

Không phải mọi lệnh gọi nạp tiền đều là hành động EIP-712. Các phương thức sau là giao dịch trực tiếp được gửi bởi ví thanh toán hoặc router:

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

depositUsdcFor được thiết kế có chủ đích để không cần ký vì msg.sender chỉ là bên thanh toán USDC. Tài khoản Hypercall được ghi có là đối số account tường minh và trường sự kiện UsdcDeposit.account. Các router và zap có thể gọi phương thức này, do đó các indexer và dịch vụ backend không được sử dụng msg.sender để xác định bên được ghi có.

depositOption đốt (burn) các token quyền chọn từ msg.sender và phát ra sự kiện Deposit(account, msg.sender, token, amount) cho indexer RSM. Luồng ghi có quyền chọn được điều khiển bởi sự kiện (event-driven) và không sử dụng chữ ký manager, agent, hoặc RSM từ người nạp.

Các Domain Separator EIP-712

Cả ba domain đều sử dụng cùng một cấu trúc nhưng khác tên:

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

Chain ID:

  • Testnet: 998
  • Mạng chính: 999

Domain 1: Yêu cầu Agent (HypercallAgentSign)

Tên Domain: "HypercallAgentSign"

Domain Separator được tính toán trước:

  • Testnet: 0x8f0a44075cd4e0c79e5bd379a6fad5fa1329a4ea76d74e4edfa1138933d35e8a
  • Mạng chính: sử dụng chain ID 999 và cấu hình verifier đã triển khai cho môi trường đang hoạt động.

Người ký: Ví API (phải được ủy quyền thông qua Exchange.addApiWallet)

Nonce: Bảo vệ chống replay theo từng người ký. Engine lưu trữ 100 nonce cao nhất cho mỗi người ký. Nonce mới phải lớn hơn nonce nhỏ nhất trong tập hợp và chưa được sử dụng. Nonce phải nằm trong khoảng (T - 2 ngày, T + 1 ngày) so với timestamp của máy chủ. Trên chain, Exchange.isNonceUsed(signer, nonce) theo dõi việc sử dụng thông qua một bitmap

HLRequestOrder

Đặt các lệnh perp/spot trên 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(...)")

Mã hóa:

  1. Hash từng HLOrder bằng structHash(HLOrder)
  2. Đóng gói các hash lệnh: keccak256(abi.encodePacked(orderHashes))
  3. Hash yêu cầu: keccak256(abi.encode(HL_ORDER_REQUEST_TYPE_HASH, packedOrderHashes, nonce))
  4. Digest EIP-712: MessageHashUtils.toTypedDataHash(domainSeparator, structHash)

Ví dụ (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);

Entrypoint On-Chain: Exchange.hlRequestOrder(HLRequestOrder memory request, bytes memory signature)

Đầu ra của Processor: Mã hóa từng lệnh dưới dạng ActionCasterEncoder.limitOrder(...) và trả về mảng hành động bytes[].

HLRequestCancel

Hủy các lệnh theo 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(...)")

Ví dụ:

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

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

Entrypoint On-Chain: Exchange.hlRequestCancel(HLRequestCancel memory request, bytes memory signature)

HLRequestCancelByCloid

Hủy các lệnh theo 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(...)")

Ví dụ:

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

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

Entrypoint On-Chain: Exchange.hlRequestCancelByCloid(HLRequestCancelByCloid memory request, bytes memory signature)

Domain 2: Hành động Manager (HypercallManagerSign)

Tên Domain: "HypercallManagerSign"

Domain Separator được tính toán trước:

  • Testnet: 0xd1f76b6138be892c14b71b0569bdb049cb44f239d34c78ef1ffaacd2466f9f18
  • Mạng chính: Sẽ được công bố sau

Người ký: Account Manager (EOA đã tạo tài khoản)

Nonce: Bảo vệ chống replay theo từng manager. Sử dụng cùng mô hình tập giới hạn (bounded-set) như nonce của Agent: 100 nonce cao nhất được lưu trữ, nonce mới phải lớn hơn giá trị nhỏ nhất trong tập và không được trùng lặp. Trên chain được theo dõi qua Exchange.isNonceUsed(manager, nonce)

HLActionSendAsset

Gửi tài sản từ Account đến một địa chỉ đích thông qua 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)")

Yêu cầu:

  • signer == managers[account] (được xác minh on-chain)
  • Nếu destination == Exchange, token phải được hỗ trợ (_checkExchangeToken)

Ví dụ:

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

Entrypoint On-Chain: Exchange.hlActionSendAsset(HLActionSendAsset memory action, bytes memory signature)

Đầu ra của Processor: Mã hóa dưới dạng ActionCasterEncoder.sendAsset(...).

HCActionWithdrawToken

Rút token từ Exchange vào 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)")

Yêu cầu:

  • signer == managers[account]
  • Token phải được hỗ trợ (_checkExchangeToken - hiện tại chỉ có spot USDC)
  • Account phải được kích hoạt trên HyperCore (ActionCasterUtils.checkAccountActivated)

Hành vi:

  • Exchange khởi tạo các hành động ActionCaster (không phải Account)
  • Chuyển token từ Exchange sang Account trên HyperCore

Ví dụ:

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

Entrypoint On-Chain: Exchange.hcActionWithdrawToken(HCActionWithdrawToken memory action, bytes memory signature)

HCActionWithdrawOption

Rút token quyền chọn từ Exchange đến một người nhận trên 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)")

Yêu cầu:

  • signer == managers[account]
  • option phải được hỗ trợ (optionRegistry.isSupportedOption(option))

Hành vi:

  • Không có hành động ActionCaster (khác với các lệnh rút khác)
  • Mint token quyền chọn cho recipient thông qua IOptionToken(option).mint(recipient, amountWei)
  • Phát ra sự kiện Withdraw(account, recipient, option, amountWei)

Ví dụ:

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

Entrypoint On-Chain: Exchange.hcActionWithdrawOption(HCActionWithdrawOption memory action, bytes memory signature)

Domain 3: Lệnh RSM (HypercallRsmSign)

Tên Domain: "HypercallRsmSign"

Domain Separator được tính toán trước:

  • Testnet: 0x650b282053fb61d3fd477bdc28f6434311fe905e27cc4ca643e87e802c45938c
  • Mạng chính: Sẽ được công bố sau

Người ký: RSM Signer (được thiết lập thông qua Exchange.setRsmSigner, được xác minh on-chain)

Nonce: Nonce theo từng RSM signer (được theo dõi bởi Exchange.nextNonce[rsmSigner])

Các lệnh RSM chỉ có thể được gọi bởi SEQUENCER_ROLE; các nhà tạo lập thị trường không gọi trực tiếp các lệnh này.

RsmCommandRebalance

Thực thi một lệnh IOC reduce-only trên HyperCore để tái cân bằng một vị thế.

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

Yêu cầu:

  • signer == rsmSigner (được xác minh on-chain)
  • Bên gọi phải có SEQUENCER_ROLE

Hành vi:

  • Mã hóa dưới dạng ActionCasterEncoder.limitOrder với reduceOnly: trueencodedTif: 3 (IOC)
  • Thực thi trên tài khoản đích

Entrypoint On-Chain: Exchange.rsmCommandRebalance(RsmCommandRebalance memory cmd, bytes memory signature)

RsmCommandRepay

Nạp token vào Exchange thay mặt cho một tài khoản (được sử dụng cho việc hoàn trả thanh lý).

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

Yêu cầu:

  • signer == rsmSigner
  • Bên gọi phải có SEQUENCER_ROLE
  • Token phải được hỗ trợ (_checkExchangeToken)

Hành vi:

  • Mã hóa dưới dạng ActionCasterEncoder.sendAsset với destination: EXCHANGE
  • Thực thi trên tài khoản đích

Entrypoint On-Chain: Exchange.rsmCommandRepay(RsmCommandRepay memory cmd, bytes memory signature)

Quản lý Nonce

Mỗi người ký (ví API, manager, RSM signer) có một không gian nonce độc lập:

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

Quy tắc:

  1. Nonce phải tăng nghiêm ngặt (không bắt buộc liên tục, nhưng nextNonce vẫn được duy trì)
  2. Một khi đã sử dụng, nonce không thể được tái sử dụng (được kiểm tra thông qua isNonceUsed(signer, nonce))
  3. nextNonce[signer]nonce nhỏ nhất được đảm bảo chưa sử dụng (các nonce thấp hơn có thể chưa được sử dụng nếu bị bỏ qua)

Truy vấn trạng thái Nonce:

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

Thực hành tốt nhất: Theo dõi nonce off-chain và tăng một cách nguyên tử (atomic). Sử dụng nextNonce để kiểm tra tính hợp lý.

Luồng xác minh chữ ký

  1. Off-Chain: Người ký tạo digest EIP-712 và ký bằng khóa riêng tư
  2. On-Chain: Exchange nhận thông điệp đã ký và gọi Processor.process*
  3. Processor: Xác minh chữ ký, khôi phục người ký, mã hóa các hành động ActionCaster
  4. Exchange: Kiểm tra nonce, xác minh ủy quyền (manager/ví API/RSM), thực thi các hành động

Luồng ví dụ (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

Các hàm không còn được khuyến nghị (Deprecated)

Các hàm sau đây không còn được khuyến nghị nhưng vẫn tồn tại để tương thích ngược:

  • placeCoreOrders (sử dụng hlRequestOrder)
  • cancelCoreOrders (sử dụng hlRequestCancel)
  • cancelCoreOrdersByCloid (sử dụng hlRequestCancelByCloid)

Các hàm này sử dụng cơ chế mã hóa MsgPack cũ và domain CoreSignatures ("Exchange", chainId 1337). Không sử dụng cho các tích hợp mới.

Các cân nhắc về bảo mật

  1. Lưu trữ khóa riêng tư: Lưu trữ khóa của ví API và manager một cách an toàn (ví phần cứng cho manager, lưu trữ mã hóa cho ví API).

  2. Replay Nonce: Không bao giờ tái sử dụng nonce. Theo dõi nonce off-chain và tăng một cách nguyên tử.

  3. Domain Separator: Luôn sử dụng đúng chain ID (998 cho testnet, mạng chính sẽ được công bố sau). Xác minh domain separator khớp với các hằng số trong hợp đồng.

  4. Xác minh chữ ký: Hợp đồng xác minh chữ ký on-chain. Không tin tưởng việc xác minh chữ ký off-chain cho các thao tác quan trọng.

  5. Manager và ví API: Manager kiểm soát quyền sở hữu tài khoản và các lệnh rút tiền. Ví API chỉ ký các yêu cầu giao dịch. Sử dụng các khóa riêng biệt.

Tài liệu tham khảo