use crate::composers::{ComposerInputs, HopInfo, PathInfo, V2HopInfo, V3HopInfo, V4HopInfo};
use crate::encoders::AddressTable;
use crate::grammar_ledger::Prot;
use crate::grammar_plan::{v2_forward, v3_forward, v3_input, Plan};
use crate::grammar_shape::v4_hop_currencies;
use alloy::primitives::Address;
fn fits_i128(v: u128) -> bool {
v <= i128::MAX as u128
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum OutDest {
Executor,
PoolManager,
Repay(Address),
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Repay {
SelfRefund,
Offstream,
NetZero,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum TerminalForm {
DirectHandoff,
UnlockInternal,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum RepayMechanism {
AutoFromExecutor,
TransferInCallback,
V4TakeInUnlock,
DownstreamFlashDelivery,
DownstreamTakeSeeds,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SeedDelivery {
Erc20Transfer,
V4TakeCompact,
}
pub struct HopFacts {
pub prot: Prot,
pub zfo: bool,
pub swap_fee: u16,
pub tick_spacing: i16,
pub out_currency: Address,
pub in_currency: Address,
pub out_dest: OutDest,
pub repay: Repay,
pub pool_address: Address,
pub pool_id_hex: Option<String>,
pub currency0_address: Address,
pub currency1_address: Address,
pub terminal_form: Option<TerminalForm>,
pub repay_mechanism: Option<RepayMechanism>,
pub seed_delivery: Option<SeedDelivery>,
}
mod mechanics {
use super::{AddressTable, HopFacts, OutDest};
use crate::encoders::{SENTINEL_NATIVE, SENTINEL_PM, SENTINEL_SELF};
use crate::grammar_ledger::Prot;
use crate::grammar_plan::{Plan, PlanStep, V4BatchSwap};
use alloy::primitives::Address;
pub fn v3_flash(
at: &mut AddressTable,
facts: &HopFacts,
out_amount: u128,
in_amount: u128,
auto_repay: bool,
recipient: Option<(u8, Option<Address>, bool)>,
callback: Vec<PlanStep>,
) -> Option<PlanStep> {
let (recipient_idx, recipient_pool_addr, recipient_pool_repays) = match recipient {
Some(r) => r,
None => match facts.out_dest {
OutDest::Executor => (SENTINEL_SELF, None, false),
OutDest::PoolManager => (SENTINEL_PM, None, false),
OutDest::Repay(_) => unreachable!("V3 hop never repays a pool here"),
},
};
Some(PlanStep::FlashSwap {
pool_idx: at.add(facts.pool_address).ok()?,
pool_addr: facts.pool_address,
protocol: Prot::V3,
zfo: facts.zfo,
fee: facts.swap_fee,
out_currency: facts.out_currency,
out_amount,
in_currency: facts.in_currency,
in_amount,
recipient_idx,
recipient_pool_addr,
recipient_pool_repays,
auto_repay,
callback,
})
}
pub fn v2_swap(
at: &mut AddressTable,
facts: &HopFacts,
out_amount: u128,
recipient_idx: u8,
recipient_pool_addr: Option<Address>,
recipient_repays: bool,
) -> Option<PlanStep> {
Some(PlanStep::V2SwapCalc {
pool_idx: at.add(facts.pool_address).ok()?,
pool_addr: facts.pool_address,
zfo: facts.zfo,
recipient_idx,
fee: facts.swap_fee,
out_currency: facts.out_currency,
out_amount,
recipient_pool_addr,
recipient_repays,
})
}
pub fn v2_flash(
at: &mut AddressTable,
facts: &HopFacts,
out_amount: u128,
in_currency: Address,
in_amount: u128,
callback: Vec<PlanStep>,
) -> Option<PlanStep> {
let pool_idx = at.add(facts.pool_address).ok()?;
let (recipient_idx, recipient_pool_addr, recipient_pool_repays) = match facts.out_dest {
OutDest::Executor => (SENTINEL_SELF, None, false),
OutDest::PoolManager => (SENTINEL_PM, None, false),
OutDest::Repay(addr) => (at.add(addr).ok()?, Some(addr), false),
};
Some(PlanStep::FlashSwap {
pool_idx,
pool_addr: facts.pool_address,
protocol: Prot::V2,
zfo: facts.zfo,
fee: facts.swap_fee,
out_currency: facts.out_currency,
out_amount,
in_currency,
in_amount,
recipient_idx,
recipient_pool_addr,
recipient_pool_repays,
auto_repay: false,
callback,
})
}
pub fn v2_swap_direct(
at: &mut AddressTable,
facts: &HopFacts,
out_amount: u128,
out_currency: Address,
recipient_idx: u8,
recipient_pool_addr: Option<Address>,
recipient_repays: bool,
) -> Option<PlanStep> {
Some(PlanStep::V2SwapDirect {
pool_idx: at.add(facts.pool_address).ok()?,
pool_addr: facts.pool_address,
zfo: facts.zfo,
out_amount,
recipient_idx,
out_currency,
recipient_pool_addr,
recipient_repays,
})
}
pub fn v4_swap(
at: &mut AddressTable,
facts: &HopFacts,
amount_in: u128,
out_amount: u128,
) -> Option<PlanStep> {
Some(PlanStep::V4Swap {
c0_idx: at.add(facts.currency0_address).ok()?,
c1_idx: at.add(facts.currency1_address).ok()?,
fee: facts.swap_fee,
tick_spacing: facts.tick_spacing,
hooks_idx: SENTINEL_NATIVE,
zfo: facts.zfo,
amount: amount_in,
in_currency: facts.in_currency,
in_amount: amount_in,
out_currency: facts.out_currency,
out_amount,
})
}
pub fn v4_unlock(inner: Plan, pool_manager_idx: u8) -> PlanStep {
PlanStep::V4Unlock {
inner,
pool_manager_idx,
}
}
pub fn v4_take_compact(
at: &mut AddressTable,
facts: &HopFacts,
recipient_idx: u8,
amount: u128,
repays_flash: Option<Address>,
) -> Option<PlanStep> {
Some(PlanStep::V4TakeCompact {
currency_idx: at.add(facts.out_currency).ok()?,
currency_addr: facts.out_currency,
recipient_idx,
amount,
seeds_pool: None,
repays_flash,
})
}
pub fn v4_settle(currency_addr: Address, amount: u128) -> PlanStep {
PlanStep::V4Settle {
currency_addr,
amount,
}
}
pub fn v4_settle_all() -> PlanStep {
PlanStep::V4SettleAll
}
pub fn v4_sync(currency_idx: u8, currency_addr: Address) -> PlanStep {
PlanStep::V4Sync {
currency_idx,
currency_addr,
}
}
pub fn erc20_transfer(
token_idx: u8,
token_addr: Address,
recipient_idx: u8,
amount: u128,
seeds_pool: Option<Address>,
repays_flash: Option<Address>,
) -> PlanStep {
PlanStep::Erc20Transfer {
token_idx,
token_addr,
recipient_idx,
amount,
seeds_pool,
repays_flash,
}
}
pub fn native_transfer(amount: u128) -> PlanStep {
PlanStep::NativeTransfer { amount }
}
pub fn v4_settle_delta(currency_idx: u8, currency_addr: Address) -> PlanStep {
PlanStep::V4SettleDelta {
currency_idx,
currency_addr,
}
}
pub fn v4_take_delta(
currency_idx: u8,
currency_addr: Address,
recipient_idx: u8,
seeds_pool: Option<Address>,
) -> PlanStep {
PlanStep::V4TakeDelta {
currency_idx,
currency_addr,
recipient_idx,
seeds_pool,
}
}
pub fn v4_take_compact_at(
currency_idx: u8,
currency_addr: Address,
recipient_idx: u8,
amount: u128,
seeds_pool: Option<Address>,
repays_flash: Option<Address>,
) -> PlanStep {
PlanStep::V4TakeCompact {
currency_idx,
currency_addr,
recipient_idx,
amount,
seeds_pool,
repays_flash,
}
}
pub fn v4_batch_entry(
facts: &HopFacts,
c0_idx: u8,
c1_idx: u8,
amount: u128,
out_amount: u128,
in_currency: Address,
) -> V4BatchSwap {
V4BatchSwap {
c0_idx,
c1_idx,
fee: facts.swap_fee,
tick_spacing: facts.tick_spacing,
hooks_idx: SENTINEL_NATIVE,
zfo: facts.zfo,
amount,
in_currency,
in_amount: amount,
out_currency: facts.out_currency,
out_amount,
}
}
pub fn weth_deposit(weth_idx: u8, weth_addr: Address, amount: u128) -> PlanStep {
PlanStep::WethDeposit {
weth_idx,
weth_addr,
amount,
}
}
pub fn weth_withdraw(weth_idx: u8, weth_addr: Address, amount: u128) -> PlanStep {
PlanStep::WethWithdraw {
weth_idx,
weth_addr,
amount,
}
}
pub fn self_fund(currency: Address, amount: u128) -> PlanStep {
PlanStep::SelfFund { currency, amount }
}
pub fn v4_batch(
at: &mut AddressTable,
facts: &HopFacts,
entries: Vec<V4BatchSwap>,
open_weth: bool,
) -> PlanStep {
let _ = (at, facts);
PlanStep::V4Batch { entries, open_weth }
}
}
mod shapes;
fn v3_hop_facts_strict(h: &V3HopInfo) -> Option<HopFacts> {
let mut f = v3_hop_facts(h);
f.swap_fee = u16::try_from(h.fee).ok()?;
Some(f)
}
fn closing_hop(mut f: HopFacts, forward: Address, weth: Address) -> HopFacts {
f.out_currency = weth;
f.in_currency = forward;
f.repay = Repay::Offstream;
f
}
pub(crate) fn facts_of_v3v4v3(
path: &PathInfo,
_inputs: &ComposerInputs<'_>,
) -> Option<Vec<HopFacts>> {
let (HopInfo::V3(a), HopInfo::V4(b), HopInfo::V3(c)) =
(&path.hops[0], &path.hops[1], &path.hops[2])
else {
return None;
};
let mut fa = v3_hop_facts_strict(a)?;
fa.out_dest = OutDest::PoolManager; let mut fb = v4_hop_facts_netzero(b);
fb.swap_fee = u16::try_from(b.fee).ok()?;
fb.tick_spacing = i16::try_from(b.tick_spacing).ok()?;
fb.out_dest = OutDest::Repay(c.pool_address); let mut fc = v3_hop_facts_strict(c)?;
fc.repay = Repay::Offstream; Some(vec![fa, fb, fc])
}
pub(crate) fn facts_of_v2v3(path: &PathInfo, inputs: &ComposerInputs<'_>) -> Option<Vec<HopFacts>> {
if path.hops.len() != 2 {
return None;
}
let (HopInfo::V2(a), HopInfo::V3(b)) = (&path.hops[0], &path.hops[1]) else {
return None;
};
let terminal = closing_hop(v3_hop_facts_strict(b)?, v2_forward(a), inputs.weth_address);
Some(vec![v2_hop_facts(a), terminal])
}
pub(crate) fn facts_of_all_v2(
path: &PathInfo,
_inputs: &ComposerInputs<'_>,
) -> Option<Vec<HopFacts>> {
let v2s = path
.hops
.iter()
.map(|h| match h {
HopInfo::V2(h) => Some(v2_hop_facts(h)),
_ => None,
})
.collect::<Option<Vec<_>>>()?;
if v2s.len() < 2 {
return None;
}
Some(v2s)
}
pub(crate) fn facts_of_v3v3(path: &PathInfo, inputs: &ComposerInputs<'_>) -> Option<Vec<HopFacts>> {
if path.hops.len() != 2 {
return None;
}
let (HopInfo::V3(a), HopInfo::V3(b)) = (&path.hops[0], &path.hops[1]) else {
return None;
};
let lead = v3_hop_facts_strict(a)?;
let terminal = closing_hop(v3_hop_facts_strict(b)?, v3_forward(a), inputs.weth_address);
Some(vec![lead, terminal])
}
pub(crate) fn facts_of_v3v2(path: &PathInfo, inputs: &ComposerInputs<'_>) -> Option<Vec<HopFacts>> {
if path.hops.len() != 2 {
return None;
}
let (HopInfo::V3(a), HopInfo::V2(b)) = (&path.hops[0], &path.hops[1]) else {
return None;
};
let lead = v3_hop_facts_strict(a)?;
let terminal = closing_hop(v2_hop_facts(b), v3_forward(a), inputs.weth_address);
Some(vec![lead, terminal])
}
pub(crate) fn v3_hop_facts(h: &V3HopInfo) -> HopFacts {
HopFacts {
prot: Prot::V3,
zfo: h.zfo,
swap_fee: u16::try_from(h.fee).unwrap_or(0),
tick_spacing: 0,
out_currency: v3_forward(h),
in_currency: v3_input(h),
out_dest: OutDest::Executor,
repay: Repay::SelfRefund,
pool_address: h.pool_address,
pool_id_hex: None,
terminal_form: None,
repay_mechanism: None,
seed_delivery: None,
currency0_address: h.token0_address,
currency1_address: h.token1_address,
}
}
pub(crate) fn v2_hop_facts(h: &V2HopInfo) -> HopFacts {
HopFacts {
prot: Prot::V2,
zfo: h.zfo,
swap_fee: h.fee,
tick_spacing: 0,
out_currency: v2_forward(h),
in_currency: if h.zfo {
h.token0_address
} else {
h.token1_address
},
out_dest: OutDest::Executor,
repay: Repay::SelfRefund,
pool_address: h.pool_address,
pool_id_hex: None,
terminal_form: None,
repay_mechanism: None,
seed_delivery: None,
currency0_address: h.token0_address,
currency1_address: h.token1_address,
}
}
pub(crate) fn v4_hop_facts(h: &V4HopInfo) -> HopFacts {
let (fwd, inv) = v4_hop_currencies(h);
HopFacts {
prot: Prot::V4,
zfo: h.zfo,
swap_fee: u16::try_from(h.fee).unwrap_or(0),
tick_spacing: i16::try_from(h.tick_spacing).unwrap_or(0),
out_currency: fwd,
in_currency: inv,
out_dest: OutDest::Executor,
repay: Repay::Offstream,
pool_address: h.pool_manager_address,
pool_id_hex: Some(h.pool_id_hex.clone()),
terminal_form: None,
repay_mechanism: None,
seed_delivery: None,
currency0_address: h.currency0_address,
currency1_address: h.currency1_address,
}
}
pub(crate) fn v4_hop_facts_netzero(h: &V4HopInfo) -> HopFacts {
let mut f = v4_hop_facts(h);
f.repay = Repay::NetZero;
f
}
fn hop_facts(h: &HopInfo) -> HopFacts {
match h {
HopInfo::V2(a) => v2_hop_facts(a),
HopInfo::V3(a) => v3_hop_facts(a),
HopInfo::V4(a) => v4_hop_facts_netzero(a),
}
}
pub(crate) fn recognized_key(key: (Option<Prot>, Option<Prot>, Option<Prot>)) -> bool {
matches!(key, (Some(_), Some(_), _))
}
fn hop_prot(h: &HopInfo) -> Prot {
match h {
HopInfo::V2(_) => Prot::V2,
HopInfo::V3(_) => Prot::V3,
HopInfo::V4(_) => Prot::V4,
}
}
pub(crate) fn facts_for(path: &PathInfo, inputs: &ComposerInputs<'_>) -> Option<Vec<HopFacts>> {
let n = path.hops.len();
let prots: Vec<Prot> = path.hops.iter().map(hop_prot).collect();
if n >= 2 && prots.iter().all(|p| *p == Prot::V2) {
return facts_of_all_v2(path, inputs);
}
let mut facts = match prots.as_slice() {
[Prot::V3, Prot::V4, Prot::V3] => facts_of_v3v4v3(path, inputs)?,
[Prot::V2, Prot::V3] => facts_of_v2v3(path, inputs)?,
[Prot::V3, Prot::V3] => facts_of_v3v3(path, inputs)?,
[Prot::V3, Prot::V2] => facts_of_v3v2(path, inputs)?,
_ if (2..=3).contains(&n) => path.hops.iter().map(hop_facts).collect(),
_ => return None,
};
if prots.len() == 3 && prots[0] == Prot::V3 && prots[1] == Prot::V4 {
let form = match prots[2] {
Prot::V2 => Some(TerminalForm::DirectHandoff),
Prot::V4 => Some(TerminalForm::UnlockInternal),
Prot::V3 => None, };
if let Some(form) = form {
facts[2].terminal_form = Some(form);
}
}
if prots == [Prot::V3, Prot::V2, Prot::V4] {
facts[1].repay_mechanism = Some(RepayMechanism::AutoFromExecutor);
}
if prots == [Prot::V2, Prot::V3, Prot::V4] {
facts[0].seed_delivery = Some(SeedDelivery::V4TakeCompact);
}
Some(facts)
}
#[must_use]
pub fn build_walk(
path: &PathInfo,
inputs: &ComposerInputs<'_>,
) -> Option<(Vec<u8>, Plan, AddressTable)> {
let facts = facts_for(path, inputs)?;
let (plan, at) = derive_plan(&facts, inputs)?;
let preamble = crate::encoders::enc_preamble(&at);
Some((preamble, plan, at))
}
#[must_use]
pub(crate) fn derive_plan(
facts: &[HopFacts],
inputs: &ComposerInputs<'_>,
) -> Option<(Plan, AddressTable)> {
if facts.len() >= 2
&& facts
.iter()
.all(|f| f.repay != Repay::NetZero && f.prot == Prot::V2)
{
return shapes::all_v2_chain::derive(facts, inputs);
}
if facts.len() == 2 && facts[0].repay == Repay::SelfRefund && facts[1].repay == Repay::NetZero {
return shapes::two_hop_seed_v4::derive(facts, inputs);
}
if facts.len() == 2 && facts[0].repay == Repay::NetZero {
return shapes::two_hop_v4_led::derive(facts, inputs);
}
if facts.len() == 3 && !facts.iter().any(|f| f.repay == Repay::Offstream) {
return shapes::three_hop::derive(facts, inputs);
}
if facts.iter().all(|f| f.repay != Repay::NetZero) && facts.len() == 2 {
return shapes::two_hop_uniswap_only::derive(facts, inputs);
}
shapes::tag_residual::derive(facts, inputs)
}
#[cfg(test)]
mod tests {
#![expect(clippy::cast_possible_truncation, clippy::panic)]
use super::*;
use crate::composers::{
ComposerInputs, EncodeOptions, PathInfo, V2HopInfo, V3HopInfo, V4HopInfo,
};
use alloy::primitives::{address, Address};
const WETH: Address = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
const USDC: Address = address!("A0b86991c6218b36c1D19D4a2e9Eb0cE3606eB48");
const WBTC: Address = address!("2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599");
const PM: Address = address!("000000000004444c5dc75cB358380D2e3dE08A90");
const EXEC: Address = address!("DeAd0000000000000000000000000000000000Be");
const OPTIMAL: u128 = 1_000_000_000_000_000_000;
static OUTS: [u128; 3] = [1_000_000_000_000_000_000; 3];
static CONSUMED: [u128; 3] = [999_999_999_999_999_999; 3];
fn combo_hops(prots: &[Prot]) -> Vec<HopInfo> {
(0..prots.len())
.map(|i| {
let in_t = match i % 3 {
0 => WETH,
1 => USDC,
_ => WBTC,
};
let out_t = match (i + 1) % 3 {
0 => WETH,
1 => USDC,
_ => WBTC,
};
match prots[i] {
Prot::V2 => HopInfo::V2(V2HopInfo {
pool_address: Address::from([0xA0 + i as u8; 20]),
token0_address: in_t,
token1_address: out_t,
fee: 30,
zfo: true,
}),
Prot::V3 => HopInfo::V3(V3HopInfo {
pool_address: Address::from([0xB0 + i as u8; 20]),
token0_address: in_t,
token1_address: out_t,
fee: 3000,
zfo: true,
}),
Prot::V4 => HopInfo::V4(V4HopInfo {
pool_manager_address: PM,
pool_id_hex: format!("0x{i:02x}"),
currency0_address: in_t,
currency1_address: out_t,
fee: 500,
tick_spacing: 10,
hook_address: Address::ZERO,
zfo: true,
}),
}
})
.collect()
}
fn family_name(prots: &[Prot]) -> String {
prots
.iter()
.map(|p| match p {
Prot::V2 => "v2",
Prot::V3 => "v3",
Prot::V4 => "v4",
})
.collect()
}
#[test]
fn d6_enclosure_derived_from_facts() {
let fams = [Prot::V2, Prot::V3, Prot::V4];
let mut netzero_missing: Vec<String> = Vec::new();
for n in [2usize, 3] {
for fidx in 0..fams.len().pow(n as u32) {
let prots: Vec<Prot> = (0..n)
.map(|i| fams[(fidx / fams.len().pow(i as u32)) % fams.len()])
.collect();
if prots == vec![Prot::V2, Prot::V2] {
continue;
}
let name = family_name(&prots);
let path = PathInfo::new(combo_hops(&prots));
let inputs = ComposerInputs {
executor_address: EXEC,
pool_manager_address: PM,
weth_address: WETH,
optimal_input: OPTIMAL,
hop_outputs: &OUTS[..n],
consumed_inputs: &CONSUMED[..n],
opts: EncodeOptions::default(),
};
let Some(tags) = facts_for(&path, &inputs)
.map(|fs| fs.iter().map(|f| f.repay).collect::<Vec<_>>())
else {
continue;
};
let has_v4 = prots.contains(&Prot::V4);
let v4_has_netzero = prots
.iter()
.zip(tags.iter())
.any(|(p, r)| *p == Prot::V4 && *r == Repay::NetZero);
if has_v4 && !v4_has_netzero {
let tag_strs: Vec<String> = tags.iter().map(|r| format!("{r:?}")).collect();
netzero_missing.push(format!("{name} (tags=[{}])", tag_strs.join(", ")));
}
}
}
assert!(
netzero_missing.is_empty(),
"D6 violation — families whose V4 hops lack Repay::NetZero (the tag \
the residual tag partition routes on):\n {}",
netzero_missing.join("\n ")
);
}
#[test]
fn terminal_form_routes_the_v3v4_pair() {
let v2 = combo_hops(&[Prot::V3, Prot::V4, Prot::V2]);
let v4 = combo_hops(&[Prot::V3, Prot::V4, Prot::V4]);
let inputs = ComposerInputs {
executor_address: EXEC,
pool_manager_address: PM,
weth_address: WETH,
optimal_input: OPTIMAL,
hop_outputs: &OUTS,
consumed_inputs: &CONSUMED,
opts: EncodeOptions::default(),
};
for (hops, expect) in [(v2, "DirectHandoff"), (v4, "UnlockInternal")] {
let Some(facts) = facts_for(&PathInfo::new(hops), &inputs) else {
panic!("facts exist");
};
assert_eq!(facts.len(), 3);
assert!(facts[0].terminal_form.is_none());
assert!(facts[1].terminal_form.is_none());
let Some(tf) = facts[2].terminal_form else {
panic!("terminal_form set on terminal");
};
let kind = format!("{tf:?}");
assert!(kind.contains(expect), "got {kind}, want {expect}");
}
}
}