Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: complete position manager #18

Merged
merged 2 commits into from
Dec 21, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
282 changes: 268 additions & 14 deletions src/position_manager.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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<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 +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<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);
shuhuiluo marked this conversation as resolved.
Show resolved Hide resolved
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);
}

shuhuiluo marked this conversation as resolved.
Show resolved Hide resolved
// 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,
})
}

shuhuiluo marked this conversation as resolved.
Show resolved Hide resolved
shuhuiluo marked this conversation as resolved.
Show resolved Hide resolved
/// 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"
);

shuhuiluo marked this conversation as resolved.
Show resolved Hide resolved
// 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
Loading