fix: sign poly1271 orders as deposit wallet

This commit is contained in:
floor-licker
2026-06-30 19:54:04 -04:00
parent d0c7ce7636
commit 8499b361c4
2 changed files with 208 additions and 16 deletions
+121 -9
View File
@@ -8,7 +8,7 @@ use crate::types::ApiCredentials;
use alloy_primitives::{hex::encode_prefixed, Address, B256, U256};
use alloy_signer::SignerSync;
use alloy_signer_local::PrivateKeySigner;
use alloy_sol_types::{eip712_domain, sol, Eip712Domain};
use alloy_sol_types::{eip712_domain, sol, Eip712Domain, SolStruct};
use base64::engine::Engine;
use hmac::{Hmac, Mac};
use serde::Serialize;
@@ -120,8 +120,22 @@ sol! {
bytes32 metadata;
bytes32 builder;
}
struct TypedDataSign {
Order contents;
string name;
string version;
uint256 chainId;
address verifyingContract;
bytes32 salt;
}
}
const DEPOSIT_WALLET_NAME: &str = "DepositWallet";
const DEPOSIT_WALLET_VERSION: &str = "1";
const ERC7739_TYPED_DATA_SIGN_SALT: B256 = B256::ZERO;
const ORDER_TYPE_STRING: &str = "Order(uint256 salt,address maker,address signer,uint256 tokenId,uint256 makerAmount,uint256 takerAmount,uint8 side,uint8 signatureType,uint256 timestamp,bytes32 metadata,bytes32 builder)";
/// V2 order signing payload. The REST body still carries `expiration`, but the EIP-712 payload
/// follows the V2 exchange struct.
#[derive(Clone)]
@@ -212,7 +226,69 @@ pub fn sign_order_message_with_domain(
order: SignedOrderMessage,
domain: &PreparedOrderDomain,
) -> Result<String> {
let order = Order {
let order = order_sol(order);
let signature = signer
.sign_typed_data_sync(&order, &domain.domain)
.map_err(|e| PolyfillError::crypto(format!("Order signature failed: {}", e)))?;
Ok(encode_prefixed(signature.as_bytes()))
}
/// Sign a POLY_1271 deposit-wallet order using the ERC-7739 wrapper expected by
/// Polymarket's V2 deposit wallet verifier.
///
/// The order itself must have `maker == signer == deposit_wallet` and
/// `signatureType == 3`. The EOA key signs a Solady `TypedDataSign(...)`
/// envelope under the CTF exchange order domain; the wire signature appends the
/// app-domain separator, order contents hash, order type string, and a uint16
/// big-endian type-string length.
pub fn sign_poly1271_order_message_with_domain(
signer: &PrivateKeySigner,
order: SignedOrderMessage,
domain: &PreparedOrderDomain,
deposit_wallet: Address,
chain_id: u64,
) -> Result<String> {
if order.maker != deposit_wallet || order.signer != deposit_wallet {
return Err(PolyfillError::validation(
"POLY_1271 orders require maker and signer to equal the deposit wallet",
));
}
let order = order_sol(order);
let app_domain_separator = domain.domain.hash_struct();
let contents_hash = order.eip712_hash_struct();
let envelope = TypedDataSign {
contents: order,
name: DEPOSIT_WALLET_NAME.to_string(),
version: DEPOSIT_WALLET_VERSION.to_string(),
chainId: U256::from(chain_id),
verifyingContract: deposit_wallet,
salt: ERC7739_TYPED_DATA_SIGN_SALT,
};
let inner = signer
.sign_typed_data_sync(&envelope, &domain.domain)
.map_err(|e| PolyfillError::crypto(format!("POLY_1271 order signature failed: {e}")))?;
let type_bytes = ORDER_TYPE_STRING.as_bytes();
let type_len: u16 = type_bytes
.len()
.try_into()
.map_err(|_| PolyfillError::validation("POLY_1271 order type string too long"))?;
let mut wrapped = Vec::with_capacity(65 + 32 + 32 + type_bytes.len() + 2);
wrapped.extend_from_slice(&inner.as_bytes());
wrapped.extend_from_slice(app_domain_separator.as_slice());
wrapped.extend_from_slice(contents_hash.as_slice());
wrapped.extend_from_slice(type_bytes);
wrapped.extend_from_slice(&type_len.to_be_bytes());
Ok(encode_prefixed(wrapped))
}
fn order_sol(order: SignedOrderMessage) -> Order {
Order {
salt: order.salt,
maker: order.maker,
signer: order.signer,
@@ -224,13 +300,7 @@ pub fn sign_order_message_with_domain(
timestamp: order.timestamp,
metadata: order.metadata,
builder: order.builder,
};
let signature = signer
.sign_typed_data_sync(&order, &domain.domain)
.map_err(|e| PolyfillError::crypto(format!("Order signature failed: {}", e)))?;
Ok(encode_prefixed(signature.as_bytes()))
}
}
/// Build HMAC signature for L2 authentication
@@ -393,6 +463,7 @@ pub fn create_l2_headers_with_body_bytes(
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn test_unix_timestamp() {
@@ -565,6 +636,47 @@ mod tests {
assert_eq!(signature.len(), 132); // 0x + 130 hex chars = 132 total
}
#[test]
fn test_poly1271_wrapped_order_signature_golden() {
let signer: PrivateKeySigner =
"0x2222222222222222222222222222222222222222222222222222222222222222"
.parse()
.expect("valid private key");
let deposit_wallet =
Address::from_str("0x000000000000000000000000000000000000d077").unwrap();
let exchange = Address::from_str("0xE111180000d2663C0091e4f400237545B87B996B").unwrap();
let order = SignedOrderMessage {
salt: U256::from(12345),
maker: deposit_wallet,
signer: deposit_wallet,
token_id: U256::from_str_radix(
"100000000000000000000000000000000000000000000000000000000000000000000000000000",
10,
)
.unwrap(),
maker_amount: U256::from(1_000_000),
taker_amount: U256::from(5_000_000),
side: 0,
signature_type: 3,
timestamp: U256::from(1_716_000_000_000_u64),
metadata: B256::ZERO,
builder: B256::ZERO,
};
let got = sign_poly1271_order_message_with_domain(
&signer,
order,
&PreparedOrderDomain::new(137, exchange),
deposit_wallet,
137,
)
.unwrap();
const WANT: &str = "0xc1c199d3822d7f465b1f213793ebb7bbc3a77810ceefa89b58ecfeb284e708953c5ec946a41c6fa2a6fac373c78b167dd6834fae6f3e37581111ffb06200f1d21b3264e159346253e26a64e00b69032db0e7d32f94628de3e6eecb50304d7af3d26814859c22020105275eba8b46528be2edd6078dea415bec6d90f2076b0c8ac64f726465722875696e743235362073616c742c61646472657373206d616b65722c61646472657373207369676e65722c75696e7432353620746f6b656e49642c75696e74323536206d616b6572416d6f756e742c75696e743235362074616b6572416d6f756e742c75696e743820736964652c75696e7438207369676e6174757265547970652c75696e743235362074696d657374616d702c62797465733332206d657461646174612c62797465733332206275696c6465722900ba";
assert_eq!(got, WANT);
assert!(got.len() > 600 && got.len() < 700);
}
#[test]
fn test_timestamp_generation() {
let ts1 = get_current_unix_time_secs();
+87 -7
View File
@@ -4,7 +4,8 @@
//! for the Polymarket CLOB, including EIP-712 signature generation.
use crate::auth::{
sign_order_message, sign_order_message_with_domain, PreparedOrderDomain, SignedOrderMessage,
sign_order_message, sign_order_message_with_domain, sign_poly1271_order_message_with_domain,
PreparedOrderDomain, SignedOrderMessage,
};
use crate::errors::{PolyfillError, Result};
use crate::types::{
@@ -64,6 +65,7 @@ pub struct OrderBuilder {
#[derive(Clone)]
pub struct PreparedOrderPath {
builder: OrderBuilder,
chain_id: u64,
token_id: String,
token_id_u256: U256,
round_config: RoundConfig,
@@ -390,6 +392,7 @@ impl OrderBuilder {
Ok(PreparedOrderPath {
builder: self.clone(),
chain_id,
token_id,
token_id_u256,
round_config,
@@ -620,10 +623,12 @@ impl OrderBuilder {
let (builder_bytes, builder) = parse_optional_bytes32("builder_code", builder_code)?;
let (metadata_bytes, metadata) = parse_optional_bytes32("metadata", metadata)?;
let signer_address = self.order_signer_address();
let signer_checksum = self.order_signer_checksum();
let order = SignedOrderMessage {
salt: U256::from(seed),
maker: self.funder,
signer: self.signer_address,
signer: signer_address,
token_id: u256_token_id,
maker_amount,
taker_amount,
@@ -634,12 +639,21 @@ impl OrderBuilder {
builder: builder_bytes,
};
let signature = sign_order_message(&self.signer, order, chain_id, exchange)?;
let signature = match self.sig_type {
SigType::Poly1271 => sign_poly1271_order_message_with_domain(
&self.signer,
order,
&PreparedOrderDomain::new(chain_id, exchange),
self.funder,
chain_id,
)?,
_ => sign_order_message(&self.signer, order, chain_id, exchange)?,
};
Ok(SignedOrderRequest {
salt: seed,
maker: self.funder_checksum.clone(),
signer: self.signer_checksum.clone(),
signer: signer_checksum,
token_id,
maker_amount: maker_amount.to_string(),
taker_amount: taker_amount.to_string(),
@@ -654,6 +668,22 @@ impl OrderBuilder {
}
}
impl OrderBuilder {
fn order_signer_address(&self) -> Address {
match self.sig_type {
SigType::Poly1271 => self.funder,
_ => self.signer_address,
}
}
fn order_signer_checksum(&self) -> String {
match self.sig_type {
SigType::Poly1271 => self.funder_checksum.clone(),
_ => self.signer_checksum.clone(),
}
}
}
impl PreparedOrderPath {
/// Create and sign a limit order using the cached market/token context.
pub fn create_limit_order(
@@ -704,10 +734,12 @@ impl PreparedOrderPath {
.expect("Time went backwards")
.as_millis();
let signer_address = self.builder.order_signer_address();
let signer_checksum = self.builder.order_signer_checksum();
let order = SignedOrderMessage {
salt: U256::from(seed),
maker: self.builder.funder,
signer: self.builder.signer_address,
signer: signer_address,
token_id: self.token_id_u256,
maker_amount,
taker_amount,
@@ -718,12 +750,21 @@ impl PreparedOrderPath {
builder: self.builder_bytes,
};
let signature = sign_order_message_with_domain(&self.builder.signer, order, &self.domain)?;
let signature = match self.builder.sig_type {
SigType::Poly1271 => sign_poly1271_order_message_with_domain(
&self.builder.signer,
order,
&self.domain,
self.builder.funder,
self.chain_id,
)?,
_ => sign_order_message_with_domain(&self.builder.signer, order, &self.domain)?,
};
Ok(SignedOrderRequest {
salt: seed,
maker: self.builder.funder_checksum.clone(),
signer: self.builder.signer_checksum.clone(),
signer: signer_checksum,
token_id: self.token_id.clone(),
maker_amount: maker_amount.to_string(),
taker_amount: taker_amount.to_string(),
@@ -919,6 +960,45 @@ mod tests {
assert_eq!(order.metadata, BYTES32_ZERO);
}
#[test]
fn test_poly1271_order_uses_deposit_wallet_as_signer_and_wrapped_signature() {
let signer: PrivateKeySigner =
"0x2222222222222222222222222222222222222222222222222222222222222222"
.parse()
.expect("valid private key");
let funder = Address::from_str("0x000000000000000000000000000000000000d077").unwrap();
let builder = OrderBuilder::new(signer, Some(SigType::Poly1271), Some(funder));
let order = builder
.create_order(
137,
&OrderArgs {
token_id: "123456".to_string(),
price: Decimal::from_str("0.50").unwrap(),
size: Decimal::from_str("10").unwrap(),
side: Side::BUY,
expiration: None,
builder_code: Some(BYTES32_ZERO.to_string()),
metadata: Some(BYTES32_ZERO.to_string()),
},
&CreateOrderOptions {
tick_size: Some(Decimal::from_str("0.01").unwrap()),
neg_risk: Some(false),
},
)
.unwrap();
let funder_checksum = funder.to_checksum(None);
assert_eq!(order.maker, funder_checksum);
assert_eq!(order.signer, funder_checksum);
assert_eq!(order.signature_type, SigType::Poly1271 as u8);
assert!(order.signature.starts_with("0x"));
assert!(
order.signature.len() > 600,
"POLY_1271 signature should be ERC-7739 wrapped"
);
}
#[test]
fn test_prepared_order_path_creates_equivalent_limit_order_fields() {
let builder = test_builder();