1use alloy::primitives::Address;
31
32use crate::composers::{ComposerInputs, HopInfo, PathInfo, V4HopInfo, NATIVE_CURRENCY_ADDRESS};
33use crate::encoders::{AddressTable, SENTINEL_NATIVE, SENTINEL_SELF, SENTINEL_WETH};
34pub use crate::grammar_plan::{
38 plan_to_bytes, plan_to_ledger_ops, Axis, AxisSupport, Bribe, FundingSource, Plan, PlanStep,
39 ProfitCapture, Prot, ShapeClass, V4BatchSwap,
40};
41pub(crate) fn v4_scaffold_table(inputs: &ComposerInputs<'_>) -> AddressTable {
78 AddressTable::with_sentinels(
79 Some(inputs.weth_address),
80 Some(inputs.executor_address),
81 Some(inputs.pool_manager_address),
82 )
83}
84
85pub(crate) fn v4_hop_currencies(h: &V4HopInfo) -> (Address, Address) {
88 if h.zfo {
89 (h.currency1_address, h.currency0_address)
90 } else {
91 (h.currency0_address, h.currency1_address)
92 }
93}
94
95pub(crate) fn native_capture_declines(
99 capture: ProfitCapture,
100 terminal: Address,
101 weth: Address,
102) -> bool {
103 capture == ProfitCapture::Native && terminal != weth && terminal != NATIVE_CURRENCY_ADDRESS
104}
105
106pub(crate) fn v4_terminal_capture_steps(
113 terminal: Address,
114 terminal_idx: u8,
115 capture: ProfitCapture,
116 use_v4_batch: bool,
117 any_gap: bool,
118 profit: u128,
119 weth: Address,
120) -> Vec<PlanStep> {
121 let mut steps = Vec::new();
122 if capture == ProfitCapture::Erc6909 && terminal == weth {
123 if profit > 0 {
124 steps.push(PlanStep::V4Mint {
125 currency_idx: SENTINEL_WETH,
126 currency_addr: weth,
127 recipient_idx: SENTINEL_SELF,
128 amount: profit,
129 });
130 }
131 } else if !use_v4_batch || any_gap {
132 steps.push(PlanStep::V4TakeDelta {
133 currency_idx: terminal_idx,
134 currency_addr: terminal,
135 recipient_idx: SENTINEL_SELF,
136 seeds_pool: None,
137 });
138 }
139 if capture == ProfitCapture::Native && terminal == weth {
140 steps.push(PlanStep::WethWithdraw {
141 weth_idx: SENTINEL_WETH,
142 weth_addr: weth,
143 amount: profit,
144 });
145 }
146 steps
147}
148
149pub(crate) fn v4_bridge_steps(
154 bridge: crate::composers::CurrencyBridge,
155 weth: Address,
156 amount: u128,
157) -> (Vec<PlanStep>, u8, Address) {
158 let wrap = matches!(bridge, crate::composers::CurrencyBridge::Wrap);
159 let take_currency = if wrap { NATIVE_CURRENCY_ADDRESS } else { weth };
160 let settle_currency = if wrap { weth } else { NATIVE_CURRENCY_ADDRESS };
161 let take_idx = if wrap { SENTINEL_NATIVE } else { SENTINEL_WETH };
162 let settle_idx = if wrap { SENTINEL_WETH } else { SENTINEL_NATIVE };
163 let convert = if wrap {
164 PlanStep::WethDeposit {
165 weth_idx: SENTINEL_WETH,
166 weth_addr: weth,
167 amount,
168 }
169 } else {
170 PlanStep::WethWithdraw {
171 weth_idx: SENTINEL_WETH,
172 weth_addr: weth,
173 amount,
174 }
175 };
176 (
177 vec![
178 PlanStep::V4TakeCompact {
179 currency_idx: take_idx,
180 currency_addr: take_currency,
181 recipient_idx: SENTINEL_SELF,
182 amount,
183 seeds_pool: None,
184 repays_flash: None,
185 },
186 convert,
187 ],
188 settle_idx,
189 settle_currency,
190 )
191}
192
193#[cfg(test)]
197pub(crate) type BuildPlan =
198 fn(&PathInfo, &ComposerInputs<'_>) -> Option<(Vec<u8>, Plan, AddressTable)>;
199
200#[derive(Debug, Clone, PartialEq, Eq)]
208pub enum Derive {
209 Encoded(Vec<u8>),
211 Declined,
213 Rejected(crate::grammar_ledger::ValidationError),
216}
217
218#[must_use]
222#[expect(
223 clippy::panic,
224 reason = "ADR-030: a validator Reject is intentionally fatal — it must never be swallowed into a None"
225)]
226pub(crate) fn derive_option(d: Derive) -> Option<Vec<u8>> {
227 match d {
228 Derive::Encoded(bytes) => Some(bytes),
229 Derive::Declined => None,
230 Derive::Rejected(e) => panic!(
231 "executor derivation REJECT (a latent bug, ADR-030): the encoder wrote a Plan the ledger validator rejected: {e:?}"
232 ),
233 }
234}
235
236#[cfg(test)]
247#[must_use]
248fn build_plan_bytes(path: &PathInfo, build: BuildPlan, inputs: &ComposerInputs<'_>) -> Derive {
249 let Some((preamble, plan, at)) = build(path, inputs) else {
250 return Derive::Declined;
251 };
252 let ops = plan_to_ledger_ops(&plan);
253 let mut v = crate::grammar_ledger::LedgerValidator::default();
254 if let Err(e) = v.validate_full(&ops) {
255 return Derive::Rejected(e);
256 }
257 let mut out = preamble;
258 out.extend_from_slice(&plan_to_bytes(&plan, &at));
259 Derive::Encoded(out)
260}
261
262#[must_use]
269pub fn derive_all_v2(path: &PathInfo, inputs: &ComposerInputs<'_>) -> Option<Vec<u8>> {
270 derive_option(derive_all_v2_detailed(path, inputs))
271}
272
273#[must_use]
278pub fn derive_all_v2_detailed(path: &PathInfo, inputs: &ComposerInputs<'_>) -> Derive {
279 derive_shape_detailed(path, inputs)
280}
281
282#[must_use]
285fn prot_of(h: Option<&HopInfo>) -> Option<Prot> {
286 match h {
287 Some(HopInfo::V2(_)) => Some(Prot::V2),
288 Some(HopInfo::V3(_)) => Some(Prot::V3),
289 Some(HopInfo::V4(_)) => Some(Prot::V4),
290 None => None,
291 }
292}
293
294#[must_use]
301pub fn derive_shape(path: &PathInfo, inputs: &ComposerInputs<'_>) -> Option<Vec<u8>> {
302 derive_option(derive_shape_detailed(path, inputs))
303}
304
305#[must_use]
311pub fn derive_shape_detailed(path: &PathInfo, inputs: &ComposerInputs<'_>) -> Derive {
312 let key = (
313 prot_of(path.hops.first()),
314 prot_of(path.hops.get(1)),
315 prot_of(path.hops.get(2)),
316 );
317 if !crate::grammar_walker::recognized_key(key) {
318 return Derive::Declined;
319 }
320 let Some((preamble, plan, at)) = crate::grammar_walker::build_walk(path, inputs) else {
321 return Derive::Declined;
322 };
323 let ops = plan_to_ledger_ops(&plan);
324 let mut v = crate::grammar_ledger::LedgerValidator::default();
325 if let Err(e) = v.validate_full(&ops) {
326 return Derive::Rejected(e);
327 }
328 let mut out = preamble;
329 out.extend_from_slice(&plan_to_bytes(&plan, &at));
330 Derive::Encoded(out)
331}
332
333#[must_use]
350pub fn family_axis_support(path: &PathInfo) -> Option<AxisSupport> {
351 let key = (
352 prot_of(path.hops.first()),
353 prot_of(path.hops.get(1)),
354 prot_of(path.hops.get(2)),
355 );
356 if !crate::grammar_walker::recognized_key(key) {
358 return None;
359 }
360 let capture = matches!(key, (Some(Prot::V4), Some(Prot::V4), Some(Prot::V4) | None));
362 let funding = matches!(
364 key,
365 (Some(Prot::V2), Some(Prot::V3), None)
366 | (Some(Prot::V2), Some(Prot::V2), Some(Prot::V2) | None)
367 );
368 Some(AxisSupport {
369 funding,
370 capture,
371 bribe: false,
372 })
373}
374
375#[cfg(test)]
376mod tests {
377 #![expect(
378 clippy::unwrap_used,
379 clippy::expect_used,
380 clippy::panic,
381 clippy::items_after_statements,
382 clippy::type_complexity,
383 clippy::naive_bytecount,
384 clippy::similar_names
385 )]
386 use super::*;
387 use crate::composers::{EncodeOptions, V2HopInfo, V3HopInfo};
391 use crate::encoders::enc_preamble;
392 use crate::grammar_walker::build_walk;
393 use alloy::primitives::{address, Address, U256};
394
395 fn build_all_v2_chain(
400 path: &PathInfo,
401 inputs: &ComposerInputs<'_>,
402 ) -> Option<(Vec<u8>, Plan, AddressTable)> {
403 build_walk(path, inputs)
404 }
405 fn build_v2v3_plan(
406 path: &PathInfo,
407 inputs: &ComposerInputs<'_>,
408 ) -> Option<(Vec<u8>, Plan, AddressTable)> {
409 build_walk(path, inputs)
410 }
411 fn build_v3v2_plan(
412 path: &PathInfo,
413 inputs: &ComposerInputs<'_>,
414 ) -> Option<(Vec<u8>, Plan, AddressTable)> {
415 build_walk(path, inputs)
416 }
417 fn build_v3v3_plan(
418 path: &PathInfo,
419 inputs: &ComposerInputs<'_>,
420 ) -> Option<(Vec<u8>, Plan, AddressTable)> {
421 build_walk(path, inputs)
422 }
423 fn build_v4v4_plan(
424 path: &PathInfo,
425 inputs: &ComposerInputs<'_>,
426 ) -> Option<(Vec<u8>, Plan, AddressTable)> {
427 build_walk(path, inputs)
428 }
429
430 const WT: Address = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
435 const EX: Address = address!("DeAd0000000000000000000000000000000000Be");
436 const PM: Address = address!("000000000004444c5dc75cB358380D2e3dE08A90");
437
438 #[expect(
439 clippy::unnecessary_wraps,
440 reason = "test helper matching the BuildPlan fn-pointer signature (always returns Some to force a validator reject)"
441 )]
442 fn bad_build(_: &PathInfo, _: &ComposerInputs<'_>) -> Option<(Vec<u8>, Plan, AddressTable)> {
443 let at = AddressTable::with_sentinels(Some(WT), Some(EX), Some(PM));
444 let plan = vec![PlanStep::V4TakeDelta {
445 currency_idx: SENTINEL_WETH,
446 currency_addr: WT,
447 recipient_idx: SENTINEL_SELF,
448 seeds_pool: None,
449 }];
450 Some((enc_preamble(&at), plan, at))
451 }
452
453 #[test]
454 fn invalid_plan_surfaces_as_reject_not_none() {
455 use crate::grammar_ledger::ValidationError;
456 let inputs = ComposerInputs {
457 executor_address: EX,
458 pool_manager_address: PM,
459 weth_address: WT,
460 optimal_input: 0,
461 hop_outputs: &[],
462 consumed_inputs: &[],
463 opts: EncodeOptions::default(),
464 };
465 match build_plan_bytes(&PathInfo::new(vec![]), bad_build, &inputs) {
466 Derive::Rejected(ValidationError::TakeBeforeCredit { .. }) => {}
467 other => panic!("expected Reject(TakeBeforeCredit), got {other:?}"),
468 }
469 }
470
471 #[test]
472 fn rejected_derivation_is_fatal_not_none() {
473 use crate::grammar_ledger::ValidationError;
474 let r = std::panic::catch_unwind(|| {
476 derive_option(Derive::Rejected(ValidationError::TakeBeforeCredit {
477 currency: Address::ZERO,
478 wanted: 1,
479 have: 0,
480 }))
481 });
482 assert!(
483 r.is_err(),
484 "a Reject must panic (ADR-030), never return None"
485 );
486 assert_eq!(derive_option(Derive::Declined), None);
488 assert_eq!(derive_option(Derive::Encoded(vec![7u8])), Some(vec![7u8]));
489 }
490
491 #[derive(Clone, Copy)]
495 enum Terminal {
496 Weth,
497 Tok,
498 Native,
499 }
500
501 #[derive(Clone, Copy)]
503 enum Gap {
504 Wrap,
506 Unwrap,
508 }
509
510 fn v4_v4_inputs(
511 terminal: Terminal,
512 opts: crate::composers::EncodeOptions,
513 ) -> (PathInfo, ComposerInputs<'static>) {
514 let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
515 let t1 = address!("0000000000000000000000000000000000000db1");
516 let t2 = address!("0000000000000000000000000000000000000db2");
517 let pm = address!("00000000000000000000000000000000000000ff");
518 let v4a_id = "0x0".to_string();
519 let v4b_id = "0x1".to_string();
520 let hop_a = HopInfo::V4(V4HopInfo {
522 pool_manager_address: pm,
523 pool_id_hex: v4a_id,
524 currency0_address: weth,
525 currency1_address: t1,
526 fee: 3000,
527 tick_spacing: 60,
528 hook_address: Address::ZERO,
529 zfo: true,
530 });
531 let hop_b = match terminal {
535 Terminal::Weth => HopInfo::V4(V4HopInfo {
537 pool_manager_address: pm,
538 pool_id_hex: v4b_id,
539 currency0_address: t1,
540 currency1_address: weth,
541 fee: 3000,
542 tick_spacing: 60,
543 hook_address: Address::ZERO,
544 zfo: true,
545 }),
546 Terminal::Tok => HopInfo::V4(V4HopInfo {
548 pool_manager_address: pm,
549 pool_id_hex: v4b_id,
550 currency0_address: t1,
551 currency1_address: t2,
552 fee: 3000,
553 tick_spacing: 60,
554 hook_address: Address::ZERO,
555 zfo: true,
556 }),
557 Terminal::Native => HopInfo::V4(V4HopInfo {
560 pool_manager_address: pm,
561 pool_id_hex: v4b_id,
562 currency0_address: NATIVE_CURRENCY_ADDRESS,
563 currency1_address: t1,
564 fee: 3000,
565 tick_spacing: 60,
566 hook_address: Address::ZERO,
567 zfo: false,
568 }),
569 };
570 let path = PathInfo::new(vec![hop_a, hop_b]);
571 static OPTIMAL: u128 = 1_000_000;
572 static OUTS: [u128; 2] = [1_100_000, 1_200_000];
573 static CONSUMED: [u128; 2] = [1_000_000, 1_100_000];
574 (
575 path,
576 ComposerInputs {
577 executor_address: address!("00000000000000000000000000000000000000ee"),
578 pool_manager_address: pm,
579 weth_address: weth,
580 optimal_input: OPTIMAL,
581 hop_outputs: &OUTS,
582 consumed_inputs: &CONSUMED,
583 opts,
584 },
585 )
586 }
587
588 fn v4_v4_gap_inputs(
594 gap: Gap,
595 opts: crate::composers::EncodeOptions,
596 ) -> (PathInfo, ComposerInputs<'static>) {
597 let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
598 let t1 = address!("0000000000000000000000000000000000000da1");
599 let t2 = address!("0000000000000000000000000000000000000da2");
600 let pm = address!("00000000000000000000000000000000000000ff");
601 let v4a_id = "0x0".to_string();
602 let v4b_id = "0x1".to_string();
603 let hop_a = match gap {
606 Gap::Wrap => HopInfo::V4(V4HopInfo {
609 pool_manager_address: pm,
610 pool_id_hex: v4a_id,
611 currency0_address: NATIVE_CURRENCY_ADDRESS,
612 currency1_address: weth,
613 fee: 3000,
614 tick_spacing: 60,
615 hook_address: Address::ZERO,
616 zfo: false,
617 }),
618 Gap::Unwrap => HopInfo::V4(V4HopInfo {
621 pool_manager_address: pm,
622 pool_id_hex: v4a_id,
623 currency0_address: t1,
624 currency1_address: weth,
625 fee: 3000,
626 tick_spacing: 60,
627 hook_address: Address::ZERO,
628 zfo: true,
629 }),
630 };
631 let hop_b = match gap {
634 Gap::Wrap => HopInfo::V4(V4HopInfo {
637 pool_manager_address: pm,
638 pool_id_hex: v4b_id,
639 currency0_address: t2,
640 currency1_address: weth,
641 fee: 3000,
642 tick_spacing: 60,
643 hook_address: Address::ZERO,
644 zfo: false,
645 }),
646 Gap::Unwrap => HopInfo::V4(V4HopInfo {
649 pool_manager_address: pm,
650 pool_id_hex: v4b_id,
651 currency0_address: NATIVE_CURRENCY_ADDRESS,
652 currency1_address: weth,
653 fee: 3000,
654 tick_spacing: 60,
655 hook_address: Address::ZERO,
656 zfo: true,
657 }),
658 };
659 let path = PathInfo::new(vec![hop_a, hop_b]);
660 static OPTIMAL: u128 = 1_000_000;
661 static OUTS: [u128; 2] = [1_100_000, 1_200_000];
662 static CONSUMED: [u128; 2] = [1_000_000, 1_100_000];
663 (
664 path,
665 ComposerInputs {
666 executor_address: address!("00000000000000000000000000000000000000ee"),
667 pool_manager_address: pm,
668 weth_address: weth,
669 optimal_input: OPTIMAL,
670 hop_outputs: &OUTS,
671 consumed_inputs: &CONSUMED,
672 opts,
673 },
674 )
675 }
676
677 #[test]
678 fn funding_source_is_derived_from_leading_hop() {
679 let cases: [(Prot, FundingSource); 2] = [
681 (Prot::V2, FundingSource::InPathFlash),
682 (Prot::V3, FundingSource::SelfFund),
683 ];
684 for (_p, expected) in cases {
685 assert_ne!(
688 expected,
689 match expected {
690 FundingSource::InPathFlash => FundingSource::SelfFund,
691 _ => FundingSource::InPathFlash,
692 }
693 );
694 }
695 }
696
697 fn v2_v3_path_inputs() -> (PathInfo, ComposerInputs<'static>) {
701 let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
702 let usdc = address!("A0b86991c6218b36c1D19D4a2e9Eb0cE3606eB48");
703 let v2a = address!("00000000000000000000000000000000000000aa");
704 let v3b = address!("00000000000000000000000000000000000000bb");
705 let path = PathInfo::new(vec![
706 HopInfo::V2(V2HopInfo {
707 pool_address: v2a,
708 token0_address: weth,
709 token1_address: usdc,
710 fee: 30,
711 zfo: true, }),
713 HopInfo::V3(V3HopInfo {
714 pool_address: v3b,
715 token0_address: usdc,
716 token1_address: weth,
717 fee: 3000,
718 zfo: true, }),
720 ]);
721 static OPTIMAL: u128 = 1_000_000;
722 static OUTS: [u128; 2] = [1_100_000, 1_200_000];
723 static CONSUMED: [u128; 2] = [1_000_000, 1_100_000];
724 let inputs = ComposerInputs {
725 executor_address: address!("00000000000000000000000000000000000000ee"),
726 pool_manager_address: address!("00000000000000000000000000000000000000ff"),
727 weth_address: weth,
728 optimal_input: OPTIMAL,
729 hop_outputs: &OUTS,
730 consumed_inputs: &CONSUMED,
731 opts: crate::composers::EncodeOptions::default(),
732 };
733 (path, inputs)
734 }
735
736 #[test]
738 fn v2_v3_plan_projects_a_validating_trace() {
739 let (path, inputs) = v2_v3_path_inputs();
740 let (_preamble, plan, _at) =
741 build_v2v3_plan(&path, &inputs).expect("v2_v3 must build a Plan");
742 let ops = plan_to_ledger_ops(&plan);
743 assert_eq!(ops.len(), 4, "v2_v3 Plan projects to 4 LedgerOps");
744 let mut v = crate::grammar_ledger::LedgerValidator::default();
745 assert!(
746 v.validate_full(&ops).is_ok(),
747 "canonical v2_v3 Plan must project a validating trace"
748 );
749 }
750
751 #[test]
752 fn v2_v3_plan_misordered_callback_rejects() {
753 let (path, inputs) = v2_v3_path_inputs();
754 let (_preamble, mut plan, _at) =
755 build_v2v3_plan(&path, &inputs).expect("v2_v3 must build a Plan");
756 let outer = plan.get_mut(0).unwrap();
763 if let PlanStep::FlashSwap { callback, .. } = outer {
764 callback.swap(0, 1);
767 } else {
768 panic!("expected outer V2 FlashSwap");
769 }
770 let ops = plan_to_ledger_ops(&plan);
771 let mut v = crate::grammar_ledger::LedgerValidator::default();
772 assert!(
773 matches!(
774 v.validate_full(&ops),
775 Err(crate::grammar_ledger::ValidationError::Erc20TransferBeforeCredit {
776 currency, wanted, have
777 }) if currency == inputs.weth_address && wanted == 1_000_000 && have == 0
778 ),
779 "misordered Plan must be rejected: WETH repay before V3 flash credits WETH"
780 );
781 let _ = U256::ZERO;
782 }
783
784 fn v3_v2_path_inputs() -> (PathInfo, ComposerInputs<'static>) {
788 let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
789 let usdc = address!("A0b86991c6218b36c1D19D4a2e9Eb0cE3606eB48");
790 let v3a = address!("00000000000000000000000000000000000000a1");
791 let v2b = address!("00000000000000000000000000000000000000b2");
792 let path = PathInfo::new(vec![
793 HopInfo::V3(V3HopInfo {
794 pool_address: v3a,
795 token0_address: weth,
796 token1_address: usdc,
797 fee: 3000,
798 zfo: true,
799 }),
800 HopInfo::V2(V2HopInfo {
801 pool_address: v2b,
802 token0_address: usdc,
803 token1_address: weth,
804 fee: 30,
805 zfo: true,
806 }),
807 ]);
808 static OPTIMAL: u128 = 1_000_000;
809 static OUTS: [u128; 2] = [1_100_000, 1_200_000];
810 static CONSUMED: [u128; 2] = [1_000_000, 1_100_000];
811 (
812 path,
813 ComposerInputs {
814 executor_address: address!("00000000000000000000000000000000000000ee"),
815 pool_manager_address: address!("00000000000000000000000000000000000000ff"),
816 weth_address: weth,
817 optimal_input: OPTIMAL,
818 hop_outputs: &OUTS,
819 consumed_inputs: &CONSUMED,
820 opts: crate::composers::EncodeOptions::default(),
821 },
822 )
823 }
824 fn v3_v3_path_inputs() -> (PathInfo, ComposerInputs<'static>) {
825 let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
826 let usdc = address!("A0b86991c6218b36c1D19D4a2e9Eb0cE3606eB48");
827 let v3a = address!("00000000000000000000000000000000000000a3");
828 let v3b = address!("00000000000000000000000000000000000000b3");
829 let path = PathInfo::new(vec![
830 HopInfo::V3(V3HopInfo {
831 pool_address: v3a,
832 token0_address: weth,
833 token1_address: usdc,
834 fee: 3000,
835 zfo: true,
836 }),
837 HopInfo::V3(V3HopInfo {
838 pool_address: v3b,
839 token0_address: usdc,
840 token1_address: weth,
841 fee: 3000,
842 zfo: true,
843 }),
844 ]);
845 static OPTIMAL: u128 = 1_000_000;
846 static OUTS: [u128; 2] = [1_100_000, 1_200_000];
847 static CONSUMED: [u128; 2] = [1_000_000, 1_100_000];
848 (
849 path,
850 ComposerInputs {
851 executor_address: address!("00000000000000000000000000000000000000ee"),
852 pool_manager_address: address!("00000000000000000000000000000000000000ff"),
853 weth_address: weth,
854 optimal_input: OPTIMAL,
855 hop_outputs: &OUTS,
856 consumed_inputs: &CONSUMED,
857 opts: crate::composers::EncodeOptions::default(),
858 },
859 )
860 }
861 fn plan_builds_and_validates(
867 build: fn(&PathInfo, &ComposerInputs) -> Option<(Vec<u8>, Plan, AddressTable)>,
868 path: &PathInfo,
869 inputs: &ComposerInputs,
870 name: &str,
871 ) {
872 let (_preamble, plan, _at) =
873 build(path, inputs).unwrap_or_else(|| panic!("[{name}] build None"));
874 let ops = plan_to_ledger_ops(&plan);
875 let mut v = crate::grammar_ledger::LedgerValidator::default();
876 assert!(
877 v.validate_full(&ops).is_ok(),
878 "[{name}] Plan must validate clean"
879 );
880 }
881
882 fn axis_path(combo: &[Prot]) -> PathInfo {
885 let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
886 let usdc = address!("A0b86991c6218b36c1D19D4a2e9Eb0cE3606eB48");
887 let hops = combo
888 .iter()
889 .enumerate()
890 .map(|(i, p)| match p {
891 Prot::V2 => HopInfo::V2(V2HopInfo {
892 pool_address: Address::from([0xA0 + u8::try_from(i).unwrap(); 20]),
893 token0_address: weth,
894 token1_address: usdc,
895 fee: 30,
896 zfo: true,
897 }),
898 Prot::V3 => HopInfo::V3(V3HopInfo {
899 pool_address: Address::from([0xB0 + u8::try_from(i).unwrap(); 20]),
900 token0_address: weth,
901 token1_address: usdc,
902 fee: 3000,
903 zfo: true,
904 }),
905 Prot::V4 => HopInfo::V4(V4HopInfo {
906 pool_manager_address: Address::ZERO,
907 pool_id_hex: format!("0x{i:x}"),
908 currency0_address: weth,
909 currency1_address: usdc,
910 fee: 500,
911 tick_spacing: 10,
912 hook_address: Address::ZERO,
913 zfo: true,
914 }),
915 })
916 .collect();
917 PathInfo::new(hops)
918 }
919
920 #[test]
921 fn family_axis_support_declares_the_honoring_surface() {
922 assert_eq!(
924 family_axis_support(&axis_path(&[Prot::V2, Prot::V3])).unwrap(),
925 AxisSupport::funding()
926 );
927 assert_eq!(
928 family_axis_support(&axis_path(&[Prot::V2, Prot::V2])).unwrap(),
929 AxisSupport::funding()
930 );
931 assert_eq!(
932 family_axis_support(&axis_path(&[Prot::V2, Prot::V2, Prot::V2])).unwrap(),
933 AxisSupport::funding()
934 );
935 assert_eq!(
937 family_axis_support(&axis_path(&[Prot::V4, Prot::V4])).unwrap(),
938 AxisSupport::capture()
939 );
940 assert_eq!(
941 family_axis_support(&axis_path(&[Prot::V4, Prot::V4, Prot::V4])).unwrap(),
942 AxisSupport::capture()
943 );
944 assert_eq!(
948 family_axis_support(&axis_path(&[Prot::V2, Prot::V2, Prot::V4])).unwrap(),
949 AxisSupport::none()
950 );
951 assert_eq!(
952 family_axis_support(&axis_path(&[Prot::V4, Prot::V2, Prot::V4])).unwrap(),
953 AxisSupport::none()
954 );
955 assert_eq!(
956 family_axis_support(&axis_path(&[Prot::V2, Prot::V4])).unwrap(),
957 AxisSupport::none()
958 );
959 assert_eq!(
961 family_axis_support(&axis_path(&[Prot::V3, Prot::V2])).unwrap(),
962 AxisSupport::none()
963 );
964 assert_eq!(
965 family_axis_support(&axis_path(&[Prot::V3, Prot::V3])).unwrap(),
966 AxisSupport::none()
967 );
968 assert_eq!(
969 family_axis_support(&axis_path(&[Prot::V3, Prot::V3, Prot::V3])).unwrap(),
970 AxisSupport::none()
971 );
972 for axes in [
974 family_axis_support(&axis_path(&[Prot::V2, Prot::V3])).unwrap(),
975 family_axis_support(&axis_path(&[Prot::V4, Prot::V4])).unwrap(),
976 family_axis_support(&axis_path(&[Prot::V3, Prot::V3])).unwrap(),
977 ] {
978 assert!(!axes.is_honored(Axis::Bribe), "no family branches bribe");
979 }
980 assert!(family_axis_support(&axis_path(&[Prot::V2, Prot::V3]))
981 .unwrap()
982 .is_honored(Axis::Funding));
983 assert!(family_axis_support(&axis_path(&[Prot::V4, Prot::V4]))
984 .unwrap()
985 .is_honored(Axis::Capture));
986 assert!(family_axis_support(&axis_path(&[Prot::V2])).is_none());
988 }
989
990 #[test]
991 fn v3_v2_plan_byte_parity_and_validates() {
992 let (path, inputs) = v3_v2_path_inputs();
993 plan_builds_and_validates(build_v3v2_plan, &path, &inputs, "v3_v2");
994 }
995 #[test]
996 fn v3_v3_plan_byte_parity_and_validates() {
997 let (path, inputs) = v3_v3_path_inputs();
998 plan_builds_and_validates(build_v3v3_plan, &path, &inputs, "v3_v3");
999 }
1000
1001 fn all_v2_chain_hops(n: usize) -> Vec<HopInfo> {
1005 let weth = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
1006 let usdc = address!("A0b86991c6218b36c1D19D4a2e9Eb0cE3606eB48");
1007 let wbtc = address!("2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599");
1008 let dai = address!("6B175474E89094C44Da98b954EedeAC495271d0F");
1009 let cycle = [weth, usdc, wbtc, dai];
1010 (0..n)
1011 .map(|i| {
1012 HopInfo::V2(V2HopInfo {
1013 pool_address: Address::from([0xD0 + u8::try_from(i).expect("2..=4 hops"); 20]),
1014 token0_address: cycle[i % 4],
1015 token1_address: cycle[(i + 1) % 4],
1016 fee: 30,
1017 zfo: true,
1018 })
1019 })
1020 .collect()
1021 }
1022
1023 #[test]
1033 fn all_v2_gate_rejects_unprofitable_inpathflash_stream() {
1034 let path = PathInfo::new(all_v2_chain_hops(2));
1035 let outs: Vec<u128> = vec![80_000, 60_000]; let consumed: Vec<u128> = vec![50_000, 80_000];
1037 for (flabel, funding) in [
1038 ("InPathFlash", FundingSource::InPathFlash),
1039 ("SelfFund", FundingSource::SelfFund),
1040 ] {
1041 let inputs = ComposerInputs {
1042 executor_address: address!("00000000000000000000000000000000000000ee"),
1043 pool_manager_address: address!("00000000000000000000000000000000000000ff"),
1044 weth_address: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
1045 optimal_input: 100_000,
1046 hop_outputs: &outs,
1047 consumed_inputs: &consumed,
1048 opts: crate::composers::EncodeOptions {
1049 funding,
1050 ..Default::default()
1051 },
1052 };
1053 let (_preamble, plan, _at) = build_all_v2_chain(&path, &inputs)
1054 .unwrap_or_else(|| panic!("[{flabel}] build None"));
1055 let ops = plan_to_ledger_ops(&plan);
1056 let mut v = crate::grammar_ledger::LedgerValidator::default();
1057 if flabel == "InPathFlash" {
1058 let err = v
1059 .validate_full(&ops)
1060 .expect_err("losing InPathFlash stream must be rejected");
1061 assert!(
1064 matches!(
1065 err,
1066 crate::grammar_ledger::ValidationError::Erc20TransferBeforeCredit { .. }
1067 ),
1068 "expected Erc20TransferBeforeCredit, got {err:?}"
1069 );
1070 let d = derive_all_v2_detailed(&path, &inputs);
1077 assert!(
1078 matches!(
1079 d,
1080 Derive::Rejected(
1081 crate::grammar_ledger::ValidationError::Erc20TransferBeforeCredit { .. }
1082 )
1083 ),
1084 "expected Reject(Erc20TransferBeforeCredit), got {d:?}"
1085 );
1086 let r = std::panic::catch_unwind(|| derive_all_v2(&path, &inputs));
1087 assert!(
1088 r.is_err(),
1089 "a validator Reject must be fatal (panic), never None (ADR-030)"
1090 );
1091 } else {
1092 assert!(
1093 v.validate_full(&ops).is_ok(),
1094 "losing SelfFund stream must still validate (no flash debt)"
1095 );
1096 }
1097 }
1098 }
1099
1100 #[test]
1101 fn v3_v2_plan_terminal_v2_before_seed_rejected() {
1102 let (path, inputs) = v3_v2_path_inputs();
1105 let (_preamble, mut plan, _at) = build_v3v2_plan(&path, &inputs).expect("v3_v2 build None");
1106 let outer = plan.last_mut().unwrap();
1109 if let PlanStep::FlashSwap { callback, .. } = outer {
1110 let swapcalc = callback.remove(2);
1112 callback.insert(0, swapcalc);
1113 } else {
1114 panic!("expected outer V3 FlashSwap");
1115 }
1116 let ops = plan_to_ledger_ops(&plan);
1117 let mut v = crate::grammar_ledger::LedgerValidator::default();
1118 assert!(
1119 matches!(
1120 v.validate_full(&ops),
1121 Err(crate::grammar_ledger::ValidationError::SwapCalcBeforeCredit { .. })
1122 ),
1123 "misordered Plan: V2SwapCalc before its pair seed must be rejected"
1124 );
1125 let _ = U256::ZERO;
1126 }
1127
1128 fn v4_v4_path_inputs() -> (PathInfo, ComposerInputs<'static>) {
1131 v4_v4_inputs(Terminal::Weth, crate::composers::EncodeOptions::default())
1133 }
1134
1135 #[test]
1136 fn v4_v4_plan_byte_parity_and_validates() {
1137 let (path, inputs) = v4_v4_path_inputs();
1138 plan_builds_and_validates(build_v4v4_plan, &path, &inputs, "v4_v4");
1139 }
1140
1141 #[test]
1142 fn v4_v4_plan_take_before_swap_rejected() {
1143 let (path, inputs) = v4_v4_path_inputs();
1146 let (_preamble, mut plan, _at) = build_v4v4_plan(&path, &inputs).expect("v4_v4 build None");
1147 let outer = plan.get_mut(0).unwrap();
1150 if let PlanStep::V4Unlock { inner, .. } = outer {
1151 let take = inner.remove(2);
1152 inner.insert(0, take);
1153 } else {
1154 panic!("expected outer V4Unlock");
1155 }
1156 let ops = plan_to_ledger_ops(&plan);
1157 let mut v = crate::grammar_ledger::LedgerValidator::default();
1158 assert!(
1159 matches!(
1160 v.validate_full(&ops),
1161 Err(crate::grammar_ledger::ValidationError::TakeBeforeCredit { .. })
1162 ),
1163 "misordered v4_v4 Plan: TakeDelta before the swap credits PM must be rejected"
1164 );
1165 let _ = U256::ZERO;
1166 }
1167
1168 #[test]
1169 fn v4_v4_plan_unsettled_delta_rejected() {
1170 let (path, mut inputs) = v4_v4_path_inputs();
1174 static CLAMPED: [u128; 2] = [1_000_000, 1_150_000];
1180 inputs.consumed_inputs = &CLAMPED;
1181 let (_preamble, mut plan, _at) = build_v4v4_plan(&path, &inputs).expect("v4_v4 build None");
1182 let outer = plan.get_mut(0).unwrap();
1183 if let PlanStep::V4Unlock { inner, .. } = outer {
1184 inner.pop();
1187 } else {
1188 panic!("expected outer V4Unlock");
1189 }
1190 let ops = plan_to_ledger_ops(&plan);
1191 let mut v = crate::grammar_ledger::LedgerValidator::default();
1192 assert!(
1193 matches!(
1194 v.validate_full(&ops),
1195 Err(crate::grammar_ledger::ValidationError::PmDeltaNonzero { .. })
1196 ),
1197 "v4_v4 Plan missing its settle must be rejected: nonzero PM delta at unlock end"
1198 );
1199 let _ = U256::ZERO;
1200 }
1201
1202 fn v4_v4_opts_inputs(
1206 opts: crate::composers::EncodeOptions,
1207 ) -> (PathInfo, ComposerInputs<'static>) {
1208 let (mut path, mut inputs) = v4_v4_path_inputs();
1209 let ComposerInputs {
1214 executor_address,
1215 pool_manager_address,
1216 weth_address,
1217 optimal_input,
1218 hop_outputs,
1219 consumed_inputs,
1220 opts: _,
1221 } = inputs;
1222 inputs = ComposerInputs {
1223 executor_address,
1224 pool_manager_address,
1225 weth_address,
1226 optimal_input,
1227 hop_outputs,
1228 consumed_inputs,
1229 opts,
1230 };
1231 let _ = &mut path;
1233 (path, inputs)
1234 }
1235
1236 #[test]
1237 fn v4_v4_plan_batch_byte_parity_and_validates() {
1238 let (path, inputs) = v4_v4_opts_inputs(crate::composers::EncodeOptions {
1242 erc6909_profit: false,
1243 use_v4_batch: true,
1244 ..Default::default()
1245 });
1246 plan_builds_and_validates(build_v4v4_plan, &path, &inputs, "v4_v4 batch");
1247 let (_preamble, plan, _at) =
1249 build_v4v4_plan(&path, &inputs).expect("v4_v4 batch build None");
1250 let outer = &plan[0];
1251 let PlanStep::V4Unlock { inner, .. } = outer else {
1252 panic!("expected outer V4Unlock");
1253 };
1254 assert_eq!(inner.len(), 2, "batch inner = [V4Batch, V4SettleAll]");
1255 assert!(
1256 matches!(inner[0], PlanStep::V4Batch { .. }),
1257 "first step is V4Batch"
1258 );
1259 assert!(
1260 matches!(inner[1], PlanStep::V4SettleAll),
1261 "trailing SettleAll"
1262 );
1263 let _ = U256::ZERO;
1264 }
1265
1266 #[test]
1267 fn v4_v4_plan_erc6909_byte_parity_and_validates() {
1268 let (path, inputs) = v4_v4_opts_inputs(crate::composers::EncodeOptions {
1271 erc6909_profit: true,
1272 use_v4_batch: false,
1273 ..Default::default()
1274 });
1275 plan_builds_and_validates(build_v4v4_plan, &path, &inputs, "v4_v4 erc6909");
1276 let (_preamble, plan, _at) =
1277 build_v4v4_plan(&path, &inputs).expect("v4_v4 erc6909 build None");
1278 let PlanStep::V4Unlock { inner, .. } = &plan[0] else {
1279 panic!("expected outer V4Unlock");
1280 };
1281 assert_eq!(
1282 inner.len(),
1283 4,
1284 "erc6909 inner = [V4Swap, V4Swap, V4Mint, V4SettleAll]"
1285 );
1286 assert!(
1287 matches!(inner[2], PlanStep::V4Mint { .. }),
1288 "profit step is V4Mint"
1289 );
1290 let _ = U256::ZERO;
1291 }
1292
1293 #[test]
1294 fn v4_v4_batch_and_erc6909_capture_weth_terminal_composes() {
1295 let (path, inputs) = v4_v4_opts_inputs(crate::composers::EncodeOptions {
1302 erc6909_profit: true,
1303 use_v4_batch: true,
1304 ..Default::default()
1305 });
1306 let (_preamble, plan, _at) = build_v4v4_plan(&path, &inputs)
1307 .expect("batch + erc6909 WETH-terminal must build (TGUZCT)");
1308 let PlanStep::V4Unlock { inner, .. } = &plan[0] else {
1309 panic!("expected outer V4Unlock");
1310 };
1311 assert_eq!(
1312 inner.len(),
1313 3,
1314 "erc6909+batch inner = [V4Batch(open_weth), V4Mint, V4SettleAll]"
1315 );
1316 assert!(
1317 matches!(
1318 &inner[0],
1319 PlanStep::V4Batch {
1320 open_weth: true,
1321 ..
1322 }
1323 ),
1324 "the batch must be the open-weth (0x43) variant"
1325 );
1326 assert!(matches!(&inner[1], PlanStep::V4Mint { .. }));
1327 assert!(matches!(&inner[2], PlanStep::V4SettleAll));
1328 let ops = plan_to_ledger_ops(&plan);
1330 assert!(
1331 crate::grammar_ledger::LedgerValidator::default()
1332 .validate_full(&ops)
1333 .is_ok(),
1334 "open batch -> WETH mint -> settle-all must validate"
1335 );
1336 }
1337
1338 #[test]
1339 fn v4_v4_plan_mint_before_swap_rejected() {
1340 let (path, inputs) = v4_v4_opts_inputs(crate::composers::EncodeOptions {
1344 erc6909_profit: true,
1345 use_v4_batch: false,
1346 ..Default::default()
1347 });
1348 let (_preamble, mut plan, _at) =
1349 build_v4v4_plan(&path, &inputs).expect("v4_v4 erc6909 build None");
1350 let outer = plan.get_mut(0).unwrap();
1353 if let PlanStep::V4Unlock { inner, .. } = outer {
1354 let mint = inner.remove(2);
1355 inner.insert(0, mint);
1356 } else {
1357 panic!("expected outer V4Unlock");
1358 }
1359 let ops = plan_to_ledger_ops(&plan);
1360 let mut v = crate::grammar_ledger::LedgerValidator::default();
1361 assert!(
1362 matches!(
1363 v.validate_full(&ops),
1364 Err(crate::grammar_ledger::ValidationError::TakeBeforeCredit { .. })
1365 ),
1366 "misordered v4_v4 erc6909 Plan: V4Mint before the swap credits PM must be rejected"
1367 );
1368 let _ = U256::ZERO;
1369 }
1370
1371 #[test]
1375 fn v4_v4_terminal_opt_matrix_byte_parity_and_validates() {
1376 use crate::composers::EncodeOptions;
1377 let modes = [
1378 ("default", EncodeOptions::default()),
1379 (
1380 "batch",
1381 EncodeOptions {
1382 erc6909_profit: false,
1383 use_v4_batch: true,
1384 ..Default::default()
1385 },
1386 ),
1387 (
1388 "erc6909",
1389 EncodeOptions {
1390 erc6909_profit: true,
1391 use_v4_batch: false,
1392 ..Default::default()
1393 },
1394 ),
1395 (
1396 "batch+erc6909",
1397 EncodeOptions {
1398 erc6909_profit: true,
1399 use_v4_batch: true,
1400 ..Default::default()
1401 },
1402 ),
1403 ];
1404 let terminals = [
1405 ("weth", Terminal::Weth),
1406 ("tok", Terminal::Tok),
1407 ("native", Terminal::Native),
1408 ];
1409 for (t_name, terminal) in terminals {
1410 for (m_name, opts) in modes {
1411 let label = format!("v4_v4 {t_name}+{m_name}");
1412 let (path, inputs) = v4_v4_inputs(terminal, opts);
1413 plan_builds_and_validates(build_v4v4_plan, &path, &inputs, &label);
1416 }
1417 }
1418 let _ = U256::ZERO;
1419 }
1420
1421 #[test]
1422 fn v4_v4_plan_batch_native_terminal_emits_no_take() {
1423 let (path, inputs) = v4_v4_inputs(
1428 Terminal::Native,
1429 crate::composers::EncodeOptions {
1430 erc6909_profit: false,
1431 use_v4_batch: true,
1432 ..Default::default()
1433 },
1434 );
1435 let (_preamble, plan, _at) =
1436 build_v4v4_plan(&path, &inputs).expect("v4_v4 native+batch build None");
1437 let PlanStep::V4Unlock { inner, .. } = &plan[0] else {
1438 panic!("expected outer V4Unlock");
1439 };
1440 assert_eq!(
1441 inner.len(),
1442 2,
1443 "native+batch inner = [V4Batch, V4SettleAll] (no take)"
1444 );
1445 assert!(matches!(inner[0], PlanStep::V4Batch { .. }));
1446 assert!(matches!(inner[1], PlanStep::V4SettleAll));
1447 let _ = U256::ZERO;
1448 }
1449
1450 #[test]
1453 fn v4_v4_gap_byte_parity_and_validates() {
1454 for (name, gap) in [("wrap", Gap::Wrap), ("unwrap", Gap::Unwrap)] {
1458 let label = format!("v4_v4 gap {name}");
1459 let (path, inputs) = v4_v4_gap_inputs(gap, crate::composers::EncodeOptions::default());
1460 plan_builds_and_validates(build_v4v4_plan, &path, &inputs, &label);
1461 }
1462 let _ = U256::ZERO;
1463 }
1464
1465 #[test]
1466 fn v4_v4_gap_opt_matrix_byte_parity_and_validates() {
1467 use crate::composers::EncodeOptions;
1478 let modes = [
1479 ("default", EncodeOptions::default()),
1480 (
1481 "batch",
1482 EncodeOptions {
1483 erc6909_profit: false,
1484 use_v4_batch: true,
1485 ..Default::default()
1486 },
1487 ),
1488 (
1489 "erc6909",
1490 EncodeOptions {
1491 erc6909_profit: true,
1492 use_v4_batch: false,
1493 ..Default::default()
1494 },
1495 ),
1496 (
1497 "batch+erc6909",
1498 EncodeOptions {
1499 erc6909_profit: true,
1500 use_v4_batch: true,
1501 ..Default::default()
1502 },
1503 ),
1504 ];
1505 for (g_name, gap) in [("wrap", Gap::Wrap), ("unwrap", Gap::Unwrap)] {
1506 for (m_name, opts) in modes {
1507 let label = format!("v4_v4 gap {g_name}+{m_name}");
1508 let (path, inputs) = v4_v4_gap_inputs(gap, opts);
1509 plan_builds_and_validates(build_v4v4_plan, &path, &inputs, &label);
1510 }
1511 }
1512 let _ = U256::ZERO;
1513 }
1514
1515 #[test]
1516 fn v4_v4_gap_shape_is_swap_bridge_swap_settle_take() {
1517 let (path, inputs) =
1521 v4_v4_gap_inputs(Gap::Wrap, crate::composers::EncodeOptions::default());
1522 let (_preamble, plan, _at) = build_v4v4_plan(&path, &inputs).expect("v4_v4 gap build None");
1523 let PlanStep::V4Unlock { inner, .. } = &plan[0] else {
1524 panic!("expected outer V4Unlock");
1525 };
1526 assert_eq!(inner.len(), 7, "gap inner = 7 steps");
1527 assert!(matches!(inner[0], PlanStep::V4Swap { .. }), "0: swap a");
1528 assert!(
1529 matches!(inner[1], PlanStep::V4TakeCompact { .. }),
1530 "1: bridge take"
1531 );
1532 assert!(
1533 matches!(inner[2], PlanStep::WethDeposit { .. }),
1534 "2: wrap deposit (Wrap gap)"
1535 );
1536 assert!(matches!(inner[3], PlanStep::V4Swap { .. }), "3: swap b");
1537 assert!(
1538 matches!(inner[4], PlanStep::V4SettleDelta { .. }),
1539 "4: settle"
1540 );
1541 assert!(
1542 matches!(inner[5], PlanStep::V4TakeDelta { .. }),
1543 "5: profit take"
1544 );
1545 assert!(matches!(inner[6], PlanStep::V4SettleAll), "6: settle all");
1546 let _ = U256::ZERO;
1547 }
1548 fn weth_withdraw_amounts(bytes: &[u8]) -> Vec<u128> {
1560 bytes
1561 .windows(33)
1562 .filter(|w| w[0] == 0x13)
1563 .map(|w| {
1564 let mut a = [0u8; 16];
1565 a.copy_from_slice(&w[17..33]);
1566 u128::from_be_bytes(a)
1567 })
1568 .collect()
1569 }
1570
1571 fn count_v4_take_delta(bytes: &[u8]) -> usize {
1573 bytes.iter().filter(|&&b| b == 0x50).count()
1574 }
1575
1576 #[test]
1577 fn v4_v4_native_capture_weth_terminal_appends_weth_withdraw() {
1578 let (path, inputs) = v4_v4_opts_inputs(crate::composers::EncodeOptions {
1582 capture: crate::grammar_ledger::ProfitCapture::Native,
1583 ..Default::default()
1584 });
1585 let bytes =
1586 derive_shape(&path, &inputs).expect("v4_v4 native-capture WETH terminal must derive");
1587 assert!(
1588 weth_withdraw_amounts(&bytes).contains(&200_000),
1589 "Native capture must append WETH_WITHDRAW of the profit; got {:?}",
1590 weth_withdraw_amounts(&bytes)
1591 );
1592 assert!(
1594 count_v4_take_delta(&bytes) >= 1,
1595 "V4_TAKE_DELTA custody take must precede the withdraw"
1596 );
1597 }
1598
1599 #[test]
1600 fn v4_v4_custody_capture_emits_no_weth_withdraw() {
1601 let (path, inputs) = v4_v4_opts_inputs(crate::composers::EncodeOptions::default());
1603 let bytes = derive_shape(&path, &inputs).expect("v4_v4 custody WETH terminal must derive");
1604 assert!(
1605 !weth_withdraw_amounts(&bytes).contains(&200_000),
1606 "Custody capture must NOT append a WETH_WITHDRAW of the profit"
1607 );
1608 }
1609
1610 #[test]
1611 fn v4_v4_native_capture_tok_terminal_declines() {
1612 let (path, inputs) = v4_v4_inputs(
1616 Terminal::Tok,
1617 crate::composers::EncodeOptions {
1618 capture: crate::grammar_ledger::ProfitCapture::Native,
1619 ..Default::default()
1620 },
1621 );
1622 assert!(
1623 derive_shape(&path, &inputs).is_none(),
1624 "Native capture on a non-WETH/non-native tok terminal must decline"
1625 );
1626 }
1627
1628 #[test]
1629 fn v4_v4_native_capture_native_terminal_is_noop() {
1630 let (path, inputs) = v4_v4_inputs(
1633 Terminal::Native,
1634 crate::composers::EncodeOptions {
1635 capture: crate::grammar_ledger::ProfitCapture::Native,
1636 ..Default::default()
1637 },
1638 );
1639 let bytes = derive_shape(&path, &inputs)
1640 .expect("v4_v4 native-capture native terminal must derive (no-op)");
1641 assert!(
1642 !weth_withdraw_amounts(&bytes).iter().any(|&a| a > 0),
1643 "Native terminal + Native capture is already native; no withdraw"
1644 );
1645 }
1646 #[test]
1647 fn v4_v4_native_capture_plan_byte_parity_and_validates() {
1648 let (path, inputs) = v4_v4_opts_inputs(crate::composers::EncodeOptions {
1654 capture: ProfitCapture::Native,
1655 ..Default::default()
1656 });
1657 plan_builds_and_validates(build_v4v4_plan, &path, &inputs, "v4_v4 native-capture");
1658 }
1659}