1use alloy::{
2 primitives::{Address, Signature as PrimitiveSignature, B256, U256},
3 sol,
4 sol_types::{Eip712Domain, SolStruct},
5};
6
7pub const HYPERCALL_TESTNET_CHAIN_ID: u64 = 998;
8pub const HYPERCALL_MAINNET_CHAIN_ID: u64 = 999;
9
10pub const HYPERCALL_API_DOMAIN_NAME: &str = "HypercallApiSign";
11pub const HYPERCALL_MANAGER_DOMAIN_NAME: &str = "HypercallManagerSign";
12pub const HYPERCALL_RSM_DOMAIN_NAME: &str = "HypercallRsmSign";
13
14pub const HYPERCALL_DOMAIN_VERSION: &str = "1";
15
16pub const HL_ACTION_VERSION: u8 = 1;
17pub const HC_ACTION_VERSION: u8 = 0;
18pub const SYSTEM_ACTION_VERSION: u8 = 0xFF;
19
20pub const HL_ACTION_ID_LIMIT_ORDER: u32 = 1;
21pub const HL_ACTION_ID_CANCEL_BY_OID: u32 = 10;
22pub const HL_ACTION_ID_CANCEL_BY_CLOID: u32 = 11;
23pub const HL_ACTION_ID_SEND_ASSET: u32 = 13;
24
25pub const HC_ACTION_ID_TRANSFER_OPTION: u32 = 1;
26pub const HC_ACTION_ID_UPDATE_API_WALLET: u32 = 2;
27pub const SYSTEM_ACTION_ID_CREDIT_TOKEN: u32 = 1;
28pub const SYSTEM_ACTION_ID_CREDIT_OPTION: u32 = 2;
29pub const SYSTEM_ACTION_ID_START_LIQUIDATION: u32 = 3;
30pub const SYSTEM_ACTION_ID_STOP_LIQUIDATION: u32 = 4;
31pub const SYSTEM_ACTION_ID_WITHDRAW_TOKEN: u32 = 5;
32
33pub const HL_TIF_ALO: u8 = 1;
34pub const HL_TIF_GTC: u8 = 2;
35pub const HL_TIF_IOC: u8 = 3;
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum DirectiveError {
39 UnsupportedChainId(u64),
40 InvalidSignatureLength(usize),
41 InvalidSignatureV(u8),
42 SignatureRecovery(String),
43}
44
45impl std::fmt::Display for DirectiveError {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 match self {
48 Self::UnsupportedChainId(chain_id) => {
49 write!(f, "Unsupported directive chain id: {}", chain_id)
50 }
51 Self::InvalidSignatureLength(len) => {
52 write!(
53 f,
54 "Invalid signature length: expected 65 bytes, got {}",
55 len
56 )
57 }
58 Self::InvalidSignatureV(v) => {
59 write!(f, "Invalid signature v value: {} (expected 0,1,27,28)", v)
60 }
61 Self::SignatureRecovery(msg) => write!(f, "Signature recovery failed: {}", msg),
62 }
63 }
64}
65
66impl std::error::Error for DirectiveError {}
67
68sol! {
69 #[derive(Debug, PartialEq, Eq)]
70 struct LimitOrder {
71 uint32 asset;
72 bool isBuy;
73 uint64 limitPx;
74 uint64 sz;
75 bool reduceOnly;
76 uint8 encodedTif;
77 uint128 cloid;
78 }
79
80 #[derive(Debug, PartialEq, Eq)]
81 struct CancelOrderByOid {
82 uint32 asset;
83 uint64 oid;
84 }
85
86 #[derive(Debug, PartialEq, Eq)]
87 struct CancelOrderByCloid {
88 uint32 asset;
89 uint128 cloid;
90 }
91
92 #[derive(Debug, PartialEq, Eq)]
93 struct SendAsset {
94 address destination;
95 address subAccount;
96 uint32 srcDex;
97 uint32 dstDex;
98 uint64 token;
99 uint64 amountWei;
100 }
101
102 #[derive(Debug, PartialEq, Eq)]
103 struct UpdateApiWallet {
104 bytes32 name;
105 address addr;
106 }
107
108 #[derive(Debug, PartialEq, Eq)]
109 struct TransferOption {
110 address recipient;
111 address option;
112 uint256 amountWei;
113 }
114
115 #[derive(Debug, PartialEq, Eq)]
116 struct CreditToken {
117 uint32 srcDex;
118 uint32 dstDex;
119 uint64 token;
120 uint64 amountWei;
121 }
122
123 #[derive(Debug, PartialEq, Eq)]
124 struct CreditOption {
125 bytes32 underlying;
126 uint256 expiry;
127 uint256 strike;
128 bool isCall;
129 uint256 amountWei;
130 }
131
132 #[derive(Debug, PartialEq, Eq)]
133 struct HLOrder {
134 address account;
135 uint64 nonce;
136 LimitOrder action;
137 }
138
139 #[derive(Debug, PartialEq, Eq)]
140 struct HLCancel {
141 address account;
142 uint64 nonce;
143 CancelOrderByOid action;
144 }
145
146 #[derive(Debug, PartialEq, Eq)]
147 struct HLCancelByCloid {
148 address account;
149 uint64 nonce;
150 CancelOrderByCloid action;
151 }
152
153 #[derive(Debug, PartialEq, Eq)]
154 struct HLSendAsset {
155 address account;
156 uint64 nonce;
157 SendAsset action;
158 }
159
160 #[derive(Debug, PartialEq, Eq)]
161 struct HCUpdateApiWallet {
162 address account;
163 uint64 nonce;
164 UpdateApiWallet action;
165 }
166
167 #[derive(Debug, PartialEq, Eq)]
168 struct HCTransferOption {
169 address account;
170 uint64 nonce;
171 TransferOption action;
172 }
173
174 #[derive(Debug, PartialEq, Eq)]
175 struct StartLiquidation {
176 int256 equity;
177 uint256 marginNeeded;
178 }
179
180 #[derive(Debug, PartialEq, Eq)]
181 struct StopLiquidation {
182 uint256 startTime;
183 }
184
185 #[derive(Debug, PartialEq, Eq)]
186 struct WithdrawToken {
187 address destination;
188 address subAccount;
189 uint32 srcDex;
190 uint32 dstDex;
191 uint64 token;
192 uint64 amountWei;
193 }
194
195 #[derive(Debug, PartialEq, Eq)]
196 struct SystemStartLiquidation {
197 address account;
198 uint64 nonce;
199 StartLiquidation action;
200 }
201
202 #[derive(Debug, PartialEq, Eq)]
203 struct SystemCreditToken {
204 address account;
205 uint64 nonce;
206 CreditToken action;
207 }
208
209 #[derive(Debug, PartialEq, Eq)]
210 struct SystemCreditOption {
211 address account;
212 uint64 nonce;
213 CreditOption action;
214 }
215
216 #[derive(Debug, PartialEq, Eq)]
217 struct SystemStopLiquidation {
218 address account;
219 uint64 nonce;
220 StopLiquidation action;
221 }
222
223 #[derive(Debug, PartialEq, Eq)]
224 struct SystemWithdrawToken {
225 address account;
226 uint64 nonce;
227 WithdrawToken action;
228 }
229}
230
231fn validate_chain_id(chain_id: u64) -> Result<(), DirectiveError> {
232 if chain_id == HYPERCALL_TESTNET_CHAIN_ID || chain_id == HYPERCALL_MAINNET_CHAIN_ID {
233 Ok(())
234 } else {
235 Err(DirectiveError::UnsupportedChainId(chain_id))
236 }
237}
238
239fn directive_domain(name: &'static str, chain_id: u64) -> Result<Eip712Domain, DirectiveError> {
240 validate_chain_id(chain_id)?;
241 Ok(Eip712Domain {
242 name: Some(name.into()),
243 version: Some(HYPERCALL_DOMAIN_VERSION.into()),
244 chain_id: Some(U256::from(chain_id)),
245 verifying_contract: Some(Address::ZERO),
246 salt: None,
247 })
248}
249
250pub fn hypercall_api_domain(chain_id: u64) -> Result<Eip712Domain, DirectiveError> {
251 directive_domain(HYPERCALL_API_DOMAIN_NAME, chain_id)
252}
253
254pub fn hypercall_manager_domain(chain_id: u64) -> Result<Eip712Domain, DirectiveError> {
255 directive_domain(HYPERCALL_MANAGER_DOMAIN_NAME, chain_id)
256}
257
258pub fn hypercall_rsm_domain(chain_id: u64) -> Result<Eip712Domain, DirectiveError> {
259 directive_domain(HYPERCALL_RSM_DOMAIN_NAME, chain_id)
260}
261
262pub fn recover_address_from_signature_bytes(
263 signature: &[u8],
264 signing_hash: &[u8; 32],
265) -> Result<Address, DirectiveError> {
266 if signature.len() != 65 {
267 return Err(DirectiveError::InvalidSignatureLength(signature.len()));
268 }
269
270 let parity = match signature[64] {
271 0 | 27 => false,
272 1 | 28 => true,
273 v => return Err(DirectiveError::InvalidSignatureV(v)),
274 };
275
276 let signature = PrimitiveSignature::from_bytes_and_parity(&signature[..64], parity);
277 signature
278 .recover_address_from_prehash(&B256::from(*signing_hash))
279 .map_err(|e| DirectiveError::SignatureRecovery(e.to_string()))
280}
281
282pub fn recover_hl_order_signer(
283 account: Address,
284 nonce: u64,
285 action: LimitOrder,
286 signature: &[u8],
287 chain_id: u64,
288) -> Result<Address, DirectiveError> {
289 let message = HLOrder {
290 account,
291 nonce,
292 action,
293 };
294 let domain = hypercall_api_domain(chain_id)?;
295 let signing_hash = message.eip712_signing_hash(&domain);
296 recover_address_from_signature_bytes(signature, &signing_hash)
297}
298
299pub fn recover_hl_cancel_by_oid_signer(
300 account: Address,
301 nonce: u64,
302 action: CancelOrderByOid,
303 signature: &[u8],
304 chain_id: u64,
305) -> Result<Address, DirectiveError> {
306 let message = HLCancel {
307 account,
308 nonce,
309 action,
310 };
311 let domain = hypercall_api_domain(chain_id)?;
312 let signing_hash = message.eip712_signing_hash(&domain);
313 recover_address_from_signature_bytes(signature, &signing_hash)
314}
315
316pub fn recover_hl_cancel_by_cloid_signer(
317 account: Address,
318 nonce: u64,
319 action: CancelOrderByCloid,
320 signature: &[u8],
321 chain_id: u64,
322) -> Result<Address, DirectiveError> {
323 let message = HLCancelByCloid {
324 account,
325 nonce,
326 action,
327 };
328 let domain = hypercall_api_domain(chain_id)?;
329 let signing_hash = message.eip712_signing_hash(&domain);
330 recover_address_from_signature_bytes(signature, &signing_hash)
331}
332
333pub fn recover_hc_update_api_wallet_signer(
334 account: Address,
335 nonce: u64,
336 action: UpdateApiWallet,
337 signature: &[u8],
338 chain_id: u64,
339) -> Result<Address, DirectiveError> {
340 let message = HCUpdateApiWallet {
341 account,
342 nonce,
343 action,
344 };
345 let domain = hypercall_manager_domain(chain_id)?;
346 let signing_hash = message.eip712_signing_hash(&domain);
347 recover_address_from_signature_bytes(signature, &signing_hash)
348}
349
350pub fn recover_rsm_hl_order_signer(
351 account: Address,
352 nonce: u64,
353 action: LimitOrder,
354 signature: &[u8],
355 chain_id: u64,
356) -> Result<Address, DirectiveError> {
357 let message = HLOrder {
358 account,
359 nonce,
360 action,
361 };
362 let domain = hypercall_rsm_domain(chain_id)?;
363 let signing_hash = message.eip712_signing_hash(&domain);
364 recover_address_from_signature_bytes(signature, &signing_hash)
365}
366
367pub fn recover_rsm_hl_cancel_by_oid_signer(
368 account: Address,
369 nonce: u64,
370 action: CancelOrderByOid,
371 signature: &[u8],
372 chain_id: u64,
373) -> Result<Address, DirectiveError> {
374 let message = HLCancel {
375 account,
376 nonce,
377 action,
378 };
379 let domain = hypercall_rsm_domain(chain_id)?;
380 let signing_hash = message.eip712_signing_hash(&domain);
381 recover_address_from_signature_bytes(signature, &signing_hash)
382}
383
384pub fn recover_rsm_hl_cancel_by_cloid_signer(
385 account: Address,
386 nonce: u64,
387 action: CancelOrderByCloid,
388 signature: &[u8],
389 chain_id: u64,
390) -> Result<Address, DirectiveError> {
391 let message = HLCancelByCloid {
392 account,
393 nonce,
394 action,
395 };
396 let domain = hypercall_rsm_domain(chain_id)?;
397 let signing_hash = message.eip712_signing_hash(&domain);
398 recover_address_from_signature_bytes(signature, &signing_hash)
399}
400
401pub fn recover_rsm_hl_send_asset_signer(
402 account: Address,
403 nonce: u64,
404 action: SendAsset,
405 signature: &[u8],
406 chain_id: u64,
407) -> Result<Address, DirectiveError> {
408 let message = HLSendAsset {
409 account,
410 nonce,
411 action,
412 };
413 let domain = hypercall_rsm_domain(chain_id)?;
414 let signing_hash = message.eip712_signing_hash(&domain);
415 recover_address_from_signature_bytes(signature, &signing_hash)
416}
417
418pub fn recover_system_credit_token_signer(
419 account: Address,
420 nonce: u64,
421 action: CreditToken,
422 signature: &[u8],
423 chain_id: u64,
424) -> Result<Address, DirectiveError> {
425 let message = SystemCreditToken {
426 account,
427 nonce,
428 action,
429 };
430 let domain = hypercall_rsm_domain(chain_id)?;
431 let signing_hash = message.eip712_signing_hash(&domain);
432 recover_address_from_signature_bytes(signature, &signing_hash)
433}
434
435pub fn recover_system_credit_option_signer(
436 account: Address,
437 nonce: u64,
438 action: CreditOption,
439 signature: &[u8],
440 chain_id: u64,
441) -> Result<Address, DirectiveError> {
442 let message = SystemCreditOption {
443 account,
444 nonce,
445 action,
446 };
447 let domain = hypercall_rsm_domain(chain_id)?;
448 let signing_hash = message.eip712_signing_hash(&domain);
449 recover_address_from_signature_bytes(signature, &signing_hash)
450}
451
452pub fn recover_system_start_liquidation_signer(
453 account: Address,
454 nonce: u64,
455 action: StartLiquidation,
456 signature: &[u8],
457 chain_id: u64,
458) -> Result<Address, DirectiveError> {
459 let message = SystemStartLiquidation {
460 account,
461 nonce,
462 action,
463 };
464 let domain = hypercall_rsm_domain(chain_id)?;
465 let signing_hash = message.eip712_signing_hash(&domain);
466 recover_address_from_signature_bytes(signature, &signing_hash)
467}
468
469pub fn recover_system_stop_liquidation_signer(
470 account: Address,
471 nonce: u64,
472 action: StopLiquidation,
473 signature: &[u8],
474 chain_id: u64,
475) -> Result<Address, DirectiveError> {
476 let message = SystemStopLiquidation {
477 account,
478 nonce,
479 action,
480 };
481 let domain = hypercall_rsm_domain(chain_id)?;
482 let signing_hash = message.eip712_signing_hash(&domain);
483 recover_address_from_signature_bytes(signature, &signing_hash)
484}
485
486pub fn recover_system_withdraw_token_signer(
487 account: Address,
488 nonce: u64,
489 action: WithdrawToken,
490 signature: &[u8],
491 chain_id: u64,
492) -> Result<Address, DirectiveError> {
493 let message = SystemWithdrawToken {
494 account,
495 nonce,
496 action,
497 };
498 let domain = hypercall_rsm_domain(chain_id)?;
499 let signing_hash = message.eip712_signing_hash(&domain);
500 recover_address_from_signature_bytes(signature, &signing_hash)
501}
502
503#[cfg(test)]
504mod tests {
505 use super::*;
506 use alloy::primitives::{address, keccak256, FixedBytes};
507 use alloy::sol_types::SolStruct;
508
509 #[test]
510 fn rsm_domain_separator_constants_match_contracts() {
511 let testnet = hypercall_rsm_domain(HYPERCALL_TESTNET_CHAIN_ID).unwrap();
512 let mainnet = hypercall_rsm_domain(HYPERCALL_MAINNET_CHAIN_ID).unwrap();
513
514 assert_eq!(
515 format!("{:#x}", testnet.hash_struct()),
516 "0x650b282053fb61d3fd477bdc28f6434311fe905e27cc4ca643e87e802c45938c"
517 );
518 assert_eq!(
519 format!("{:#x}", mainnet.hash_struct()),
520 "0x3d0cae2af623c614099dbadd67a1e1457fabde576aa270a70a57e39bf338a7be"
521 );
522 }
523
524 #[test]
525 fn rsm_type_hashes_match_contract_vectors() {
526 let account = address!("0x1111111111111111111111111111111111111111");
527
528 let credit_token = CreditToken {
529 srcDex: 1,
530 dstDex: 2,
531 token: 3,
532 amountWei: 4,
533 };
534 let credit_option = CreditOption {
535 underlying: FixedBytes::<32>::ZERO,
536 expiry: U256::from(1),
537 strike: U256::from(2),
538 isCall: true,
539 amountWei: U256::from(3),
540 };
541 let start = StartLiquidation {
542 equity: alloy::primitives::I256::try_from(-1_i128).unwrap(),
543 marginNeeded: U256::from(2),
544 };
545 let stop = StopLiquidation {
546 startTime: U256::from(1),
547 };
548 let order = LimitOrder {
549 asset: 1,
550 isBuy: true,
551 limitPx: 2,
552 sz: 3,
553 reduceOnly: false,
554 encodedTif: 2,
555 cloid: 4,
556 };
557 let cancel_oid = CancelOrderByOid { asset: 1, oid: 2 };
558 let cancel_cloid = CancelOrderByCloid { asset: 1, cloid: 2 };
559 let send_asset = SendAsset {
560 destination: account,
561 subAccount: Address::ZERO,
562 srcDex: 1,
563 dstDex: 2,
564 token: 3,
565 amountWei: 4,
566 };
567
568 let cases = [
569 (
570 keccak256(CreditToken::eip712_encode_type().as_bytes()),
571 "0xb3df0c3aecbb057482a169cb5a282d13cc163a21c12ee21da37e6b2b646312ba",
572 ),
573 (
574 SystemCreditToken {
575 account,
576 nonce: 1,
577 action: credit_token,
578 }
579 .eip712_type_hash(),
580 "0xe845f2049264182802994dae939dc8ac42d3ae543a7eeb530dd021428743f052",
581 ),
582 (
583 keccak256(CreditOption::eip712_encode_type().as_bytes()),
584 "0x60662ff4c34f920ea34056c513435d0aeafe81ad26d5f76bb26e1bf1608b910b",
585 ),
586 (
587 SystemCreditOption {
588 account,
589 nonce: 1,
590 action: credit_option,
591 }
592 .eip712_type_hash(),
593 "0x65aedd5af06882e1e62d752b43b72ec62a85cacf2ca59e8ec2affc57eed9d8ae",
594 ),
595 (
596 keccak256(StartLiquidation::eip712_encode_type().as_bytes()),
597 "0x322d6003c0c25354ccfe01c4c703c29f2d51892c2995c0ae32aea305bbd85086",
598 ),
599 (
600 SystemStartLiquidation {
601 account,
602 nonce: 1,
603 action: start,
604 }
605 .eip712_type_hash(),
606 "0x92b6a135a656e489c24fb5511684842b0ab3895c22b51e4910dee90a7d23511e",
607 ),
608 (
609 keccak256(StopLiquidation::eip712_encode_type().as_bytes()),
610 "0xc22a843839eef1d6531ff7299ff6e1cd92160be18e3b9d6f99dec5f14cdfd7b2",
611 ),
612 (
613 SystemStopLiquidation {
614 account,
615 nonce: 1,
616 action: stop,
617 }
618 .eip712_type_hash(),
619 "0xe1af98778e9cfd1d1a679bb0b64b6dc8aa3fb152ff6469d0a81a1c7eb87a4bd3",
620 ),
621 (
622 keccak256(LimitOrder::eip712_encode_type().as_bytes()),
623 "0xcc39512ca4619253e6a3650a478ceb1caa927f3429dd70c23e343970ca89d904",
624 ),
625 (
626 HLOrder {
627 account,
628 nonce: 1,
629 action: order,
630 }
631 .eip712_type_hash(),
632 "0xe6facb816c5f9c321054ef43c7d8fb1ec750f65d630ec39c5c18119a6f4707dd",
633 ),
634 (
635 keccak256(CancelOrderByOid::eip712_encode_type().as_bytes()),
636 "0xa623d44292da4a773c6bda0a721c370128f7bdfa31315a7799840c9c2b54b896",
637 ),
638 (
639 HLCancel {
640 account,
641 nonce: 1,
642 action: cancel_oid,
643 }
644 .eip712_type_hash(),
645 "0x23c5529f34a875b6c0d11a6dfafdd945996dbdc5fbd1bdace2c2e0cf6a253a85",
646 ),
647 (
648 keccak256(CancelOrderByCloid::eip712_encode_type().as_bytes()),
649 "0x66c29d69ed49e1f99d3a57d9a4ff80248962ccbf1553fc864a3dd38436ded220",
650 ),
651 (
652 HLCancelByCloid {
653 account,
654 nonce: 1,
655 action: cancel_cloid,
656 }
657 .eip712_type_hash(),
658 "0x10cbcfa56297fc737e4defee7b7937a52978ebbe5920e584317f7f25416836f5",
659 ),
660 (
661 keccak256(SendAsset::eip712_encode_type().as_bytes()),
662 "0x9900fb5faaa28fcaadbe37dc0bc284a05049ae8b10b5b83daef5e701c10e1569",
663 ),
664 (
665 HLSendAsset {
666 account,
667 nonce: 1,
668 action: send_asset,
669 }
670 .eip712_type_hash(),
671 "0x7feffdf7e7b191d5d27c2a209ca71c789289da73de203e99546abf9632860595",
672 ),
673 ];
674
675 for (actual, expected) in cases {
676 assert_eq!(format!("{:#x}", actual), expected);
677 }
678 }
679}