use alloy_primitives::{Address, B256, Bytes, U256, address, keccak256};
use alloy_sol_types::{SolValue, sol};
use std::fmt::{self, Display};
use crate::chain::Chain;
sol! {
#[derive(Debug, Eq, Hash, PartialEq)]
struct ConditionalOrderParams {
address handler;
bytes32 salt;
bytes staticInput;
}
#[derive(Debug, Eq, Hash, PartialEq)]
struct TwapStaticInput {
address sellToken;
address buyToken;
address receiver;
uint256 partSellAmount;
uint256 minPartLimit;
uint256 t0;
uint256 n;
uint256 t;
uint256 span;
bytes32 appData;
}
#[derive(Debug, Eq, Hash, PartialEq)]
struct Proof {
uint256 location;
bytes data;
}
#[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);
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PollOutcome {
OrderNotValid(String),
TryNextBlock(String),
TryAtBlock {
block: u64,
reason: String,
},
TryAtEpoch {
timestamp: u64,
reason: String,
},
Never(String),
}
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::Gnosis | Self::Sepolia | Self::ArbitrumOne
)
}
pub const fn composable_cow_address(self) -> Option<Address> {
if self.supports_composable_cow() {
Some(COMPOSABLE_COW)
} else {
None
}
}
pub const fn extensible_fallback_handler_address(self) -> Option<Address> {
if self.supports_composable_cow() {
Some(EXTENSIBLE_FALLBACK_HANDLER)
} else {
None
}
}
pub const fn current_block_timestamp_factory_address(self) -> Option<Address> {
if self.supports_composable_cow() {
Some(CURRENT_BLOCK_TIMESTAMP_FACTORY)
} else {
None
}
}
}
pub const TWAP_HANDLER: Address = address!("0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5");
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum TwapStart {
AtMiningTime,
AtEpoch(u32),
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum TwapDuration {
Auto,
LimitDuration(u32),
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct TwapData {
pub sell_token: Address,
pub buy_token: Address,
pub receiver: Address,
pub sell_amount: U256,
pub buy_amount: U256,
pub time_between_parts: u32,
pub number_of_parts: u32,
pub start: TwapStart,
pub duration: TwapDuration,
pub app_data: B256,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TwapError {
SameToken,
InvalidToken,
InvalidSellAmount,
InvalidBuyAmount,
InvalidNumParts,
InvalidFrequency,
InvalidSpan,
}
impl Display for TwapError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::SameToken => "TWAP sell_token equals buy_token",
Self::InvalidToken => "TWAP token is the zero address",
Self::InvalidSellAmount => "TWAP sell amount is zero or smaller than number_of_parts",
Self::InvalidBuyAmount => "TWAP buy amount is zero or smaller than number_of_parts",
Self::InvalidNumParts => "TWAP number_of_parts must be > 1",
Self::InvalidFrequency => "TWAP time_between_parts must be > 0 and <= 365 days",
Self::InvalidSpan => "TWAP LimitDuration span must be <= time_between_parts",
})
}
}
impl std::error::Error for TwapError {}
const TWAP_MAX_FREQUENCY_SECONDS: u32 = 365 * 24 * 60 * 60;
impl TwapData {
pub fn static_input(&self) -> Result<TwapStaticInput, TwapError> {
if self.sell_token == self.buy_token {
return Err(TwapError::SameToken);
}
if self.sell_token == Address::ZERO || self.buy_token == Address::ZERO {
return Err(TwapError::InvalidToken);
}
if self.number_of_parts <= 1 {
return Err(TwapError::InvalidNumParts);
}
if self.time_between_parts == 0 || self.time_between_parts > TWAP_MAX_FREQUENCY_SECONDS {
return Err(TwapError::InvalidFrequency);
}
let span = match self.duration {
TwapDuration::Auto => 0,
TwapDuration::LimitDuration(s) => {
if s > self.time_between_parts {
return Err(TwapError::InvalidSpan);
}
s
}
};
let t0 = match self.start {
TwapStart::AtMiningTime => 0,
TwapStart::AtEpoch(s) => s,
};
let n = U256::from(self.number_of_parts);
let part_sell_amount = self.sell_amount / n;
let min_part_limit = self.buy_amount / n;
if part_sell_amount.is_zero() {
return Err(TwapError::InvalidSellAmount);
}
if min_part_limit.is_zero() {
return Err(TwapError::InvalidBuyAmount);
}
Ok(TwapStaticInput {
sellToken: self.sell_token,
buyToken: self.buy_token,
receiver: self.receiver,
partSellAmount: part_sell_amount,
minPartLimit: min_part_limit,
t0: U256::from(t0),
n,
t: U256::from(self.time_between_parts),
span: U256::from(span),
appData: self.app_data,
})
}
pub fn encode_static_input(&self) -> Result<Vec<u8>, TwapError> {
Ok(self.static_input()?.abi_encode())
}
pub fn into_params(&self, salt: B256) -> Result<ConditionalOrderParams, TwapError> {
Ok(ConditionalOrderParams {
handler: TWAP_HANDLER,
salt,
staticInput: Bytes::from(self.encode_static_input()?),
})
}
pub fn leaf_id(&self, salt: B256) -> Result<B256, TwapError> {
Ok(keccak256(self.into_params(salt)?.abi_encode()))
}
}
#[cfg(test)]
mod tests {
use alloy_primitives::{B256, Bytes, U256, hex, keccak256};
use alloy_sol_types::{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 {
location: U256::from(0_u64),
data: 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 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")
);
for chain in [
Chain::Mainnet,
Chain::Gnosis,
Chain::Sepolia,
Chain::ArbitrumOne,
] {
assert_eq!(chain.composable_cow_address(), Some(COMPOSABLE_COW));
}
for chain in [
Chain::Bnb,
Chain::Polygon,
Chain::Base,
Chain::Plasma,
Chain::Avalanche,
Chain::Ink,
Chain::Linea,
] {
assert!(chain.composable_cow_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)")
);
}
#[test]
fn twap_leaf_id_matches_cow_py_vector() {
let twap = TwapData {
sell_token: address!("6810e776880C02933D47DB1b9fc05908e5386b96"),
buy_token: address!("DAE5F1590db13E3B40423B5b5c5fbf175515910b"),
receiver: address!("DeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"),
sell_amount: U256::from(1_000_000_000_000_000_000_u128),
buy_amount: U256::from(1_000_000_000_000_000_000_u128),
time_between_parts: 3600,
number_of_parts: 10,
start: TwapStart::AtMiningTime,
duration: TwapDuration::Auto,
app_data: B256::from(hex!(
"d51f28edffcaaa76be4a22f6375ad289272c037f3cc072345676e88d92ced8b5"
)),
};
let salt = B256::from(hex!(
"d98a87ed4e45bfeae3f779e1ac09ceacdfb57da214c7fffa6434aeb969f396c0"
));
let id = twap.leaf_id(salt).unwrap();
assert_eq!(
id.0,
hex!("d8a6889486a47d8ca8f4189f11573b39dbc04f605719ebf4050e44ae53c1bedf"),
);
let salt2 = B256::from(hex!(
"d98a87ed4e45bfeae3f779e1ac09ceacdfb57da214c7fffa6434aeb969f396c1"
));
let id2 = twap.leaf_id(salt2).unwrap();
assert_eq!(
id2.0,
hex!("8ddb7e8e1cd6a06d5bb6f91af21a2b26a433a5d8402ccddb00a72e4006c46994"),
);
}
#[test]
fn twap_validation_rejects_invalid_parameters() {
let base = TwapData {
sell_token: address!("6810e776880C02933D47DB1b9fc05908e5386b96"),
buy_token: address!("DAE5F1590db13E3B40423B5b5c5fbf175515910b"),
receiver: address!("DeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"),
sell_amount: U256::from(1_000_000_000_000_000_000_u128),
buy_amount: U256::from(1_000_000_000_000_000_000_u128),
time_between_parts: 3600,
number_of_parts: 10,
start: TwapStart::AtMiningTime,
duration: TwapDuration::Auto,
app_data: B256::ZERO,
};
assert!(base.static_input().is_ok());
let mut same_token = base;
same_token.buy_token = base.sell_token;
assert_eq!(same_token.static_input().unwrap_err(), TwapError::SameToken);
let mut zero_sell = base;
zero_sell.sell_token = alloy_primitives::Address::ZERO;
assert_eq!(
zero_sell.static_input().unwrap_err(),
TwapError::InvalidToken
);
let mut one_part = base;
one_part.number_of_parts = 1;
assert_eq!(
one_part.static_input().unwrap_err(),
TwapError::InvalidNumParts
);
let mut zero_freq = base;
zero_freq.time_between_parts = 0;
assert_eq!(
zero_freq.static_input().unwrap_err(),
TwapError::InvalidFrequency
);
let mut excess_span = base;
excess_span.duration = TwapDuration::LimitDuration(7200);
assert_eq!(
excess_span.static_input().unwrap_err(),
TwapError::InvalidSpan
);
let mut tiny_sell = base;
tiny_sell.sell_amount = U256::from(5_u64); assert_eq!(
tiny_sell.static_input().unwrap_err(),
TwapError::InvalidSellAmount
);
}
#[test]
fn conditional_order_created_event_data_round_trips() {
let params = ConditionalOrderParams {
handler: COMPOSABLE_COW,
salt: B256::from(hex!(
"0202020202020202020202020202020202020202020202020202020202020202"
)),
staticInput: Bytes::from_static(b"static-payload"),
};
let event = ComposableCoW::ConditionalOrderCreated {
owner: alloy_primitives::address!("70997970C51812dc3A010C7d01b50e0d17dc79C8"),
params: params.clone(),
};
let data = event.encode_data();
let decoded = ComposableCoW::ConditionalOrderCreated::abi_decode_data(&data).unwrap();
assert_eq!(decoded.0, params);
}
}