1use crate::data::{
10 AddLiquidityParams, CollectFeesParams, ExactInParams, ExactOutParams, PlanFragment, PoolState,
11 Quote, RemoveAndCollectParams, RemoveLiquidityParams, SwapRouterKind,
12};
13use alloy_primitives::Address;
14use alloy_primitives::{aliases::U160, U256};
15use alloy_sol_types::{sol, SolCall};
16use wp_evm_base::types::{Call, SlippageBps, TokenApproval};
17use wp_evm_v3_interfaces::periphery::router::IPeripheryRouter;
18
19sol! {
20 #[derive(Debug)]
21 struct ExactInputSingleParams {
22 address tokenIn;
23 address tokenOut;
24 uint24 fee;
25 address recipient;
26 uint256 deadline;
27 uint256 amountIn;
28 uint256 amountOutMinimum;
29 uint160 sqrtPriceLimitX96;
30 }
31
32 function exactInputSingle(ExactInputSingleParams params)
33 external payable returns (uint256 amountOut);
34}
35
36sol! {
37 #[derive(Debug)]
41 struct ExactInputSingleParamsV02 {
42 address tokenIn;
43 address tokenOut;
44 uint24 fee;
45 address recipient;
46 uint256 amountIn;
47 uint256 amountOutMinimum;
48 uint160 sqrtPriceLimitX96;
49 }
50
51 interface ISwapRouter02 {
54 function exactInputSingle(ExactInputSingleParamsV02 params)
55 external payable returns (uint256 amountOut);
56 }
57}
58
59sol! {
60 #[derive(Debug)]
63 struct ExactOutputSingleParams {
64 address tokenIn;
65 address tokenOut;
66 uint24 fee;
67 address recipient;
68 uint256 deadline;
69 uint256 amountOut;
70 uint256 amountInMaximum;
71 uint160 sqrtPriceLimitX96;
72 }
73
74 function exactOutputSingle(ExactOutputSingleParams params)
75 external payable returns (uint256 amountIn);
76}
77
78sol! {
79 #[derive(Debug)]
82 struct ExactOutputSingleParamsV02 {
83 address tokenIn;
84 address tokenOut;
85 uint24 fee;
86 address recipient;
87 uint256 amountOut;
88 uint256 amountInMaximum;
89 uint160 sqrtPriceLimitX96;
90 }
91
92 interface ISwapRouter02ExactOut {
94 function exactOutputSingle(ExactOutputSingleParamsV02 params)
95 external payable returns (uint256 amountIn);
96 }
97}
98
99sol! {
100 #[derive(Debug)]
101 struct MintParams {
102 address token0;
103 address token1;
104 uint24 fee;
105 int24 tickLower;
106 int24 tickUpper;
107 uint256 amount0Desired;
108 uint256 amount1Desired;
109 uint256 amount0Min;
110 uint256 amount1Min;
111 address recipient;
112 uint256 deadline;
113 }
114 function mint(MintParams params) external payable
115 returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
116
117 #[derive(Debug)]
118 struct IncreaseLiquidityParams {
119 uint256 tokenId;
120 uint256 amount0Desired;
121 uint256 amount1Desired;
122 uint256 amount0Min;
123 uint256 amount1Min;
124 uint256 deadline;
125 }
126 function increaseLiquidity(IncreaseLiquidityParams params) external payable
127 returns (uint128 liquidity, uint256 amount0, uint256 amount1);
128
129 #[derive(Debug)]
130 struct DecreaseLiquidityParams {
131 uint256 tokenId;
132 uint128 liquidity;
133 uint256 amount0Min;
134 uint256 amount1Min;
135 uint256 deadline;
136 }
137 function decreaseLiquidity(DecreaseLiquidityParams params) external payable
138 returns (uint256 amount0, uint256 amount1);
139
140 #[derive(Debug)]
141 struct CollectParams {
142 uint256 tokenId;
143 address recipient;
144 uint128 amount0Max;
145 uint128 amount1Max;
146 }
147 function collect(CollectParams params) external payable
148 returns (uint256 amount0, uint256 amount1);
149
150 function burn(uint256 tokenId) external payable;
151}
152
153pub fn swap_exact_in(
158 state: &PoolState,
159 quote: &Quote,
160 params: &ExactInParams,
161 slippage: SlippageBps,
162 deadline: u64,
163 router: Address,
164 kind: SwapRouterKind,
165) -> PlanFragment {
166 swap_exact_in_with_fee_fn(state, quote, params, slippage, deadline, router, kind, |s| s.fee)
167}
168
169#[allow(
177 clippy::too_many_arguments,
178 reason = "public planner API threads router ABI kind plus fee injection"
179)]
180pub fn swap_exact_in_with_fee_fn<F>(
181 state: &PoolState,
182 quote: &Quote,
183 params: &ExactInParams,
184 slippage: SlippageBps,
185 deadline: u64,
186 router: Address,
187 kind: SwapRouterKind,
188 fee_fn: F,
189) -> PlanFragment
190where
191 F: Fn(&PoolState) -> u32,
192{
193 let effective_fee = fee_fn(state);
194 let amount_out_min = apply_slippage_min(quote.amount_out, slippage);
195 let fee_u24 = alloy_primitives::aliases::U24::from(effective_fee);
196
197 let calldata = match kind {
198 SwapRouterKind::V1 => {
199 let call_params = ExactInputSingleParams {
200 tokenIn: params.token_in,
201 tokenOut: params.token_out,
202 fee: fee_u24,
203 recipient: params.recipient,
204 deadline: U256::from(deadline),
205 amountIn: params.amount_in,
206 amountOutMinimum: amount_out_min,
207 sqrtPriceLimitX96: U160::ZERO,
208 };
209 exactInputSingleCall { params: call_params }.abi_encode().into()
210 }
211 SwapRouterKind::V02 => {
212 let call_params = ExactInputSingleParamsV02 {
213 tokenIn: params.token_in,
214 tokenOut: params.token_out,
215 fee: fee_u24,
216 recipient: params.recipient,
217 amountIn: params.amount_in,
218 amountOutMinimum: amount_out_min,
219 sqrtPriceLimitX96: U160::ZERO,
220 };
221 ISwapRouter02::exactInputSingleCall { params: call_params }.abi_encode().into()
222 }
223 };
224
225 PlanFragment {
226 calls: vec![Call { target: router, calldata, value: U256::ZERO }],
227 approvals: vec![TokenApproval {
228 token: params.token_in,
229 spender: router,
230 min_amount: params.amount_in,
231 }],
232 value: U256::ZERO,
233 }
234}
235
236pub fn swap_exact_out(
245 state: &PoolState,
246 quote: &Quote,
247 params: &ExactOutParams,
248 slippage: SlippageBps,
249 deadline: u64,
250 router: Address,
251 kind: SwapRouterKind,
252) -> PlanFragment {
253 swap_exact_out_with_fee_fn(state, quote, params, slippage, deadline, router, kind, |s| s.fee)
254}
255
256#[allow(
258 clippy::too_many_arguments,
259 reason = "public planner API threads router ABI kind plus fee injection"
260)]
261pub fn swap_exact_out_with_fee_fn<F>(
262 state: &PoolState,
263 quote: &Quote,
264 params: &ExactOutParams,
265 slippage: SlippageBps,
266 deadline: u64,
267 router: Address,
268 kind: SwapRouterKind,
269 fee_fn: F,
270) -> PlanFragment
271where
272 F: Fn(&PoolState) -> u32,
273{
274 let effective_fee = fee_fn(state);
275 let amount_in_max = apply_slippage_max(quote.amount_in, slippage);
276 let fee_u24 = alloy_primitives::aliases::U24::from(effective_fee);
277
278 let calldata = match kind {
279 SwapRouterKind::V1 => {
280 let call_params = ExactOutputSingleParams {
281 tokenIn: params.token_in,
282 tokenOut: params.token_out,
283 fee: fee_u24,
284 recipient: params.recipient,
285 deadline: U256::from(deadline),
286 amountOut: params.amount_out,
287 amountInMaximum: amount_in_max,
288 sqrtPriceLimitX96: U160::ZERO,
289 };
290 exactOutputSingleCall { params: call_params }.abi_encode().into()
291 }
292 SwapRouterKind::V02 => {
293 let call_params = ExactOutputSingleParamsV02 {
294 tokenIn: params.token_in,
295 tokenOut: params.token_out,
296 fee: fee_u24,
297 recipient: params.recipient,
298 amountOut: params.amount_out,
299 amountInMaximum: amount_in_max,
300 sqrtPriceLimitX96: U160::ZERO,
301 };
302 ISwapRouter02ExactOut::exactOutputSingleCall { params: call_params }.abi_encode().into()
303 }
304 };
305
306 PlanFragment {
307 calls: vec![Call { target: router, calldata, value: U256::ZERO }],
308 approvals: vec![TokenApproval {
309 token: params.token_in,
310 spender: router,
311 min_amount: amount_in_max,
312 }],
313 value: U256::ZERO,
314 }
315}
316
317pub fn apply_slippage_min(quoted: U256, slippage: SlippageBps) -> U256 {
321 let bps = U256::from(slippage.as_bps());
322 let denom = U256::from(10_000u64);
323 quoted * (denom - bps) / denom
324}
325
326pub fn apply_slippage_max(quoted: U256, slippage: SlippageBps) -> U256 {
328 let bps = U256::from(slippage.as_bps());
329 let denom = U256::from(10_000u64);
330 quoted * (denom + bps) / denom
331}
332
333pub fn add_liquidity(
338 params: &AddLiquidityParams,
339 slippage: SlippageBps,
340 deadline: u64,
341 position_manager: Address,
342) -> PlanFragment {
343 add_liquidity_with_min(
344 params,
345 apply_slippage_min(params.amount0_desired, slippage),
346 apply_slippage_min(params.amount1_desired, slippage),
347 deadline,
348 position_manager,
349 )
350}
351
352pub fn add_liquidity_with_min(
361 params: &AddLiquidityParams,
362 amount0_min: U256,
363 amount1_min: U256,
364 deadline: u64,
365 position_manager: Address,
366) -> PlanFragment {
367 let mint_params = MintParams {
368 token0: params.token0,
369 token1: params.token1,
370 fee: alloy_primitives::aliases::U24::from(params.fee),
371 tickLower: alloy_primitives::aliases::I24::try_from(params.tick_lower)
372 .expect("tick_lower within i24 range"),
373 tickUpper: alloy_primitives::aliases::I24::try_from(params.tick_upper)
374 .expect("tick_upper within i24 range"),
375 amount0Desired: params.amount0_desired,
376 amount1Desired: params.amount1_desired,
377 amount0Min: amount0_min,
378 amount1Min: amount1_min,
379 recipient: params.recipient,
380 deadline: U256::from(deadline),
381 };
382 let calldata = mintCall { params: mint_params }.abi_encode().into();
383 PlanFragment {
384 calls: vec![Call { target: position_manager, calldata, value: U256::ZERO }],
385 approvals: vec![
386 TokenApproval {
387 token: params.token0,
388 spender: position_manager,
389 min_amount: params.amount0_desired,
390 },
391 TokenApproval {
392 token: params.token1,
393 spender: position_manager,
394 min_amount: params.amount1_desired,
395 },
396 ],
397 value: U256::ZERO,
398 }
399}
400
401#[allow(
414 clippy::too_many_arguments,
415 reason = "public planner API mirrors NFPM increaseLiquidity parameters"
416)]
417pub fn increase_liquidity(
418 token_id: U256,
419 token0: Address,
420 token1: Address,
421 amount0_desired: U256,
422 amount1_desired: U256,
423 slippage: SlippageBps,
424 deadline: u64,
425 position_manager: Address,
426) -> PlanFragment {
427 increase_liquidity_with_min(
428 token_id,
429 token0,
430 token1,
431 amount0_desired,
432 amount1_desired,
433 apply_slippage_min(amount0_desired, slippage),
434 apply_slippage_min(amount1_desired, slippage),
435 deadline,
436 position_manager,
437 )
438}
439
440#[allow(
449 clippy::too_many_arguments,
450 reason = "public planner API mirrors NFPM increaseLiquidity parameters"
451)]
452pub fn increase_liquidity_with_min(
453 token_id: U256,
454 token0: Address,
455 token1: Address,
456 amount0_desired: U256,
457 amount1_desired: U256,
458 amount0_min: U256,
459 amount1_min: U256,
460 deadline: u64,
461 position_manager: Address,
462) -> PlanFragment {
463 let inc_params = IncreaseLiquidityParams {
464 tokenId: token_id,
465 amount0Desired: amount0_desired,
466 amount1Desired: amount1_desired,
467 amount0Min: amount0_min,
468 amount1Min: amount1_min,
469 deadline: U256::from(deadline),
470 };
471 let calldata = increaseLiquidityCall { params: inc_params }.abi_encode().into();
472 PlanFragment {
473 calls: vec![Call { target: position_manager, calldata, value: U256::ZERO }],
474 approvals: vec![
475 TokenApproval { token: token0, spender: position_manager, min_amount: amount0_desired },
476 TokenApproval { token: token1, spender: position_manager, min_amount: amount1_desired },
477 ],
478 value: U256::ZERO,
479 }
480}
481
482pub fn remove_liquidity(
492 params: &RemoveLiquidityParams,
493 deadline: u64,
494 position_manager: Address,
495) -> PlanFragment {
496 let dec = DecreaseLiquidityParams {
497 tokenId: params.token_id,
498 liquidity: params.liquidity,
499 amount0Min: params.amount0_min.unwrap_or(U256::ZERO),
500 amount1Min: params.amount1_min.unwrap_or(U256::ZERO),
501 deadline: U256::from(deadline),
502 };
503 let calldata = decreaseLiquidityCall { params: dec }.abi_encode().into();
504 PlanFragment {
505 calls: vec![Call { target: position_manager, calldata, value: U256::ZERO }],
506 approvals: vec![],
507 value: U256::ZERO,
508 }
509}
510
511pub fn remove_liquidity_and_collect(
539 params: &RemoveAndCollectParams,
540 deadline: u64,
541 position_manager: Address,
542) -> PlanFragment {
543 let dec_params = DecreaseLiquidityParams {
545 tokenId: params.token_id,
546 liquidity: params.liquidity,
547 amount0Min: params.amount0_min.unwrap_or(U256::ZERO),
548 amount1Min: params.amount1_min.unwrap_or(U256::ZERO),
549 deadline: U256::from(deadline),
550 };
551 let decrease_calldata: alloy_primitives::Bytes =
552 decreaseLiquidityCall { params: dec_params }.abi_encode().into();
553
554 let collect_params = CollectParams {
556 tokenId: params.token_id,
557 recipient: params.recipient,
558 amount0Max: u128::MAX,
559 amount1Max: u128::MAX,
560 };
561 let collect_calldata: alloy_primitives::Bytes =
562 collectCall { params: collect_params }.abi_encode().into();
563
564 let mut multicall_data = vec![decrease_calldata, collect_calldata];
566 if params.burn {
567 let burn_calldata: alloy_primitives::Bytes =
568 burnCall { tokenId: params.token_id }.abi_encode().into();
569 multicall_data.push(burn_calldata);
570 }
571 let multicall_calldata =
572 IPeripheryRouter::multicallCall { data: multicall_data }.abi_encode().into();
573
574 PlanFragment {
575 calls: vec![Call {
576 target: position_manager,
577 value: U256::ZERO,
578 calldata: multicall_calldata,
579 }],
580 approvals: vec![],
581 value: U256::ZERO,
582 }
583}
584
585pub fn collect_fees(params: &CollectFeesParams, position_manager: Address) -> PlanFragment {
590 let coll = CollectParams {
591 tokenId: params.token_id,
592 recipient: params.recipient,
593 amount0Max: u128::MAX,
594 amount1Max: u128::MAX,
595 };
596 let calldata = collectCall { params: coll }.abi_encode().into();
597 PlanFragment {
598 calls: vec![Call { target: position_manager, calldata, value: U256::ZERO }],
599 approvals: vec![],
600 value: U256::ZERO,
601 }
602}
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607 use crate::data::{
608 AddLiquidityParams, CollectFeesParams, RemoveAndCollectParams, RemoveLiquidityParams,
609 SwapRouterKind, V3ProtocolConfig,
610 };
611 use alloy_primitives::{address, b256, Address};
612
613 const TEST_CFG: V3ProtocolConfig = V3ProtocolConfig {
614 factory: address!("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
615 pool_deployer: None,
616 router: address!("0xE592427A0AEce92De3Edee1F18E0157C05861564"),
617 swap_router_kind: SwapRouterKind::V1,
618 position_mgr: address!("0xC36442b4a4522E871399CD717aBDD847Ab11FE88"),
619 init_code_hash: b256!("0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54"),
620 fee_tiers: &[100, 500, 3000, 10000],
621 multicall: address!("0xcA11bde05977b3631167028862bE2a173976CA11"),
622 quoter: None,
623 };
624
625 fn dummy_pool_state(t0: Address, t1: Address) -> PoolState {
626 PoolState {
627 token0: t0,
628 token1: t1,
629 fee: 3000,
630 tick_spacing: 60,
631 sqrt_price_x96: U256::from(1u64) << 96,
632 liquidity: 0,
633 tick: 0,
634 ticks: vec![],
635 }
636 }
637
638 fn fixture_exact_in_params() -> ExactInParams {
639 ExactInParams {
640 token_in: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
641 token_out: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
642 amount_in: U256::from(1_000_000u64),
643 recipient: address!("0000000000000000000000000000000000000099"),
644 }
645 }
646
647 fn fixture_exact_out_params() -> ExactOutParams {
648 ExactOutParams {
649 token_in: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
650 token_out: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
651 amount_out: U256::from(500_000_000_000_000u64),
652 recipient: address!("0000000000000000000000000000000000000099"),
653 }
654 }
655
656 fn fixture_quote(state: &PoolState) -> Quote {
657 Quote {
658 amount_in: U256::from(1_000_000u64),
659 amount_out: U256::from(500_000_000_000_000u64),
660 sqrt_price_x96_after: state.sqrt_price_x96,
661 price_impact_bps: 0,
662 }
663 }
664
665 #[test]
666 fn plan_swap_emits_one_call_with_router_target() {
667 let token_in = address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
668 let token_out = address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
669 let s = dummy_pool_state(token_in, token_out);
670 let q = Quote {
671 amount_in: U256::from(1_000_000u64),
672 amount_out: U256::from(500_000_000_000_000u64),
673 sqrt_price_x96_after: s.sqrt_price_x96,
674 price_impact_bps: 0,
675 };
676 let p = ExactInParams {
677 token_in,
678 token_out,
679 amount_in: q.amount_in,
680 recipient: address!("0x0000000000000000000000000000000000000099"),
681 };
682 let frag = swap_exact_in(
683 &s,
684 &q,
685 &p,
686 SlippageBps::new(50),
687 9_999_999_999,
688 TEST_CFG.router,
689 SwapRouterKind::V1,
690 );
691 assert_eq!(frag.calls.len(), 1);
692 assert_eq!(frag.calls[0].target, TEST_CFG.router);
693 assert_eq!(frag.approvals.len(), 1);
694 assert_eq!(frag.approvals[0].token, token_in);
695 assert_eq!(frag.approvals[0].spender, TEST_CFG.router);
696 assert_eq!(frag.approvals[0].min_amount, q.amount_in);
697 assert_eq!(frag.value, U256::ZERO);
698 }
699
700 #[test]
701 fn swap_exact_in_v1_emits_v1_selector() {
702 let params = fixture_exact_in_params();
703 let state = dummy_pool_state(params.token_in, params.token_out);
704 let quote = fixture_quote(&state);
705 let plan = swap_exact_in(
706 &state,
707 "e,
708 ¶ms,
709 SlippageBps::new(50),
710 0,
711 TEST_CFG.router,
712 SwapRouterKind::V1,
713 );
714 assert_eq!(
715 &plan.calls[0].calldata[..4],
716 &[0x41, 0x4b, 0xf3, 0x89],
717 "V1 selector must be 0x414bf389"
718 );
719 }
720
721 #[test]
722 fn swap_exact_in_v02_emits_v02_selector() {
723 let params = fixture_exact_in_params();
724 let state = dummy_pool_state(params.token_in, params.token_out);
725 let quote = fixture_quote(&state);
726 let plan = swap_exact_in(
727 &state,
728 "e,
729 ¶ms,
730 SlippageBps::new(50),
731 0,
732 TEST_CFG.router,
733 SwapRouterKind::V02,
734 );
735 assert_eq!(
736 &plan.calls[0].calldata[..4],
737 &[0x04, 0xe4, 0x5a, 0xaf],
738 "V02 selector must be 0x04e45aaf"
739 );
740 }
741
742 #[test]
743 fn swap_exact_in_v1_includes_deadline() {
744 let params = fixture_exact_in_params();
745 let state = dummy_pool_state(params.token_in, params.token_out);
746 let quote = fixture_quote(&state);
747 let v1_plan = swap_exact_in(
748 &state,
749 "e,
750 ¶ms,
751 SlippageBps::new(50),
752 1_700_000_000,
753 TEST_CFG.router,
754 SwapRouterKind::V1,
755 );
756 let v02_plan = swap_exact_in(
757 &state,
758 "e,
759 ¶ms,
760 SlippageBps::new(50),
761 1_700_000_000,
762 TEST_CFG.router,
763 SwapRouterKind::V02,
764 );
765 assert_eq!(
766 v1_plan.calls[0].calldata.len(),
767 v02_plan.calls[0].calldata.len() + 32,
768 "V1 calldata must be 32 bytes longer than V02 (deadline field)"
769 );
770 }
771
772 #[test]
773 fn swap_exact_out_v1_emits_v1_selector() {
774 let params = fixture_exact_out_params();
775 let state = dummy_pool_state(params.token_in, params.token_out);
776 let quote = fixture_quote(&state);
777 let plan = swap_exact_out(
778 &state,
779 "e,
780 ¶ms,
781 SlippageBps::new(50),
782 0,
783 TEST_CFG.router,
784 SwapRouterKind::V1,
785 );
786 assert_eq!(
787 &plan.calls[0].calldata[..4],
788 &[0xdb, 0x3e, 0x21, 0x98],
789 "V1 selector must be 0xdb3e2198"
790 );
791 }
792
793 #[test]
794 fn swap_exact_out_v02_emits_v02_selector() {
795 let params = fixture_exact_out_params();
796 let state = dummy_pool_state(params.token_in, params.token_out);
797 let quote = fixture_quote(&state);
798 let plan = swap_exact_out(
799 &state,
800 "e,
801 ¶ms,
802 SlippageBps::new(50),
803 0,
804 TEST_CFG.router,
805 SwapRouterKind::V02,
806 );
807 assert_eq!(
808 &plan.calls[0].calldata[..4],
809 &[0x50, 0x23, 0xb4, 0xdf],
810 "V02 selector must be 0x5023b4df"
811 );
812 }
813
814 #[test]
815 fn swap_exact_out_v1_includes_deadline() {
816 let params = fixture_exact_out_params();
817 let state = dummy_pool_state(params.token_in, params.token_out);
818 let quote = fixture_quote(&state);
819 let v1_plan = swap_exact_out(
820 &state,
821 "e,
822 ¶ms,
823 SlippageBps::new(50),
824 1_700_000_000,
825 TEST_CFG.router,
826 SwapRouterKind::V1,
827 );
828 let v02_plan = swap_exact_out(
829 &state,
830 "e,
831 ¶ms,
832 SlippageBps::new(50),
833 1_700_000_000,
834 TEST_CFG.router,
835 SwapRouterKind::V02,
836 );
837 assert_eq!(
838 v1_plan.calls[0].calldata.len(),
839 v02_plan.calls[0].calldata.len() + 32,
840 "V1 calldata must be 32 bytes longer than V02 (deadline field)"
841 );
842 }
843
844 #[test]
845 fn swap_exact_out_approval_min_amount_is_amount_in_max() {
846 let params = fixture_exact_out_params();
847 let state = dummy_pool_state(params.token_in, params.token_out);
848 let quote = Quote {
849 amount_in: U256::from(1_000_000u64),
850 amount_out: params.amount_out,
851 sqrt_price_x96_after: state.sqrt_price_x96,
852 price_impact_bps: 0,
853 };
854 let plan = swap_exact_out(
855 &state,
856 "e,
857 ¶ms,
858 SlippageBps::new(50),
859 0,
860 TEST_CFG.router,
861 SwapRouterKind::V1,
862 );
863 assert_eq!(
864 plan.approvals[0].min_amount,
865 U256::from(1_005_000u64),
866 "approval min_amount must equal amount_in_max (worst case)"
867 );
868 }
869
870 #[test]
871 fn slippage_min_50bps_on_1eth() {
872 let out =
873 apply_slippage_min(U256::from(1_000_000_000_000_000_000u64), SlippageBps::new(50));
874 assert_eq!(out, U256::from(995_000_000_000_000_000u64));
876 }
877
878 #[test]
879 fn slippage_max_50bps_on_1eth() {
880 let out =
881 apply_slippage_max(U256::from(1_000_000_000_000_000_000u64), SlippageBps::new(50));
882 assert_eq!(out, U256::from(1_005_000_000_000_000_000u64));
883 }
884
885 #[test]
886 fn plan_add_liquidity_targets_position_manager() {
887 let p = AddLiquidityParams {
888 token0: address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
889 token1: address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
890 fee: 3000,
891 tick_lower: -201_000,
892 tick_upper: -198_960,
893 amount0_desired: U256::from(1_000_000u64),
894 amount1_desired: U256::from(500_000_000_000_000u64),
895 recipient: address!("0x0000000000000000000000000000000000000099"),
896 };
897 let frag = add_liquidity(&p, SlippageBps::new(50), 9_999_999_999, TEST_CFG.position_mgr);
898 assert_eq!(frag.calls.len(), 1);
899 assert_eq!(frag.calls[0].target, TEST_CFG.position_mgr);
900 assert_eq!(frag.approvals.len(), 2);
901 assert_eq!(frag.approvals[0].token, p.token0);
902 assert_eq!(frag.approvals[0].spender, TEST_CFG.position_mgr);
903 assert_eq!(frag.approvals[1].token, p.token1);
904 assert_eq!(frag.approvals[1].spender, TEST_CFG.position_mgr);
905 assert_eq!(frag.value, U256::ZERO);
906 }
907
908 #[test]
909 fn plan_remove_liquidity_targets_position_manager_no_approvals() {
910 let p = RemoveLiquidityParams {
911 token_id: U256::from(42u64),
912 liquidity: 1_000_000_000_000u128,
913 amount0_min: None,
914 amount1_min: None,
915 };
916 let frag = remove_liquidity(&p, 9_999_999_999, TEST_CFG.position_mgr);
917 assert_eq!(frag.calls.len(), 1);
918 assert_eq!(frag.calls[0].target, TEST_CFG.position_mgr);
919 assert!(frag.approvals.is_empty());
920 assert_eq!(frag.value, U256::ZERO);
921 }
922
923 #[test]
924 fn plan_remove_liquidity_passes_min_amounts_when_supplied() {
925 let p = RemoveLiquidityParams {
926 token_id: U256::from(42u64),
927 liquidity: 1_000_000_000_000u128,
928 amount0_min: Some(U256::from(500_000u64)),
929 amount1_min: Some(U256::from(1_000_000_000u64)),
930 };
931 let frag = remove_liquidity(&p, 9_999_999_999, TEST_CFG.position_mgr);
932 let decoded =
934 decreaseLiquidityCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
935 assert_eq!(decoded.params.amount0Min, U256::from(500_000u64));
936 assert_eq!(decoded.params.amount1Min, U256::from(1_000_000_000u64));
937 }
938
939 #[test]
940 fn plan_remove_liquidity_defaults_missing_mins_to_zero() {
941 let p = RemoveLiquidityParams {
942 token_id: U256::from(42u64),
943 liquidity: 1_000_000_000_000u128,
944 amount0_min: None,
945 amount1_min: None,
946 };
947 let frag = remove_liquidity(&p, 9_999_999_999, TEST_CFG.position_mgr);
948 let decoded =
949 decreaseLiquidityCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
950 assert_eq!(decoded.params.amount0Min, U256::ZERO);
951 assert_eq!(decoded.params.amount1Min, U256::ZERO);
952 }
953
954 #[test]
955 fn slippage_min_50bps_on_100k_weth() {
956 let quoted: U256 = U256::from(10u64).pow(U256::from(23u64));
959 let out = apply_slippage_min(quoted, SlippageBps::new(50));
960 let expected: U256 = quoted * U256::from(9950u64) / U256::from(10_000u64);
961 assert_eq!(out, expected);
962 assert!(out > U256::from(u64::MAX));
964 }
965
966 #[test]
967 fn plan_collect_fees_targets_position_manager_no_approvals() {
968 let p = CollectFeesParams {
969 token_id: U256::from(42u64),
970 recipient: address!("0x0000000000000000000000000000000000000099"),
971 token0: address!("0x0000000000000000000000000000000000000001"),
972 token1: address!("0x0000000000000000000000000000000000000002"),
973 caller: Address::ZERO,
974 };
975 let frag = collect_fees(&p, TEST_CFG.position_mgr);
976 assert_eq!(frag.calls.len(), 1);
977 assert_eq!(frag.calls[0].target, TEST_CFG.position_mgr);
978 assert!(frag.approvals.is_empty());
979 assert_eq!(frag.value, U256::ZERO);
980 }
981
982 #[test]
983 fn plan_collect_fees_calldata_round_trips_all_fields() {
984 let p = CollectFeesParams {
985 token_id: U256::from(42u64),
986 recipient: address!("0000000000000000000000000000000000000099"),
987 token0: address!("0000000000000000000000000000000000000001"),
988 token1: address!("0000000000000000000000000000000000000002"),
989 caller: Address::ZERO,
990 };
991 let frag = collect_fees(&p, TEST_CFG.position_mgr);
992
993 let decoded = collectCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
994 assert_eq!(decoded.params.tokenId, p.token_id);
995 assert_eq!(decoded.params.recipient, p.recipient);
996 assert_eq!(decoded.params.amount0Max, u128::MAX);
997 assert_eq!(decoded.params.amount1Max, u128::MAX);
998 }
999
1000 #[test]
1001 fn plan_remove_liquidity_and_collect_targets_nfpm_with_multicall_outer() {
1002 let p = RemoveAndCollectParams {
1003 token_id: U256::from(1u64),
1004 liquidity: 1000u128,
1005 amount0_min: Some(U256::from(99u64)),
1006 amount1_min: Some(U256::from(199u64)),
1007 recipient: address!("0000000000000000000000000000000000000099"),
1008 token0: address!("0000000000000000000000000000000000000001"),
1009 token1: address!("0000000000000000000000000000000000000002"),
1010 caller: Address::ZERO,
1011 burn: false,
1012 };
1013 let frag = remove_liquidity_and_collect(&p, 9_999_999_999, TEST_CFG.position_mgr);
1014 assert_eq!(frag.calls.len(), 1);
1015 assert_eq!(frag.calls[0].target, TEST_CFG.position_mgr);
1016 assert_eq!(
1017 &frag.calls[0].calldata[..4],
1018 &[0xac, 0x96, 0x50, 0xd8],
1019 "outer call selector must be multicall"
1020 );
1021 assert!(frag.approvals.is_empty());
1022 assert_eq!(frag.value, U256::ZERO);
1023 }
1024
1025 #[test]
1026 fn plan_remove_liquidity_and_collect_inner_decoded_correctly() {
1027 let p = RemoveAndCollectParams {
1028 token_id: U256::from(42u64),
1029 liquidity: 1_000_000_000_000u128,
1030 amount0_min: Some(U256::from(500_000u64)),
1031 amount1_min: Some(U256::from(1_000_000_000u64)),
1032 recipient: address!("0000000000000000000000000000000000000099"),
1033 token0: address!("0000000000000000000000000000000000000001"),
1034 token1: address!("0000000000000000000000000000000000000002"),
1035 caller: Address::ZERO,
1036 burn: false,
1037 };
1038 let frag = remove_liquidity_and_collect(&p, 9_999_999_999, TEST_CFG.position_mgr);
1039
1040 let outer = IPeripheryRouter::multicallCall::abi_decode(&frag.calls[0].calldata)
1041 .expect("decode outer multicall");
1042 assert_eq!(outer.data.len(), 2);
1043 assert_eq!(&outer.data[0][..4], decreaseLiquidityCall::SELECTOR.as_slice());
1044 assert_eq!(&outer.data[1][..4], collectCall::SELECTOR.as_slice());
1045
1046 let decrease = decreaseLiquidityCall::abi_decode(&outer.data[0]).expect("decode decrease");
1047 assert_eq!(decrease.params.tokenId, p.token_id);
1048 assert_eq!(decrease.params.liquidity, p.liquidity);
1049 assert_eq!(decrease.params.amount0Min, U256::from(500_000u64));
1050 assert_eq!(decrease.params.amount1Min, U256::from(1_000_000_000u64));
1051 assert_eq!(decrease.params.deadline, U256::from(9_999_999_999u64));
1052
1053 let collect = collectCall::abi_decode(&outer.data[1]).expect("decode collect");
1054 assert_eq!(collect.params.tokenId, p.token_id);
1055 assert_eq!(collect.params.recipient, p.recipient);
1056 assert_eq!(collect.params.amount0Max, u128::MAX);
1057 assert_eq!(collect.params.amount1Max, u128::MAX);
1058 }
1059
1060 #[test]
1061 fn plan_remove_liquidity_and_collect_defaults_missing_mins_to_zero() {
1062 let p = RemoveAndCollectParams {
1063 token_id: U256::from(42u64),
1064 liquidity: 1_000_000_000_000u128,
1065 amount0_min: None,
1066 amount1_min: None,
1067 recipient: address!("0000000000000000000000000000000000000099"),
1068 token0: address!("0000000000000000000000000000000000000001"),
1069 token1: address!("0000000000000000000000000000000000000002"),
1070 caller: Address::ZERO,
1071 burn: false,
1072 };
1073 let frag = remove_liquidity_and_collect(&p, 9_999_999_999, TEST_CFG.position_mgr);
1074
1075 let outer = IPeripheryRouter::multicallCall::abi_decode(&frag.calls[0].calldata)
1076 .expect("decode outer multicall");
1077 let decrease = decreaseLiquidityCall::abi_decode(&outer.data[0]).expect("decode decrease");
1078 assert_eq!(decrease.params.amount0Min, U256::ZERO);
1079 assert_eq!(decrease.params.amount1Min, U256::ZERO);
1080 }
1081
1082 #[test]
1083 fn remove_and_collect_appends_burn_when_set() {
1084 let p = RemoveAndCollectParams {
1085 token_id: U256::from(7u64),
1086 liquidity: 1000,
1087 amount0_min: None,
1088 amount1_min: None,
1089 recipient: Address::ZERO,
1090 token0: Address::ZERO,
1091 token1: Address::ZERO,
1092 caller: Address::ZERO,
1093 burn: true,
1094 };
1095 let frag = remove_liquidity_and_collect(&p, 9_999_999_999, Address::ZERO);
1096 let outer = IPeripheryRouter::multicallCall::abi_decode(&frag.calls[0].calldata).unwrap();
1097 assert_eq!(outer.data.len(), 3, "decrease, collect, burn");
1098 assert_eq!(&outer.data[0][..4], decreaseLiquidityCall::SELECTOR.as_slice());
1099 assert_eq!(&outer.data[1][..4], collectCall::SELECTOR.as_slice());
1100 assert_eq!(&outer.data[2][..4], burnCall::SELECTOR.as_slice());
1101 let burn = burnCall::abi_decode(&outer.data[2]).unwrap();
1102 assert_eq!(burn.tokenId, U256::from(7u64));
1103 }
1104
1105 #[test]
1106 fn remove_and_collect_no_burn_when_unset() {
1107 let p = RemoveAndCollectParams {
1108 token_id: U256::from(7u64),
1109 liquidity: 1000,
1110 amount0_min: None,
1111 amount1_min: None,
1112 recipient: Address::ZERO,
1113 token0: Address::ZERO,
1114 token1: Address::ZERO,
1115 caller: Address::ZERO,
1116 burn: false,
1117 };
1118 let frag = remove_liquidity_and_collect(&p, 9_999_999_999, Address::ZERO);
1119 let outer = IPeripheryRouter::multicallCall::abi_decode(&frag.calls[0].calldata).unwrap();
1120 assert_eq!(outer.data.len(), 2, "decrease, collect — no burn");
1121 }
1122
1123 #[test]
1124 fn swap_exact_in_calldata_round_trips_all_fields() {
1125 let token_in = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
1126 let token_out = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
1127 let s = dummy_pool_state(token_in, token_out); let q = Quote {
1129 amount_in: U256::from(1_000_000u64),
1130 amount_out: U256::from(500_000_000_000_000u64),
1131 sqrt_price_x96_after: s.sqrt_price_x96,
1132 price_impact_bps: 0,
1133 };
1134 let recipient = address!("0000000000000000000000000000000000000099");
1135 let p = ExactInParams { token_in, token_out, amount_in: q.amount_in, recipient };
1136 let deadline = 1_700_000_000u64;
1137 let frag = swap_exact_in(
1138 &s,
1139 &q,
1140 &p,
1141 SlippageBps::new(50),
1142 deadline,
1143 TEST_CFG.router,
1144 SwapRouterKind::V1,
1145 );
1146
1147 let decoded = exactInputSingleCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
1148 let params = decoded.params;
1149 assert_eq!(params.tokenIn, token_in);
1150 assert_eq!(params.tokenOut, token_out);
1151 assert_eq!(params.fee, alloy_primitives::aliases::U24::from(3000u32));
1152 assert_eq!(params.recipient, recipient);
1153 assert_eq!(params.deadline, U256::from(deadline));
1154 assert_eq!(params.amountIn, q.amount_in);
1155 let expected_min = q.amount_out * U256::from(9950u64) / U256::from(10000u64);
1157 assert_eq!(params.amountOutMinimum, expected_min);
1158 assert_eq!(params.sqrtPriceLimitX96, alloy_primitives::aliases::U160::ZERO);
1159 }
1160
1161 #[test]
1162 fn add_liquidity_calldata_round_trips_all_fields() {
1163 let p = AddLiquidityParams {
1164 token0: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
1165 token1: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
1166 fee: 3000,
1167 tick_lower: -201_000,
1168 tick_upper: -198_960,
1169 amount0_desired: U256::from(1_000_000u64),
1170 amount1_desired: U256::from(500_000_000_000_000u64),
1171 recipient: address!("0000000000000000000000000000000000000099"),
1172 };
1173 let deadline = 1_700_000_000u64;
1174 let frag = add_liquidity(&p, SlippageBps::new(100), deadline, TEST_CFG.position_mgr);
1175
1176 let decoded = mintCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
1177 let mp = decoded.params;
1178 assert_eq!(mp.token0, p.token0);
1179 assert_eq!(mp.token1, p.token1);
1180 assert_eq!(mp.fee, alloy_primitives::aliases::U24::from(3000u32));
1181 assert_eq!(mp.tickLower, alloy_primitives::aliases::I24::try_from(-201_000i32).unwrap());
1182 assert_eq!(mp.tickUpper, alloy_primitives::aliases::I24::try_from(-198_960i32).unwrap());
1183 assert_eq!(mp.amount0Desired, p.amount0_desired);
1184 assert_eq!(mp.amount1Desired, p.amount1_desired);
1185 let expected_min0 = p.amount0_desired * U256::from(9900u64) / U256::from(10000u64);
1187 assert_eq!(mp.amount0Min, expected_min0);
1188 let expected_min1 = p.amount1_desired * U256::from(9900u64) / U256::from(10000u64);
1189 assert_eq!(mp.amount1Min, expected_min1);
1190 assert_eq!(mp.recipient, p.recipient);
1191 assert_eq!(mp.deadline, U256::from(deadline));
1192 }
1193
1194 #[test]
1195 fn plan_increase_liquidity_targets_position_manager_two_approvals() {
1196 let token0 = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
1197 let token1 = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
1198 let frag = increase_liquidity(
1199 U256::from(123_456u64),
1200 token0,
1201 token1,
1202 U256::from(1_000_000u64),
1203 U256::from(500_000_000_000_000u64),
1204 SlippageBps::new(50),
1205 9_999_999_999,
1206 TEST_CFG.position_mgr,
1207 );
1208 assert_eq!(frag.calls.len(), 1);
1209 assert_eq!(frag.calls[0].target, TEST_CFG.position_mgr);
1210 assert_eq!(frag.calls[0].value, U256::ZERO);
1211 assert_eq!(frag.approvals.len(), 2);
1212 assert_eq!(frag.approvals[0].token, token0);
1213 assert_eq!(frag.approvals[0].spender, TEST_CFG.position_mgr);
1214 assert_eq!(frag.approvals[0].min_amount, U256::from(1_000_000u64));
1215 assert_eq!(frag.approvals[1].token, token1);
1216 assert_eq!(frag.approvals[1].spender, TEST_CFG.position_mgr);
1217 assert_eq!(frag.approvals[1].min_amount, U256::from(500_000_000_000_000u64));
1218 assert_eq!(frag.value, U256::ZERO);
1219 }
1220
1221 #[test]
1222 fn increase_liquidity_calldata_round_trips_all_fields() {
1223 let token0 = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
1224 let token1 = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
1225 let token_id = U256::from(987_654u64);
1226 let amount0 = U256::from(2_000_000u64);
1227 let amount1 = U256::from(750_000_000_000_000u64);
1228 let deadline = 1_700_000_000u64;
1229 let frag = increase_liquidity(
1230 token_id,
1231 token0,
1232 token1,
1233 amount0,
1234 amount1,
1235 SlippageBps::new(100),
1236 deadline,
1237 TEST_CFG.position_mgr,
1238 );
1239
1240 let decoded =
1241 increaseLiquidityCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
1242 let ip = decoded.params;
1243 assert_eq!(ip.tokenId, token_id);
1244 assert_eq!(ip.amount0Desired, amount0);
1245 assert_eq!(ip.amount1Desired, amount1);
1246 let expected_min0 = amount0 * U256::from(9900u64) / U256::from(10000u64);
1248 assert_eq!(ip.amount0Min, expected_min0);
1249 let expected_min1 = amount1 * U256::from(9900u64) / U256::from(10000u64);
1250 assert_eq!(ip.amount1Min, expected_min1);
1251 assert_eq!(ip.deadline, U256::from(deadline));
1252 }
1253
1254 #[test]
1257 fn increase_liquidity_selector_matches_v3_canonical() {
1258 assert_eq!(increaseLiquidityCall::SELECTOR, [0x21, 0x9f, 0x5d, 0x17]);
1259 }
1260
1261 #[test]
1262 fn swap_exact_in_with_fee_fn_injects_fee_into_calldata() {
1263 let token_in = address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
1264 let token_out = address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
1265 let s = dummy_pool_state(token_in, token_out); let q = Quote {
1267 amount_in: U256::from(1_000_000u64),
1268 amount_out: U256::from(500_000_000_000_000u64),
1269 sqrt_price_x96_after: s.sqrt_price_x96,
1270 price_impact_bps: 0,
1271 };
1272 let p = ExactInParams {
1273 token_in,
1274 token_out,
1275 amount_in: q.amount_in,
1276 recipient: address!("0x0000000000000000000000000000000000000099"),
1277 };
1278 let frag = swap_exact_in_with_fee_fn(
1280 &s,
1281 &q,
1282 &p,
1283 SlippageBps::new(50),
1284 9_999_999_999,
1285 TEST_CFG.router,
1286 SwapRouterKind::V1,
1287 |_| 500,
1288 );
1289 let decoded = exactInputSingleCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
1291 assert_eq!(decoded.params.fee, alloy_primitives::aliases::U24::from(500u32));
1292 }
1293
1294 #[test]
1297 fn slippage_min_zero_bps_returns_full_amount() {
1298 let out = apply_slippage_min(U256::from(1_000_000u64), SlippageBps::new(0));
1301 assert_eq!(out, U256::from(1_000_000u64));
1302 }
1303
1304 #[test]
1305 fn slippage_min_10000_bps_returns_zero() {
1306 let out = apply_slippage_min(U256::from(1_000_000u64), SlippageBps::new(10_000));
1309 assert_eq!(out, U256::ZERO);
1310 }
1311
1312 #[test]
1313 fn slippage_min_zero_quoted_returns_zero() {
1314 let out = apply_slippage_min(U256::ZERO, SlippageBps::new(50));
1318 assert_eq!(out, U256::ZERO);
1319 }
1320
1321 #[test]
1322 fn slippage_min_max_u256_does_not_overflow() {
1323 let out = apply_slippage_min(U256::MAX, SlippageBps::new(50));
1330 assert!(out > U256::ZERO, "expected nonzero result for MAX input");
1333 assert!(out < U256::MAX, "expected result < MAX (slippage was applied)");
1334 }
1335
1336 #[test]
1337 fn burn_selector_is_known() {
1338 assert_eq!(burnCall::SELECTOR, [0x42, 0x96, 0x6c, 0x68]); }
1340
1341 #[test]
1342 fn add_liquidity_with_min_encodes_supplied_mins() {
1343 let p = AddLiquidityParams {
1344 token0: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
1345 token1: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
1346 fee: 3000,
1347 tick_lower: -201_000,
1348 tick_upper: -198_960,
1349 amount0_desired: U256::from(1_000_000u64),
1350 amount1_desired: U256::from(500_000_000_000_000u64),
1351 recipient: address!("0000000000000000000000000000000000000099"),
1352 };
1353 let m0 = U256::from(123_456u64);
1354 let m1 = U256::from(789_012u64);
1355 let frag = add_liquidity_with_min(&p, m0, m1, 9_999_999_999, TEST_CFG.position_mgr);
1356 let decoded = mintCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
1357 assert_eq!(decoded.params.amount0Min, m0);
1358 assert_eq!(decoded.params.amount1Min, m1);
1359 }
1360
1361 #[test]
1362 fn add_liquidity_delegates_to_with_min_byte_identical() {
1363 let p = AddLiquidityParams {
1364 token0: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
1365 token1: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
1366 fee: 3000,
1367 tick_lower: -201_000,
1368 tick_upper: -198_960,
1369 amount0_desired: U256::from(1_000_000u64),
1370 amount1_desired: U256::from(500_000_000_000_000u64),
1371 recipient: address!("0000000000000000000000000000000000000099"),
1372 };
1373 let slip = SlippageBps::new(50);
1374 let dl = 9_999_999_999u64;
1375 let pm = TEST_CFG.position_mgr;
1376 let via_slippage = add_liquidity(&p, slip, dl, pm);
1377 let via_min = add_liquidity_with_min(
1378 &p,
1379 apply_slippage_min(p.amount0_desired, slip),
1380 apply_slippage_min(p.amount1_desired, slip),
1381 dl,
1382 pm,
1383 );
1384 assert_eq!(via_slippage, via_min);
1385 }
1386
1387 #[test]
1388 fn increase_liquidity_with_min_encodes_supplied_mins() {
1389 let token0 = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
1390 let token1 = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
1391 let m0 = U256::from(123_456u64);
1392 let m1 = U256::from(789_012u64);
1393 let frag = increase_liquidity_with_min(
1394 U256::from(987_654u64),
1395 token0,
1396 token1,
1397 U256::from(2_000_000u64),
1398 U256::from(750_000_000_000_000u64),
1399 m0,
1400 m1,
1401 9_999_999_999,
1402 TEST_CFG.position_mgr,
1403 );
1404 let decoded =
1405 increaseLiquidityCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
1406 assert_eq!(decoded.params.amount0Min, m0);
1407 assert_eq!(decoded.params.amount1Min, m1);
1408 }
1409
1410 #[test]
1411 fn increase_liquidity_delegates_to_with_min_byte_identical() {
1412 let token0 = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
1413 let token1 = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
1414 let token_id = U256::from(987_654u64);
1415 let amount0 = U256::from(2_000_000u64);
1416 let amount1 = U256::from(750_000_000_000_000u64);
1417 let slip = SlippageBps::new(100);
1418 let dl = 1_700_000_000u64;
1419 let pm = TEST_CFG.position_mgr;
1420 let via_slippage =
1421 increase_liquidity(token_id, token0, token1, amount0, amount1, slip, dl, pm);
1422 let via_min = increase_liquidity_with_min(
1423 token_id,
1424 token0,
1425 token1,
1426 amount0,
1427 amount1,
1428 apply_slippage_min(amount0, slip),
1429 apply_slippage_min(amount1, slip),
1430 dl,
1431 pm,
1432 );
1433 assert_eq!(via_slippage, via_min);
1434 }
1435}