mod twap;
pub use twap::*;
use alloy_primitives::{Address, B256, Bytes, U256, address};
use alloy_sol_types::{SolCall, SolValue, sol};
use crate::chain::Chain;
use crate::contracts::{GPV2_ORDER_TYPE_HASH, GPv2OrderData};
sol! {
#[derive(Debug, Eq, Hash, PartialEq)]
struct ConditionalOrderParams {
address handler;
bytes32 salt;
bytes staticInput;
}
#[derive(Debug, Eq, Hash, PartialEq)]
struct Proof {
uint256 location;
bytes data;
}
#[derive(Debug, Eq, Hash, PartialEq)]
struct PayloadStruct {
bytes32[] proof;
ConditionalOrderParams params;
bytes offchainInput;
}
#[derive(Debug)]
interface ComposableCoW {
event MerkleRootSet(address indexed owner, bytes32 root, Proof proof);
event ConditionalOrderCreated(
address indexed owner,
ConditionalOrderParams params
);
event SwapGuardSet(address indexed owner, address swapGuard);
function create(ConditionalOrderParams params, bool dispatch) external;
function createWithContext(
ConditionalOrderParams params,
address factory,
bytes data,
bool dispatch
) external;
function remove(bytes32 singleOrderHash) external;
function setRoot(bytes32 root, Proof proof) external;
function setRootWithContext(
bytes32 root,
Proof proof,
address factory,
bytes data
) external;
function setSwapGuard(address swapGuard) external;
function singleOrders(address owner, bytes32 singleOrderHash) external view returns (bool);
function roots(address owner) external view returns (bytes32);
function swapGuards(address owner) external view returns (address);
function cabinet(address owner, bytes32 ctx) external view returns (bytes32);
function hash(ConditionalOrderParams params) external pure returns (bytes32);
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[repr(u8)]
pub enum ProofLocation {
Private = 0,
Emitted = 1,
Swarm = 2,
Waku = 3,
Reserved = 4,
Ipfs = 5,
}
impl From<ProofLocation> for U256 {
fn from(location: ProofLocation) -> Self {
Self::from(location as u8)
}
}
impl Proof {
pub fn new(location: ProofLocation, data: Bytes) -> Self {
Self {
location: location.into(),
data,
}
}
}
sol! {
function safeSignature(
bytes32 domainSeparator,
bytes32 typeHash,
bytes encodeData,
bytes payload
) external view returns (bytes4);
}
pub fn safe_handler_signature(
domain_separator: B256,
order: &GPv2OrderData,
payload: &PayloadStruct,
) -> Vec<u8> {
safeSignatureCall {
domainSeparator: domain_separator,
typeHash: GPV2_ORDER_TYPE_HASH,
encodeData: order.abi_encode().into(),
payload: payload.abi_encode().into(),
}
.abi_encode()
}
pub fn forwarder_signature(order: &GPv2OrderData, payload: &PayloadStruct) -> Vec<u8> {
(order.clone(), payload.clone()).abi_encode_params()
}
pub const COMPOSABLE_COW: Address = address!("0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74");
pub const EXTENSIBLE_FALLBACK_HANDLER: Address =
address!("0x2f55e8b20D0B9FEFA187AA7d00B6Cbe563605bF5");
pub const CURRENT_BLOCK_TIMESTAMP_FACTORY: Address =
address!("0x52eD56Da04309Aca4c3FECC595298d80C2f16BAc");
impl Chain {
pub const fn supports_composable_cow(self) -> bool {
matches!(
self,
Self::Mainnet
| Self::Bnb
| Self::Gnosis
| Self::Sepolia
| Self::ArbitrumOne
| Self::Linea
| Self::Plasma
)
}
const fn deployed(self, addr: Address) -> Option<Address> {
if self.supports_composable_cow() {
Some(addr)
} else {
None
}
}
pub const fn composable_cow_address(self) -> Option<Address> {
self.deployed(COMPOSABLE_COW)
}
pub const fn extensible_fallback_handler_address(self) -> Option<Address> {
self.deployed(EXTENSIBLE_FALLBACK_HANDLER)
}
pub const fn current_block_timestamp_factory_address(self) -> Option<Address> {
self.deployed(CURRENT_BLOCK_TIMESTAMP_FACTORY)
}
pub const fn twap_handler_address(self) -> Option<Address> {
self.deployed(TWAP_HANDLER)
}
}
#[cfg(test)]
mod tests {
use alloy_primitives::{B256, Bytes, U256, hex, keccak256};
use alloy_sol_types::{SolCall, SolEvent, SolValue};
use super::*;
#[test]
fn conditional_order_leaf_id_matches_cow_py_vector() {
let params = ConditionalOrderParams {
handler: address!("910d00a310f7Dc5B29FE73458F47f519be547D3d"),
salt: B256::from(hex!(
"9379a0bf532ff9a66ffde940f94b1a025d6f18803054c1aef52dc94b15255bbe"
)),
staticInput: Bytes::new(),
};
let id = keccak256(params.abi_encode());
assert_eq!(
id.0,
hex!("88ca0698d8c5500b31015d84fa0166272e1812320d9af8b60e29ae00153363b3"),
);
}
#[test]
fn conditional_order_params_round_trips_via_abi() {
let params = ConditionalOrderParams {
handler: COMPOSABLE_COW,
salt: B256::from(hex!(
"0101010101010101010101010101010101010101010101010101010101010101"
)),
staticInput: Bytes::from_static(&hex!("deadbeef")),
};
let encoded = params.abi_encode();
let decoded = ConditionalOrderParams::abi_decode(&encoded).unwrap();
assert_eq!(decoded.handler, params.handler);
assert_eq!(decoded.salt, params.salt);
assert_eq!(decoded.staticInput, params.staticInput);
}
#[test]
fn proof_round_trips_via_abi() {
let proof = Proof::new(ProofLocation::Private, Bytes::from_static(b"hello"));
let encoded = proof.abi_encode();
let decoded = Proof::abi_decode(&encoded).unwrap();
assert_eq!(decoded.location, proof.location);
assert_eq!(decoded.data, proof.data);
}
#[test]
fn proof_location_discriminants_match_cow_sdk() {
let cases: [(ProofLocation, u8); 6] = [
(ProofLocation::Private, 0),
(ProofLocation::Emitted, 1),
(ProofLocation::Swarm, 2),
(ProofLocation::Waku, 3),
(ProofLocation::Reserved, 4),
(ProofLocation::Ipfs, 5),
];
for (location, code) in cases {
assert_eq!(location as u8, code, "{location:?}");
let widened: U256 = location.into();
assert_eq!(widened, U256::from(code), "{location:?}");
}
}
#[test]
fn proof_new_widens_location_to_uint256() {
let proof = Proof::new(ProofLocation::Ipfs, Bytes::from_static(b"ipfs://bafy"));
assert_eq!(proof.location, U256::from(5));
assert_eq!(proof.data.as_ref(), b"ipfs://bafy");
}
fn sample_order_and_payload() -> (GPv2OrderData, PayloadStruct) {
let order = GPv2OrderData {
sellToken: address!("6810e776880C02933D47DB1b9fc05908e5386b96"),
buyToken: address!("DAE5F1590db13E3B40423B5b5c5fbf175515910b"),
receiver: address!("DeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"),
sellAmount: U256::from(1_000_u64),
buyAmount: U256::from(2_000_u64),
validTo: 1_700_000_000,
appData: B256::repeat_byte(0xaa),
feeAmount: U256::ZERO,
kind: B256::repeat_byte(0xbb),
partiallyFillable: false,
sellTokenBalance: B256::repeat_byte(0xcc),
buyTokenBalance: B256::repeat_byte(0xdd),
};
let payload = PayloadStruct {
proof: vec![B256::repeat_byte(0x22), B256::repeat_byte(0x33)],
params: ConditionalOrderParams {
handler: TWAP_HANDLER,
salt: B256::repeat_byte(0x11),
staticInput: Bytes::from_static(&hex!("c0ffee")),
},
offchainInput: Bytes::from_static(b"offchain"),
};
(order, payload)
}
#[test]
fn safe_handler_signature_matches_encode_with_signature() {
let (order, payload) = sample_order_and_payload();
let domain = B256::repeat_byte(0x44);
let blob = safe_handler_signature(domain, &order, &payload);
assert_eq!(&blob[..4], &safeSignatureCall::SELECTOR);
let decoded = safeSignatureCall::abi_decode(&blob).unwrap();
assert_eq!(decoded.domainSeparator, domain);
assert_eq!(decoded.typeHash, GPV2_ORDER_TYPE_HASH);
assert_eq!(decoded.encodeData.as_ref(), order.abi_encode().as_slice());
assert_eq!(decoded.payload.as_ref(), payload.abi_encode().as_slice());
}
#[test]
fn forwarder_signature_round_trips_order_and_payload() {
let (order, payload) = sample_order_and_payload();
let blob = forwarder_signature(&order, &payload);
let (decoded_order, decoded_payload) =
<(GPv2OrderData, PayloadStruct)>::abi_decode_params(&blob).unwrap();
assert_eq!(decoded_order.sellToken, order.sellToken);
assert_eq!(decoded_order.buyTokenBalance, order.buyTokenBalance);
assert_eq!(decoded_payload, payload);
}
#[test]
fn composable_cow_selectors_match_keccak() {
let cases: &[(&[u8; 4], &[u8])] = &[
(
&ComposableCoW::createCall::SELECTOR,
b"create((address,bytes32,bytes),bool)",
),
(
&ComposableCoW::createWithContextCall::SELECTOR,
b"createWithContext((address,bytes32,bytes),address,bytes,bool)",
),
(&ComposableCoW::removeCall::SELECTOR, b"remove(bytes32)"),
(
&ComposableCoW::setRootCall::SELECTOR,
b"setRoot(bytes32,(uint256,bytes))",
),
(
&ComposableCoW::setRootWithContextCall::SELECTOR,
b"setRootWithContext(bytes32,(uint256,bytes),address,bytes)",
),
(
&ComposableCoW::setSwapGuardCall::SELECTOR,
b"setSwapGuard(address)",
),
(
&ComposableCoW::singleOrdersCall::SELECTOR,
b"singleOrders(address,bytes32)",
),
(&ComposableCoW::rootsCall::SELECTOR, b"roots(address)"),
(
&ComposableCoW::swapGuardsCall::SELECTOR,
b"swapGuards(address)",
),
(
&ComposableCoW::cabinetCall::SELECTOR,
b"cabinet(address,bytes32)",
),
(
&ComposableCoW::hashCall::SELECTOR,
b"hash((address,bytes32,bytes))",
),
];
for (selector, signature) in cases {
let expected = &keccak256(signature)[..4];
assert_eq!(
selector.as_slice(),
expected,
"selector for {} does not match keccak256(signature)",
std::str::from_utf8(signature).unwrap(),
);
}
}
#[test]
fn set_root_call_round_trips() {
let root = B256::from(hex!(
"abababababababababababababababababababababababababababababababab"
));
let proof = Proof::new(ProofLocation::Ipfs, Bytes::from_static(b"ipfs://bafy"));
let call = ComposableCoW::setRootCall {
root,
proof: proof.clone(),
};
let encoded = call.abi_encode();
assert_eq!(&encoded[..4], &ComposableCoW::setRootCall::SELECTOR);
let decoded = ComposableCoW::setRootCall::abi_decode(&encoded).unwrap();
assert_eq!(decoded.root, root);
assert_eq!(decoded.proof.location, proof.location);
assert_eq!(decoded.proof.data, proof.data);
}
#[test]
fn create_call_round_trips() {
let params = ConditionalOrderParams {
handler: TWAP_HANDLER,
salt: B256::from(hex!(
"0202020202020202020202020202020202020202020202020202020202020202"
)),
staticInput: Bytes::from_static(&hex!("c0ffee")),
};
let call = ComposableCoW::createCall {
params: params.clone(),
dispatch: true,
};
let encoded = call.abi_encode();
assert_eq!(&encoded[..4], &ComposableCoW::createCall::SELECTOR);
let decoded = ComposableCoW::createCall::abi_decode(&encoded).unwrap();
assert_eq!(decoded.params.handler, params.handler);
assert_eq!(decoded.params.salt, params.salt);
assert_eq!(decoded.params.staticInput, params.staticInput);
assert!(decoded.dispatch);
}
#[test]
fn hash_call_selector_matches_inner_leaf_derivation() {
let inner_signature = b"hash((address,bytes32,bytes))";
assert_eq!(
&ComposableCoW::hashCall::SELECTOR,
&keccak256(inner_signature)[..4]
);
}
#[test]
fn composable_cow_addresses_match_canonical_deployment() {
assert_eq!(
COMPOSABLE_COW,
address!("fdaFc9d1902f4e0b84f65F49f244b32b31013b74")
);
assert_eq!(
EXTENSIBLE_FALLBACK_HANDLER,
address!("2f55e8b20D0B9FEFA187AA7d00B6Cbe563605bF5")
);
assert_eq!(
CURRENT_BLOCK_TIMESTAMP_FACTORY,
address!("52eD56Da04309Aca4c3FECC595298d80C2f16BAc")
);
assert_eq!(
TWAP_HANDLER,
address!("6cF1e9cA41f7611dEf408122793c358a3d11E5a5")
);
for chain in [
Chain::Mainnet,
Chain::Bnb,
Chain::Gnosis,
Chain::Sepolia,
Chain::ArbitrumOne,
Chain::Linea,
Chain::Plasma,
] {
assert!(chain.supports_composable_cow());
assert_eq!(chain.composable_cow_address(), Some(COMPOSABLE_COW));
assert_eq!(
chain.extensible_fallback_handler_address(),
Some(EXTENSIBLE_FALLBACK_HANDLER)
);
assert_eq!(
chain.current_block_timestamp_factory_address(),
Some(CURRENT_BLOCK_TIMESTAMP_FACTORY)
);
assert_eq!(chain.twap_handler_address(), Some(TWAP_HANDLER));
}
for chain in [Chain::Polygon, Chain::Base, Chain::Avalanche] {
assert!(chain.composable_cow_address().is_none());
assert!(chain.twap_handler_address().is_none());
}
}
#[test]
fn composable_cow_event_topic_hashes_match_keccak() {
assert_eq!(
ComposableCoW::MerkleRootSet::SIGNATURE_HASH,
keccak256("MerkleRootSet(address,bytes32,(uint256,bytes))")
);
assert_eq!(
ComposableCoW::ConditionalOrderCreated::SIGNATURE_HASH,
keccak256("ConditionalOrderCreated(address,(address,bytes32,bytes))")
);
assert_eq!(
ComposableCoW::SwapGuardSet::SIGNATURE_HASH,
keccak256("SwapGuardSet(address,address)")
);
}
}