#![expect(
clippy::unwrap_used,
clippy::panic,
clippy::print_stdout,
clippy::doc_markdown,
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::type_complexity
)]
use alloy::primitives::U256;
use degenbot_simulation::harness::{
assert_erc6909_capture, assert_profitable, v3_amount_out, Harness, Hop, HopPool,
};
#[derive(Clone, Copy, PartialEq)]
enum Prot {
V2,
V3,
V4,
}
const PROT_NAMES: [&str; 3] = ["v2", "v3", "v4"];
fn q96_one() -> U256 {
U256::from(1u128) << 96
}
fn sqrt_x(x: u64) -> U256 {
if x == 1 {
q96_one()
} else {
let s = ((x as f64).sqrt() * 65536.0) as u64;
q96_one() * U256::from(s) / U256::from(65536)
}
}
fn liq() -> u128 {
10u128.pow(22)
}
fn pool_for(
h: &mut Harness,
p: Prot,
src: alloy::primitives::Address,
dst: alloy::primitives::Address,
mult: u64,
) -> HopPool {
match p {
Prot::V2 => {
let r: u128 = 1_000_000_000_000;
HopPool::V2(h.add_pool(src, dst, r, r * u128::from(mult)).unwrap())
}
Prot::V3 => HopPool::V3(
h.add_v3_pool(
src,
dst,
3000,
sqrt_x(mult),
liq(),
1_000_000_000_000,
1_000_000_000_000,
)
.unwrap(),
),
Prot::V4 => HopPool::V4(
h.add_v4_pool(
src,
dst,
3000,
60,
sqrt_x(mult),
liq(),
1_000_000_000_000,
1_000_000_000_000,
)
.unwrap(),
),
}
}
fn build_family(h: &mut Harness, seq: &[Prot]) -> Vec<Hop> {
let n = seq.len();
let mut tokens = Vec::with_capacity(n - 1);
for _ in 0..n - 1 {
tokens.push(h.add_token().unwrap());
}
let mut hops = Vec::with_capacity(n);
for (i, p) in seq.iter().enumerate() {
let (src, dst) = if i == 0 {
(h.weth, tokens[0])
} else if i == n - 1 {
(tokens[n - 2], h.weth)
} else {
(tokens[i - 1], tokens[i])
};
let mult = if i == n - 1 { 3 } else { 1 };
hops.push(Hop {
src,
dst,
pool: pool_for(h, *p, src, dst, mult),
});
}
hops
}
fn family_list(n: usize) -> Vec<String> {
let mut cur: Vec<String> = vec![String::new()];
for _ in 0..n {
let mut next = Vec::new();
for prefix in &cur {
for name in PROT_NAMES {
next.push(if prefix.is_empty() {
name.to_string()
} else {
format!("{prefix}_{name}")
});
}
}
cur = next;
}
cur
}
struct Case {
name: String,
build: Box<dyn Fn(&mut Harness) -> Vec<Hop>>,
gas: u64,
swaps: usize,
}
fn cases() -> Vec<Case> {
let mut m = Vec::new();
for n in [2usize, 3] {
for fam in family_list(n) {
let names: Vec<&str> = fam.split('_').collect();
let seq: Vec<Prot> = names
.iter()
.map(|s| match *s {
"v2" => Prot::V2,
"v3" => Prot::V3,
_ => Prot::V4,
})
.collect();
let gas = if n == 2 { 8_000_000 } else { 40_000_000 };
m.push(Case {
name: fam,
build: Box::new(move |h| build_family(h, &seq)),
gas,
swaps: n,
});
}
}
m
}
#[test]
fn full_matrix_executes_with_expected_profit() {
for c in cases() {
let mut h = Harness::new().unwrap();
let hops = (c.build)(&mut h);
println!("── {:<14}", c.name);
let result = h
.run_chain(&hops, 100_000, c.gas)
.unwrap_or_else(|e| panic!("[{}] run_chain: {e}", c.name));
println!(
" outcome={:?} predicted={} actual={}",
result.outcome, result.predicted_profit, result.actual_weth_delta
);
assert_profitable(&result, c.swaps, &c.name);
}
}
#[test]
fn matrix_covers_full_reachable_grammar() {
let covered: std::collections::BTreeSet<String> = cases().into_iter().map(|c| c.name).collect();
let mut reachable: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
reachable.extend(family_list(2));
reachable.extend(family_list(3));
let missing: Vec<String> = reachable
.iter()
.filter(|n| !covered.contains(n.as_str()))
.cloned()
.collect();
assert!(
missing.is_empty(),
"reachable grammar families with no matrix row: {missing:?}"
);
let unexpected: Vec<String> = covered
.iter()
.filter(|&n| !reachable.contains(n))
.cloned()
.collect();
assert!(
unexpected.is_empty(),
"matrix rows that are not reachable grammar families: {unexpected:?}"
);
assert_eq!(
covered.len(),
reachable.len(),
"matrix ({}) must equal the full reachable grammar ({})",
covered.len(),
reachable.len()
);
}
#[test]
#[should_panic(expected = "payload must execute (reach 2 pools)")]
fn unprofitable_chain_is_rejected() {
let mut h = Harness::new().unwrap();
let u = h.add_token().unwrap();
let pa = h.add_pool(u, h.weth, 2_000_000, 1_000_000).unwrap();
let pb = h.add_pool(u, h.weth, 2_000_000, 1_000_000).unwrap();
let hops = vec![
Hop {
src: h.weth,
dst: u,
pool: HopPool::V2(pa),
},
Hop {
src: u,
dst: h.weth,
pool: HopPool::V2(pb),
},
];
let result = h
.run_chain_with_opts(
&hops,
100_000,
5_000_000,
degenbot_executor::composers::EncodeOptions {
funding: degenbot_executor::grammar_ledger::FundingSource::SelfFund,
..Default::default()
},
)
.unwrap();
assert!(
matches!(
result.outcome,
degenbot_simulation::harness::ExecOutcome::Reverted { .. }
),
"losing self-fund path must revert at the profit assert: {result:?}"
);
assert_profitable(&result, 2, "unprofitable");
}
#[test]
#[should_panic(expected = "expected a profitable (positive) WETH delta")]
fn unprofitable_chain_sweep_defeats_assert_but_delta_guard_fires() {
let mut h = Harness::new().unwrap();
let u = h.add_token().unwrap();
let pa = h.add_pool(u, h.weth, 2_000_000, 1_000_000).unwrap();
let pb = h.add_pool(u, h.weth, 2_000_000, 1_000_000).unwrap();
let hops = vec![
Hop {
src: h.weth,
dst: u,
pool: HopPool::V2(pa),
},
Hop {
src: u,
dst: h.weth,
pool: HopPool::V2(pb),
},
];
let opts = degenbot_executor::composers::EncodeOptions {
funding: degenbot_executor::grammar_ledger::FundingSource::SelfFund,
capture: degenbot_executor::grammar_ledger::ProfitCapture::SweepToAddress,
..Default::default()
};
let result = h
.run_chain_with_opts(&hops, 100_000, 5_000_000, opts)
.unwrap();
assert!(
result.outcome.executed(2),
"sweep defeats the on-chain assert; the loss path still executes: {result:?}"
);
assert!(
result.actual_weth_delta < 0,
"must actually lose: {result:?}"
);
assert_profitable(&result, 2, "unprofitable-sweep");
}
#[test]
fn harness_encode_context_projects_deployed_addresses() {
let h = Harness::new().unwrap();
let ctx = h.encode_context();
assert_eq!(
ctx,
degenbot_executor::composers::EncodeContext::new(h.executor, h.pool_manager, h.weth,),
"encode_context must project (executor, pool_manager, weth) from the deployed addresses"
);
let ctx2 = ctx;
assert_eq!(ctx, ctx2);
}
#[test]
fn cl_hop_aligned_clamp_shape_executes_with_consistent_delta() {
let mut h = Harness::new().unwrap();
let t1 = h.add_token().unwrap();
let r0: u128 = 1_000_000_000_000;
let v2 = h.add_pool(h.weth, t1, r0, r0).unwrap();
let v3 = h
.add_v3_pool(
t1,
h.weth,
3000,
sqrt_x(3),
liq(),
1_000_000_000_000,
1_000_000_000_000,
)
.unwrap();
let r_in = if v2.token0 == h.weth {
v2.reserve0
} else {
v2.reserve1
};
let r_out = if v2.token0 == h.weth {
v2.reserve1
} else {
v2.reserve0
};
let v2_out = |in_weth: u128| {
let amp = U256::from(in_weth) * U256::from(997u64);
let num = amp * U256::from(r_out);
let den = U256::from(r_in) * U256::from(1000u64) + amp;
(num / den).to::<u128>()
};
let natural_in: u128 = 100_000;
let natural_v2_out = v2_out(natural_in);
let clamped_v2_out = v2_out(natural_in * 70 / 100);
assert!(
clamped_v2_out < natural_v2_out,
"clamped V2 output must be below the natural output"
);
let v3_out = v3_amount_out(v3.sqrt_price, v3.liquidity, clamped_v2_out, true, 3000);
let hops = vec![
Hop {
src: h.weth,
dst: t1,
pool: HopPool::V2(v2),
},
Hop {
src: t1,
dst: h.weth,
pool: HopPool::V3(v3),
},
];
let result = h
.run_chain_with_consumed(
&hops,
natural_in * 70 / 100,
&[clamped_v2_out, v3_out],
&[natural_in * 70 / 100, clamped_v2_out],
8_000_000,
degenbot_executor::composers::EncodeOptions::default(),
)
.unwrap_or_else(|e| panic!("run_chain_with_consumed: {e}"));
assert!(
result.outcome.executed(2),
"aligned clamp shape must execute both pools: {result:?}"
);
assert_profitable(&result, 2, "aligned-clamp-shape");
}
#[test]
fn erc6909_capture_v4v4_lands_in_vault() {
let mut h = Harness::new().unwrap();
let t = h.add_token().unwrap();
let hops = vec![
Hop {
src: h.weth,
dst: t,
pool: HopPool::V4(
h.add_v4_pool(
h.weth,
t,
3000,
60,
sqrt_x(1),
liq(),
1_000_000_000_000,
1_000_000_000_000,
)
.unwrap(),
),
},
Hop {
src: t,
dst: h.weth,
pool: HopPool::V4(
h.add_v4_pool(
t,
h.weth,
3000,
60,
sqrt_x(3),
liq(),
1_000_000_000_000,
1_000_000_000_000,
)
.unwrap(),
),
},
];
assert_eq!(
h.pm_balance_of(h.executor, h.weth).unwrap(),
alloy::primitives::U256::ZERO,
"fixture starts from a zero ERC6909 position"
);
let opts = degenbot_executor::composers::EncodeOptions {
erc6909_profit: true,
..Default::default()
};
let result = h
.run_chain_with_opts(&hops, 100_000, 8_000_000, opts)
.unwrap_or_else(|e| panic!("run erc6909 capture: {e}"));
assert_erc6909_capture(&result, 2, "v4_v4 erc6909 capture");
}
#[test]
fn erc6909_capture_v4v4v4_lands_in_vault() {
let mut h = Harness::new().unwrap();
let t1 = h.add_token().unwrap();
let t2 = h.add_token().unwrap();
let hops = vec![
Hop {
src: h.weth,
dst: t1,
pool: HopPool::V4(
h.add_v4_pool(
h.weth,
t1,
3000,
60,
sqrt_x(1),
liq(),
1_000_000_000_000,
1_000_000_000_000,
)
.unwrap(),
),
},
Hop {
src: t1,
dst: t2,
pool: HopPool::V4(
h.add_v4_pool(
t1,
t2,
3000,
60,
sqrt_x(1),
liq(),
1_000_000_000_000,
1_000_000_000_000,
)
.unwrap(),
),
},
Hop {
src: t2,
dst: h.weth,
pool: HopPool::V4(
h.add_v4_pool(
t2,
h.weth,
3000,
60,
sqrt_x(3),
liq(),
1_000_000_000_000,
1_000_000_000_000,
)
.unwrap(),
),
},
];
let opts = degenbot_executor::composers::EncodeOptions {
erc6909_profit: true,
..Default::default()
};
let result = h
.run_chain_with_opts(&hops, 100_000, 40_000_000, opts)
.unwrap_or_else(|e| panic!("run erc6909 capture 3-hop: {e}"));
assert_erc6909_capture(&result, 3, "v4_v4_v4 erc6909 capture");
}
#[test]
fn erc6909_capture_with_batch_composes_via_open_weth_batch() {
let mut h = Harness::new().unwrap();
let t = h.add_token().unwrap();
let hops = vec![
Hop {
src: h.weth,
dst: t,
pool: HopPool::V4(
h.add_v4_pool(
h.weth,
t,
3000,
60,
sqrt_x(1),
liq(),
1_000_000_000_000,
1_000_000_000_000,
)
.unwrap(),
),
},
Hop {
src: t,
dst: h.weth,
pool: HopPool::V4(
h.add_v4_pool(
t,
h.weth,
3000,
60,
sqrt_x(3),
liq(),
1_000_000_000_000,
1_000_000_000_000,
)
.unwrap(),
),
},
];
let opts = degenbot_executor::composers::EncodeOptions {
erc6909_profit: true,
use_v4_batch: true,
..Default::default()
};
let result = h
.run_chain_with_opts(&hops, 100_000, 8_000_000, opts)
.unwrap_or_else(|e| panic!("batch + erc6909 must execute via 0x43 (TGUZCT): {e}"));
assert_erc6909_capture(&result, 2, "v4_v4 batch erc6909 capture (0x43)");
}