use alloy_primitives::{Address, B256, Bytes, U256, address, keccak256};
use alloy_sol_types::{SolValue, sol};
use super::ConditionalOrderParams;
sol! {
#[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;
}
}
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, thiserror::Error, Eq, PartialEq)]
pub enum TwapError {
#[error("TWAP sell_token equals buy_token")]
SameToken,
#[error("TWAP token is the zero address")]
InvalidToken,
#[error("TWAP sell amount is zero or does not divide cleanly across number_of_parts")]
InvalidSellAmount,
#[error("TWAP buy amount is zero or does not divide cleanly across number_of_parts")]
InvalidBuyAmount,
#[error("TWAP number_of_parts must be > 1")]
InvalidNumParts,
#[error("TWAP time_between_parts must be > 0 and <= 365 days")]
InvalidFrequency,
#[error("TWAP LimitDuration span must be <= time_between_parts")]
InvalidSpan,
#[error("TWAP AtEpoch must be in 1..u32::MAX (0 and u32::MAX are reserved)")]
InvalidEpoch,
}
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(0 | u32::MAX) => return Err(TwapError::InvalidEpoch),
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() || self.sell_amount % n != U256::ZERO {
return Err(TwapError::InvalidSellAmount);
}
if min_part_limit.is_zero() || self.buy_amount % n != U256::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 to_params(&self, salt: B256) -> Result<ConditionalOrderParams, TwapError> {
Ok(ConditionalOrderParams {
handler: TWAP_HANDLER,
salt,
staticInput: Bytes::from(self.static_input()?.abi_encode()),
})
}
pub fn leaf_id(&self, salt: B256) -> Result<B256, TwapError> {
Ok(keccak256(self.to_params(salt)?.abi_encode()))
}
}
#[cfg(test)]
mod tests {
use alloy_primitives::{B256, hex};
use super::*;
#[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
);
let mut indivisible_sell = base;
indivisible_sell.sell_amount = U256::from(101_u64);
assert_eq!(
indivisible_sell.static_input().unwrap_err(),
TwapError::InvalidSellAmount
);
let mut indivisible_buy = base;
indivisible_buy.buy_amount = U256::from(101_u64);
assert_eq!(
indivisible_buy.static_input().unwrap_err(),
TwapError::InvalidBuyAmount
);
}
#[test]
fn twap_at_epoch_zero_rejected() {
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::AtEpoch(0),
duration: TwapDuration::Auto,
app_data: B256::ZERO,
};
assert_eq!(twap.static_input().unwrap_err(), TwapError::InvalidEpoch);
let mut at_max = twap;
at_max.start = TwapStart::AtEpoch(u32::MAX);
assert_eq!(at_max.static_input().unwrap_err(), TwapError::InvalidEpoch);
let mut just_under = twap;
just_under.start = TwapStart::AtEpoch(u32::MAX - 1);
assert!(just_under.static_input().is_ok());
}
#[test]
fn twap_at_mining_time_still_encodes_t0_zero() {
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::ZERO,
};
assert_eq!(twap.static_input().unwrap().t0, U256::ZERO);
}
#[test]
fn twap_at_epoch_one_round_trips() {
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::AtEpoch(1),
duration: TwapDuration::Auto,
app_data: B256::ZERO,
};
assert_eq!(twap.static_input().unwrap().t0, U256::from(1u32));
}
}