use alloy_primitives::{Address, B256, U256, keccak256};
use crate::{
config::contracts::SETTLEMENT_CONTRACT,
error::CowError,
order_signing::types::{OrderDomain, OrderTypedData, UnsignedOrder},
types::OrderKind,
};
pub const BUY_ETH_ADDRESS: Address = Address::new([
0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee,
0xee, 0xee, 0xee, 0xee,
]);
pub const ORDER_PRIMARY_TYPE: &str = "Order";
pub const ORDER_TYPE_HASH: &str =
"0xd5a25ba2e97094ad7d83dc28a6572da797d6b3e7fc6663bd93efb789fc17e489";
pub const ORDER_UID_LENGTH: usize = 56;
pub const EIP1271_MAGICVALUE: &str = "0x1626ba7e";
pub const PRE_SIGNED: &str = "0xf59c009283ff87aa78203fc4d9c2df025ee851130fb69cc3e068941f6b5e2d6f";
fn domain_type_hash() -> B256 {
keccak256(b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
}
fn order_type_hash() -> B256 {
keccak256(
b"GPv2Order.Data(\
address sellToken,\
address buyToken,\
address receiver,\
uint256 sellAmount,\
uint256 buyAmount,\
uint32 validTo,\
bytes32 appData,\
uint256 feeAmount,\
string kind,\
bool partiallyFillable,\
string sellTokenBalance,\
string buyTokenBalance\
)",
)
}
fn abi_address(a: Address) -> [u8; 32] {
let mut buf = [0u8; 32];
buf[12..].copy_from_slice(a.as_slice());
buf
}
#[must_use]
const fn abi_u256(v: U256) -> [u8; 32] {
v.to_be_bytes()
}
fn abi_u32(v: u32) -> [u8; 32] {
let mut buf = [0u8; 32];
buf[28..].copy_from_slice(&v.to_be_bytes());
buf
}
fn abi_bool(v: bool) -> [u8; 32] {
abi_u32(u32::from(v))
}
#[must_use]
pub fn domain_separator(chain_id: u64) -> B256 {
let name_hash = keccak256(b"Gnosis Protocol v2");
let version_hash = keccak256(b"v2");
let mut buf = [0u8; 5 * 32];
buf[0..32].copy_from_slice(domain_type_hash().as_slice());
buf[32..64].copy_from_slice(name_hash.as_slice());
buf[64..96].copy_from_slice(version_hash.as_slice());
buf[96..128].copy_from_slice(&abi_u256(U256::from(chain_id)));
buf[128..160].copy_from_slice(&abi_address(SETTLEMENT_CONTRACT));
keccak256(buf)
}
#[must_use]
pub fn domain_separator_from(domain: &OrderDomain) -> B256 {
let name_hash = keccak256(domain.name.as_bytes());
let version_hash = keccak256(domain.version.as_bytes());
let mut buf = [0u8; 5 * 32];
buf[0..32].copy_from_slice(domain_type_hash().as_slice());
buf[32..64].copy_from_slice(name_hash.as_slice());
buf[64..96].copy_from_slice(version_hash.as_slice());
buf[96..128].copy_from_slice(&abi_u256(U256::from(domain.chain_id)));
buf[128..160].copy_from_slice(&abi_address(domain.verifying_contract));
keccak256(buf)
}
#[must_use]
pub fn order_hash(order: &UnsignedOrder) -> B256 {
let kind_hash = keccak256(match order.kind {
OrderKind::Sell => b"sell" as &[u8],
OrderKind::Buy => b"buy" as &[u8],
});
let mut buf = [0u8; 13 * 32];
buf[0..32].copy_from_slice(order_type_hash().as_slice());
buf[32..64].copy_from_slice(&abi_address(order.sell_token));
buf[64..96].copy_from_slice(&abi_address(order.buy_token));
buf[96..128].copy_from_slice(&abi_address(order.receiver));
buf[128..160].copy_from_slice(&abi_u256(order.sell_amount));
buf[160..192].copy_from_slice(&abi_u256(order.buy_amount));
buf[192..224].copy_from_slice(&abi_u32(order.valid_to));
buf[224..256].copy_from_slice(order.app_data.as_slice());
buf[256..288].copy_from_slice(&abi_u256(order.fee_amount));
buf[288..320].copy_from_slice(kind_hash.as_slice());
buf[320..352].copy_from_slice(&abi_bool(order.partially_fillable));
buf[352..384].copy_from_slice(order.sell_token_balance.eip712_hash().as_slice());
buf[384..416].copy_from_slice(order.buy_token_balance.eip712_hash().as_slice());
keccak256(buf)
}
#[must_use]
pub fn signing_digest(domain_sep: B256, o_hash: B256) -> B256 {
let mut buf = [0u8; 66];
buf[0] = 0x19;
buf[1] = 0x01;
buf[2..34].copy_from_slice(domain_sep.as_slice());
buf[34..66].copy_from_slice(o_hash.as_slice());
keccak256(buf)
}
#[must_use]
pub const fn build_order_typed_data(order: UnsignedOrder, chain_id: u64) -> OrderTypedData {
OrderTypedData {
domain: OrderDomain {
name: "Gnosis Protocol v2",
version: "v2",
chain_id,
verifying_contract: SETTLEMENT_CONTRACT,
},
primary_type: "GPv2Order.Data",
order,
}
}
fn cancellations_type_hash() -> B256 {
keccak256(b"OrderCancellations(bytes[] orderUids)")
}
pub fn cancellations_hash(order_uids: &[&str]) -> Result<B256, CowError> {
let mut element_hashes: Vec<u8> = Vec::with_capacity(order_uids.len() * 32);
for uid in order_uids {
let stripped = uid.trim_start_matches("0x");
let bytes = alloy_primitives::hex::decode(stripped)
.map_err(|_e| CowError::Api { status: 0, body: format!("invalid orderUid: {uid}") })?;
element_hashes.extend_from_slice(keccak256(&bytes).as_slice());
}
let array_hash = keccak256(&element_hashes);
let mut buf = [0u8; 64];
buf[0..32].copy_from_slice(cancellations_type_hash().as_slice());
buf[32..64].copy_from_slice(array_hash.as_slice());
Ok(keccak256(buf))
}
#[must_use]
pub fn hashify(value: u64) -> B256 {
let u = U256::from(value);
B256::from(u.to_be_bytes())
}
#[must_use]
pub fn hash_typed_data(domain_sep: B256, struct_hash: B256) -> B256 {
signing_digest(domain_sep, struct_hash)
}
pub fn hash_order_cancellation(chain_id: u64, order_uid: &str) -> Result<B256, CowError> {
hash_order_cancellations(chain_id, &[order_uid])
}
pub fn hash_order_cancellations(chain_id: u64, order_uids: &[&str]) -> Result<B256, CowError> {
let ds = domain_separator(chain_id);
let ch = cancellations_hash(order_uids)?;
Ok(signing_digest(ds, ch))
}
#[must_use]
pub fn normalize_order(order: &UnsignedOrder) -> UnsignedOrder {
use crate::order_signing::flags::normalize_buy_token_balance;
let mut normalized = order.clone();
normalized.buy_token_balance = normalize_buy_token_balance(order.buy_token_balance);
normalized
}
#[must_use]
pub fn pack_order_uid_params(order_digest: B256, owner: Address, valid_to: u32) -> String {
let mut uid = Vec::with_capacity(ORDER_UID_LENGTH);
uid.extend_from_slice(order_digest.as_slice()); uid.extend_from_slice(owner.as_slice()); uid.extend_from_slice(&valid_to.to_be_bytes()); format!("0x{}", alloy_primitives::hex::encode(&uid))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OrderUidParams {
pub order_digest: B256,
pub owner: Address,
pub valid_to: u32,
}
pub fn extract_order_uid_params(order_uid: &str) -> Result<OrderUidParams, CowError> {
let stripped = order_uid.trim_start_matches("0x");
let bytes = alloy_primitives::hex::decode(stripped).map_err(|_e| CowError::Parse {
field: "orderUid",
reason: format!("invalid hex: {order_uid}"),
})?;
if bytes.len() != ORDER_UID_LENGTH {
return Err(CowError::Parse {
field: "orderUid",
reason: format!("invalid length: expected {ORDER_UID_LENGTH}, got {}", bytes.len()),
});
}
let order_digest = B256::from_slice(&bytes[0..32]);
let owner = Address::from_slice(&bytes[32..52]);
let valid_to = u32::from_be_bytes([bytes[52], bytes[53], bytes[54], bytes[55]]);
Ok(OrderUidParams { order_digest, owner, valid_to })
}
#[cfg(test)]
mod tests {
use crate::types::TokenBalance;
use super::*;
fn sample_order() -> UnsignedOrder {
UnsignedOrder {
sell_token: Address::repeat_byte(0x11),
buy_token: Address::repeat_byte(0x22),
receiver: Address::repeat_byte(0x33),
sell_amount: U256::from(1_000_000u64),
buy_amount: U256::from(900_000u64),
valid_to: 1_700_000_000,
app_data: B256::ZERO,
fee_amount: U256::from(1000u64),
kind: OrderKind::Sell,
partially_fillable: false,
sell_token_balance: TokenBalance::Erc20,
buy_token_balance: TokenBalance::Erc20,
}
}
#[test]
fn normalize_order_external_to_erc20() {
let mut order = sample_order();
order.buy_token_balance = TokenBalance::External;
let normalized = normalize_order(&order);
assert_eq!(normalized.buy_token_balance, TokenBalance::Erc20);
}
#[test]
fn normalize_order_erc20_stays() {
let order = sample_order();
let normalized = normalize_order(&order);
assert_eq!(normalized.buy_token_balance, TokenBalance::Erc20);
assert_eq!(normalized.sell_token_balance, TokenBalance::Erc20);
}
#[test]
fn normalize_order_internal_stays() {
let mut order = sample_order();
order.buy_token_balance = TokenBalance::Internal;
let normalized = normalize_order(&order);
assert_eq!(normalized.buy_token_balance, TokenBalance::Internal);
}
#[test]
fn pack_extract_order_uid_roundtrip() {
let digest = B256::new([0xab; 32]);
let owner = Address::repeat_byte(0xcd);
let valid_to = 1_700_000_000u32;
let uid = pack_order_uid_params(digest, owner, valid_to);
assert!(uid.starts_with("0x"));
assert_eq!(uid.len(), 2 + 112);
let params = extract_order_uid_params(&uid).unwrap();
assert_eq!(params.order_digest, digest);
assert_eq!(params.owner, owner);
assert_eq!(params.valid_to, valid_to);
}
#[test]
fn extract_order_uid_params_wrong_length() {
let result = extract_order_uid_params("0xabcd");
assert!(result.is_err());
}
#[test]
fn extract_order_uid_params_invalid_hex() {
let result = extract_order_uid_params("0xZZZZ");
assert!(result.is_err());
}
#[test]
fn pack_order_uid_params_zero_valid_to() {
let uid = pack_order_uid_params(B256::ZERO, Address::ZERO, 0);
let params = extract_order_uid_params(&uid).unwrap();
assert_eq!(params.valid_to, 0);
}
#[test]
fn hash_order_cancellation_produces_nonzero() {
let uid = format!("0x{}", "ab".repeat(56));
let result = hash_order_cancellation(1, &uid).unwrap();
assert_ne!(result, B256::ZERO);
}
#[test]
fn hash_order_cancellations_batch() {
let uid1 = format!("0x{}", "aa".repeat(56));
let uid2 = format!("0x{}", "bb".repeat(56));
let result = hash_order_cancellations(1, &[uid1.as_str(), uid2.as_str()]).unwrap();
assert_ne!(result, B256::ZERO);
}
#[test]
fn hash_order_cancellations_single_matches_convenience() {
let uid = format!("0x{}", "cc".repeat(56));
let single = hash_order_cancellation(1, &uid).unwrap();
let batch = hash_order_cancellations(1, &[uid.as_str()]).unwrap();
assert_eq!(single, batch);
}
#[test]
fn hash_order_cancellations_invalid_hex() {
let result = hash_order_cancellations(1, &["0xNOTHEX"]);
assert!(result.is_err());
}
#[test]
fn cancellations_hash_deterministic() {
let uid = format!("0x{}", "dd".repeat(20));
let h1 = cancellations_hash(&[uid.as_str()]).unwrap();
let h2 = cancellations_hash(&[uid.as_str()]).unwrap();
assert_eq!(h1, h2);
}
#[test]
fn cancellations_hash_order_matters() {
let uid1 = format!("0x{}", "aa".repeat(20));
let uid2 = format!("0x{}", "bb".repeat(20));
let h_ab = cancellations_hash(&[uid1.as_str(), uid2.as_str()]).unwrap();
let h_ba = cancellations_hash(&[uid2.as_str(), uid1.as_str()]).unwrap();
assert_ne!(h_ab, h_ba);
}
#[test]
fn domain_separator_deterministic() {
let ds1 = domain_separator(1);
let ds2 = domain_separator(1);
assert_eq!(ds1, ds2);
}
#[test]
fn domain_separator_differs_by_chain() {
assert_ne!(domain_separator(1), domain_separator(5));
}
#[test]
fn order_hash_deterministic() {
let order = sample_order();
assert_eq!(order_hash(&order), order_hash(&order));
}
#[test]
fn order_hash_differs_by_kind() {
let mut sell = sample_order();
sell.kind = OrderKind::Sell;
let mut buy = sample_order();
buy.kind = OrderKind::Buy;
assert_ne!(order_hash(&sell), order_hash(&buy));
}
#[test]
fn signing_digest_starts_with_eip712_prefix() {
let ds = domain_separator(1);
let oh = order_hash(&sample_order());
let digest = signing_digest(ds, oh);
assert_ne!(digest, B256::ZERO);
}
#[test]
fn hash_typed_data_equals_signing_digest() {
let ds = domain_separator(1);
let oh = order_hash(&sample_order());
assert_eq!(hash_typed_data(ds, oh), signing_digest(ds, oh));
}
#[test]
fn build_order_typed_data_fields() {
let order = sample_order();
let typed = build_order_typed_data(order.clone(), 1);
assert_eq!(typed.primary_type, "GPv2Order.Data");
assert_eq!(typed.domain.chain_id, 1);
assert_eq!(typed.domain.name, "Gnosis Protocol v2");
assert_eq!(typed.order.sell_token, order.sell_token);
}
#[test]
fn order_domain_default_separator_matches_domain_separator() {
use crate::order_signing::types::OrderDomain;
let domain = OrderDomain::for_chain(1);
assert_eq!(domain_separator_from(&domain), domain_separator(1));
}
#[test]
fn order_domain_with_chain_id() {
use crate::order_signing::types::OrderDomain;
let domain = OrderDomain::for_chain(1).with_chain_id(5);
assert_eq!(domain.chain_id, 5);
assert_eq!(domain_separator_from(&domain), domain_separator(5));
}
#[test]
fn order_domain_with_name_changes_separator() {
use crate::order_signing::types::OrderDomain;
let standard = OrderDomain::for_chain(1);
let custom = OrderDomain::for_chain(1).with_name("Custom Protocol");
assert_ne!(domain_separator_from(&standard), domain_separator_from(&custom));
}
#[test]
fn order_domain_with_version_changes_separator() {
use crate::order_signing::types::OrderDomain;
let standard = OrderDomain::for_chain(1);
let custom = OrderDomain::for_chain(1).with_version("v3");
assert_ne!(domain_separator_from(&standard), domain_separator_from(&custom));
}
#[test]
fn order_domain_with_verifying_contract_changes_separator() {
use crate::order_signing::types::OrderDomain;
let standard = OrderDomain::for_chain(1);
let custom = OrderDomain::for_chain(1).with_verifying_contract(Address::repeat_byte(0xff));
assert_ne!(domain_separator_from(&standard), domain_separator_from(&custom));
}
#[test]
fn order_domain_builder_chaining() {
use crate::order_signing::types::OrderDomain;
let domain = OrderDomain::for_chain(1)
.with_name("Test")
.with_version("v1")
.with_chain_id(42)
.with_verifying_contract(Address::repeat_byte(0xab));
assert_eq!(domain.name, "Test");
assert_eq!(domain.version, "v1");
assert_eq!(domain.chain_id, 42);
assert_eq!(domain.verifying_contract, Address::repeat_byte(0xab));
let sep = domain_separator_from(&domain);
assert_ne!(sep, B256::ZERO);
assert_eq!(sep, domain_separator_from(&domain));
}
#[test]
fn order_domain_domain_separator_method_uses_custom_fields() {
use crate::order_signing::types::OrderDomain;
let custom = OrderDomain::for_chain(1).with_name("Fork");
assert_ne!(custom.domain_separator(), domain_separator(1));
assert_eq!(custom.domain_separator(), domain_separator_from(&custom));
}
#[test]
fn hashify_zero() {
assert_eq!(hashify(0), B256::ZERO);
}
#[test]
fn hashify_small_value() {
let h = hashify(42);
assert_eq!(h, B256::left_padding_from(&[42]));
}
}