use alloy::{
primitives::{keccak256, Address, U256},
rpc::types::{BlockNumberOrTag, TransactionInput, TransactionRequest},
};
use tycho_common::models::blockchain::BlockTag;
pub(crate) fn arbitrary_recipient() -> Address {
let hash = keccak256(b"propeller");
Address::from_slice(&hash[..20])
}
pub(crate) struct ObservedTransfer {
pub(crate) sent: U256,
pub(crate) balance_before: U256,
pub(crate) balance_after: U256,
}
impl ObservedTransfer {
fn fee_bps(&self) -> Result<U256, String> {
let expected_after = self
.balance_before
.checked_add(self.sent)
.ok_or_else(|| format!("balance {} + {} overflows", self.balance_before, self.sent))?;
if self.sent.is_zero() || self.balance_after >= expected_after {
return Ok(U256::ZERO);
}
let shortfall = expected_after - self.balance_after;
Ok(shortfall
.checked_mul(U256::from(10_000))
.ok_or_else(|| format!("shortfall {shortfall} * 10_000 overflows"))? /
self.sent)
}
}
pub(crate) fn calculate_fee_bps(
inbound: ObservedTransfer,
outbound: ObservedTransfer,
) -> Result<U256, String> {
Ok(inbound
.fee_bps()?
.max(outbound.fee_bps()?))
}
pub(crate) fn map_block_tag(block: BlockTag) -> BlockNumberOrTag {
match block {
BlockTag::Finalized => BlockNumberOrTag::Finalized,
BlockTag::Safe => BlockNumberOrTag::Safe,
BlockTag::Latest => BlockNumberOrTag::Latest,
BlockTag::Earliest => BlockNumberOrTag::Earliest,
BlockTag::Pending => BlockNumberOrTag::Pending,
BlockTag::Number(n) => BlockNumberOrTag::Number(n),
}
}
pub(crate) fn call_request(
from: Option<Address>,
to: Address,
calldata: Vec<u8>,
) -> TransactionRequest {
let mut req = TransactionRequest::default()
.to(to)
.input(TransactionInput::both(calldata.into()));
if let Some(addr) = from {
req = req.from(addr);
}
req
}
#[cfg(test)]
mod tests {
use alloy::{primitives::U256, rpc::types::BlockNumberOrTag};
use tycho_common::models::blockchain::BlockTag;
use super::{calculate_fee_bps, map_block_tag, ObservedTransfer};
fn fee(
amount: u64,
before_in: u64,
after_in: u64,
recipient_before: u64,
recipient_after: u64,
) -> Result<U256, String> {
let before_in = U256::from(before_in);
let after_in = U256::from(after_in);
calculate_fee_bps(
ObservedTransfer {
sent: U256::from(amount),
balance_before: before_in,
balance_after: after_in,
},
ObservedTransfer {
sent: after_in - before_in,
balance_before: U256::from(recipient_before),
balance_after: U256::from(recipient_after),
},
)
}
#[test]
fn calculate_fee_bps_no_fee() {
assert_eq!(fee(1_000_000, 0, 1_000_000, 0, 1_000_000), Ok(U256::ZERO));
}
#[test]
fn calculate_fee_bps_one_percent() {
assert_eq!(fee(1_000_000, 0, 990_000, 0, 980_100), Ok(U256::from(100)));
}
#[test]
fn calculate_fee_bps_settlement_balance_exceeds_fee() {
assert_eq!(fee(1_000_000, 50_000, 1_040_000, 0, 990_000), Ok(U256::from(100)));
}
#[test]
fn calculate_fee_bps_one_wei_short_is_zero() {
assert_eq!(fee(1_000_000, 2, 1_000_001, 0, 999_999), Ok(U256::ZERO));
}
#[test]
fn calculate_fee_bps_credits_more_than_sent_is_zero() {
assert_eq!(fee(1_000_000, 0, 1_000_001, 0, 1_000_001), Ok(U256::ZERO));
}
#[test]
fn calculate_fee_bps_takes_the_higher_rate() {
assert_eq!(fee(1_000_000, 0, 990_000, 0, 792_000), Ok(U256::from(2_000)));
}
#[test]
fn calculate_fee_bps_full_fee() {
assert_eq!(fee(1_000_000, 7, 7, 0, 0), Ok(U256::from(10_000)));
}
#[test]
fn calculate_fee_bps_balance_near_max_errors() {
let result = calculate_fee_bps(
ObservedTransfer {
sent: U256::from(1_000_000),
balance_before: U256::MAX,
balance_after: U256::MAX,
},
ObservedTransfer {
sent: U256::ZERO,
balance_before: U256::ZERO,
balance_after: U256::ZERO,
},
);
assert!(result
.unwrap_err()
.ends_with("+ 1000000 overflows"));
}
#[test]
fn calculate_fee_bps_shortfall_near_max_errors() {
let result = calculate_fee_bps(
ObservedTransfer {
sent: U256::MAX,
balance_before: U256::ZERO,
balance_after: U256::ZERO,
},
ObservedTransfer {
sent: U256::ZERO,
balance_before: U256::ZERO,
balance_after: U256::ZERO,
},
);
assert!(result
.unwrap_err()
.ends_with("* 10_000 overflows"));
}
#[test]
fn test_map_block_tag() {
assert_eq!(map_block_tag(BlockTag::Finalized), BlockNumberOrTag::Finalized);
assert_eq!(map_block_tag(BlockTag::Safe), BlockNumberOrTag::Safe);
assert_eq!(map_block_tag(BlockTag::Latest), BlockNumberOrTag::Latest);
assert_eq!(map_block_tag(BlockTag::Earliest), BlockNumberOrTag::Earliest);
assert_eq!(map_block_tag(BlockTag::Pending), BlockNumberOrTag::Pending);
assert_eq!(map_block_tag(BlockTag::Number(123)), BlockNumberOrTag::Number(123));
}
}