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: add PositionManager module and batch permit support #17

Merged
merged 2 commits into from
Dec 15, 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
4 changes: 3 additions & 1 deletion 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 @@ -21,6 +21,8 @@ uniswap-sdk-core = "3.2.0"
uniswap-v3-sdk = "2.9.0"

[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;
shuhuiluo marked this conversation as resolved.
Show resolved Hide resolved
}

/// @notice The permit message signed for a single token allowance
#[derive(Debug, Default, PartialEq, Eq)]
struct PermitSingle {
// the permit data for a single token alownce
shuhuiluo marked this conversation as resolved.
Show resolved Hide resolved
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);
shuhuiluo marked this conversation as resolved.
Show resolved Hide resolved

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

#[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,
}
}
Loading