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;
79use slipstream_nfpm::{mintCall as slipstreamMintCall, SlipstreamMintParams};
80
81pub use shadow_nfpm::{mintCall, ShadowMintParams};
85
86pub fn swap_exact_in(
87 state: &PoolState,
88 quote: &Quote,
89 params: &ExactInParams,
90 slippage: SlippageBps,
91 deadline: u64,
92 router: Address,
93) -> PlanFragment {
94 let amount_out_min = apply_slippage_min(quote.amount_out, slippage);
95
96 let call_params = ExactInputSingleParams {
97 tokenIn: params.token_in,
98 tokenOut: params.token_out,
99 tickSpacing: alloy_primitives::aliases::I24::try_from(state.tick_spacing)
100 .expect("tick_spacing fits in i24"),
101 recipient: params.recipient,
102 deadline: U256::from(deadline),
103 amountIn: params.amount_in,
104 amountOutMinimum: amount_out_min,
105 sqrtPriceLimitX96: alloy_primitives::aliases::U160::ZERO,
106 };
107
108 let calldata = exactInputSingleCall { params: call_params }.abi_encode().into();
109
110 PlanFragment {
111 calls: vec![Call { target: router, calldata, value: U256::ZERO }],
112 approvals: vec![TokenApproval {
113 token: params.token_in,
114 spender: router,
115 min_amount: params.amount_in,
116 }],
117 value: U256::ZERO,
118 }
119}
120
121pub fn add_liquidity(
122 params: &RamsesAddLiquidityParams,
123 slippage: SlippageBps,
124 deadline: u64,
125 position_manager: Address,
126) -> PlanFragment {
127 let mint_params = ShadowMintParams {
128 token0: params.token0,
129 token1: params.token1,
130 tickSpacing: alloy_primitives::aliases::I24::try_from(params.tick_spacing)
131 .expect("tick_spacing fits in i24"),
132 tickLower: alloy_primitives::aliases::I24::try_from(params.tick_lower)
133 .expect("tick_lower within i24 range"),
134 tickUpper: alloy_primitives::aliases::I24::try_from(params.tick_upper)
135 .expect("tick_upper within i24 range"),
136 amount0Desired: params.amount0_desired,
137 amount1Desired: params.amount1_desired,
138 amount0Min: apply_slippage_min(params.amount0_desired, slippage),
139 amount1Min: apply_slippage_min(params.amount1_desired, slippage),
140 recipient: params.recipient,
141 deadline: U256::from(deadline),
142 };
143 let calldata = shadowMintCall { params: mint_params }.abi_encode().into();
144 PlanFragment {
145 calls: vec![Call { target: position_manager, calldata, value: U256::ZERO }],
146 approvals: vec![
147 TokenApproval {
148 token: params.token0,
149 spender: position_manager,
150 min_amount: params.amount0_desired,
151 },
152 TokenApproval {
153 token: params.token1,
154 spender: position_manager,
155 min_amount: params.amount1_desired,
156 },
157 ],
158 value: U256::ZERO,
159 }
160}
161
162pub fn add_liquidity_slipstream(
163 params: &RamsesAddLiquidityParams,
164 slippage: SlippageBps,
165 deadline: u64,
166 position_manager: Address,
167) -> PlanFragment {
168 let mint_params = SlipstreamMintParams {
169 token0: params.token0,
170 token1: params.token1,
171 tickSpacing: alloy_primitives::aliases::I24::try_from(params.tick_spacing)
172 .expect("tick_spacing fits in i24"),
173 tickLower: alloy_primitives::aliases::I24::try_from(params.tick_lower)
174 .expect("tick_lower within i24 range"),
175 tickUpper: alloy_primitives::aliases::I24::try_from(params.tick_upper)
176 .expect("tick_upper within i24 range"),
177 amount0Desired: params.amount0_desired,
178 amount1Desired: params.amount1_desired,
179 amount0Min: apply_slippage_min(params.amount0_desired, slippage),
180 amount1Min: apply_slippage_min(params.amount1_desired, slippage),
181 recipient: params.recipient,
182 deadline: U256::from(deadline),
183 sqrtPriceX96: alloy_primitives::aliases::U160::ZERO,
184 };
185 let calldata = slipstreamMintCall { params: mint_params }.abi_encode().into();
186 PlanFragment {
187 calls: vec![Call { target: position_manager, calldata, value: U256::ZERO }],
188 approvals: vec![
189 TokenApproval {
190 token: params.token0,
191 spender: position_manager,
192 min_amount: params.amount0_desired,
193 },
194 TokenApproval {
195 token: params.token1,
196 spender: position_manager,
197 min_amount: params.amount1_desired,
198 },
199 ],
200 value: U256::ZERO,
201 }
202}
203
204#[allow(clippy::too_many_arguments)]
205pub fn increase_liquidity(
206 token_id: alloy_primitives::U256,
207 token0: alloy_primitives::Address,
208 token1: alloy_primitives::Address,
209 amount0_desired: alloy_primitives::U256,
210 amount1_desired: alloy_primitives::U256,
211 slippage: SlippageBps,
212 deadline: u64,
213 position_manager: Address,
214) -> PlanFragment {
215 wp_evm_v3_core::plan::increase_liquidity(
216 token_id,
217 token0,
218 token1,
219 amount0_desired,
220 amount1_desired,
221 slippage,
222 deadline,
223 position_manager,
224 )
225}
226
227pub fn remove_liquidity(
228 params: &RemoveLiquidityParams,
229 deadline: u64,
230 position_manager: Address,
231) -> PlanFragment {
232 wp_evm_v3_core::plan::remove_liquidity(params, deadline, position_manager)
233}
234
235pub fn remove_liquidity_and_collect(
248 params: &RemoveAndCollectParams,
249 deadline: u64,
250 position_manager: Address,
251) -> PlanFragment {
252 wp_evm_v3_core::plan::remove_liquidity_and_collect(params, deadline, position_manager)
253}
254
255pub fn collect_fees(params: &CollectFeesParams, position_manager: Address) -> PlanFragment {
256 wp_evm_v3_core::plan::collect_fees(params, position_manager)
257}
258
259pub fn claim_cl_gauge_rewards(voter: Address, claims: &[GaugeClaim]) -> PlanFragment {
267 let gauges: Vec<Address> = claims.iter().map(|c| c.gauge).collect();
268 let tokens: Vec<Vec<Address>> = claims.iter().map(|c| c.reward_tokens.clone()).collect();
269 let nfp_token_ids: Vec<Vec<U256>> = claims.iter().map(|c| c.token_ids.clone()).collect();
270 let calldata = IRamsesVoter::claimClGaugeRewardsCall {
271 _gauges: gauges,
272 _tokens: tokens,
273 _nfpTokenIds: nfp_token_ids,
274 }
275 .abi_encode()
276 .into();
277 PlanFragment {
278 calls: vec![Call { target: voter, calldata, value: U256::ZERO }],
279 approvals: vec![],
280 value: U256::ZERO,
281 }
282}
283
284pub fn claim_gauge(gauge: Address, token_id: U256) -> PlanFragment {
288 let calldata = IVelodromeCLGauge::getRewardCall { tokenId: token_id }.abi_encode().into();
289 PlanFragment {
290 calls: vec![Call { target: gauge, calldata, value: U256::ZERO }],
291 approvals: vec![],
292 value: U256::ZERO,
293 }
294}
295
296pub fn build_gauge_claims(grids: &[GaugeEarnedGrid]) -> Vec<GaugeClaim> {
302 let mut out = Vec::new();
303 for g in grids {
304 let kept_tokens: Vec<usize> = (0..g.reward_tokens.len())
305 .filter(|&t| (0..g.token_ids.len()).any(|p| g.earned[t][p] > U256::ZERO))
306 .collect();
307 let kept_positions: Vec<usize> = (0..g.token_ids.len())
308 .filter(|&p| (0..g.reward_tokens.len()).any(|t| g.earned[t][p] > U256::ZERO))
309 .collect();
310 if kept_tokens.is_empty() || kept_positions.is_empty() {
311 continue;
312 }
313 out.push(GaugeClaim {
314 gauge: g.gauge,
315 reward_tokens: kept_tokens.iter().map(|&t| g.reward_tokens[t]).collect(),
316 token_ids: kept_positions.iter().map(|&p| g.token_ids[p]).collect(),
317 });
318 }
319 out
320}
321
322pub fn claim_cl_gauge_rewards_from_grids(
327 voter: Address,
328 grids: &[GaugeEarnedGrid],
329) -> Option<PlanFragment> {
330 let claims = build_gauge_claims(grids);
331 if claims.is_empty() {
332 None
333 } else {
334 Some(claim_cl_gauge_rewards(voter, &claims))
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341 use alloy_primitives::{address, Address};
342
343 const ROUTER: Address = address!("0x2222222222222222222222222222222222222222");
344 const POSITION_MANAGER: Address = address!("0x3333333333333333333333333333333333333333");
345
346 fn dummy_pool_state(t0: Address, t1: Address) -> PoolState {
347 PoolState {
348 token0: t0,
349 token1: t1,
350 fee: 0,
351 tick_spacing: 50,
352 sqrt_price_x96: U256::from(1u64) << 96,
353 liquidity: 0,
354 tick: 0,
355 ticks: vec![],
356 }
357 }
358
359 #[test]
360 fn plan_swap_emits_ramses_selector() {
361 let token_in = address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
362 let token_out = address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
363 let s = dummy_pool_state(token_in, token_out);
364 let q = Quote {
365 amount_in: U256::from(1_000_000u64),
366 amount_out: U256::from(500_000_000_000_000u64),
367 sqrt_price_x96_after: s.sqrt_price_x96,
368 price_impact_bps: 0,
369 };
370 let p = ExactInParams {
371 token_in,
372 token_out,
373 amount_in: q.amount_in,
374 recipient: address!("0x0000000000000000000000000000000000000099"),
375 };
376 let frag = swap_exact_in(&s, &q, &p, SlippageBps::new(50), 9_999_999_999, ROUTER);
377 assert_eq!(frag.calls.len(), 1);
378 assert_eq!(frag.calls[0].target, ROUTER);
379 assert_eq!(&frag.calls[0].calldata[..4], &[0xa0, 0x26, 0x38, 0x3e]);
380 }
381
382 #[test]
383 fn plan_swap_encodes_tick_spacing_not_fee() {
384 let token_in = address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
385 let token_out = address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
386 let mut s = dummy_pool_state(token_in, token_out);
387 s.tick_spacing = 200;
388 let q = Quote {
389 amount_in: U256::from(1_000_000u64),
390 amount_out: U256::from(500_000_000_000_000u64),
391 sqrt_price_x96_after: s.sqrt_price_x96,
392 price_impact_bps: 0,
393 };
394 let p = ExactInParams {
395 token_in,
396 token_out,
397 amount_in: q.amount_in,
398 recipient: address!("0x0000000000000000000000000000000000000099"),
399 };
400 let frag = swap_exact_in(&s, &q, &p, SlippageBps::new(50), 9_999_999_999, ROUTER);
401 let decoded = exactInputSingleCall::abi_decode(&frag.calls[0].calldata).expect("decode ok");
402 assert_eq!(
403 decoded.params.tickSpacing,
404 alloy_primitives::aliases::I24::try_from(200i32).unwrap()
405 );
406 }
407
408 #[test]
409 fn add_liquidity_targets_position_manager() {
410 let p = RamsesAddLiquidityParams {
411 token0: address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
412 token1: address!("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
413 tick_spacing: 50,
414 tick_lower: -887_272,
415 tick_upper: 887_272,
416 amount0_desired: U256::from(1_000_000u64),
417 amount1_desired: U256::from(500_000_000_000_000u64),
418 recipient: address!("0x0000000000000000000000000000000000000099"),
419 };
420 let frag = add_liquidity(&p, SlippageBps::new(50), 9_999_999_999, POSITION_MANAGER);
421 assert_eq!(frag.calls[0].target, POSITION_MANAGER);
422 assert_eq!(frag.approvals.len(), 2);
423 }
424
425 #[test]
426 fn remove_liquidity_and_collect_targets_position_manager_with_outer_multicall() {
427 use alloy_sol_types::SolValue;
431 let p = RemoveAndCollectParams {
432 token_id: U256::from(1u64),
433 liquidity: 1_000u128,
434 amount0_min: Some(U256::from(99u64)),
435 amount1_min: Some(U256::from(199u64)),
436 recipient: address!("0000000000000000000000000000000000000099"),
437 token0: address!("0000000000000000000000000000000000000001"),
438 token1: address!("0000000000000000000000000000000000000002"),
439 caller: Address::ZERO,
440 burn: false,
441 };
442 let frag = remove_liquidity_and_collect(&p, 9_999_999_999, POSITION_MANAGER);
443 assert_eq!(frag.calls.len(), 1);
444 assert_eq!(frag.calls[0].target, POSITION_MANAGER);
445 assert_eq!(
446 &frag.calls[0].calldata[..4],
447 &[0xac, 0x96, 0x50, 0xd8],
448 "outer selector must be multicall(bytes[])"
449 );
450 assert!(frag.approvals.is_empty());
451 assert_eq!(frag.value, U256::ZERO);
452
453 let (inner,): (Vec<alloy_primitives::Bytes>,) =
457 <(Vec<alloy_primitives::Bytes>,)>::abi_decode_params(&frag.calls[0].calldata[4..])
458 .expect("decode outer multicall params");
459 assert_eq!(inner.len(), 2);
460 assert_eq!(&inner[0][..4], &[0x0c, 0x49, 0xcc, 0xbe], "inner[0] = decreaseLiquidity");
461 assert_eq!(&inner[1][..4], &[0xfc, 0x6f, 0x78, 0x65], "inner[1] = collect");
462 }
463
464 #[test]
465 fn remove_liquidity_and_collect_delegation_matches_v3_core_bytewise() {
466 let p = RemoveAndCollectParams {
469 token_id: U256::from(42u64),
470 liquidity: 1_000_000_000_000u128,
471 amount0_min: Some(U256::from(500_000u64)),
472 amount1_min: Some(U256::from(1_000_000_000u64)),
473 recipient: address!("0000000000000000000000000000000000000099"),
474 token0: address!("0000000000000000000000000000000000000001"),
475 token1: address!("0000000000000000000000000000000000000002"),
476 caller: Address::ZERO,
477 burn: false,
478 };
479 let ours = remove_liquidity_and_collect(&p, 9_999_999_999, POSITION_MANAGER);
480 let core =
481 wp_evm_v3_core::plan::remove_liquidity_and_collect(&p, 9_999_999_999, POSITION_MANAGER);
482 assert_eq!(ours.calls[0].calldata, core.calls[0].calldata);
483 assert_eq!(ours.calls[0].target, core.calls[0].target);
484 }
485
486 #[test]
487 fn remove_liquidity_and_collect_delegation_carries_burn() {
488 let p = RemoveAndCollectParams {
489 token_id: U256::from(42u64),
490 liquidity: 1_000_000_000_000u128,
491 amount0_min: Some(U256::from(500_000u64)),
492 amount1_min: Some(U256::from(1_000_000_000u64)),
493 recipient: address!("0000000000000000000000000000000000000099"),
494 token0: address!("0000000000000000000000000000000000000001"),
495 token1: address!("0000000000000000000000000000000000000002"),
496 caller: Address::ZERO,
497 burn: true,
498 };
499 let frag = remove_liquidity_and_collect(&p, 9_999_999_999, POSITION_MANAGER);
500 let v3 =
501 wp_evm_v3_core::plan::remove_liquidity_and_collect(&p, 9_999_999_999, POSITION_MANAGER);
502 assert_eq!(frag, v3, "ramses delegates burn:true byte-for-byte");
503 }
504
505 #[test]
506 fn claim_cl_gauge_rewards_targets_voter_with_ordered_arrays() {
507 use alloy_sol_types::SolCall;
508 let voter = address!("9f59398d0a397b2eEB8a6123a6c7295cb0b0062D");
509 let claims = vec![
510 GaugeClaim {
511 gauge: address!("1111111111111111111111111111111111111111"),
512 reward_tokens: vec![
513 address!("aaaa000000000000000000000000000000000000"),
514 address!("bbbb000000000000000000000000000000000000"),
515 ],
516 token_ids: vec![U256::from(10u64), U256::from(11u64)],
517 },
518 GaugeClaim {
519 gauge: address!("2222222222222222222222222222222222222222"),
520 reward_tokens: vec![address!("cccc000000000000000000000000000000000000")],
521 token_ids: vec![U256::from(20u64)],
522 },
523 ];
524 let frag = claim_cl_gauge_rewards(voter, &claims);
525
526 assert_eq!(frag.calls.len(), 1);
527 assert_eq!(frag.calls[0].target, voter);
528 assert!(frag.approvals.is_empty());
529 assert_eq!(frag.value, U256::ZERO);
530 assert_eq!(&frag.calls[0].calldata[..4], &[0xea, 0xb3, 0x7e, 0xec]);
532
533 let decoded =
535 wp_evm_ramses_interfaces::gauge::IRamsesVoter::claimClGaugeRewardsCall::abi_decode(
536 &frag.calls[0].calldata,
537 )
538 .expect("decode");
539 assert_eq!(decoded._gauges, vec![claims[0].gauge, claims[1].gauge]);
540 assert_eq!(decoded._tokens[0], claims[0].reward_tokens);
541 assert_eq!(decoded._nfpTokenIds[1], claims[1].token_ids);
542
543 let reference = wp_evm_ramses_interfaces::gauge::IRamsesVoter::claimClGaugeRewardsCall {
547 _gauges: vec![
548 address!("1111111111111111111111111111111111111111"),
549 address!("2222222222222222222222222222222222222222"),
550 ],
551 _tokens: vec![
552 vec![
553 address!("aaaa000000000000000000000000000000000000"),
554 address!("bbbb000000000000000000000000000000000000"),
555 ],
556 vec![address!("cccc000000000000000000000000000000000000")],
557 ],
558 _nfpTokenIds: vec![vec![U256::from(10u64), U256::from(11u64)], vec![U256::from(20u64)]],
559 }
560 .abi_encode();
561 assert_eq!(&frag.calls[0].calldata[..], &reference[..]);
562 }
563
564 #[test]
565 fn claim_gauge_targets_gauge_with_get_reward_selector() {
566 let gauge = address!("4444444444444444444444444444444444444444");
567 let frag = claim_gauge(gauge, U256::from(42u64));
568 assert_eq!(frag.calls.len(), 1);
569 assert_eq!(frag.calls[0].target, gauge);
570 assert!(frag.approvals.is_empty());
571 assert_eq!(frag.value, U256::ZERO);
572 assert_eq!(&frag.calls[0].calldata[..4], &[0x1c, 0x4b, 0x77, 0x4b]);
574 }
575
576 #[test]
577 fn claim_cl_gauge_rewards_from_grids_none_when_empty_or_all_zero() {
578 let voter = Address::from([9u8; 20]);
579 let zero = grid(0x11, 1, &[1], vec![vec![0]]);
581 assert!(claim_cl_gauge_rewards_from_grids(voter, &[zero]).is_none());
582 assert!(claim_cl_gauge_rewards_from_grids(voter, &[]).is_none());
584 }
585
586 #[test]
587 fn claim_cl_gauge_rewards_from_grids_some_targets_voter_when_nonzero() {
588 let voter = address!("9f59398d0a397b2eEB8a6123a6c7295cb0b0062D");
589 let g = grid(0x11, 1, &[1], vec![vec![7]]);
590 let frag = claim_cl_gauge_rewards_from_grids(voter, &[g]).expect("nonzero -> Some");
591 assert_eq!(frag.calls[0].target, voter);
592 assert_eq!(&frag.calls[0].calldata[..4], &[0xea, 0xb3, 0x7e, 0xec]);
593 }
594
595 fn grid(
596 gauge_byte: u8,
597 tokens: usize,
598 ids: &[u64],
599 earned: Vec<Vec<u64>>,
600 ) -> crate::data::GaugeEarnedGrid {
601 crate::data::GaugeEarnedGrid {
602 gauge: Address::from([gauge_byte; 20]),
603 reward_tokens: (0..tokens).map(|i| Address::from([i as u8 + 1; 20])).collect(),
604 token_ids: ids.iter().map(|&i| U256::from(i)).collect(),
605 earned: earned.into_iter().map(|r| r.into_iter().map(U256::from).collect()).collect(),
606 }
607 }
608
609 #[test]
610 fn build_gauge_claims_drops_all_zero_token_and_position() {
611 let g = grid(0x11, 2, &[100, 101], vec![vec![0, 5], vec![0, 0]]);
614 let claims = build_gauge_claims(&[g]);
615 assert_eq!(claims.len(), 1);
616 assert_eq!(claims[0].reward_tokens, vec![Address::from([1u8; 20])]);
617 assert_eq!(claims[0].token_ids, vec![U256::from(101u64)]);
618 }
619
620 #[test]
621 fn build_gauge_claims_drops_fully_empty_gauge() {
622 let g = grid(0x22, 2, &[1, 2], vec![vec![0, 0], vec![0, 0]]);
623 assert!(build_gauge_claims(&[g]).is_empty());
624 }
625
626 #[test]
627 fn build_gauge_claims_keeps_multiple_gauges() {
628 let a = grid(0x11, 1, &[1], vec![vec![9]]);
629 let b = grid(0x22, 1, &[2], vec![vec![0]]); let c = grid(0x33, 1, &[3], vec![vec![3]]);
631 let claims = build_gauge_claims(&[a, b, c]);
632 assert_eq!(claims.len(), 2);
633 assert_eq!(claims[0].gauge, Address::from([0x11u8; 20]));
634 assert_eq!(claims[1].gauge, Address::from([0x33u8; 20]));
635 }
636
637 #[test]
638 fn build_gauge_claims_empty_input_empty_output() {
639 assert!(build_gauge_claims(&[]).is_empty());
640 }
641}