EIP-712 서명
이 문서는 온체인 액션에 사용되는 세 가지 EIP-712 서명 도메인인 에이전트 요청(Agent Requests), 매니저 액션(Manager Actions), RSM 커맨드(RSM Commands) 에 대해 설명합니다.
개요
대부분의 프로토콜 액션은 EIP-712 타입 데이터 서명이 필요합니다. 서명자는 다음 중 하나여야 합니다:
- 에이전트 (API 월렛):
Exchange.addApiWallet을 통해 인가됨 - 거래 요청에 서명 - 매니저: 계정 소유자 - 출금 및 자산 이전에 서명
- RSM 서명자: 프로토콜이 제어 - 청산/리밸런스 커맨드에 서명
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 도메인 구분자(Domain Separator)
세 도메인 모두 동일한 구조를 사용하지만 이름이 다릅니다:
{
"name": "<DomainName>",
"version": "1",
"chainId": <chainId>,
"verifyingContract": "0x0000000000000000000000000000000000000000"
}
체인 ID:
- 테스트넷:
998 - 메인넷:
999
도메인 1: 에이전트 요청 (HypercallAgentSign)
도메인 이름: "HypercallAgentSign"
사전 계산된 도메인 구분자:
- 테스트넷:
0x8f0a44075cd4e0c79e5bd379a6fad5fa1329a4ea76d74e4edfa1138933d35e8a - 메인넷: 체인 ID
999와 현재 활성 환경에 배포된 검증자 설정을 사용하십시오.
서명자: API 월렛 (Exchange.addApiWallet을 통해 인가되어야 함)
논스(Nonce): 서명자별 재전송 방지. 엔진은 서명자별로 가장 높은 100개의 논스를 저장합니다. 새 논스는 저장된 집합의 최솟값보다 커야 하며 이미 사용된 것이 아니어야 합니다. 논스는 서버 타임스탬프 기준 (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: 매니저 액션 (HypercallManagerSign)
도메인 이름: "HypercallManagerSign"
사전 계산된 도메인 구분자:
- 테스트넷:
0xd1f76b6138be892c14b71b0569bdb049cb44f239d34c78ef1ffaacd2466f9f18 - 메인넷: 미정
서명자: 계정 매니저 (계정을 생성한 EOA)
논스(Nonce): 매니저별 재전송 방지. 에이전트 논스와 동일한 제한 집합 모델을 사용합니다: 가장 높은 100개의 논스가 저장되며, 새 논스는 집합의 최솟값을 초과해야 하고 중복이 아니어야 합니다. 온체인에서는 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 커맨드 (HypercallRsmSign)
도메인 이름: "HypercallRsmSign"
사전 계산된 도메인 구분자:
- 테스트넷:
0x650b282053fb61d3fd477bdc28f6434311fe905e27cc4ca643e87e802c45938c - 메인넷: 미정
서명자: RSM 서명자 (Exchange.setRsmSigner를 통해 설정되며 온체인에서 검증됨)
논스(Nonce): RSM 서명자별 논스 (Exchange.nextNonce[rsmSigner]로 추적됨)
RSM 커맨드는 SEQUENCER_ROLE이 호출할 수 있으며, 마켓메이커는 이를 직접 호출하지 않습니다.
RsmCommandRebalance
포지션 리밸런싱을 위해 HyperCore에서 감소 전용(reduce-only) 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)
논스 관리
각 서명자(API 월렛, 매니저, RSM 서명자)는 독립적인 논스 공간을 가집니다:
mapping(address signer => uint256 nonce) public nextNonce;
mapping(address signer => BitMaps.BitMap) private _nonces; // Tracks used nonces
규칙:
- 논스는 엄격하게 증가해야 합니다 (공백은 허용되나
nextNonce가 유지됨) - 한 번 사용된 논스는 재사용할 수 없습니다 (
isNonceUsed(signer, nonce)로 확인) nextNonce[signer]는 미사용이 보장되는 최소 논스입니다 (건너뛴 경우 더 낮은 논스가 미사용 상태일 수 있음)
논스 상태 조회:
function isNonceUsed(address signer, uint256 nonce) external view returns (bool);
모범 사례: 논스를 오프체인에서 추적하고 원자적으로 증가시키십시오. nextNonce는 정합성 확인 용도로 사용하십시오.
서명 검증 흐름
- 오프체인: 서명자가 EIP-712 다이제스트를 생성하고 개인키로 서명합니다
- 온체인:
Exchange가 서명된 메시지를 수신하고Processor.process*를 호출합니다 - Processor: 서명을 검증하고 서명자를 복원하며 ActionCaster 액션을 인코딩합니다
- Exchange: 논스를 확인하고 인가(매니저/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
지원 중단(Deprecated) 함수
다음 함수들은 지원 중단되었지만 하위 호환성을 위해 여전히 존재합니다:
placeCoreOrders(hlRequestOrder사용 권장)cancelCoreOrders(hlRequestCancel사용 권장)cancelCoreOrdersByCloid(hlRequestCancelByCloid사용 권장)
이 함수들은 레거시 MsgPack 인코딩 방식과 CoreSignatures 도메인("Exchange", chainId 1337)을 사용합니다. 신규 통합에는 사용하지 마십시오.
보안 고려 사항
-
개인키 보관: API 월렛과 매니저 키를 안전하게 보관하십시오 (매니저는 하드웨어 지갑, API 월렛은 암호화된 저장소 사용).
-
논스 재전송: 논스를 절대 재사용하지 마십시오. 논스를 오프체인에서 추적하고 원자적으로 증가시키십시오.
-
도메인 구분자: 항상 올바른 체인 ID를 사용하십시오 (테스트넷은 998, 메인넷은 미정). 도메인 구분자가 컨트랙트 상수와 일치하는지 확인하십시오.
-
서명 검증: 컨트랙트는 온체인에서 서명을 검증합니다. 중요한 작업에는 오프체인 서명 검증을 신뢰하지 마십시오.
-
매니저 vs API 월렛: 매니저는 계정 소유권과 출금을 제어합니다. API 월렛은 거래 요청에만 서명합니다. 별도의 키를 사용하십시오.