이 페이지는 자동 번역되었습니다. 영어 원문이 정본입니다. 영어로 읽기
메인 콘텐츠로 건너뛰기

EIP-712 서명

이 문서는 온체인 액션에 사용되는 세 가지 EIP-712 서명 도메인인 에이전트 요청(Agent Requests), 매니저 액션(Manager Actions), RSM 커맨드(RSM Commands) 에 대해 설명합니다.

개요

대부분의 프로토콜 액션은 EIP-712 타입 데이터 서명이 필요합니다. 서명자는 다음 중 하나여야 합니다:

  1. 에이전트 (API 월렛): Exchange.addApiWallet을 통해 인가됨 - 거래 요청에 서명
  2. 매니저: 계정 소유자 - 출금 및 자산 이전에 서명
  3. RSM 서명자: 프로토콜이 제어 - 청산/리밸런스 커맨드에 서명

Exchange 컨트랙트는 서명을 검증하고 액션을 Processor로 전달하며, Processor는 이를 ActionCaster 메시지로 인코딩합니다.

서명 없는 자금 입출금 엔트리포인트

모든 자금 관련 호출이 EIP-712 액션인 것은 아닙니다. 다음 메서드는 지불 지갑 또는 라우터가 직접 전송하는 트랜잭션입니다:

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

depositUsdcFormsg.sender가 단지 USDC 지불자일 뿐이므로 의도적으로 서명이 없습니다. 입금이 반영되는 Hypercall 계정은 명시적인 account 인자이자 UsdcDeposit.account 이벤트 필드입니다. 라우터와 zap이 이 메서드를 호출할 수 있으므로, 인덱서와 백엔드 서비스는 입금 귀속을 판단할 때 msg.sender를 사용해서는 안 됩니다.

depositOptionmsg.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(...)")

인코딩:

  1. HLOrderstructHash(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: 매니저 액션 (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: trueencodedTif: 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: EXCHANGEActionCasterEncoder.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

규칙:

  1. 논스는 엄격하게 증가해야 합니다 (공백은 허용되나 nextNonce가 유지됨)
  2. 한 번 사용된 논스는 재사용할 수 없습니다 (isNonceUsed(signer, nonce)로 확인)
  3. nextNonce[signer]미사용이 보장되는 최소 논스입니다 (건너뛴 경우 더 낮은 논스가 미사용 상태일 수 있음)

논스 상태 조회:

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

모범 사례: 논스를 오프체인에서 추적하고 원자적으로 증가시키십시오. nextNonce는 정합성 확인 용도로 사용하십시오.

서명 검증 흐름

  1. 오프체인: 서명자가 EIP-712 다이제스트를 생성하고 개인키로 서명합니다
  2. 온체인: Exchange가 서명된 메시지를 수신하고 Processor.process*를 호출합니다
  3. Processor: 서명을 검증하고 서명자를 복원하며 ActionCaster 액션을 인코딩합니다
  4. 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)을 사용합니다. 신규 통합에는 사용하지 마십시오.

보안 고려 사항

  1. 개인키 보관: API 월렛과 매니저 키를 안전하게 보관하십시오 (매니저는 하드웨어 지갑, API 월렛은 암호화된 저장소 사용).

  2. 논스 재전송: 논스를 절대 재사용하지 마십시오. 논스를 오프체인에서 추적하고 원자적으로 증가시키십시오.

  3. 도메인 구분자: 항상 올바른 체인 ID를 사용하십시오 (테스트넷은 998, 메인넷은 미정). 도메인 구분자가 컨트랙트 상수와 일치하는지 확인하십시오.

  4. 서명 검증: 컨트랙트는 온체인에서 서명을 검증합니다. 중요한 작업에는 오프체인 서명 검증을 신뢰하지 마십시오.

  5. 매니저 vs API 월렛: 매니저는 계정 소유권과 출금을 제어합니다. API 월렛은 거래 요청에만 서명합니다. 별도의 키를 사용하십시오.

참고 자료

  • 계정 생성 및 API 월렛 설정은 온보딩을 참조하십시오
  • 오프체인 API 인증은 API 인증을 참조하십시오