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 let mint_params = MintParams {
344 token0: params.token0,
345 token1: params.token1,
346 fee: alloy_primitives::aliases::U24::from(params.fee),
347 tickLower: alloy_primitives::aliases::I24::try_from(params.tick_lower)
348 .expect("tick_lower within i24 range"),
349 tickUpper: alloy_primitives::aliases::I24::try_from(params.tick_upper)
350 .expect("tick_upper within i24 range"),
351 amount0Desired: params.amount0_desired,
352 amount1Desired: params.amount1_desired,
353 amount0Min: apply_slippage_min(params.amount0_desired, slippage),
354 amount1Min: apply_slippage_min(params.amount1_desired, slippage),
355 recipient: params.recipient,
356 deadline: U256::from(deadline),
357 };
358 let calldata = mintCall { params: mint_params }.abi_encode().into();
359 PlanFragment {
360 calls: vec![Call { target: position_manager, calldata, value: U256::ZERO }],
361 approvals: vec![
362 TokenApproval {
363 token: params.token0,
364 spender: position_manager,
365 min_amount: params.amount0_desired,
366 },
367 TokenApproval {
368 token: params.token1,
369 spender: position_manager,
370 min_amount: params.amount1_desired,
371 },
372 ],
373 value: U256::ZERO,
374 }
375}
376
377#[allow(
390 clippy::too_many_arguments,
391 reason = "public planner API mirrors NFPM increaseLiquidity parameters"
392)]
393pub fn increase_liquidity(
394 token_id: U256,
395 token0: Address,
396 token1: Address,
397 amount0_desired: U256,
398 amount1_desired: U256,
399 slippage: SlippageBps,
400 deadline: u64,
401 position_manager: Address,
402) -> PlanFragment {
403 let inc_params = IncreaseLiquidityParams {
404 tokenId: token_id,
405 amount0Desired: amount0_desired,
406 amount1Desired: amount1_desired,
407 amount0Min: apply_slippage_min(amount0_desired, slippage),
408 amount1Min: apply_slippage_min(amount1_desired, slippage),
409 deadline: U256::from(deadline),
410 };
411 let calldata = increaseLiquidityCall { params: inc_params }.abi_encode().into();
412 PlanFragment {
413 calls: vec![Call { target: position_manager, calldata, value: U256::ZERO }],
414 approvals: vec![
415 TokenApproval { token: token0, spender: position_manager, min_amount: amount0_desired },
416 TokenApproval { token: token1, spender: position_manager, min_amount: amount1_desired },
417 ],
418 value: U256::ZERO,
419 }
420}
421
422pub fn remove_liquidity(
432 params: &RemoveLiquidityParams,
433 deadline: u64,
434 position_manager: Address,
435) -> PlanFragment {
436 let dec = DecreaseLiquidityParams {
437 tokenId: params.token_id,
438 liquidity: params.liquidity,
439 amount0Min: params.amount0_min.unwrap_or(U256::ZERO),
440 amount1Min: params.amount1_min.unwrap_or(U256::ZERO),
441 deadline: U256::from(deadline),
442 };
443 let calldata = decreaseLiquidityCall { params: dec }.abi_encode().into();
444 PlanFragment {
445 calls: vec![Call { target: position_manager, calldata, value: U256::ZERO }],
446 approvals: vec![],
447 value: U256::ZERO,
448 }
449}
450
451pub fn remove_liquidity_and_collect(
479 params: &RemoveAndCollectParams,
480 deadline: u64,
481 position_manager: Address,
482) -> PlanFragment {
483 let dec_params = DecreaseLiquidityParams {
485 tokenId: params.token_id,
486 liquidity: params.liquidity,
487 amount0Min: params.amount0_min.unwrap_or(U256::ZERO),
488 amount1Min: params.amount1_min.unwrap_or(U256::ZERO),
489 deadline: U256::from(deadline),
490 };
491 let decrease_calldata: alloy_primitives::Bytes =
492 decreaseLiquidityCall { params: dec_params }.abi_encode().into();
493
494 let collect_params = CollectParams {
496 tokenId: params.token_id,
497 recipient: params.recipient,
498 amount0Max: u128::MAX,
499 amount1Max: u128::MAX,
500 };
501 let collect_calldata: alloy_primitives::Bytes =
502 collectCall { params: collect_params }.abi_encode().into();
503
504 let mut multicall_data = vec![decrease_calldata, collect_calldata];
506 if params.burn {
507 let burn_calldata: alloy_primitives::Bytes =
508 burnCall { tokenId: params.token_id }.abi_encode().into();
509 multicall_data.push(burn_calldata);
510 }
511 let multicall_calldata =
512 IPeripheryRouter::multicallCall { data: multicall_data }.abi_encode().into();
513
514 PlanFragment {
515 calls: vec![Call {
516 target: position_manager,
517 value: U256::ZERO,
518 calldata: multicall_calldata,
519 }],
520 approvals: vec![],
521 value: U256::ZERO,
522 }
523}
524
525pub fn collect_fees(params: &CollectFeesParams, position_manager: Address) -> PlanFragment {
530 let coll = CollectParams {
531 tokenId: params.token_id,
532 recipient: params.recipient,
533 amount0Max: u128::MAX,
534 amount1Max: u128::MAX,
535 };
536 let calldata = collectCall { params: coll }.abi_encode().into();
537 PlanFragment {
538 calls: vec![Call { target: position_manager, calldata, value: U256::ZERO }],
539 approvals: vec![],
540 value: U256::ZERO,
541 }
542}
543
544#[cfg(test)]
545mod tests {
546 use super::*;
547 use crate::data::{
548 AddLiquidityParams, CollectFeesParams, RemoveAndCollectParams, RemoveLiquidityParams,
549 SwapRouterKind, V3ProtocolConfig,
550 };
551 use alloy_primitives::{address, b256, Address};
552
553 const TEST_CFG: V3ProtocolConfig = V3ProtocolConfig {
554 factory: address!("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
555 pool_deployer: None,
556 router: address!("0xE592427A0AEce92De3Edee1F18E0157C05861564"),
557 swap_router_kind: SwapRouterKind::V1,
558 position_mgr: address!("0xC36442b4a4522E871399CD717aBDD847Ab11FE88"),
559 init_code_hash: b256!("0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54"),
560 fee_tiers: &[100, 500, 3000, 10000],
561 multicall: address!("0xcA11bde05977b3631167028862bE2a173976CA11"),
562 quoter: None,
563 };
564
565 fn dummy_pool_state(t0: Address, t1: Address) -> PoolState {
566 PoolState {
567 token0: t0,
568 token1: t1,
569 fee: 3000,
570 tick_spacing: 60,
571 sqrt_price_x96: U256::from(1u64) << 96,
572 liquidity: 0,
573 tick: 0,
574 ticks: vec![],
575 }
576 }
577
578 fn fixture_exact_in_params() -> ExactInParams {
579 ExactInParams {
580 token_in: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
581 token_out: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
582 amount_in: U256::from(1_000_000u64),
583 recipient: address!("0000000000000000000000000000000000000099"),
584 }
585 }
586
587 fn fixture_exact_out_params() -> ExactOutParams {
588 ExactOutParams {
589 token_in: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
590 token_out: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
591 amount_out: U256::from(500_000_000_000_000u64),
592 recipient: address!("0000000000000000000000000000000000000099"),
593 }
594 }
595
596 fn fixture_quote(state: &PoolState) -> Quote {
597 Quote {
598 amount_in: U256::from(1_000_000u64),
599 amount_out: U256::from(500_000_000_000_000u64),
600 sqrt_price_x96_after: state.sqrt_price_x96,
601 price_impact_bps: 0,
602 }
603 }
604
605 #[test]
606 fn plan_swap_emits_one_call_with_router_target() {
607 let token_in = address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
608 let token_out = address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
609 let s = dummy_pool_state(token_in, token_out);
610 let q = Quote {
611 amount_in: U256::from(1_000_000u64),
612 amount_out: U256::from(500_000_000_000_000u64),
613 sqrt_price_x96_after: s.sqrt_price_x96,
614 price_impact_bps: 0,
615 };
616 let p = ExactInParams {
617 token_in,
618 token_out,
619 amount_in: q.amount_in,
620 recipient: address!("0x0000000000000000000000000000000000000099"),
621 };
622 let frag = swap_exact_in(
623 &s,
624 &q,
625 &p,
626 SlippageBps::new(50),
627 9_999_999_999,
628 TEST_CFG.router,
629 SwapRouterKind::V1,
630 );
631 assert_eq!(frag.calls.len(), 1);
632 assert_eq!(frag.calls[0].target, TEST_CFG.router);
633 assert_eq!(frag.approvals.len(), 1);
634 assert_eq!(frag.approvals[0].token, token_in);
635 assert_eq!(frag.approvals[0].spender, TEST_CFG.router);
636 assert_eq!(frag.approvals[0].min_amount, q.amount_in);
637 assert_eq!(frag.value, U256::ZERO);
638 }
639
640 #[test]
641 fn swap_exact_in_v1_emits_v1_selector() {
642 let params = fixture_exact_in_params();
643 let state = dummy_pool_state(params.token_in, params.token_out);
644 let quote = fixture_quote(&state);
645 let plan = swap_exact_in(
646 &state,
647 "e,
648 ¶ms,
649 SlippageBps::new(50),
650 0,
651 TEST_CFG.router,
652 SwapRouterKind::V1,
653 );
654 assert_eq!(
655 &plan.calls[0].calldata[..4],
656 &[0x41, 0x4b, 0xf3, 0x89],
657 "V1 selector must be 0x414bf389"
658 );
659 }
660
661 #[test]
662 fn swap_exact_in_v02_emits_v02_selector() {
663 let params = fixture_exact_in_params();
664 let state = dummy_pool_state(params.token_in, params.token_out);
665 let quote = fixture_quote(&state);
666 let plan = swap_exact_in(
667 &state,
668 "e,
669 ¶ms,
670 SlippageBps::new(50),
671 0,
672 TEST_CFG.router,
673 SwapRouterKind::V02,
674 );
675 assert_eq!(
676 &plan.calls[0].calldata[..4],
677 &[0x04, 0xe4, 0x5a, 0xaf],
678 "V02 selector must be 0x04e45aaf"
679 );
680 }
681
682 #[test]
683 fn swap_exact_in_v1_includes_deadline() {
684 let params = fixture_exact_in_params();
685 let state = dummy_pool_state(params.token_in, params.token_out);
686 let quote = fixture_quote(&state);
687 let v1_plan = swap_exact_in(
688 &state,
689 "e,
690 ¶ms,
691 SlippageBps::new(50),
692 1_700_000_000,
693 TEST_CFG.router,
694 SwapRouterKind::V1,
695 );
696 let v02_plan = swap_exact_in(
697 &state,
698 "e,
699 ¶ms,
700 SlippageBps::new(50),
701 1_700_000_000,
702 TEST_CFG.router,
703 SwapRouterKind::V02,
704 );
705 assert_eq!(
706 v1_plan.calls[0].calldata.len(),
707 v02_plan.calls[0].calldata.len() + 32,
708 "V1 calldata must be 32 bytes longer than V02 (deadline field)"
709 );
710 }
711
712 #[test]
713 fn swap_exact_out_v1_emits_v1_selector() {
714 let params = fixture_exact_out_params();
715 let state = dummy_pool_state(params.token_in, params.token_out);
716 let quote = fixture_quote(&state);
717 let plan = swap_exact_out(
718 &state,
719 "e,
720 ¶ms,
721 SlippageBps::new(50),
722 0,
723 TEST_CFG.router,
724 SwapRouterKind::V1,
725 );
726 assert_eq!(
727 &plan.calls[0].calldata[..4],
728 &[0xdb, 0x3e, 0x21, 0x98],
729 "V1 selector must be 0xdb3e2198"
730 );
731 }
732
733 #[test]
734 fn swap_exact_out_v02_emits_v02_selector() {
735 let params = fixture_exact_out_params();
736 let state = dummy_pool_state(params.token_in, params.token_out);
737 let quote = fixture_quote(&state);
738 let plan = swap_exact_out(
739 &state,
740 "e,
741 ¶ms,
742 SlippageBps::new(50),
743 0,
744 TEST_CFG.router,
745 SwapRouterKind::V02,
746 );
747 assert_eq!(
748 &plan.calls[0].calldata[..4],
749 &[0x50, 0x23, 0xb4, 0xdf],
750 "V02 selector must be 0x5023b4df"
751 );
752 }
753
754 #[test]
755 fn swap_exact_out_v1_includes_deadline() {
756 let params = fixture_exact_out_params();
757 let state = dummy_pool_state(params.token_in, params.token_out);
758 let quote = fixture_quote(&state);
759 let v1_plan = swap_exact_out(
760 &state,
761 "e,
762 ¶ms,
763 SlippageBps::new(50),
764 1_700_000_000,
765 TEST_CFG.router,
766 SwapRouterKind::V1,
767 );
768 let v02_plan = swap_exact_out(
769 &state,
770 "e,
771 ¶ms,
772 SlippageBps::new(50),
773 1_700_000_000,
774 TEST_CFG.router,
775 SwapRouterKind::V02,
776 );
777 assert_eq!(
778 v1_plan.calls[0].calldata.len(),
779 v02_plan.calls[0].calldata.len() + 32,
780 "V1 calldata must be 32 bytes longer than V02 (deadline field)"
781 );
782 }
783
784 #[test]
785 fn swap_exact_out_approval_min_amount_is_amount_in_max() {
786 let params = fixture_exact_out_params();
787 let state = dummy_pool_state(params.token_in, params.token_out);
788 let quote = Quote {
789 amount_in: U256::from(1_000_000u64),
790 amount_out: params.amount_out,
791 sqrt_price_x96_after: state.sqrt_price_x96,
792 price_impact_bps: 0,
793 };
794 let plan = swap_exact_out(
795 &state,
796 "e,
797 ¶ms,
798 SlippageBps::new(50),
799 0,
800 TEST_CFG.router,
801 SwapRouterKind::V1,
802 );
803 assert_eq!(
804 plan.approvals[0].min_amount,
805 U256::from(1_005_000u64),
806 "approval min_amount must equal amount_in_max (worst case)"
807 );
808 }
809
810 #[test]
811 fn slippage_min_50bps_on_1eth() {
812 let out =
813 apply_slippage_min(U256::from(1_000_000_000_000_000_000u64), SlippageBps::new(50));
814 assert_eq!(out, U256::from(995_000_000_000_000_000u64));
816 }
817
818 #[test]
819 fn slippage_max_50bps_on_1eth() {
820 let out =
821 apply_slippage_max(U256::from(1_000_000_000_000_000_000u64), SlippageBps::new(50));
822 assert_eq!(out, U256::from(1_005_000_000_000_000_000u64));
823 }
824
825 #[test]
826 fn plan_add_liquidity_targets_position_manager() {
827 let p = AddLiquidityParams {
828 token0: address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
829 token1: address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
830 fee: 3000,
831 tick_lower: -201_000,
832 tick_upper: -198_960,
833 amount0_desired: U256::from(1_000_000u64),
834 amount1_desired: U256::from(500_000_000_000_000u64),
835 recipient: address!("0x0000000000000000000000000000000000000099"),
836 };
837 let frag = add_liquidity(&p, SlippageBps::new(50), 9_999_999_999, TEST_CFG.position_mgr);
838 assert_eq!(frag.calls.len(), 1);
839 assert_eq!(frag.calls[0].target, TEST_CFG.position_mgr);
840 assert_eq!(frag.approvals.len(), 2);
841 assert_eq!(frag.approvals[0].token, p.token0);
842 assert_eq!(frag.approvals[0].spender, TEST_CFG.position_mgr);
843 assert_eq!(frag.approvals[1].token, p.token1);
844 assert_eq!(frag.approvals[1].spender, TEST_CFG.position_mgr);
845 assert_eq!(frag.value, U256::ZERO);
846 }
847
848 #[test]
849 fn plan_remove_liquidity_targets_position_manager_no_approvals() {
850 let p = RemoveLiquidityParams {
851 token_id: U256::from(42u64),
852 liquidity: 1_000_000_000_000u128,
853 amount0_min: None,
854 amount1_min: None,
855 };
856 let frag = remove_liquidity(&p, 9_999_999_999, TEST_CFG.position_mgr);
857 assert_eq!(frag.calls.len(), 1);
858 assert_eq!(frag.calls[0].target, TEST_CFG.position_mgr);
859 assert!(frag.approvals.is_empty());
860 assert_eq!(frag.value, U256::ZERO);
861 }
862
863 #[test]
864 fn plan_remove_liquidity_passes_min_amounts_when_supplied() {
865 let p = RemoveLiquidityParams {
866 token_id: U256::from(42u64),
867 liquidity: 1_000_000_000_000u128,
868 amount0_min: Some(U256::from(500_000u64)),
869 amount1_min: Some(U256::from(1_000_000_000u64)),
870 };
871 let frag = remove_liquidity(&p, 9_999_999_999, TEST_CFG.position_mgr);
872 let decoded =
874 decreaseLiquidityCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
875 assert_eq!(decoded.params.amount0Min, U256::from(500_000u64));
876 assert_eq!(decoded.params.amount1Min, U256::from(1_000_000_000u64));
877 }
878
879 #[test]
880 fn plan_remove_liquidity_defaults_missing_mins_to_zero() {
881 let p = RemoveLiquidityParams {
882 token_id: U256::from(42u64),
883 liquidity: 1_000_000_000_000u128,
884 amount0_min: None,
885 amount1_min: None,
886 };
887 let frag = remove_liquidity(&p, 9_999_999_999, TEST_CFG.position_mgr);
888 let decoded =
889 decreaseLiquidityCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
890 assert_eq!(decoded.params.amount0Min, U256::ZERO);
891 assert_eq!(decoded.params.amount1Min, U256::ZERO);
892 }
893
894 #[test]
895 fn slippage_min_50bps_on_100k_weth() {
896 let quoted: U256 = U256::from(10u64).pow(U256::from(23u64));
899 let out = apply_slippage_min(quoted, SlippageBps::new(50));
900 let expected: U256 = quoted * U256::from(9950u64) / U256::from(10_000u64);
901 assert_eq!(out, expected);
902 assert!(out > U256::from(u64::MAX));
904 }
905
906 #[test]
907 fn plan_collect_fees_targets_position_manager_no_approvals() {
908 let p = CollectFeesParams {
909 token_id: U256::from(42u64),
910 recipient: address!("0x0000000000000000000000000000000000000099"),
911 token0: address!("0x0000000000000000000000000000000000000001"),
912 token1: address!("0x0000000000000000000000000000000000000002"),
913 caller: Address::ZERO,
914 };
915 let frag = collect_fees(&p, TEST_CFG.position_mgr);
916 assert_eq!(frag.calls.len(), 1);
917 assert_eq!(frag.calls[0].target, TEST_CFG.position_mgr);
918 assert!(frag.approvals.is_empty());
919 assert_eq!(frag.value, U256::ZERO);
920 }
921
922 #[test]
923 fn plan_collect_fees_calldata_round_trips_all_fields() {
924 let p = CollectFeesParams {
925 token_id: U256::from(42u64),
926 recipient: address!("0000000000000000000000000000000000000099"),
927 token0: address!("0000000000000000000000000000000000000001"),
928 token1: address!("0000000000000000000000000000000000000002"),
929 caller: Address::ZERO,
930 };
931 let frag = collect_fees(&p, TEST_CFG.position_mgr);
932
933 let decoded = collectCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
934 assert_eq!(decoded.params.tokenId, p.token_id);
935 assert_eq!(decoded.params.recipient, p.recipient);
936 assert_eq!(decoded.params.amount0Max, u128::MAX);
937 assert_eq!(decoded.params.amount1Max, u128::MAX);
938 }
939
940 #[test]
941 fn plan_remove_liquidity_and_collect_targets_nfpm_with_multicall_outer() {
942 let p = RemoveAndCollectParams {
943 token_id: U256::from(1u64),
944 liquidity: 1000u128,
945 amount0_min: Some(U256::from(99u64)),
946 amount1_min: Some(U256::from(199u64)),
947 recipient: address!("0000000000000000000000000000000000000099"),
948 token0: address!("0000000000000000000000000000000000000001"),
949 token1: address!("0000000000000000000000000000000000000002"),
950 caller: Address::ZERO,
951 burn: false,
952 };
953 let frag = remove_liquidity_and_collect(&p, 9_999_999_999, TEST_CFG.position_mgr);
954 assert_eq!(frag.calls.len(), 1);
955 assert_eq!(frag.calls[0].target, TEST_CFG.position_mgr);
956 assert_eq!(
957 &frag.calls[0].calldata[..4],
958 &[0xac, 0x96, 0x50, 0xd8],
959 "outer call selector must be multicall"
960 );
961 assert!(frag.approvals.is_empty());
962 assert_eq!(frag.value, U256::ZERO);
963 }
964
965 #[test]
966 fn plan_remove_liquidity_and_collect_inner_decoded_correctly() {
967 let p = RemoveAndCollectParams {
968 token_id: U256::from(42u64),
969 liquidity: 1_000_000_000_000u128,
970 amount0_min: Some(U256::from(500_000u64)),
971 amount1_min: Some(U256::from(1_000_000_000u64)),
972 recipient: address!("0000000000000000000000000000000000000099"),
973 token0: address!("0000000000000000000000000000000000000001"),
974 token1: address!("0000000000000000000000000000000000000002"),
975 caller: Address::ZERO,
976 burn: false,
977 };
978 let frag = remove_liquidity_and_collect(&p, 9_999_999_999, TEST_CFG.position_mgr);
979
980 let outer = IPeripheryRouter::multicallCall::abi_decode(&frag.calls[0].calldata)
981 .expect("decode outer multicall");
982 assert_eq!(outer.data.len(), 2);
983 assert_eq!(&outer.data[0][..4], decreaseLiquidityCall::SELECTOR.as_slice());
984 assert_eq!(&outer.data[1][..4], collectCall::SELECTOR.as_slice());
985
986 let decrease = decreaseLiquidityCall::abi_decode(&outer.data[0]).expect("decode decrease");
987 assert_eq!(decrease.params.tokenId, p.token_id);
988 assert_eq!(decrease.params.liquidity, p.liquidity);
989 assert_eq!(decrease.params.amount0Min, U256::from(500_000u64));
990 assert_eq!(decrease.params.amount1Min, U256::from(1_000_000_000u64));
991 assert_eq!(decrease.params.deadline, U256::from(9_999_999_999u64));
992
993 let collect = collectCall::abi_decode(&outer.data[1]).expect("decode collect");
994 assert_eq!(collect.params.tokenId, p.token_id);
995 assert_eq!(collect.params.recipient, p.recipient);
996 assert_eq!(collect.params.amount0Max, u128::MAX);
997 assert_eq!(collect.params.amount1Max, u128::MAX);
998 }
999
1000 #[test]
1001 fn plan_remove_liquidity_and_collect_defaults_missing_mins_to_zero() {
1002 let p = RemoveAndCollectParams {
1003 token_id: U256::from(42u64),
1004 liquidity: 1_000_000_000_000u128,
1005 amount0_min: None,
1006 amount1_min: None,
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
1015 let outer = IPeripheryRouter::multicallCall::abi_decode(&frag.calls[0].calldata)
1016 .expect("decode outer multicall");
1017 let decrease = decreaseLiquidityCall::abi_decode(&outer.data[0]).expect("decode decrease");
1018 assert_eq!(decrease.params.amount0Min, U256::ZERO);
1019 assert_eq!(decrease.params.amount1Min, U256::ZERO);
1020 }
1021
1022 #[test]
1023 fn remove_and_collect_appends_burn_when_set() {
1024 let p = RemoveAndCollectParams {
1025 token_id: U256::from(7u64),
1026 liquidity: 1000,
1027 amount0_min: None,
1028 amount1_min: None,
1029 recipient: Address::ZERO,
1030 token0: Address::ZERO,
1031 token1: Address::ZERO,
1032 caller: Address::ZERO,
1033 burn: true,
1034 };
1035 let frag = remove_liquidity_and_collect(&p, 9_999_999_999, Address::ZERO);
1036 let outer = IPeripheryRouter::multicallCall::abi_decode(&frag.calls[0].calldata).unwrap();
1037 assert_eq!(outer.data.len(), 3, "decrease, collect, burn");
1038 assert_eq!(&outer.data[0][..4], decreaseLiquidityCall::SELECTOR.as_slice());
1039 assert_eq!(&outer.data[1][..4], collectCall::SELECTOR.as_slice());
1040 assert_eq!(&outer.data[2][..4], burnCall::SELECTOR.as_slice());
1041 let burn = burnCall::abi_decode(&outer.data[2]).unwrap();
1042 assert_eq!(burn.tokenId, U256::from(7u64));
1043 }
1044
1045 #[test]
1046 fn remove_and_collect_no_burn_when_unset() {
1047 let p = RemoveAndCollectParams {
1048 token_id: U256::from(7u64),
1049 liquidity: 1000,
1050 amount0_min: None,
1051 amount1_min: None,
1052 recipient: Address::ZERO,
1053 token0: Address::ZERO,
1054 token1: Address::ZERO,
1055 caller: Address::ZERO,
1056 burn: false,
1057 };
1058 let frag = remove_liquidity_and_collect(&p, 9_999_999_999, Address::ZERO);
1059 let outer = IPeripheryRouter::multicallCall::abi_decode(&frag.calls[0].calldata).unwrap();
1060 assert_eq!(outer.data.len(), 2, "decrease, collect — no burn");
1061 }
1062
1063 #[test]
1064 fn swap_exact_in_calldata_round_trips_all_fields() {
1065 let token_in = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
1066 let token_out = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
1067 let s = dummy_pool_state(token_in, token_out); let q = Quote {
1069 amount_in: U256::from(1_000_000u64),
1070 amount_out: U256::from(500_000_000_000_000u64),
1071 sqrt_price_x96_after: s.sqrt_price_x96,
1072 price_impact_bps: 0,
1073 };
1074 let recipient = address!("0000000000000000000000000000000000000099");
1075 let p = ExactInParams { token_in, token_out, amount_in: q.amount_in, recipient };
1076 let deadline = 1_700_000_000u64;
1077 let frag = swap_exact_in(
1078 &s,
1079 &q,
1080 &p,
1081 SlippageBps::new(50),
1082 deadline,
1083 TEST_CFG.router,
1084 SwapRouterKind::V1,
1085 );
1086
1087 let decoded = exactInputSingleCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
1088 let params = decoded.params;
1089 assert_eq!(params.tokenIn, token_in);
1090 assert_eq!(params.tokenOut, token_out);
1091 assert_eq!(params.fee, alloy_primitives::aliases::U24::from(3000u32));
1092 assert_eq!(params.recipient, recipient);
1093 assert_eq!(params.deadline, U256::from(deadline));
1094 assert_eq!(params.amountIn, q.amount_in);
1095 let expected_min = q.amount_out * U256::from(9950u64) / U256::from(10000u64);
1097 assert_eq!(params.amountOutMinimum, expected_min);
1098 assert_eq!(params.sqrtPriceLimitX96, alloy_primitives::aliases::U160::ZERO);
1099 }
1100
1101 #[test]
1102 fn add_liquidity_calldata_round_trips_all_fields() {
1103 let p = AddLiquidityParams {
1104 token0: address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
1105 token1: address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
1106 fee: 3000,
1107 tick_lower: -201_000,
1108 tick_upper: -198_960,
1109 amount0_desired: U256::from(1_000_000u64),
1110 amount1_desired: U256::from(500_000_000_000_000u64),
1111 recipient: address!("0000000000000000000000000000000000000099"),
1112 };
1113 let deadline = 1_700_000_000u64;
1114 let frag = add_liquidity(&p, SlippageBps::new(100), deadline, TEST_CFG.position_mgr);
1115
1116 let decoded = mintCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
1117 let mp = decoded.params;
1118 assert_eq!(mp.token0, p.token0);
1119 assert_eq!(mp.token1, p.token1);
1120 assert_eq!(mp.fee, alloy_primitives::aliases::U24::from(3000u32));
1121 assert_eq!(mp.tickLower, alloy_primitives::aliases::I24::try_from(-201_000i32).unwrap());
1122 assert_eq!(mp.tickUpper, alloy_primitives::aliases::I24::try_from(-198_960i32).unwrap());
1123 assert_eq!(mp.amount0Desired, p.amount0_desired);
1124 assert_eq!(mp.amount1Desired, p.amount1_desired);
1125 let expected_min0 = p.amount0_desired * U256::from(9900u64) / U256::from(10000u64);
1127 assert_eq!(mp.amount0Min, expected_min0);
1128 let expected_min1 = p.amount1_desired * U256::from(9900u64) / U256::from(10000u64);
1129 assert_eq!(mp.amount1Min, expected_min1);
1130 assert_eq!(mp.recipient, p.recipient);
1131 assert_eq!(mp.deadline, U256::from(deadline));
1132 }
1133
1134 #[test]
1135 fn plan_increase_liquidity_targets_position_manager_two_approvals() {
1136 let token0 = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
1137 let token1 = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
1138 let frag = increase_liquidity(
1139 U256::from(123_456u64),
1140 token0,
1141 token1,
1142 U256::from(1_000_000u64),
1143 U256::from(500_000_000_000_000u64),
1144 SlippageBps::new(50),
1145 9_999_999_999,
1146 TEST_CFG.position_mgr,
1147 );
1148 assert_eq!(frag.calls.len(), 1);
1149 assert_eq!(frag.calls[0].target, TEST_CFG.position_mgr);
1150 assert_eq!(frag.calls[0].value, U256::ZERO);
1151 assert_eq!(frag.approvals.len(), 2);
1152 assert_eq!(frag.approvals[0].token, token0);
1153 assert_eq!(frag.approvals[0].spender, TEST_CFG.position_mgr);
1154 assert_eq!(frag.approvals[0].min_amount, U256::from(1_000_000u64));
1155 assert_eq!(frag.approvals[1].token, token1);
1156 assert_eq!(frag.approvals[1].spender, TEST_CFG.position_mgr);
1157 assert_eq!(frag.approvals[1].min_amount, U256::from(500_000_000_000_000u64));
1158 assert_eq!(frag.value, U256::ZERO);
1159 }
1160
1161 #[test]
1162 fn increase_liquidity_calldata_round_trips_all_fields() {
1163 let token0 = address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
1164 let token1 = address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
1165 let token_id = U256::from(987_654u64);
1166 let amount0 = U256::from(2_000_000u64);
1167 let amount1 = U256::from(750_000_000_000_000u64);
1168 let deadline = 1_700_000_000u64;
1169 let frag = increase_liquidity(
1170 token_id,
1171 token0,
1172 token1,
1173 amount0,
1174 amount1,
1175 SlippageBps::new(100),
1176 deadline,
1177 TEST_CFG.position_mgr,
1178 );
1179
1180 let decoded =
1181 increaseLiquidityCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
1182 let ip = decoded.params;
1183 assert_eq!(ip.tokenId, token_id);
1184 assert_eq!(ip.amount0Desired, amount0);
1185 assert_eq!(ip.amount1Desired, amount1);
1186 let expected_min0 = amount0 * U256::from(9900u64) / U256::from(10000u64);
1188 assert_eq!(ip.amount0Min, expected_min0);
1189 let expected_min1 = amount1 * U256::from(9900u64) / U256::from(10000u64);
1190 assert_eq!(ip.amount1Min, expected_min1);
1191 assert_eq!(ip.deadline, U256::from(deadline));
1192 }
1193
1194 #[test]
1197 fn increase_liquidity_selector_matches_v3_canonical() {
1198 assert_eq!(increaseLiquidityCall::SELECTOR, [0x21, 0x9f, 0x5d, 0x17]);
1199 }
1200
1201 #[test]
1202 fn swap_exact_in_with_fee_fn_injects_fee_into_calldata() {
1203 let token_in = address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
1204 let token_out = address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
1205 let s = dummy_pool_state(token_in, token_out); let q = Quote {
1207 amount_in: U256::from(1_000_000u64),
1208 amount_out: U256::from(500_000_000_000_000u64),
1209 sqrt_price_x96_after: s.sqrt_price_x96,
1210 price_impact_bps: 0,
1211 };
1212 let p = ExactInParams {
1213 token_in,
1214 token_out,
1215 amount_in: q.amount_in,
1216 recipient: address!("0x0000000000000000000000000000000000000099"),
1217 };
1218 let frag = swap_exact_in_with_fee_fn(
1220 &s,
1221 &q,
1222 &p,
1223 SlippageBps::new(50),
1224 9_999_999_999,
1225 TEST_CFG.router,
1226 SwapRouterKind::V1,
1227 |_| 500,
1228 );
1229 let decoded = exactInputSingleCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
1231 assert_eq!(decoded.params.fee, alloy_primitives::aliases::U24::from(500u32));
1232 }
1233
1234 #[test]
1237 fn slippage_min_zero_bps_returns_full_amount() {
1238 let out = apply_slippage_min(U256::from(1_000_000u64), SlippageBps::new(0));
1241 assert_eq!(out, U256::from(1_000_000u64));
1242 }
1243
1244 #[test]
1245 fn slippage_min_10000_bps_returns_zero() {
1246 let out = apply_slippage_min(U256::from(1_000_000u64), SlippageBps::new(10_000));
1249 assert_eq!(out, U256::ZERO);
1250 }
1251
1252 #[test]
1253 fn slippage_min_zero_quoted_returns_zero() {
1254 let out = apply_slippage_min(U256::ZERO, SlippageBps::new(50));
1258 assert_eq!(out, U256::ZERO);
1259 }
1260
1261 #[test]
1262 fn slippage_min_max_u256_does_not_overflow() {
1263 let out = apply_slippage_min(U256::MAX, SlippageBps::new(50));
1270 assert!(out > U256::ZERO, "expected nonzero result for MAX input");
1273 assert!(out < U256::MAX, "expected result < MAX (slippage was applied)");
1274 }
1275
1276 #[test]
1277 fn burn_selector_is_known() {
1278 assert_eq!(burnCall::SELECTOR, [0x42, 0x96, 0x6c, 0x68]); }
1280}