From e8823aabe2ecfe43920d56d5b0554c0f98938728 Mon Sep 17 00:00:00 2001 From: Shuhui Luo <107524008+shuhuiluo@users.noreply.github.com> Date: Sat, 21 Dec 2024 02:40:12 -0500 Subject: [PATCH 1/2] feat: complete position manager Add comprehensive implementations for core position management functions, including adding, removing, and collecting liquidity. Introduce support for calldata generation with detailed handling of various scenarios like minting, migration, and slippage adjustments. --- src/position_manager.rs | 282 +++++++++++++++++++++++++++++-- src/utils/v4_position_planner.rs | 14 +- 2 files changed, 275 insertions(+), 21 deletions(-) diff --git a/src/position_manager.rs b/src/position_manager.rs index a6697f0..a2671fa 100644 --- a/src/position_manager.rs +++ b/src/position_manager.rs @@ -1,15 +1,17 @@ -use crate::prelude::*; -use alloy_primitives::{Address, Bytes, PrimitiveSignature, U160, U256}; +use crate::prelude::{Error, *}; +use alloc::vec::Vec; +use alloy_primitives::{address, Address, Bytes, PrimitiveSignature, U160, U256}; use alloy_sol_types::{eip712_domain, SolCall}; use derive_more::{Deref, DerefMut}; -use uniswap_sdk_core::prelude::{Ether, Percent}; -use uniswap_v3_sdk::{ - entities::TickDataProvider, - prelude::{IERC721Permit, MethodParameters}, +use uniswap_sdk_core::prelude::*; +use uniswap_v3_sdk::prelude::{ + IERC721Permit, MethodParameters, MintAmounts, TickDataProvider, TickIndex, }; pub use uniswap_v3_sdk::prelude::NFTPermitData; +pub const MSG_SENDER: Address = address!("0000000000000000000000000000000000000001"); + #[derive(Debug, Clone, PartialEq, Eq)] pub struct CommonOptions { /// How much the pool price is allowed to move from the specified action. @@ -52,6 +54,8 @@ pub struct AddLiquidityOptions { pub common_opts: CommonOptions, /// Whether to spend ether. If true, one of the currencies must be the NATIVE currency. pub use_native: Option, + /// The optional permit2 batch permit parameters for spending token0 and token1. + pub batch_permit: Option, /// [`MintSpecificOptions`] or [`IncreaseSpecificOptions`] pub specific_opts: AddLiquiditySpecificOptions, } @@ -123,15 +127,265 @@ pub fn create_call_parameters(pool_key: PoolKey, sqrt_price_x96: U160) -> Method } } +/// Encodes the method parameters for adding liquidity to a position. +/// +/// ## Notes +/// +/// - If the pool does not exist yet, the `initializePool` call is encoded. +/// - If it is a mint, encode `MINT_POSITION`. If migrating, encode a `SETTLE` and `SWEEP` for both +/// currencies. Else, encode a `SETTLE_PAIR`. If on a NATIVE pool, encode a `SWEEP`. +/// - Else, encode `INCREASE_LIQUIDITY` and `SETTLE_PAIR`. If it is on a NATIVE pool, encode a +/// `SWEEP`. +/// +/// ## Arguments +/// +/// * `position`: The position to be added. +/// * `options`: The options for adding liquidity. +#[inline] +pub fn add_call_parameters( + position: &mut Position, + options: AddLiquidityOptions, +) -> Result { + assert!(position.liquidity > 0, "ZERO_LIQUIDITY"); + + let mut calldatas: Vec = Vec::with_capacity(3); + let mut planner = V4PositionPlanner::default(); + + // Encode initialize pool. + if let AddLiquiditySpecificOptions::Mint(opts) = options.specific_opts { + if opts.create_pool { + // No planner used here because initializePool is not supported as an Action + calldatas.push(encode_initialize_pool( + position.pool.pool_key.clone(), + opts.sqrt_price_x96.expect("NO_SQRT_PRICE"), + )); + } + } + + // adjust for slippage + let MintAmounts { + amount0: amount0_max, + amount1: amount1_max, + } = position.mint_amounts_with_slippage(&options.slippage_tolerance)?; + + // We use permit2 to approve tokens to the position manager + if let Some(batch_permit) = options.batch_permit { + calldatas.push(encode_permit_batch( + batch_permit.owner, + batch_permit.permit_batch, + batch_permit.signature, + )); + } + + match options.specific_opts { + AddLiquiditySpecificOptions::Mint(opts) => { + planner.add_mint( + &position.pool, + position.tick_lower, + position.tick_upper, + U256::from(position.liquidity), + u128::try_from(amount0_max).unwrap(), + u128::try_from(amount1_max).unwrap(), + opts.recipient, + options.common_opts.hook_data, + ); + } + AddLiquiditySpecificOptions::Increase(opts) => { + planner.add_increase( + opts.token_id, + U256::from(position.liquidity), + u128::try_from(amount0_max).unwrap(), + u128::try_from(amount1_max).unwrap(), + options.common_opts.hook_data, + ); + } + } + + // If migrating, we need to settle and sweep both currencies individually + if let AddLiquiditySpecificOptions::Mint(opts) = options.specific_opts { + if opts.migrate { + // payer is v4 positiion manager + planner.add_settle(&position.pool.currency0, false, None); + planner.add_settle(&position.pool.currency1, false, None); + planner.add_sweep(&position.pool.currency0, opts.recipient); + planner.add_sweep(&position.pool.currency1, opts.recipient); + } else { + // need to settle both currencies when minting / adding liquidity (user is the payer) + planner.add_settle_pair(&position.pool.currency0, &position.pool.currency1); + } + } else { + planner.add_settle_pair(&position.pool.currency0, &position.pool.currency1); + } + + // Any sweeping must happen after the settling. + let mut value = U256::ZERO; + if options.use_native.is_some() { + assert!( + position.pool.currency0.is_native() || position.pool.currency1.is_native(), + "NO_NATIVE" + ); + let native_currency: &Currency; + (native_currency, value) = if position.pool.currency0.is_native() { + (&position.pool.currency0, amount0_max) + } else { + (&position.pool.currency1, amount1_max) + }; + planner.add_sweep(native_currency, MSG_SENDER); + } + + calldatas.push(encode_modify_liquidities( + planner.0.finalize(), + options.common_opts.deadline, + )); + + Ok(MethodParameters { + calldata: encode_multicall(calldatas), + value, + }) +} + +/// Produces the calldata for completely or partially exiting a position +/// +/// ## Notes +/// +/// - If the liquidity percentage is 100%, encode `BURN_POSITION` and then `TAKE_PAIR`. +/// - Else, encode `DECREASE_LIQUIDITY` and then `TAKE_PAIR`. +/// +/// ## Arguments +/// +/// * `position`: The position to exit +/// * `options`: Additional information necessary for generating the calldata +#[inline] +pub fn remove_call_parameters( + position: &Position, + options: RemoveLiquidityOptions, +) -> Result { + let mut calldatas: Vec = Vec::with_capacity(2); + let mut planner = V4PositionPlanner::default(); + + let token_id = options.token_id; + + if options.burn_token { + // if burnToken is true, the specified liquidity percentage must be 100% + assert_eq!( + options.liquidity_percentage, + Percent::new(1, 1), + "CANNOT_BURN" + ); + + // if there is a permit, encode the ERC721Permit permit call + if let Some(permit) = options.permit { + calldatas.push(encode_erc721_permit( + permit.spender, + token_id, + permit.deadline, + permit.nonce, + permit.signature.as_bytes().into(), + )); + } + + // slippage-adjusted amounts derived from current position liquidity + let (amount0_min, amount1_min) = + position.burn_amounts_with_slippage(&options.common_opts.slippage_tolerance)?; + planner.add_burn( + token_id, + u128::try_from(amount0_min).unwrap(), + u128::try_from(amount1_min).unwrap(), + options.common_opts.hook_data, + ); + } else { + // construct a partial position with a percentage of liquidity + let partial_position = Position::new( + Pool::new( + position.pool.currency0.clone(), + position.pool.currency1.clone(), + position.pool.fee, + position.pool.tick_spacing.to_i24().as_i32(), + position.pool.hooks, + position.pool.sqrt_price_x96, + position.pool.liquidity, + )?, + (options.liquidity_percentage * Percent::new(position.liquidity, 1)) + .quotient() + .to_u128() + .unwrap(), + position.tick_lower.try_into().unwrap(), + position.tick_upper.try_into().unwrap(), + ); + + // If the partial position has liquidity=0, this is a collect call and collectCallParameters + // should be used + assert!(partial_position.liquidity > 0, "ZERO_LIQUIDITY"); + + // slippage-adjusted underlying amounts + let (amount0_min, amount1_min) = + partial_position.burn_amounts_with_slippage(&options.common_opts.slippage_tolerance)?; + + planner.add_decrease( + token_id, + U256::from(partial_position.liquidity), + u128::try_from(amount0_min).unwrap(), + u128::try_from(amount1_min).unwrap(), + options.common_opts.hook_data, + ); + } + + planner.add_take_pair( + &position.pool.currency0, + &position.pool.currency1, + MSG_SENDER, + ); + calldatas.push(encode_modify_liquidities( + planner.0.finalize(), + options.common_opts.deadline, + )); + + Ok(MethodParameters { + calldata: encode_multicall(calldatas), + value: U256::ZERO, + }) +} + +/// Produces the calldata for collecting fees from a position +/// +/// ## Arguments +/// +/// * `position`: The position to collect fees from +/// * `options`: Additional information necessary for generating the calldata #[inline] -pub fn add_call_parameters( - _position: Position, - _options: AddLiquidityOptions, -) -> MethodParameters -where - TP: TickDataProvider, -{ - unimplemented!("add_call_parameters") +pub fn collect_call_parameters( + position: &Position, + options: CollectOptions, +) -> MethodParameters { + let mut calldatas: Vec = Vec::with_capacity(1); + let mut planner = V4PositionPlanner::default(); + + // To collect fees in V4, we need to: + // - encode a decrease liquidity by 0 + // - and encode a TAKE_PAIR + planner.add_decrease( + options.token_id, + U256::ZERO, + 0, + 0, + options.common_opts.hook_data, + ); + + planner.add_take_pair( + &position.pool.currency0, + &position.pool.currency1, + options.recipient, + ); + + calldatas.push(encode_modify_liquidities( + planner.0.finalize(), + options.common_opts.deadline, + )); + + MethodParameters { + calldata: encode_multicall(calldatas), + value: U256::ZERO, + } } #[inline] diff --git a/src/utils/v4_position_planner.rs b/src/utils/v4_position_planner.rs index 46b756a..50f7aae 100644 --- a/src/utils/v4_position_planner.rs +++ b/src/utils/v4_position_planner.rs @@ -1,20 +1,20 @@ -use crate::{entities::Pool, prelude::*}; +use crate::prelude::*; use alloy_primitives::{Address, Bytes, U256}; use derive_more::{Deref, DerefMut}; use uniswap_sdk_core::prelude::BaseCurrency; -use uniswap_v3_sdk::prelude::TickIndex; +use uniswap_v3_sdk::prelude::{TickDataProvider, TickIndex}; #[derive(Clone, Debug, Default, PartialEq, Deref, DerefMut)] -pub struct V4PositionPlanner(V4Planner); +pub struct V4PositionPlanner(pub V4Planner); impl V4PositionPlanner { #[allow(clippy::too_many_arguments)] #[inline] - pub fn add_mint( + pub fn add_mint( &mut self, - pool: &Pool, - tick_lower: I, - tick_upper: I, + pool: &Pool, + tick_lower: TP::Index, + tick_upper: TP::Index, liquidity: U256, amount0_max: u128, amount1_max: u128, From 56996c814542fc236561cb2669c71523a9a0ab9c Mon Sep 17 00:00:00 2001 From: Shuhui Luo <107524008+shuhuiluo@users.noreply.github.com> Date: Sat, 21 Dec 2024 02:47:45 -0500 Subject: [PATCH 2/2] nit --- src/position_manager.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/position_manager.rs b/src/position_manager.rs index a2671fa..ada9d87 100644 --- a/src/position_manager.rs +++ b/src/position_manager.rs @@ -10,6 +10,7 @@ use uniswap_v3_sdk::prelude::{ pub use uniswap_v3_sdk::prelude::NFTPermitData; +/// Shared Action Constants used in the v4 Router and v4 position manager pub const MSG_SENDER: Address = address!("0000000000000000000000000000000000000001"); #[derive(Debug, Clone, PartialEq, Eq)]