Skip to content

Commit

Permalink
feat: add PositionManager module and batch permit support (#17)
Browse files Browse the repository at this point in the history
* feat: add PositionManager module and batch permit support

Introduce the `PositionManager` module with functionality for handling positions and liquidity in Uniswap V4. Add structs for `PermitSingle` and `PermitBatch`, enabling batch permit operations, and integrate the necessary dependencies for signing and encoding methods. Updated the library structure to include the new module and adjusted the Cargo.toml file.

* type
  • Loading branch information
shuhuiluo authored Dec 15, 2024
1 parent e2130e2 commit 9c98c75
Show file tree
Hide file tree
Showing 4 changed files with 312 additions and 3 deletions.
6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[package]
name = "uniswap-v4-sdk-rs"
name = "uniswap-v4-sdk"
version = "0.1.0"
edition = "2021"
authors = ["Shuhui Luo <twitter.com/aureliano_law>"]
Expand All @@ -18,9 +18,11 @@ derive_more = "1.0.0"
rustc-hash = "2.1.0"
thiserror = { version = "2", default-features = false }
uniswap-sdk-core = "3.2.0"
uniswap-v3-sdk = "2.9.0"
uniswap-v3-sdk = "2.9.1"

[dev-dependencies]
alloy-signer = "0.8"
alloy-signer-local = "0.8"
once_cell = "1.20.2"

[features]
Expand Down
52 changes: 52 additions & 0 deletions src/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,56 @@ sol! {
bytes actions;
bytes[] params;
}

interface IAllowanceTransfer {
/// @notice The permit data for a token
#[derive(Debug, Default, PartialEq, Eq)]
struct PermitDetails {
// ERC20 token address
address token;
// the maximum amount allowed to spend
uint160 amount;
// timestamp at which a spender's token allowances become invalid
uint48 expiration;
// an incrementing value indexed per owner,token,and spender for each signature
uint48 nonce;
}

/// @notice The permit message signed for a single token allowance
#[derive(Debug, Default, PartialEq, Eq)]
struct PermitSingle {
// the permit data for a single token allowance
PermitDetails details;
// address permissioned on the allowed tokens
address spender;
// deadline on the permit signature
uint256 sigDeadline;
}

/// @notice The permit message signed for multiple token allowances
#[derive(Debug, Default, PartialEq, Eq)]
struct PermitBatch {
// the permit data for multiple token allowances
PermitDetails[] details;
// address permissioned on the allowed tokens
address spender;
// deadline on the permit signature
uint256 sigDeadline;
}
}

interface IPositionManager {
function initializePool(PoolKey calldata key, uint160 sqrtPriceX96) external payable returns (int24);

function modifyLiquidities(bytes calldata unlockData, uint256 deadline) external payable;

function permitBatch(address owner, IAllowanceTransfer.PermitBatch calldata _permitBatch, bytes calldata signature)
external
payable
returns (bytes memory err);

function permit(address spender, uint256 tokenId, uint256 deadline, uint256 nonce, bytes calldata signature)
external
payable;
}
}
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ extern crate alloc;
pub mod abi;
pub mod entities;
pub mod error;
pub mod position_manager;
pub mod utils;

pub use uniswap_v3_sdk::multicall;
Expand All @@ -38,5 +39,5 @@ pub use uniswap_v3_sdk::multicall;
mod tests;

pub mod prelude {
pub use crate::{abi::*, entities::*, error::*, multicall::*, utils::*};
pub use crate::{abi::*, entities::*, error::*, multicall::*, position_manager::*, utils::*};
}
254 changes: 254 additions & 0 deletions src/position_manager.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
use crate::prelude::*;
use alloy_primitives::{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},
};

pub use uniswap_v3_sdk::prelude::NFTPermitData;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommonOptions {
/// How much the pool price is allowed to move from the specified action.
pub slippage_tolerance: Percent,
/// When the transaction expires, in epoch seconds.
pub deadline: U256,
/// Optional data to pass to hooks.
pub hook_data: Bytes,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ModifyPositionSpecificOptions {
/// Indicates the ID of the position to increase liquidity for.
pub token_id: U256,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MintSpecificOptions {
/// The account that should receive the minted NFT.
pub recipient: Address,
/// Creates pool if not initialized before mint.
pub create_pool: bool,
/// Initial price to set on the pool if creating.
pub sqrt_price_x96: Option<U160>,
/// Whether the mint is part of a migration from V3 to V4.
pub migrate: bool,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AddLiquiditySpecificOptions {
Mint(MintSpecificOptions),
Increase(ModifyPositionSpecificOptions),
}

/// Options for producing the calldata to add liquidity.
#[derive(Debug, Clone, PartialEq, Deref, DerefMut)]
pub struct AddLiquidityOptions {
#[deref]
#[deref_mut]
pub common_opts: CommonOptions,
/// Whether to spend ether. If true, one of the currencies must be the NATIVE currency.
pub use_native: Option<Ether>,
/// [`MintSpecificOptions`] or [`IncreaseSpecificOptions`]
pub specific_opts: AddLiquiditySpecificOptions,
}

/// Options for producing the calldata to exit a position.
#[derive(Debug, Clone, PartialEq, Eq, Deref, DerefMut)]
pub struct RemoveLiquidityOptions {
#[deref]
#[deref_mut]
pub common_opts: CommonOptions,
/// The ID of the token to exit
pub token_id: U256,
/// The percentage of position liquidity to exit.
pub liquidity_percentage: Percent,
/// Whether the NFT should be burned if the entire position is being exited, by default false.
pub burn_token: bool,
/// The optional permit of the token ID being exited, in case the exit transaction is being
/// sent by an account that does not own the NFT
pub permit: Option<NFTPermitOptions>,
}

#[derive(Debug, Clone, PartialEq, Eq, Deref, DerefMut)]
pub struct CollectOptions {
#[deref]
#[deref_mut]
pub common_opts: CommonOptions,
/// Indicates the ID of the position to collect for.
pub token_id: U256,
/// The account that should receive the tokens.
pub recipient: Address,
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct TransferOptions {
/// The account sending the NFT.
pub sender: Address,
/// The account that should receive the NFT.
pub recipient: Address,
/// The id of the token being sent.
pub token_id: U256,
}

pub type AllowanceTransferPermitSingle = IAllowanceTransfer::PermitSingle;
pub type AllowanceTransferPermitBatch = IAllowanceTransfer::PermitBatch;
pub type NFTPermitValues = IERC721Permit::Permit;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BatchPermitOptions {
pub owner: Address,
pub permit_batch: AllowanceTransferPermitBatch,
pub signature: Bytes,
}

#[derive(Debug, Clone, PartialEq, Eq, Deref, DerefMut)]
pub struct NFTPermitOptions {
#[deref]
#[deref_mut]
pub values: NFTPermitValues,
pub signature: PrimitiveSignature,
}

/// Public methods to encode method parameters for different actions on the PositionManager contract
#[inline]
#[must_use]
pub fn create_call_parameters(pool_key: PoolKey, sqrt_price_x96: U160) -> MethodParameters {
MethodParameters {
calldata: encode_initialize_pool(pool_key, sqrt_price_x96),
value: U256::ZERO,
}
}

#[inline]
pub fn add_call_parameters<TP>(
_position: Position<TP>,
_options: AddLiquidityOptions,
) -> MethodParameters
where
TP: TickDataProvider,
{
unimplemented!("add_call_parameters")
}

#[inline]
fn encode_initialize_pool(pool_key: PoolKey, sqrt_price_x96: U160) -> Bytes {
IPositionManager::initializePoolCall {
key: pool_key,
sqrtPriceX96: sqrt_price_x96,
}
.abi_encode()
.into()
}

#[inline]
pub fn encode_modify_liquidities(unlock_data: Bytes, deadline: U256) -> Bytes {
IPositionManager::modifyLiquiditiesCall {
unlockData: unlock_data,
deadline,
}
.abi_encode()
.into()
}

#[inline]
pub fn encode_permit_batch(
owner: Address,
permit_batch: AllowanceTransferPermitBatch,
signature: Bytes,
) -> Bytes {
IPositionManager::permitBatchCall {
owner,
_permitBatch: permit_batch,
signature,
}
.abi_encode()
.into()
}

#[inline]
pub fn encode_erc721_permit(
spender: Address,
token_id: U256,
deadline: U256,
nonce: U256,
signature: Bytes,
) -> Bytes {
IPositionManager::permitCall {
spender,
tokenId: token_id,
deadline,
nonce,
signature,
}
.abi_encode()
.into()
}

/// Prepares the parameters for EIP712 signing
///
/// ## Arguments
///
/// * `permit`: The permit values to sign
/// * `position_manager`: The address of the position manager contract
/// * `chain_id`: The chain ID
///
/// ## Returns
///
/// The EIP712 domain and values to sign
///
/// ## Examples
///
/// ```
/// use alloy_primitives::{address, b256, uint, PrimitiveSignature, B256};
/// use alloy_signer::SignerSync;
/// use alloy_signer_local::PrivateKeySigner;
/// use alloy_sol_types::SolStruct;
/// use uniswap_v4_sdk::prelude::*;
///
/// let permit = NFTPermitValues {
/// spender: address!("000000000000000000000000000000000000000b"),
/// tokenId: uint!(1_U256),
/// nonce: uint!(1_U256),
/// deadline: uint!(123_U256),
/// };
/// assert_eq!(
/// permit.eip712_type_hash(),
/// b256!("49ecf333e5b8c95c40fdafc95c1ad136e8914a8fb55e9dc8bb01eaa83a2df9ad")
/// );
/// let data: NFTPermitData = get_permit_data(
/// permit,
/// address!("000000000000000000000000000000000000000b"),
/// 1,
/// );
///
/// // Derive the EIP-712 signing hash.
/// let hash: B256 = data.values.eip712_signing_hash(&data.domain);
///
/// let signer = PrivateKeySigner::random();
/// let signature: PrimitiveSignature = signer.sign_hash_sync(&hash).unwrap();
/// assert_eq!(
/// signature.recover_address_from_prehash(&hash).unwrap(),
/// signer.address()
/// );
/// ```
#[inline]
#[must_use]
pub const fn get_permit_data(
permit: NFTPermitValues,
position_manager: Address,
chain_id: u64,
) -> NFTPermitData {
let domain = eip712_domain! {
name: "Uniswap V4 Positions NFT",
chain_id: chain_id,
verifying_contract: position_manager,
};
NFTPermitData {
domain,
values: permit,
}
}

0 comments on commit 9c98c75

Please sign in to comment.