1use crate::data::{
4 CollectFeesParams, ExactInParams, GaugeClaim, GaugeEarnedGrid, PlanFragment, PoolState, Quote,
5 RamsesAddLiquidityParams, 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_ramses_interfaces::gauge::IRamsesVoter;
11use wp_evm_v3_core::plan::apply_slippage_min;
12use wp_evm_velodrome_interfaces::gauge::IVelodromeCLGauge;
13
14sol! {
15 #[derive(Debug)]
16 struct ExactInputSingleParams {
17 address tokenIn;
18 address tokenOut;
19 int24 tickSpacing;
20 address recipient;
21 uint256 deadline;
22 uint256 amountIn;
23 uint256 amountOutMinimum;
24 uint160 sqrtPriceLimitX96;
25 }
26
27 function exactInputSingle(ExactInputSingleParams params)
28 external payable returns (uint256 amountOut);
29}
30
31pub(crate) mod shadow_nfpm {
32 use alloy_sol_types::sol;
33
34 sol! {
35 #[derive(Debug)]
36 struct ShadowMintParams {
37 address token0;
38 address token1;
39 int24 tickSpacing;
40 int24 tickLower;
41 int24 tickUpper;
42 uint256 amount0Desired;
43 uint256 amount1Desired;
44 uint256 amount0Min;
45 uint256 amount1Min;
46 address recipient;
47 uint256 deadline;
48 }
49 function mint(ShadowMintParams params) external payable
50 returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
51 }
52}
53
54pub(crate) mod slipstream_nfpm {
55 use alloy_sol_types::sol;
56
57 sol! {
58 #[derive(Debug)]
59 struct SlipstreamMintParams {
60 address token0;
61 address token1;
62 int24 tickSpacing;
63 int24 tickLower;
64 int24 tickUpper;
65 uint256 amount0Desired;
66 uint256 amount1Desired;
67 uint256 amount0Min;
68 uint256 amount1Min;
69 address recipient;
70 uint256 deadline;
71 uint160 sqrtPriceX96;
72 }
73 function mint(SlipstreamMintParams params) external payable
74 returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
75 }
76}
77
78use shadow_nfpm::{mintCall as shadowMintCall, ShadowMintParams};
79use slipstream_nfpm::{mintCall as slipstreamMintCall, SlipstreamMintParams};
80
81pub fn swap_exact_in(
82 state: &PoolState,
83 quote: &Quote,
84 params: &ExactInParams,
85 slippage: SlippageBps,
86 deadline: u64,
87 router: Address,
88) -> PlanFragment {
89 let amount_out_min = apply_slippage_min(quote.amount_out, slippage);
90
91 let call_params = ExactInputSingleParams {
92 tokenIn: params.token_in,
93 tokenOut: params.token_out,
94 tickSpacing: alloy_primitives::aliases::I24::try_from(state.tick_spacing)
95 .expect("tick_spacing fits in i24"),
96 recipient: params.recipient,
97 deadline: U256::from(deadline),
98 amountIn: params.amount_in,
99 amountOutMinimum: amount_out_min,
100 sqrtPriceLimitX96: alloy_primitives::aliases::U160::ZERO,
101 };
102
103 let calldata = exactInputSingleCall { params: call_params }.abi_encode().into();
104
105 PlanFragment {
106 calls: vec![Call { target: router, calldata, value: U256::ZERO }],
107 approvals: vec![TokenApproval {
108 token: params.token_in,
109 spender: router,
110 min_amount: params.amount_in,
111 }],
112 value: U256::ZERO,
113 }
114}
115
116pub fn add_liquidity(
117 params: &RamsesAddLiquidityParams,
118 slippage: SlippageBps,
119 deadline: u64,
120 position_manager: Address,
121) -> PlanFragment {
122 let mint_params = ShadowMintParams {
123 token0: params.token0,
124 token1: params.token1,
125 tickSpacing: alloy_primitives::aliases::I24::try_from(params.tick_spacing)
126 .expect("tick_spacing fits in i24"),
127 tickLower: alloy_primitives::aliases::I24::try_from(params.tick_lower)
128 .expect("tick_lower within i24 range"),
129 tickUpper: alloy_primitives::aliases::I24::try_from(params.tick_upper)
130 .expect("tick_upper within i24 range"),
131 amount0Desired: params.amount0_desired,
132 amount1Desired: params.amount1_desired,
133 amount0Min: apply_slippage_min(params.amount0_desired, slippage),
134 amount1Min: apply_slippage_min(params.amount1_desired, slippage),
135 recipient: params.recipient,
136 deadline: U256::from(deadline),
137 };
138 let calldata = shadowMintCall { params: mint_params }.abi_encode().into();
139 PlanFragment {
140 calls: vec![Call { target: position_manager, calldata, value: U256::ZERO }],
141 approvals: vec![
142 TokenApproval {
143 token: params.token0,
144 spender: position_manager,
145 min_amount: params.amount0_desired,
146 },
147 TokenApproval {
148 token: params.token1,
149 spender: position_manager,
150 min_amount: params.amount1_desired,
151 },
152 ],
153 value: U256::ZERO,
154 }
155}
156
157pub fn add_liquidity_slipstream(
158 params: &RamsesAddLiquidityParams,
159 slippage: SlippageBps,
160 deadline: u64,
161 position_manager: Address,
162) -> PlanFragment {
163 let mint_params = SlipstreamMintParams {
164 token0: params.token0,
165 token1: params.token1,
166 tickSpacing: alloy_primitives::aliases::I24::try_from(params.tick_spacing)
167 .expect("tick_spacing fits in i24"),
168 tickLower: alloy_primitives::aliases::I24::try_from(params.tick_lower)
169 .expect("tick_lower within i24 range"),
170 tickUpper: alloy_primitives::aliases::I24::try_from(params.tick_upper)
171 .expect("tick_upper within i24 range"),
172 amount0Desired: params.amount0_desired,
173 amount1Desired: params.amount1_desired,
174 amount0Min: apply_slippage_min(params.amount0_desired, slippage),
175 amount1Min: apply_slippage_min(params.amount1_desired, slippage),
176 recipient: params.recipient,
177 deadline: U256::from(deadline),
178 sqrtPriceX96: alloy_primitives::aliases::U160::ZERO,
179 };
180 let calldata = slipstreamMintCall { params: mint_params }.abi_encode().into();
181 PlanFragment {
182 calls: vec![Call { target: position_manager, calldata, value: U256::ZERO }],
183 approvals: vec![
184 TokenApproval {
185 token: params.token0,
186 spender: position_manager,
187 min_amount: params.amount0_desired,
188 },
189 TokenApproval {
190 token: params.token1,
191 spender: position_manager,
192 min_amount: params.amount1_desired,
193 },
194 ],
195 value: U256::ZERO,
196 }
197}
198
199#[allow(clippy::too_many_arguments)]
200pub fn increase_liquidity(
201 token_id: alloy_primitives::U256,
202 token0: alloy_primitives::Address,
203 token1: alloy_primitives::Address,
204 amount0_desired: alloy_primitives::U256,
205 amount1_desired: alloy_primitives::U256,
206 slippage: SlippageBps,
207 deadline: u64,
208 position_manager: Address,
209) -> PlanFragment {
210 wp_evm_v3_core::plan::increase_liquidity(
211 token_id,
212 token0,
213 token1,
214 amount0_desired,
215 amount1_desired,
216 slippage,
217 deadline,
218 position_manager,
219 )
220}
221
222pub fn remove_liquidity(
223 params: &RemoveLiquidityParams,
224 deadline: u64,
225 position_manager: Address,
226) -> PlanFragment {
227 wp_evm_v3_core::plan::remove_liquidity(params, deadline, position_manager)
228}
229
230pub fn remove_liquidity_and_collect(
243 params: &RemoveAndCollectParams,
244 deadline: u64,
245 position_manager: Address,
246) -> PlanFragment {
247 wp_evm_v3_core::plan::remove_liquidity_and_collect(params, deadline, position_manager)
248}
249
250pub fn collect_fees(params: &CollectFeesParams, position_manager: Address) -> PlanFragment {
251 wp_evm_v3_core::plan::collect_fees(params, position_manager)
252}
253
254pub fn claim_cl_gauge_rewards(voter: Address, claims: &[GaugeClaim]) -> PlanFragment {
262 let gauges: Vec<Address> = claims.iter().map(|c| c.gauge).collect();
263 let tokens: Vec<Vec<Address>> = claims.iter().map(|c| c.reward_tokens.clone()).collect();
264 let nfp_token_ids: Vec<Vec<U256>> = claims.iter().map(|c| c.token_ids.clone()).collect();
265 let calldata = IRamsesVoter::claimClGaugeRewardsCall {
266 _gauges: gauges,
267 _tokens: tokens,
268 _nfpTokenIds: nfp_token_ids,
269 }
270 .abi_encode()
271 .into();
272 PlanFragment {
273 calls: vec![Call { target: voter, calldata, value: U256::ZERO }],
274 approvals: vec![],
275 value: U256::ZERO,
276 }
277}
278
279pub fn claim_gauge(gauge: Address, token_id: U256) -> PlanFragment {
283 let calldata = IVelodromeCLGauge::getRewardCall { tokenId: token_id }.abi_encode().into();
284 PlanFragment {
285 calls: vec![Call { target: gauge, calldata, value: U256::ZERO }],
286 approvals: vec![],
287 value: U256::ZERO,
288 }
289}
290
291pub fn build_gauge_claims(grids: &[GaugeEarnedGrid]) -> Vec<GaugeClaim> {
297 let mut out = Vec::new();
298 for g in grids {
299 let kept_tokens: Vec<usize> = (0..g.reward_tokens.len())
300 .filter(|&t| (0..g.token_ids.len()).any(|p| g.earned[t][p] > U256::ZERO))
301 .collect();
302 let kept_positions: Vec<usize> = (0..g.token_ids.len())
303 .filter(|&p| (0..g.reward_tokens.len()).any(|t| g.earned[t][p] > U256::ZERO))
304 .collect();
305 if kept_tokens.is_empty() || kept_positions.is_empty() {
306 continue;
307 }
308 out.push(GaugeClaim {
309 gauge: g.gauge,
310 reward_tokens: kept_tokens.iter().map(|&t| g.reward_tokens[t]).collect(),
311 token_ids: kept_positions.iter().map(|&p| g.token_ids[p]).collect(),
312 });
313 }
314 out
315}
316
317pub fn claim_cl_gauge_rewards_from_grids(
322 voter: Address,
323 grids: &[GaugeEarnedGrid],
324) -> Option<PlanFragment> {
325 let claims = build_gauge_claims(grids);
326 if claims.is_empty() {
327 None
328 } else {
329 Some(claim_cl_gauge_rewards(voter, &claims))
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336 use alloy_primitives::{address, Address};
337
338 const ROUTER: Address = address!("0x2222222222222222222222222222222222222222");
339 const POSITION_MANAGER: Address = address!("0x3333333333333333333333333333333333333333");
340
341 fn dummy_pool_state(t0: Address, t1: Address) -> PoolState {
342 PoolState {
343 token0: t0,
344 token1: t1,
345 fee: 0,
346 tick_spacing: 50,
347 sqrt_price_x96: U256::from(1u64) << 96,
348 liquidity: 0,
349 tick: 0,
350 ticks: vec![],
351 }
352 }
353
354 #[test]
355 fn plan_swap_emits_ramses_selector() {
356 let token_in = address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
357 let token_out = address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
358 let s = dummy_pool_state(token_in, token_out);
359 let q = Quote {
360 amount_in: U256::from(1_000_000u64),
361 amount_out: U256::from(500_000_000_000_000u64),
362 sqrt_price_x96_after: s.sqrt_price_x96,
363 price_impact_bps: 0,
364 };
365 let p = ExactInParams {
366 token_in,
367 token_out,
368 amount_in: q.amount_in,
369 recipient: address!("0x0000000000000000000000000000000000000099"),
370 };
371 let frag = swap_exact_in(&s, &q, &p, SlippageBps::new(50), 9_999_999_999, ROUTER);
372 assert_eq!(frag.calls.len(), 1);
373 assert_eq!(frag.calls[0].target, ROUTER);
374 assert_eq!(&frag.calls[0].calldata[..4], &[0xa0, 0x26, 0x38, 0x3e]);
375 }
376
377 #[test]
378 fn plan_swap_encodes_tick_spacing_not_fee() {
379 let token_in = address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
380 let token_out = address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
381 let mut s = dummy_pool_state(token_in, token_out);
382 s.tick_spacing = 200;
383 let q = Quote {
384 amount_in: U256::from(1_000_000u64),
385 amount_out: U256::from(500_000_000_000_000u64),
386 sqrt_price_x96_after: s.sqrt_price_x96,
387 price_impact_bps: 0,
388 };
389 let p = ExactInParams {
390 token_in,
391 token_out,
392 amount_in: q.amount_in,
393 recipient: address!("0x0000000000000000000000000000000000000099"),
394 };
395 let frag = swap_exact_in(&s, &q, &p, SlippageBps::new(50), 9_999_999_999, ROUTER);
396 let decoded = exactInputSingleCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
397 assert_eq!(
398 decoded.params.tickSpacing,
399 alloy_primitives::aliases::I24::try_from(200i32).unwrap()
400 );
401 }
402
403 #[test]
404 fn add_liquidity_targets_position_manager() {
405 let p = RamsesAddLiquidityParams {
406 token0: address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
407 token1: address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
408 tick_spacing: 50,
409 tick_lower: -887_272,
410 tick_upper: 887_272,
411 amount0_desired: U256::from(1_000_000u64),
412 amount1_desired: U256::from(500_000_000_000_000u64),
413 recipient: address!("0x0000000000000000000000000000000000000099"),
414 };
415 let frag = add_liquidity(&p, SlippageBps::new(50), 9_999_999_999, POSITION_MANAGER);
416 assert_eq!(frag.calls[0].target, POSITION_MANAGER);
417 assert_eq!(frag.approvals.len(), 2);
418 }
419
420 #[test]
421 fn remove_liquidity_and_collect_targets_position_manager_with_outer_multicall() {
422 use alloy_sol_types::SolValue;
426 let p = RemoveAndCollectParams {
427 token_id: U256::from(1u64),
428 liquidity: 1_000u128,
429 amount0_min: Some(U256::from(99u64)),
430 amount1_min: Some(U256::from(199u64)),
431 recipient: address!("0000000000000000000000000000000000000099"),
432 token0: address!("0000000000000000000000000000000000000001"),
433 token1: address!("0000000000000000000000000000000000000002"),
434 caller: Address::ZERO,
435 };
436 let frag = remove_liquidity_and_collect(&p, 9_999_999_999, POSITION_MANAGER);
437 assert_eq!(frag.calls.len(), 1);
438 assert_eq!(frag.calls[0].target, POSITION_MANAGER);
439 assert_eq!(
440 &frag.calls[0].calldata[..4],
441 &[0xac, 0x96, 0x50, 0xd8],
442 "outer selector must be multicall(bytes[])"
443 );
444 assert!(frag.approvals.is_empty());
445 assert_eq!(frag.value, U256::ZERO);
446
447 let (inner,): (Vec<alloy_primitives::Bytes>,) =
451 <(Vec<alloy_primitives::Bytes>,)>::abi_decode_params(&frag.calls[0].calldata[4..])
452 .expect("decode outer multicall params");
453 assert_eq!(inner.len(), 2);
454 assert_eq!(&inner[0][..4], &[0x0c, 0x49, 0xcc, 0xbe], "inner[0] = decreaseLiquidity");
455 assert_eq!(&inner[1][..4], &[0xfc, 0x6f, 0x78, 0x65], "inner[1] = collect");
456 }
457
458 #[test]
459 fn remove_liquidity_and_collect_delegation_matches_v3_core_bytewise() {
460 let p = RemoveAndCollectParams {
463 token_id: U256::from(42u64),
464 liquidity: 1_000_000_000_000u128,
465 amount0_min: Some(U256::from(500_000u64)),
466 amount1_min: Some(U256::from(1_000_000_000u64)),
467 recipient: address!("0000000000000000000000000000000000000099"),
468 token0: address!("0000000000000000000000000000000000000001"),
469 token1: address!("0000000000000000000000000000000000000002"),
470 caller: Address::ZERO,
471 };
472 let ours = remove_liquidity_and_collect(&p, 9_999_999_999, POSITION_MANAGER);
473 let core =
474 wp_evm_v3_core::plan::remove_liquidity_and_collect(&p, 9_999_999_999, POSITION_MANAGER);
475 assert_eq!(ours.calls[0].calldata, core.calls[0].calldata);
476 assert_eq!(ours.calls[0].target, core.calls[0].target);
477 }
478
479 #[test]
480 fn claim_cl_gauge_rewards_targets_voter_with_ordered_arrays() {
481 use alloy_sol_types::SolCall;
482 let voter = address!("9f59398d0a397b2eEB8a6123a6c7295cb0b0062D");
483 let claims = vec![
484 GaugeClaim {
485 gauge: address!("1111111111111111111111111111111111111111"),
486 reward_tokens: vec![
487 address!("aaaa000000000000000000000000000000000000"),
488 address!("bbbb000000000000000000000000000000000000"),
489 ],
490 token_ids: vec![U256::from(10u64), U256::from(11u64)],
491 },
492 GaugeClaim {
493 gauge: address!("2222222222222222222222222222222222222222"),
494 reward_tokens: vec![address!("cccc000000000000000000000000000000000000")],
495 token_ids: vec![U256::from(20u64)],
496 },
497 ];
498 let frag = claim_cl_gauge_rewards(voter, &claims);
499
500 assert_eq!(frag.calls.len(), 1);
501 assert_eq!(frag.calls[0].target, voter);
502 assert!(frag.approvals.is_empty());
503 assert_eq!(frag.value, U256::ZERO);
504 assert_eq!(&frag.calls[0].calldata[..4], &[0xea, 0xb3, 0x7e, 0xec]);
506
507 let decoded =
509 wp_evm_ramses_interfaces::gauge::IRamsesVoter::claimClGaugeRewardsCall::abi_decode(
510 &frag.calls[0].calldata,
511 )
512 .expect("decode");
513 assert_eq!(decoded._gauges, vec![claims[0].gauge, claims[1].gauge]);
514 assert_eq!(decoded._tokens[0], claims[0].reward_tokens);
515 assert_eq!(decoded._nfpTokenIds[1], claims[1].token_ids);
516
517 let reference = wp_evm_ramses_interfaces::gauge::IRamsesVoter::claimClGaugeRewardsCall {
521 _gauges: vec![
522 address!("1111111111111111111111111111111111111111"),
523 address!("2222222222222222222222222222222222222222"),
524 ],
525 _tokens: vec![
526 vec![
527 address!("aaaa000000000000000000000000000000000000"),
528 address!("bbbb000000000000000000000000000000000000"),
529 ],
530 vec![address!("cccc000000000000000000000000000000000000")],
531 ],
532 _nfpTokenIds: vec![vec![U256::from(10u64), U256::from(11u64)], vec![U256::from(20u64)]],
533 }
534 .abi_encode();
535 assert_eq!(&frag.calls[0].calldata[..], &reference[..]);
536 }
537
538 #[test]
539 fn claim_gauge_targets_gauge_with_get_reward_selector() {
540 let gauge = address!("4444444444444444444444444444444444444444");
541 let frag = claim_gauge(gauge, U256::from(42u64));
542 assert_eq!(frag.calls.len(), 1);
543 assert_eq!(frag.calls[0].target, gauge);
544 assert!(frag.approvals.is_empty());
545 assert_eq!(frag.value, U256::ZERO);
546 assert_eq!(&frag.calls[0].calldata[..4], &[0x1c, 0x4b, 0x77, 0x4b]);
548 }
549
550 #[test]
551 fn claim_cl_gauge_rewards_from_grids_none_when_empty_or_all_zero() {
552 let voter = Address::from([9u8; 20]);
553 let zero = grid(0x11, 1, &[1], vec![vec![0]]);
555 assert!(claim_cl_gauge_rewards_from_grids(voter, &[zero]).is_none());
556 assert!(claim_cl_gauge_rewards_from_grids(voter, &[]).is_none());
558 }
559
560 #[test]
561 fn claim_cl_gauge_rewards_from_grids_some_targets_voter_when_nonzero() {
562 let voter = address!("9f59398d0a397b2eEB8a6123a6c7295cb0b0062D");
563 let g = grid(0x11, 1, &[1], vec![vec![7]]);
564 let frag = claim_cl_gauge_rewards_from_grids(voter, &[g]).expect("nonzero -> Some");
565 assert_eq!(frag.calls[0].target, voter);
566 assert_eq!(&frag.calls[0].calldata[..4], &[0xea, 0xb3, 0x7e, 0xec]);
567 }
568
569 fn grid(
570 gauge_byte: u8,
571 tokens: usize,
572 ids: &[u64],
573 earned: Vec<Vec<u64>>,
574 ) -> crate::data::GaugeEarnedGrid {
575 crate::data::GaugeEarnedGrid {
576 gauge: Address::from([gauge_byte; 20]),
577 reward_tokens: (0..tokens).map(|i| Address::from([i as u8 + 1; 20])).collect(),
578 token_ids: ids.iter().map(|&i| U256::from(i)).collect(),
579 earned: earned.into_iter().map(|r| r.into_iter().map(U256::from).collect()).collect(),
580 }
581 }
582
583 #[test]
584 fn build_gauge_claims_drops_all_zero_token_and_position() {
585 let g = grid(0x11, 2, &[100, 101], vec![vec![0, 5], vec![0, 0]]);
588 let claims = build_gauge_claims(&[g]);
589 assert_eq!(claims.len(), 1);
590 assert_eq!(claims[0].reward_tokens, vec![Address::from([1u8; 20])]);
591 assert_eq!(claims[0].token_ids, vec![U256::from(101u64)]);
592 }
593
594 #[test]
595 fn build_gauge_claims_drops_fully_empty_gauge() {
596 let g = grid(0x22, 2, &[1, 2], vec![vec![0, 0], vec![0, 0]]);
597 assert!(build_gauge_claims(&[g]).is_empty());
598 }
599
600 #[test]
601 fn build_gauge_claims_keeps_multiple_gauges() {
602 let a = grid(0x11, 1, &[1], vec![vec![9]]);
603 let b = grid(0x22, 1, &[2], vec![vec![0]]); let c = grid(0x33, 1, &[3], vec![vec![3]]);
605 let claims = build_gauge_claims(&[a, b, c]);
606 assert_eq!(claims.len(), 2);
607 assert_eq!(claims[0].gauge, Address::from([0x11u8; 20]));
608 assert_eq!(claims[1].gauge, Address::from([0x33u8; 20]));
609 }
610
611 #[test]
612 fn build_gauge_claims_empty_input_empty_output() {
613 assert!(build_gauge_claims(&[]).is_empty());
614 }
615}