Skip to main content

nautilus_polymarket/signing/
eip712.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! EIP-712 order signing for the Polymarket CTF Exchange.
17//!
18//! Orders on Polymarket are signed typed structured data (EIP-712) against the
19//! CTF Exchange contract on Polygon. Two exchange contracts exist:
20//! - [`CTF_EXCHANGE`]: Standard binary markets.
21//! - [`NEG_RISK_CTF_EXCHANGE`]: Negative-risk (multi-outcome) markets.
22//!
23//! Both share the same EIP-712 domain name and version; only the
24//! `verifyingContract` differs.
25
26use std::str::FromStr;
27
28use alloy::{
29    signers::{SignerSync, local::PrivateKeySigner},
30    sol_types::{SolStruct, SolValue, eip712_domain},
31};
32use alloy_primitives::{Address, B256, FixedBytes, U256, address, keccak256};
33use rust_decimal::Decimal;
34
35use crate::{
36    common::{
37        credential::EvmPrivateKey,
38        enums::{PolymarketOrderSide, SignatureType},
39    },
40    http::{
41        error::{Error, Result},
42        models::PolymarketOrder,
43    },
44};
45
46// L1 ClobAuth constants
47const CLOB_AUTH_DOMAIN_NAME: &str = "ClobAuthDomain";
48const CLOB_AUTH_DOMAIN_VERSION: &str = "1";
49const CLOB_AUTH_MESSAGE: &str = "This message attests that I control the given wallet";
50
51/// CTF Exchange contract address on Polygon mainnet (CLOB V2).
52pub const CTF_EXCHANGE: Address = address!("0xE111180000d2663C0091e4f400237545B87B996B");
53
54/// Neg Risk CTF Exchange contract address on Polygon mainnet (CLOB V2).
55pub const NEG_RISK_CTF_EXCHANGE: Address = address!("0xe2222d279d744050d28e00520010520000310F59");
56
57const DOMAIN_NAME: &str = "Polymarket CTF Exchange";
58const DOMAIN_VERSION: &str = "2";
59const POLYGON_CHAIN_ID: u64 = 137;
60const ORDER_TYPE_STRING: &str = concat!(
61    "Order(uint256 salt,address maker,address signer,uint256 tokenId,",
62    "uint256 makerAmount,uint256 takerAmount,uint8 side,uint8 signatureType,",
63    "uint256 timestamp,bytes32 metadata,bytes32 builder)",
64);
65const SOLADY_TYPE_STRING: &str = concat!(
66    "TypedDataSign(Order contents,string name,string version,uint256 chainId,",
67    "address verifyingContract,bytes32 salt)",
68    "Order(uint256 salt,address maker,address signer,uint256 tokenId,",
69    "uint256 makerAmount,uint256 takerAmount,uint8 side,uint8 signatureType,",
70    "uint256 timestamp,bytes32 metadata,bytes32 builder)",
71);
72const DOMAIN_TYPE_STRING: &str =
73    "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)";
74const DEPOSIT_WALLET_DOMAIN_NAME: &str = "DepositWallet";
75const DEPOSIT_WALLET_DOMAIN_VERSION: &str = "1";
76
77// EIP-712 ClobAuth struct for L1 API authentication.
78//
79// Reference: <https://docs.polymarket.com/api-reference/authentication#l1-authentication>
80alloy::sol! {
81    struct ClobAuth {
82        address address;
83        string timestamp;
84        uint256 nonce;
85        string message;
86    }
87}
88
89// EIP-712 Order struct for CLOB V2 CTFExchange.
90//
91// Fees are set by the protocol at match time (not signed) and per-address
92// uniqueness comes from `timestamp` (milliseconds) rather than `nonce`.
93alloy::sol! {
94    struct Order {
95        uint256 salt;
96        address maker;
97        address signer;
98        uint256 tokenId;
99        uint256 makerAmount;
100        uint256 takerAmount;
101        uint8 side;
102        uint8 signatureType;
103        uint256 timestamp;
104        bytes32 metadata;
105        bytes32 builder;
106    }
107}
108
109/// EIP-712 order signer for the Polymarket CTF Exchange.
110#[derive(Debug)]
111pub struct OrderSigner {
112    signer: PrivateKeySigner,
113}
114
115impl OrderSigner {
116    /// Creates a new [`OrderSigner`] from an EVM private key.
117    pub fn new(private_key: &EvmPrivateKey) -> Result<Self> {
118        let key_hex = private_key
119            .as_hex()
120            .strip_prefix("0x")
121            .unwrap_or(private_key.as_hex());
122        let signer = PrivateKeySigner::from_str(key_hex)
123            .map_err(|e| Error::bad_request(format!("Failed to create signer: {e}")))?;
124        Ok(Self { signer })
125    }
126
127    /// Returns the signer's Ethereum address.
128    #[must_use]
129    pub fn address(&self) -> Address {
130        self.signer.address()
131    }
132
133    /// Signs a [`PolymarketOrder`] and returns the hex-encoded ECDSA signature.
134    ///
135    /// The `neg_risk` flag selects which exchange contract to use as the
136    /// EIP-712 `verifyingContract`.
137    ///
138    /// # Errors
139    ///
140    /// Returns an error if a non-`POLY_1271` order signer does not match this
141    /// signer's address, or if a `POLY_1271` order does not use the deposit
142    /// wallet for both `maker` and `signer`.
143    pub fn sign_order(&self, order: &PolymarketOrder, neg_risk: bool) -> Result<String> {
144        let order_signer = parse_address(&order.signer, "signer")?;
145        let order_maker = parse_address(&order.maker, "maker")?;
146        if order.signature_type == SignatureType::Poly1271 {
147            if order_signer != order_maker {
148                return Err(Error::bad_request(format!(
149                    "POLY_1271 orders require maker and signer to both be the deposit wallet, maker was {order_maker}, signer was {order_signer}",
150                )));
151            }
152        } else if order_signer != self.signer.address() {
153            return Err(Error::bad_request(format!(
154                "Order signer {order_signer} does not match local signer {}",
155                self.signer.address(),
156            )));
157        }
158
159        let eip712_order = build_eip712_order(order)?;
160        let contract = exchange_contract(neg_risk);
161
162        if order.signature_type == SignatureType::Poly1271 {
163            return self.sign_poly_1271_order(&eip712_order, contract);
164        }
165
166        let domain = eip712_domain! {
167            name: DOMAIN_NAME,
168            version: DOMAIN_VERSION,
169            chain_id: POLYGON_CHAIN_ID,
170            verifying_contract: contract,
171        };
172
173        let signing_hash = eip712_order.eip712_signing_hash(&domain);
174        self.sign_hash(&signing_hash.0)
175    }
176
177    fn sign_poly_1271_order(&self, order: &Order, contract: Address) -> Result<String> {
178        let contents_hash = poly_1271_contents_hash(order);
179        let wallet_struct_hash = poly_1271_wallet_struct_hash(contents_hash, order.signer);
180        let app_domain_separator = ctf_exchange_domain_separator(contract);
181        let signing_hash = typed_data_hash(app_domain_separator, wallet_struct_hash);
182        let signature = self.sign_hash_b256(&signing_hash)?;
183
184        let mut encoded =
185            Vec::with_capacity(65 + 32 + 32 + ORDER_TYPE_STRING.len() + std::mem::size_of::<u16>());
186        encoded.extend_from_slice(&signature);
187        encoded.extend_from_slice(app_domain_separator.as_slice());
188        encoded.extend_from_slice(contents_hash.as_slice());
189        encoded.extend_from_slice(ORDER_TYPE_STRING.as_bytes());
190        encoded.extend_from_slice(&(ORDER_TYPE_STRING.len() as u16).to_be_bytes());
191
192        Ok(format!(
193            "0x{}",
194            alloy_primitives::hex::encode(encoded.as_slice())
195        ))
196    }
197
198    fn sign_hash_b256(&self, hash: &B256) -> Result<[u8; 65]> {
199        let signature = self
200            .signer
201            .sign_hash_sync(hash)
202            .map_err(|e| Error::bad_request(format!("Failed to sign order: {e}")))?;
203        Ok(signature.as_bytes())
204    }
205
206    fn sign_hash(&self, hash: &[u8; 32]) -> Result<String> {
207        let hash_b256 = B256::from(*hash);
208        let signature = self.sign_hash_b256(&hash_b256)?;
209        Ok(format!(
210            "0x{}",
211            alloy_primitives::hex::encode(signature.as_slice())
212        ))
213    }
214}
215
216/// Computes the EIP-712 signing hash used by Polymarket as the order ID.
217///
218/// The `neg_risk` flag selects which exchange contract to use as the
219/// EIP-712 `verifyingContract`.
220pub fn order_hash(order: &PolymarketOrder, neg_risk: bool) -> Result<B256> {
221    let eip712_order = build_eip712_order(order)?;
222    let contract = exchange_contract(neg_risk);
223
224    let domain = eip712_domain! {
225        name: DOMAIN_NAME,
226        version: DOMAIN_VERSION,
227        chain_id: POLYGON_CHAIN_ID,
228        verifying_contract: contract,
229    };
230
231    Ok(eip712_order.eip712_signing_hash(&domain))
232}
233
234const fn exchange_contract(neg_risk: bool) -> Address {
235    if neg_risk {
236        NEG_RISK_CTF_EXCHANGE
237    } else {
238        CTF_EXCHANGE
239    }
240}
241
242fn order_type_hash() -> B256 {
243    keccak256(ORDER_TYPE_STRING.as_bytes())
244}
245
246fn solady_type_hash() -> B256 {
247    keccak256(SOLADY_TYPE_STRING.as_bytes())
248}
249
250fn domain_type_hash() -> B256 {
251    keccak256(DOMAIN_TYPE_STRING.as_bytes())
252}
253
254fn domain_name_hash() -> B256 {
255    keccak256(DOMAIN_NAME.as_bytes())
256}
257
258fn domain_version_hash() -> B256 {
259    keccak256(DOMAIN_VERSION.as_bytes())
260}
261
262fn deposit_wallet_name_hash() -> B256 {
263    keccak256(DEPOSIT_WALLET_DOMAIN_NAME.as_bytes())
264}
265
266fn deposit_wallet_version_hash() -> B256 {
267    keccak256(DEPOSIT_WALLET_DOMAIN_VERSION.as_bytes())
268}
269
270fn poly_1271_contents_hash(order: &Order) -> B256 {
271    let tuple = (
272        order_type_hash(),
273        order.salt,
274        order.maker,
275        order.signer,
276        order.tokenId,
277        order.makerAmount,
278        order.takerAmount,
279        U256::from(order.side),
280        U256::from(order.signatureType),
281        order.timestamp,
282        order.metadata,
283        order.builder,
284    );
285    keccak256(tuple.abi_encode())
286}
287
288fn poly_1271_wallet_struct_hash(contents_hash: B256, deposit_wallet: Address) -> B256 {
289    let tuple = (
290        solady_type_hash(),
291        contents_hash,
292        deposit_wallet_name_hash(),
293        deposit_wallet_version_hash(),
294        U256::from(POLYGON_CHAIN_ID),
295        deposit_wallet,
296        FixedBytes::<32>::ZERO,
297    );
298    keccak256(tuple.abi_encode())
299}
300
301fn ctf_exchange_domain_separator(contract: Address) -> B256 {
302    let tuple = (
303        domain_type_hash(),
304        domain_name_hash(),
305        domain_version_hash(),
306        U256::from(POLYGON_CHAIN_ID),
307        contract,
308    );
309    keccak256(tuple.abi_encode())
310}
311
312fn typed_data_hash(domain_separator: B256, struct_hash: B256) -> B256 {
313    let mut bytes = Vec::with_capacity(2 + 32 + 32);
314    bytes.push(0x19);
315    bytes.push(0x01);
316    bytes.extend_from_slice(domain_separator.as_slice());
317    bytes.extend_from_slice(struct_hash.as_slice());
318    keccak256(bytes)
319}
320
321/// Signs a ClobAuth EIP-712 message for L1 API authentication.
322///
323/// Used to create or derive API credentials via the CLOB `/auth/api-key`
324/// and `/auth/derive-api-key` endpoints.
325///
326/// Returns `(signer_address_hex, signature_hex)`.
327pub fn sign_clob_auth(
328    private_key: &EvmPrivateKey,
329    timestamp: &str,
330    nonce: u64,
331) -> Result<(String, String)> {
332    let key_hex = private_key
333        .as_hex()
334        .strip_prefix("0x")
335        .unwrap_or(private_key.as_hex());
336    let signer = PrivateKeySigner::from_str(key_hex)
337        .map_err(|e| Error::bad_request(format!("Failed to create signer: {e}")))?;
338
339    let address = signer.address();
340
341    let auth = ClobAuth {
342        address,
343        timestamp: timestamp.to_string(),
344        nonce: U256::from(nonce),
345        message: CLOB_AUTH_MESSAGE.to_string(),
346    };
347
348    let domain = eip712_domain! {
349        name: CLOB_AUTH_DOMAIN_NAME,
350        version: CLOB_AUTH_DOMAIN_VERSION,
351        chain_id: POLYGON_CHAIN_ID,
352    };
353
354    let signing_hash = auth.eip712_signing_hash(&domain);
355    let signature = signer
356        .sign_hash_sync(&signing_hash)
357        .map_err(|e| Error::bad_request(format!("Failed to sign ClobAuth: {e}")))?;
358
359    let r = signature.r();
360    let s = signature.s();
361    let v = if signature.v() { 28u8 } else { 27u8 };
362
363    Ok((
364        format!("{address:#x}"),
365        format!("0x{r:064x}{s:064x}{v:02x}"),
366    ))
367}
368
369// Converts a PolymarketOrder to the EIP-712 Order struct
370fn build_eip712_order(order: &PolymarketOrder) -> Result<Order> {
371    Ok(Order {
372        salt: U256::from(order.salt),
373        maker: parse_address(&order.maker, "maker")?,
374        signer: parse_address(&order.signer, "signer")?,
375        tokenId: U256::from_str(order.token_id.as_str())
376            .map_err(|e| Error::bad_request(format!("Invalid token ID: {e}")))?,
377        makerAmount: decimal_to_u256(order.maker_amount, "maker_amount")?,
378        takerAmount: decimal_to_u256(order.taker_amount, "taker_amount")?,
379        side: order_side_to_u8(order.side),
380        signatureType: order.signature_type as u8,
381        timestamp: U256::from_str(&order.timestamp)
382            .map_err(|e| Error::bad_request(format!("Invalid timestamp: {e}")))?,
383        metadata: parse_bytes32(&order.metadata, "metadata")?,
384        builder: parse_bytes32(&order.builder, "builder")?,
385    })
386}
387
388fn parse_address(addr: &str, field: &str) -> Result<Address> {
389    Address::from_str(addr).map_err(|e| Error::bad_request(format!("Invalid {field} address: {e}")))
390}
391
392fn parse_bytes32(value: &str, field: &str) -> Result<FixedBytes<32>> {
393    FixedBytes::<32>::from_str(value)
394        .map_err(|e| Error::bad_request(format!("Invalid {field} bytes32: {e}")))
395}
396
397fn decimal_to_u256(d: Decimal, field: &str) -> Result<U256> {
398    let normalized = d.normalize();
399    if normalized.scale() != 0 {
400        return Err(Error::bad_request(format!("{field} must be an integer")));
401    }
402    let mantissa = normalized.mantissa();
403    if mantissa < 0 {
404        return Err(Error::bad_request(format!("{field} must be non-negative")));
405    }
406    Ok(U256::from(mantissa as u128))
407}
408
409fn order_side_to_u8(side: PolymarketOrderSide) -> u8 {
410    match side {
411        PolymarketOrderSide::Buy => 0,
412        PolymarketOrderSide::Sell => 1,
413    }
414}
415
416#[cfg(test)]
417mod tests {
418    use alloy_primitives::{Signature, keccak256};
419    use nautilus_core::hex;
420    use rstest::rstest;
421    use rust_decimal_macros::dec;
422    use ustr::Ustr;
423
424    use super::*;
425    use crate::common::enums::SignatureType;
426
427    const TEST_PRIVATE_KEY: &str =
428        "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
429
430    fn test_signer() -> OrderSigner {
431        let pk = EvmPrivateKey::new(TEST_PRIVATE_KEY).unwrap();
432        OrderSigner::new(&pk).unwrap()
433    }
434
435    const ZERO_BYTES32: &str = "0x0000000000000000000000000000000000000000000000000000000000000000";
436
437    fn test_order() -> PolymarketOrder {
438        PolymarketOrder {
439            salt: 123456789,
440            maker: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266".to_string(),
441            signer: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266".to_string(),
442            token_id: Ustr::from(
443                "71321045679252212594626385532706912750332728571942532289631379312455583992563",
444            ),
445            maker_amount: dec!(100000000),
446            taker_amount: dec!(50000000),
447            side: PolymarketOrderSide::Buy,
448            signature_type: SignatureType::Eoa,
449            expiration: "0".to_string(),
450            timestamp: "1713398400000".to_string(),
451            metadata: ZERO_BYTES32.to_string(),
452            builder: ZERO_BYTES32.to_string(),
453            signature: String::new(),
454        }
455    }
456
457    #[rstest]
458    fn test_order_typehash_matches_contract() {
459        // ORDER_TYPEHASH from the CLOB V2 CTFExchange contract
460        let expected = keccak256(
461            "Order(uint256 salt,address maker,address signer,uint256 tokenId,uint256 makerAmount,uint256 takerAmount,uint8 side,uint8 signatureType,uint256 timestamp,bytes32 metadata,bytes32 builder)",
462        );
463        let order = test_order();
464        let eip712_order = build_eip712_order(&order).unwrap();
465        assert_eq!(eip712_order.eip712_type_hash(), expected);
466    }
467
468    #[rstest]
469    fn test_signer_address_derivation() {
470        let signer = test_signer();
471        // Hardhat account #0
472        let expected = Address::from_str("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266").unwrap();
473        assert_eq!(signer.address(), expected);
474    }
475
476    #[rstest]
477    fn test_sign_order_format() {
478        let signer = test_signer();
479        let order = test_order();
480
481        let sig = signer.sign_order(&order, false).unwrap();
482
483        assert!(sig.starts_with("0x"));
484        assert_eq!(sig.len(), 132); // 0x + r(64) + s(64) + v(2)
485    }
486
487    #[rstest]
488    fn test_sign_poly_1271_order_format() {
489        let signer = test_signer();
490        let mut order = test_order();
491        order.maker = "0x1111111111111111111111111111111111111111".to_string();
492        order.signer = order.maker.clone();
493        order.signature_type = SignatureType::Poly1271;
494
495        let sig = signer.sign_order(&order, false).unwrap();
496
497        assert!(sig.starts_with("0x"));
498        assert_eq!(sig.len(), 636);
499    }
500
501    #[rstest]
502    fn test_sign_poly_1271_order_requires_deposit_wallet_signer() {
503        let signer = test_signer();
504        let mut order = test_order();
505        order.maker = "0x1111111111111111111111111111111111111111".to_string();
506        order.signature_type = SignatureType::Poly1271;
507
508        let err = signer.sign_order(&order, false).unwrap_err();
509
510        assert!(
511            err.to_string()
512                .contains("maker and signer to both be the deposit wallet")
513        );
514    }
515
516    #[rstest]
517    fn test_sign_order_deterministic() {
518        let signer = test_signer();
519        let order = test_order();
520
521        let sig1 = signer.sign_order(&order, false).unwrap();
522        let sig2 = signer.sign_order(&order, false).unwrap();
523        assert_eq!(sig1, sig2);
524    }
525
526    #[rstest]
527    fn test_sign_order_neg_risk_differs() {
528        let signer = test_signer();
529        let order = test_order();
530
531        let sig_normal = signer.sign_order(&order, false).unwrap();
532        let sig_neg_risk = signer.sign_order(&order, true).unwrap();
533        assert_ne!(sig_normal, sig_neg_risk);
534    }
535
536    #[rstest]
537    fn test_sign_order_sell_side() {
538        let signer = test_signer();
539        let mut order = test_order();
540        let sig_buy = signer.sign_order(&order, false).unwrap();
541
542        order.side = PolymarketOrderSide::Sell;
543        let sig_sell = signer.sign_order(&order, false).unwrap();
544        assert_ne!(sig_buy, sig_sell);
545    }
546
547    #[rstest]
548    fn test_sign_order_different_amounts() {
549        let signer = test_signer();
550        let mut order = test_order();
551        let sig1 = signer.sign_order(&order, false).unwrap();
552
553        order.maker_amount = dec!(200000000);
554        let sig2 = signer.sign_order(&order, false).unwrap();
555        assert_ne!(sig1, sig2);
556    }
557
558    #[rstest]
559    fn test_build_eip712_order() {
560        let order = test_order();
561        let eip712 = build_eip712_order(&order).unwrap();
562
563        assert_eq!(eip712.salt, U256::from(123456789u64));
564        assert_eq!(
565            eip712.maker,
566            Address::from_str("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266").unwrap()
567        );
568        assert_eq!(eip712.makerAmount, U256::from(100000000u128));
569        assert_eq!(eip712.takerAmount, U256::from(50000000u128));
570        assert_eq!(eip712.side, 0); // BUY
571        assert_eq!(eip712.signatureType, 0); // EOA
572        assert_eq!(eip712.timestamp, U256::from(1713398400000u128));
573        assert_eq!(eip712.metadata, FixedBytes::<32>::ZERO);
574        assert_eq!(eip712.builder, FixedBytes::<32>::ZERO);
575    }
576
577    #[rstest]
578    fn test_build_eip712_order_with_builder_code() {
579        let mut order = test_order();
580        order.builder =
581            "0x0000000000000000000000000000000000000000000000000000000000000001".to_string();
582        let eip712 = build_eip712_order(&order).unwrap();
583
584        let mut expected = [0u8; 32];
585        expected[31] = 1;
586        assert_eq!(eip712.builder, FixedBytes::<32>::from(expected));
587    }
588
589    #[rstest]
590    fn test_decimal_to_u256_integer() {
591        let result = decimal_to_u256(dec!(100000000), "test").unwrap();
592        assert_eq!(result, U256::from(100000000u128));
593    }
594
595    #[rstest]
596    fn test_decimal_to_u256_zero() {
597        let result = decimal_to_u256(dec!(0), "test").unwrap();
598        assert_eq!(result, U256::ZERO);
599    }
600
601    #[rstest]
602    fn test_decimal_to_u256_rejects_fractional() {
603        let result = decimal_to_u256(dec!(100.5), "test");
604        assert!(result.is_err());
605    }
606
607    #[rstest]
608    fn test_decimal_to_u256_rejects_negative() {
609        let result = decimal_to_u256(dec!(-1), "test");
610        assert!(result.is_err());
611    }
612
613    #[rstest]
614    fn test_order_side_mapping() {
615        assert_eq!(order_side_to_u8(PolymarketOrderSide::Buy), 0);
616        assert_eq!(order_side_to_u8(PolymarketOrderSide::Sell), 1);
617    }
618
619    #[rstest]
620    fn test_contract_addresses_nonzero() {
621        assert_ne!(CTF_EXCHANGE, Address::ZERO);
622        assert_ne!(NEG_RISK_CTF_EXCHANGE, Address::ZERO);
623        assert_ne!(CTF_EXCHANGE, NEG_RISK_CTF_EXCHANGE);
624    }
625
626    #[rstest]
627    fn test_v2_contract_addresses_pinned() {
628        // Pin the V2 contract addresses so a revert to V1 is caught by unit tests.
629        // V1 addresses (must NOT match): 0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E,
630        // 0xC5d563A36AE78145C45a50134d48A1215220f80a.
631        assert_eq!(
632            format!("{CTF_EXCHANGE:#x}"),
633            "0xe111180000d2663c0091e4f400237545b87b996b"
634        );
635        assert_eq!(
636            format!("{NEG_RISK_CTF_EXCHANGE:#x}"),
637            "0xe2222d279d744050d28e00520010520000310f59"
638        );
639    }
640
641    #[rstest]
642    fn test_domain_version_is_v2() {
643        // Domain version is embedded in the EIP-712 signing hash; a revert to
644        // "1" would silently break V2 order acceptance.
645        assert_eq!(DOMAIN_VERSION, "2");
646    }
647
648    #[rstest]
649    fn test_sign_order_recoverable() {
650        let signer = test_signer();
651        let order = test_order();
652        let sig_hex = signer.sign_order(&order, false).unwrap();
653
654        let sig_bytes = hex::decode(&sig_hex[2..]).unwrap();
655        assert_eq!(sig_bytes.len(), 65);
656
657        let r = U256::from_be_slice(&sig_bytes[..32]);
658        let s = U256::from_be_slice(&sig_bytes[32..64]);
659        let v = sig_bytes[64];
660        let y_parity = v == 28;
661
662        let signature = Signature::new(r, s, y_parity);
663
664        let eip712_order = build_eip712_order(&order).unwrap();
665        let domain = eip712_domain! {
666            name: DOMAIN_NAME,
667            version: DOMAIN_VERSION,
668            chain_id: POLYGON_CHAIN_ID,
669            verifying_contract: CTF_EXCHANGE,
670        };
671        let signing_hash = eip712_order.eip712_signing_hash(&domain);
672
673        let recovered = signature
674            .recover_address_from_prehash(&signing_hash)
675            .unwrap();
676        assert_eq!(recovered, signer.address());
677    }
678
679    // Reference vectors generated with `py_clob_client_v2==1.0.0`'s
680    // `ExchangeOrderBuilderV2`. Same test private key (Hardhat account #0),
681    // same contract addresses and chain id. Locks our EIP-712 hash + ECDSA
682    // signature output to the SDK's, so drift between the two signers (domain
683    // typo, struct field reorder, bytes32 padding, etc.) is caught locally
684    // before orders get sent to the venue.
685    const PARITY_TOKEN_ID: &str =
686        "71321045679252212594626385532706912750332728571942532289631379312455583992563";
687
688    fn parity_order(
689        salt: u64,
690        side: PolymarketOrderSide,
691        signature_type: SignatureType,
692        maker_amount: Decimal,
693        taker_amount: Decimal,
694        timestamp: &str,
695        builder: &str,
696    ) -> PolymarketOrder {
697        PolymarketOrder {
698            salt,
699            maker: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".to_string(),
700            signer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".to_string(),
701            token_id: Ustr::from(PARITY_TOKEN_ID),
702            maker_amount,
703            taker_amount,
704            side,
705            signature_type,
706            expiration: "0".to_string(),
707            timestamp: timestamp.to_string(),
708            metadata: ZERO_BYTES32.to_string(),
709            builder: builder.to_string(),
710            signature: String::new(),
711        }
712    }
713
714    #[rstest]
715    #[case::buy_standard_eoa(
716        parity_order(
717            123456789,
718            PolymarketOrderSide::Buy,
719            SignatureType::Eoa,
720            dec!(100000000),
721            dec!(50000000),
722            "1713398400000",
723            ZERO_BYTES32,
724        ),
725        false,
726        "0x32961c48ddac87ed3582f8e02097cd0eff4fcf80460306bd44b3710438dfa64c",
727        "0x89f178136333c8ebb32a19146cb891233e3202d474be6ef730c24dbc06ae4d2a0c99948a86d9b57de0f2c0bb8ec6964aa244ecf17cfa3a95b86878d0b64ad78a1b",
728    )]
729    #[case::sell_neg_risk_eoa(
730        parity_order(
731            987654321,
732            PolymarketOrderSide::Sell,
733            SignatureType::Eoa,
734            dec!(50000000),
735            dec!(100000000),
736            "1713398400000",
737            ZERO_BYTES32,
738        ),
739        true,
740        "0x8b878404bd92dea2bfea9975c9fcd816ec70a57ae431cb20d67bb773744aaef3",
741        "0xf7d60d64364e2615b08d9f69f3ea9afd3b4f83ecfbf05ddd3ca83f4916277fb97d2d080758d17c8e91c928aceb8ff252477ebaec051b672832500f63b4d36b061c",
742    )]
743    #[case::buy_with_builder_code_eoa(
744        parity_order(
745            1,
746            PolymarketOrderSide::Buy,
747            SignatureType::Eoa,
748            dec!(100000000),
749            dec!(50000000),
750            "1713398500000",
751            "0x0000000000000000000000000000000000000000000000000000000000000001",
752        ),
753        false,
754        "0x3df0b6f6ddfca837bc36964cae968b34ad35640b5d98f557c104da97e804e36a",
755        "0xf4d2b34659e8bc07a9572d40ee5a1639a1157409613b4c21566b1f33fd8fe11a364b3f306668cae7248ca7cdf72378f9266bc5628585aa939644400030671e081c",
756    )]
757    #[case::buy_poly_proxy(
758        // V2 unblocks EIP-1271 smart contract wallet signing. signatureType
759        // enters the typed-data hash directly, so a regression that only
760        // manifests for proxy/safe wallets is undetectable from the Eoa
761        // cases above.
762        parity_order(
763            111_111_111,
764            PolymarketOrderSide::Buy,
765            SignatureType::PolyProxy,
766            dec!(100000000),
767            dec!(50000000),
768            "1713398400000",
769            ZERO_BYTES32,
770        ),
771        false,
772        "0x8f88fe2fb3448f4b8ba639992029f0a47a01a14d15b5f2bf9833516571efd279",
773        "0x71a63c85b730cc934688f23ea6374afffef57a61690eba63dcb97a706c8a8d0f3d2a8e0280f3e252eb83be088ebae6b461a8eda18e559e079f59050b90057afa1c",
774    )]
775    #[case::sell_neg_risk_poly_gnosis_safe(
776        parity_order(
777            222_222_222,
778            PolymarketOrderSide::Sell,
779            SignatureType::PolyGnosisSafe,
780            dec!(50000000),
781            dec!(100000000),
782            "1713398400000",
783            ZERO_BYTES32,
784        ),
785        true,
786        "0xb34248702810a1d76580234a33f942a9801c3680de54cb3ef104572a8d482190",
787        "0xab9d33aee8b578fe5588c4a4b16bbef6fa05fc757020f95b98212e877a919e360a90296f02e97ecbabf3c200271ffe88eb9fd86912e8376cd27237bdad5f3abc1c",
788    )]
789    fn test_signature_matches_py_clob_client_v2(
790        #[case] order: PolymarketOrder,
791        #[case] neg_risk: bool,
792        #[case] expected_hash_hex: &str,
793        #[case] expected_signature_hex: &str,
794    ) {
795        let signer = test_signer();
796
797        let hash = order_hash(&order, neg_risk).unwrap();
798        assert_eq!(format!("{hash:#x}"), expected_hash_hex, "signing hash");
799
800        let signature = signer.sign_order(&order, neg_risk).unwrap();
801        assert_eq!(signature, expected_signature_hex, "signature");
802    }
803
804    #[rstest]
805    #[case::standard_exchange(
806        false,
807        "0x48cfd4c03dcee72230750e2dc5ea71048e91244c2a9fec2ed6ef790a74869596",
808        concat!(
809            "0x780beffb568c4510b8a135a92ca8071f124cf368fb0e902fe451c5f51e555791",
810            "0a2a87968caa08a242de85e83c27375fc09242e897aa77680f56622a54e6e7c",
811            "61c3264e159346253e26a64e00b69032db0e7d32f94628de3e6eecb50304d",
812            "7af3d2d3db4f9eed41f0490532a9460395d96602c6534a931bde4f0e3aad5",
813            "71e4f84a04f726465722875696e743235362073616c742c6164647265737320",
814            "6d616b65722c61646472657373207369676e65722c75696e7432353620746f",
815            "6b656e49642c75696e74323536206d616b6572416d6f756e742c75696e7432",
816            "35362074616b6572416d6f756e742c75696e743820736964652c75696e7438",
817            "207369676e6174757265547970652c75696e743235362074696d657374616d",
818            "702c62797465733332206d657461646174612c62797465733332206275696c",
819            "6465722900ba",
820        ),
821    )]
822    #[case::neg_risk_exchange(
823        true,
824        "0x82b94b9d570fdd7b07f517ea848606a9b74029f2387fbf07d8b9c856d652e608",
825        concat!(
826            "0xa411986b67c58113f8787ee54c41def9376d57ef4262b56438438710720604b7",
827            "00137e0175d7ae6afc40c2988179ad84b1b71a739f0974c8df3c12372f31b22c",
828            "1c9b858f53327b0bd13af8ec14cfb35234fb9eb7b0504d1a4e61f433840d3",
829            "0e81ad3db4f9eed41f0490532a9460395d96602c6534a931bde4f0e3aad571",
830            "e4f84a04f726465722875696e743235362073616c742c61646472657373206d",
831            "616b65722c61646472657373207369676e65722c75696e7432353620746f6b",
832            "656e49642c75696e74323536206d616b6572416d6f756e742c75696e743235",
833            "362074616b6572416d6f756e742c75696e743820736964652c75696e743820",
834            "7369676e6174757265547970652c75696e743235362074696d657374616d70",
835            "2c62797465733332206d657461646174612c62797465733332206275696c64",
836            "65722900ba",
837        ),
838    )]
839    fn test_poly_1271_signature_matches_py_clob_client_v2(
840        #[case] neg_risk: bool,
841        #[case] expected_hash: &str,
842        #[case] expected_signature: &str,
843    ) {
844        let signer = test_signer();
845        let mut order = parity_order(
846            333_333_333,
847            PolymarketOrderSide::Buy,
848            SignatureType::Poly1271,
849            dec!(100000000),
850            dec!(50000000),
851            "1713398400000",
852            ZERO_BYTES32,
853        );
854        order.maker = "0x1111111111111111111111111111111111111111".to_string();
855        order.signer = order.maker.clone();
856
857        let hash = order_hash(&order, neg_risk).unwrap();
858        assert_eq!(format!("{hash:#x}"), expected_hash, "signing hash");
859
860        let signature = signer.sign_order(&order, neg_risk).unwrap();
861        assert_eq!(signature, expected_signature, "signature");
862    }
863}