1use std::str::FromStr;
30
31use alloy::{
32 signers::{SignerSync, local::PrivateKeySigner},
33 sol_types::{SolStruct, SolValue, eip712_domain},
34};
35use alloy_primitives::{Address, B256, FixedBytes, U256, address, keccak256};
36use rust_decimal::Decimal;
37
38use crate::{
39 common::{
40 credential::EvmPrivateKey,
41 enums::{PolymarketOrderSide, SignatureType},
42 },
43 http::{
44 error::{Error, Result},
45 models::PolymarketOrder,
46 },
47};
48
49const CLOB_AUTH_DOMAIN_NAME: &str = "ClobAuthDomain";
51const CLOB_AUTH_DOMAIN_VERSION: &str = "1";
52const CLOB_AUTH_MESSAGE: &str = "This message attests that I control the given wallet";
53
54pub const CTF_EXCHANGE: Address = address!("0xE111180000d2663C0091e4f400237545B87B996B");
56
57pub const NEG_RISK_CTF_EXCHANGE: Address = address!("0xe2222d279d744050d28e00520010520000310F59");
59
60pub const NEG_RISK_CTF_COLLATERAL_ADAPTER: Address =
62 address!("0xadA2005600Dec949baf300f4C6120000bDB6eAab");
63
64pub const POLYMARKET_COLLATERAL_TOKEN: Address =
66 address!("0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB");
67
68pub const CONDITIONAL_TOKENS: Address = address!("0x4D97DCd97eC945f40cF65F87097ACe5EA0476045");
70
71pub const COLLATERAL_APPROVAL_TARGETS: &[Address] = &[
73 CTF_EXCHANGE,
74 NEG_RISK_CTF_EXCHANGE,
75 NEG_RISK_CTF_COLLATERAL_ADAPTER,
76];
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum PolymarketApproval {
81 Collateral {
83 contract: Address,
85 spender: Address,
87 amount: U256,
89 },
90 ConditionalTokens {
92 contract: Address,
94 operator: Address,
96 approved: bool,
98 },
99}
100
101pub fn approval_plan() -> impl Iterator<Item = PolymarketApproval> {
103 COLLATERAL_APPROVAL_TARGETS
104 .iter()
105 .copied()
106 .flat_map(|target| {
107 [
108 PolymarketApproval::Collateral {
109 contract: POLYMARKET_COLLATERAL_TOKEN,
110 spender: target,
111 amount: U256::MAX,
112 },
113 PolymarketApproval::ConditionalTokens {
114 contract: CONDITIONAL_TOKENS,
115 operator: target,
116 approved: true,
117 },
118 ]
119 })
120}
121
122const DOMAIN_NAME: &str = "Polymarket CTF Exchange";
123const DOMAIN_VERSION: &str = "2";
124const POLYGON_CHAIN_ID: u64 = 137;
125const ORDER_TYPE_STRING: &str = concat!(
126 "Order(uint256 salt,address maker,address signer,uint256 tokenId,",
127 "uint256 makerAmount,uint256 takerAmount,uint8 side,uint8 signatureType,",
128 "uint256 timestamp,bytes32 metadata,bytes32 builder)",
129);
130const SOLADY_TYPE_STRING: &str = concat!(
131 "TypedDataSign(Order contents,string name,string version,uint256 chainId,",
132 "address verifyingContract,bytes32 salt)",
133 "Order(uint256 salt,address maker,address signer,uint256 tokenId,",
134 "uint256 makerAmount,uint256 takerAmount,uint8 side,uint8 signatureType,",
135 "uint256 timestamp,bytes32 metadata,bytes32 builder)",
136);
137const DOMAIN_TYPE_STRING: &str =
138 "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)";
139const DEPOSIT_WALLET_DOMAIN_NAME: &str = "DepositWallet";
140const DEPOSIT_WALLET_DOMAIN_VERSION: &str = "1";
141
142alloy::sol! {
146 struct ClobAuth {
147 address address;
148 string timestamp;
149 uint256 nonce;
150 string message;
151 }
152}
153
154alloy::sol! {
159 struct Order {
160 uint256 salt;
161 address maker;
162 address signer;
163 uint256 tokenId;
164 uint256 makerAmount;
165 uint256 takerAmount;
166 uint8 side;
167 uint8 signatureType;
168 uint256 timestamp;
169 bytes32 metadata;
170 bytes32 builder;
171 }
172}
173
174#[derive(Debug)]
176pub struct OrderSigner {
177 signer: PrivateKeySigner,
178}
179
180impl OrderSigner {
181 pub fn new(private_key: &EvmPrivateKey) -> Result<Self> {
183 let key_hex = private_key
184 .as_hex()
185 .strip_prefix("0x")
186 .unwrap_or(private_key.as_hex());
187 let signer = PrivateKeySigner::from_str(key_hex)
188 .map_err(|e| Error::bad_request(format!("Failed to create signer: {e}")))?;
189 Ok(Self { signer })
190 }
191
192 #[must_use]
194 pub fn address(&self) -> Address {
195 self.signer.address()
196 }
197
198 pub fn sign_order(&self, order: &PolymarketOrder, neg_risk: bool) -> Result<String> {
209 let order_signer = parse_address(&order.signer, "signer")?;
210 let order_maker = parse_address(&order.maker, "maker")?;
211 if order.signature_type == SignatureType::Poly1271 {
212 if order_signer != order_maker {
213 return Err(Error::bad_request(format!(
214 "POLY_1271 orders require maker and signer to both be the deposit wallet, maker was {order_maker}, signer was {order_signer}",
215 )));
216 }
217 } else if order_signer != self.signer.address() {
218 return Err(Error::bad_request(format!(
219 "Order signer {order_signer} does not match local signer {}",
220 self.signer.address(),
221 )));
222 }
223
224 let eip712_order = build_eip712_order(order)?;
225 let contract = exchange_contract(neg_risk);
226
227 if order.signature_type == SignatureType::Poly1271 {
228 return self.sign_poly_1271_order(&eip712_order, contract);
229 }
230
231 let domain = eip712_domain! {
232 name: DOMAIN_NAME,
233 version: DOMAIN_VERSION,
234 chain_id: POLYGON_CHAIN_ID,
235 verifying_contract: contract,
236 };
237
238 let signing_hash = eip712_order.eip712_signing_hash(&domain);
239 self.sign_hash(&signing_hash.0)
240 }
241
242 fn sign_poly_1271_order(&self, order: &Order, contract: Address) -> Result<String> {
243 let contents_hash = poly_1271_contents_hash(order);
244 let wallet_struct_hash = poly_1271_wallet_struct_hash(contents_hash, order.signer);
245 let app_domain_separator = ctf_exchange_domain_separator(contract);
246 let signing_hash = typed_data_hash(app_domain_separator, wallet_struct_hash);
247 let signature = self.sign_hash_b256(&signing_hash)?;
248
249 let mut encoded =
250 Vec::with_capacity(65 + 32 + 32 + ORDER_TYPE_STRING.len() + std::mem::size_of::<u16>());
251 encoded.extend_from_slice(&signature);
252 encoded.extend_from_slice(app_domain_separator.as_slice());
253 encoded.extend_from_slice(contents_hash.as_slice());
254 encoded.extend_from_slice(ORDER_TYPE_STRING.as_bytes());
255 encoded.extend_from_slice(&(ORDER_TYPE_STRING.len() as u16).to_be_bytes());
256
257 Ok(format!(
258 "0x{}",
259 alloy_primitives::hex::encode(encoded.as_slice())
260 ))
261 }
262
263 fn sign_hash_b256(&self, hash: &B256) -> Result<[u8; 65]> {
264 let signature = self
265 .signer
266 .sign_hash_sync(hash)
267 .map_err(|e| Error::bad_request(format!("Failed to sign order: {e}")))?;
268 Ok(signature.as_bytes())
269 }
270
271 fn sign_hash(&self, hash: &[u8; 32]) -> Result<String> {
272 let hash_b256 = B256::from(*hash);
273 let signature = self.sign_hash_b256(&hash_b256)?;
274 Ok(format!(
275 "0x{}",
276 alloy_primitives::hex::encode(signature.as_slice())
277 ))
278 }
279}
280
281pub fn order_hash(order: &PolymarketOrder, neg_risk: bool) -> Result<B256> {
286 let eip712_order = build_eip712_order(order)?;
287 let contract = exchange_contract(neg_risk);
288
289 let domain = eip712_domain! {
290 name: DOMAIN_NAME,
291 version: DOMAIN_VERSION,
292 chain_id: POLYGON_CHAIN_ID,
293 verifying_contract: contract,
294 };
295
296 Ok(eip712_order.eip712_signing_hash(&domain))
297}
298
299const fn exchange_contract(neg_risk: bool) -> Address {
300 if neg_risk {
301 NEG_RISK_CTF_EXCHANGE
302 } else {
303 CTF_EXCHANGE
304 }
305}
306
307fn order_type_hash() -> B256 {
308 keccak256(ORDER_TYPE_STRING.as_bytes())
309}
310
311fn solady_type_hash() -> B256 {
312 keccak256(SOLADY_TYPE_STRING.as_bytes())
313}
314
315fn domain_type_hash() -> B256 {
316 keccak256(DOMAIN_TYPE_STRING.as_bytes())
317}
318
319fn domain_name_hash() -> B256 {
320 keccak256(DOMAIN_NAME.as_bytes())
321}
322
323fn domain_version_hash() -> B256 {
324 keccak256(DOMAIN_VERSION.as_bytes())
325}
326
327fn deposit_wallet_name_hash() -> B256 {
328 keccak256(DEPOSIT_WALLET_DOMAIN_NAME.as_bytes())
329}
330
331fn deposit_wallet_version_hash() -> B256 {
332 keccak256(DEPOSIT_WALLET_DOMAIN_VERSION.as_bytes())
333}
334
335fn poly_1271_contents_hash(order: &Order) -> B256 {
336 let tuple = (
337 order_type_hash(),
338 order.salt,
339 order.maker,
340 order.signer,
341 order.tokenId,
342 order.makerAmount,
343 order.takerAmount,
344 U256::from(order.side),
345 U256::from(order.signatureType),
346 order.timestamp,
347 order.metadata,
348 order.builder,
349 );
350 keccak256(tuple.abi_encode())
351}
352
353fn poly_1271_wallet_struct_hash(contents_hash: B256, deposit_wallet: Address) -> B256 {
354 let tuple = (
355 solady_type_hash(),
356 contents_hash,
357 deposit_wallet_name_hash(),
358 deposit_wallet_version_hash(),
359 U256::from(POLYGON_CHAIN_ID),
360 deposit_wallet,
361 FixedBytes::<32>::ZERO,
362 );
363 keccak256(tuple.abi_encode())
364}
365
366fn ctf_exchange_domain_separator(contract: Address) -> B256 {
367 let tuple = (
368 domain_type_hash(),
369 domain_name_hash(),
370 domain_version_hash(),
371 U256::from(POLYGON_CHAIN_ID),
372 contract,
373 );
374 keccak256(tuple.abi_encode())
375}
376
377fn typed_data_hash(domain_separator: B256, struct_hash: B256) -> B256 {
378 let mut bytes = Vec::with_capacity(2 + 32 + 32);
379 bytes.push(0x19);
380 bytes.push(0x01);
381 bytes.extend_from_slice(domain_separator.as_slice());
382 bytes.extend_from_slice(struct_hash.as_slice());
383 keccak256(bytes)
384}
385
386pub fn sign_clob_auth(
393 private_key: &EvmPrivateKey,
394 timestamp: &str,
395 nonce: u64,
396) -> Result<(String, String)> {
397 let key_hex = private_key
398 .as_hex()
399 .strip_prefix("0x")
400 .unwrap_or(private_key.as_hex());
401 let signer = PrivateKeySigner::from_str(key_hex)
402 .map_err(|e| Error::bad_request(format!("Failed to create signer: {e}")))?;
403
404 let address = signer.address();
405
406 let auth = ClobAuth {
407 address,
408 timestamp: timestamp.to_string(),
409 nonce: U256::from(nonce),
410 message: CLOB_AUTH_MESSAGE.to_string(),
411 };
412
413 let domain = eip712_domain! {
414 name: CLOB_AUTH_DOMAIN_NAME,
415 version: CLOB_AUTH_DOMAIN_VERSION,
416 chain_id: POLYGON_CHAIN_ID,
417 };
418
419 let signing_hash = auth.eip712_signing_hash(&domain);
420 let signature = signer
421 .sign_hash_sync(&signing_hash)
422 .map_err(|e| Error::bad_request(format!("Failed to sign ClobAuth: {e}")))?;
423
424 let r = signature.r();
425 let s = signature.s();
426 let v = if signature.v() { 28u8 } else { 27u8 };
427
428 Ok((
429 format!("{address:#x}"),
430 format!("0x{r:064x}{s:064x}{v:02x}"),
431 ))
432}
433
434fn build_eip712_order(order: &PolymarketOrder) -> Result<Order> {
436 Ok(Order {
437 salt: U256::from(order.salt),
438 maker: parse_address(&order.maker, "maker")?,
439 signer: parse_address(&order.signer, "signer")?,
440 tokenId: U256::from_str(order.token_id.as_str())
441 .map_err(|e| Error::bad_request(format!("Invalid token ID: {e}")))?,
442 makerAmount: decimal_to_u256(order.maker_amount, "maker_amount")?,
443 takerAmount: decimal_to_u256(order.taker_amount, "taker_amount")?,
444 side: order_side_to_u8(order.side),
445 signatureType: order.signature_type as u8,
446 timestamp: U256::from_str(&order.timestamp)
447 .map_err(|e| Error::bad_request(format!("Invalid timestamp: {e}")))?,
448 metadata: parse_bytes32(&order.metadata, "metadata")?,
449 builder: parse_bytes32(&order.builder, "builder")?,
450 })
451}
452
453fn parse_address(addr: &str, field: &str) -> Result<Address> {
454 Address::from_str(addr).map_err(|e| Error::bad_request(format!("Invalid {field} address: {e}")))
455}
456
457fn parse_bytes32(value: &str, field: &str) -> Result<FixedBytes<32>> {
458 FixedBytes::<32>::from_str(value)
459 .map_err(|e| Error::bad_request(format!("Invalid {field} bytes32: {e}")))
460}
461
462fn decimal_to_u256(d: Decimal, field: &str) -> Result<U256> {
463 let normalized = d.normalize();
464 if normalized.scale() != 0 {
465 return Err(Error::bad_request(format!("{field} must be an integer")));
466 }
467 let mantissa = normalized.mantissa();
468 if mantissa < 0 {
469 return Err(Error::bad_request(format!("{field} must be non-negative")));
470 }
471 Ok(U256::from(mantissa as u128))
472}
473
474fn order_side_to_u8(side: PolymarketOrderSide) -> u8 {
475 match side {
476 PolymarketOrderSide::Buy => 0,
477 PolymarketOrderSide::Sell => 1,
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 use alloy_primitives::{Signature, keccak256};
484 use nautilus_core::hex;
485 use rstest::rstest;
486 use rust_decimal_macros::dec;
487 use ustr::Ustr;
488
489 use super::*;
490 use crate::common::enums::SignatureType;
491
492 const TEST_PRIVATE_KEY: &str =
493 "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
494
495 fn test_signer() -> OrderSigner {
496 let pk = EvmPrivateKey::new(TEST_PRIVATE_KEY).unwrap();
497 OrderSigner::new(&pk).unwrap()
498 }
499
500 const ZERO_BYTES32: &str = "0x0000000000000000000000000000000000000000000000000000000000000000";
501
502 fn test_order() -> PolymarketOrder {
503 PolymarketOrder {
504 salt: 123456789,
505 maker: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266".to_string(),
506 signer: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266".to_string(),
507 token_id: Ustr::from(
508 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
509 ),
510 maker_amount: dec!(100000000),
511 taker_amount: dec!(50000000),
512 side: PolymarketOrderSide::Buy,
513 signature_type: SignatureType::Eoa,
514 expiration: "0".to_string(),
515 timestamp: "1713398400000".to_string(),
516 metadata: ZERO_BYTES32.to_string(),
517 builder: ZERO_BYTES32.to_string(),
518 signature: String::new(),
519 }
520 }
521
522 #[rstest]
523 fn test_order_typehash_matches_contract() {
524 let expected = keccak256(
526 "Order(uint256 salt,address maker,address signer,uint256 tokenId,uint256 makerAmount,uint256 takerAmount,uint8 side,uint8 signatureType,uint256 timestamp,bytes32 metadata,bytes32 builder)",
527 );
528 let order = test_order();
529 let eip712_order = build_eip712_order(&order).unwrap();
530 assert_eq!(eip712_order.eip712_type_hash(), expected);
531 }
532
533 #[rstest]
534 fn test_signer_address_derivation() {
535 let signer = test_signer();
536 let expected = Address::from_str("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266").unwrap();
538 assert_eq!(signer.address(), expected);
539 }
540
541 #[rstest]
542 fn test_sign_order_format() {
543 let signer = test_signer();
544 let order = test_order();
545
546 let sig = signer.sign_order(&order, false).unwrap();
547
548 assert!(sig.starts_with("0x"));
549 assert_eq!(sig.len(), 132); }
551
552 #[rstest]
553 fn test_sign_poly_1271_order_format() {
554 let signer = test_signer();
555 let mut order = test_order();
556 order.maker = "0x1111111111111111111111111111111111111111".to_string();
557 order.signer = order.maker.clone();
558 order.signature_type = SignatureType::Poly1271;
559
560 let sig = signer.sign_order(&order, false).unwrap();
561
562 assert!(sig.starts_with("0x"));
563 assert_eq!(sig.len(), 636);
564 }
565
566 #[rstest]
567 fn test_sign_poly_1271_order_requires_deposit_wallet_signer() {
568 let signer = test_signer();
569 let mut order = test_order();
570 order.maker = "0x1111111111111111111111111111111111111111".to_string();
571 order.signature_type = SignatureType::Poly1271;
572
573 let err = signer.sign_order(&order, false).unwrap_err();
574
575 assert!(
576 err.to_string()
577 .contains("maker and signer to both be the deposit wallet")
578 );
579 }
580
581 #[rstest]
582 fn test_sign_order_deterministic() {
583 let signer = test_signer();
584 let order = test_order();
585
586 let sig1 = signer.sign_order(&order, false).unwrap();
587 let sig2 = signer.sign_order(&order, false).unwrap();
588 assert_eq!(sig1, sig2);
589 }
590
591 #[rstest]
592 fn test_sign_order_neg_risk_differs() {
593 let signer = test_signer();
594 let order = test_order();
595
596 let sig_normal = signer.sign_order(&order, false).unwrap();
597 let sig_neg_risk = signer.sign_order(&order, true).unwrap();
598 assert_ne!(sig_normal, sig_neg_risk);
599 }
600
601 #[rstest]
602 fn test_sign_order_sell_side() {
603 let signer = test_signer();
604 let mut order = test_order();
605 let sig_buy = signer.sign_order(&order, false).unwrap();
606
607 order.side = PolymarketOrderSide::Sell;
608 let sig_sell = signer.sign_order(&order, false).unwrap();
609 assert_ne!(sig_buy, sig_sell);
610 }
611
612 #[rstest]
613 fn test_sign_order_different_amounts() {
614 let signer = test_signer();
615 let mut order = test_order();
616 let sig1 = signer.sign_order(&order, false).unwrap();
617
618 order.maker_amount = dec!(200000000);
619 let sig2 = signer.sign_order(&order, false).unwrap();
620 assert_ne!(sig1, sig2);
621 }
622
623 #[rstest]
624 fn test_build_eip712_order() {
625 let order = test_order();
626 let eip712 = build_eip712_order(&order).unwrap();
627
628 assert_eq!(eip712.salt, U256::from(123456789u64));
629 assert_eq!(
630 eip712.maker,
631 Address::from_str("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266").unwrap()
632 );
633 assert_eq!(eip712.makerAmount, U256::from(100000000u128));
634 assert_eq!(eip712.takerAmount, U256::from(50000000u128));
635 assert_eq!(eip712.side, 0); assert_eq!(eip712.signatureType, 0); assert_eq!(eip712.timestamp, U256::from(1713398400000u128));
638 assert_eq!(eip712.metadata, FixedBytes::<32>::ZERO);
639 assert_eq!(eip712.builder, FixedBytes::<32>::ZERO);
640 }
641
642 #[rstest]
643 fn test_build_eip712_order_with_builder_code() {
644 let mut order = test_order();
645 order.builder =
646 "0x0000000000000000000000000000000000000000000000000000000000000001".to_string();
647 let eip712 = build_eip712_order(&order).unwrap();
648
649 let mut expected = [0u8; 32];
650 expected[31] = 1;
651 assert_eq!(eip712.builder, FixedBytes::<32>::from(expected));
652 }
653
654 #[rstest]
655 fn test_decimal_to_u256_integer() {
656 let result = decimal_to_u256(dec!(100000000), "test").unwrap();
657 assert_eq!(result, U256::from(100000000u128));
658 }
659
660 #[rstest]
661 fn test_decimal_to_u256_zero() {
662 let result = decimal_to_u256(dec!(0), "test").unwrap();
663 assert_eq!(result, U256::ZERO);
664 }
665
666 #[rstest]
667 fn test_decimal_to_u256_rejects_fractional() {
668 let result = decimal_to_u256(dec!(100.5), "test");
669 assert!(result.is_err());
670 }
671
672 #[rstest]
673 fn test_decimal_to_u256_rejects_negative() {
674 let result = decimal_to_u256(dec!(-1), "test");
675 assert!(result.is_err());
676 }
677
678 #[rstest]
679 fn test_order_side_mapping() {
680 assert_eq!(order_side_to_u8(PolymarketOrderSide::Buy), 0);
681 assert_eq!(order_side_to_u8(PolymarketOrderSide::Sell), 1);
682 }
683
684 #[rstest]
685 fn test_contract_addresses_nonzero() {
686 assert_ne!(CTF_EXCHANGE, Address::ZERO);
687 assert_ne!(NEG_RISK_CTF_EXCHANGE, Address::ZERO);
688 assert_ne!(CTF_EXCHANGE, NEG_RISK_CTF_EXCHANGE);
689 }
690
691 #[rstest]
692 fn test_v2_contract_addresses_pinned() {
693 assert_eq!(
697 format!("{CTF_EXCHANGE:#x}"),
698 "0xe111180000d2663c0091e4f400237545b87b996b"
699 );
700 assert_eq!(
701 format!("{NEG_RISK_CTF_EXCHANGE:#x}"),
702 "0xe2222d279d744050d28e00520010520000310f59"
703 );
704 assert_eq!(
705 format!("{NEG_RISK_CTF_COLLATERAL_ADAPTER:#x}"),
706 "0xada2005600dec949baf300f4c6120000bdb6eaab"
707 );
708 assert_eq!(
709 format!("{POLYMARKET_COLLATERAL_TOKEN:#x}"),
710 "0xc011a7e12a19f7b1f670d46f03b03f3342e82dfb"
711 );
712 assert_eq!(
713 format!("{CONDITIONAL_TOKENS:#x}"),
714 "0x4d97dcd97ec945f40cf65f87097ace5ea0476045"
715 );
716 assert_eq!(
717 COLLATERAL_APPROVAL_TARGETS,
718 &[
719 CTF_EXCHANGE,
720 NEG_RISK_CTF_EXCHANGE,
721 NEG_RISK_CTF_COLLATERAL_ADAPTER,
722 ]
723 );
724 }
725
726 #[rstest]
727 fn test_approval_plan() {
728 let collateral_token = address!("0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB");
729 let conditional_tokens = address!("0x4D97DCd97eC945f40cF65F87097ACe5EA0476045");
730 let ctf_exchange = address!("0xE111180000d2663C0091e4f400237545B87B996B");
731 let neg_risk_ctf_exchange = address!("0xe2222d279d744050d28e00520010520000310F59");
732 let neg_risk_collateral_adapter = address!("0xadA2005600Dec949baf300f4C6120000bDB6eAab");
733 let expected = vec![
734 PolymarketApproval::Collateral {
735 contract: collateral_token,
736 spender: ctf_exchange,
737 amount: U256::MAX,
738 },
739 PolymarketApproval::ConditionalTokens {
740 contract: conditional_tokens,
741 operator: ctf_exchange,
742 approved: true,
743 },
744 PolymarketApproval::Collateral {
745 contract: collateral_token,
746 spender: neg_risk_ctf_exchange,
747 amount: U256::MAX,
748 },
749 PolymarketApproval::ConditionalTokens {
750 contract: conditional_tokens,
751 operator: neg_risk_ctf_exchange,
752 approved: true,
753 },
754 PolymarketApproval::Collateral {
755 contract: collateral_token,
756 spender: neg_risk_collateral_adapter,
757 amount: U256::MAX,
758 },
759 PolymarketApproval::ConditionalTokens {
760 contract: conditional_tokens,
761 operator: neg_risk_collateral_adapter,
762 approved: true,
763 },
764 ];
765
766 assert_eq!(approval_plan().collect::<Vec<_>>(), expected);
767 }
768
769 #[rstest]
770 fn test_domain_version_is_v2() {
771 assert_eq!(DOMAIN_VERSION, "2");
774 }
775
776 #[rstest]
777 fn test_sign_order_recoverable() {
778 let signer = test_signer();
779 let order = test_order();
780 let sig_hex = signer.sign_order(&order, false).unwrap();
781
782 let sig_bytes = hex::decode(&sig_hex[2..]).unwrap();
783 assert_eq!(sig_bytes.len(), 65);
784
785 let r = U256::from_be_slice(&sig_bytes[..32]);
786 let s = U256::from_be_slice(&sig_bytes[32..64]);
787 let v = sig_bytes[64];
788 let y_parity = v == 28;
789
790 let signature = Signature::new(r, s, y_parity);
791
792 let eip712_order = build_eip712_order(&order).unwrap();
793 let domain = eip712_domain! {
794 name: DOMAIN_NAME,
795 version: DOMAIN_VERSION,
796 chain_id: POLYGON_CHAIN_ID,
797 verifying_contract: CTF_EXCHANGE,
798 };
799 let signing_hash = eip712_order.eip712_signing_hash(&domain);
800
801 let recovered = signature
802 .recover_address_from_prehash(&signing_hash)
803 .unwrap();
804 assert_eq!(recovered, signer.address());
805 }
806
807 const PARITY_TOKEN_ID: &str =
814 "71321045679252212594626385532706912750332728571942532289631379312455583992563";
815
816 fn parity_order(
817 salt: u64,
818 side: PolymarketOrderSide,
819 signature_type: SignatureType,
820 maker_amount: Decimal,
821 taker_amount: Decimal,
822 timestamp: &str,
823 builder: &str,
824 ) -> PolymarketOrder {
825 PolymarketOrder {
826 salt,
827 maker: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".to_string(),
828 signer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".to_string(),
829 token_id: Ustr::from(PARITY_TOKEN_ID),
830 maker_amount,
831 taker_amount,
832 side,
833 signature_type,
834 expiration: "0".to_string(),
835 timestamp: timestamp.to_string(),
836 metadata: ZERO_BYTES32.to_string(),
837 builder: builder.to_string(),
838 signature: String::new(),
839 }
840 }
841
842 #[rstest]
843 #[case::buy_standard_eoa(
844 parity_order(
845 123456789,
846 PolymarketOrderSide::Buy,
847 SignatureType::Eoa,
848 dec!(100000000),
849 dec!(50000000),
850 "1713398400000",
851 ZERO_BYTES32,
852 ),
853 false,
854 "0x32961c48ddac87ed3582f8e02097cd0eff4fcf80460306bd44b3710438dfa64c",
855 "0x89f178136333c8ebb32a19146cb891233e3202d474be6ef730c24dbc06ae4d2a0c99948a86d9b57de0f2c0bb8ec6964aa244ecf17cfa3a95b86878d0b64ad78a1b",
856 )]
857 #[case::sell_neg_risk_eoa(
858 parity_order(
859 987654321,
860 PolymarketOrderSide::Sell,
861 SignatureType::Eoa,
862 dec!(50000000),
863 dec!(100000000),
864 "1713398400000",
865 ZERO_BYTES32,
866 ),
867 true,
868 "0x8b878404bd92dea2bfea9975c9fcd816ec70a57ae431cb20d67bb773744aaef3",
869 "0xf7d60d64364e2615b08d9f69f3ea9afd3b4f83ecfbf05ddd3ca83f4916277fb97d2d080758d17c8e91c928aceb8ff252477ebaec051b672832500f63b4d36b061c",
870 )]
871 #[case::buy_with_builder_code_eoa(
872 parity_order(
873 1,
874 PolymarketOrderSide::Buy,
875 SignatureType::Eoa,
876 dec!(100000000),
877 dec!(50000000),
878 "1713398500000",
879 "0x0000000000000000000000000000000000000000000000000000000000000001",
880 ),
881 false,
882 "0x3df0b6f6ddfca837bc36964cae968b34ad35640b5d98f557c104da97e804e36a",
883 "0xf4d2b34659e8bc07a9572d40ee5a1639a1157409613b4c21566b1f33fd8fe11a364b3f306668cae7248ca7cdf72378f9266bc5628585aa939644400030671e081c",
884 )]
885 #[case::buy_poly_proxy(
886 parity_order(
891 111_111_111,
892 PolymarketOrderSide::Buy,
893 SignatureType::PolyProxy,
894 dec!(100000000),
895 dec!(50000000),
896 "1713398400000",
897 ZERO_BYTES32,
898 ),
899 false,
900 "0x8f88fe2fb3448f4b8ba639992029f0a47a01a14d15b5f2bf9833516571efd279",
901 "0x71a63c85b730cc934688f23ea6374afffef57a61690eba63dcb97a706c8a8d0f3d2a8e0280f3e252eb83be088ebae6b461a8eda18e559e079f59050b90057afa1c",
902 )]
903 #[case::sell_neg_risk_poly_gnosis_safe(
904 parity_order(
905 222_222_222,
906 PolymarketOrderSide::Sell,
907 SignatureType::PolyGnosisSafe,
908 dec!(50000000),
909 dec!(100000000),
910 "1713398400000",
911 ZERO_BYTES32,
912 ),
913 true,
914 "0xb34248702810a1d76580234a33f942a9801c3680de54cb3ef104572a8d482190",
915 "0xab9d33aee8b578fe5588c4a4b16bbef6fa05fc757020f95b98212e877a919e360a90296f02e97ecbabf3c200271ffe88eb9fd86912e8376cd27237bdad5f3abc1c",
916 )]
917 fn test_signature_matches_py_clob_client_v2(
918 #[case] order: PolymarketOrder,
919 #[case] neg_risk: bool,
920 #[case] expected_hash_hex: &str,
921 #[case] expected_signature_hex: &str,
922 ) {
923 let signer = test_signer();
924
925 let hash = order_hash(&order, neg_risk).unwrap();
926 assert_eq!(format!("{hash:#x}"), expected_hash_hex, "signing hash");
927
928 let signature = signer.sign_order(&order, neg_risk).unwrap();
929 assert_eq!(signature, expected_signature_hex, "signature");
930 }
931
932 #[rstest]
933 #[case::standard_exchange(
934 false,
935 "0x48cfd4c03dcee72230750e2dc5ea71048e91244c2a9fec2ed6ef790a74869596",
936 concat!(
937 "0x780beffb568c4510b8a135a92ca8071f124cf368fb0e902fe451c5f51e555791",
938 "0a2a87968caa08a242de85e83c27375fc09242e897aa77680f56622a54e6e7c",
939 "61c3264e159346253e26a64e00b69032db0e7d32f94628de3e6eecb50304d",
940 "7af3d2d3db4f9eed41f0490532a9460395d96602c6534a931bde4f0e3aad5",
941 "71e4f84a04f726465722875696e743235362073616c742c6164647265737320",
942 "6d616b65722c61646472657373207369676e65722c75696e7432353620746f",
943 "6b656e49642c75696e74323536206d616b6572416d6f756e742c75696e7432",
944 "35362074616b6572416d6f756e742c75696e743820736964652c75696e7438",
945 "207369676e6174757265547970652c75696e743235362074696d657374616d",
946 "702c62797465733332206d657461646174612c62797465733332206275696c",
947 "6465722900ba",
948 ),
949 )]
950 #[case::neg_risk_exchange(
951 true,
952 "0x82b94b9d570fdd7b07f517ea848606a9b74029f2387fbf07d8b9c856d652e608",
953 concat!(
954 "0xa411986b67c58113f8787ee54c41def9376d57ef4262b56438438710720604b7",
955 "00137e0175d7ae6afc40c2988179ad84b1b71a739f0974c8df3c12372f31b22c",
956 "1c9b858f53327b0bd13af8ec14cfb35234fb9eb7b0504d1a4e61f433840d3",
957 "0e81ad3db4f9eed41f0490532a9460395d96602c6534a931bde4f0e3aad571",
958 "e4f84a04f726465722875696e743235362073616c742c61646472657373206d",
959 "616b65722c61646472657373207369676e65722c75696e7432353620746f6b",
960 "656e49642c75696e74323536206d616b6572416d6f756e742c75696e743235",
961 "362074616b6572416d6f756e742c75696e743820736964652c75696e743820",
962 "7369676e6174757265547970652c75696e743235362074696d657374616d70",
963 "2c62797465733332206d657461646174612c62797465733332206275696c64",
964 "65722900ba",
965 ),
966 )]
967 fn test_poly_1271_signature_matches_py_clob_client_v2(
968 #[case] neg_risk: bool,
969 #[case] expected_hash: &str,
970 #[case] expected_signature: &str,
971 ) {
972 let signer = test_signer();
973 let mut order = parity_order(
974 333_333_333,
975 PolymarketOrderSide::Buy,
976 SignatureType::Poly1271,
977 dec!(100000000),
978 dec!(50000000),
979 "1713398400000",
980 ZERO_BYTES32,
981 );
982 order.maker = "0x1111111111111111111111111111111111111111".to_string();
983 order.signer = order.maker.clone();
984
985 let hash = order_hash(&order, neg_risk).unwrap();
986 assert_eq!(format!("{hash:#x}"), expected_hash, "signing hash");
987
988 let signature = signer.sign_order(&order, neg_risk).unwrap();
989 assert_eq!(signature, expected_signature, "signature");
990 }
991}