mirror of
https://github.com/floor-licker/polyfill-rs.git
synced 2026-07-27 20:47:46 +00:00
Merge pull request #74 from floor-licker/fix/poly1271-deposit-wallet-signing
fix: sign POLY_1271 orders as deposit wallet
This commit is contained in:
+121
-9
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user