use alloy::primitives::Address;
use crate::composers::{ComposerInputs, HopInfo, PathInfo, V4HopInfo, NATIVE_CURRENCY_ADDRESS};
use crate::encoders::{AddressTable, SENTINEL_NATIVE, SENTINEL_SELF, SENTINEL_WETH};
pub use crate::grammar_plan::{
plan_to_bytes, plan_to_ledger_ops, Axis, AxisSupport, Bribe, FundingSource, Plan, PlanStep,
ProfitCapture, Prot, ShapeClass, V4BatchSwap,
};
pub(crate) fn v4_scaffold_table(inputs: &ComposerInputs<'_>) -> AddressTable {
AddressTable::with_sentinels(
Some(inputs.weth_address),
Some(inputs.executor_address),
Some(inputs.pool_manager_address),
)
}
pub(crate) fn v4_hop_currencies(h: &V4HopInfo) -> (Address, Address) {
if h.zfo {
(h.currency1_address, h.currency0_address)
} else {
(h.currency0_address, h.currency1_address)
}
}
pub(crate) fn native_capture_declines(
capture: ProfitCapture,
terminal: Address,
weth: Address,
) -> bool {
capture == ProfitCapture::Native && terminal != weth && terminal != NATIVE_CURRENCY_ADDRESS
}
pub(crate) fn v4_terminal_capture_steps(
terminal: Address,
terminal_idx: u8,
capture: ProfitCapture,
use_v4_batch: bool,
any_gap: bool,
profit: u128,
weth: Address,
) -> Vec<PlanStep> {
let mut steps = Vec::new();
if capture == ProfitCapture::Erc6909 && terminal == weth {
if profit > 0 {
steps.push(PlanStep::V4Mint {
currency_idx: SENTINEL_WETH,
currency_addr: weth,
recipient_idx: SENTINEL_SELF,
amount: profit,
});
}
} else if !use_v4_batch || any_gap {
steps.push(PlanStep::V4TakeDelta {
currency_idx: terminal_idx,
currency_addr: terminal,
recipient_idx: SENTINEL_SELF,
seeds_pool: None,
});
}
if capture == ProfitCapture::Native && terminal == weth {
steps.push(PlanStep::WethWithdraw {
weth_idx: SENTINEL_WETH,
weth_addr: weth,
amount: profit,
});
}
steps
}
pub(crate) fn v4_bridge_steps(
bridge: crate::composers::CurrencyBridge,
weth: Address,
amount: u128,
) -> (Vec<PlanStep>, u8, Address) {
let wrap = matches!(bridge, crate::composers::CurrencyBridge::Wrap);
let take_currency = if wrap { NATIVE_CURRENCY_ADDRESS } else { weth };
let settle_currency = if wrap { weth } else { NATIVE_CURRENCY_ADDRESS };
let take_idx = if wrap { SENTINEL_NATIVE } else { SENTINEL_WETH };
let settle_idx = if wrap { SENTINEL_WETH } else { SENTINEL_NATIVE };
let convert = if wrap {
PlanStep::WethDeposit {
weth_idx: SENTINEL_WETH,
weth_addr: weth,
amount,
}
} else {
PlanStep::WethWithdraw {
weth_idx: SENTINEL_WETH,
weth_addr: weth,
amount,
}
};
(
vec![
PlanStep::V4TakeCompact {
currency_idx: take_idx,
currency_addr: take_currency,
recipient_idx: SENTINEL_SELF,
amount,
seeds_pool: None,
repays_flash: None,
},
convert,
],
settle_idx,
settle_currency,
)
}
#[cfg(test)]
pub(crate) type BuildPlan =
fn(&PathInfo, &ComposerInputs<'_>) -> Option<(Vec<u8>, Plan, AddressTable)>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Derive {
Encoded(Vec<u8>),
Declined,
Rejected(crate::grammar_ledger::ValidationError),
}
#[must_use]
#[expect(
clippy::panic,
reason = "ADR-030: a validator Reject is intentionally fatal — it must never be swallowed into a None"
)]
pub(crate) fn derive_option(d: Derive) -> Option<Vec<u8>> {
match d {
Derive::Encoded(bytes) => Some(bytes),
Derive::Declined => None,
Derive::Rejected(e) => panic!(
"executor derivation REJECT (a latent bug, ADR-030): the encoder wrote a Plan the ledger validator rejected: {e:?}"
),
}
}
#[cfg(test)]
#[must_use]
fn build_plan_bytes(path: &PathInfo, build: BuildPlan, inputs: &ComposerInputs<'_>) -> Derive {
let Some((preamble, plan, at)) = build(path, inputs) else {
return Derive::Declined;
};
let ops = plan_to_ledger_ops(&plan);
let mut v = crate::grammar_ledger::LedgerValidator::default();
if let Err(e) = v.validate_full(&ops) {
return Derive::Rejected(e);
}
let mut out = preamble;
out.extend_from_slice(&plan_to_bytes(&plan, &at));
Derive::Encoded(out)
}
#[must_use]
pub fn derive_all_v2(path: &PathInfo, inputs: &ComposerInputs<'_>) -> Option<Vec<u8>> {
derive_option(derive_all_v2_detailed(path, inputs))
}
#[must_use]
pub fn derive_all_v2_detailed(path: &PathInfo, inputs: &ComposerInputs<'_>) -> Derive {
derive_shape_detailed(path, inputs)
}
#[must_use]
fn prot_of(h: Option<&HopInfo>) -> Option<Prot> {
match h {
Some(HopInfo::V2(_)) => Some(Prot::V2),
Some(HopInfo::V3(_)) => Some(Prot::V3),
Some(HopInfo::V4(_)) => Some(Prot::V4),
None => None,
}
}
#[must_use]
pub fn derive_shape(path: &PathInfo, inputs: &ComposerInputs<'_>) -> Option<Vec<u8>> {
derive_option(derive_shape_detailed(path, inputs))
}
#[must_use]
pub fn derive_shape_detailed(path: &PathInfo, inputs: &ComposerInputs<'_>) -> Derive {
let key = (
prot_of(path.hops.first()),
prot_of(path.hops.get(1)),
prot_of(path.hops.get(2)),
);
if !crate::grammar_walker::recognized_key(key) {
return Derive::Declined;
}
let Some((preamble, plan, at)) = crate::grammar_walker::build_walk(path, inputs) else {
return Derive::Declined;
};
let ops = plan_to_ledger_ops(&plan);
let mut v = crate::grammar_ledger::LedgerValidator::default();
if let Err(e) = v.validate_full(&ops) {
return Derive::Rejected(e);
}
let mut out = preamble;
out.extend_from_slice(&plan_to_bytes(&plan, &at));
Derive::Encoded(out)
}
#[must_use]
pub fn family_axis_support(path: &PathInfo) -> Option<AxisSupport> {
let key = (
prot_of(path.hops.first()),
prot_of(path.hops.get(1)),
prot_of(path.hops.get(2)),
);
if !crate::grammar_walker::recognized_key(key) {
return None;
}
let capture = matches!(key, (Some(Prot::V4), Some(Prot::V4), Some(Prot::V4) | None));
let funding = matches!(
key,
(Some(Prot::V2), Some(Prot::V3), None)
| (Some(Prot::V2), Some(Prot::V2), Some(Prot::V2) | None)
);
Some(AxisSupport {
funding,
capture,
bribe: false,
})
}
#[cfg(test)]
mod tests {
#![expect(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::items_after_statements,
clippy::type_complexity,
clippy::naive_bytecount,
clippy::similar_names
)]
use super::*;
use crate::composers::{EncodeOptions, V2HopInfo, V3HopInfo};
use crate::encoders::enc_preamble;
use crate::grammar_walker::build_walk;
use alloy::primitives::{address, Address, U256};
fn build_all_v2_chain(
path: &PathInfo,
inputs: &ComposerInputs<'_>,
) -> Option<(Vec<u8>, Plan, AddressTable)> {
build_walk(path, inputs)
}
fn build_v2v3_plan(
path: &PathInfo,
inputs: &ComposerInputs<'_>,
) -> Option<(Vec<u8>, Plan, AddressTable)> {
build_walk(path, inputs)
}
fn build_v3v2_plan(
path: &PathInfo,
inputs: &ComposerInputs<'_>,
) -> Option<(Vec<u8>, Plan, AddressTable)> {
build_walk(path, inputs)
}
fn build_v3v3_plan(
path: &PathInfo,
inputs: &ComposerInputs<'_>,
) -> Option<(Vec<u8>, Plan, AddressTable)> {
build_walk(path, inputs)
}
fn build_v4v4_plan(
path: &PathInfo,
inputs: &ComposerInputs<'_>,
) -> Option<(Vec<u8>, Plan, AddressTable)> {
build_walk(path, inputs)
}
const WT: Address = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
const EX: Address = address!("DeAd0000000000000000000000000000000000Be");
const PM: Address = address!("000000000004444c5dc75cB358380D2e3dE08A90");
#[expect(
clippy::unnecessary_wraps,
reason = "test helper matching the BuildPlan fn-pointer signature (always returns Some to force a validator reject)"
)]
fn bad_build(_: &PathInfo, _: &ComposerInputs<'_>) -> Option<(Vec<u8>, Plan, AddressTable)> {
let at = AddressTable::with_sentinels(Some(WT), Some(EX), Some(PM));
let plan = vec![PlanStep::V4TakeDelta {
currency_idx: SENTINEL_WETH,
currency_addr: WT,
recipient_idx: SENTINEL_SELF,
seeds_pool: None,
}];
Some((enc_preamble(&at), plan, at))
}
#[test]
fn invalid_plan_surfaces_as_reject_not_none() {
use crate::grammar_ledger::ValidationError;
let inputs = ComposerInputs {
executor_address: EX,
pool_manager_address: PM,
weth_address: WT,
optimal_input: 0,
hop_outputs: &[],
consumed_inputs: &[],
opts: EncodeOptions::default(),
};
match build_plan_bytes(&PathInfo::new(vec![]), bad_build, &inputs) {
Derive::Rejected(ValidationError::TakeBeforeCredit { .. }) => {}
other => panic!("expected Reject(TakeBeforeCredit), got {other:?}"),
}
}
#[test]
fn rejected_derivation_is_fatal_not_none() {
use crate::grammar_ledger::ValidationError;
let r = std::panic::catch_unwind(|| {
derive_option(Derive::Rejected(ValidationError::TakeBeforeCredit {
currency: Address::ZERO,
wanted: 1,
have: 0,
}))
});
assert!(
r.is_err(),
"a Reject must panic (ADR-030), never return None"
);
assert_eq!(derive_option(Derive::Declined), None);
assert_eq!(derive_option(Derive::Encoded(vec![7u8])), Some(vec![7u8]));
}
#[derive(Clone, Copy)]
enum Terminal {
Weth,
Tok,
Native,
}
#[derive(Clone, Copy)]
enum Gap {
Wrap,
Unwrap,
}
fn v4_v4_inputs(
terminal: Terminal,
opts: crate::composers::EncodeOptions,
) -> (PathInfo, ComposerInputs<'static>) {
let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let t1 = address!("0000000000000000000000000000000000000db1");
let t2 = address!("0000000000000000000000000000000000000db2");
let pm = address!("00000000000000000000000000000000000000ff");
let v4a_id = "0x0".to_string();
let v4b_id = "0x1".to_string();
let hop_a = HopInfo::V4(V4HopInfo {
pool_manager_address: pm,
pool_id_hex: v4a_id,
currency0_address: weth,
currency1_address: t1,
fee: 3000,
tick_spacing: 60,
hook_address: Address::ZERO,
zfo: true,
});
let hop_b = match terminal {
Terminal::Weth => HopInfo::V4(V4HopInfo {
pool_manager_address: pm,
pool_id_hex: v4b_id,
currency0_address: t1,
currency1_address: weth,
fee: 3000,
tick_spacing: 60,
hook_address: Address::ZERO,
zfo: true,
}),
Terminal::Tok => HopInfo::V4(V4HopInfo {
pool_manager_address: pm,
pool_id_hex: v4b_id,
currency0_address: t1,
currency1_address: t2,
fee: 3000,
tick_spacing: 60,
hook_address: Address::ZERO,
zfo: true,
}),
Terminal::Native => HopInfo::V4(V4HopInfo {
pool_manager_address: pm,
pool_id_hex: v4b_id,
currency0_address: NATIVE_CURRENCY_ADDRESS,
currency1_address: t1,
fee: 3000,
tick_spacing: 60,
hook_address: Address::ZERO,
zfo: false,
}),
};
let path = PathInfo::new(vec![hop_a, hop_b]);
static OPTIMAL: u128 = 1_000_000;
static OUTS: [u128; 2] = [1_100_000, 1_200_000];
static CONSUMED: [u128; 2] = [1_000_000, 1_100_000];
(
path,
ComposerInputs {
executor_address: address!("00000000000000000000000000000000000000ee"),
pool_manager_address: pm,
weth_address: weth,
optimal_input: OPTIMAL,
hop_outputs: &OUTS,
consumed_inputs: &CONSUMED,
opts,
},
)
}
fn v4_v4_gap_inputs(
gap: Gap,
opts: crate::composers::EncodeOptions,
) -> (PathInfo, ComposerInputs<'static>) {
let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let t1 = address!("0000000000000000000000000000000000000da1");
let t2 = address!("0000000000000000000000000000000000000da2");
let pm = address!("00000000000000000000000000000000000000ff");
let v4a_id = "0x0".to_string();
let v4b_id = "0x1".to_string();
let hop_a = match gap {
Gap::Wrap => HopInfo::V4(V4HopInfo {
pool_manager_address: pm,
pool_id_hex: v4a_id,
currency0_address: NATIVE_CURRENCY_ADDRESS,
currency1_address: weth,
fee: 3000,
tick_spacing: 60,
hook_address: Address::ZERO,
zfo: false,
}),
Gap::Unwrap => HopInfo::V4(V4HopInfo {
pool_manager_address: pm,
pool_id_hex: v4a_id,
currency0_address: t1,
currency1_address: weth,
fee: 3000,
tick_spacing: 60,
hook_address: Address::ZERO,
zfo: true,
}),
};
let hop_b = match gap {
Gap::Wrap => HopInfo::V4(V4HopInfo {
pool_manager_address: pm,
pool_id_hex: v4b_id,
currency0_address: t2,
currency1_address: weth,
fee: 3000,
tick_spacing: 60,
hook_address: Address::ZERO,
zfo: false,
}),
Gap::Unwrap => HopInfo::V4(V4HopInfo {
pool_manager_address: pm,
pool_id_hex: v4b_id,
currency0_address: NATIVE_CURRENCY_ADDRESS,
currency1_address: weth,
fee: 3000,
tick_spacing: 60,
hook_address: Address::ZERO,
zfo: true,
}),
};
let path = PathInfo::new(vec![hop_a, hop_b]);
static OPTIMAL: u128 = 1_000_000;
static OUTS: [u128; 2] = [1_100_000, 1_200_000];
static CONSUMED: [u128; 2] = [1_000_000, 1_100_000];
(
path,
ComposerInputs {
executor_address: address!("00000000000000000000000000000000000000ee"),
pool_manager_address: pm,
weth_address: weth,
optimal_input: OPTIMAL,
hop_outputs: &OUTS,
consumed_inputs: &CONSUMED,
opts,
},
)
}
#[test]
fn funding_source_is_derived_from_leading_hop() {
let cases: [(Prot, FundingSource); 2] = [
(Prot::V2, FundingSource::InPathFlash),
(Prot::V3, FundingSource::SelfFund),
];
for (_p, expected) in cases {
assert_ne!(
expected,
match expected {
FundingSource::InPathFlash => FundingSource::SelfFund,
_ => FundingSource::InPathFlash,
}
);
}
}
fn v2_v3_path_inputs() -> (PathInfo, ComposerInputs<'static>) {
let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let usdc = address!("A0b86991c6218b36c1D19D4a2e9Eb0cE3606eB48");
let v2a = address!("00000000000000000000000000000000000000aa");
let v3b = address!("00000000000000000000000000000000000000bb");
let path = PathInfo::new(vec![
HopInfo::V2(V2HopInfo {
pool_address: v2a,
token0_address: weth,
token1_address: usdc,
fee: 30,
zfo: true, }),
HopInfo::V3(V3HopInfo {
pool_address: v3b,
token0_address: usdc,
token1_address: weth,
fee: 3000,
zfo: true, }),
]);
static OPTIMAL: u128 = 1_000_000;
static OUTS: [u128; 2] = [1_100_000, 1_200_000];
static CONSUMED: [u128; 2] = [1_000_000, 1_100_000];
let inputs = ComposerInputs {
executor_address: address!("00000000000000000000000000000000000000ee"),
pool_manager_address: address!("00000000000000000000000000000000000000ff"),
weth_address: weth,
optimal_input: OPTIMAL,
hop_outputs: &OUTS,
consumed_inputs: &CONSUMED,
opts: crate::composers::EncodeOptions::default(),
};
(path, inputs)
}
#[test]
fn v2_v3_plan_projects_a_validating_trace() {
let (path, inputs) = v2_v3_path_inputs();
let (_preamble, plan, _at) =
build_v2v3_plan(&path, &inputs).expect("v2_v3 must build a Plan");
let ops = plan_to_ledger_ops(&plan);
assert_eq!(ops.len(), 4, "v2_v3 Plan projects to 4 LedgerOps");
let mut v = crate::grammar_ledger::LedgerValidator::default();
assert!(
v.validate_full(&ops).is_ok(),
"canonical v2_v3 Plan must project a validating trace"
);
}
#[test]
fn v2_v3_plan_misordered_callback_rejects() {
let (path, inputs) = v2_v3_path_inputs();
let (_preamble, mut plan, _at) =
build_v2v3_plan(&path, &inputs).expect("v2_v3 must build a Plan");
let outer = plan.get_mut(0).unwrap();
if let PlanStep::FlashSwap { callback, .. } = outer {
callback.swap(0, 1);
} else {
panic!("expected outer V2 FlashSwap");
}
let ops = plan_to_ledger_ops(&plan);
let mut v = crate::grammar_ledger::LedgerValidator::default();
assert!(
matches!(
v.validate_full(&ops),
Err(crate::grammar_ledger::ValidationError::Erc20TransferBeforeCredit {
currency, wanted, have
}) if currency == inputs.weth_address && wanted == 1_000_000 && have == 0
),
"misordered Plan must be rejected: WETH repay before V3 flash credits WETH"
);
let _ = U256::ZERO;
}
fn v3_v2_path_inputs() -> (PathInfo, ComposerInputs<'static>) {
let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let usdc = address!("A0b86991c6218b36c1D19D4a2e9Eb0cE3606eB48");
let v3a = address!("00000000000000000000000000000000000000a1");
let v2b = address!("00000000000000000000000000000000000000b2");
let path = PathInfo::new(vec![
HopInfo::V3(V3HopInfo {
pool_address: v3a,
token0_address: weth,
token1_address: usdc,
fee: 3000,
zfo: true,
}),
HopInfo::V2(V2HopInfo {
pool_address: v2b,
token0_address: usdc,
token1_address: weth,
fee: 30,
zfo: true,
}),
]);
static OPTIMAL: u128 = 1_000_000;
static OUTS: [u128; 2] = [1_100_000, 1_200_000];
static CONSUMED: [u128; 2] = [1_000_000, 1_100_000];
(
path,
ComposerInputs {
executor_address: address!("00000000000000000000000000000000000000ee"),
pool_manager_address: address!("00000000000000000000000000000000000000ff"),
weth_address: weth,
optimal_input: OPTIMAL,
hop_outputs: &OUTS,
consumed_inputs: &CONSUMED,
opts: crate::composers::EncodeOptions::default(),
},
)
}
fn v3_v3_path_inputs() -> (PathInfo, ComposerInputs<'static>) {
let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let usdc = address!("A0b86991c6218b36c1D19D4a2e9Eb0cE3606eB48");
let v3a = address!("00000000000000000000000000000000000000a3");
let v3b = address!("00000000000000000000000000000000000000b3");
let path = PathInfo::new(vec![
HopInfo::V3(V3HopInfo {
pool_address: v3a,
token0_address: weth,
token1_address: usdc,
fee: 3000,
zfo: true,
}),
HopInfo::V3(V3HopInfo {
pool_address: v3b,
token0_address: usdc,
token1_address: weth,
fee: 3000,
zfo: true,
}),
]);
static OPTIMAL: u128 = 1_000_000;
static OUTS: [u128; 2] = [1_100_000, 1_200_000];
static CONSUMED: [u128; 2] = [1_000_000, 1_100_000];
(
path,
ComposerInputs {
executor_address: address!("00000000000000000000000000000000000000ee"),
pool_manager_address: address!("00000000000000000000000000000000000000ff"),
weth_address: weth,
optimal_input: OPTIMAL,
hop_outputs: &OUTS,
consumed_inputs: &CONSUMED,
opts: crate::composers::EncodeOptions::default(),
},
)
}
fn plan_builds_and_validates(
build: fn(&PathInfo, &ComposerInputs) -> Option<(Vec<u8>, Plan, AddressTable)>,
path: &PathInfo,
inputs: &ComposerInputs,
name: &str,
) {
let (_preamble, plan, _at) =
build(path, inputs).unwrap_or_else(|| panic!("[{name}] build None"));
let ops = plan_to_ledger_ops(&plan);
let mut v = crate::grammar_ledger::LedgerValidator::default();
assert!(
v.validate_full(&ops).is_ok(),
"[{name}] Plan must validate clean"
);
}
fn axis_path(combo: &[Prot]) -> PathInfo {
let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let usdc = address!("A0b86991c6218b36c1D19D4a2e9Eb0cE3606eB48");
let hops = combo
.iter()
.enumerate()
.map(|(i, p)| match p {
Prot::V2 => HopInfo::V2(V2HopInfo {
pool_address: Address::from([0xA0 + u8::try_from(i).unwrap(); 20]),
token0_address: weth,
token1_address: usdc,
fee: 30,
zfo: true,
}),
Prot::V3 => HopInfo::V3(V3HopInfo {
pool_address: Address::from([0xB0 + u8::try_from(i).unwrap(); 20]),
token0_address: weth,
token1_address: usdc,
fee: 3000,
zfo: true,
}),
Prot::V4 => HopInfo::V4(V4HopInfo {
pool_manager_address: Address::ZERO,
pool_id_hex: format!("0x{i:x}"),
currency0_address: weth,
currency1_address: usdc,
fee: 500,
tick_spacing: 10,
hook_address: Address::ZERO,
zfo: true,
}),
})
.collect();
PathInfo::new(hops)
}
#[test]
fn family_axis_support_declares_the_honoring_surface() {
assert_eq!(
family_axis_support(&axis_path(&[Prot::V2, Prot::V3])).unwrap(),
AxisSupport::funding()
);
assert_eq!(
family_axis_support(&axis_path(&[Prot::V2, Prot::V2])).unwrap(),
AxisSupport::funding()
);
assert_eq!(
family_axis_support(&axis_path(&[Prot::V2, Prot::V2, Prot::V2])).unwrap(),
AxisSupport::funding()
);
assert_eq!(
family_axis_support(&axis_path(&[Prot::V4, Prot::V4])).unwrap(),
AxisSupport::capture()
);
assert_eq!(
family_axis_support(&axis_path(&[Prot::V4, Prot::V4, Prot::V4])).unwrap(),
AxisSupport::capture()
);
assert_eq!(
family_axis_support(&axis_path(&[Prot::V2, Prot::V2, Prot::V4])).unwrap(),
AxisSupport::none()
);
assert_eq!(
family_axis_support(&axis_path(&[Prot::V4, Prot::V2, Prot::V4])).unwrap(),
AxisSupport::none()
);
assert_eq!(
family_axis_support(&axis_path(&[Prot::V2, Prot::V4])).unwrap(),
AxisSupport::none()
);
assert_eq!(
family_axis_support(&axis_path(&[Prot::V3, Prot::V2])).unwrap(),
AxisSupport::none()
);
assert_eq!(
family_axis_support(&axis_path(&[Prot::V3, Prot::V3])).unwrap(),
AxisSupport::none()
);
assert_eq!(
family_axis_support(&axis_path(&[Prot::V3, Prot::V3, Prot::V3])).unwrap(),
AxisSupport::none()
);
for axes in [
family_axis_support(&axis_path(&[Prot::V2, Prot::V3])).unwrap(),
family_axis_support(&axis_path(&[Prot::V4, Prot::V4])).unwrap(),
family_axis_support(&axis_path(&[Prot::V3, Prot::V3])).unwrap(),
] {
assert!(!axes.is_honored(Axis::Bribe), "no family branches bribe");
}
assert!(family_axis_support(&axis_path(&[Prot::V2, Prot::V3]))
.unwrap()
.is_honored(Axis::Funding));
assert!(family_axis_support(&axis_path(&[Prot::V4, Prot::V4]))
.unwrap()
.is_honored(Axis::Capture));
assert!(family_axis_support(&axis_path(&[Prot::V2])).is_none());
}
#[test]
fn v3_v2_plan_byte_parity_and_validates() {
let (path, inputs) = v3_v2_path_inputs();
plan_builds_and_validates(build_v3v2_plan, &path, &inputs, "v3_v2");
}
#[test]
fn v3_v3_plan_byte_parity_and_validates() {
let (path, inputs) = v3_v3_path_inputs();
plan_builds_and_validates(build_v3v3_plan, &path, &inputs, "v3_v3");
}
fn all_v2_chain_hops(n: usize) -> Vec<HopInfo> {
let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let usdc = address!("A0b86991c6218b36c1D19D4a2e9Eb0cE3606eB48");
let wbtc = address!("2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599");
let dai = address!("6B175474E89094C44Da98b954EedeAC495271d0F");
let cycle = [weth, usdc, wbtc, dai];
(0..n)
.map(|i| {
HopInfo::V2(V2HopInfo {
pool_address: Address::from([0xD0 + u8::try_from(i).expect("2..=4 hops"); 20]),
token0_address: cycle[i % 4],
token1_address: cycle[(i + 1) % 4],
fee: 30,
zfo: true,
})
})
.collect()
}
#[test]
fn all_v2_gate_rejects_unprofitable_inpathflash_stream() {
let path = PathInfo::new(all_v2_chain_hops(2));
let outs: Vec<u128> = vec![80_000, 60_000]; let consumed: Vec<u128> = vec![50_000, 80_000];
for (flabel, funding) in [
("InPathFlash", FundingSource::InPathFlash),
("SelfFund", FundingSource::SelfFund),
] {
let inputs = ComposerInputs {
executor_address: address!("00000000000000000000000000000000000000ee"),
pool_manager_address: address!("00000000000000000000000000000000000000ff"),
weth_address: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
optimal_input: 100_000,
hop_outputs: &outs,
consumed_inputs: &consumed,
opts: crate::composers::EncodeOptions {
funding,
..Default::default()
},
};
let (_preamble, plan, _at) = build_all_v2_chain(&path, &inputs)
.unwrap_or_else(|| panic!("[{flabel}] build None"));
let ops = plan_to_ledger_ops(&plan);
let mut v = crate::grammar_ledger::LedgerValidator::default();
if flabel == "InPathFlash" {
let err = v
.validate_full(&ops)
.expect_err("losing InPathFlash stream must be rejected");
assert!(
matches!(
err,
crate::grammar_ledger::ValidationError::Erc20TransferBeforeCredit { .. }
),
"expected Erc20TransferBeforeCredit, got {err:?}"
);
let d = derive_all_v2_detailed(&path, &inputs);
assert!(
matches!(
d,
Derive::Rejected(
crate::grammar_ledger::ValidationError::Erc20TransferBeforeCredit { .. }
)
),
"expected Reject(Erc20TransferBeforeCredit), got {d:?}"
);
let r = std::panic::catch_unwind(|| derive_all_v2(&path, &inputs));
assert!(
r.is_err(),
"a validator Reject must be fatal (panic), never None (ADR-030)"
);
} else {
assert!(
v.validate_full(&ops).is_ok(),
"losing SelfFund stream must still validate (no flash debt)"
);
}
}
}
#[test]
fn v3_v2_plan_terminal_v2_before_seed_rejected() {
let (path, inputs) = v3_v2_path_inputs();
let (_preamble, mut plan, _at) = build_v3v2_plan(&path, &inputs).expect("v3_v2 build None");
let outer = plan.last_mut().unwrap();
if let PlanStep::FlashSwap { callback, .. } = outer {
let swapcalc = callback.remove(2);
callback.insert(0, swapcalc);
} else {
panic!("expected outer V3 FlashSwap");
}
let ops = plan_to_ledger_ops(&plan);
let mut v = crate::grammar_ledger::LedgerValidator::default();
assert!(
matches!(
v.validate_full(&ops),
Err(crate::grammar_ledger::ValidationError::SwapCalcBeforeCredit { .. })
),
"misordered Plan: V2SwapCalc before its pair seed must be rejected"
);
let _ = U256::ZERO;
}
fn v4_v4_path_inputs() -> (PathInfo, ComposerInputs<'static>) {
v4_v4_inputs(Terminal::Weth, crate::composers::EncodeOptions::default())
}
#[test]
fn v4_v4_plan_byte_parity_and_validates() {
let (path, inputs) = v4_v4_path_inputs();
plan_builds_and_validates(build_v4v4_plan, &path, &inputs, "v4_v4");
}
#[test]
fn v4_v4_plan_take_before_swap_rejected() {
let (path, inputs) = v4_v4_path_inputs();
let (_preamble, mut plan, _at) = build_v4v4_plan(&path, &inputs).expect("v4_v4 build None");
let outer = plan.get_mut(0).unwrap();
if let PlanStep::V4Unlock { inner, .. } = outer {
let take = inner.remove(2);
inner.insert(0, take);
} else {
panic!("expected outer V4Unlock");
}
let ops = plan_to_ledger_ops(&plan);
let mut v = crate::grammar_ledger::LedgerValidator::default();
assert!(
matches!(
v.validate_full(&ops),
Err(crate::grammar_ledger::ValidationError::TakeBeforeCredit { .. })
),
"misordered v4_v4 Plan: TakeDelta before the swap credits PM must be rejected"
);
let _ = U256::ZERO;
}
#[test]
fn v4_v4_plan_unsettled_delta_rejected() {
let (path, mut inputs) = v4_v4_path_inputs();
static CLAMPED: [u128; 2] = [1_000_000, 1_150_000];
inputs.consumed_inputs = &CLAMPED;
let (_preamble, mut plan, _at) = build_v4v4_plan(&path, &inputs).expect("v4_v4 build None");
let outer = plan.get_mut(0).unwrap();
if let PlanStep::V4Unlock { inner, .. } = outer {
inner.pop();
} else {
panic!("expected outer V4Unlock");
}
let ops = plan_to_ledger_ops(&plan);
let mut v = crate::grammar_ledger::LedgerValidator::default();
assert!(
matches!(
v.validate_full(&ops),
Err(crate::grammar_ledger::ValidationError::PmDeltaNonzero { .. })
),
"v4_v4 Plan missing its settle must be rejected: nonzero PM delta at unlock end"
);
let _ = U256::ZERO;
}
fn v4_v4_opts_inputs(
opts: crate::composers::EncodeOptions,
) -> (PathInfo, ComposerInputs<'static>) {
let (mut path, mut inputs) = v4_v4_path_inputs();
let ComposerInputs {
executor_address,
pool_manager_address,
weth_address,
optimal_input,
hop_outputs,
consumed_inputs,
opts: _,
} = inputs;
inputs = ComposerInputs {
executor_address,
pool_manager_address,
weth_address,
optimal_input,
hop_outputs,
consumed_inputs,
opts,
};
let _ = &mut path;
(path, inputs)
}
#[test]
fn v4_v4_plan_batch_byte_parity_and_validates() {
let (path, inputs) = v4_v4_opts_inputs(crate::composers::EncodeOptions {
erc6909_profit: false,
use_v4_batch: true,
..Default::default()
});
plan_builds_and_validates(build_v4v4_plan, &path, &inputs, "v4_v4 batch");
let (_preamble, plan, _at) =
build_v4v4_plan(&path, &inputs).expect("v4_v4 batch build None");
let outer = &plan[0];
let PlanStep::V4Unlock { inner, .. } = outer else {
panic!("expected outer V4Unlock");
};
assert_eq!(inner.len(), 2, "batch inner = [V4Batch, V4SettleAll]");
assert!(
matches!(inner[0], PlanStep::V4Batch { .. }),
"first step is V4Batch"
);
assert!(
matches!(inner[1], PlanStep::V4SettleAll),
"trailing SettleAll"
);
let _ = U256::ZERO;
}
#[test]
fn v4_v4_plan_erc6909_byte_parity_and_validates() {
let (path, inputs) = v4_v4_opts_inputs(crate::composers::EncodeOptions {
erc6909_profit: true,
use_v4_batch: false,
..Default::default()
});
plan_builds_and_validates(build_v4v4_plan, &path, &inputs, "v4_v4 erc6909");
let (_preamble, plan, _at) =
build_v4v4_plan(&path, &inputs).expect("v4_v4 erc6909 build None");
let PlanStep::V4Unlock { inner, .. } = &plan[0] else {
panic!("expected outer V4Unlock");
};
assert_eq!(
inner.len(),
4,
"erc6909 inner = [V4Swap, V4Swap, V4Mint, V4SettleAll]"
);
assert!(
matches!(inner[2], PlanStep::V4Mint { .. }),
"profit step is V4Mint"
);
let _ = U256::ZERO;
}
#[test]
fn v4_v4_batch_and_erc6909_capture_weth_terminal_composes() {
let (path, inputs) = v4_v4_opts_inputs(crate::composers::EncodeOptions {
erc6909_profit: true,
use_v4_batch: true,
..Default::default()
});
let (_preamble, plan, _at) = build_v4v4_plan(&path, &inputs)
.expect("batch + erc6909 WETH-terminal must build (TGUZCT)");
let PlanStep::V4Unlock { inner, .. } = &plan[0] else {
panic!("expected outer V4Unlock");
};
assert_eq!(
inner.len(),
3,
"erc6909+batch inner = [V4Batch(open_weth), V4Mint, V4SettleAll]"
);
assert!(
matches!(
&inner[0],
PlanStep::V4Batch {
open_weth: true,
..
}
),
"the batch must be the open-weth (0x43) variant"
);
assert!(matches!(&inner[1], PlanStep::V4Mint { .. }));
assert!(matches!(&inner[2], PlanStep::V4SettleAll));
let ops = plan_to_ledger_ops(&plan);
assert!(
crate::grammar_ledger::LedgerValidator::default()
.validate_full(&ops)
.is_ok(),
"open batch -> WETH mint -> settle-all must validate"
);
}
#[test]
fn v4_v4_plan_mint_before_swap_rejected() {
let (path, inputs) = v4_v4_opts_inputs(crate::composers::EncodeOptions {
erc6909_profit: true,
use_v4_batch: false,
..Default::default()
});
let (_preamble, mut plan, _at) =
build_v4v4_plan(&path, &inputs).expect("v4_v4 erc6909 build None");
let outer = plan.get_mut(0).unwrap();
if let PlanStep::V4Unlock { inner, .. } = outer {
let mint = inner.remove(2);
inner.insert(0, mint);
} else {
panic!("expected outer V4Unlock");
}
let ops = plan_to_ledger_ops(&plan);
let mut v = crate::grammar_ledger::LedgerValidator::default();
assert!(
matches!(
v.validate_full(&ops),
Err(crate::grammar_ledger::ValidationError::TakeBeforeCredit { .. })
),
"misordered v4_v4 erc6909 Plan: V4Mint before the swap credits PM must be rejected"
);
let _ = U256::ZERO;
}
#[test]
fn v4_v4_terminal_opt_matrix_byte_parity_and_validates() {
use crate::composers::EncodeOptions;
let modes = [
("default", EncodeOptions::default()),
(
"batch",
EncodeOptions {
erc6909_profit: false,
use_v4_batch: true,
..Default::default()
},
),
(
"erc6909",
EncodeOptions {
erc6909_profit: true,
use_v4_batch: false,
..Default::default()
},
),
(
"batch+erc6909",
EncodeOptions {
erc6909_profit: true,
use_v4_batch: true,
..Default::default()
},
),
];
let terminals = [
("weth", Terminal::Weth),
("tok", Terminal::Tok),
("native", Terminal::Native),
];
for (t_name, terminal) in terminals {
for (m_name, opts) in modes {
let label = format!("v4_v4 {t_name}+{m_name}");
let (path, inputs) = v4_v4_inputs(terminal, opts);
plan_builds_and_validates(build_v4v4_plan, &path, &inputs, &label);
}
}
let _ = U256::ZERO;
}
#[test]
fn v4_v4_plan_batch_native_terminal_emits_no_take() {
let (path, inputs) = v4_v4_inputs(
Terminal::Native,
crate::composers::EncodeOptions {
erc6909_profit: false,
use_v4_batch: true,
..Default::default()
},
);
let (_preamble, plan, _at) =
build_v4v4_plan(&path, &inputs).expect("v4_v4 native+batch build None");
let PlanStep::V4Unlock { inner, .. } = &plan[0] else {
panic!("expected outer V4Unlock");
};
assert_eq!(
inner.len(),
2,
"native+batch inner = [V4Batch, V4SettleAll] (no take)"
);
assert!(matches!(inner[0], PlanStep::V4Batch { .. }));
assert!(matches!(inner[1], PlanStep::V4SettleAll));
let _ = U256::ZERO;
}
#[test]
fn v4_v4_gap_byte_parity_and_validates() {
for (name, gap) in [("wrap", Gap::Wrap), ("unwrap", Gap::Unwrap)] {
let label = format!("v4_v4 gap {name}");
let (path, inputs) = v4_v4_gap_inputs(gap, crate::composers::EncodeOptions::default());
plan_builds_and_validates(build_v4v4_plan, &path, &inputs, &label);
}
let _ = U256::ZERO;
}
#[test]
fn v4_v4_gap_opt_matrix_byte_parity_and_validates() {
use crate::composers::EncodeOptions;
let modes = [
("default", EncodeOptions::default()),
(
"batch",
EncodeOptions {
erc6909_profit: false,
use_v4_batch: true,
..Default::default()
},
),
(
"erc6909",
EncodeOptions {
erc6909_profit: true,
use_v4_batch: false,
..Default::default()
},
),
(
"batch+erc6909",
EncodeOptions {
erc6909_profit: true,
use_v4_batch: true,
..Default::default()
},
),
];
for (g_name, gap) in [("wrap", Gap::Wrap), ("unwrap", Gap::Unwrap)] {
for (m_name, opts) in modes {
let label = format!("v4_v4 gap {g_name}+{m_name}");
let (path, inputs) = v4_v4_gap_inputs(gap, opts);
plan_builds_and_validates(build_v4v4_plan, &path, &inputs, &label);
}
}
let _ = U256::ZERO;
}
#[test]
fn v4_v4_gap_shape_is_swap_bridge_swap_settle_take() {
let (path, inputs) =
v4_v4_gap_inputs(Gap::Wrap, crate::composers::EncodeOptions::default());
let (_preamble, plan, _at) = build_v4v4_plan(&path, &inputs).expect("v4_v4 gap build None");
let PlanStep::V4Unlock { inner, .. } = &plan[0] else {
panic!("expected outer V4Unlock");
};
assert_eq!(inner.len(), 7, "gap inner = 7 steps");
assert!(matches!(inner[0], PlanStep::V4Swap { .. }), "0: swap a");
assert!(
matches!(inner[1], PlanStep::V4TakeCompact { .. }),
"1: bridge take"
);
assert!(
matches!(inner[2], PlanStep::WethDeposit { .. }),
"2: wrap deposit (Wrap gap)"
);
assert!(matches!(inner[3], PlanStep::V4Swap { .. }), "3: swap b");
assert!(
matches!(inner[4], PlanStep::V4SettleDelta { .. }),
"4: settle"
);
assert!(
matches!(inner[5], PlanStep::V4TakeDelta { .. }),
"5: profit take"
);
assert!(matches!(inner[6], PlanStep::V4SettleAll), "6: settle all");
let _ = U256::ZERO;
}
fn weth_withdraw_amounts(bytes: &[u8]) -> Vec<u128> {
bytes
.windows(33)
.filter(|w| w[0] == 0x13)
.map(|w| {
let mut a = [0u8; 16];
a.copy_from_slice(&w[17..33]);
u128::from_be_bytes(a)
})
.collect()
}
fn count_v4_take_delta(bytes: &[u8]) -> usize {
bytes.iter().filter(|&&b| b == 0x50).count()
}
#[test]
fn v4_v4_native_capture_weth_terminal_appends_weth_withdraw() {
let (path, inputs) = v4_v4_opts_inputs(crate::composers::EncodeOptions {
capture: crate::grammar_ledger::ProfitCapture::Native,
..Default::default()
});
let bytes =
derive_shape(&path, &inputs).expect("v4_v4 native-capture WETH terminal must derive");
assert!(
weth_withdraw_amounts(&bytes).contains(&200_000),
"Native capture must append WETH_WITHDRAW of the profit; got {:?}",
weth_withdraw_amounts(&bytes)
);
assert!(
count_v4_take_delta(&bytes) >= 1,
"V4_TAKE_DELTA custody take must precede the withdraw"
);
}
#[test]
fn v4_v4_custody_capture_emits_no_weth_withdraw() {
let (path, inputs) = v4_v4_opts_inputs(crate::composers::EncodeOptions::default());
let bytes = derive_shape(&path, &inputs).expect("v4_v4 custody WETH terminal must derive");
assert!(
!weth_withdraw_amounts(&bytes).contains(&200_000),
"Custody capture must NOT append a WETH_WITHDRAW of the profit"
);
}
#[test]
fn v4_v4_native_capture_tok_terminal_declines() {
let (path, inputs) = v4_v4_inputs(
Terminal::Tok,
crate::composers::EncodeOptions {
capture: crate::grammar_ledger::ProfitCapture::Native,
..Default::default()
},
);
assert!(
derive_shape(&path, &inputs).is_none(),
"Native capture on a non-WETH/non-native tok terminal must decline"
);
}
#[test]
fn v4_v4_native_capture_native_terminal_is_noop() {
let (path, inputs) = v4_v4_inputs(
Terminal::Native,
crate::composers::EncodeOptions {
capture: crate::grammar_ledger::ProfitCapture::Native,
..Default::default()
},
);
let bytes = derive_shape(&path, &inputs)
.expect("v4_v4 native-capture native terminal must derive (no-op)");
assert!(
!weth_withdraw_amounts(&bytes).iter().any(|&a| a > 0),
"Native terminal + Native capture is already native; no withdraw"
);
}
#[test]
fn v4_v4_native_capture_plan_byte_parity_and_validates() {
let (path, inputs) = v4_v4_opts_inputs(crate::composers::EncodeOptions {
capture: ProfitCapture::Native,
..Default::default()
});
plan_builds_and_validates(build_v4v4_plan, &path, &inputs, "v4_v4 native-capture");
}
}