Skip to main content

wp_evm_ramses_core/
plan.rs

1//! Pure plan functions for the Ramses family.
2
3use 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
228/// Build an atomic `multicall(decreaseLiquidity, collect)` plan fragment
229/// for a Ramses-family NFPM position.
230///
231/// Delegates straight to `wp_evm_v3_core::plan::remove_liquidity_and_collect`
232/// — Ramses-fork NFPMs inherit V3's `decreaseLiquidity` + `collect` + outer
233/// `multicall(bytes[])` byte-for-byte (the family's structural divergence
234/// from V3 is `tickSpacing` instead of `fee` in mint params; remove+collect
235/// has no `fee` / `tickSpacing` field on the inner ABI). Mirrors this
236/// module's `remove_liquidity` one-line delegation.
237///
238/// Native-unwrap composition is NOT done here — the facade layer pairs
239/// this with `compose_native_remove_collect_multicall`.
240pub 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        // Outer call must be `multicall(bytes[])` (selector `0xac9650d8`,
342        // universal across V3/Ramses/Velodrome) over exactly 2 inner
343        // calls: decreaseLiquidity then collect.
344        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        // Decode outer and assert inner pair: decreaseLiquidity (0x0c49ccbe)
367        // followed by collect (0xfc6f7865). These selectors are V3-canonical
368        // and Ramses inherits them via the v3-core delegation.
369        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        // Ramses delegates to v3-core verbatim — proving byte-equality
380        // here pins the delegation against accidental local re-encoding.
381        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}