#![expect(clippy::cast_possible_wrap)]
use alloy::primitives::{Address, U256};
use degenbot_executor::composers::{HopInfo, PathInfo, V2HopInfo, V3HopInfo, V4HopInfo};
use super::{v3_amount_out, ExecOutcome, Harness, V2Pool, V3Pool, V4Pool};
#[derive(Debug, Clone, Copy)]
pub struct Hop {
pub src: Address,
pub dst: Address,
pub pool: HopPool,
}
#[derive(Debug, Clone, Copy)]
pub enum HopPool {
V2(V2Pool),
V3(V3Pool),
V4(V4Pool),
}
impl HopPool {
fn zfo(&self, src: Address) -> bool {
match self {
HopPool::V2(p) => src == p.token0,
HopPool::V3(p) => src == p.token0,
HopPool::V4(p) => src == p.currency0,
}
}
fn v2_amt_out(r_in: u128, r_out: u128, input_in: u128) -> u128 {
let amp = U256::from(input_in) * U256::from(997u64);
let num = amp * U256::from(r_out);
let den = U256::from(r_in) * U256::from(1000u64) + amp;
(num / den).to::<u128>()
}
fn amount_out(&self, src: Address, dst: Address, input_in: u128) -> u128 {
match self {
HopPool::V2(p) => {
let r_in = if src == p.token0 {
p.reserve0
} else {
p.reserve1
};
let r_out = if dst == p.token0 {
p.reserve0
} else {
p.reserve1
};
Self::v2_amt_out(r_in, r_out, input_in)
}
HopPool::V3(p) => {
v3_amount_out(p.sqrt_price, p.liquidity, input_in, self.zfo(src), p.fee)
}
HopPool::V4(p) => {
v3_amount_out(p.sqrt_price, p.liquidity, input_in, self.zfo(src), p.fee)
}
}
}
fn to_hop_info(self, pool_manager: Address, src: Address) -> HopInfo {
match self {
HopPool::V2(p) => HopInfo::V2(V2HopInfo {
pool_address: p.pair,
token0_address: p.token0,
token1_address: p.token1,
fee: 30,
zfo: self.zfo(src),
}),
HopPool::V3(p) => HopInfo::V3(V3HopInfo {
pool_address: p.pool,
token0_address: p.token0,
token1_address: p.token1,
fee: p.fee,
zfo: self.zfo(src),
}),
HopPool::V4(p) => HopInfo::V4(V4HopInfo {
pool_manager_address: pool_manager,
pool_id_hex: "0x0".into(),
currency0_address: p.currency0,
currency1_address: p.currency1,
fee: p.fee,
tick_spacing: p.tick_spacing,
hook_address: Address::ZERO,
zfo: self.zfo(src),
}),
}
}
}
#[derive(Debug)]
pub struct ChainResult {
pub outcome: ExecOutcome,
pub hop_outputs: Vec<u128>,
pub predicted_profit: i128,
pub actual_weth_delta: i128,
pub erc6909_delta: i128,
}
impl Harness {
pub fn run_chain(
&mut self,
hops: &[Hop],
optimal_input: u128,
gas: u64,
) -> Result<ChainResult, String> {
self.run_chain_with_opts(
hops,
optimal_input,
gas,
degenbot_executor::composers::EncodeOptions::default(),
)
}
pub fn run_chain_with_opts(
&mut self,
hops: &[Hop],
optimal_input: u128,
gas: u64,
opts: degenbot_executor::composers::EncodeOptions,
) -> Result<ChainResult, String> {
let n = hops.len();
if n < 1 {
return Err("run_chain needs >=1 hops".to_string());
}
let mut hop_outputs = Vec::with_capacity(n);
let mut consumed = optimal_input;
for hop in hops {
let out = hop.pool.amount_out(hop.src, hop.dst, consumed);
hop_outputs.push(out);
consumed = out;
}
let out_terminal = consumed;
let predicted_profit = out_terminal as i128 - optimal_input as i128;
let path_hops: Vec<HopInfo> = hops
.iter()
.map(|hop| hop.pool.to_hop_info(self.pool_manager, hop.src))
.collect();
let path = PathInfo::new(path_hops);
self.fund(self.weth, self.executor, optimal_input * 2)?;
let mut funded = vec![self.weth];
for (i, hop) in hops.iter().enumerate().skip(1) {
if hop.src != self.weth && !funded.contains(&hop.src) {
self.fund(hop.src, self.executor, hop_outputs[i - 1] * 2)?;
funded.push(hop.src);
}
}
for hop in hops {
if let HopPool::V2(p) = &hop.pool {
self.executor_approve_pair(*p)?;
}
}
let before = self.balance_of(self.weth, self.executor)?.to::<u128>();
let erc6909_before = self.pm_balance_of(self.executor, self.weth)?.to::<u128>();
let outcome = self.run_path_with_opts(&path, optimal_input, &hop_outputs, gas, opts)?;
let after = self.balance_of(self.weth, self.executor)?.to::<u128>();
let erc6909_after = self.pm_balance_of(self.executor, self.weth)?.to::<u128>();
let actual_weth_delta = after as i128 - before as i128;
let erc6909_delta = erc6909_after as i128 - erc6909_before as i128;
Ok(ChainResult {
outcome,
hop_outputs,
predicted_profit,
actual_weth_delta,
erc6909_delta,
})
}
pub fn run_chain_with_consumed(
&mut self,
hops: &[Hop],
optimal_input: u128,
hop_outputs: &[u128],
consumed_inputs: &[u128],
gas: u64,
opts: degenbot_executor::composers::EncodeOptions,
) -> Result<ChainResult, String> {
let n = hops.len();
if n < 1 {
return Err("run_chain_with_consumed needs >=1 hops".to_string());
}
if hop_outputs.len() != n || consumed_inputs.len() != n {
return Err(format!(
"run_chain_with_consumed: per-hop arrays need {n} entries ({} outputs, {} consumed)",
hop_outputs.len(),
consumed_inputs.len()
));
}
let out_terminal = *hop_outputs.last().expect("validated non-empty");
let predicted_profit = out_terminal as i128 - optimal_input as i128;
let path_hops: Vec<HopInfo> = hops
.iter()
.map(|hop| hop.pool.to_hop_info(self.pool_manager, hop.src))
.collect();
let path = PathInfo::new(path_hops);
let entry_buffer = optimal_input.max(consumed_inputs[0]);
self.fund(self.weth, self.executor, entry_buffer * 2)?;
let mut funded = vec![self.weth];
for (i, hop) in hops.iter().enumerate().skip(1) {
if hop.src != self.weth && !funded.contains(&hop.src) {
let buf = hop_outputs[i - 1].max(consumed_inputs[i]) * 2;
self.fund(hop.src, self.executor, buf)?;
funded.push(hop.src);
}
}
for hop in hops {
if let HopPool::V2(p) = &hop.pool {
self.executor_approve_pair(*p)?;
}
}
let before = self.balance_of(self.weth, self.executor)?.to::<u128>();
let erc6909_before = self.pm_balance_of(self.executor, self.weth)?.to::<u128>();
let outcome = self.run_path_with_consumed(
&path,
optimal_input,
hop_outputs,
consumed_inputs,
gas,
opts,
)?;
let after = self.balance_of(self.weth, self.executor)?.to::<u128>();
let erc6909_after = self.pm_balance_of(self.executor, self.weth)?.to::<u128>();
let actual_weth_delta = after as i128 - before as i128;
let erc6909_delta = erc6909_after as i128 - erc6909_before as i128;
Ok(ChainResult {
outcome,
hop_outputs: hop_outputs.to_vec(),
predicted_profit,
actual_weth_delta,
erc6909_delta,
})
}
pub fn path_and_amounts(
&mut self,
hops: &[Hop],
optimal_input: u128,
) -> (PathInfo, Vec<u128>, Vec<u128>) {
let n = hops.len();
let mut hop_outputs = Vec::with_capacity(n);
let mut consumed = optimal_input;
for hop in hops {
let out = hop.pool.amount_out(hop.src, hop.dst, consumed);
hop_outputs.push(out);
consumed = out;
}
let path_hops: Vec<HopInfo> = hops
.iter()
.map(|hop| hop.pool.to_hop_info(self.pool_manager, hop.src))
.collect();
let consumed_inputs: Vec<u128> = std::iter::once(optimal_input)
.chain(hop_outputs.iter().copied())
.take(n)
.collect();
(PathInfo::new(path_hops), hop_outputs, consumed_inputs)
}
pub fn run_raw_payload(
&mut self,
hops: &[Hop],
payload: &[u8],
optimal_input: u128,
gas: u64,
) -> Result<ChainResult, String> {
let n = hops.len();
if n < 1 {
return Err("run_raw_payload needs >=1 hops".to_string());
}
let mut hop_outputs = Vec::with_capacity(n);
let mut consumed = optimal_input;
for hop in hops {
let out = hop.pool.amount_out(hop.src, hop.dst, consumed);
hop_outputs.push(out);
consumed = out;
}
let out_terminal = consumed;
let predicted_profit = out_terminal as i128 - optimal_input as i128;
self.fund(self.weth, self.executor, optimal_input * 2)?;
let mut funded = vec![self.weth];
for (i, hop) in hops.iter().enumerate().skip(1) {
if hop.src != self.weth && !funded.contains(&hop.src) {
self.fund(hop.src, self.executor, hop_outputs[i - 1] * 2)?;
funded.push(hop.src);
}
}
for hop in hops {
if let HopPool::V2(p) = &hop.pool {
self.executor_approve_pair(*p)?;
}
}
let before = self.balance_of(self.weth, self.executor)?.to::<u128>();
let erc6909_before = self.pm_balance_of(self.executor, self.weth)?.to::<u128>();
let outcome = self.execute_payload(payload, gas)?;
let after = self.balance_of(self.weth, self.executor)?.to::<u128>();
let erc6909_after = self.pm_balance_of(self.executor, self.weth)?.to::<u128>();
let actual_weth_delta = after as i128 - before as i128;
let erc6909_delta = erc6909_after as i128 - erc6909_before as i128;
Ok(ChainResult {
outcome,
hop_outputs,
predicted_profit,
actual_weth_delta,
erc6909_delta,
})
}
}
#[track_caller]
pub fn assert_profitable(result: &ChainResult, expected_swaps: usize, label: &str) {
assert!(
result.outcome.executed(expected_swaps),
"[{label}] payload must execute (reach {expected_swaps} pools): {:?}",
result.outcome
);
assert!(
result.actual_weth_delta > 0,
"[{label}] expected a profitable (positive) WETH delta, got {result:?}"
);
let tol = (result.predicted_profit.abs() / 1000).max(64);
assert!(
(result.actual_weth_delta - result.predicted_profit).abs() <= tol,
"[{label}] measured WETH delta {} diverges from predicted {} (tol {}): {:?}",
result.actual_weth_delta,
result.predicted_profit,
tol,
result
);
}
#[track_caller]
pub fn assert_erc6909_capture(result: &ChainResult, expected_swaps: usize, label: &str) {
assert!(
result.outcome.executed(expected_swaps),
"[{label}] payload must execute (reach {expected_swaps} pools): {:?}",
result.outcome
);
assert!(
result.erc6909_delta > 0,
"[{label}] expected an ERC6909 vault capture (positive PM balance delta), got {result:?}"
);
let tol = (result.predicted_profit.abs() / 1000).max(64);
assert!(
(result.erc6909_delta - result.predicted_profit).abs() <= tol,
"[{label}] ERC6909 delta {} diverges from predicted {} (tol {}): {:?}",
result.erc6909_delta,
result.predicted_profit,
tol,
result
);
assert!(
result.actual_weth_delta <= tol,
"[{label}] custody WETH delta {} must not also carry the profit (tol {}): {:?}",
result.actual_weth_delta,
tol,
result
);
}