1use crate::data::{
4 CollectFeesParams, ExactInParams, PlanFragment, PoolState, Quote, RamsesAddLiquidityParams,
5 RemoveAndCollectParams, RemoveLiquidityParams,
6};
7use alloy_primitives::{Address, U256};
8use alloy_sol_types::{sol, SolCall};
9use wp_evm_base::types::{Call, SlippageBps, TokenApproval};
10use wp_evm_v3_core::plan::apply_slippage_min;
11
12sol! {
13 #[derive(Debug)]
14 struct ExactInputSingleParams {
15 address tokenIn;
16 address tokenOut;
17 int24 tickSpacing;
18 address recipient;
19 uint256 deadline;
20 uint256 amountIn;
21 uint256 amountOutMinimum;
22 uint160 sqrtPriceLimitX96;
23 }
24
25 function exactInputSingle(ExactInputSingleParams params)
26 external payable returns (uint256 amountOut);
27}
28
29pub(crate) mod shadow_nfpm {
30 use alloy_sol_types::sol;
31
32 sol! {
33 #[derive(Debug)]
34 struct ShadowMintParams {
35 address token0;
36 address token1;
37 int24 tickSpacing;
38 int24 tickLower;
39 int24 tickUpper;
40 uint256 amount0Desired;
41 uint256 amount1Desired;
42 uint256 amount0Min;
43 uint256 amount1Min;
44 address recipient;
45 uint256 deadline;
46 }
47 function mint(ShadowMintParams params) external payable
48 returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
49 }
50}
51
52pub(crate) mod slipstream_nfpm {
53 use alloy_sol_types::sol;
54
55 sol! {
56 #[derive(Debug)]
57 struct SlipstreamMintParams {
58 address token0;
59 address token1;
60 int24 tickSpacing;
61 int24 tickLower;
62 int24 tickUpper;
63 uint256 amount0Desired;
64 uint256 amount1Desired;
65 uint256 amount0Min;
66 uint256 amount1Min;
67 address recipient;
68 uint256 deadline;
69 uint160 sqrtPriceX96;
70 }
71 function mint(SlipstreamMintParams params) external payable
72 returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
73 }
74}
75
76use shadow_nfpm::{mintCall as shadowMintCall, ShadowMintParams};
77use slipstream_nfpm::{mintCall as slipstreamMintCall, SlipstreamMintParams};
78
79pub fn swap_exact_in(
80 state: &PoolState,
81 quote: &Quote,
82 params: &ExactInParams,
83 slippage: SlippageBps,
84 deadline: u64,
85 router: Address,
86) -> PlanFragment {
87 let amount_out_min = apply_slippage_min(quote.amount_out, slippage);
88
89 let call_params = ExactInputSingleParams {
90 tokenIn: params.token_in,
91 tokenOut: params.token_out,
92 tickSpacing: alloy_primitives::aliases::I24::try_from(state.tick_spacing)
93 .expect("tick_spacing fits in i24"),
94 recipient: params.recipient,
95 deadline: U256::from(deadline),
96 amountIn: params.amount_in,
97 amountOutMinimum: amount_out_min,
98 sqrtPriceLimitX96: alloy_primitives::aliases::U160::ZERO,
99 };
100
101 let calldata = exactInputSingleCall { params: call_params }.abi_encode().into();
102
103 PlanFragment {
104 calls: vec![Call { target: router, calldata, value: U256::ZERO }],
105 approvals: vec![TokenApproval {
106 token: params.token_in,
107 spender: router,
108 min_amount: params.amount_in,
109 }],
110 value: U256::ZERO,
111 }
112}
113
114pub fn add_liquidity(
115 params: &RamsesAddLiquidityParams,
116 slippage: SlippageBps,
117 deadline: u64,
118 position_manager: Address,
119) -> PlanFragment {
120 let mint_params = ShadowMintParams {
121 token0: params.token0,
122 token1: params.token1,
123 tickSpacing: alloy_primitives::aliases::I24::try_from(params.tick_spacing)
124 .expect("tick_spacing fits in i24"),
125 tickLower: alloy_primitives::aliases::I24::try_from(params.tick_lower)
126 .expect("tick_lower within i24 range"),
127 tickUpper: alloy_primitives::aliases::I24::try_from(params.tick_upper)
128 .expect("tick_upper within i24 range"),
129 amount0Desired: params.amount0_desired,
130 amount1Desired: params.amount1_desired,
131 amount0Min: apply_slippage_min(params.amount0_desired, slippage),
132 amount1Min: apply_slippage_min(params.amount1_desired, slippage),
133 recipient: params.recipient,
134 deadline: U256::from(deadline),
135 };
136 let calldata = shadowMintCall { params: mint_params }.abi_encode().into();
137 PlanFragment {
138 calls: vec![Call { target: position_manager, calldata, value: U256::ZERO }],
139 approvals: vec![
140 TokenApproval {
141 token: params.token0,
142 spender: position_manager,
143 min_amount: params.amount0_desired,
144 },
145 TokenApproval {
146 token: params.token1,
147 spender: position_manager,
148 min_amount: params.amount1_desired,
149 },
150 ],
151 value: U256::ZERO,
152 }
153}
154
155pub fn add_liquidity_slipstream(
156 params: &RamsesAddLiquidityParams,
157 slippage: SlippageBps,
158 deadline: u64,
159 position_manager: Address,
160) -> PlanFragment {
161 let mint_params = SlipstreamMintParams {
162 token0: params.token0,
163 token1: params.token1,
164 tickSpacing: alloy_primitives::aliases::I24::try_from(params.tick_spacing)
165 .expect("tick_spacing fits in i24"),
166 tickLower: alloy_primitives::aliases::I24::try_from(params.tick_lower)
167 .expect("tick_lower within i24 range"),
168 tickUpper: alloy_primitives::aliases::I24::try_from(params.tick_upper)
169 .expect("tick_upper within i24 range"),
170 amount0Desired: params.amount0_desired,
171 amount1Desired: params.amount1_desired,
172 amount0Min: apply_slippage_min(params.amount0_desired, slippage),
173 amount1Min: apply_slippage_min(params.amount1_desired, slippage),
174 recipient: params.recipient,
175 deadline: U256::from(deadline),
176 sqrtPriceX96: alloy_primitives::aliases::U160::ZERO,
177 };
178 let calldata = slipstreamMintCall { params: mint_params }.abi_encode().into();
179 PlanFragment {
180 calls: vec![Call { target: position_manager, calldata, value: U256::ZERO }],
181 approvals: vec![
182 TokenApproval {
183 token: params.token0,
184 spender: position_manager,
185 min_amount: params.amount0_desired,
186 },
187 TokenApproval {
188 token: params.token1,
189 spender: position_manager,
190 min_amount: params.amount1_desired,
191 },
192 ],
193 value: U256::ZERO,
194 }
195}
196
197#[allow(clippy::too_many_arguments)]
198pub fn increase_liquidity(
199 token_id: alloy_primitives::U256,
200 token0: alloy_primitives::Address,
201 token1: alloy_primitives::Address,
202 amount0_desired: alloy_primitives::U256,
203 amount1_desired: alloy_primitives::U256,
204 slippage: SlippageBps,
205 deadline: u64,
206 position_manager: Address,
207) -> PlanFragment {
208 wp_evm_v3_core::plan::increase_liquidity(
209 token_id,
210 token0,
211 token1,
212 amount0_desired,
213 amount1_desired,
214 slippage,
215 deadline,
216 position_manager,
217 )
218}
219
220pub fn remove_liquidity(
221 params: &RemoveLiquidityParams,
222 deadline: u64,
223 position_manager: Address,
224) -> PlanFragment {
225 wp_evm_v3_core::plan::remove_liquidity(params, deadline, position_manager)
226}
227
228pub fn remove_liquidity_and_collect(
241 params: &RemoveAndCollectParams,
242 deadline: u64,
243 position_manager: Address,
244) -> PlanFragment {
245 wp_evm_v3_core::plan::remove_liquidity_and_collect(params, deadline, position_manager)
246}
247
248pub fn collect_fees(params: &CollectFeesParams, position_manager: Address) -> PlanFragment {
249 wp_evm_v3_core::plan::collect_fees(params, position_manager)
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use alloy_primitives::{address, Address};
256
257 const ROUTER: Address = address!("0x2222222222222222222222222222222222222222");
258 const POSITION_MANAGER: Address = address!("0x3333333333333333333333333333333333333333");
259
260 fn dummy_pool_state(t0: Address, t1: Address) -> PoolState {
261 PoolState {
262 token0: t0,
263 token1: t1,
264 fee: 0,
265 tick_spacing: 50,
266 sqrt_price_x96: U256::from(1u64) << 96,
267 liquidity: 0,
268 tick: 0,
269 ticks: vec![],
270 }
271 }
272
273 #[test]
274 fn plan_swap_emits_ramses_selector() {
275 let token_in = address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
276 let token_out = address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
277 let s = dummy_pool_state(token_in, token_out);
278 let q = Quote {
279 amount_in: U256::from(1_000_000u64),
280 amount_out: U256::from(500_000_000_000_000u64),
281 sqrt_price_x96_after: s.sqrt_price_x96,
282 price_impact_bps: 0,
283 };
284 let p = ExactInParams {
285 token_in,
286 token_out,
287 amount_in: q.amount_in,
288 recipient: address!("0x0000000000000000000000000000000000000099"),
289 };
290 let frag = swap_exact_in(&s, &q, &p, SlippageBps::new(50), 9_999_999_999, ROUTER);
291 assert_eq!(frag.calls.len(), 1);
292 assert_eq!(frag.calls[0].target, ROUTER);
293 assert_eq!(&frag.calls[0].calldata[..4], &[0xa0, 0x26, 0x38, 0x3e]);
294 }
295
296 #[test]
297 fn plan_swap_encodes_tick_spacing_not_fee() {
298 let token_in = address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
299 let token_out = address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
300 let mut s = dummy_pool_state(token_in, token_out);
301 s.tick_spacing = 200;
302 let q = Quote {
303 amount_in: U256::from(1_000_000u64),
304 amount_out: U256::from(500_000_000_000_000u64),
305 sqrt_price_x96_after: s.sqrt_price_x96,
306 price_impact_bps: 0,
307 };
308 let p = ExactInParams {
309 token_in,
310 token_out,
311 amount_in: q.amount_in,
312 recipient: address!("0x0000000000000000000000000000000000000099"),
313 };
314 let frag = swap_exact_in(&s, &q, &p, SlippageBps::new(50), 9_999_999_999, ROUTER);
315 let decoded = exactInputSingleCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
316 assert_eq!(
317 decoded.params.tickSpacing,
318 alloy_primitives::aliases::I24::try_from(200i32).unwrap()
319 );
320 }
321
322 #[test]
323 fn add_liquidity_targets_position_manager() {
324 let p = RamsesAddLiquidityParams {
325 token0: address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
326 token1: address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
327 tick_spacing: 50,
328 tick_lower: -887_272,
329 tick_upper: 887_272,
330 amount0_desired: U256::from(1_000_000u64),
331 amount1_desired: U256::from(500_000_000_000_000u64),
332 recipient: address!("0x0000000000000000000000000000000000000099"),
333 };
334 let frag = add_liquidity(&p, SlippageBps::new(50), 9_999_999_999, POSITION_MANAGER);
335 assert_eq!(frag.calls[0].target, POSITION_MANAGER);
336 assert_eq!(frag.approvals.len(), 2);
337 }
338
339 #[test]
340 fn remove_liquidity_and_collect_targets_position_manager_with_outer_multicall() {
341 use alloy_sol_types::SolValue;
345 let p = RemoveAndCollectParams {
346 token_id: U256::from(1u64),
347 liquidity: 1_000u128,
348 amount0_min: Some(U256::from(99u64)),
349 amount1_min: Some(U256::from(199u64)),
350 recipient: address!("0000000000000000000000000000000000000099"),
351 token0: address!("0000000000000000000000000000000000000001"),
352 token1: address!("0000000000000000000000000000000000000002"),
353 caller: Address::ZERO,
354 };
355 let frag = remove_liquidity_and_collect(&p, 9_999_999_999, POSITION_MANAGER);
356 assert_eq!(frag.calls.len(), 1);
357 assert_eq!(frag.calls[0].target, POSITION_MANAGER);
358 assert_eq!(
359 &frag.calls[0].calldata[..4],
360 &[0xac, 0x96, 0x50, 0xd8],
361 "outer selector must be multicall(bytes[])"
362 );
363 assert!(frag.approvals.is_empty());
364 assert_eq!(frag.value, U256::ZERO);
365
366 let (inner,): (Vec<alloy_primitives::Bytes>,) =
370 <(Vec<alloy_primitives::Bytes>,)>::abi_decode_params(&frag.calls[0].calldata[4..])
371 .expect("decode outer multicall params");
372 assert_eq!(inner.len(), 2);
373 assert_eq!(&inner[0][..4], &[0x0c, 0x49, 0xcc, 0xbe], "inner[0] = decreaseLiquidity");
374 assert_eq!(&inner[1][..4], &[0xfc, 0x6f, 0x78, 0x65], "inner[1] = collect");
375 }
376
377 #[test]
378 fn remove_liquidity_and_collect_delegation_matches_v3_core_bytewise() {
379 let p = RemoveAndCollectParams {
382 token_id: U256::from(42u64),
383 liquidity: 1_000_000_000_000u128,
384 amount0_min: Some(U256::from(500_000u64)),
385 amount1_min: Some(U256::from(1_000_000_000u64)),
386 recipient: address!("0000000000000000000000000000000000000099"),
387 token0: address!("0000000000000000000000000000000000000001"),
388 token1: address!("0000000000000000000000000000000000000002"),
389 caller: Address::ZERO,
390 };
391 let ours = remove_liquidity_and_collect(&p, 9_999_999_999, POSITION_MANAGER);
392 let core =
393 wp_evm_v3_core::plan::remove_liquidity_and_collect(&p, 9_999_999_999, POSITION_MANAGER);
394 assert_eq!(ours.calls[0].calldata, core.calls[0].calldata);
395 assert_eq!(ours.calls[0].target, core.calls[0].target);
396 }
397}