use alloy_primitives::{Address, B256, 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;
#[doc(hidden)]
pub 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);
#[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: d.kind.as_str().to_owned(),
partiallyFillable: d.partially_fillable,
sellTokenBalance: d.sell_token_balance.as_str().to_owned(),
buyTokenBalance: d.buy_token_balance.as_str().to_owned(),
}
}
}
fn order_type_entries() -> Vec<serde_json::Value> {
use alloy_sol_types::SolStruct;
let root = <eip712::Order as SolStruct>::eip712_root_type();
let fields = root
.strip_prefix("Order(")
.and_then(|s| s.strip_suffix(')'))
.expect("canonical Order root type is `Order(...)`");
fields
.split(',')
.map(|field| {
let (sol_type, name) = field
.split_once(' ')
.expect("each EIP-712 field is `<type> <name>`");
serde_json::json!({ "name": name, "type": sol_type })
})
.collect()
}
pub fn order_typed_data(
order: &OrderData,
chain_id: u64,
verifying_contract: Address,
) -> serde_json::Value {
let mut message = serde_json::to_value(order).expect("OrderData serialises to JSON");
if message
.get("receiver")
.is_none_or(serde_json::Value::is_null)
{
message["receiver"] = serde_json::Value::String(Address::ZERO.to_string());
}
serde_json::json!({
"domain": {
"name": crate::domain::DOMAIN_NAME,
"version": crate::domain::DOMAIN_VERSION,
"chainId": chain_id,
"verifyingContract": verifying_contract.to_string(),
},
"primaryType": "Order",
"types": { "Order": order_type_entries() },
"message": message,
})
}
#[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 const fn as_str(self) -> &'static str {
match self {
Self::Erc20 => "erc20",
Self::External => "external",
Self::Internal => "internal",
}
}
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 = SellTokenSource::ERC20;
pub const INTERNAL: B256 = SellTokenSource::INTERNAL;
pub const fn as_str(self) -> &'static str {
match self {
Self::Erc20 => "erc20",
Self::Internal => "internal",
}
}
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 use cowprotocol_primitives::order_id::{
OrderClass, OrderUid, OrderUidParseError, OrderUidParts, parse_order_uid,
};
#[cfg(test)]
mod tests {
use alloy_primitives::{address, keccak256};
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",
);
assert_eq!(
crate::contracts::GPV2_ORDER_TYPE_HASH,
<eip712::Order as SolStruct>::eip712_type_hash(&sol_order),
);
}
#[test]
fn cross_chain_domain_separators_match_ethers() {
let cases: [(u64, [u8; 32]); 10] = [
(
1,
hex!("c078f884a2676e1345748b1feace7b0abee5d00ecadb6e574dcdd109a63e8943"),
),
(
56,
hex!("0cbb18dfca28d2ceac8c72a17289168e03c1ad121338f5573e3b0c3255207fc7"),
),
(
100,
hex!("8f05589c4b810bc2f706854508d66d447cd971f8354a4bb0b3471ceb0a466bc7"),
),
(
137,
hex!("132e0e39721b0cb53216fc42764f69c300d4d21e0caf24e0713b1e3e11120dc2"),
),
(
8453,
hex!("d72ffa789b6fae41254d0b5a13e6e1e92ed947ec6a251edf1cf0b6c02c257b4b"),
),
(
9745,
hex!("e1f9c97768e45812440cd3317c07069178cc2f69971fb204c0211d8bfb1f8e76"),
),
(
42161,
hex!("69d78e7a7cafcaf924483f99f65e8f4e303a99a446db7ab319f9d40e940bced2"),
),
(
43114,
hex!("81fd4ff99b8f80b96c946c146cd5b79181aaf08ecb5808eeee1d047c1de267a5"),
),
(
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]); 10] = [
(
1,
hex!("8295b35c74972663a29a02be0fa8de8a157215b36938caa461fdf183e02cd82e"),
),
(
56,
hex!("f676f63e14dc6a9da6bbe9e57398f060a2cc24d79e12345709ac15c8e4f5b8c1"),
),
(
100,
hex!("3dee66b2accacd71dd607b281d1485ef960c37beff85374f4b7c65eb05ed1252"),
),
(
137,
hex!("f2a78d43cf0922ef45e56a7f36d7eba13a8eb9407c1dc59e087322388e622fbb"),
),
(
8453,
hex!("28862a0c28aab4b8a4403fdf5cfd71686e7dd665db1469ec7f84bf45d1a3dd9b"),
),
(
9745,
hex!("f7941fdf92e8d5b4815995973c1cdf0e58cbe3f404ba4a8f672da5a951832e4c"),
),
(
42161,
hex!("c5677211ea383a13f4d47c092fc48fb3a0a5ade451c82f19dff69a400080f34b"),
),
(
43114,
hex!("310880582f800792d606e89b94c8f23529469003cf71da6aa172737702b8a4be"),
),
(
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 order_typed_data_table_matches_canonical_gpv2_fields() {
let expected: [(&str, &str); 12] = [
("sellToken", "address"),
("buyToken", "address"),
("receiver", "address"),
("sellAmount", "uint256"),
("buyAmount", "uint256"),
("validTo", "uint32"),
("appData", "bytes32"),
("feeAmount", "uint256"),
("kind", "string"),
("partiallyFillable", "bool"),
("sellTokenBalance", "string"),
("buyTokenBalance", "string"),
];
let typed = order_typed_data(&sample_order(), 1, SETTLEMENT);
let table = typed["types"]["Order"].as_array().unwrap();
assert_eq!(table.len(), expected.len());
for (entry, (name, ty)) in table.iter().zip(expected) {
assert_eq!(entry["name"], name, "field name");
assert_eq!(entry["type"], ty, "field solidity type");
}
}
#[test]
fn order_typed_data_envelope_shape() {
let mut order = sample_order();
order.receiver = None;
let typed = order_typed_data(&order, 1, SETTLEMENT);
assert_eq!(typed["domain"]["name"], crate::domain::DOMAIN_NAME);
assert_eq!(typed["domain"]["version"], crate::domain::DOMAIN_VERSION);
assert_eq!(typed["domain"]["chainId"], 1);
assert_eq!(typed["domain"]["verifyingContract"], SETTLEMENT.to_string());
assert_eq!(typed["primaryType"], "Order");
assert!(typed["types"].get("EIP712Domain").is_none());
assert_eq!(typed["message"]["receiver"], Address::ZERO.to_string());
}
#[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);
}
}