#[cfg(test)]
use alloy_primitives::keccak256;
use alloy_primitives::{Address, B256, FixedBytes, U256, b256};
use serde::{Deserialize, Serialize};
use serde_with::{DisplayFromStr, serde_as};
use std::fmt::{self, Display};
use crate::app_data::AppDataHash;
use crate::domain::DomainSeparator;
pub(crate) mod eip712 {
use alloy_sol_types::sol;
sol! {
#[derive(Debug)]
struct Order {
address sellToken;
address buyToken;
address receiver;
uint256 sellAmount;
uint256 buyAmount;
uint32 validTo;
bytes32 appData;
uint256 feeAmount;
string kind;
bool partiallyFillable;
string sellTokenBalance;
string buyTokenBalance;
}
}
}
pub const BUY_ETH_ADDRESS: Address = Address::repeat_byte(0xee);
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum OrderStatus {
PresignaturePending,
#[default]
Open,
Fulfilled,
Cancelled,
Expired,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum OrderClass {
#[default]
Market,
Liquidity,
Limit,
}
#[serde_as]
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderData {
pub sell_token: Address,
pub buy_token: Address,
#[serde(default)]
pub receiver: Option<Address>,
#[serde_as(as = "DisplayFromStr")]
pub sell_amount: U256,
#[serde_as(as = "DisplayFromStr")]
pub buy_amount: U256,
pub valid_to: u32,
pub app_data: AppDataHash,
#[serde_as(as = "DisplayFromStr")]
pub fee_amount: U256,
pub kind: OrderKind,
pub partially_fillable: bool,
#[serde(default)]
pub sell_token_balance: SellTokenSource,
#[serde(default)]
pub buy_token_balance: BuyTokenDestination,
}
impl OrderData {
pub fn hash_struct(&self) -> B256 {
use alloy_sol_types::SolStruct;
eip712::Order::from(self).eip712_hash_struct()
}
pub fn uid(&self, domain: &DomainSeparator, owner: Address) -> OrderUid {
use alloy_sol_types::SolStruct;
let signing_hash = eip712::Order::from(self).eip712_signing_hash(domain);
OrderUid::from_parts(signing_hash, owner, self.valid_to)
}
pub fn sign<S: alloy_signer::SignerSync>(
&self,
scheme: crate::signing_scheme::EcdsaSigningScheme,
domain: &DomainSeparator,
signer: &S,
) -> Result<crate::signature::Signature, crate::signature::SignatureError> {
let ecdsa = self.sign_ecdsa(scheme, domain, signer)?;
Ok(crate::signature::Signature::from_ecdsa(ecdsa, scheme))
}
pub fn sign_ecdsa<S: alloy_signer::SignerSync>(
&self,
scheme: crate::signing_scheme::EcdsaSigningScheme,
domain: &DomainSeparator,
signer: &S,
) -> Result<crate::signature::EcdsaSignature, crate::signature::SignatureError> {
let payload = eip712::Order::from(self);
crate::signature::sign_ecdsa(scheme, domain, &payload, signer)
}
}
impl From<&OrderData> for eip712::Order {
fn from(d: &OrderData) -> Self {
Self {
sellToken: d.sell_token,
buyToken: d.buy_token,
receiver: d.receiver.unwrap_or(Address::ZERO),
sellAmount: d.sell_amount,
buyAmount: d.buy_amount,
validTo: d.valid_to,
appData: d.app_data,
feeAmount: d.fee_amount,
kind: match d.kind {
OrderKind::Sell => "sell".to_owned(),
OrderKind::Buy => "buy".to_owned(),
},
partiallyFillable: d.partially_fillable,
sellTokenBalance: match d.sell_token_balance {
SellTokenSource::Erc20 => "erc20".to_owned(),
SellTokenSource::External => "external".to_owned(),
SellTokenSource::Internal => "internal".to_owned(),
},
buyTokenBalance: match d.buy_token_balance {
BuyTokenDestination::Erc20 => "erc20".to_owned(),
BuyTokenDestination::Internal => "internal".to_owned(),
},
}
}
}
#[serde_as]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Order {
#[serde(flatten)]
pub data: OrderData,
pub uid: OrderUid,
pub owner: alloy_primitives::Address,
pub signing_scheme: crate::signing_scheme::SigningScheme,
pub signature: String,
pub creation_date: String,
pub status: OrderStatus,
pub class: OrderClass,
#[serde_as(as = "DisplayFromStr")]
pub executed_buy_amount: alloy_primitives::U256,
#[serde_as(as = "DisplayFromStr")]
pub executed_sell_amount: alloy_primitives::U256,
#[serde_as(as = "Option<DisplayFromStr>")]
#[serde(default)]
pub executed_fee: Option<alloy_primitives::U256>,
#[serde(default)]
pub executed_fee_token: Option<alloy_primitives::Address>,
#[serde(default)]
pub invalidated: bool,
#[serde(default)]
pub is_liquidity_order: bool,
#[serde(default)]
pub full_app_data: Option<String>,
#[serde(default)]
pub quote: Option<serde_json::Value>,
#[serde(default)]
pub interactions: Option<serde_json::Value>,
#[serde(default)]
pub ethflow_data: Option<serde_json::Value>,
#[serde(default)]
pub onchain_order_data: Option<serde_json::Value>,
#[serde(default)]
pub onchain_user: Option<alloy_primitives::Address>,
#[serde(default)]
pub settlement_contract: Option<alloy_primitives::Address>,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum OrderKind {
#[default]
Buy,
Sell,
}
impl OrderKind {
pub const BUY: B256 = b256!("6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc");
pub const SELL: B256 =
b256!("f3b277728b3fee749481eb3e0b3b48980dbbab78658fc419025cb16eee346775");
pub const fn as_str(self) -> &'static str {
match self {
Self::Buy => "buy",
Self::Sell => "sell",
}
}
pub fn from_contract_bytes(bytes: B256) -> Option<Self> {
if bytes == Self::BUY {
Some(Self::Buy)
} else if bytes == Self::SELL {
Some(Self::Sell)
} else {
None
}
}
}
impl Display for OrderKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SellTokenSource {
#[default]
Erc20,
External,
Internal,
}
impl SellTokenSource {
pub const ERC20: B256 =
b256!("5a28e9363bb942b639270062aa6bb295f434bcdfc42c97267bf003f272060dc9");
pub const EXTERNAL: B256 =
b256!("abee3b73373acd583a130924aad6dc38cfdc44ba0555ba94ce2ff63980ea0632");
pub const INTERNAL: B256 =
b256!("4ac99ace14ee0a5ef932dc609df0943ab7ac16b7583634612f8dc35a4289a6ce");
pub fn from_contract_bytes(bytes: B256) -> Option<Self> {
if bytes == Self::ERC20 {
Some(Self::Erc20)
} else if bytes == Self::EXTERNAL {
Some(Self::External)
} else if bytes == Self::INTERNAL {
Some(Self::Internal)
} else {
None
}
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BuyTokenDestination {
#[default]
Erc20,
Internal,
}
impl BuyTokenDestination {
pub const ERC20: B256 =
b256!("5a28e9363bb942b639270062aa6bb295f434bcdfc42c97267bf003f272060dc9");
pub const INTERNAL: B256 =
b256!("4ac99ace14ee0a5ef932dc609df0943ab7ac16b7583634612f8dc35a4289a6ce");
pub fn from_contract_bytes(bytes: B256) -> Option<Self> {
if bytes == Self::ERC20 {
Some(Self::Erc20)
} else if bytes == Self::INTERNAL {
Some(Self::Internal)
} else {
None
}
}
}
pub type OrderUid = FixedBytes<56>;
pub trait OrderUidParts {
fn from_parts(hash: B256, owner: Address, valid_to: u32) -> Self;
fn to_parts(&self) -> (B256, Address, u32);
}
#[derive(Debug, thiserror::Error)]
pub enum OrderUidParseError {
#[error("order UID must be 0x-prefixed")]
MissingPrefix,
#[error("invalid order UID hex: {0}")]
Hex(#[from] alloy_primitives::hex::FromHexError),
}
pub fn parse_order_uid(s: &str) -> Result<OrderUid, OrderUidParseError> {
if !s.starts_with("0x") {
return Err(OrderUidParseError::MissingPrefix);
}
Ok(s.parse::<OrderUid>()?)
}
impl OrderUidParts for OrderUid {
fn from_parts(hash: B256, owner: Address, valid_to: u32) -> Self {
let mut uid = [0u8; 56];
uid[0..32].copy_from_slice(hash.as_slice());
uid[32..52].copy_from_slice(owner.as_slice());
uid[52..56].copy_from_slice(&valid_to.to_be_bytes());
Self::new(uid)
}
fn to_parts(&self) -> (B256, Address, u32) {
let bytes = self.as_slice();
let mut valid_to = [0u8; 4];
valid_to.copy_from_slice(&bytes[52..56]);
(
B256::from_slice(&bytes[0..32]),
Address::from_slice(&bytes[32..52]),
u32::from_be_bytes(valid_to),
)
}
}
#[cfg(test)]
mod tests {
use alloy_primitives::address;
use hex_literal::hex;
use std::str::FromStr;
use super::*;
use crate::contracts::GPV2_SETTLEMENT as SETTLEMENT;
fn sample_order() -> OrderData {
OrderData {
sell_token: Address::from(hex!("0101010101010101010101010101010101010101")),
buy_token: Address::from(hex!("0202020202020202020202020202020202020202")),
receiver: Some(Address::from(hex!(
"0303030303030303030303030303030303030303"
))),
sell_amount: U256::from(0x0246ddf97976680000_u128),
buy_amount: U256::from(0xb98bc829a6f90000_u128),
valid_to: 0xffffffff,
app_data: AppDataHash::default(),
fee_amount: U256::from(0x0de0b6b3a7640000_u128),
kind: OrderKind::Sell,
partially_fillable: false,
sell_token_balance: SellTokenSource::Erc20,
buy_token_balance: BuyTokenDestination::Erc20,
}
}
fn sample_owner() -> Address {
address!("70997970C51812dc3A010C7d01b50e0d17dc79C8")
}
#[test]
fn sample_order_struct_hash_matches_ethers() {
assert_eq!(
sample_order().hash_struct(),
b256!("7d9bf070168f9950003bdad00194ef63a5389dd0b594a1288407d551abf147d5")
);
}
#[test]
fn eip712_signature_matches_ethers_golden() {
use crate::signature::sign_ecdsa;
use crate::signing_scheme::EcdsaSigningScheme;
use alloy_signer_local::PrivateKeySigner;
let private_key = B256::from(hex!(
"ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
));
let signer = PrivateKeySigner::from_bytes(&private_key).unwrap();
let domain = crate::domain::settlement_domain(1, SETTLEMENT);
let payload = eip712::Order::from(&sample_order());
let ecdsa = sign_ecdsa(EcdsaSigningScheme::Eip712, &domain, &payload, &signer).unwrap();
let expected_r = hex!("78bd3f7f240eb91bf94264f1bab99a5efaf97e8c76b9f76eeb4520f46861ed13");
let expected_s = hex!("70c2f3362f17d4668a02ad82f61bff52bd33a785afeff727ddab43210dfebea2");
let bytes = ecdsa.as_bytes();
assert_eq!(&bytes[..32], &expected_r, "r component");
assert_eq!(&bytes[32..64], &expected_s, "s component");
assert_eq!(bytes[64], 28, "v component");
}
#[test]
fn eip712_order_type_hash_matches_canonical() {
use alloy_sol_types::SolStruct;
let sol_order = eip712::Order::from(&sample_order());
assert_eq!(
<eip712::Order as SolStruct>::eip712_type_hash(&sol_order).0,
hex!("d5a25ba2e97094ad7d83dc28a6572da797d6b3e7fc6663bd93efb789fc17e489"),
"Order(...) typeHash must match the GPv2Settlement-verified value",
);
}
#[test]
fn cross_chain_domain_separators_match_ethers() {
let cases: [(u64, [u8; 32]); 11] = [
(
1,
hex!("c078f884a2676e1345748b1feace7b0abee5d00ecadb6e574dcdd109a63e8943"),
),
(
56,
hex!("0cbb18dfca28d2ceac8c72a17289168e03c1ad121338f5573e3b0c3255207fc7"),
),
(
100,
hex!("8f05589c4b810bc2f706854508d66d447cd971f8354a4bb0b3471ceb0a466bc7"),
),
(
137,
hex!("132e0e39721b0cb53216fc42764f69c300d4d21e0caf24e0713b1e3e11120dc2"),
),
(
8453,
hex!("d72ffa789b6fae41254d0b5a13e6e1e92ed947ec6a251edf1cf0b6c02c257b4b"),
),
(
9745,
hex!("e1f9c97768e45812440cd3317c07069178cc2f69971fb204c0211d8bfb1f8e76"),
),
(
42161,
hex!("69d78e7a7cafcaf924483f99f65e8f4e303a99a446db7ab319f9d40e940bced2"),
),
(
43114,
hex!("81fd4ff99b8f80b96c946c146cd5b79181aaf08ecb5808eeee1d047c1de267a5"),
),
(
57073,
hex!("5aced6090755c424bc1d6bbd39a2cdf57e6abfb4663598f4c3c821fb942d52e0"),
),
(
59144,
hex!("b219bb2b8733b80b7ebef0229e7f0c91436f9a0a5b9705fa519237ae0493addb"),
),
(
11_155_111,
hex!("daee378bd0eb30ddf479272accf91761e697bc00e067a268f95f1d2732ed230b"),
),
];
for (chain_id, expected) in cases {
let separator = crate::domain::settlement_domain(chain_id, SETTLEMENT).separator();
assert_eq!(separator, expected, "domain separator for chain {chain_id}");
}
}
#[test]
fn cross_chain_uids_match_ethers() {
const TAIL: [u8; 24] = hex!("70997970c51812dc3a010c7d01b50e0d17dc79c8ffffffff");
let cases: [(u64, [u8; 32]); 11] = [
(
1,
hex!("8295b35c74972663a29a02be0fa8de8a157215b36938caa461fdf183e02cd82e"),
),
(
56,
hex!("f676f63e14dc6a9da6bbe9e57398f060a2cc24d79e12345709ac15c8e4f5b8c1"),
),
(
100,
hex!("3dee66b2accacd71dd607b281d1485ef960c37beff85374f4b7c65eb05ed1252"),
),
(
137,
hex!("f2a78d43cf0922ef45e56a7f36d7eba13a8eb9407c1dc59e087322388e622fbb"),
),
(
8453,
hex!("28862a0c28aab4b8a4403fdf5cfd71686e7dd665db1469ec7f84bf45d1a3dd9b"),
),
(
9745,
hex!("f7941fdf92e8d5b4815995973c1cdf0e58cbe3f404ba4a8f672da5a951832e4c"),
),
(
42161,
hex!("c5677211ea383a13f4d47c092fc48fb3a0a5ade451c82f19dff69a400080f34b"),
),
(
43114,
hex!("310880582f800792d606e89b94c8f23529469003cf71da6aa172737702b8a4be"),
),
(
57073,
hex!("7daedf408aec4bacb29278b0febf3c40ce52c7911d0a23eb5c4c116a8dd44852"),
),
(
59144,
hex!("5aa640f484ef090ab25171d6cbc6adff0cbda7a342ed7ef6371636a1575eca40"),
),
(
11_155_111,
hex!("d69c063b99b74a6690df5541787acc942828219a0ba12fded27eff853da8f6fd"),
),
];
let order = sample_order();
let owner = sample_owner();
for (chain_id, expected_digest) in cases {
let domain = crate::domain::settlement_domain(chain_id, SETTLEMENT);
let uid = order.uid(&domain, owner);
let mut expected = [0u8; 56];
expected[0..32].copy_from_slice(&expected_digest);
expected[32..56].copy_from_slice(&TAIL);
assert_eq!(uid.0, expected, "uid for chain {chain_id}");
}
}
#[test]
fn hash_struct_byte_permutations_match_ethers() {
let mut buy = sample_order();
buy.kind = OrderKind::Buy;
assert_eq!(
buy.hash_struct(),
b256!("7f6ff8bfee1c5f54ca8ac13dabf84e6646592775700fce0e5ead7049620f9ea5")
);
let mut partial = sample_order();
partial.partially_fillable = true;
assert_eq!(
partial.hash_struct(),
b256!("4a7892b4e3cc787cc8dbb22afb249a52b144ae7aec066d2f41f521aa05c7388c")
);
let mut external = sample_order();
external.sell_token_balance = SellTokenSource::External;
assert_eq!(
external.hash_struct(),
b256!("250972eafa5a01e4103f50f3987422339582583b36d2a47e3c6920b4acca3509")
);
let mut internal_sell = sample_order();
internal_sell.sell_token_balance = SellTokenSource::Internal;
assert_eq!(
internal_sell.hash_struct(),
b256!("c94d0a2b1c1b41042d41e0d9f2d05bc91fbe1cb053b716176850029cdb88f679")
);
let mut internal_buy = sample_order();
internal_buy.buy_token_balance = BuyTokenDestination::Internal;
assert_eq!(
internal_buy.hash_struct(),
b256!("4d19213af5ed0adb5ec3d67b00cdcd360ea0f9378a9392f599de132106a558d9")
);
let mut no_receiver = sample_order();
no_receiver.receiver = None;
let mut zero_receiver = sample_order();
zero_receiver.receiver = Some(Address::ZERO);
assert_eq!(no_receiver.hash_struct(), zero_receiver.hash_struct());
assert_eq!(
no_receiver.hash_struct(),
b256!("5388e8a0f9cf9129fd0fd54d3192e502cf5519ee4316f0c77860bfc0c3f42994")
);
}
#[test]
fn order_type_hash_matches_canonical_signature() {
use alloy_sol_types::SolStruct;
let signature = b"Order(\
address sellToken,\
address buyToken,\
address receiver,\
uint256 sellAmount,\
uint256 buyAmount,\
uint32 validTo,\
bytes32 appData,\
uint256 feeAmount,\
string kind,\
bool partiallyFillable,\
string sellTokenBalance,\
string buyTokenBalance\
)";
let sol_order = eip712::Order::from(&sample_order());
assert_eq!(
<eip712::Order as SolStruct>::eip712_type_hash(&sol_order),
keccak256(signature),
);
}
#[test]
fn buy_eth_address_matches_canonical_sentinel() {
assert_eq!(
BUY_ETH_ADDRESS,
address!("EeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE")
);
}
#[test]
fn order_kind_keccak_constants() {
assert_eq!(OrderKind::BUY, keccak256(b"buy"));
assert_eq!(OrderKind::SELL, keccak256(b"sell"));
}
#[test]
fn sell_token_source_keccak_constants() {
assert_eq!(SellTokenSource::ERC20, keccak256(b"erc20"));
assert_eq!(SellTokenSource::EXTERNAL, keccak256(b"external"));
assert_eq!(SellTokenSource::INTERNAL, keccak256(b"internal"));
}
#[test]
fn buy_token_destination_keccak_constants() {
assert_eq!(BuyTokenDestination::ERC20, keccak256(b"erc20"));
assert_eq!(BuyTokenDestination::INTERNAL, keccak256(b"internal"));
}
#[test]
fn order_uid_round_trips_via_string() {
let original = OrderUid::from_str(
"0x5668997bd3fb981d1b3ec44e8483e7c369756df47d10241c1c7a26fde4d1090e89984d17af2f18f8c54873c0de68a56cc5a23e0f695ba915",
)
.unwrap();
let (hash, owner, valid_to) = original.to_parts();
assert_eq!(
hash,
B256::from(hex!(
"5668997bd3fb981d1b3ec44e8483e7c369756df47d10241c1c7a26fde4d1090e"
))
);
assert_eq!(
owner,
address!("0x89984d17af2f18f8c54873c0de68a56cc5a23e0f")
);
assert_eq!(valid_to, 0x695ba915);
let rebuilt = OrderUid::from_parts(hash, owner, valid_to);
assert_eq!(rebuilt, original);
}
#[test]
fn parse_order_uid_requires_0x_prefix() {
let body = "11".repeat(56);
let prefixed = format!("0x{body}");
assert!(parse_order_uid(&prefixed).is_ok());
assert!(matches!(
parse_order_uid(&body),
Err(OrderUidParseError::MissingPrefix)
));
assert!(OrderUid::from_str(&body).is_ok());
assert!(matches!(
parse_order_uid("0xnothex"),
Err(OrderUidParseError::Hex(_))
));
}
#[test]
fn order_uid_displays_as_prefixed_hex() {
let mut uid = OrderUid::default();
uid.0[0] = 0x01;
uid.0[55] = 0xff;
let expected = "0x01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff";
assert_eq!(uid.to_string(), expected);
}
#[test]
fn order_uid_pack_matches_contracts_solidity_reference() {
let digest = keccak256(b"order digest");
let owner_seed = keccak256(b"owner");
let owner = Address::from_slice(&owner_seed[12..32]);
let valid_to_seed = keccak256(b"valid to");
let valid_to = u32::from_be_bytes(valid_to_seed[28..32].try_into().unwrap());
let uid = OrderUid::from_parts(digest, owner, valid_to);
let mut expected = [0u8; 56];
expected[0..32].copy_from_slice(digest.as_slice());
expected[32..52].copy_from_slice(owner.as_slice());
expected[52..56].copy_from_slice(&valid_to.to_be_bytes());
assert_eq!(uid.0, expected);
let (round_digest, round_owner, round_valid_to) = uid.to_parts();
assert_eq!(round_digest, digest);
assert_eq!(round_owner, owner);
assert_eq!(round_valid_to, valid_to);
}
}