use crate::types::TransactionRequest;
use crate::{client::Web3, jsonrpc::error::Web3Error, types::SendTxOption};
use clarity::utils::display_uint256_as_address;
use clarity::{
abi::{encode_call, encode_tokens, AbiToken},
constants::tt256m1,
Address, Int256, PrivateKey, Uint256,
};
use std::time::Duration;
use tokio::time::timeout as future_timeout;
use super::uniswapv3::{options_contains_glm, DEFAULT_GAS_LIMIT_MULT};
pub const MIN_TICK: i32 = -887272;
pub const MAX_TICK: i32 = 887272;
pub const MIN_TICK_SPACING: i32 = 1;
pub const MAX_TICK_SPACING: i32 = 16383;
lazy_static! {
pub static ref UNISWAP_V4_POOL_MANAGER_ADDRESS: Address =
Address::parse_and_validate("0x000000000004444c5dc75cB358380D2e3dE08A90").unwrap();
pub static ref UNISWAP_V4_UNIVERSAL_ROUTER_ADDRESS: Address =
Address::parse_and_validate("0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af").unwrap();
pub static ref UNISWAP_V4_QUOTER_ADDRESS: Address =
Address::parse_and_validate("0x52F0E24D1c21C8A0cB1e5a5dD6198556BD9E1203").unwrap();
pub static ref UNISWAP_V4_POSITION_MANAGER_ADDRESS: Address =
Address::parse_and_validate("0xbD216513d74C8cf14cf4747E6AaA6420FF64ee9e").unwrap();
pub static ref UNISWAP_V4_STATE_VIEW_ADDRESS: Address =
Address::parse_and_validate("0x7fFE42C4a5DEeA5b0feC41C94C136Cf115597227").unwrap();
pub static ref PERMIT2_ADDRESS: Address =
Address::parse_and_validate("0x000000000022D473030F116dDEE9F6B43aC78BA3").unwrap();
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UniversalRouterCommand {
V4Swap = 0x10,
Permit2Permit = 0x0a,
WrapEth = 0x0b,
UnwrapWeth = 0x0c,
Sweep = 0x04,
PayPortion = 0x06,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum V4RouterAction {
IncreaseLiquidity = 0x00,
DecreaseLiquidity = 0x01,
MintPosition = 0x02,
BurnPosition = 0x03,
IncreaseLiquidityFromDeltas = 0x04,
MintPositionFromDeltas = 0x05,
SwapExactInSingle = 0x06,
SwapExactIn = 0x07,
SwapExactOutSingle = 0x08,
SwapExactOut = 0x09,
Donate = 0x0a,
Settle = 0x0b,
SettleAll = 0x0c,
SettlePair = 0x0d,
Take = 0x0e,
TakeAll = 0x0f,
TakePortion = 0x10,
TakePair = 0x11,
CloseCurrency = 0x12,
ClearOrTake = 0x13,
Sweep = 0x14,
Wrap = 0x15,
Unwrap = 0x16,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PoolKey {
pub currency0: Address,
pub currency1: Address,
pub fee: Uint256,
pub tick_spacing: i32,
pub hooks: Address,
}
impl PoolKey {
pub fn new(
token_a: Address,
token_b: Address,
fee: Uint256,
tick_spacing: i32,
hooks: Address,
) -> Self {
let (currency0, currency1) = if token_a < token_b {
(token_a, token_b)
} else {
(token_b, token_a)
};
Self {
currency0,
currency1,
fee,
tick_spacing,
hooks,
}
}
pub fn try_new(
token_a: Address,
token_b: Address,
fee: Uint256,
tick_spacing: i32,
hooks: Address,
) -> Result<Self, UniswapV4Error> {
if token_a == token_b {
return Err(UniswapV4Error::IdenticalCurrencies);
}
let fee_u32: u32 = fee.to_string().parse().unwrap_or(u32::MAX);
if fee_u32 != DYNAMIC_FEE_FLAG && fee_u32 > MAX_LP_FEE {
return Err(UniswapV4Error::InvalidFee(fee_u32));
}
if !(MIN_TICK_SPACING..=MAX_TICK_SPACING).contains(&tick_spacing) {
return Err(UniswapV4Error::InvalidTickSpacing(tick_spacing));
}
let (currency0, currency1) = if token_a < token_b {
(token_a, token_b)
} else {
(token_b, token_a)
};
Ok(Self {
currency0,
currency1,
fee,
tick_spacing,
hooks,
})
}
pub fn standard(token_a: Address, token_b: Address, fee: Uint256, tick_spacing: i32) -> Self {
Self::new(token_a, token_b, fee, tick_spacing, Address::default())
}
pub fn try_standard(
token_a: Address,
token_b: Address,
fee: Uint256,
tick_spacing: i32,
) -> Result<Self, UniswapV4Error> {
Self::try_new(token_a, token_b, fee, tick_spacing, Address::default())
}
pub fn is_zero_for_one(&self, token_in: Address) -> bool {
token_in == self.currency0
}
pub fn is_dynamic_fee(&self) -> bool {
let fee_u32: u32 = self.fee.to_string().parse().unwrap_or(0);
fee_u32 == DYNAMIC_FEE_FLAG
}
pub fn dynamic_fee(
token_a: Address,
token_b: Address,
tick_spacing: i32,
hooks: Address,
) -> Self {
Self::new(
token_a,
token_b,
DYNAMIC_FEE_FLAG.into(),
tick_spacing,
hooks,
)
}
pub fn get_fee_pips(&self) -> u32 {
if self.is_dynamic_fee() {
0
} else {
self.fee.to_string().parse().unwrap_or(0)
}
}
pub fn is_valid_fee(&self) -> bool {
let fee_u32: u32 = self.fee.to_string().parse().unwrap_or(u32::MAX);
fee_u32 == DYNAMIC_FEE_FLAG || fee_u32 <= MAX_LP_FEE
}
pub fn is_valid_tick_spacing(&self) -> bool {
self.tick_spacing >= MIN_TICK_SPACING && self.tick_spacing <= MAX_TICK_SPACING
}
pub fn validate_standard_tick_spacing(&self) -> Result<(), UniswapV4Error> {
if self.is_dynamic_fee() {
return Ok(());
}
let fee_u32 = self.get_fee_pips();
let expected = match fee_u32 {
100 => Some(tick_spacings::FEE_100),
500 => Some(tick_spacings::FEE_500),
3000 => Some(tick_spacings::FEE_3000),
10000 => Some(tick_spacings::FEE_10000),
_ => None, };
if let Some(expected_spacing) = expected {
if self.tick_spacing != expected_spacing {
return Err(UniswapV4Error::TickSpacingFeeMismatch {
fee: fee_u32,
tick_spacing: self.tick_spacing,
expected: expected_spacing,
});
}
}
Ok(())
}
pub fn validate(&self) -> Result<(), UniswapV4Error> {
if self.currency0 == self.currency1 {
return Err(UniswapV4Error::IdenticalCurrencies);
}
if !self.is_valid_fee() {
let fee_u32: u32 = self.fee.to_string().parse().unwrap_or(u32::MAX);
return Err(UniswapV4Error::InvalidFee(fee_u32));
}
if !self.is_valid_tick_spacing() {
return Err(UniswapV4Error::InvalidTickSpacing(self.tick_spacing));
}
self.validate_standard_tick_spacing()?;
Ok(())
}
pub fn to_abi_token(&self) -> AbiToken {
let tick_spacing_i256 = Int256::from(self.tick_spacing);
AbiToken::Struct(vec![
AbiToken::Address(self.currency0),
AbiToken::Address(self.currency1),
AbiToken::Uint(self.fee), AbiToken::Int(tick_spacing_i256), AbiToken::Address(self.hooks),
])
}
}
pub const DYNAMIC_FEE_FLAG: u32 = 0x800000;
pub const MAX_LP_FEE: u32 = 1_000_000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UniswapV4Error {
InvalidFee(u32),
InvalidTickSpacing(i32),
TickSpacingFeeMismatch {
fee: u32,
tick_spacing: i32,
expected: i32,
},
InvalidHookAddress(Address),
DeadlineInPast {
deadline: Uint256,
current_time: Uint256,
},
PoolNotFound,
IdenticalCurrencies,
}
impl std::fmt::Display for UniswapV4Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UniswapV4Error::InvalidFee(fee) => {
write!(f, "Invalid fee: {} exceeds maximum {}", fee, MAX_LP_FEE)
}
UniswapV4Error::InvalidTickSpacing(ts) => {
write!(
f,
"Invalid tick spacing: {} (must be between {} and {})",
ts, MIN_TICK_SPACING, MAX_TICK_SPACING
)
}
UniswapV4Error::TickSpacingFeeMismatch {
fee,
tick_spacing,
expected,
} => {
write!(
f,
"Tick spacing {} does not match expected {} for fee tier {}",
tick_spacing, expected, fee
)
}
UniswapV4Error::InvalidHookAddress(addr) => {
write!(f, "Invalid hook address: {}", addr)
}
UniswapV4Error::DeadlineInPast {
deadline,
current_time,
} => {
write!(
f,
"Deadline {} is in the past (current time: {})",
deadline, current_time
)
}
UniswapV4Error::PoolNotFound => {
write!(f, "Pool does not exist or has no liquidity")
}
UniswapV4Error::IdenticalCurrencies => {
write!(f, "Currency addresses cannot be identical")
}
}
}
}
impl std::error::Error for UniswapV4Error {}
#[derive(Debug, Clone)]
pub struct ExactInputSingleParams {
pub pool_key: PoolKey,
pub zero_for_one: bool,
pub amount_in: Uint256,
pub amount_out_minimum: Uint256,
pub hook_data: Vec<u8>,
}
impl ExactInputSingleParams {
pub fn to_abi_token(&self) -> AbiToken {
AbiToken::Struct(vec![
self.pool_key.to_abi_token(),
AbiToken::Bool(self.zero_for_one),
AbiToken::Uint(self.amount_in),
AbiToken::Uint(self.amount_out_minimum),
AbiToken::UnboundedBytes(self.hook_data.clone()),
])
}
}
#[derive(Debug, Clone)]
pub struct ExactOutputSingleParams {
pub pool_key: PoolKey,
pub zero_for_one: bool,
pub amount_out: Uint256,
pub amount_in_maximum: Uint256,
pub hook_data: Vec<u8>,
}
impl ExactOutputSingleParams {
pub fn to_abi_token(&self) -> AbiToken {
AbiToken::Struct(vec![
self.pool_key.to_abi_token(),
AbiToken::Bool(self.zero_for_one),
AbiToken::Uint(self.amount_out),
AbiToken::Uint(self.amount_in_maximum),
AbiToken::UnboundedBytes(self.hook_data.clone()),
])
}
}
impl Web3 {
#[allow(clippy::too_many_arguments)]
pub async fn get_uniswap_v4_quote(
&self,
caller_address: Address,
pool_key: &PoolKey,
zero_for_one: bool,
amount_in: Uint256,
sqrt_price_limit_x96: Option<Uint256>,
quoter: Option<Address>,
) -> Result<Uint256, Web3Error> {
let quoter = quoter.unwrap_or(*UNISWAP_V4_QUOTER_ADDRESS);
let sqrt_price_limit = sqrt_price_limit_x96.unwrap_or_default();
let params = AbiToken::Struct(vec![
pool_key.to_abi_token(),
AbiToken::Bool(zero_for_one),
AbiToken::Uint(amount_in),
AbiToken::Uint(sqrt_price_limit),
AbiToken::Bytes(vec![]), ]);
let payload = encode_call("quoteExactInputSingle((((address,address,uint24,int24,address),bool,uint128,uint160,bytes)))", &[params])?;
let result = self
.simulate_transaction(
TransactionRequest::quick_tx(caller_address, quoter, payload),
vec![],
None,
)
.await?;
if result.len() < 32 {
return Err(Web3Error::BadResponse(
"Invalid quote response from V4 Quoter".to_string(),
));
}
let amount_out = Uint256::from_be_bytes(&result[0..32]);
Ok(amount_out)
}
#[allow(clippy::too_many_arguments)]
pub async fn swap_uniswap_v4(
&self,
eth_private_key: PrivateKey,
pool_key: &PoolKey,
token_in: Address,
amount_in: Uint256,
amount_out_min: Uint256,
deadline: Option<Uint256>,
universal_router: Option<Address>,
options: Option<Vec<SendTxOption>>,
wait_timeout: Option<Duration>,
) -> Result<Uint256, Web3Error> {
let router = universal_router.unwrap_or(*UNISWAP_V4_UNIVERSAL_ROUTER_ADDRESS);
let eth_address = eth_private_key.to_address();
let zero_for_one = pool_key.is_zero_for_one(token_in);
let current_block = self.eth_get_latest_block().await?;
let current_time = current_block.timestamp;
let deadline = match deadline {
None => current_time + (10u64 * 60u64).into(),
Some(val) => {
if val <= current_time {
return Err(Web3Error::BadInput(format!(
"Deadline {} is in the past (current time: {})",
val, current_time
)));
}
val
}
};
let commands = vec![UniversalRouterCommand::V4Swap as u8];
let actions = vec![
V4RouterAction::SwapExactInSingle as u8,
V4RouterAction::SettleAll as u8,
V4RouterAction::TakeAll as u8,
];
let swap_params = ExactInputSingleParams {
pool_key: pool_key.clone(),
zero_for_one,
amount_in,
amount_out_minimum: amount_out_min,
hook_data: vec![],
};
let (currency_in, currency_out) = if zero_for_one {
(pool_key.currency0, pool_key.currency1)
} else {
(pool_key.currency1, pool_key.currency0)
};
let swap_param_bytes = encode_tokens(&[swap_params.to_abi_token()]);
let settle_param_bytes =
encode_tokens(&[AbiToken::Address(currency_in), AbiToken::Uint(amount_in)]);
let take_param_bytes = encode_tokens(&[
AbiToken::Address(currency_out),
AbiToken::Uint(amount_out_min),
]);
let v4_input = encode_tokens(&[
AbiToken::UnboundedBytes(actions),
AbiToken::Dynamic(vec![
AbiToken::UnboundedBytes(swap_param_bytes),
AbiToken::UnboundedBytes(settle_param_bytes),
AbiToken::UnboundedBytes(take_param_bytes),
]),
]);
let payload = encode_call(
"execute(bytes,bytes[],uint256)",
&[
AbiToken::UnboundedBytes(commands),
AbiToken::Dynamic(vec![AbiToken::UnboundedBytes(v4_input)]),
AbiToken::Uint(deadline),
],
)?;
let mut options = options.unwrap_or_default();
if !options_contains_glm(&options) {
options.push(SendTxOption::GasLimitMultiplier(DEFAULT_GAS_LIMIT_MULT));
}
if token_in != Address::default() {
let permit2_allowance = self
.get_erc20_allowance(token_in, eth_address, *PERMIT2_ADDRESS, options.clone())
.await?;
if permit2_allowance < amount_in {
debug!("Approving Permit2 for token_in");
let nonce = self.eth_get_transaction_count(eth_address).await?;
let _approval = self
.erc20_approve(
token_in,
tt256m1(), eth_private_key,
*PERMIT2_ADDRESS,
wait_timeout,
options.clone(),
)
.await?;
if wait_timeout.is_none() {
options.push(SendTxOption::Nonce(nonce + 1u8.into()));
}
}
let router_allowance = self
.get_permit2_allowance(token_in, eth_address, router)
.await?;
if router_allowance < amount_in {
debug!("Approving Universal Router via Permit2");
let nonce = self.eth_get_transaction_count(eth_address).await?;
let _approval = self
.permit2_approve(
token_in,
router,
amount_in,
eth_private_key,
wait_timeout,
options.clone(),
)
.await?;
if wait_timeout.is_none() {
options.push(SendTxOption::Nonce(nonce + 1u8.into()));
}
}
}
let value = if token_in == Address::default() {
amount_in
} else {
Uint256::from(0u8)
};
trace!("V4 swap payload: {:?}", payload);
let tx = self
.prepare_transaction(router, payload, value, eth_private_key, options)
.await?;
let txid = self.eth_send_raw_transaction(tx.to_bytes()).await?;
debug!(
"txid for uniswap v4 swap is {}",
display_uint256_as_address(txid)
);
if let Some(timeout) = wait_timeout {
future_timeout(timeout, self.wait_for_transaction(txid, timeout, None)).await??;
}
Ok(txid)
}
#[allow(clippy::too_many_arguments)]
pub async fn swap_uniswap_v4_with_slippage(
&self,
eth_private_key: PrivateKey,
pool_key: &PoolKey,
token_in: Address,
amount_in: Uint256,
slippage_bps: Option<u32>,
deadline: Option<Uint256>,
universal_router: Option<Address>,
options: Option<Vec<SendTxOption>>,
wait_timeout: Option<Duration>,
) -> Result<Uint256, Web3Error> {
let slippage_bps = slippage_bps.unwrap_or(50);
let caller_address = eth_private_key.to_address();
let zero_for_one = pool_key.is_zero_for_one(token_in);
let quote = self
.get_uniswap_v4_quote(
caller_address,
pool_key,
zero_for_one,
amount_in,
None,
None,
)
.await?;
let basis_points_denom: Uint256 = 10000u32.into();
let slippage_factor: Uint256 = (10000u32 - slippage_bps).into();
let amount_out_min = (quote * slippage_factor) / basis_points_denom;
self.swap_uniswap_v4(
eth_private_key,
pool_key,
token_in,
amount_in,
amount_out_min,
deadline,
universal_router,
options,
wait_timeout,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn swap_uniswap_v4_eth_in(
&self,
eth_private_key: PrivateKey,
token_out: Address,
fee: Uint256,
tick_spacing: i32,
amount_in: Uint256,
amount_out_min: Uint256,
deadline: Option<Uint256>,
options: Option<Vec<SendTxOption>>,
wait_timeout: Option<Duration>,
) -> Result<Uint256, Web3Error> {
let pool_key = PoolKey::standard(Address::default(), token_out, fee, tick_spacing);
self.swap_uniswap_v4(
eth_private_key,
&pool_key,
Address::default(), amount_in,
amount_out_min,
deadline,
None,
options,
wait_timeout,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn swap_uniswap_v4_eth_in_with_slippage(
&self,
eth_private_key: PrivateKey,
token_out: Address,
fee: Uint256,
tick_spacing: i32,
amount_in: Uint256,
slippage_bps: Option<u32>,
deadline: Option<Uint256>,
options: Option<Vec<SendTxOption>>,
wait_timeout: Option<Duration>,
) -> Result<Uint256, Web3Error> {
let pool_key = PoolKey::standard(Address::default(), token_out, fee, tick_spacing);
self.swap_uniswap_v4_with_slippage(
eth_private_key,
&pool_key,
Address::default(), amount_in,
slippage_bps,
deadline,
None,
options,
wait_timeout,
)
.await
}
pub async fn get_permit2_allowance(
&self,
token: Address,
owner: Address,
spender: Address,
) -> Result<Uint256, Web3Error> {
let payload = encode_call(
"allowance(address,address,address)",
&[
AbiToken::Address(owner),
AbiToken::Address(token),
AbiToken::Address(spender),
],
)?;
let result = self
.simulate_transaction(
TransactionRequest::quick_tx(owner, *PERMIT2_ADDRESS, payload),
vec![],
None,
)
.await?;
if result.len() < 32 {
return Ok(Uint256::from(0u8));
}
let amount = Uint256::from_be_bytes(&result[12..32]); Ok(amount)
}
pub async fn permit2_approve(
&self,
token: Address,
spender: Address,
amount: Uint256,
eth_private_key: PrivateKey,
wait_timeout: Option<Duration>,
options: Vec<SendTxOption>,
) -> Result<Uint256, Web3Error> {
let expiration: Uint256 = Uint256::from(u64::MAX);
let payload = encode_call(
"approve(address,address,uint160,uint48)",
&[
AbiToken::Address(token),
AbiToken::Address(spender),
AbiToken::Uint(amount),
AbiToken::Uint(expiration),
],
)?;
let tx = self
.prepare_transaction(
*PERMIT2_ADDRESS,
payload,
0u8.into(),
eth_private_key,
options,
)
.await?;
let txid = self.eth_send_raw_transaction(tx.to_bytes()).await?;
if let Some(timeout) = wait_timeout {
future_timeout(timeout, self.wait_for_transaction(txid, timeout, None)).await??;
}
Ok(txid)
}
pub async fn get_uniswap_v4_pool_state(
&self,
caller_address: Address,
pool_key: &PoolKey,
state_view: Option<Address>,
) -> Result<(Uint256, i32), Web3Error> {
let state_view = state_view.unwrap_or(*UNISWAP_V4_STATE_VIEW_ADDRESS);
let payload = encode_call(
"getSlot0((address,address,uint24,int24,address))",
&[pool_key.to_abi_token()],
)?;
let result = self
.simulate_transaction(
TransactionRequest::quick_tx(caller_address, state_view, payload),
vec![],
None,
)
.await?;
if result.len() < 64 {
return Err(Web3Error::BadResponse(
"Invalid state response from V4 StateView".to_string(),
));
}
let sqrt_price = Uint256::from_be_bytes(&result[12..32]);
let tick_bytes = &result[32..64];
let raw_tick_bytes = [tick_bytes[29], tick_bytes[30], tick_bytes[31]];
let tick = sign_extend_i24_to_i32(raw_tick_bytes);
Ok((sqrt_price, tick))
}
pub async fn uniswap_v4_pool_exists(
&self,
caller_address: Address,
pool_key: &PoolKey,
state_view: Option<Address>,
) -> Result<bool, Web3Error> {
match self
.get_uniswap_v4_pool_state(caller_address, pool_key, state_view)
.await
{
Ok((sqrt_price, _)) => {
Ok(sqrt_price > Uint256::from(0u8))
}
Err(_) => Ok(false),
}
}
}
fn sign_extend_i24_to_i32(bytes: [u8; 3]) -> i32 {
let is_negative = bytes[0] & 0x80 != 0;
if is_negative {
i32::from_be_bytes([0xFF, bytes[0], bytes[1], bytes[2]])
} else {
i32::from_be_bytes([0x00, bytes[0], bytes[1], bytes[2]])
}
}
pub mod tick_spacings {
pub const FEE_100: i32 = 1;
pub const FEE_500: i32 = 10;
pub const FEE_3000: i32 = 60;
pub const FEE_10000: i32 = 200;
pub fn for_fee(fee_pips: u32) -> Option<i32> {
match fee_pips {
100 => Some(FEE_100),
500 => Some(FEE_500),
3000 => Some(FEE_3000),
10000 => Some(FEE_10000),
_ => None,
}
}
pub fn is_valid_for_fee(fee_pips: u32, tick_spacing: i32) -> bool {
match for_fee(fee_pips) {
Some(expected) => tick_spacing == expected,
None => (super::MIN_TICK_SPACING..=super::MAX_TICK_SPACING).contains(&tick_spacing),
}
}
}