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, Bytes, FixedBytes, U256, address, keccak256};
36#[cfg(test)]
37use nautilus_core::string::secret::SecretString;
38use rust_decimal::Decimal;
39
40use crate::{
41 common::{
42 credential::EvmPrivateKey,
43 enums::{PolymarketOrderSide, PolymarketSignatureType, PolymarketSignerType},
44 },
45 http::{
46 error::{Error, Result},
47 models::PolymarketOrder,
48 },
49};
50
51const CLOB_AUTH_DOMAIN_NAME: &str = "ClobAuthDomain";
53const CLOB_AUTH_DOMAIN_VERSION: &str = "1";
54const CLOB_AUTH_MESSAGE: &str = "This message attests that I control the given wallet";
55
56pub const CTF_EXCHANGE: Address = address!("0xE111180000d2663C0091e4f400237545B87B996B");
58
59pub const NEG_RISK_CTF_EXCHANGE: Address = address!("0xe2222d279d744050d28e00520010520000310F59");
61
62pub const CTF_COLLATERAL_ADAPTER: Address = address!("0xAdA100Db00Ca00073811820692005400218FcE1f");
64
65pub const NEG_RISK_CTF_COLLATERAL_ADAPTER: Address =
67 address!("0xadA2005600Dec949baf300f4C6120000bDB6eAab");
68
69pub const DEPOSIT_WALLET_FACTORY: Address = address!("0x00000000000Fb5C9ADea0298D729A0CB3823Cc07");
71
72pub const POLYMARKET_COLLATERAL_TOKEN: Address =
74 address!("0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB");
75
76pub const CONDITIONAL_TOKENS: Address = address!("0x4D97DCd97eC945f40cF65F87097ACe5EA0476045");
78
79pub const COLLATERAL_APPROVAL_TARGETS: &[Address] = &[
81 CTF_EXCHANGE,
82 NEG_RISK_CTF_EXCHANGE,
83 NEG_RISK_CTF_COLLATERAL_ADAPTER,
84];
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum PolymarketApproval {
89 Collateral {
91 contract: Address,
93 spender: Address,
95 amount: U256,
97 },
98 ConditionalTokens {
100 contract: Address,
102 operator: Address,
104 approved: bool,
106 },
107}
108
109pub fn approval_plan() -> impl Iterator<Item = PolymarketApproval> {
111 COLLATERAL_APPROVAL_TARGETS
112 .iter()
113 .copied()
114 .flat_map(|target| {
115 [
116 PolymarketApproval::Collateral {
117 contract: POLYMARKET_COLLATERAL_TOKEN,
118 spender: target,
119 amount: U256::MAX,
120 },
121 PolymarketApproval::ConditionalTokens {
122 contract: CONDITIONAL_TOKENS,
123 operator: target,
124 approved: true,
125 },
126 ]
127 })
128}
129
130const DOMAIN_NAME: &str = "Polymarket CTF Exchange";
131const DOMAIN_VERSION: &str = "2";
132const POLYGON_CHAIN_ID: u64 = 137;
133const ORDER_TYPE_STRING: &str = concat!(
134 "Order(uint256 salt,address maker,address signer,uint256 tokenId,",
135 "uint256 makerAmount,uint256 takerAmount,uint8 side,uint8 signatureType,",
136 "uint256 timestamp,bytes32 metadata,bytes32 builder)",
137);
138const SOLADY_TYPE_STRING: &str = concat!(
139 "TypedDataSign(Order contents,string name,string version,uint256 chainId,",
140 "address verifyingContract,bytes32 salt)",
141 "Order(uint256 salt,address maker,address signer,uint256 tokenId,",
142 "uint256 makerAmount,uint256 takerAmount,uint8 side,uint8 signatureType,",
143 "uint256 timestamp,bytes32 metadata,bytes32 builder)",
144);
145const DOMAIN_TYPE_STRING: &str =
146 "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)";
147const DEPOSIT_WALLET_DOMAIN_NAME: &str = "DepositWallet";
148const DEPOSIT_WALLET_DOMAIN_VERSION: &str = "1";
149
150alloy::sol! {
154 struct ClobAuth {
155 address address;
156 string timestamp;
157 uint256 nonce;
158 string message;
159 }
160}
161
162alloy::sol! {
166 struct Call {
167 address target;
168 uint256 value;
169 bytes data;
170 }
171
172 struct Batch {
173 address wallet;
174 uint256 nonce;
175 uint256 deadline;
176 Call[] calls;
177 }
178}
179
180alloy::sol! {
185 struct Order {
186 uint256 salt;
187 address maker;
188 address signer;
189 uint256 tokenId;
190 uint256 makerAmount;
191 uint256 takerAmount;
192 uint8 side;
193 uint8 signatureType;
194 uint256 timestamp;
195 bytes32 metadata;
196 bytes32 builder;
197 }
198}
199
200#[derive(Debug)]
202pub struct OrderSigner {
203 signer: PrivateKeySigner,
204 signer_type: PolymarketSignerType,
205}
206
207impl OrderSigner {
208 pub fn new(private_key: &EvmPrivateKey) -> Result<Self> {
210 let key_hex = private_key
211 .as_hex()
212 .strip_prefix("0x")
213 .unwrap_or(private_key.as_hex());
214 let signer = PrivateKeySigner::from_str(key_hex)
215 .map_err(|e| Error::bad_request(format!("Failed to create signer: {e}")))?;
216 Ok(Self {
217 signer,
218 signer_type: PolymarketSignerType::Owner,
219 })
220 }
221
222 #[must_use]
224 pub fn with_signer_type(mut self, signer_type: PolymarketSignerType) -> Self {
225 self.signer_type = signer_type;
226 self
227 }
228
229 #[must_use]
231 pub fn address(&self) -> Address {
232 self.signer.address()
233 }
234
235 pub fn sign_order(&self, order: &PolymarketOrder, neg_risk: bool) -> Result<String> {
246 if self.signer_type == PolymarketSignerType::Session
247 && order.signature_type != PolymarketSignatureType::Poly1271
248 {
249 return Err(Error::bad_request(
250 "Session signers require POLY_1271 orders",
251 ));
252 }
253
254 let order_signer = parse_address(&order.signer, "signer")?;
255 let order_maker = parse_address(&order.maker, "maker")?;
256 if order.signature_type == PolymarketSignatureType::Poly1271 {
257 if order_signer != order_maker {
258 return Err(Error::bad_request(format!(
259 "POLY_1271 orders require maker and signer to both be the deposit wallet, maker was {order_maker}, signer was {order_signer}",
260 )));
261 }
262 } else if order_signer != self.signer.address() {
263 return Err(Error::bad_request(format!(
264 "Order signer {order_signer} does not match local signer {}",
265 self.signer.address(),
266 )));
267 }
268
269 let eip712_order = build_eip712_order(order)?;
270 let contract = exchange_contract(neg_risk);
271
272 if order.signature_type == PolymarketSignatureType::Poly1271 {
273 return self.sign_poly_1271_order(&eip712_order, contract);
274 }
275
276 let domain = eip712_domain! {
277 name: DOMAIN_NAME,
278 version: DOMAIN_VERSION,
279 chain_id: POLYGON_CHAIN_ID,
280 verifying_contract: contract,
281 };
282
283 let signing_hash = eip712_order.eip712_signing_hash(&domain);
284 self.sign_hash(&signing_hash.0)
285 }
286
287 fn sign_poly_1271_order(&self, order: &Order, contract: Address) -> Result<String> {
288 let contents_hash = poly_1271_contents_hash(order);
289 let wallet_struct_hash = poly_1271_wallet_struct_hash(contents_hash, order.signer);
290 let app_domain_separator = ctf_exchange_domain_separator(contract);
291 let signing_hash = typed_data_hash(app_domain_separator, wallet_struct_hash);
292 let signature = self.sign_hash_b256(&signing_hash)?;
293
294 let mut encoded =
295 Vec::with_capacity(65 + 32 + 32 + ORDER_TYPE_STRING.len() + std::mem::size_of::<u16>());
296 encoded.extend_from_slice(&signature);
297 encoded.extend_from_slice(app_domain_separator.as_slice());
298 encoded.extend_from_slice(contents_hash.as_slice());
299 encoded.extend_from_slice(ORDER_TYPE_STRING.as_bytes());
300 encoded.extend_from_slice(&(ORDER_TYPE_STRING.len() as u16).to_be_bytes());
301
302 if self.signer_type == PolymarketSignerType::Session {
303 let signer_id = B256::left_padding_from(self.address().as_slice());
305 encoded = (signer_id, B256::ZERO, Bytes::from(encoded)).abi_encode_params();
306 encoded.extend_from_slice(&[0x64, 0x92].repeat(16));
307 }
308
309 Ok(format!(
310 "0x{}",
311 alloy_primitives::hex::encode(encoded.as_slice())
312 ))
313 }
314
315 pub fn sign_deposit_wallet_batch(
321 &self,
322 deposit_wallet: Address,
323 nonce: U256,
324 deadline: U256,
325 calls: &[DepositWalletCall],
326 ) -> Result<String> {
327 if calls.is_empty() {
328 return Err(Error::bad_request(
329 "Deposit Wallet batch must contain at least one call",
330 ));
331 }
332
333 let batch = Batch {
334 wallet: deposit_wallet,
335 nonce,
336 deadline,
337 calls: calls
338 .iter()
339 .map(|call| Call {
340 target: call.target,
341 value: call.value,
342 data: call.data.clone(),
343 })
344 .collect(),
345 };
346
347 let domain = eip712_domain! {
348 name: DEPOSIT_WALLET_DOMAIN_NAME,
349 version: DEPOSIT_WALLET_DOMAIN_VERSION,
350 chain_id: POLYGON_CHAIN_ID,
351 verifying_contract: deposit_wallet,
352 };
353 let signing_hash = batch.eip712_signing_hash(&domain);
354 self.sign_hash(&signing_hash.0)
355 }
356
357 fn sign_hash_b256(&self, hash: &B256) -> Result<[u8; 65]> {
358 let signature = self
359 .signer
360 .sign_hash_sync(hash)
361 .map_err(|e| Error::bad_request(format!("Failed to sign order: {e}")))?;
362 Ok(signature.as_bytes())
363 }
364
365 fn sign_hash(&self, hash: &[u8; 32]) -> Result<String> {
366 let hash_b256 = B256::from(*hash);
367 let signature = self.sign_hash_b256(&hash_b256)?;
368 Ok(format!(
369 "0x{}",
370 alloy_primitives::hex::encode(signature.as_slice())
371 ))
372 }
373}
374
375#[derive(Clone, Debug, PartialEq, Eq)]
377pub struct DepositWalletCall {
378 pub target: Address,
380 pub value: U256,
382 pub data: Bytes,
384}
385
386pub fn order_hash(order: &PolymarketOrder, neg_risk: bool) -> Result<B256> {
391 let eip712_order = build_eip712_order(order)?;
392 let contract = exchange_contract(neg_risk);
393
394 let domain = eip712_domain! {
395 name: DOMAIN_NAME,
396 version: DOMAIN_VERSION,
397 chain_id: POLYGON_CHAIN_ID,
398 verifying_contract: contract,
399 };
400
401 Ok(eip712_order.eip712_signing_hash(&domain))
402}
403
404const fn exchange_contract(neg_risk: bool) -> Address {
405 if neg_risk {
406 NEG_RISK_CTF_EXCHANGE
407 } else {
408 CTF_EXCHANGE
409 }
410}
411
412fn order_type_hash() -> B256 {
413 keccak256(ORDER_TYPE_STRING.as_bytes())
414}
415
416fn solady_type_hash() -> B256 {
417 keccak256(SOLADY_TYPE_STRING.as_bytes())
418}
419
420fn domain_type_hash() -> B256 {
421 keccak256(DOMAIN_TYPE_STRING.as_bytes())
422}
423
424fn domain_name_hash() -> B256 {
425 keccak256(DOMAIN_NAME.as_bytes())
426}
427
428fn domain_version_hash() -> B256 {
429 keccak256(DOMAIN_VERSION.as_bytes())
430}
431
432fn deposit_wallet_name_hash() -> B256 {
433 keccak256(DEPOSIT_WALLET_DOMAIN_NAME.as_bytes())
434}
435
436fn deposit_wallet_version_hash() -> B256 {
437 keccak256(DEPOSIT_WALLET_DOMAIN_VERSION.as_bytes())
438}
439
440fn poly_1271_contents_hash(order: &Order) -> B256 {
441 let tuple = (
442 order_type_hash(),
443 order.salt,
444 order.maker,
445 order.signer,
446 order.tokenId,
447 order.makerAmount,
448 order.takerAmount,
449 U256::from(order.side),
450 U256::from(order.signatureType),
451 order.timestamp,
452 order.metadata,
453 order.builder,
454 );
455 keccak256(tuple.abi_encode())
456}
457
458fn poly_1271_wallet_struct_hash(contents_hash: B256, deposit_wallet: Address) -> B256 {
459 let tuple = (
460 solady_type_hash(),
461 contents_hash,
462 deposit_wallet_name_hash(),
463 deposit_wallet_version_hash(),
464 U256::from(POLYGON_CHAIN_ID),
465 deposit_wallet,
466 FixedBytes::<32>::ZERO,
467 );
468 keccak256(tuple.abi_encode())
469}
470
471fn ctf_exchange_domain_separator(contract: Address) -> B256 {
472 let tuple = (
473 domain_type_hash(),
474 domain_name_hash(),
475 domain_version_hash(),
476 U256::from(POLYGON_CHAIN_ID),
477 contract,
478 );
479 keccak256(tuple.abi_encode())
480}
481
482fn typed_data_hash(domain_separator: B256, struct_hash: B256) -> B256 {
483 let mut bytes = Vec::with_capacity(2 + 32 + 32);
484 bytes.push(0x19);
485 bytes.push(0x01);
486 bytes.extend_from_slice(domain_separator.as_slice());
487 bytes.extend_from_slice(struct_hash.as_slice());
488 keccak256(bytes)
489}
490
491pub fn sign_clob_auth(
498 private_key: &EvmPrivateKey,
499 timestamp: &str,
500 nonce: u64,
501) -> Result<(String, String)> {
502 let key_hex = private_key
503 .as_hex()
504 .strip_prefix("0x")
505 .unwrap_or(private_key.as_hex());
506 let signer = PrivateKeySigner::from_str(key_hex)
507 .map_err(|e| Error::bad_request(format!("Failed to create signer: {e}")))?;
508
509 let address = signer.address();
510
511 let auth = ClobAuth {
512 address,
513 timestamp: timestamp.to_string(),
514 nonce: U256::from(nonce),
515 message: CLOB_AUTH_MESSAGE.to_string(),
516 };
517
518 let domain = eip712_domain! {
519 name: CLOB_AUTH_DOMAIN_NAME,
520 version: CLOB_AUTH_DOMAIN_VERSION,
521 chain_id: POLYGON_CHAIN_ID,
522 };
523
524 let signing_hash = auth.eip712_signing_hash(&domain);
525 let signature = signer
526 .sign_hash_sync(&signing_hash)
527 .map_err(|e| Error::bad_request(format!("Failed to sign ClobAuth: {e}")))?;
528
529 let r = signature.r();
530 let s = signature.s();
531 let v = if signature.v() { 28u8 } else { 27u8 };
532
533 Ok((
534 format!("{address:#x}"),
535 format!("0x{r:064x}{s:064x}{v:02x}"),
536 ))
537}
538
539fn build_eip712_order(order: &PolymarketOrder) -> Result<Order> {
541 Ok(Order {
542 salt: U256::from(order.salt),
543 maker: parse_address(&order.maker, "maker")?,
544 signer: parse_address(&order.signer, "signer")?,
545 tokenId: U256::from_str(order.token_id.as_str())
546 .map_err(|e| Error::bad_request(format!("Invalid token ID: {e}")))?,
547 makerAmount: decimal_to_u256(order.maker_amount, "maker_amount")?,
548 takerAmount: decimal_to_u256(order.taker_amount, "taker_amount")?,
549 side: order_side_to_u8(order.side),
550 signatureType: order.signature_type as u8,
551 timestamp: U256::from_str(&order.timestamp)
552 .map_err(|e| Error::bad_request(format!("Invalid timestamp: {e}")))?,
553 metadata: parse_bytes32(&order.metadata, "metadata")?,
554 builder: parse_bytes32(&order.builder, "builder")?,
555 })
556}
557
558pub(crate) fn parse_address(addr: &str, field: &str) -> Result<Address> {
559 Address::from_str(addr).map_err(|e| Error::bad_request(format!("Invalid {field} address: {e}")))
560}
561
562pub(crate) fn parse_bytes32(value: &str, field: &str) -> Result<FixedBytes<32>> {
563 FixedBytes::<32>::from_str(value)
564 .map_err(|e| Error::bad_request(format!("Invalid {field} bytes32: {e}")))
565}
566
567fn decimal_to_u256(d: Decimal, field: &str) -> Result<U256> {
568 let normalized = d.normalize();
569 if normalized.scale() != 0 {
570 return Err(Error::bad_request(format!("{field} must be an integer")));
571 }
572 let mantissa = normalized.mantissa();
573 if mantissa < 0 {
574 return Err(Error::bad_request(format!("{field} must be non-negative")));
575 }
576 Ok(U256::from(mantissa as u128))
577}
578
579fn order_side_to_u8(side: PolymarketOrderSide) -> u8 {
580 match side {
581 PolymarketOrderSide::Buy => 0,
582 PolymarketOrderSide::Sell => 1,
583 }
584}
585
586#[cfg(test)]
587mod tests {
588 use alloy_primitives::{Signature, keccak256};
589 use nautilus_core::hex;
590 use rstest::rstest;
591 use rust_decimal_macros::dec;
592 use ustr::Ustr;
593
594 use super::*;
595 use crate::common::enums::PolymarketSignatureType;
596
597 const TEST_PRIVATE_KEY: &str =
598 "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
599
600 fn test_signer() -> OrderSigner {
601 let pk = EvmPrivateKey::new(TEST_PRIVATE_KEY).unwrap();
602 OrderSigner::new(&pk).unwrap()
603 }
604
605 const ZERO_BYTES32: &str = "0x0000000000000000000000000000000000000000000000000000000000000000";
606
607 fn test_order() -> PolymarketOrder {
608 PolymarketOrder {
609 salt: 123456789,
610 maker: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266".to_string(),
611 signer: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266".to_string(),
612 token_id: Ustr::from(
613 "71321045679252212594626385532706912750332728571942532289631379312455583992563",
614 ),
615 maker_amount: dec!(100000000),
616 taker_amount: dec!(50000000),
617 side: PolymarketOrderSide::Buy,
618 signature_type: PolymarketSignatureType::Eoa,
619 expiration: "0".to_string(),
620 timestamp: "1713398400000".to_string(),
621 metadata: ZERO_BYTES32.to_string(),
622 builder: ZERO_BYTES32.to_string(),
623 signature: SecretString::default(),
624 }
625 }
626
627 #[rstest]
628 fn test_order_typehash_matches_contract() {
629 let expected = keccak256(
631 "Order(uint256 salt,address maker,address signer,uint256 tokenId,uint256 makerAmount,uint256 takerAmount,uint8 side,uint8 signatureType,uint256 timestamp,bytes32 metadata,bytes32 builder)",
632 );
633 let order = test_order();
634 let eip712_order = build_eip712_order(&order).unwrap();
635 assert_eq!(eip712_order.eip712_type_hash(), expected);
636 }
637
638 #[rstest]
639 fn test_signer_debug_redacts_private_key() {
640 let signer = test_signer();
641 let debug = format!("{signer:?}");
642
643 assert!(debug.contains(&format!("{:#x}", signer.address())));
644 assert!(!debug.contains(TEST_PRIVATE_KEY.trim_start_matches("0x")));
645 }
646
647 #[rstest]
648 fn test_signer_address_derivation() {
649 let signer = test_signer();
650 let expected = Address::from_str("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266").unwrap();
652 assert_eq!(signer.address(), expected);
653 }
654
655 #[rstest]
656 fn test_sign_clob_auth_matches_polymarket_mainnet_vector() {
657 let private_key = EvmPrivateKey::new(TEST_PRIVATE_KEY).unwrap();
658
659 let (address, signature) = sign_clob_auth(&private_key, "10000000", 23).unwrap();
660
661 assert_eq!(address, "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266");
663 assert_eq!(
664 signature,
665 "0x1a7118db6100dfd8efd102be36f472b59475dcac56eb4c9a2a94748d3655ba7c3c89deb8c19ee79eceb0a531122fbfbe88ed118034f9d8212e2b725e7b296b9d1c",
666 );
667 }
668
669 #[rstest]
670 fn test_sign_order_format() {
671 let signer = test_signer();
672 let order = test_order();
673
674 let sig = signer.sign_order(&order, false).unwrap();
675
676 assert!(sig.starts_with("0x"));
677 assert_eq!(sig.len(), 132); }
679
680 #[rstest]
681 fn test_sign_poly_1271_order_format() {
682 let signer = test_signer();
683 let mut order = test_order();
684 order.maker = "0x1111111111111111111111111111111111111111".to_string();
685 order.signer = order.maker.clone();
686 order.signature_type = PolymarketSignatureType::Poly1271;
687
688 let sig = signer.sign_order(&order, false).unwrap();
689
690 assert!(sig.starts_with("0x"));
691 assert_eq!(sig.len(), 636);
692 }
693
694 #[rstest]
695 fn test_sign_poly_1271_order_requires_deposit_wallet_signer() {
696 let signer = test_signer();
697 let mut order = test_order();
698 order.maker = "0x1111111111111111111111111111111111111111".to_string();
699 order.signature_type = PolymarketSignatureType::Poly1271;
700
701 let err = signer.sign_order(&order, false).unwrap_err();
702
703 assert!(
704 err.to_string()
705 .contains("maker and signer to both be the deposit wallet")
706 );
707 }
708
709 #[rstest]
710 fn test_sign_order_deterministic() {
711 let signer = test_signer();
712 let order = test_order();
713
714 let sig1 = signer.sign_order(&order, false).unwrap();
715 let sig2 = signer.sign_order(&order, false).unwrap();
716 assert_eq!(sig1, sig2);
717 }
718
719 #[rstest]
720 fn test_sign_deposit_wallet_batch_format_and_recovery() {
721 let signer = test_signer();
722 let deposit_wallet = address!("0x1111111111111111111111111111111111111111");
723
724 let calls = [DepositWalletCall {
725 target: CTF_COLLATERAL_ADAPTER,
726 value: U256::ZERO,
727 data: Bytes::from_static(&[0xde, 0xad, 0xbe, 0xef]),
728 }];
729
730 let sig = signer
731 .sign_deposit_wallet_batch(
732 deposit_wallet,
733 U256::from(7u64),
734 U256::from(1_714_000_000u64),
735 &calls,
736 )
737 .unwrap();
738
739 assert!(sig.starts_with("0x"));
740 assert_eq!(sig.len(), 132);
741
742 let batch = Batch {
743 wallet: deposit_wallet,
744 nonce: U256::from(7u64),
745 deadline: U256::from(1_714_000_000u64),
746 calls: vec![Call {
747 target: CTF_COLLATERAL_ADAPTER,
748 value: U256::ZERO,
749 data: Bytes::from_static(&[0xde, 0xad, 0xbe, 0xef]),
750 }],
751 };
752
753 let domain = eip712_domain! {
754 name: DEPOSIT_WALLET_DOMAIN_NAME,
755 version: DEPOSIT_WALLET_DOMAIN_VERSION,
756 chain_id: POLYGON_CHAIN_ID,
757 verifying_contract: deposit_wallet,
758 };
759 let signing_hash = batch.eip712_signing_hash(&domain);
760 let recovered = Signature::from_str(&sig)
761 .unwrap()
762 .recover_address_from_prehash(&signing_hash)
763 .unwrap();
764 assert_eq!(recovered, signer.address());
765 }
766
767 #[rstest]
768 fn test_sign_deposit_wallet_batch_rejects_empty_calls() {
769 let signer = test_signer();
770 let err = signer
771 .sign_deposit_wallet_batch(
772 address!("0x1111111111111111111111111111111111111111"),
773 U256::from(1u64),
774 U256::from(1_714_000_000u64),
775 &[],
776 )
777 .unwrap_err();
778 assert!(
779 err.to_string()
780 .contains("Deposit Wallet batch must contain at least one call")
781 );
782 }
783
784 #[rstest]
785 fn test_sign_order_neg_risk_differs() {
786 let signer = test_signer();
787 let order = test_order();
788
789 let sig_normal = signer.sign_order(&order, false).unwrap();
790 let sig_neg_risk = signer.sign_order(&order, true).unwrap();
791 assert_ne!(sig_normal, sig_neg_risk);
792 }
793
794 #[rstest]
795 fn test_sign_order_sell_side() {
796 let signer = test_signer();
797 let mut order = test_order();
798 let sig_buy = signer.sign_order(&order, false).unwrap();
799
800 order.side = PolymarketOrderSide::Sell;
801 let sig_sell = signer.sign_order(&order, false).unwrap();
802 assert_ne!(sig_buy, sig_sell);
803 }
804
805 #[rstest]
806 fn test_sign_order_different_amounts() {
807 let signer = test_signer();
808 let mut order = test_order();
809 let sig1 = signer.sign_order(&order, false).unwrap();
810
811 order.maker_amount = dec!(200000000);
812 let sig2 = signer.sign_order(&order, false).unwrap();
813 assert_ne!(sig1, sig2);
814 }
815
816 #[rstest]
817 fn test_build_eip712_order() {
818 let order = test_order();
819 let eip712 = build_eip712_order(&order).unwrap();
820
821 assert_eq!(eip712.salt, U256::from(123456789u64));
822 assert_eq!(
823 eip712.maker,
824 Address::from_str("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266").unwrap()
825 );
826 assert_eq!(eip712.makerAmount, U256::from(100000000u128));
827 assert_eq!(eip712.takerAmount, U256::from(50000000u128));
828 assert_eq!(eip712.side, 0); assert_eq!(eip712.signatureType, 0); assert_eq!(eip712.timestamp, U256::from(1713398400000u128));
831 assert_eq!(eip712.metadata, FixedBytes::<32>::ZERO);
832 assert_eq!(eip712.builder, FixedBytes::<32>::ZERO);
833 }
834
835 #[rstest]
836 fn test_build_eip712_order_with_builder_code() {
837 let mut order = test_order();
838 order.builder =
839 "0x0000000000000000000000000000000000000000000000000000000000000001".to_string();
840 let eip712 = build_eip712_order(&order).unwrap();
841
842 let mut expected = [0u8; 32];
843 expected[31] = 1;
844 assert_eq!(eip712.builder, FixedBytes::<32>::from(expected));
845 }
846
847 #[rstest]
848 fn test_decimal_to_u256_integer() {
849 let result = decimal_to_u256(dec!(100000000), "test").unwrap();
850 assert_eq!(result, U256::from(100000000u128));
851 }
852
853 #[rstest]
854 fn test_decimal_to_u256_zero() {
855 let result = decimal_to_u256(dec!(0), "test").unwrap();
856 assert_eq!(result, U256::ZERO);
857 }
858
859 #[rstest]
860 fn test_decimal_to_u256_rejects_fractional() {
861 let result = decimal_to_u256(dec!(100.5), "test");
862 assert!(result.is_err());
863 }
864
865 #[rstest]
866 fn test_decimal_to_u256_rejects_negative() {
867 let result = decimal_to_u256(dec!(-1), "test");
868 assert!(result.is_err());
869 }
870
871 #[rstest]
872 fn test_order_side_mapping() {
873 assert_eq!(order_side_to_u8(PolymarketOrderSide::Buy), 0);
874 assert_eq!(order_side_to_u8(PolymarketOrderSide::Sell), 1);
875 }
876
877 #[rstest]
878 fn test_contract_addresses_nonzero() {
879 assert_ne!(CTF_EXCHANGE, Address::ZERO);
880 assert_ne!(NEG_RISK_CTF_EXCHANGE, Address::ZERO);
881 assert_ne!(CTF_EXCHANGE, NEG_RISK_CTF_EXCHANGE);
882 }
883
884 #[rstest]
885 fn test_v2_contract_addresses_pinned() {
886 assert_eq!(
890 format!("{CTF_EXCHANGE:#x}"),
891 "0xe111180000d2663c0091e4f400237545b87b996b"
892 );
893 assert_eq!(
894 format!("{NEG_RISK_CTF_EXCHANGE:#x}"),
895 "0xe2222d279d744050d28e00520010520000310f59"
896 );
897 assert_eq!(
898 format!("{CTF_COLLATERAL_ADAPTER:#x}"),
899 "0xada100db00ca00073811820692005400218fce1f"
900 );
901 assert_eq!(
902 format!("{NEG_RISK_CTF_COLLATERAL_ADAPTER:#x}"),
903 "0xada2005600dec949baf300f4c6120000bdb6eaab"
904 );
905 assert_eq!(
906 format!("{DEPOSIT_WALLET_FACTORY:#x}"),
907 "0x00000000000fb5c9adea0298d729a0cb3823cc07"
908 );
909 assert_eq!(
910 format!("{POLYMARKET_COLLATERAL_TOKEN:#x}"),
911 "0xc011a7e12a19f7b1f670d46f03b03f3342e82dfb"
912 );
913 assert_eq!(
914 format!("{CONDITIONAL_TOKENS:#x}"),
915 "0x4d97dcd97ec945f40cf65f87097ace5ea0476045"
916 );
917 assert_eq!(
918 COLLATERAL_APPROVAL_TARGETS,
919 &[
920 CTF_EXCHANGE,
921 NEG_RISK_CTF_EXCHANGE,
922 NEG_RISK_CTF_COLLATERAL_ADAPTER,
923 ]
924 );
925 }
926
927 #[rstest]
928 fn test_approval_plan() {
929 let collateral_token = address!("0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB");
930 let conditional_tokens = address!("0x4D97DCd97eC945f40cF65F87097ACe5EA0476045");
931 let ctf_exchange = address!("0xE111180000d2663C0091e4f400237545B87B996B");
932 let neg_risk_ctf_exchange = address!("0xe2222d279d744050d28e00520010520000310F59");
933 let neg_risk_collateral_adapter = address!("0xadA2005600Dec949baf300f4C6120000bDB6eAab");
934 let expected = vec![
935 PolymarketApproval::Collateral {
936 contract: collateral_token,
937 spender: ctf_exchange,
938 amount: U256::MAX,
939 },
940 PolymarketApproval::ConditionalTokens {
941 contract: conditional_tokens,
942 operator: ctf_exchange,
943 approved: true,
944 },
945 PolymarketApproval::Collateral {
946 contract: collateral_token,
947 spender: neg_risk_ctf_exchange,
948 amount: U256::MAX,
949 },
950 PolymarketApproval::ConditionalTokens {
951 contract: conditional_tokens,
952 operator: neg_risk_ctf_exchange,
953 approved: true,
954 },
955 PolymarketApproval::Collateral {
956 contract: collateral_token,
957 spender: neg_risk_collateral_adapter,
958 amount: U256::MAX,
959 },
960 PolymarketApproval::ConditionalTokens {
961 contract: conditional_tokens,
962 operator: neg_risk_collateral_adapter,
963 approved: true,
964 },
965 ];
966
967 assert_eq!(approval_plan().collect::<Vec<_>>(), expected);
968 }
969
970 #[rstest]
971 fn test_domain_version_is_v2() {
972 assert_eq!(DOMAIN_VERSION, "2");
975 }
976
977 #[rstest]
978 fn test_sign_order_recoverable() {
979 let signer = test_signer();
980 let order = test_order();
981 let sig_hex = signer.sign_order(&order, false).unwrap();
982
983 let sig_bytes = hex::decode(&sig_hex[2..]).unwrap();
984 assert_eq!(sig_bytes.len(), 65);
985
986 let r = U256::from_be_slice(&sig_bytes[..32]);
987 let s = U256::from_be_slice(&sig_bytes[32..64]);
988 let v = sig_bytes[64];
989 let y_parity = v == 28;
990
991 let signature = Signature::new(r, s, y_parity);
992
993 let eip712_order = build_eip712_order(&order).unwrap();
994 let domain = eip712_domain! {
995 name: DOMAIN_NAME,
996 version: DOMAIN_VERSION,
997 chain_id: POLYGON_CHAIN_ID,
998 verifying_contract: CTF_EXCHANGE,
999 };
1000 let signing_hash = eip712_order.eip712_signing_hash(&domain);
1001
1002 let recovered = signature
1003 .recover_address_from_prehash(&signing_hash)
1004 .unwrap();
1005 assert_eq!(recovered, signer.address());
1006 }
1007
1008 const PARITY_TOKEN_ID: &str =
1015 "71321045679252212594626385532706912750332728571942532289631379312455583992563";
1016
1017 fn parity_order(
1018 salt: u64,
1019 side: PolymarketOrderSide,
1020 signature_type: PolymarketSignatureType,
1021 maker_amount: Decimal,
1022 taker_amount: Decimal,
1023 timestamp: &str,
1024 builder: &str,
1025 ) -> PolymarketOrder {
1026 PolymarketOrder {
1027 salt,
1028 maker: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".to_string(),
1029 signer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".to_string(),
1030 token_id: Ustr::from(PARITY_TOKEN_ID),
1031 maker_amount,
1032 taker_amount,
1033 side,
1034 signature_type,
1035 expiration: "0".to_string(),
1036 timestamp: timestamp.to_string(),
1037 metadata: ZERO_BYTES32.to_string(),
1038 builder: builder.to_string(),
1039 signature: SecretString::default(),
1040 }
1041 }
1042
1043 #[rstest]
1044 #[case::buy_standard_eoa(
1045 parity_order(
1046 123456789,
1047 PolymarketOrderSide::Buy,
1048 PolymarketSignatureType::Eoa,
1049 dec!(100000000),
1050 dec!(50000000),
1051 "1713398400000",
1052 ZERO_BYTES32,
1053 ),
1054 false,
1055 "0x32961c48ddac87ed3582f8e02097cd0eff4fcf80460306bd44b3710438dfa64c",
1056 "0x89f178136333c8ebb32a19146cb891233e3202d474be6ef730c24dbc06ae4d2a0c99948a86d9b57de0f2c0bb8ec6964aa244ecf17cfa3a95b86878d0b64ad78a1b",
1057 )]
1058 #[case::sell_neg_risk_eoa(
1059 parity_order(
1060 987654321,
1061 PolymarketOrderSide::Sell,
1062 PolymarketSignatureType::Eoa,
1063 dec!(50000000),
1064 dec!(100000000),
1065 "1713398400000",
1066 ZERO_BYTES32,
1067 ),
1068 true,
1069 "0x8b878404bd92dea2bfea9975c9fcd816ec70a57ae431cb20d67bb773744aaef3",
1070 "0xf7d60d64364e2615b08d9f69f3ea9afd3b4f83ecfbf05ddd3ca83f4916277fb97d2d080758d17c8e91c928aceb8ff252477ebaec051b672832500f63b4d36b061c",
1071 )]
1072 #[case::buy_with_builder_code_eoa(
1073 parity_order(
1074 1,
1075 PolymarketOrderSide::Buy,
1076 PolymarketSignatureType::Eoa,
1077 dec!(100000000),
1078 dec!(50000000),
1079 "1713398500000",
1080 "0x0000000000000000000000000000000000000000000000000000000000000001",
1081 ),
1082 false,
1083 "0x3df0b6f6ddfca837bc36964cae968b34ad35640b5d98f557c104da97e804e36a",
1084 "0xf4d2b34659e8bc07a9572d40ee5a1639a1157409613b4c21566b1f33fd8fe11a364b3f306668cae7248ca7cdf72378f9266bc5628585aa939644400030671e081c",
1085 )]
1086 #[case::buy_poly_proxy(
1087 parity_order(
1092 111_111_111,
1093 PolymarketOrderSide::Buy,
1094 PolymarketSignatureType::PolyProxy,
1095 dec!(100000000),
1096 dec!(50000000),
1097 "1713398400000",
1098 ZERO_BYTES32,
1099 ),
1100 false,
1101 "0x8f88fe2fb3448f4b8ba639992029f0a47a01a14d15b5f2bf9833516571efd279",
1102 "0x71a63c85b730cc934688f23ea6374afffef57a61690eba63dcb97a706c8a8d0f3d2a8e0280f3e252eb83be088ebae6b461a8eda18e559e079f59050b90057afa1c",
1103 )]
1104 #[case::sell_neg_risk_poly_gnosis_safe(
1105 parity_order(
1106 222_222_222,
1107 PolymarketOrderSide::Sell,
1108 PolymarketSignatureType::PolyGnosisSafe,
1109 dec!(50000000),
1110 dec!(100000000),
1111 "1713398400000",
1112 ZERO_BYTES32,
1113 ),
1114 true,
1115 "0xb34248702810a1d76580234a33f942a9801c3680de54cb3ef104572a8d482190",
1116 "0xab9d33aee8b578fe5588c4a4b16bbef6fa05fc757020f95b98212e877a919e360a90296f02e97ecbabf3c200271ffe88eb9fd86912e8376cd27237bdad5f3abc1c",
1117 )]
1118 fn test_signature_matches_py_clob_client_v2(
1119 #[case] order: PolymarketOrder,
1120 #[case] neg_risk: bool,
1121 #[case] expected_hash_hex: &str,
1122 #[case] expected_signature_hex: &str,
1123 ) {
1124 let signer = test_signer();
1125
1126 let hash = order_hash(&order, neg_risk).unwrap();
1127 assert_eq!(format!("{hash:#x}"), expected_hash_hex, "signing hash");
1128
1129 let signature = signer.sign_order(&order, neg_risk).unwrap();
1130 assert_eq!(signature, expected_signature_hex, "signature");
1131 }
1132
1133 #[rstest]
1134 #[case::standard_exchange(
1135 false,
1136 "0x48cfd4c03dcee72230750e2dc5ea71048e91244c2a9fec2ed6ef790a74869596",
1137 concat!(
1138 "0x780beffb568c4510b8a135a92ca8071f124cf368fb0e902fe451c5f51e555791",
1139 "0a2a87968caa08a242de85e83c27375fc09242e897aa77680f56622a54e6e7c",
1140 "61c3264e159346253e26a64e00b69032db0e7d32f94628de3e6eecb50304d",
1141 "7af3d2d3db4f9eed41f0490532a9460395d96602c6534a931bde4f0e3aad5",
1142 "71e4f84a04f726465722875696e743235362073616c742c6164647265737320",
1143 "6d616b65722c61646472657373207369676e65722c75696e7432353620746f",
1144 "6b656e49642c75696e74323536206d616b6572416d6f756e742c75696e7432",
1145 "35362074616b6572416d6f756e742c75696e743820736964652c75696e7438",
1146 "207369676e6174757265547970652c75696e743235362074696d657374616d",
1147 "702c62797465733332206d657461646174612c62797465733332206275696c",
1148 "6465722900ba",
1149 ),
1150 )]
1151 #[case::neg_risk_exchange(
1152 true,
1153 "0x82b94b9d570fdd7b07f517ea848606a9b74029f2387fbf07d8b9c856d652e608",
1154 concat!(
1155 "0xa411986b67c58113f8787ee54c41def9376d57ef4262b56438438710720604b7",
1156 "00137e0175d7ae6afc40c2988179ad84b1b71a739f0974c8df3c12372f31b22c",
1157 "1c9b858f53327b0bd13af8ec14cfb35234fb9eb7b0504d1a4e61f433840d3",
1158 "0e81ad3db4f9eed41f0490532a9460395d96602c6534a931bde4f0e3aad571",
1159 "e4f84a04f726465722875696e743235362073616c742c61646472657373206d",
1160 "616b65722c61646472657373207369676e65722c75696e7432353620746f6b",
1161 "656e49642c75696e74323536206d616b6572416d6f756e742c75696e743235",
1162 "362074616b6572416d6f756e742c75696e743820736964652c75696e743820",
1163 "7369676e6174757265547970652c75696e743235362074696d657374616d70",
1164 "2c62797465733332206d657461646174612c62797465733332206275696c64",
1165 "65722900ba",
1166 ),
1167 )]
1168 fn test_poly_1271_signature_matches_py_clob_client_v2(
1169 #[case] neg_risk: bool,
1170 #[case] expected_hash: &str,
1171 #[case] expected_signature: &str,
1172 ) {
1173 let signer = test_signer();
1174 let mut order = parity_order(
1175 333_333_333,
1176 PolymarketOrderSide::Buy,
1177 PolymarketSignatureType::Poly1271,
1178 dec!(100000000),
1179 dec!(50000000),
1180 "1713398400000",
1181 ZERO_BYTES32,
1182 );
1183 order.maker = "0x1111111111111111111111111111111111111111".to_string();
1184 order.signer = order.maker.clone();
1185
1186 let hash = order_hash(&order, neg_risk).unwrap();
1187 assert_eq!(format!("{hash:#x}"), expected_hash, "signing hash");
1188
1189 let signature = signer.sign_order(&order, neg_risk).unwrap();
1190 assert_eq!(signature, expected_signature, "signature");
1191 }
1192
1193 #[rstest]
1194 #[case(false)]
1195 #[case(true)]
1196 fn test_session_signature_envelope(#[case] neg_risk: bool) {
1197 let mut order = test_order();
1198 order.signature_type = PolymarketSignatureType::Poly1271;
1199 order.maker = "0x1111111111111111111111111111111111111111".to_string();
1200 order.signer = order.maker.clone();
1201 let owner = test_signer();
1202 let inner = owner.sign_order(&order, neg_risk).unwrap();
1203 let session = test_signer().with_signer_type(PolymarketSignerType::Session);
1204 let signature = session.sign_order(&order, neg_risk).unwrap();
1205 let bytes = alloy_primitives::hex::decode(&signature[2..]).unwrap();
1206 let (payload, magic) = bytes.split_at(bytes.len() - 32);
1207 let (signer_id, validator, wrapped) =
1208 <(B256, B256, Bytes)>::abi_decode_params(payload).unwrap();
1209 assert_eq!(
1210 signer_id,
1211 B256::left_padding_from(owner.address().as_slice())
1212 );
1213 assert_eq!(validator, B256::ZERO);
1214 assert_eq!(
1215 format!("0x{}", alloy_primitives::hex::encode(wrapped)),
1216 inner
1217 );
1218 assert_eq!(magic, &[0x64, 0x92].repeat(16));
1219 }
1220
1221 #[rstest]
1222 #[case(PolymarketSignatureType::Eoa)]
1223 #[case(PolymarketSignatureType::PolyProxy)]
1224 #[case(PolymarketSignatureType::PolyGnosisSafe)]
1225 fn test_session_rejects_other_wallets(#[case] signature_type: PolymarketSignatureType) {
1226 let mut order = test_order();
1227 order.signature_type = signature_type;
1228 let signer = test_signer().with_signer_type(PolymarketSignerType::Session);
1229 assert_eq!(
1230 signer.sign_order(&order, false).unwrap_err().to_string(),
1231 "bad request: Session signers require POLY_1271 orders"
1232 );
1233 }
1234
1235 #[rstest]
1236 fn test_session_signatures_match_official_sdk() {
1237 #[derive(serde::Deserialize)]
1238 struct Fixture {
1239 private_key: String,
1240 order: PolymarketOrder,
1241 vectors: Vec<Vector>,
1242 }
1243
1244 #[derive(serde::Deserialize)]
1245 struct Vector {
1246 neg_risk: bool,
1247 signature_chunks: Vec<String>,
1248 }
1249
1250 let fixture: Fixture =
1251 serde_json::from_str(include_str!("../../test_data/session_signatures.json")).unwrap();
1252 let key = EvmPrivateKey::new(&fixture.private_key).unwrap();
1253 let signer = OrderSigner::new(&key)
1254 .unwrap()
1255 .with_signer_type(PolymarketSignerType::Session);
1256 assert_eq!(fixture.vectors.len(), 2);
1257
1258 for vector in fixture.vectors {
1259 assert_eq!(
1260 signer.sign_order(&fixture.order, vector.neg_risk).unwrap(),
1261 vector.signature_chunks.concat()
1262 );
1263 }
1264 }
1265}