diff --git a/programs/zap/src/error.rs b/programs/zap/src/error.rs index 91bb8a9..27e1fa7 100644 --- a/programs/zap/src/error.rs +++ b/programs/zap/src/error.rs @@ -25,4 +25,10 @@ pub enum ZapError { #[msg("Exceeded slippage tolerance")] ExceededSlippage, + + #[msg("Invalid dlmm zap in parameters")] + InvalidDlmmZapInParameters, + + #[msg("Unsupported fee mode")] + UnsupportedFeeMode, } diff --git a/programs/zap/src/instructions/ix_zap_in_damm_v2.rs b/programs/zap/src/instructions/ix_zap_in_damm_v2.rs index 74e509e..06544f8 100644 --- a/programs/zap/src/instructions/ix_zap_in_damm_v2.rs +++ b/programs/zap/src/instructions/ix_zap_in_damm_v2.rs @@ -6,7 +6,7 @@ use damm_v2::{ }; use crate::{ - damm_v2_ultils::{calculate_swap_amount, get_price_change_bps}, + damm_v2_utils::{calculate_swap_amount, get_price_change_bps}, error::ZapError, new_transfer_fee_calculator, UserLedger, }; @@ -193,17 +193,43 @@ pub fn handle_zap_in_damm_v2( if remaining_amount > 0 { let pool = ctx.accounts.pool.load()?; let current_point = ActivationHandler::get_current_point(pool.activation_type)?; - let swap_amount = calculate_swap_amount( + let swap_result = calculate_swap_amount( &pool, &token_a_transfer_fee_calculator, &token_b_transfer_fee_calculator, remaining_amount, trade_direction, current_point, - )?; - if swap_amount > 0 { - drop(pool); - ctx.accounts.swap(swap_amount, trade_direction)?; + ); + match swap_result { + Ok((swap_in_amount, swap_out_amount)) => { + if swap_in_amount == 0 || swap_out_amount == 0 { + msg!( + "max_deposit_amounts: {} {}, remaining_amounts: {} {}, swap_amounts: {} {}", + max_deposit_a_amount, + max_deposit_b_amount, + ledger.amount_a, + ledger.amount_b, + swap_in_amount, + swap_out_amount + ); + return Ok(()); // no need to swap, just return + } + drop(pool); + ctx.accounts.swap(swap_in_amount, trade_direction)?; + } + Err(err) => { + // if calculation fail, we just skip swap and add liquidity with remaining amount + msg!("Calculate swap amount error: {:?}", err); + msg!( + "max_deposit_amounts: {} {}, remaining_amounts: {} {}", + max_deposit_a_amount, + max_deposit_b_amount, + ledger.amount_a, + ledger.amount_b + ); + return Ok(()); + } } } diff --git a/programs/zap/src/instructions/zap_in_dlmm/ix_zap_in_dlmm_for_initialized_position.rs b/programs/zap/src/instructions/zap_in_dlmm/ix_zap_in_dlmm_for_initialized_position.rs index 2ab9ca7..e9be5c6 100644 --- a/programs/zap/src/instructions/zap_in_dlmm/ix_zap_in_dlmm_for_initialized_position.rs +++ b/programs/zap/src/instructions/zap_in_dlmm/ix_zap_in_dlmm_for_initialized_position.rs @@ -6,7 +6,9 @@ use dlmm::{ types::{AddLiquidityParams, RebalanceLiquidityParams, RemainingAccountsInfo}, }; -use crate::{StrategyType, UnparsedAddLiquidityParams, UserLedger, ZapInRebalancingParams}; +use crate::{ + error::ZapError, StrategyType, UnparsedAddLiquidityParams, UserLedger, ZapInRebalancingParams, +}; #[derive(Accounts)] pub struct ZapInDlmmForInitializedPositionCtx<'info> { @@ -106,6 +108,11 @@ pub fn handle_zap_in_dlmm_for_initialized_position<'c: 'info, 'info>( strategy, }; + require!( + min_delta_id <= max_delta_id, + ZapError::InvalidDlmmZapInParameters + ); + let UnparsedAddLiquidityParams { x0, y0, diff --git a/programs/zap/src/instructions/zap_in_dlmm/ix_zap_in_dlmm_for_uninitialized_position.rs b/programs/zap/src/instructions/zap_in_dlmm/ix_zap_in_dlmm_for_uninitialized_position.rs index be73a2c..ce5593d 100644 --- a/programs/zap/src/instructions/zap_in_dlmm/ix_zap_in_dlmm_for_uninitialized_position.rs +++ b/programs/zap/src/instructions/zap_in_dlmm/ix_zap_in_dlmm_for_uninitialized_position.rs @@ -19,8 +19,8 @@ pub struct ZapInDlmmForUnintializedPositionCtx<'info> { pub lb_pair: AccountLoader<'info, LbPair>, /// user position - /// Check it is different from owner to advoid user to pass owner address wrongly - #[account(mut, constraint = position.key.ne(owner.key))] + /// Check it is different from owner to avoid user to pass owner address wrongly + #[account(mut, constraint = position.key.ne(owner.key) && position.key.ne(rent_payer.key))] pub position: Signer<'info>, /// CHECK: will be validated in dlmm program @@ -150,6 +150,11 @@ pub fn handle_zap_in_dlmm_for_uninitialized_position<'c: 'info, 'info>( strategy, }; + require!( + min_delta_id <= max_delta_id, + ZapError::InvalidDlmmZapInParameters + ); + let UnparsedAddLiquidityParams { x0, y0, diff --git a/programs/zap/src/lib.rs b/programs/zap/src/lib.rs index 1851296..e998a23 100644 --- a/programs/zap/src/lib.rs +++ b/programs/zap/src/lib.rs @@ -12,9 +12,9 @@ pub mod state; #[cfg(test)] pub mod tests; pub use state::*; -pub mod ultils; +pub mod utils; use dlmm::types::RemainingAccountsInfo; -pub use ultils::*; +pub use utils::*; declare_id!("zapvX9M3uf5pvy4wRPAbQgdQsM1xmuiFnkfHKPvwMiz"); #[program] diff --git a/programs/zap/src/math/price_math.rs b/programs/zap/src/math/price_math.rs index a288e7f..3abadf4 100644 --- a/programs/zap/src/math/price_math.rs +++ b/programs/zap/src/math/price_math.rs @@ -1,10 +1,10 @@ use anchor_lang::prelude::*; -use crate::{error::ZapError, safe_math::SafeMath}; +use crate::{constants::MAX_BASIS_POINT, error::ZapError, safe_math::SafeMath}; // Number of bits to scale. This will decide the position of the radix point. const SCALE_OFFSET: u8 = 64; -const BASIS_POINT_MAX: i32 = 10000; + // 1.0000... representation of 64x64 pub const ONE: u128 = 1u128 << SCALE_OFFSET; const MAX_EXPONENTIAL: u32 = 0x80000; // 1048576 @@ -23,13 +23,19 @@ pub fn get_price_base_factor(bin_step: u16) -> Result { // Make bin_step into Q64x64, and divided by BASIS_POINT_MAX. If bin_step = 1, we get 0.0001 in Q64x64 let bps = u128::from(bin_step) .safe_shl(SCALE_OFFSET.into())? - .safe_div(BASIS_POINT_MAX as u128)?; + .safe_div(MAX_BASIS_POINT as u128)?; // Add 1 to bps, we get 1.0001 in Q64.64 let base = ONE.safe_add(bps)?; Ok(base) } pub fn pow(base: u128, exp: i32) -> Option { + // https://doc.rust-lang.org/std/primitive.i32.html#method.abs + // The absolute value of i32::MIN cannot be represented as an i32, and attempting to calculate it will cause an overflow. + if exp == i32::MIN { + return None; + } + // If exponent is negative. We will invert the result later by 1 / base^exp.abs() let mut invert = exp.is_negative(); diff --git a/programs/zap/src/state/user_ledger.rs b/programs/zap/src/state/user_ledger.rs index 6f295ad..ff64336 100644 --- a/programs/zap/src/state/user_ledger.rs +++ b/programs/zap/src/state/user_ledger.rs @@ -1,5 +1,6 @@ use crate::{ - damm_v2_ultils::{get_liquidity_from_amount_a, get_liquidity_from_amount_b}, + damm_v2_utils::{get_liquidity_from_amount_a, get_liquidity_from_amount_b}, + error::ZapError, math::safe_math::SafeMath, TransferFeeCalculator, }; @@ -22,14 +23,17 @@ impl UserLedger { pre_amount_b: u64, post_amount_b: u64, ) -> Result<()> { - self.amount_a = self - .amount_a - .safe_add(post_amount_a)? - .safe_sub(pre_amount_a)?; - self.amount_b = self - .amount_b - .safe_add(post_amount_b)? - .safe_sub(pre_amount_b)?; + self.amount_a = u128::from(self.amount_a) + .safe_add(post_amount_a.into())? + .safe_sub(pre_amount_a.into())? + .try_into() + .map_err(|_| ZapError::MathOverflow)?; + + self.amount_b = u128::from(self.amount_b) + .safe_add(post_amount_b.into())? + .safe_sub(pre_amount_b.into())? + .try_into() + .map_err(|_| ZapError::MathOverflow)?; Ok(()) } // only needed for damm v2 function diff --git a/programs/zap/src/tests/dlmm_rebalancing_tests/bid_ask_strategy_tests.rs b/programs/zap/src/tests/dlmm_rebalancing_tests/bid_ask_strategy_tests.rs index e9e7aa3..0c69324 100644 --- a/programs/zap/src/tests/dlmm_rebalancing_tests/bid_ask_strategy_tests.rs +++ b/programs/zap/src/tests/dlmm_rebalancing_tests/bid_ask_strategy_tests.rs @@ -8,6 +8,33 @@ use crate::{ const STRATEGY: StrategyType = StrategyType::BidAsk; +#[test] +fn test_strategy_only_ask_side_single_bin() { + let active_id = 100; + let bin_step = 100; + let total_amount_x = 100_000_000; + let min_delta_id = 100; + let max_delta_id = 100; + let favor_x_in_active_id = true; + + let params = build_add_liquidity_params( + total_amount_x, + 0, + active_id, + bin_step, + min_delta_id, + max_delta_id, + favor_x_in_active_id, + STRATEGY, + ); + + let amount_in_bins = get_bin_add_liquidity(¶ms, active_id, bin_step).unwrap(); + let amount_in_bin = &amount_in_bins[0]; + + let diff = total_amount_x - amount_in_bin.amount_x; + assert_eq!(diff, 12); +} + #[test] fn test_strategy_only_bid_side_favour_x() { let active_id = 100; diff --git a/programs/zap/src/tests/dlmm_rebalancing_tests/curve_strategy_tests.rs b/programs/zap/src/tests/dlmm_rebalancing_tests/curve_strategy_tests.rs index c337d80..97bddc7 100644 --- a/programs/zap/src/tests/dlmm_rebalancing_tests/curve_strategy_tests.rs +++ b/programs/zap/src/tests/dlmm_rebalancing_tests/curve_strategy_tests.rs @@ -8,6 +8,33 @@ use crate::{ const STRATEGY: StrategyType = StrategyType::Curve; +#[test] +fn test_strategy_only_ask_side_single_bin() { + let active_id = 100; + let bin_step = 100; + let total_amount_x = 100_000_000; + let min_delta_id = 100; + let max_delta_id = 100; + let favor_x_in_active_id = true; + + let params = build_add_liquidity_params( + total_amount_x, + 0, + active_id, + bin_step, + min_delta_id, + max_delta_id, + favor_x_in_active_id, + STRATEGY, + ); + + let amount_in_bins = get_bin_add_liquidity(¶ms, active_id, bin_step).unwrap(); + let amount_in_bin = &amount_in_bins[0]; + + let diff = total_amount_x - amount_in_bin.amount_x; + assert_eq!(diff, 1); +} + #[test] fn test_strategy_only_bid_side_favour_x() { let active_id = 100; diff --git a/programs/zap/src/tests/zap_in_damm_v2_tests.rs b/programs/zap/src/tests/zap_in_damm_v2_tests.rs index 3b9ef9f..55bb831 100644 --- a/programs/zap/src/tests/zap_in_damm_v2_tests.rs +++ b/programs/zap/src/tests/zap_in_damm_v2_tests.rs @@ -28,7 +28,7 @@ fn test_calculate_swap_result() { epoch_transfer_fee: TransferFee::default(), no_transfer_fee_extension: true, }; - let swap_amount = calculate_swap_amount( + let (swap_amount, _swap_out_amount) = calculate_swap_amount( &pool, &transfer_fee_calculator, &transfer_fee_calculator, diff --git a/programs/zap/src/ultils/damm_v2_ultils.rs b/programs/zap/src/utils/damm_v2_utils.rs similarity index 82% rename from programs/zap/src/ultils/damm_v2_ultils.rs rename to programs/zap/src/utils/damm_v2_utils.rs index 10394fe..af79a97 100644 --- a/programs/zap/src/ultils/damm_v2_ultils.rs +++ b/programs/zap/src/utils/damm_v2_utils.rs @@ -1,4 +1,4 @@ -use anchor_lang::prelude::*; +use anchor_lang::{prelude::*, solana_program::log::sol_log_compute_units}; use damm_v2::{ base_fee::{BaseFeeHandler, FeeRateLimiter}, constants::fee::get_max_fee_numerator, @@ -329,23 +329,7 @@ fn get_fee_handler( } } } - _ => { - // otherwise, we just use cliff_fee_numerator - // that is in case the damm v2 update for the new base fee function - let base_fee_numerator = pool.pool_fees.base_fee.cliff_fee_numerator; - let total_fee_numerator = get_total_fee_numerator( - base_fee_numerator, - variable_fee_numerator, - max_fee_numerator, - )?; - Ok(FeeHandler { - rate_limiter_handler: FeeRateLimiter::default(), - variable_fee_numerator, - max_fee_numerator, - total_fee_numerator, - is_rate_limiter: false, - }) - } + _ => Err(ZapError::UnsupportedFeeMode.into()), } } @@ -373,10 +357,11 @@ pub fn calculate_swap_amount( remaining_amount: u64, trade_direction: TradeDirection, current_point: u64, -) -> Result { +) -> Result<(u64, u64)> { let mut max_swap_amount = remaining_amount; let mut min_swap_amount = 0; - let mut swap_amount = 0; + let mut swap_in_amount = 0; + let mut swap_out_amount = 0; let fee_handler = get_fee_handler(pool, current_point, trade_direction)?; @@ -385,12 +370,17 @@ pub fn calculate_swap_amount( let (pool_amount_a, pool_amount_b) = pool.get_reserves_amount()?; // max 20 loops - // For each loop program consumed ~ 5.19 CUs - // So the 20 loops will consume maximum ~ 100 CUs + // For each loop program consumed ~ 5394.3 -> 5,395 CUs + // So the 20 loops will consume maximum ~ 107,900 CUs for _i in 0..20 { - let amount_in = max_swap_amount.safe_add(min_swap_amount)?.safe_div(2)?; + let delta_half = max_swap_amount.safe_sub(min_swap_amount)? >> 1; + let amount_in = min_swap_amount.safe_add(delta_half)?; - if let Ok(swap_result) = calculate_swap_result( + if amount_in == swap_in_amount { + break; + } + + let swap_result = calculate_swap_result( pool, token_a_transfer_fee_calculator, token_b_transfer_fee_calculator, @@ -399,58 +389,50 @@ pub fn calculate_swap_amount( trade_direction, &fee_handler, &fee_mode, - ) { - // update swap amount - swap_amount = amount_in; - if let Ok(status) = validate_swap_result( - &swap_result, - token_a_transfer_fee_calculator, - token_b_transfer_fee_calculator, - remaining_amount, - pool_amount_a, - pool_amount_b, - trade_direction, - ) { - match status { - SwapResultStatus::Done => { - #[cfg(test)] - println!("Done calculate swap result {}", _i); - break; - } - SwapResultStatus::ExceededA => { - if trade_direction == TradeDirection::AtoB { - // need to increase swap amount - min_swap_amount = swap_amount; - } else { - // need to decrease swap amount - max_swap_amount = swap_amount; - } - } - SwapResultStatus::ExceededB => { - if trade_direction == TradeDirection::AtoB { - // need to decrease swap amount - max_swap_amount = swap_amount; - } else { - // need to increase swap amount - min_swap_amount = swap_amount; - } - } - } - } else { - #[cfg(test)] - println!("can't validate swap result {}", _i); + )?; - break; // if we can't validate swap result, then just break - } - } else { - #[cfg(test)] - println!("can't simulate swap result {}", _i); + // update swap amount + swap_in_amount = amount_in; + swap_out_amount = swap_result.user_amount_in; - break; // if we can't simulate swap result, then just break + let status = validate_swap_result( + &swap_result, + token_a_transfer_fee_calculator, + token_b_transfer_fee_calculator, + remaining_amount, + pool_amount_a, + pool_amount_b, + trade_direction, + )?; + + match status { + SwapResultStatus::Done => { + #[cfg(test)] + println!("Done calculate swap result {}", _i); + break; + } + SwapResultStatus::ExceededA => { + if trade_direction == TradeDirection::AtoB { + // need to increase swap amount + min_swap_amount = swap_in_amount; + } else { + // need to decrease swap amount + max_swap_amount = swap_in_amount; + } + } + SwapResultStatus::ExceededB => { + if trade_direction == TradeDirection::AtoB { + // need to decrease swap amount + max_swap_amount = swap_in_amount; + } else { + // need to increase swap amount + min_swap_amount = swap_in_amount; + } + } } } - Ok(swap_amount) + Ok((swap_in_amount, swap_out_amount)) } // Δa = L * (1 / √P_lower - 1 / √P_upper) => L = Δa / (1 / √P_lower - 1 / √P_upper) @@ -489,7 +471,7 @@ pub fn get_price_change_bps(pre_sqrt_price: u128, post_sqrt_price: u128) -> Resu let price_diff_prod = U192::from(price_diff).safe_mul(U192::from(MAX_BASIS_POINT))?; - let price_diff_bps = price_diff_prod.safe_div(U192::from(pre_sqrt_price))?; + let price_diff_bps = price_diff_prod.div_ceil(U192::from(pre_sqrt_price)); Ok(price_diff_bps .try_into() .map_err(|_| ZapError::TypeCastFailed)?) diff --git a/programs/zap/src/ultils/dlmm_utils.rs b/programs/zap/src/utils/dlmm_utils.rs similarity index 89% rename from programs/zap/src/ultils/dlmm_utils.rs rename to programs/zap/src/utils/dlmm_utils.rs index e215652..9df0718 100644 --- a/programs/zap/src/ultils/dlmm_utils.rs +++ b/programs/zap/src/utils/dlmm_utils.rs @@ -3,7 +3,7 @@ use anchor_lang::prelude::*; use std::ops::Neg; use damm_v2::safe_math::SafeMath; -use ruint::aliases::U256; +use ruint::aliases::{U256, U512}; use crate::{error::ZapError, price_math::get_price_from_id}; #[derive(AnchorSerialize, AnchorDeserialize, Eq, PartialEq, Clone, Debug)] @@ -251,7 +251,7 @@ impl StrategyHandler for CurveHandler { // sum(amounts) = y0 * (m1-m2+1) - y0 * (m1 * (m1+1)/2 - m2 * (m2-1)/2) / m1 // A = (m1-m2+1) - (m1 * (m1+1)/2 - m2 * (m2-1)/2) / m1 // y0 = sum(amounts) / A - // advoid precision loss: + // avoid precision loss: // y0 = sum(amounts) * m1 / ((m1-m2+1) * m1 - (m1 * (m1+1)/2 - m2 * (m2-1)/2)) // noted: y0 > 0 and delta_y < 0 in curve strategy @@ -267,11 +267,11 @@ impl StrategyHandler for CurveHandler { let a = (m1 - m2 + 1) * m1 - (m1 * (m1 + 1) / 2 - m2 * (m2 - 1) / 2); let y0 = i128::from(amount_y) * m1 / a; // we round down delta_y firstly - // m1 can't be zero becase we've checked for min_delta_id <= max_delta_id, and both delta id is smaller than or equa 0 + // m1 can't be zero because we've checked for min_delta_id <= max_delta_id, and both delta id is smaller than or equal 0 let delta_y = -(y0 / m1); // then we update y0 to ensure the first amount (active_id - m1 = y0 + delta_y * m1) > 0 - // delta_y is negative and round up, while y0 is possitive and round down + // delta_y is negative and round up, while y0 is positive and round down // it will ensure sum(amounts) <= amount_y // sum(amounts) = y0 * (m1-m2+1) + delta_y * (m1 * (m1+1)/2 - m2 * (m2-1)/2) // sum(amounts) = -(delta_y * m1) * (m1-m2+1) + delta_y * (m1 * (m1+1)/2 - m2 * (m2-1)/2) @@ -304,30 +304,41 @@ impl StrategyHandler for CurveHandler { // B = (p(m1)+..+p(m2)) // C = (m1 * p(m1) + ... + m2 * p(m2)) / m2 // x0 = sum(amounts) / (B-C) - // noted: x0 > 0 and delta_x < 0 in curve strategy + // note: x0 >= 0 and delta_x <= 0 in curve strategy + + if min_delta_id == max_delta_id { + let bin_id = active_id.safe_add(min_delta_id)?; + let pm = U256::from(get_price_from_id(bin_id, bin_step)?); + let x0 = U256::from(amount_x).safe_mul(pm)?.safe_shr(64)?; + let x0: i128 = x0.try_into().map_err(|_| ZapError::TypeCastFailed)?; + return Ok((x0, 0)); + } let mut b = U256::ZERO; - let mut c = U256::ZERO; + let m1 = min_delta_id; let m2 = max_delta_id; + let mut c_numerator = U256::ZERO; + for m in m1..=m2 { let bin_id = active_id.safe_add(m)?; let pm = U256::from(get_price_from_id(bin_id.neg(), bin_step)?); b = b.safe_add(pm)?; - let c_delta = U256::from(m).safe_mul(pm)?.safe_div(U256::from(m2))?; - - c = c.safe_add(c_delta)?; + c_numerator = c_numerator.safe_add(U256::from(m).safe_mul(pm)?)?; } + let c = c_numerator.safe_div(U256::from(m2))?; + let x0 = U256::from(amount_x) .safe_shl(64)? .safe_div(b.safe_sub(c)?)?; let x0: i128 = x0.try_into().map_err(|_| ZapError::TypeCastFailed)?; let m2: i128 = max_delta_id.into(); - let delta_x = if m2 != 0 { -x0 / m2 } else { 0 }; + // note: m2 impossible be zero because max_delta_id > min_delta_id >= 0 + let delta_x = -x0 / m2; // same handle as get y0, delta_y let x0 = -(delta_x * m2); @@ -392,8 +403,17 @@ impl StrategyHandler for BidAskHandler { // A = -m1 * (p(m1)+..+p(m2)) + (m1 * p(m1) + ... + m2 * p(m2)) // B = m1 * (p(m1)+..+p(m2)) // C = (m1 * p(m1) + ... + m2 * p(m2)) - // x0 = sum(amounts) / (C-B) - // note: in bid ask strategy: x0 < 0 and delta_x > 0 + // delta_x = sum(amounts) / (C-B) + // note: in bid ask strategy: x0 <= 0 and delta_x >= 0 + + if min_delta_id == max_delta_id { + let bin_id = active_id.safe_add(min_delta_id)?; + let pm = get_price_from_id(bin_id.neg(), bin_step)?; + let denominator = U256::from(min_delta_id).safe_mul(U256::from(pm))?; + let delta_x = U256::from(amount_x).safe_shl(64)?.safe_div(denominator)?; + let delta_x: i128 = delta_x.try_into().map_err(|_| ZapError::TypeCastFailed)?; + return Ok((0, delta_x)); + } let mut b = U256::ZERO; let mut c = U256::ZERO; diff --git a/programs/zap/src/ultils/mod.rs b/programs/zap/src/utils/mod.rs similarity index 59% rename from programs/zap/src/ultils/mod.rs rename to programs/zap/src/utils/mod.rs index 6d01f7d..5641e17 100644 --- a/programs/zap/src/ultils/mod.rs +++ b/programs/zap/src/utils/mod.rs @@ -1,5 +1,5 @@ -pub mod damm_v2_ultils; -pub use damm_v2_ultils::*; +pub mod damm_v2_utils; +pub use damm_v2_utils::*; pub mod dlmm_utils; pub use dlmm_utils::*; pub mod token; diff --git a/programs/zap/src/ultils/token.rs b/programs/zap/src/utils/token.rs similarity index 100% rename from programs/zap/src/ultils/token.rs rename to programs/zap/src/utils/token.rs