Эта страница переведена автоматически. Оригинал на английском языке является каноническим. Читать на английском
Перейти к основному содержимому

Подписание EIP-712

В этом документе описаны три домена подписания EIP-712, используемые для ончейн-действий: Agent Requests (запросы агента), Manager Actions (действия менеджера) и RSM Commands (команды RSM).

Обзор

Большинство действий протокола требуют типизированных подписей EIP-712. Подписантом должен быть:

  1. Агент (API-кошелёк): авторизуется через Exchange.addApiWallet — подписывает торговые запросы
  2. Менеджер: владелец аккаунта — подписывает выводы средств и переводы активов
  3. RSM-подписант: контролируется протоколом — подписывает команды ликвидации/ребалансировки

Контракт Exchange проверяет подписи и передаёт действия в 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. Этот метод могут вызывать роутеры и запы, поэтому индексаторы и бэкенд-сервисы не должны использовать msg.sender для атрибуции зачислений.

depositOption сжигает опционные токены с адреса msg.sender и эмитирует событие Deposit(account, msg.sender, token, amount) для индексатора RSM. Путь зачисления опционов управляется событиями и не использует подпись менеджера, агента или RSM со стороны вносителя.

Разделители доменов EIP-712

Все три домена используют одинаковую структуру, но разные имена:

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

Идентификаторы сетей (Chain ID):

  • Тестнет: 998
  • Основная сеть: 999

Домен 1: Agent Requests (HypercallAgentSign)

Имя домена: "HypercallAgentSign"

Предвычисленные разделители домена:

  • Тестнет: 0x8f0a44075cd4e0c79e5bd379a6fad5fa1329a4ea76d74e4edfa1138933d35e8a
  • Основная сеть: используйте chain ID 999 и конфигурацию развёрнутого верификатора для активного окружения.

Подписант: API-кошелёк (должен быть авторизован через Exchange.addApiWallet)

Nonce: защита от повторного воспроизведения на уровне подписанта. Движок хранит 100 наибольших nonce для каждого подписанта. Новый nonce должен быть больше минимального в наборе и не должен быть использован ранее. Nonce должны находиться в диапазоне (T - 2 дня, T + 1 день) относительно серверной метки времени. В ончейне использование отслеживается через битовую карту с помощью Exchange.isNonceUsed(signer, nonce)

HLRequestOrder

Размещает perp/spot-ордера на 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(...)")

Кодирование:

  1. Захешируйте каждый HLOrder с помощью structHash(HLOrder)
  2. Упакуйте хеши ордеров: keccak256(abi.encodePacked(orderHashes))
  3. Захешируйте запрос: keccak256(abi.encode(HL_ORDER_REQUEST_TYPE_HASH, packedOrderHashes, nonce))
  4. Дайджест 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: защита от повторного воспроизведения на уровне менеджера. Та же модель ограниченного набора, что и для nonce агента: хранятся 100 наибольших nonce, новый nonce должен превышать минимум набора и не быть дубликатом. В ончейне отслеживается через Exchange.isNonceUsed(manager, nonce)

HLActionSendAsset

Отправляет активы с аккаунта на адрес назначения через ActionCaster.

Структура:

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 на аккаунт.

Структура:

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)

Поведение:

  • Действия ActionCaster инициирует Exchange (а не аккаунт)
  • Переводит токен с Exchange на аккаунт на HyperCore

Пример:

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 (в отличие от других выводов)
  • Минтит опционный токен на адрес recipient через IOptionToken(option).mint(recipient, amountWei)
  • Эмитирует событие 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-подписант (устанавливается через Exchange.setRsmSigner, проверяется в ончейне)

Nonce: nonce на уровне RSM-подписанта (отслеживается через Exchange.nextNonce[rsmSigner])

Команды RSM могут вызываться только ролью SEQUENCER_ROLE; маркетмейкеры не вызывают их напрямую.

RsmCommandRebalance

Исполняет reduce-only IOC-ордер на HyperCore для ребалансировки позиции.

Структура:

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

Поведение:

  • Кодируется как ActionCasterEncoder.limitOrder с reduceOnly: true и encodedTif: 3 (IOC)
  • Исполняется на целевом аккаунте

Ончейн-точка входа: 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)

Поведение:

  • Кодируется как ActionCasterEncoder.sendAsset с destination: EXCHANGE
  • Исполняется на целевом аккаунте

Ончейн-точка входа: 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

Правила:

  1. Nonce должны строго возрастать (пропуски допускаются, но nextNonce поддерживается)
  2. Использованный nonce нельзя использовать повторно (проверяется через isNonceUsed(signer, nonce))
  3. nextNonce[signer] — это минимальный гарантированно неиспользованный nonce (меньшие nonce могут быть неиспользованными, если были пропущены)

Запрос статуса nonce:

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

Рекомендация: отслеживайте nonce офчейн и увеличивайте их атомарно. Используйте nextNonce как контрольную проверку.

Схема проверки подписи

  1. Офчейн: подписант формирует дайджест EIP-712 и подписывает его приватным ключом
  2. Ончейн: Exchange получает подписанное сообщение и вызывает Processor.process*
  3. Processor: проверяет подпись, восстанавливает подписанта, кодирует действия ActionCaster
  4. 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). Не используйте их для новых интеграций.

Вопросы безопасности

  1. Хранение приватных ключей: храните ключи API-кошелька и менеджера в безопасности (аппаратный кошелёк для менеджера, зашифрованное хранилище для API-кошельков).

  2. Повторное использование nonce: никогда не используйте nonce повторно. Отслеживайте nonce офчейн и увеличивайте их атомарно.

  3. Разделитель домена: всегда используйте корректный chain ID (998 для тестнета, для основной сети будет объявлено позже). Проверяйте, что разделитель домена соответствует константам контракта.

  4. Проверка подписей: контракт проверяет подписи в ончейне. Не доверяйте офчейн-проверке подписей для критически важных операций.

  5. Менеджер и API-кошелёк: менеджеры контролируют владение аккаунтом и выводы средств. API-кошельки подписывают только торговые запросы. Используйте разные ключи.

Ссылки