Skip to content

Commit

Permalink
feat: complete position manager (#18)
Browse files Browse the repository at this point in the history
* 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.

* nit
  • Loading branch information
shuhuiluo authored Dec 21, 2024
1 parent 9c98c75 commit a86c558
Show file tree
Hide file tree
Showing 2 changed files with 276 additions and 21 deletions.
283 changes: 269 additions & 14 deletions src/position_manager.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
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;

/// Shared Action Constants used in the v4 Router and v4 position manager
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.
Expand Down Expand Up @@ -52,6 +55,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<Ether>,
/// The optional permit2 batch permit parameters for spending token0 and token1.
pub batch_permit: Option<BatchPermitOptions>,
/// [`MintSpecificOptions`] or [`IncreaseSpecificOptions`]
pub specific_opts: AddLiquiditySpecificOptions,
}
Expand Down Expand Up @@ -123,15 +128,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<TP: TickDataProvider>(
position: &mut Position<TP>,
options: AddLiquidityOptions,
) -> Result<MethodParameters, Error> {
assert!(position.liquidity > 0, "ZERO_LIQUIDITY");

let mut calldatas: Vec<Bytes> = 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<TP: TickDataProvider>(
position: &Position<TP>,
options: RemoveLiquidityOptions,
) -> Result<MethodParameters, Error> {
let mut calldatas: Vec<Bytes> = 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<TP>(
_position: Position<TP>,
_options: AddLiquidityOptions,
) -> MethodParameters
where
TP: TickDataProvider,
{
unimplemented!("add_call_parameters")
pub fn collect_call_parameters<TP: TickDataProvider>(
position: &Position<TP>,
options: CollectOptions,
) -> MethodParameters {
let mut calldatas: Vec<Bytes> = 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]
Expand Down
14 changes: 7 additions & 7 deletions src/utils/v4_position_planner.rs
Original file line number Diff line number Diff line change
@@ -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<I: TickIndex>(
pub fn add_mint<TP: TickDataProvider>(
&mut self,
pool: &Pool,
tick_lower: I,
tick_upper: I,
pool: &Pool<TP>,
tick_lower: TP::Index,
tick_upper: TP::Index,
liquidity: U256,
amount0_max: u128,
amount1_max: u128,
Expand Down

0 comments on commit a86c558

Please sign in to comment.