From c08e3886e18ebf5d1f2afe70e1b7c85575e63973 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Tue, 23 Apr 2024 14:03:03 +0700 Subject: [PATCH] init --- Cargo.lock | 1 + programs/lb_clmm/Cargo.toml | 1 + programs/lb_clmm/src/constants.rs | 228 +- programs/lb_clmm/src/errors.rs | 30 + programs/lb_clmm/src/events.rs | 38 + .../lb_clmm/src/instructions/add_liquidity.rs | 99 - .../add_liquidity_by_strategy_one_side.rs | 70 - .../add_liquidity_by_weight_one_side.rs | 146 -- .../lb_clmm/src/instructions/claim_fee.rs | 90 +- .../lb_clmm/src/instructions/claim_reward.rs | 86 +- .../src/instructions/close_position.rs | 21 +- .../instructions/decrease_position_length.rs | 29 + .../src/instructions/deposit/add_liquidity.rs | 562 +++++ .../add_liquidity_by_strategy.rs | 213 +- .../add_liquidity_by_strategy_one_side.rs | 204 ++ .../{ => deposit}/add_liquidity_by_weight.rs | 88 +- .../add_liquidity_by_weight_one_side.rs | 382 +++ .../add_liquidity_single_side_precise.rs | 26 + .../lb_clmm/src/instructions/deposit/mod.rs | 6 + .../lb_clmm/src/instructions/fund_reward.rs | 45 +- .../lb_clmm/src/instructions/go_to_a_bin.rs | 31 + .../instructions/increase_oracle_length.rs | 5 +- .../instructions/increase_position_length.rs | 37 + .../src/instructions/initialize_bin_array.rs | 2 + .../src/instructions/initialize_lb_pair.rs | 79 + .../initialize_permission_lb_pair.rs | 75 + .../src/instructions/initialize_position.rs | 31 +- .../initialize_position_by_operator.rs | 30 +- .../instructions/initialize_position_pda.rs | 13 +- .../initialize_preset_parameters.rs | 61 +- .../src/instructions/initialize_reward.rs | 16 + .../src/instructions/migrate_bin_array.rs | 5 +- ...osition.rs => migrate_position_from_v1.rs} | 17 +- .../instructions/migrate_position_from_v2.rs | 53 + programs/lb_clmm/src/instructions/mod.rs | 14 +- .../src/instructions/position_authorize.rs | 8 +- .../src/instructions/remove_all_liquidity.rs | 14 +- .../src/instructions/remove_liquidity.rs | 129 +- .../instructions/remove_liquidity_by_range.rs | 13 + .../src/instructions/set_activation_slot.rs | 15 +- .../src/instructions/set_lock_release_slot.rs | 199 +- programs/lb_clmm/src/instructions/swap.rs | 129 +- .../src/instructions/toggle_pair_status.rs | 1 + .../src/instructions/update_fee_parameters.rs | 1 + .../instructions/update_fees_and_rewards.rs | 24 +- .../instructions/update_position_operator.rs | 6 +- .../instructions/update_reward_duration.rs | 32 + .../src/instructions/update_reward_funder.rs | 16 + .../withdraw_ineligible_reward.rs | 63 +- .../src/instructions/withdraw_protocol_fee.rs | 63 +- programs/lb_clmm/src/lib.rs | 114 +- .../lb_clmm/src/manager/bin_array_manager.rs | 26 +- programs/lb_clmm/src/math/bin_math.rs | 44 + programs/lb_clmm/src/math/price_math.rs | 39 + programs/lb_clmm/src/math/safe_math.rs | 47 + programs/lb_clmm/src/math/u128x128_math.rs | 170 ++ programs/lb_clmm/src/math/u64x64_math.rs | 187 ++ .../lb_clmm/src/math/weight_to_amounts.rs | 620 ++++- programs/lb_clmm/src/state/action_access.rs | 119 - programs/lb_clmm/src/state/bin.rs | 450 +++- .../src/state/bin_array_bitmap_extension.rs | 642 ++++- .../lb_clmm/src/state/dynamic_position.rs | 583 +++++ programs/lb_clmm/src/state/lb_pair.rs | 814 ------ .../src/state/lb_pair/action_access.rs | 281 +++ programs/lb_clmm/src/state/lb_pair/mod.rs | 3 + programs/lb_clmm/src/state/lb_pair/state.rs | 1696 +++++++++++++ programs/lb_clmm/src/state/mod.rs | 3 +- programs/lb_clmm/src/state/oracle.rs | 333 +++ programs/lb_clmm/src/state/parameters.rs | 193 ++ programs/lb_clmm/src/state/position.rs | 313 +-- .../lb_clmm/src/state/preset_parameters.rs | 70 +- programs/lb_clmm/src/tests/mod.rs | 8 + .../src/tests/reward_integration_tests.rs | 953 +++++++ .../src/tests/swap_integration_tests.rs | 148 ++ programs/lb_clmm/src/utils/pda.rs | 2 +- target/idl/lb_clmm.json | 1006 ++++++-- target/types/lb_clmm.ts | 2214 +++++++++++++---- 77 files changed, 12148 insertions(+), 2477 deletions(-) delete mode 100644 programs/lb_clmm/src/instructions/add_liquidity.rs delete mode 100644 programs/lb_clmm/src/instructions/add_liquidity_by_strategy_one_side.rs delete mode 100644 programs/lb_clmm/src/instructions/add_liquidity_by_weight_one_side.rs create mode 100644 programs/lb_clmm/src/instructions/decrease_position_length.rs create mode 100644 programs/lb_clmm/src/instructions/deposit/add_liquidity.rs rename programs/lb_clmm/src/instructions/{ => deposit}/add_liquidity_by_strategy.rs (56%) create mode 100644 programs/lb_clmm/src/instructions/deposit/add_liquidity_by_strategy_one_side.rs rename programs/lb_clmm/src/instructions/{ => deposit}/add_liquidity_by_weight.rs (67%) create mode 100644 programs/lb_clmm/src/instructions/deposit/add_liquidity_by_weight_one_side.rs create mode 100644 programs/lb_clmm/src/instructions/deposit/add_liquidity_single_side_precise.rs create mode 100644 programs/lb_clmm/src/instructions/deposit/mod.rs create mode 100644 programs/lb_clmm/src/instructions/go_to_a_bin.rs create mode 100644 programs/lb_clmm/src/instructions/increase_position_length.rs rename programs/lb_clmm/src/instructions/{migrate_position.rs => migrate_position_from_v1.rs} (64%) create mode 100644 programs/lb_clmm/src/instructions/migrate_position_from_v2.rs create mode 100644 programs/lb_clmm/src/instructions/remove_liquidity_by_range.rs delete mode 100644 programs/lb_clmm/src/state/action_access.rs create mode 100644 programs/lb_clmm/src/state/dynamic_position.rs delete mode 100644 programs/lb_clmm/src/state/lb_pair.rs create mode 100644 programs/lb_clmm/src/state/lb_pair/action_access.rs create mode 100644 programs/lb_clmm/src/state/lb_pair/mod.rs create mode 100644 programs/lb_clmm/src/state/lb_pair/state.rs create mode 100644 programs/lb_clmm/src/tests/mod.rs create mode 100644 programs/lb_clmm/src/tests/reward_integration_tests.rs create mode 100644 programs/lb_clmm/src/tests/swap_integration_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 3bac4b3..eb0f6c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2254,6 +2254,7 @@ dependencies = [ "solana-program", "solana-program-test", "solana-sdk", + "static_assertions", "uint", ] diff --git a/programs/lb_clmm/Cargo.toml b/programs/lb_clmm/Cargo.toml index 30e2940..9aa3152 100644 --- a/programs/lb_clmm/Cargo.toml +++ b/programs/lb_clmm/Cargo.toml @@ -29,6 +29,7 @@ num-integer = "0.1.45" mpl-token-metadata = "3.0.1" solana-program = "1.16.0" num_enum = "0.7.1" +static_assertions = "1.1.0" [dev-dependencies] proptest = "1.2.0" diff --git a/programs/lb_clmm/src/constants.rs b/programs/lb_clmm/src/constants.rs index a1ff603..f57bbb9 100644 --- a/programs/lb_clmm/src/constants.rs +++ b/programs/lb_clmm/src/constants.rs @@ -1,5 +1,4 @@ use anchor_lang::prelude::*; -use anchor_lang::solana_program::{pubkey, pubkey::Pubkey}; // TODO: Macro to compute the constants which changes based on the bit system used ? // Smallest step between bin is 0.01%, 1 bps @@ -10,9 +9,17 @@ pub const BASIS_POINT_MAX: i32 = 10000; #[constant] pub const MAX_BIN_PER_ARRAY: usize = 70; +/// Default number of bin per position contains. +#[constant] +pub const DEFAULT_BIN_PER_POSITION: usize = 70; + +/// Max resize length allowed +#[constant] +pub const MAX_RESIZE_LENGTH: usize = 70; + /// Maximum number of bin per position contains. #[constant] -pub const MAX_BIN_PER_POSITION: usize = 70; +pub const POSITION_MAX_LENGTH: usize = 1400; /// Minimum bin ID supported. Computed based on 1 bps. #[constant] @@ -65,10 +72,215 @@ pub const MAX_FEE_UPDATE_WINDOW: i64 = 0; #[constant] pub const MAX_REWARD_BIN_SPLIT: usize = 15; -#[cfg(feature = "localnet")] -pub static ALPHA_ACCESS_COLLECTION_MINTS: [Pubkey; 1] = - [pubkey!("J1S9H3QjnRtBbbuD4HjPV6RpRhwuk4zKbxsnCHuTgh9w")]; +#[cfg(test)] +pub mod tests { + use super::*; + use crate::math::price_math; + use crate::state::parameters::StaticParameters; + pub const PRESET_BIN_STEP: [u16; 12] = [1, 2, 4, 5, 8, 10, 15, 20, 25, 50, 60, 100]; + + /// Preset / supported static parameters. These default values are references from Trader Joe by querying trader joe factory. + /// https://snowtrace.io/address/0x8e42f2F4101563bF679975178e880FD87d3eFd4e + pub const fn get_preset(bin_step: u16) -> Option { + let params = match bin_step { + // TODO: enable protocol share back later + 1 => Some(StaticParameters { + base_factor: 20000, + filter_period: 10, + decay_period: 120, + reduction_factor: 5000, + variable_fee_control: 2000000, + // protocol_share: 500, + protocol_share: 0, + max_volatility_accumulator: 100000, + max_bin_id: 436704, + min_bin_id: -436704, + _padding: [0u8; 6], + }), + 2 => Some(StaticParameters { + base_factor: 15000, + filter_period: 10, + decay_period: 120, + reduction_factor: 5000, + variable_fee_control: 500000, + // protocol_share: 1000, + protocol_share: 0, + max_bin_id: 218363, + min_bin_id: -218363, + max_volatility_accumulator: 250000, + _padding: [0u8; 6], + }), + 4 => Some(StaticParameters { + base_factor: 50000, + filter_period: 30, + decay_period: 600, + reduction_factor: 5000, + variable_fee_control: 120000, + // protocol_share: 2500, + protocol_share: 0, + max_bin_id: 109192, + min_bin_id: -109192, + max_volatility_accumulator: 300000, + _padding: [0u8; 6], + }), + 5 => Some(StaticParameters { + base_factor: 8000, + filter_period: 30, + decay_period: 600, + reduction_factor: 5000, + variable_fee_control: 120000, + // protocol_share: 2500, + protocol_share: 0, + max_bin_id: 87358, + min_bin_id: -87358, + max_volatility_accumulator: 300000, + _padding: [0u8; 6], + }), + // this preset is included to match with orca pools + 8 => Some(StaticParameters { + base_factor: 6250, + filter_period: 30, + decay_period: 600, + reduction_factor: 5000, + variable_fee_control: 120000, + // protocol_share: 2500, + protocol_share: 0, + max_bin_id: 54190, + min_bin_id: -54190, + max_volatility_accumulator: 300000, + _padding: [0u8; 6], + }), + 10 => Some(StaticParameters { + base_factor: 10000, + filter_period: 30, + decay_period: 600, + reduction_factor: 5000, + variable_fee_control: 40000, + // protocol_share: 1000, + protocol_share: 0, + max_bin_id: 43690, + min_bin_id: -43690, + max_volatility_accumulator: 350000, + _padding: [0u8; 6], + }), + 15 => Some(StaticParameters { + base_factor: 10000, + filter_period: 30, + decay_period: 600, + reduction_factor: 5000, + variable_fee_control: 30000, + // protocol_share: 1000, + protocol_share: 0, + max_bin_id: 29134, + min_bin_id: -29134, + max_volatility_accumulator: 350000, + _padding: [0u8; 6], + }), + 20 => Some(StaticParameters { + base_factor: 10000, + filter_period: 30, + decay_period: 600, + reduction_factor: 5000, + variable_fee_control: 20000, + // protocol_share: 2000, + protocol_share: 0, + max_bin_id: 21855, + min_bin_id: -21855, + max_volatility_accumulator: 350000, + _padding: [0u8; 6], + }), + 25 => Some(StaticParameters { + base_factor: 10000, + filter_period: 30, + decay_period: 600, + reduction_factor: 5000, + variable_fee_control: 15000, + // protocol_share: 2000, + protocol_share: 0, + max_bin_id: 17481, + min_bin_id: -17481, + max_volatility_accumulator: 350000, + _padding: [0u8; 6], + }), + 50 => Some(StaticParameters { + base_factor: 8000, + filter_period: 120, + decay_period: 1200, + reduction_factor: 5000, + variable_fee_control: 10000, + // protocol_share: 2500, + protocol_share: 0, + max_bin_id: 8754, + min_bin_id: -8754, + max_volatility_accumulator: 250000, + _padding: [0u8; 6], + }), + 60 => Some(StaticParameters { + base_factor: 5000, + filter_period: 120, + decay_period: 1200, + reduction_factor: 5000, + variable_fee_control: 10000, + max_volatility_accumulator: 250000, + min_bin_id: -7299, + max_bin_id: 7299, + // protocol_share: 2500, + protocol_share: 0, + _padding: [0u8; 6], + }), + 100 => Some(StaticParameters { + base_factor: 8000, + filter_period: 300, + decay_period: 1200, + reduction_factor: 5000, + variable_fee_control: 7500, + // protocol_share: 2500, + protocol_share: 0, + max_bin_id: 4386, + min_bin_id: -4386, + max_volatility_accumulator: 150000, + _padding: [0u8; 6], + }), + _ => None, + }; + + // Is it possible to move the checking to compile time ? + if let Some(params) = ¶ms { + // Make sure the params stay within the bound. But it result in ugly runtime panic ... + // This couldn't prevent the team deploy with invalid parameters that causes the program overflow unexpectedly. But, at least it prevent user from creating such pools ... + // Increasing the bound will increase the bytes needed for fee calculation. + assert!(params.max_volatility_accumulator <= U24_MAX); + assert!(params.variable_fee_control <= U24_MAX); + assert!(params.protocol_share <= MAX_PROTOCOL_SHARE); + } + + params + } + + #[test] + fn test_get_preset() { + for bin_step in PRESET_BIN_STEP { + assert!(get_preset(bin_step).is_some()); + } + } + + #[test] + fn test_preset_min_max_bin_id() { + for bin_step in PRESET_BIN_STEP { + let param = get_preset(bin_step); + assert!(param.is_some()); + + if let Some(param) = param { + let max_price = price_math::get_price_from_id(param.max_bin_id, bin_step); + let min_price = price_math::get_price_from_id(param.min_bin_id, bin_step); + + assert!(max_price.is_ok()); + assert!(min_price.is_ok()); -#[cfg(not(feature = "localnet"))] -pub static ALPHA_ACCESS_COLLECTION_MINTS: [Pubkey; 1] = - [pubkey!("5rwhXUgAAdbVEaFQzAwgrcWwoCqYGzR1Mo2KwUYfbRuS")]; + // Bin is not swap-able when the price is u128::MAX, and 1 + assert!(max_price.unwrap() == 170141183460469231731687303715884105727); + assert!(min_price.unwrap() == 2); + } + } + } +} diff --git a/programs/lb_clmm/src/errors.rs b/programs/lb_clmm/src/errors.rs index 6b9c880..573bc3b 100644 --- a/programs/lb_clmm/src/errors.rs +++ b/programs/lb_clmm/src/errors.rs @@ -147,6 +147,27 @@ pub enum LBError { #[msg("Must withdraw ineligible reward")] MustWithdrawnIneligibleReward, + #[msg("Unauthorized address")] + UnauthorizedAddress, + + #[msg("Cannot update because operators are the same")] + OperatorsAreTheSame, + + #[msg("Withdraw to wrong token account")] + WithdrawToWrongTokenAccount, + + #[msg("Wrong rent receiver")] + WrongRentReceiver, + + #[msg("Already activated")] + AlreadyPassActivationSlot, + + #[msg("Last slot cannot be smaller than activate slot")] + LastSlotCannotBeSmallerThanActivateSlot, + + #[msg("Swapped amount is exceeded max swapped amount")] + ExceedMaxSwappedAmount, + #[msg("Invalid strategy parameters")] InvalidStrategyParameters, @@ -155,4 +176,13 @@ pub enum LBError { #[msg("Invalid lock release slot")] InvalidLockReleaseSlot, + + #[msg("Bin range is not empty")] + BinRangeIsNotEmpty, + + #[msg("Invalid side")] + InvalidSide, + + #[msg("Invalid resize length")] + InvalidResizeLength, } diff --git a/programs/lb_clmm/src/events.rs b/programs/lb_clmm/src/events.rs index 578eda4..9b15374 100644 --- a/programs/lb_clmm/src/events.rs +++ b/programs/lb_clmm/src/events.rs @@ -179,6 +179,34 @@ pub struct PositionCreate { pub owner: Pubkey, } +#[event] +pub struct IncreasePositionLength { + // Liquidity pool pair + pub lb_pair: Pubkey, + // Address of the position + pub position: Pubkey, + // Owner of the position + pub owner: Pubkey, + // Length to add + pub length_to_add: u16, + // side + pub side: u8, +} + +#[event] +pub struct DecreasePositionLength { + // Liquidity pool pair + pub lb_pair: Pubkey, + // Address of the position + pub position: Pubkey, + // Owner of the position + pub owner: Pubkey, + // Length to remove + pub length_to_remove: u16, + // side + pub side: u8, +} + #[event] pub struct FeeParameterUpdate { // Liquidity pool pair @@ -230,3 +258,13 @@ pub struct UpdatePositionLockReleaseSlot { // Sender public key pub sender: Pubkey, } + +#[event] +pub struct GoToABin { + // Pool pair + pub lb_pair: Pubkey, + // from bin id + pub from_bin_id: i32, + // to bin id + pub to_bin_id: i32, +} diff --git a/programs/lb_clmm/src/instructions/add_liquidity.rs b/programs/lb_clmm/src/instructions/add_liquidity.rs deleted file mode 100644 index 9186c0e..0000000 --- a/programs/lb_clmm/src/instructions/add_liquidity.rs +++ /dev/null @@ -1,99 +0,0 @@ -use crate::authorize_modify_position; -use crate::state::bin_array_bitmap_extension::BinArrayBitmapExtension; -use crate::state::position::PositionV2; -use crate::state::{bin::BinArray, lb_pair::LbPair}; -use anchor_lang::prelude::*; -use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; - -pub struct CompositeDepositInfo { - pub liquidity_share: u128, - pub protocol_token_x_fee_amount: u64, - pub protocol_token_y_fee_amount: u64, -} - -#[derive(AnchorSerialize, AnchorDeserialize, Eq, PartialEq, Clone, Debug)] -pub struct BinLiquidityDistribution { - /// Define the bin ID wish to deposit to. - pub bin_id: i32, - /// DistributionX (or distributionY) is the percentages of amountX (or amountY) you want to add to each bin. - pub distribution_x: u16, - /// DistributionX (or distributionY) is the percentages of amountX (or amountY) you want to add to each bin. - pub distribution_y: u16, -} - -#[derive(AnchorSerialize, AnchorDeserialize, Eq, PartialEq, Clone, Debug)] -pub struct LiquidityParameter { - /// Amount of X token to deposit - pub amount_x: u64, - /// Amount of Y token to deposit - pub amount_y: u64, - /// Liquidity distribution to each bins - pub bin_liquidity_dist: Vec, -} - -#[event_cpi] -#[derive(Accounts)] -pub struct ModifyLiquidity<'info> { - #[account( - mut, - has_one = lb_pair, - constraint = authorize_modify_position(&position, sender.key())? - )] - pub position: AccountLoader<'info, PositionV2>, - - #[account( - mut, - has_one = reserve_x, - has_one = reserve_y, - has_one = token_x_mint, - has_one = token_y_mint, - )] - pub lb_pair: AccountLoader<'info, LbPair>, - - #[account( - mut, - has_one = lb_pair, - )] - pub bin_array_bitmap_extension: Option>, - - #[account( - mut, - token::mint = token_x_mint - )] - pub user_token_x: Box>, - #[account( - mut, - token::mint = token_y_mint - )] - pub user_token_y: Box>, - - #[account(mut)] - pub reserve_x: Box>, - #[account(mut)] - pub reserve_y: Box>, - - pub token_x_mint: Box>, - pub token_y_mint: Box>, - - #[account( - mut, - has_one = lb_pair - )] - pub bin_array_lower: AccountLoader<'info, BinArray>, - #[account( - mut, - has_one = lb_pair - )] - pub bin_array_upper: AccountLoader<'info, BinArray>, - - pub sender: Signer<'info>, - pub token_x_program: Interface<'info, TokenInterface>, - pub token_y_program: Interface<'info, TokenInterface>, -} - -pub fn handle<'a, 'b, 'c, 'info>( - ctx: Context<'a, 'b, 'c, 'info, ModifyLiquidity<'info>>, - liquidity_parameter: LiquidityParameter, -) -> Result<()> { - Ok(()) -} diff --git a/programs/lb_clmm/src/instructions/add_liquidity_by_strategy_one_side.rs b/programs/lb_clmm/src/instructions/add_liquidity_by_strategy_one_side.rs deleted file mode 100644 index 12541ea..0000000 --- a/programs/lb_clmm/src/instructions/add_liquidity_by_strategy_one_side.rs +++ /dev/null @@ -1,70 +0,0 @@ -use super::add_liquidity_by_strategy::StrategyParameters; -use crate::errors::LBError; -use crate::math::weight_to_amounts::to_amount_ask_side; -use crate::math::weight_to_amounts::to_amount_bid_side; -use crate::to_weight_ascending_order; -use crate::to_weight_descending_order; -use crate::to_weight_spot_balanced; -use crate::ModifyLiquidityOneSide; -use crate::StrategyType; -use anchor_lang::prelude::*; - -#[derive(AnchorSerialize, AnchorDeserialize, Eq, PartialEq, Clone, Debug, Default)] -pub struct LiquidityParameterByStrategyOneSide { - /// Amount of X token or Y token to deposit - pub amount: u64, - /// Active bin that integrator observe off-chain - pub active_id: i32, - /// max active bin slippage allowed - pub max_active_bin_slippage: i32, - /// strategy parameters - pub strategy_parameters: StrategyParameters, -} - -impl LiquidityParameterByStrategyOneSide { - pub fn to_amounts_into_bin( - &self, - active_id: i32, - bin_step: u16, - deposit_for_y: bool, - ) -> Result> { - let min_bin_id = self.strategy_parameters.min_bin_id; - let max_bin_id = self.strategy_parameters.max_bin_id; - - let weights = match self.strategy_parameters.strategy_type { - StrategyType::SpotOneSide => Some(to_weight_spot_balanced( - self.strategy_parameters.min_bin_id, - self.strategy_parameters.max_bin_id, - )), - StrategyType::CurveOneSide => { - if deposit_for_y { - Some(to_weight_ascending_order(min_bin_id, max_bin_id)) - } else { - Some(to_weight_descending_order(min_bin_id, max_bin_id)) - } - } - StrategyType::BidAskOneSide => { - if deposit_for_y { - Some(to_weight_descending_order(min_bin_id, max_bin_id)) - } else { - Some(to_weight_ascending_order(min_bin_id, max_bin_id)) - } - } - _ => None, - } - .ok_or(LBError::InvalidStrategyParameters)?; - - if deposit_for_y { - to_amount_bid_side(active_id, self.amount, &weights) - } else { - to_amount_ask_side(active_id, self.amount, bin_step, &weights) - } - } -} - -pub fn handle<'a, 'b, 'c, 'info>( - ctx: Context<'a, 'b, 'c, 'info, ModifyLiquidityOneSide<'info>>, - liquidity_parameter: &LiquidityParameterByStrategyOneSide, -) -> Result<()> { - Ok(()) -} diff --git a/programs/lb_clmm/src/instructions/add_liquidity_by_weight_one_side.rs b/programs/lb_clmm/src/instructions/add_liquidity_by_weight_one_side.rs deleted file mode 100644 index 7f7cf3d..0000000 --- a/programs/lb_clmm/src/instructions/add_liquidity_by_weight_one_side.rs +++ /dev/null @@ -1,146 +0,0 @@ -use crate::authorize_modify_position; -use crate::constants::MAX_BIN_PER_POSITION; -use crate::errors::LBError; -use crate::math::weight_to_amounts::to_amount_ask_side; -use crate::math::weight_to_amounts::to_amount_bid_side; -use crate::state::bin_array_bitmap_extension::BinArrayBitmapExtension; -use crate::state::position::PositionV2; -use crate::state::{bin::BinArray, lb_pair::LbPair}; -use anchor_lang::prelude::*; -use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; - -use super::add_liquidity_by_weight::BinLiquidityDistributionByWeight; - -#[derive(AnchorSerialize, AnchorDeserialize, Eq, PartialEq, Clone, Debug)] -pub struct LiquidityOneSideParameter { - /// Amount of X token or Y token to deposit - pub amount: u64, - /// Active bin that integrator observe off-chain - pub active_id: i32, - /// max active bin slippage allowed - pub max_active_bin_slippage: i32, - /// Liquidity distribution to each bins - pub bin_liquidity_dist: Vec, -} - -impl LiquidityOneSideParameter { - fn bin_count(&self) -> u32 { - self.bin_liquidity_dist.len() as u32 - } - - fn validate<'a, 'info>(&'a self, active_id: i32) -> Result<()> { - require!(self.amount != 0, LBError::InvalidInput); - - let bin_count = self.bin_count(); - require!(bin_count > 0, LBError::InvalidInput); - - require!( - bin_count <= MAX_BIN_PER_POSITION as u32, - LBError::InvalidInput - ); - - let bin_shift = if active_id > self.active_id { - active_id - self.active_id - } else { - self.active_id - active_id - }; - - require!( - bin_shift <= self.max_active_bin_slippage.into(), - LBError::ExceededBinSlippageTolerance - ); - - // bin dist must be in consecutive order and weight is non-zero - for (i, val) in self.bin_liquidity_dist.iter().enumerate() { - require!(val.weight != 0, LBError::InvalidInput); - // bin id must in right order - if i != 0 { - require!( - val.bin_id > self.bin_liquidity_dist[i - 1].bin_id, - LBError::InvalidInput - ); - } - } - Ok(()) - } - - // require bin id to be sorted before doing this - fn to_amounts_into_bin<'a, 'info>( - &'a self, - active_id: i32, - bin_step: u16, - deposit_for_y: bool, - ) -> Result> { - if deposit_for_y { - to_amount_bid_side( - active_id, - self.amount, - &self - .bin_liquidity_dist - .iter() - .map(|x| (x.bin_id, x.weight)) - .collect::>(), - ) - } else { - to_amount_ask_side( - active_id, - self.amount, - bin_step, - &self - .bin_liquidity_dist - .iter() - .map(|x| (x.bin_id, x.weight)) - .collect::>(), - ) - } - } -} - -#[event_cpi] -#[derive(Accounts)] -pub struct ModifyLiquidityOneSide<'info> { - #[account( - mut, - has_one = lb_pair, - constraint = authorize_modify_position(&position, sender.key())? - )] - pub position: AccountLoader<'info, PositionV2>, - - #[account(mut)] - pub lb_pair: AccountLoader<'info, LbPair>, - - #[account( - mut, - has_one = lb_pair, - )] - pub bin_array_bitmap_extension: Option>, - - #[account(mut)] - pub user_token: Box>, - - #[account(mut)] - pub reserve: Box>, - - pub token_mint: Box>, - - #[account( - mut, - has_one = lb_pair - )] - pub bin_array_lower: AccountLoader<'info, BinArray>, - #[account( - mut, - has_one = lb_pair - )] - pub bin_array_upper: AccountLoader<'info, BinArray>, - - pub sender: Signer<'info>, - pub token_program: Interface<'info, TokenInterface>, -} - -pub fn handle<'a, 'b, 'c, 'info>( - ctx: &Context<'a, 'b, 'c, 'info, ModifyLiquidityOneSide<'info>>, - liquidity_parameter: &LiquidityOneSideParameter, -) -> Result<()> { - Ok(()) -} diff --git a/programs/lb_clmm/src/instructions/claim_fee.rs b/programs/lb_clmm/src/instructions/claim_fee.rs index 89b9ea1..5208e7d 100644 --- a/programs/lb_clmm/src/instructions/claim_fee.rs +++ b/programs/lb_clmm/src/instructions/claim_fee.rs @@ -1,7 +1,15 @@ use crate::authorize_claim_fee_position; -use crate::state::{bin::BinArray, lb_pair::LbPair, position::PositionV2}; +use crate::errors::LBError; +use crate::events::ClaimFee as ClaimFeeEvent; +use crate::manager::bin_array_manager::BinArrayManager; +use crate::math::safe_math::SafeMath; +use crate::state::dynamic_position::{DynamicPositionLoader, PositionV3}; +use crate::state::{bin::BinArray, lb_pair::LbPair}; +use crate::BinArrayAccount; +use crate::PositionLiquidityFlowValidator; use anchor_lang::prelude::*; -use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface, TransferChecked}; +use std::collections::{BTreeMap, BTreeSet}; #[event_cpi] #[derive(Accounts)] pub struct ClaimFee<'info> { @@ -19,18 +27,7 @@ pub struct ClaimFee<'info> { has_one = lb_pair, constraint = authorize_claim_fee_position(&position, sender.key())? )] - pub position: AccountLoader<'info, PositionV2>, - - #[account( - mut, - has_one = lb_pair - )] - pub bin_array_lower: AccountLoader<'info, BinArray>, - #[account( - mut, - has_one = lb_pair - )] - pub bin_array_upper: AccountLoader<'info, BinArray>, + pub position: AccountLoader<'info, PositionV3>, pub sender: Signer<'info>, @@ -50,6 +47,69 @@ pub struct ClaimFee<'info> { pub token_program: Interface<'info, TokenInterface>, } -pub fn handle(ctx: Context) -> Result<()> { +impl<'info> ClaimFee<'info> { + fn transfer_from_reserve_x(&self, amount: u64) -> Result<()> { + let lb_pair = self.lb_pair.load()?; + let signer_seeds = &[&lb_pair.seeds()?[..]]; + anchor_spl::token_2022::transfer_checked( + CpiContext::new_with_signer( + self.token_program.to_account_info(), + TransferChecked { + from: self.reserve_x.to_account_info(), + to: self.user_token_x.to_account_info(), + authority: self.lb_pair.to_account_info(), + mint: self.token_x_mint.to_account_info(), + }, + signer_seeds, + ), + amount, + self.token_x_mint.decimals, + ) + } + + fn transfer_from_reserve_y(&self, amount: u64) -> Result<()> { + let lb_pair = self.lb_pair.load()?; + let signer_seeds = &[&lb_pair.seeds()?[..]]; + anchor_spl::token_2022::transfer_checked( + CpiContext::new_with_signer( + self.token_program.to_account_info(), + TransferChecked { + from: self.reserve_y.to_account_info(), + to: self.user_token_y.to_account_info(), + authority: self.lb_pair.to_account_info(), + mint: self.token_y_mint.to_account_info(), + }, + signer_seeds, + ), + amount, + self.token_y_mint.decimals, + ) + } +} + +impl<'a, 'b, 'c, 'info> PositionLiquidityFlowValidator for ClaimFee<'info> { + fn validate_outflow_to_ata_of_position_owner(&self, owner: Pubkey) -> Result<()> { + let dest_token_x = anchor_spl::associated_token::get_associated_token_address( + &owner, + &self.token_x_mint.key(), + ); + require!( + dest_token_x == self.user_token_x.key() && self.user_token_x.owner == owner, + LBError::WithdrawToWrongTokenAccount + ); + + let dest_token_y = anchor_spl::associated_token::get_associated_token_address( + &owner, + &self.token_y_mint.key(), + ); + require!( + dest_token_y == self.user_token_y.key() && self.user_token_y.owner == owner, + LBError::WithdrawToWrongTokenAccount + ); + Ok(()) + } +} + +pub fn handle(ctx: Context, min_bin_id: i32, max_bin_id: i32) -> Result<()> { Ok(()) } diff --git a/programs/lb_clmm/src/instructions/claim_reward.rs b/programs/lb_clmm/src/instructions/claim_reward.rs index ad94894..c0456d8 100644 --- a/programs/lb_clmm/src/instructions/claim_reward.rs +++ b/programs/lb_clmm/src/instructions/claim_reward.rs @@ -1,7 +1,18 @@ use crate::authorize_modify_position; -use crate::state::{bin::BinArray, lb_pair::LbPair, position::PositionV2}; +use crate::errors::LBError; +use crate::events::ClaimReward as ClaimRewardEvent; +use crate::manager::bin_array_manager::BinArrayManager; +use crate::math::safe_math::SafeMath; +use crate::state::dynamic_position::{DynamicPositionLoader, PositionV3}; +use crate::BinArrayAccount; +use crate::PositionLiquidityFlowValidator; +use crate::{ + constants::NUM_REWARDS, + state::{bin::BinArray, lb_pair::LbPair}, +}; use anchor_lang::prelude::*; -use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface, TransferChecked}; +use std::collections::{BTreeMap, BTreeSet}; #[event_cpi] #[derive(Accounts)] @@ -15,18 +26,7 @@ pub struct ClaimReward<'info> { has_one = lb_pair, constraint = authorize_modify_position(&position, sender.key())? )] - pub position: AccountLoader<'info, PositionV2>, - - #[account( - mut, - has_one = lb_pair - )] - pub bin_array_lower: AccountLoader<'info, BinArray>, - #[account( - mut, - has_one = lb_pair - )] - pub bin_array_upper: AccountLoader<'info, BinArray>, + pub position: AccountLoader<'info, PositionV3>, pub sender: Signer<'info>, @@ -40,7 +40,63 @@ pub struct ClaimReward<'info> { pub token_program: Interface<'info, TokenInterface>, } +impl<'info> ClaimReward<'info> { + fn validate(&self, reward_index: usize) -> Result<()> { + let lb_pair = self.lb_pair.load()?; + require!(reward_index < NUM_REWARDS, LBError::InvalidRewardIndex); + + let reward_info = &lb_pair.reward_infos[reward_index]; + require!(reward_info.initialized(), LBError::RewardUninitialized); + require!( + reward_info.vault.eq(&self.reward_vault.key()), + LBError::InvalidRewardVault + ); + + Ok(()) + } + + fn transfer_from_reward_vault_to_user(&self, amount: u64) -> Result<()> { + let lb_pair = self.lb_pair.load()?; + let signer_seeds = &[&lb_pair.seeds()?[..]]; + anchor_spl::token_2022::transfer_checked( + CpiContext::new_with_signer( + self.token_program.to_account_info(), + TransferChecked { + from: self.reward_vault.to_account_info(), + to: self.user_token_account.to_account_info(), + authority: self.lb_pair.to_account_info(), + mint: self.reward_mint.to_account_info(), + }, + signer_seeds, + ), + amount, + self.reward_mint.decimals, + ) + } +} + +impl<'a, 'b, 'c, 'info> PositionLiquidityFlowValidator for ClaimReward<'info> { + fn validate_outflow_to_ata_of_position_owner(&self, owner: Pubkey) -> Result<()> { + let dest_reward_token = anchor_spl::associated_token::get_associated_token_address( + &owner, + &self.reward_mint.key(), + ); + require!( + dest_reward_token == self.user_token_account.key() + && self.user_token_account.owner == owner, + LBError::WithdrawToWrongTokenAccount + ); + + Ok(()) + } +} + // TODO: Should we pass in range of bin we are going to collect reward ? It could help us in heap / compute unit issue by chunking into multiple tx. -pub fn handle(ctx: Context, index: u64) -> Result<()> { +pub fn handle( + ctx: Context, + index: u64, + min_bin_id: i32, + max_bin_id: i32, +) -> Result<()> { Ok(()) } diff --git a/programs/lb_clmm/src/instructions/close_position.rs b/programs/lb_clmm/src/instructions/close_position.rs index 184b2e8..245ee79 100644 --- a/programs/lb_clmm/src/instructions/close_position.rs +++ b/programs/lb_clmm/src/instructions/close_position.rs @@ -1,32 +1,19 @@ use anchor_lang::prelude::*; use crate::authorize_modify_position; -use crate::state::{bin::BinArray, lb_pair::LbPair, position::PositionV2}; +use crate::errors::LBError; +use crate::events::PositionClose; +use crate::state::dynamic_position::{DynamicPositionLoader, PositionV3}; #[event_cpi] #[derive(Accounts)] pub struct ClosePosition<'info> { #[account( mut, - has_one = lb_pair, constraint = authorize_modify_position(&position, sender.key())?, close = rent_receiver )] - pub position: AccountLoader<'info, PositionV2>, - - #[account(mut)] - pub lb_pair: AccountLoader<'info, LbPair>, - - #[account( - mut, - has_one = lb_pair - )] - pub bin_array_lower: AccountLoader<'info, BinArray>, - #[account( - mut, - has_one = lb_pair - )] - pub bin_array_upper: AccountLoader<'info, BinArray>, + pub position: AccountLoader<'info, PositionV3>, pub sender: Signer<'info>, diff --git a/programs/lb_clmm/src/instructions/decrease_position_length.rs b/programs/lb_clmm/src/instructions/decrease_position_length.rs new file mode 100644 index 0000000..5d84947 --- /dev/null +++ b/programs/lb_clmm/src/instructions/decrease_position_length.rs @@ -0,0 +1,29 @@ +use crate::constants::MAX_RESIZE_LENGTH; +use crate::errors::LBError; +use crate::events::DecreasePositionLength as DecreasePositionLengthEvent; +use crate::math::safe_math::SafeMath; +use crate::state::dynamic_position::DynamicPositionLoader; +use crate::state::dynamic_position::PositionV3; +use crate::state::dynamic_position::ResizeSide; +use anchor_lang::prelude::*; +#[event_cpi] +#[derive(Accounts)] +#[instruction(length_to_remove: u16, side: u8)] +pub struct DecreasePositionLength<'info> { + /// CHECK: Account to receive closed account rental SOL + #[account(mut)] + pub rent_receiver: UncheckedAccount<'info>, + + #[account( + mut, + has_one = owner, + )] + pub position: AccountLoader<'info, PositionV3>, + pub owner: Signer<'info>, + pub system_program: Program<'info, System>, +} + +// side: 0 lower side, and 1 upper side +pub fn handle(ctx: Context, length_to_remove: u16, side: u8) -> Result<()> { + Ok(()) +} diff --git a/programs/lb_clmm/src/instructions/deposit/add_liquidity.rs b/programs/lb_clmm/src/instructions/deposit/add_liquidity.rs new file mode 100644 index 0000000..cdd0162 --- /dev/null +++ b/programs/lb_clmm/src/instructions/deposit/add_liquidity.rs @@ -0,0 +1,562 @@ +use crate::authorize_modify_position; +use crate::constants::BASIS_POINT_MAX; +use crate::errors::LBError; +use crate::events::{AddLiquidity as AddLiquidityEvent, CompositionFee}; +use crate::manager::bin_array_manager::BinArrayManager; +use crate::math::bin_math::get_liquidity; +use crate::math::safe_math::SafeMath; +use crate::math::weight_to_amounts::AmountInBin; +use crate::state::action_access::get_lb_pair_type_access_validator; +use crate::state::bin::{get_liquidity_share, get_out_amount, Bin}; +use crate::state::bin_array_bitmap_extension::BinArrayBitmapExtension; +use crate::state::dynamic_position::{DynamicPosition, DynamicPositionLoader, PositionV3}; +use crate::state::lb_pair::LbPair; +use crate::BinArrayAccount; +use anchor_lang::prelude::*; +use anchor_spl::token_2022::TransferChecked; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; +use num_traits::Zero; +use std::cell::RefMut; +use std::collections::{BTreeMap, BTreeSet}; + +pub struct CompositeDepositInfo { + pub liquidity_share: u128, + pub protocol_token_x_fee_amount: u64, + pub protocol_token_y_fee_amount: u64, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Eq, PartialEq, Clone, Debug)] +pub struct BinLiquidityDistribution { + /// Define the bin ID wish to deposit to. + pub bin_id: i32, + /// DistributionX (or distributionY) is the percentages of amountX (or amountY) you want to add to each bin. + pub distribution_x: u16, + /// DistributionX (or distributionY) is the percentages of amountX (or amountY) you want to add to each bin. + pub distribution_y: u16, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Eq, PartialEq, Clone, Debug)] +pub struct LiquidityParameter { + /// Amount of X token to deposit + pub amount_x: u64, + /// Amount of Y token to deposit + pub amount_y: u64, + /// Liquidity distribution to each bins + pub bin_liquidity_dist: Vec, +} + +impl LiquidityParameter { + fn bin_count(&self) -> u32 { + self.bin_liquidity_dist.len() as u32 + } + + fn validate<'a, 'info>(&'a self, position_width: u64) -> Result<()> { + let bin_count = self.bin_count(); + require!(bin_count > 0, LBError::InvalidInput); + + require!(bin_count as u64 <= position_width, LBError::InvalidInput); + + let mut sum_x_distribution = 0u32; + let mut sum_y_distribution = 0u32; + for bin_dist in self.bin_liquidity_dist.iter() { + sum_x_distribution = sum_x_distribution.safe_add(bin_dist.distribution_x.into())?; + sum_y_distribution = sum_y_distribution.safe_add(bin_dist.distribution_y.into())?; + } + + // bin dist must be in consecutive order + for (i, val) in self.bin_liquidity_dist.iter().enumerate() { + // bin id must in right order + if i != 0 { + require!( + val.bin_id > self.bin_liquidity_dist[i - 1].bin_id, + LBError::InvalidInput + ); + } + } + + require!( + sum_x_distribution <= BASIS_POINT_MAX as u32, + LBError::InvalidInput + ); + + require!( + sum_y_distribution <= BASIS_POINT_MAX as u32, + LBError::InvalidInput + ); + + Ok(()) + } +} + +pub trait AddLiquidity { + fn transfer_to_reserve_x(&self, amount_x: u64) -> Result<()>; + fn transfer_to_reserve_y(&self, amount_y: u64) -> Result<()>; +} + +impl<'a, 'b, 'c, 'info> AddLiquidity for Context<'a, 'b, 'c, 'info, ModifyLiquidity<'info>> { + fn transfer_to_reserve_x(&self, amount_x: u64) -> Result<()> { + anchor_spl::token_2022::transfer_checked( + CpiContext::new( + self.accounts.token_x_program.to_account_info(), + TransferChecked { + from: self.accounts.user_token_x.to_account_info(), + to: self.accounts.reserve_x.to_account_info(), + authority: self.accounts.sender.to_account_info(), + mint: self.accounts.token_x_mint.to_account_info(), + }, + ), + amount_x, + self.accounts.token_x_mint.decimals, + ) + } + + fn transfer_to_reserve_y(&self, amount_y: u64) -> Result<()> { + anchor_spl::token_2022::transfer_checked( + CpiContext::new( + self.accounts.token_y_program.to_account_info(), + TransferChecked { + from: self.accounts.user_token_y.to_account_info(), + to: self.accounts.reserve_y.to_account_info(), + authority: self.accounts.sender.to_account_info(), + mint: self.accounts.token_y_mint.to_account_info(), + }, + ), + amount_y, + self.accounts.token_y_mint.decimals, + ) + } +} + +#[event_cpi] +#[derive(Accounts)] +pub struct ModifyLiquidity<'info> { + #[account( + mut, + has_one = lb_pair, + constraint = authorize_modify_position(&position, sender.key())? + )] + pub position: AccountLoader<'info, PositionV3>, + + #[account( + mut, + has_one = reserve_x, + has_one = reserve_y, + has_one = token_x_mint, + has_one = token_y_mint, + )] + pub lb_pair: AccountLoader<'info, LbPair>, + + #[account( + mut, + has_one = lb_pair, + )] + pub bin_array_bitmap_extension: Option>, + + #[account( + mut, + token::mint = token_x_mint + )] + pub user_token_x: Box>, + #[account( + mut, + token::mint = token_y_mint + )] + pub user_token_y: Box>, + + #[account(mut)] + pub reserve_x: Box>, + #[account(mut)] + pub reserve_y: Box>, + + pub token_x_mint: Box>, + pub token_y_mint: Box>, + + pub sender: Signer<'info>, + pub token_x_program: Interface<'info, TokenInterface>, + pub token_y_program: Interface<'info, TokenInterface>, +} + +pub struct DepositBinInfo { + /// Token X amount to be deposited into the bin + pub amount_x_into_bin: u64, + /// Token Y amount to be deposited into the bin + pub amount_y_into_bin: u64, + /// Token X amount if immediately withdraw + pub out_amount_x: u64, + /// Token Y amount if immediately withdraw + pub out_amount_y: u64, + /// Total share deposited into the bin based on in_amount_x and in_amount_y. + pub liquidity_share: u128, + /// Liquidity of the bin + pub bin_liquidity: u128, + /// Price of the bin + pub bin_price: u128, +} + +pub fn handle<'a, 'b, 'c, 'info>( + ctx: Context<'a, 'b, 'c, 'info, ModifyLiquidity<'info>>, + liquidity_parameter: LiquidityParameter, +) -> Result<()> { + Ok(()) +} + +/// handle deposit both side +pub fn handle_deposit_by_amounts<'a, 'b, 'c, 'info>( + ctx: &Context<'a, 'b, 'c, 'info, ModifyLiquidity<'info>>, + amounts_in_bin: &[AmountInBin], // vec of bin id and amount +) -> Result<()> { + Ok(()) +} + +/// Get token X, Y amount if withdraw immediately upon deposit +pub fn get_out_amount_after_deposit( + liquidity_share: u128, + in_amount_x: u64, + in_amount_y: u64, + bin: &Bin, +) -> Result<(u64, u64)> { + let out_amount_x: u64 = get_out_amount( + liquidity_share, + in_amount_x.safe_add(bin.amount_x)?, + bin.liquidity_supply.safe_add(liquidity_share)?, + )?; + + let out_amount_y: u64 = get_out_amount( + liquidity_share, + in_amount_y.safe_add(bin.amount_y)?, + bin.liquidity_supply.safe_add(liquidity_share)?, + )?; + + Ok((out_amount_x, out_amount_y)) +} + +/// Charge protocol fee from composition fee. +pub fn charge_protocol_fee( + lb_pair: &mut RefMut<'_, LbPair>, + composition_fee_x: u64, + composition_fee_y: u64, +) -> Result<(u64, u64)> { + let protocol_fee_x = lb_pair.compute_protocol_fee(composition_fee_x)?; + let protocol_fee_y = lb_pair.compute_protocol_fee(composition_fee_y)?; + + lb_pair.accumulate_protocol_fees(protocol_fee_x, protocol_fee_y)?; + + Ok((protocol_fee_x, protocol_fee_y)) +} + +/// Charge swap fee and deposit to bin. Return liquidity share to mint after charge swap fee. +pub fn charge_fee_and_deposit<'a, 'info>( + lb_pair: &'a mut RefMut<'_, LbPair>, + bin_array_manager: &'a mut BinArrayManager, + in_id: i32, + bin_price: u128, + amount_x: u64, + amount_y: u64, + composition_fee_x: u64, + composition_fee_y: u64, +) -> Result { + let (protocol_fee_x, protocol_fee_y) = + charge_protocol_fee(lb_pair, composition_fee_x, composition_fee_y)?; + + let bin = bin_array_manager.get_bin_mut(in_id)?; + + // pay swap fee firstly + bin.deposit_composition_fee( + composition_fee_x.safe_sub(protocol_fee_x)?, + composition_fee_y.safe_sub(protocol_fee_y)?, + )?; + + // Amount the user is depositing after internal swap. + let amount_x_into_bin_after_fee = amount_x.safe_sub(composition_fee_x)?; + let amount_y_into_bin_after_fee = amount_y.safe_sub(composition_fee_y)?; + + // Calculate liquidity after charge swap fee + let in_liquidity = get_liquidity( + amount_x_into_bin_after_fee, + amount_y_into_bin_after_fee, + bin_price, + )?; + // calculate bin_liquidity after deposit composition fee + let bin_liquidity = get_liquidity(bin.amount_x, bin.amount_y, bin_price)?; + + // Calculate liquidity share to mint after charge swap fee + let liquidity_share = get_liquidity_share(in_liquidity, bin_liquidity, bin.liquidity_supply)?; + + // Protocol fee is not accumulated in the bin liquidity. + bin.deposit( + amount_x_into_bin_after_fee, + amount_y_into_bin_after_fee, + liquidity_share, + )?; + + Ok(CompositeDepositInfo { + liquidity_share, + protocol_token_x_fee_amount: protocol_fee_x, + protocol_token_y_fee_amount: protocol_fee_y, + }) +} + +/// Deposit to bin without charging internal swap fee +fn deposit<'info>( + bin_array_manager: &mut BinArrayManager, + in_id: i32, + amount_x: u64, + amount_y: u64, + liquidity_share: u128, +) -> Result<()> { + let bin = bin_array_manager.get_bin_mut(in_id)?; + bin.deposit(amount_x, amount_y, liquidity_share) +} + +pub fn get_amount_into_bin(in_amount: u64, distribution: u64) -> Result { + require!(distribution <= BASIS_POINT_MAX as u64, LBError::InvalidBps); + + let amount: u64 = u128::from(in_amount) + .safe_mul(distribution.into()) + .and_then(|v| v.safe_div(BASIS_POINT_MAX as u128))? + .try_into() + .map_err(|_| LBError::TypeCastFailed)?; + + Ok(amount) +} + +/// Get bin, and deposit liquidity +pub fn get_deposit_bin_info( + in_id: i32, + bin_step: u16, + amount_x_into_bin: u64, + amount_y_into_bin: u64, + bin_array_manager: &mut BinArrayManager, +) -> Result { + let LiquidityShareInfo { + liquidity_share, + bin_liquidity, + } = get_liquidity_share_by_in_amount( + in_id, + bin_step, + amount_x_into_bin, + amount_y_into_bin, + bin_array_manager, + )?; + + let bin = bin_array_manager.get_bin_mut(in_id)?; + let price = bin.get_or_store_bin_price(in_id, bin_step)?; + + if bin.liquidity_supply == 0 { + return Ok(DepositBinInfo { + amount_x_into_bin, + amount_y_into_bin, + liquidity_share, + bin_price: price, + out_amount_x: amount_x_into_bin, + out_amount_y: amount_y_into_bin, + bin_liquidity, + }); + } + + let (out_amount_x, out_amount_y) = + get_out_amount_after_deposit(liquidity_share, amount_x_into_bin, amount_y_into_bin, &bin)?; + + Ok(DepositBinInfo { + amount_x_into_bin, + amount_y_into_bin, + liquidity_share, + bin_price: price, + out_amount_x, + out_amount_y, + bin_liquidity, + }) +} + +/// Calculate composition fee for LP and protocol. The protocol share is inclusive of the total composition fee. +pub fn compute_composition_fee<'info>( + out_amount_x: u64, + amount_x_into_bin: u64, + out_amount_y: u64, + amount_y_into_bin: u64, + lb_pair: &LbPair, +) -> Result { + // Eg: X out > X_in is similar to Swap Y -> X, charge Y for fee (Y == opposite delta) + if out_amount_x > amount_x_into_bin { + let delta = amount_y_into_bin.safe_sub(out_amount_y)?; + lb_pair.compute_composition_fee(delta) + } else { + Ok(0) + } +} + +/// Verify that the amounts are correct and that the composition factor is not flawed. +/// Which means, bin before active bin can only contains token Y, bin after active bin can only contain token X. +fn verify_in_amounts(amount_x: u64, amount_y: u64, active_id: i32, id: i32) -> Result<()> { + if id < active_id { + require!(amount_x == 0, LBError::CompositionFactorFlawed); + } + + if id > active_id { + require!(amount_y == 0, LBError::CompositionFactorFlawed); + } + + // id == active_id allows X and Y to be deposited in + Ok(()) +} + +// deposit in a bin +pub fn deposit_in_bin_id( + in_id: i32, + amount_x_into_bin: u64, + amount_y_into_bin: u64, + lb_pair: &mut RefMut<'_, LbPair>, + position: &mut DynamicPosition, + bin_array_manager: &mut BinArrayManager, + sender: Pubkey, +) -> Result> { + let active_id = lb_pair.active_id; + let bin_step = lb_pair.bin_step; + let mut composition_fee_event = None; + // Make sure composition factor not flawed + verify_in_amounts(amount_x_into_bin, amount_y_into_bin, active_id, in_id)?; + let liquidity_share = if in_id == lb_pair.active_id { + // Update volatility parameters to reflect the latest volatile fee for composite swap + let current_timestamp = Clock::get()?.unix_timestamp; + lb_pair.update_volatility_parameters(current_timestamp)?; + + let DepositBinInfo { + amount_x_into_bin, + amount_y_into_bin, + liquidity_share, + bin_price, + out_amount_x, + out_amount_y, + .. + } = get_deposit_bin_info( + in_id, + bin_step, + amount_x_into_bin, + amount_y_into_bin, + bin_array_manager, + )?; + + // assert for max_swapped_amount if it is internal swap + if out_amount_x > amount_x_into_bin { + let (throttled_status, max_swapped_amount) = + lb_pair.get_swap_cap_status_and_amount(current_timestamp as u64, false)?; + + if throttled_status { + let delta = out_amount_x.safe_sub(amount_x_into_bin)?; + require!(delta <= max_swapped_amount, LBError::ExceedMaxSwappedAmount); + } + } + + let composition_fee_x = compute_composition_fee( + out_amount_y, + amount_y_into_bin, + out_amount_x, + amount_x_into_bin, + lb_pair, + )?; + + let composition_fee_y = compute_composition_fee( + out_amount_x, + amount_x_into_bin, + out_amount_y, + amount_y_into_bin, + &lb_pair, + )?; + if composition_fee_x.safe_add(composition_fee_y)? > 0 { + let CompositeDepositInfo { + liquidity_share, + protocol_token_x_fee_amount, + protocol_token_y_fee_amount, + } = charge_fee_and_deposit( + lb_pair, + bin_array_manager, + in_id, + bin_price, + amount_x_into_bin, + amount_y_into_bin, + composition_fee_x, + composition_fee_y, + )?; + + composition_fee_event = Some(CompositionFee { + bin_id: in_id as i16, + from: sender, + protocol_token_x_fee_amount, + protocol_token_y_fee_amount, + token_x_fee_amount: composition_fee_x, + token_y_fee_amount: composition_fee_y, + }); + + liquidity_share + } else { + deposit( + bin_array_manager, + in_id, + amount_x_into_bin, + amount_y_into_bin, + liquidity_share, + )?; + // No fee + liquidity_share + } + } else { + let LiquidityShareInfo { + liquidity_share, .. + } = get_liquidity_share_by_in_amount( + in_id, + bin_step, + amount_x_into_bin, + amount_y_into_bin, + bin_array_manager, + )?; + + deposit( + bin_array_manager, + in_id, + amount_x_into_bin, + amount_y_into_bin, + liquidity_share, + )?; + liquidity_share + }; + + require!(liquidity_share > 0, LBError::ZeroLiquidity); + + position.deposit(in_id, liquidity_share)?; + Ok(composition_fee_event) +} + +pub struct LiquidityShareInfo { + /// bin_liquidity + pub bin_liquidity: u128, + /// liquidity_share + pub liquidity_share: u128, +} + +/// Get bin, and deposit liquidity +pub fn get_liquidity_share_by_in_amount( + in_id: i32, + bin_step: u16, + in_amount_x: u64, + in_amount_y: u64, + bin_array_manager: &mut BinArrayManager, +) -> Result { + let bin = bin_array_manager.get_bin_mut(in_id)?; + let price = bin.get_or_store_bin_price(in_id, bin_step)?; + + let in_liquidity: u128 = get_liquidity(in_amount_x, in_amount_y, price)?; + let bin_liquidity = get_liquidity(bin.amount_x, bin.amount_y, price)?; + if bin.liquidity_supply == 0 { + return Ok(LiquidityShareInfo { + bin_liquidity, + liquidity_share: in_liquidity, + }); + } + + let liquidity_share = get_liquidity_share(in_liquidity, bin_liquidity, bin.liquidity_supply)?; + + return Ok(LiquidityShareInfo { + bin_liquidity, + liquidity_share, + }); +} diff --git a/programs/lb_clmm/src/instructions/add_liquidity_by_strategy.rs b/programs/lb_clmm/src/instructions/deposit/add_liquidity_by_strategy.rs similarity index 56% rename from programs/lb_clmm/src/instructions/add_liquidity_by_strategy.rs rename to programs/lb_clmm/src/instructions/deposit/add_liquidity_by_strategy.rs index c6e5560..076f453 100644 --- a/programs/lb_clmm/src/instructions/add_liquidity_by_strategy.rs +++ b/programs/lb_clmm/src/instructions/deposit/add_liquidity_by_strategy.rs @@ -1,9 +1,11 @@ use crate::errors::LBError; +use crate::find_amount_in_active_bin; use crate::math::safe_math::SafeMath; -use crate::math::weight_to_amounts::{to_amount_ask_side, to_amount_bid_side, to_amount_both_side}; -use crate::ModifyLiquidity; +use crate::math::weight_to_amounts::{ + to_amount_ask_side, to_amount_bid_side, to_amount_both_side, AmountInBin, AmountInBinSingleSide, +}; +use crate::{handle_deposit_by_amounts, ModifyLiquidity}; use anchor_lang::prelude::*; - const DEFAULT_MIN_WEIGHT: u16 = 200; const DEFAULT_MAX_WEIGHT: u16 = 2000; @@ -21,26 +23,83 @@ pub struct LiquidityParameterByStrategy { pub strategy_parameters: StrategyParameters, } +pub fn validate_add_liquidity_by_strategy_params( + active_id: i32, + current_active_id: i32, + max_active_bin_slippage: i32, + strategy_parameters: &StrategyParameters, +) -> Result<()> { + let bin_count = strategy_parameters.bin_count()?; + require!(bin_count > 0, LBError::InvalidInput); + + let bin_shift = if active_id > current_active_id { + active_id - current_active_id + } else { + current_active_id - active_id + }; + + require!( + bin_shift <= max_active_bin_slippage, + LBError::ExceededBinSlippageTolerance + ); + Ok(()) +} + impl LiquidityParameterByStrategy { + fn is_bin_id_within_range(&self, bin_id: i32) -> bool { + self.strategy_parameters.min_bin_id <= bin_id + && bin_id <= self.strategy_parameters.max_bin_id + } + + fn is_balanced_deposit(&self) -> bool { + match self.strategy_parameters.strategy_type { + StrategyType::SpotBalanced => true, + StrategyType::CurveBalanced => true, + StrategyType::BidAskBalanced => true, + _ => false, + } + } + + pub fn is_bin_id_within_range_and_balanced_deposit(&self, bin_id: i32) -> bool { + self.is_bin_id_within_range(bin_id) && self.is_balanced_deposit() + } + pub fn to_amounts_into_bin( &self, active_id: i32, bin_step: u16, amount_x_in_active_bin: u64, amount_y_in_active_bin: u64, - ) -> Result> { + ) -> Result> { let min_bin_id = self.strategy_parameters.min_bin_id; let max_bin_id = self.strategy_parameters.max_bin_id; match self.strategy_parameters.strategy_type { StrategyType::SpotImBalanced => { + if active_id < min_bin_id || active_id > max_bin_id { + let weights = to_weight_spot_balanced(min_bin_id, max_bin_id); + return to_amount_both_side( + active_id, + bin_step, + amount_x_in_active_bin, + amount_y_in_active_bin, + self.amount_x, + self.amount_y, + &weights, + ); + } let mut amounts_in_bin = vec![]; if min_bin_id <= active_id { let weights = to_weight_spot_balanced(min_bin_id, active_id); let amounts_into_bid_side = to_amount_bid_side(active_id, self.amount_y, &weights)?; - for &(bin_id, amount) in amounts_into_bid_side.iter() { - amounts_in_bin.push((bin_id, 0, amount)) + for &amount_in_bin in amounts_into_bid_side.iter() { + let AmountInBinSingleSide { bin_id, amount } = amount_in_bin; + amounts_in_bin.push(AmountInBin { + bin_id, + amount_x: 0, + amount_y: amount, + }) } } if active_id < max_bin_id { @@ -48,20 +107,56 @@ impl LiquidityParameterByStrategy { let amounts_into_ask_side = to_amount_ask_side(active_id, self.amount_x, bin_step, &weights)?; - for &(bin_id, amount) in amounts_into_ask_side.iter() { - amounts_in_bin.push((bin_id, amount, 0)) + for &amount_in_bin in amounts_into_ask_side.iter() { + let AmountInBinSingleSide { bin_id, amount } = amount_in_bin; + amounts_in_bin.push(AmountInBin { + bin_id, + amount_x: amount, + amount_y: 0, + }) } } Ok(amounts_in_bin) } StrategyType::CurveImBalanced => { + // ask side + if active_id < min_bin_id { + let weights = to_weight_descending_order(min_bin_id, max_bin_id); + return to_amount_both_side( + active_id, + bin_step, + amount_x_in_active_bin, + amount_y_in_active_bin, + self.amount_x, + self.amount_y, + &weights, + ); + } + // bid side + if active_id > max_bin_id { + let weights = to_weight_ascending_order(min_bin_id, max_bin_id); + return to_amount_both_side( + active_id, + bin_step, + amount_x_in_active_bin, + amount_y_in_active_bin, + self.amount_x, + self.amount_y, + &weights, + ); + } let mut amounts_in_bin = vec![]; if min_bin_id <= active_id { let weights = to_weight_ascending_order(min_bin_id, active_id); let amounts_into_bid_side = to_amount_bid_side(active_id, self.amount_y, &weights)?; - for &(bin_id, amount) in amounts_into_bid_side.iter() { - amounts_in_bin.push((bin_id, 0, amount)) + for &amount_in_bin in amounts_into_bid_side.iter() { + let AmountInBinSingleSide { bin_id, amount } = amount_in_bin; + amounts_in_bin.push(AmountInBin { + bin_id, + amount_x: 0, + amount_y: amount, + }) } } if active_id < max_bin_id { @@ -69,20 +164,56 @@ impl LiquidityParameterByStrategy { let amounts_into_ask_side = to_amount_ask_side(active_id, self.amount_x, bin_step, &weights)?; - for &(bin_id, amount) in amounts_into_ask_side.iter() { - amounts_in_bin.push((bin_id, amount, 0)) + for &amount_in_bin in amounts_into_ask_side.iter() { + let AmountInBinSingleSide { bin_id, amount } = amount_in_bin; + amounts_in_bin.push(AmountInBin { + bin_id, + amount_x: amount, + amount_y: 0, + }) } } Ok(amounts_in_bin) } StrategyType::BidAskImBalanced => { + // ask side + if active_id < min_bin_id { + let weights = to_weight_ascending_order(min_bin_id, max_bin_id); + return to_amount_both_side( + active_id, + bin_step, + amount_x_in_active_bin, + amount_y_in_active_bin, + self.amount_x, + self.amount_y, + &weights, + ); + } + // bid side + if active_id > max_bin_id { + let weights = to_weight_descending_order(min_bin_id, max_bin_id); + return to_amount_both_side( + active_id, + bin_step, + amount_x_in_active_bin, + amount_y_in_active_bin, + self.amount_x, + self.amount_y, + &weights, + ); + } let mut amounts_in_bin = vec![]; if min_bin_id <= active_id { let weights = to_weight_descending_order(min_bin_id, active_id); let amounts_into_bid_side = to_amount_bid_side(active_id, self.amount_y, &weights)?; - for &(bin_id, amount) in amounts_into_bid_side.iter() { - amounts_in_bin.push((bin_id, 0, amount)) + for &amount_in_bin in amounts_into_bid_side.iter() { + let AmountInBinSingleSide { bin_id, amount } = amount_in_bin; + amounts_in_bin.push(AmountInBin { + bin_id, + amount_x: 0, + amount_y: amount, + }) } } if active_id < max_bin_id { @@ -90,8 +221,13 @@ impl LiquidityParameterByStrategy { let amounts_into_ask_side = to_amount_ask_side(active_id, self.amount_x, bin_step, &weights)?; - for &(bin_id, amount) in amounts_into_ask_side.iter() { - amounts_in_bin.push((bin_id, amount, 0)) + for &amount_in_bin in amounts_into_ask_side.iter() { + let AmountInBinSingleSide { bin_id, amount } = amount_in_bin; + amounts_in_bin.push(AmountInBin { + bin_id, + amount_x: amount, + amount_y: 0, + }) } } Ok(amounts_in_bin) @@ -170,34 +306,34 @@ pub enum StrategyType { CurveBalanced, // bid ask both side, balanced deposit BidAskBalanced, - // spot both side, imbalanced deposit + // spot both side, imbalanced deposit, only token y is added in active bin SpotImBalanced, - // curve both side, imbalanced deposit + // curve both side, imbalanced deposit, only token y is added in active bin CurveImBalanced, - // bid ask both side, imbalanced deposit + // bid ask both side, imbalanced deposit, only token y is added in active bin BidAskImBalanced, } -pub fn to_weight_spot_balanced(min_bin_id: i32, max_bin_id: i32) -> Vec<(i32, u16)> { +pub fn to_weight_descending_order(min_bin_id: i32, max_bin_id: i32) -> Vec<(i32, u16)> { let mut weights = vec![]; for i in min_bin_id..=max_bin_id { - weights.push((i, 1)); + weights.push((i, (max_bin_id - i + 1) as u16)); } weights } -pub fn to_weight_descending_order(min_bin_id: i32, max_bin_id: i32) -> Vec<(i32, u16)> { +pub fn to_weight_ascending_order(min_bin_id: i32, max_bin_id: i32) -> Vec<(i32, u16)> { let mut weights = vec![]; for i in min_bin_id..=max_bin_id { - weights.push((i, (max_bin_id - i + 1) as u16)); + weights.push((i, (i - min_bin_id + 1) as u16)); } weights } -pub fn to_weight_ascending_order(min_bin_id: i32, max_bin_id: i32) -> Vec<(i32, u16)> { +pub fn to_weight_spot_balanced(min_bin_id: i32, max_bin_id: i32) -> Vec<(i32, u16)> { let mut weights = vec![]; for i in min_bin_id..=max_bin_id { - weights.push((i, (i - min_bin_id + 1) as u16)); + weights.push((i, 1)); } weights } @@ -207,8 +343,13 @@ pub fn to_weight_curve( max_bin_id: i32, active_id: i32, ) -> Result> { - if active_id < min_bin_id || active_id > max_bin_id { - return Err(LBError::InvalidStrategyParameters.into()); + // only bid side + if active_id > max_bin_id { + return Ok(to_weight_ascending_order(min_bin_id, max_bin_id)); + } + // only ask side + if active_id < min_bin_id { + return Ok(to_weight_descending_order(min_bin_id, max_bin_id)); } let max_weight = DEFAULT_MAX_WEIGHT; let min_weight = DEFAULT_MIN_WEIGHT; @@ -247,8 +388,13 @@ pub fn to_weight_bid_ask( max_bin_id: i32, active_id: i32, ) -> Result> { - if active_id < min_bin_id || active_id > max_bin_id { - return Err(LBError::InvalidStrategyParameters.into()); + // only bid side + if active_id > max_bin_id { + return Ok(to_weight_descending_order(min_bin_id, max_bin_id)); + } + // only ask side + if active_id < min_bin_id { + return Ok(to_weight_ascending_order(min_bin_id, max_bin_id)); } let max_weight = DEFAULT_MAX_WEIGHT; @@ -285,15 +431,8 @@ pub fn to_weight_bid_ask( } impl StrategyParameters { - pub fn validate_both_side(&self, active_id: i32) -> Result<()> { - if active_id < self.min_bin_id || active_id > self.max_bin_id { - Err(LBError::InvalidStrategyParameters.into()) - } else { - Ok(()) - } - } pub fn bin_count(&self) -> Result { - let bin_count = self.max_bin_id.safe_sub(self.min_bin_id)?; + let bin_count = self.max_bin_id.safe_sub(self.min_bin_id)?.safe_add(1)?; Ok(bin_count as usize) } } diff --git a/programs/lb_clmm/src/instructions/deposit/add_liquidity_by_strategy_one_side.rs b/programs/lb_clmm/src/instructions/deposit/add_liquidity_by_strategy_one_side.rs new file mode 100644 index 0000000..9dd9894 --- /dev/null +++ b/programs/lb_clmm/src/instructions/deposit/add_liquidity_by_strategy_one_side.rs @@ -0,0 +1,204 @@ +use super::add_liquidity_by_strategy::StrategyParameters; +use crate::errors::LBError; +use crate::math::weight_to_amounts::to_amount_ask_side; +use crate::math::weight_to_amounts::to_amount_bid_side; +use crate::math::weight_to_amounts::AmountInBinSingleSide; +use crate::to_weight_ascending_order; +use crate::to_weight_descending_order; +use crate::to_weight_spot_balanced; +use crate::validate_add_liquidity_by_strategy_params; +use crate::StrategyType; +use crate::{handle_deposit_by_amounts_one_side, ModifyLiquidityOneSide}; +use anchor_lang::prelude::*; + +#[derive(AnchorSerialize, AnchorDeserialize, Eq, PartialEq, Clone, Debug, Default)] +pub struct LiquidityParameterByStrategyOneSide { + /// Amount of X token or Y token to deposit + pub amount: u64, + /// Active bin that integrator observe off-chain + pub active_id: i32, + /// max active bin slippage allowed + pub max_active_bin_slippage: i32, + /// strategy parameters + pub strategy_parameters: StrategyParameters, +} + +impl LiquidityParameterByStrategyOneSide { + pub fn to_amounts_into_bin( + &self, + active_id: i32, + bin_step: u16, + deposit_for_y: bool, + ) -> Result> { + let min_bin_id = self.strategy_parameters.min_bin_id; + let max_bin_id = self.strategy_parameters.max_bin_id; + + let weights = match self.strategy_parameters.strategy_type { + StrategyType::SpotOneSide => Some(to_weight_spot_balanced( + self.strategy_parameters.min_bin_id, + self.strategy_parameters.max_bin_id, + )), + StrategyType::CurveOneSide => { + if deposit_for_y { + Some(to_weight_ascending_order(min_bin_id, max_bin_id)) + } else { + Some(to_weight_descending_order(min_bin_id, max_bin_id)) + } + } + StrategyType::BidAskOneSide => { + if deposit_for_y { + Some(to_weight_descending_order(min_bin_id, max_bin_id)) + } else { + Some(to_weight_ascending_order(min_bin_id, max_bin_id)) + } + } + _ => None, + } + .ok_or(LBError::InvalidStrategyParameters)?; + + if deposit_for_y { + to_amount_bid_side(active_id, self.amount, &weights) + } else { + to_amount_ask_side(active_id, self.amount, bin_step, &weights) + } + } +} + +pub fn handle<'a, 'b, 'c, 'info>( + ctx: Context<'a, 'b, 'c, 'info, ModifyLiquidityOneSide<'info>>, + liquidity_parameter: &LiquidityParameterByStrategyOneSide, +) -> Result<()> { + Ok(()) +} + +#[cfg(test)] +mod add_liquidity_by_strategy_one_side { + use super::*; + + fn new_add_liquidity_by_strategy_one_side_parameter( + amount: u64, + active_id: i32, + min_bin_id: i32, + max_bin_id: i32, + strategy_type: StrategyType, + ) -> LiquidityParameterByStrategyOneSide { + LiquidityParameterByStrategyOneSide { + amount, + active_id, + max_active_bin_slippage: i32::MAX, + strategy_parameters: StrategyParameters { + max_bin_id, + min_bin_id, + strategy_type, + parameteres: [0u8; 64], + }, + } + } + #[test] + fn test_add_liquidity_by_strategy_ask_side() { + let active_id = 100; + let bin_step = 10; + let deposit_for_y = false; + let amount = 10000; + let min_bin_id = 100; + let max_bin_id = 200; + { + let parameters = new_add_liquidity_by_strategy_one_side_parameter( + amount, + active_id, + min_bin_id, + max_bin_id, + StrategyType::SpotOneSide, + ); + let amounts_in_bin = parameters + .to_amounts_into_bin(active_id, bin_step, deposit_for_y) + .unwrap(); + println!("spot one ask side {:?}", amounts_in_bin); + } + + { + let parameters = new_add_liquidity_by_strategy_one_side_parameter( + amount, + active_id, + min_bin_id, + max_bin_id, + StrategyType::CurveOneSide, + ); + let amounts_in_bin = parameters + .to_amounts_into_bin(active_id, bin_step, deposit_for_y) + .unwrap(); + println!("currve one ask side {:?}", amounts_in_bin); + } + + { + let parameters = new_add_liquidity_by_strategy_one_side_parameter( + amount, + active_id, + min_bin_id, + max_bin_id, + StrategyType::BidAskOneSide, + ); + let amounts_in_bin = parameters + .to_amounts_into_bin(active_id, bin_step, deposit_for_y) + .unwrap(); + println!("bid/ask one ask side {:?}", amounts_in_bin); + } + } + + #[test] + fn test_add_liquidity_by_strategy_bid_side() { + let active_id = 100; + let bin_step = 10; + let deposit_for_y = true; + let amount = 10000; + let min_bin_id = 0; + let max_bin_id = 100; + { + let parameters = new_add_liquidity_by_strategy_one_side_parameter( + amount, + active_id, + min_bin_id, + max_bin_id, + StrategyType::SpotOneSide, + ); + + let amounts_in_bin = parameters + .to_amounts_into_bin(active_id, bin_step, deposit_for_y) + .unwrap(); + + println!("spot one bid side {:?}", amounts_in_bin); + } + + { + let parameters = new_add_liquidity_by_strategy_one_side_parameter( + amount, + active_id, + min_bin_id, + max_bin_id, + StrategyType::CurveOneSide, + ); + + let amounts_in_bin = parameters + .to_amounts_into_bin(active_id, bin_step, deposit_for_y) + .unwrap(); + + println!("curve one bid side{:?}", amounts_in_bin); + } + + { + let parameters = new_add_liquidity_by_strategy_one_side_parameter( + amount, + active_id, + min_bin_id, + max_bin_id, + StrategyType::BidAskOneSide, + ); + + let amounts_in_bin = parameters + .to_amounts_into_bin(active_id, bin_step, deposit_for_y) + .unwrap(); + + println!("bid/ask one bid side{:?}", amounts_in_bin); + } + } +} diff --git a/programs/lb_clmm/src/instructions/add_liquidity_by_weight.rs b/programs/lb_clmm/src/instructions/deposit/add_liquidity_by_weight.rs similarity index 67% rename from programs/lb_clmm/src/instructions/add_liquidity_by_weight.rs rename to programs/lb_clmm/src/instructions/deposit/add_liquidity_by_weight.rs index 3360364..66fa3be 100644 --- a/programs/lb_clmm/src/instructions/add_liquidity_by_weight.rs +++ b/programs/lb_clmm/src/instructions/deposit/add_liquidity_by_weight.rs @@ -1,8 +1,11 @@ -use crate::constants::MAX_BIN_PER_POSITION; use crate::errors::LBError; -use crate::math::weight_to_amounts::{to_amount_ask_side, to_amount_bid_side, to_amount_both_side}; +use crate::handle_deposit_by_amounts; +use crate::manager::bin_array_manager::BinArrayManager; +use crate::math::weight_to_amounts::{to_amount_both_side, AmountInBin}; +use crate::BinArrayAccount; use crate::ModifyLiquidity; use anchor_lang::prelude::*; +use std::collections::{BTreeMap, BTreeSet}; #[derive(AnchorSerialize, AnchorDeserialize, Eq, PartialEq, Clone, Debug, Default)] pub struct BinLiquidityDistributionByWeight { @@ -35,11 +38,6 @@ impl LiquidityParameterByWeight { let bin_count = self.bin_count(); require!(bin_count > 0, LBError::InvalidInput); - require!( - bin_count <= MAX_BIN_PER_POSITION as u32, - LBError::InvalidInput - ); - let bin_shift = if active_id > self.active_id { active_id - self.active_id } else { @@ -75,6 +73,14 @@ impl LiquidityParameterByWeight { Ok(()) } + pub fn is_include_bin_id(&self, bin_id: i32) -> bool { + for bin_dist in self.bin_liquidity_dist.iter() { + if bin_dist.bin_id == bin_id { + return true; + } + } + false + } // require bin id to be sorted before doing this pub fn to_amounts_into_bin<'a, 'info>( &'a self, @@ -82,47 +88,7 @@ impl LiquidityParameterByWeight { bin_step: u16, amount_x_in_active_bin: u64, // amount x in active bin amount_y_in_active_bin: u64, // amount y in active bin - ) -> Result> { - // only bid side - if active_id > self.bin_liquidity_dist[self.bin_liquidity_dist.len() - 1].bin_id { - let amounts = to_amount_bid_side( - active_id, - self.amount_y, - &self - .bin_liquidity_dist - .iter() - .map(|x| (x.bin_id, x.weight)) - .collect::>(), - )?; - - let amounts = amounts - .iter() - .map(|x| (x.0, 0, x.1)) - .collect::>(); - - return Ok(amounts); - } - // only ask side - if active_id < self.bin_liquidity_dist[0].bin_id { - let amounts = to_amount_ask_side( - active_id, - self.amount_x, - bin_step, - &self - .bin_liquidity_dist - .iter() - .map(|x| (x.bin_id, x.weight)) - .collect::>(), - )?; - - let amounts = amounts - .iter() - .map(|x| (x.0, x.1, 0)) - .collect::>(); - - return Ok(amounts); - } - + ) -> Result> { to_amount_both_side( active_id, bin_step, @@ -145,3 +111,29 @@ pub fn handle<'a, 'b, 'c, 'info>( ) -> Result<()> { Ok(()) } + +pub fn find_amount_in_active_bin<'a>( + lb_pair_pk: Pubkey, + active_id: i32, + remaining_accounts: &mut &[AccountInfo<'a>], +) -> Result<(u64, u64)> { + let amount_x; + let amount_y; + loop { + let bin_array_account = BinArrayAccount::try_accounts( + &crate::ID, + remaining_accounts, + &[], + &mut BTreeMap::new(), + &mut BTreeSet::new(), + )?; + let mut bin_arrays = [bin_array_account.load_and_validate(lb_pair_pk)?]; + let bin_array_manager = BinArrayManager::new(&mut bin_arrays)?; + if let Ok(bin) = bin_array_manager.get_bin(active_id) { + amount_x = bin.amount_x; + amount_y = bin.amount_y; + break; + }; + } + Ok((amount_x, amount_y)) +} diff --git a/programs/lb_clmm/src/instructions/deposit/add_liquidity_by_weight_one_side.rs b/programs/lb_clmm/src/instructions/deposit/add_liquidity_by_weight_one_side.rs new file mode 100644 index 0000000..dc3a1a9 --- /dev/null +++ b/programs/lb_clmm/src/instructions/deposit/add_liquidity_by_weight_one_side.rs @@ -0,0 +1,382 @@ +use super::add_liquidity_by_weight::BinLiquidityDistributionByWeight; +use crate::authorize_modify_position; +use crate::deposit_in_bin_id; +use crate::errors::LBError; +use crate::events::AddLiquidity as AddLiquidityEvent; +use crate::manager::bin_array_manager::BinArrayManager; +use crate::math::safe_math::SafeMath; +use crate::math::weight_to_amounts::to_amount_ask_side; +use crate::math::weight_to_amounts::to_amount_bid_side; +use crate::math::weight_to_amounts::AmountInBinSingleSide; +use crate::state::action_access::get_lb_pair_type_access_validator; +use crate::state::bin_array_bitmap_extension::BinArrayBitmapExtension; +use crate::state::dynamic_position::{DynamicPositionLoader, PositionV3}; +use crate::state::lb_pair::LbPair; +use crate::BinArrayAccount; +use anchor_lang::prelude::*; +use anchor_spl::token_2022::TransferChecked; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; +use std::collections::{BTreeMap, BTreeSet}; + +#[derive(AnchorSerialize, AnchorDeserialize, Eq, PartialEq, Clone, Debug)] +pub struct LiquidityOneSideParameter { + /// Amount of X token or Y token to deposit + pub amount: u64, + /// Active bin that integrator observe off-chain + pub active_id: i32, + /// max active bin slippage allowed + pub max_active_bin_slippage: i32, + /// Liquidity distribution to each bins + pub bin_liquidity_dist: Vec, +} + +impl LiquidityOneSideParameter { + fn bin_count(&self) -> u32 { + self.bin_liquidity_dist.len() as u32 + } + + fn validate<'a, 'info>(&'a self, active_id: i32) -> Result<()> { + require!(self.amount != 0, LBError::InvalidInput); + + let bin_count = self.bin_count(); + require!(bin_count > 0, LBError::InvalidInput); + + let bin_shift = if active_id > self.active_id { + active_id - self.active_id + } else { + self.active_id - active_id + }; + + require!( + bin_shift <= self.max_active_bin_slippage.into(), + LBError::ExceededBinSlippageTolerance + ); + + // bin dist must be in consecutive order and weight is non-zero + for (i, val) in self.bin_liquidity_dist.iter().enumerate() { + require!(val.weight != 0, LBError::InvalidInput); + // bin id must in right order + if i != 0 { + require!( + val.bin_id > self.bin_liquidity_dist[i - 1].bin_id, + LBError::InvalidInput + ); + } + } + Ok(()) + } + + // require bin id to be sorted before doing this + fn to_amounts_into_bin<'a, 'info>( + &'a self, + active_id: i32, + bin_step: u16, + deposit_for_y: bool, + ) -> Result> { + if deposit_for_y { + to_amount_bid_side( + active_id, + self.amount, + &self + .bin_liquidity_dist + .iter() + .map(|x| (x.bin_id, x.weight)) + .collect::>(), + ) + } else { + to_amount_ask_side( + active_id, + self.amount, + bin_step, + &self + .bin_liquidity_dist + .iter() + .map(|x| (x.bin_id, x.weight)) + .collect::>(), + ) + } + } +} + +#[event_cpi] +#[derive(Accounts)] +pub struct ModifyLiquidityOneSide<'info> { + #[account( + mut, + has_one = lb_pair, + constraint = authorize_modify_position(&position, sender.key())? + )] + pub position: AccountLoader<'info, PositionV3>, + + #[account(mut)] + pub lb_pair: AccountLoader<'info, LbPair>, + + #[account( + mut, + has_one = lb_pair, + )] + pub bin_array_bitmap_extension: Option>, + + #[account(mut)] + pub user_token: Box>, + + #[account(mut)] + pub reserve: Box>, + + pub token_mint: Box>, + + pub sender: Signer<'info>, + pub token_program: Interface<'info, TokenInterface>, +} + +impl<'info> ModifyLiquidityOneSide<'info> { + pub fn is_deposit_y(&self, lb_pair: &LbPair) -> bool { + self.user_token.mint == lb_pair.token_y_mint + } + // true mean deposit in token y, false mean deposit in token x + fn validate(&self, lb_pair: &LbPair, deposit_for_y: bool) -> Result<()> { + if deposit_for_y { + require!( + self.token_mint.key() == lb_pair.token_y_mint, + LBError::InvalidAccountForSingleDeposit + ); + require!( + self.user_token.mint == lb_pair.token_y_mint, + LBError::InvalidAccountForSingleDeposit + ); + require!( + self.reserve.key() == lb_pair.reserve_y, + LBError::InvalidAccountForSingleDeposit + ); + } else { + require!( + self.token_mint.key() == lb_pair.token_x_mint, + LBError::InvalidAccountForSingleDeposit + ); + require!( + self.user_token.mint == lb_pair.token_x_mint, + LBError::InvalidAccountForSingleDeposit + ); + require!( + self.reserve.key() == lb_pair.reserve_x, + LBError::InvalidAccountForSingleDeposit + ); + } + Ok(()) + } + + fn transfer_to_reserve(&self, amount: u64) -> Result<()> { + anchor_spl::token_2022::transfer_checked( + CpiContext::new( + self.token_program.to_account_info(), + TransferChecked { + from: self.user_token.to_account_info(), + to: self.reserve.to_account_info(), + authority: self.sender.to_account_info(), + mint: self.token_mint.to_account_info(), + }, + ), + amount, + self.token_mint.decimals, + ) + } +} +pub fn handle<'a, 'b, 'c, 'info>( + ctx: &Context<'a, 'b, 'c, 'info, ModifyLiquidityOneSide<'info>>, + liquidity_parameter: &LiquidityOneSideParameter, +) -> Result<()> { + let (amounts_in_bin, deposit_for_y) = { + let lb_pair = ctx.accounts.lb_pair.load()?; + let active_id = lb_pair.active_id; + let bin_step = lb_pair.bin_step; + liquidity_parameter.validate(active_id)?; + let deposit_for_y = ctx.accounts.is_deposit_y(&lb_pair); + ( + liquidity_parameter.to_amounts_into_bin(active_id, bin_step, deposit_for_y)?, + deposit_for_y, + ) + }; + + handle_deposit_by_amounts_one_side(&ctx, &amounts_in_bin, deposit_for_y) +} + +pub fn handle_deposit_by_amounts_one_side<'a, 'b, 'c, 'info>( + ctx: &Context<'a, 'b, 'c, 'info, ModifyLiquidityOneSide<'info>>, + amounts_in_bin: &Vec, // vec of bin id and amount + deposit_for_y: bool, +) -> Result<()> { + let lb_pair_pk = ctx.accounts.lb_pair.key(); + let mut lb_pair = ctx.accounts.lb_pair.load_mut()?; + { + let pair_type_access_validator = + get_lb_pair_type_access_validator(&lb_pair, Clock::get()?.slot)?; + require!( + pair_type_access_validator.validate_add_liquidity_access(ctx.accounts.sender.key()), + LBError::PoolDisabled + ); + } + + // let min_bin_id = amounts_in_bin[0].bin_id; + // let max_bin_id = amounts_in_bin[amounts_in_bin.len() - 1].bin_id; + + let active_id = lb_pair.active_id; + + ctx.accounts.validate(&lb_pair, deposit_for_y)?; + + let mut position = ctx.accounts.position.load_content_mut()?; + + // amounts_in_bin must be sorted in ascending order + let mut remaining_accounts = &ctx.remaining_accounts[..]; + let mut total_amount_x = 0; + let mut total_amount_y = 0; + + let mut amounts_in_bin_iter = amounts_in_bin.iter(); + let mut amount_in_bin = amounts_in_bin_iter.next(); + + loop { + if amount_in_bin.is_none() { + break; + } + let bin_array_account = BinArrayAccount::try_accounts( + &crate::ID, + &mut remaining_accounts, + &[], + &mut BTreeMap::new(), + &mut BTreeSet::new(), + )?; + + let mut bin_arrays = [bin_array_account.load_and_validate(lb_pair_pk)?]; + + let mut bin_array_manager = BinArrayManager::new(&mut bin_arrays)?; + bin_array_manager.validate_bin_arrays(amount_in_bin.unwrap().bin_id)?; + + let before_liquidity_flags = bin_array_manager.get_zero_liquidity_flags(); + + bin_array_manager.migrate_to_v2()?; + + // Update reward per liquidity store for active bin + bin_array_manager.update_rewards(&mut lb_pair)?; + + let (lower_bin_id, upper_bin_id) = bin_array_manager.get_lower_upper_bin_id()?; + loop { + if amount_in_bin.is_none() { + break; + } + let &AmountInBinSingleSide { amount, bin_id } = amount_in_bin.unwrap(); + + if lower_bin_id <= bin_id && bin_id <= upper_bin_id { + position.update_earning_per_token_stored(&bin_array_manager, bin_id, bin_id)?; + if amount != 0 { + let (amount_x_into_bin, amount_y_into_bin) = if deposit_for_y { + total_amount_y = total_amount_y.safe_add(amount)?; + (0, amount) + } else { + total_amount_x = total_amount_x.safe_add(amount)?; + (amount, 0) + }; + + if let Some(event) = deposit_in_bin_id( + bin_id, + amount_x_into_bin, + amount_y_into_bin, + &mut lb_pair, + &mut position, + &mut bin_array_manager, + ctx.accounts.sender.key(), + )? { + emit_cpi!(event); + }; + } + amount_in_bin = amounts_in_bin_iter.next(); + } else { + break; + } + } + + lb_pair.flip_bin_arrays( + &before_liquidity_flags, + &bin_array_manager, + &ctx.accounts.bin_array_bitmap_extension, + )?; + } + + if deposit_for_y { + require!(total_amount_y > 0, LBError::InvalidInput); + ctx.accounts.transfer_to_reserve(total_amount_y)?; + } else { + require!(total_amount_x > 0, LBError::InvalidInput); + ctx.accounts.transfer_to_reserve(total_amount_x)?; + } + + position.set_last_updated_at(Clock::get()?.unix_timestamp); + + emit_cpi!(AddLiquidityEvent { + position: ctx.accounts.position.key(), + lb_pair: ctx.accounts.lb_pair.key(), + from: ctx.accounts.sender.key(), + amounts: [total_amount_x, total_amount_y], + active_bin_id: active_id, + }); + Ok(()) +} + +#[cfg(test)] +mod add_liquidity_one_side_test { + use super::*; + + fn new_liquidity_parameter_from_dist( + amount: u64, + bin_liquidity_dist: Vec, + ) -> LiquidityOneSideParameter { + LiquidityOneSideParameter { + amount, + active_id: 0, + max_active_bin_slippage: i32::MAX, + bin_liquidity_dist, + } + } + + #[test] + fn test_simple_case_one_side() { + let amount_x = 100000; + let amount_y = 2000000; + + let bin_step = 10; + let bin_liquidity_dist = vec![ + BinLiquidityDistributionByWeight { + bin_id: 1, + weight: 20, + }, + BinLiquidityDistributionByWeight { + bin_id: 3, + weight: 10, + }, + BinLiquidityDistributionByWeight { + bin_id: 5, + weight: 10, + }, + BinLiquidityDistributionByWeight { + bin_id: 7, + weight: 10, + }, + ]; + + // bid side + let liquidity_parameter = + new_liquidity_parameter_from_dist(amount_y, bin_liquidity_dist.clone()); + + let active_id = 6; + let in_amounts = liquidity_parameter + .to_amounts_into_bin(active_id, bin_step, true) + .unwrap(); + println!("ask side {:?}", in_amounts); + + // ask side + let liquidity_parameter = new_liquidity_parameter_from_dist(amount_x, bin_liquidity_dist); + let active_id = 4; + let in_amounts = liquidity_parameter + .to_amounts_into_bin(active_id, bin_step, false) + .unwrap(); + println!("bid side {:?}", in_amounts); + } +} diff --git a/programs/lb_clmm/src/instructions/deposit/add_liquidity_single_side_precise.rs b/programs/lb_clmm/src/instructions/deposit/add_liquidity_single_side_precise.rs new file mode 100644 index 0000000..7c2d30b --- /dev/null +++ b/programs/lb_clmm/src/instructions/deposit/add_liquidity_single_side_precise.rs @@ -0,0 +1,26 @@ +use anchor_lang::prelude::*; + +use crate::{ + handle_deposit_by_amounts_one_side, + math::{safe_math::SafeMath, weight_to_amounts::AmountInBinSingleSide}, + ModifyLiquidityOneSide, +}; + +#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] +pub struct AddLiquiditySingleSidePreciseParameter { + pub bins: Vec, + pub decompress_multiplier: u64, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] +pub struct CompressedBinDepositAmount { + pub bin_id: i32, + pub amount: u32, +} + +pub fn handle<'a, 'b, 'c, 'info>( + ctx: Context<'a, 'b, 'c, 'info, ModifyLiquidityOneSide<'info>>, + parameter: AddLiquiditySingleSidePreciseParameter, +) -> Result<()> { + Ok(()) +} diff --git a/programs/lb_clmm/src/instructions/deposit/mod.rs b/programs/lb_clmm/src/instructions/deposit/mod.rs new file mode 100644 index 0000000..e5c16ff --- /dev/null +++ b/programs/lb_clmm/src/instructions/deposit/mod.rs @@ -0,0 +1,6 @@ +pub mod add_liquidity; +pub mod add_liquidity_by_strategy; +pub mod add_liquidity_by_strategy_one_side; +pub mod add_liquidity_by_weight; +pub mod add_liquidity_by_weight_one_side; +pub mod add_liquidity_single_side_precise; diff --git a/programs/lb_clmm/src/instructions/fund_reward.rs b/programs/lb_clmm/src/instructions/fund_reward.rs index 0f58b2d..9c94d92 100644 --- a/programs/lb_clmm/src/instructions/fund_reward.rs +++ b/programs/lb_clmm/src/instructions/fund_reward.rs @@ -1,6 +1,13 @@ +use crate::constants::NUM_REWARDS; +use crate::errors::LBError; +use crate::events::FundReward as FundRewardEvent; +use crate::manager::bin_array_manager::BinArrayManager; +use crate::math::safe_math::SafeMath; +use crate::math::u64x64_math::SCALE_OFFSET; use crate::state::{bin::BinArray, lb_pair::LbPair}; use anchor_lang::prelude::*; -use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface, TransferChecked}; +use ruint::aliases::U256; #[event_cpi] #[derive(Accounts)] @@ -26,6 +33,42 @@ pub struct FundReward<'info> { pub token_program: Interface<'info, TokenInterface>, } +impl<'info> FundReward<'info> { + fn validate(&self, reward_index: usize) -> Result<()> { + let lb_pair = self.lb_pair.load()?; + require!(reward_index < NUM_REWARDS, LBError::InvalidRewardIndex); + + let reward_info = &lb_pair.reward_infos[reward_index]; + require!(reward_info.initialized(), LBError::RewardUninitialized); + require!( + reward_info.vault.eq(&self.reward_vault.key()), + LBError::InvalidRewardVault + ); + require!( + reward_info.is_valid_funder(self.funder.key()), + LBError::InvalidAdmin + ); + + Ok(()) + } + + fn transfer_from_funder_to_vault(&self, amount: u64) -> Result<()> { + anchor_spl::token_2022::transfer_checked( + CpiContext::new( + self.token_program.to_account_info(), + TransferChecked { + from: self.funder_token_account.to_account_info(), + to: self.reward_vault.to_account_info(), + authority: self.funder.to_account_info(), + mint: self.reward_mint.to_account_info(), + }, + ), + amount, + self.reward_mint.decimals, + ) + } +} + pub fn handle( ctx: Context, index: u64, diff --git a/programs/lb_clmm/src/instructions/go_to_a_bin.rs b/programs/lb_clmm/src/instructions/go_to_a_bin.rs new file mode 100644 index 0000000..fe303a2 --- /dev/null +++ b/programs/lb_clmm/src/instructions/go_to_a_bin.rs @@ -0,0 +1,31 @@ +use crate::errors::LBError; +use crate::events::GoToABin as GoToABinEvent; +use crate::math::safe_math::SafeMath; +use crate::state::bin::BinArray; +use crate::state::bin_array_bitmap_extension::BinArrayBitmapExtension; +use crate::state::lb_pair::LbPair; +use anchor_lang::prelude::*; +/// this endpoint allows user to go from current lb.active_id to a bin id x, if there is no liquidity between lb.active_id and bin id x +#[event_cpi] +#[derive(Accounts)] +pub struct GoToABin<'info> { + #[account(mut)] + pub lb_pair: AccountLoader<'info, LbPair>, + + #[account( + has_one = lb_pair, + )] + pub bin_array_bitmap_extension: Option>, + #[account( + has_one = lb_pair, + )] + pub from_bin_array: Option>, // binArray includes current lb_pair.active_id + #[account( + has_one = lb_pair, + )] + pub to_bin_array: Option>, // binArray includes bin_id +} + +pub fn handle(ctx: Context, bin_id: i32) -> Result<()> { + Ok(()) +} diff --git a/programs/lb_clmm/src/instructions/increase_oracle_length.rs b/programs/lb_clmm/src/instructions/increase_oracle_length.rs index 2f1880b..6427812 100644 --- a/programs/lb_clmm/src/instructions/increase_oracle_length.rs +++ b/programs/lb_clmm/src/instructions/increase_oracle_length.rs @@ -1,4 +1,7 @@ -use crate::state::oracle::Oracle; +use crate::{ + events::IncreaseObservation, + state::oracle::{Oracle, OracleContentLoader}, +}; use anchor_lang::prelude::*; #[event_cpi] diff --git a/programs/lb_clmm/src/instructions/increase_position_length.rs b/programs/lb_clmm/src/instructions/increase_position_length.rs new file mode 100644 index 0000000..3579837 --- /dev/null +++ b/programs/lb_clmm/src/instructions/increase_position_length.rs @@ -0,0 +1,37 @@ +use crate::constants::MAX_RESIZE_LENGTH; +use crate::constants::POSITION_MAX_LENGTH; +use crate::errors::LBError; +use crate::events::IncreasePositionLength as IncreasePositionLengthEvent; +use crate::math::safe_math::SafeMath; +use crate::state::dynamic_position::DynamicPositionLoader; +use crate::state::dynamic_position::PositionV3; +use crate::state::dynamic_position::ResizeSide; +use crate::state::LbPair; +use anchor_lang::prelude::*; + +#[event_cpi] +#[derive(Accounts)] +#[instruction(length_to_add: u16, side: u8)] +pub struct IncreasePositionLength<'info> { + #[account(mut)] + pub funder: Signer<'info>, + + pub lb_pair: AccountLoader<'info, LbPair>, + + #[account( + mut, + has_one = owner, + has_one = lb_pair, + realloc = PositionV3::new_space_after_add(length_to_add.into(), &position)?, + realloc::payer = funder, + realloc::zero = true, + )] + pub position: AccountLoader<'info, PositionV3>, + pub owner: Signer<'info>, + pub system_program: Program<'info, System>, +} + +// side: 0 lower side, and 1 upper side +pub fn handle(ctx: Context, length_to_add: u16, side: u8) -> Result<()> { + Ok(()) +} diff --git a/programs/lb_clmm/src/instructions/initialize_bin_array.rs b/programs/lb_clmm/src/instructions/initialize_bin_array.rs index 4dc72cf..ea868de 100644 --- a/programs/lb_clmm/src/instructions/initialize_bin_array.rs +++ b/programs/lb_clmm/src/instructions/initialize_bin_array.rs @@ -1,3 +1,5 @@ +use crate::errors::LBError; +use crate::state::action_access::get_lb_pair_type_access_validator; use crate::state::{bin::BinArray, lb_pair::LbPair}; use crate::utils::seeds::BIN_ARRAY; use anchor_lang::prelude::*; diff --git a/programs/lb_clmm/src/instructions/initialize_lb_pair.rs b/programs/lb_clmm/src/instructions/initialize_lb_pair.rs index 05e910f..8d7ae7f 100644 --- a/programs/lb_clmm/src/instructions/initialize_lb_pair.rs +++ b/programs/lb_clmm/src/instructions/initialize_lb_pair.rs @@ -1,9 +1,14 @@ use crate::constants::DEFAULT_OBSERVATION_LENGTH; use crate::errors::LBError; +use crate::events::LbPairCreate; +use crate::state::bin::BinArray; use crate::state::bin_array_bitmap_extension::BinArrayBitmapExtension; use crate::state::lb_pair::LbPair; +use crate::state::lb_pair::PairType; use crate::state::oracle::Oracle; +use crate::state::oracle::OracleContentLoader; use crate::state::preset_parameters::PresetParameter; +use crate::state::PairStatus; use crate::utils::seeds::BIN_ARRAY_BITMAP_SEED; use crate::utils::seeds::ORACLE; use anchor_lang::prelude::*; @@ -98,3 +103,77 @@ pub struct InitializeLbPair<'info> { pub fn handle(ctx: Context, active_id: i32, bin_step: u16) -> Result<()> { Ok(()) } + +pub struct InitializePairKeys { + pub token_mint_x: Pubkey, + pub token_mint_y: Pubkey, + pub reserve_x: Pubkey, + pub reserve_y: Pubkey, + pub base: Pubkey, + pub creator: Pubkey, +} + +pub struct InitializePairAccounts<'a, 'info> { + pub lb_pair: &'a AccountLoader<'info, LbPair>, + pub oracle: &'a AccountLoader<'info, Oracle>, + pub bin_array_bitmap_extension: Option<&'a AccountLoader<'info, BinArrayBitmapExtension>>, +} + +pub fn handle_initialize_pair<'a, 'info>( + accounts: InitializePairAccounts<'a, 'info>, + keys: InitializePairKeys, + active_id: i32, + preset_parameter: PresetParameter, + pair_type: PairType, + bump: u8, + lock_duration_in_slot: u64, +) -> Result<()> { + // Initialization of preset parameter already ensure the min, and max bin id bound + require!( + active_id >= preset_parameter.min_bin_id && active_id <= preset_parameter.max_bin_id, + LBError::InvalidBinId + ); + + let mut lb_pair = accounts.lb_pair.load_init()?; + + lb_pair.initialize( + bump, + active_id, + preset_parameter.bin_step, + keys.token_mint_x, + keys.token_mint_y, + keys.reserve_x, + keys.reserve_y, + accounts.oracle.key(), + preset_parameter.to_static_parameters(), + pair_type, + PairStatus::Enabled.into(), + keys.base, + lock_duration_in_slot, + keys.creator, + )?; + + // Extra safety check on preset_parameter won't overflow in edge case. Revert pair creation if overflow. + lb_pair.compute_composition_fee(u64::MAX)?; + lb_pair.compute_fee(u64::MAX)?; + lb_pair.compute_fee_from_amount(u64::MAX)?; + lb_pair.compute_protocol_fee(u64::MAX)?; + + let mut dynamic_oracle = accounts.oracle.load_content_init()?; + dynamic_oracle.metadata.init(); + + let bin_array_index = BinArray::bin_id_to_bin_array_index(active_id)?; + + if lb_pair.is_overflow_default_bin_array_bitmap(bin_array_index) { + // init bit array bitmap extension + require!( + accounts.bin_array_bitmap_extension.is_some(), + LBError::BitmapExtensionAccountIsNotProvided + ); + + if let Some(bitmap_ext) = accounts.bin_array_bitmap_extension { + bitmap_ext.load_init()?.initialize(accounts.lb_pair.key()); + } + } + Ok(()) +} diff --git a/programs/lb_clmm/src/instructions/initialize_permission_lb_pair.rs b/programs/lb_clmm/src/instructions/initialize_permission_lb_pair.rs index 7a39e82..7b3f70e 100644 --- a/programs/lb_clmm/src/instructions/initialize_permission_lb_pair.rs +++ b/programs/lb_clmm/src/instructions/initialize_permission_lb_pair.rs @@ -2,10 +2,14 @@ use crate::assert_eq_launch_pool_admin; use crate::constants::DEFAULT_OBSERVATION_LENGTH; use crate::errors::LBError; use crate::events::LbPairCreate; +use crate::instructions::initialize_lb_pair::handle_initialize_pair; +use crate::instructions::initialize_lb_pair::InitializePairAccounts; +use crate::instructions::initialize_lb_pair::InitializePairKeys; use crate::state::bin_array_bitmap_extension::BinArrayBitmapExtension; use crate::state::lb_pair::LbPair; use crate::state::lb_pair::PairType; use crate::state::oracle::Oracle; +use crate::state::preset_parameters::validate_min_max_bin_id; use crate::state::preset_parameters::PresetParameter; use crate::utils::seeds::BIN_ARRAY_BITMAP_SEED; use crate::utils::seeds::ORACLE; @@ -102,6 +106,7 @@ pub struct InitializePermissionLbPair<'info> { )] pub admin: Signer<'info>, + // #[account(address = Token2022::id())] pub token_program: Interface<'info, TokenInterface>, pub system_program: Program<'info, System>, pub rent: Sysvar<'info, Rent>, @@ -113,3 +118,73 @@ pub fn handle( ) -> Result<()> { Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::state::PairStatus; + use num_traits::Zero; + use proptest::proptest; + + proptest! { + #[test] + fn test_preset_parameter_without_variable_fee_configuration( + bin_step in 1..=10000_u16, + active_id in i32::MIN..=i32::MAX, + current_timestamp in 0_i64..=(u16::MAX as i64), + bin_swapped in 0..=(u16::MAX as i32), + swap_direction in 0..=1, + seconds_elapsed in 0_i64..=(u16::MAX as i64) + ) { + let preset_parameter = PresetParameter { + bin_step, + base_factor: 10000, + min_bin_id: i32::MIN, + max_bin_id: i32::MAX, + ..Default::default() + }; + + let mut lb_pair = LbPair::default(); + assert!(lb_pair + .initialize( + 0, + active_id, + bin_step, + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + preset_parameter.to_static_parameters(), + PairType::Permission, + PairStatus::Enabled.into(), + Pubkey::new_unique(), + 0, + Pubkey::new_unique() + ).is_ok()); + + assert!(lb_pair.update_references(current_timestamp).is_ok()); + + let variable_fee_rate = lb_pair.get_variable_fee(); + assert!(variable_fee_rate.is_ok()); + // No variable fee rate + assert!(variable_fee_rate.unwrap().is_zero()); + + let delta = if swap_direction == 0 { + -bin_swapped + } else { + bin_swapped + }; + + lb_pair.active_id += delta; + assert!(lb_pair.update_volatility_accumulator().is_ok()); + + assert!(lb_pair.update_references(current_timestamp + seconds_elapsed).is_ok()); + + let variable_fee_rate = lb_pair.get_variable_fee(); + assert!(variable_fee_rate.is_ok()); + // No variable fee rate + assert!(variable_fee_rate.unwrap().is_zero()); + } + } +} diff --git a/programs/lb_clmm/src/instructions/initialize_position.rs b/programs/lb_clmm/src/instructions/initialize_position.rs index 856a98c..36264ac 100644 --- a/programs/lb_clmm/src/instructions/initialize_position.rs +++ b/programs/lb_clmm/src/instructions/initialize_position.rs @@ -1,9 +1,17 @@ use anchor_lang::prelude::*; -use crate::state::{lb_pair::LbPair, position::PositionV2}; +use crate::constants::DEFAULT_BIN_PER_POSITION; +use crate::state::dynamic_position::PositionV3; +use crate::{ + errors::LBError, + events::PositionCreate, + math::safe_math::SafeMath, + state::{action_access::get_lb_pair_type_access_validator, lb_pair::LbPair}, +}; #[event_cpi] #[derive(Accounts)] +#[instruction(lower_bin_id: i32, width: i32)] pub struct InitializePosition<'info> { #[account(mut)] pub payer: Signer<'info>, @@ -11,9 +19,9 @@ pub struct InitializePosition<'info> { #[account( init, payer = payer, - space = 8 + PositionV2::INIT_SPACE, + space = PositionV3::space(width as usize), )] - pub position: AccountLoader<'info, PositionV2>, + pub position: AccountLoader<'info, PositionV3>, pub lb_pair: AccountLoader<'info, LbPair>, @@ -26,3 +34,20 @@ pub struct InitializePosition<'info> { pub fn handle(ctx: Context, lower_bin_id: i32, width: i32) -> Result<()> { Ok(()) } + +pub struct InitializePositionAccounts<'a, 'info> { + pub lb_pair_account: &'a AccountLoader<'info, LbPair>, + pub position_account: &'a AccountLoader<'info, PositionV3>, +} + +pub fn handle_initialize_position<'a, 'info>( + accounts: InitializePositionAccounts<'a, 'info>, + lower_bin_id: i32, + width: i32, + owner: Pubkey, + operator: Pubkey, + creator: Pubkey, + fee_owner: Pubkey, +) -> Result<()> { + Ok(()) +} diff --git a/programs/lb_clmm/src/instructions/initialize_position_by_operator.rs b/programs/lb_clmm/src/instructions/initialize_position_by_operator.rs index ad9e471..233994c 100644 --- a/programs/lb_clmm/src/instructions/initialize_position_by_operator.rs +++ b/programs/lb_clmm/src/instructions/initialize_position_by_operator.rs @@ -1,6 +1,13 @@ -use crate::state::position::PositionV2; -use crate::state::{lb_pair::LbPair}; +#[cfg(feature = "alpha-access")] +use super::validate_alpha_access; +use crate::errors::LBError; +use crate::events::PositionCreate; +use crate::handle_initialize_position; +use crate::state::action_access::get_lb_pair_type_access_validator; +use crate::state::dynamic_position::PositionV3; +use crate::state::lb_pair::LbPair; use crate::utils::seeds; +use crate::InitializePositionAccounts; use anchor_lang::prelude::*; #[event_cpi] @@ -12,7 +19,7 @@ pub struct InitializePositionByOperator<'info> { pub base: Signer<'info>, #[account( - init, + init, seeds = [ seeds::POSITION.as_ref(), lb_pair.key().as_ref(), @@ -22,13 +29,13 @@ pub struct InitializePositionByOperator<'info> { ], bump, payer = payer, - space = 8 + PositionV2::INIT_SPACE, + space = PositionV3::space(width as usize), )] - pub position: AccountLoader<'info, PositionV2>, + pub position: AccountLoader<'info, PositionV3>, pub lb_pair: AccountLoader<'info, LbPair>, - /// operator + /// operator pub operator: Signer<'info>, pub system_program: Program<'info, System>, @@ -36,11 +43,14 @@ pub struct InitializePositionByOperator<'info> { pub rent: Sysvar<'info, Rent>, } - - /// There is scenario that operator create and deposit position with non-valid owner /// Then fund will be lost forever, so only whitelisted operators are able to perform this action -pub fn handle(ctx: Context, lower_bin_id: i32, width: i32, owner: Pubkey, fee_owner: Pubkey) -> Result<()> { +pub fn handle( + ctx: Context, + lower_bin_id: i32, + width: i32, + owner: Pubkey, + fee_owner: Pubkey, +) -> Result<()> { Ok(()) } - diff --git a/programs/lb_clmm/src/instructions/initialize_position_pda.rs b/programs/lb_clmm/src/instructions/initialize_position_pda.rs index c07b234..cbaea40 100644 --- a/programs/lb_clmm/src/instructions/initialize_position_pda.rs +++ b/programs/lb_clmm/src/instructions/initialize_position_pda.rs @@ -1,6 +1,9 @@ -use crate::state::position::PositionV2; +use crate::events::PositionCreate; +use crate::handle_initialize_position; +use crate::state::dynamic_position::PositionV3; use crate::state::lb_pair::LbPair; use crate::utils::seeds; +use crate::InitializePositionAccounts; use anchor_lang::prelude::*; #[event_cpi] @@ -12,7 +15,7 @@ pub struct InitializePositionPda<'info> { pub base: Signer<'info>, #[account( - init, + init, seeds = [ seeds::POSITION.as_ref(), lb_pair.key().as_ref(), @@ -22,13 +25,13 @@ pub struct InitializePositionPda<'info> { ], bump, payer = payer, - space = 8 + PositionV2::INIT_SPACE, + space = PositionV3::space(width as usize), )] - pub position: AccountLoader<'info, PositionV2>, + pub position: AccountLoader<'info, PositionV3>, pub lb_pair: AccountLoader<'info, LbPair>, - /// owner + /// owner pub owner: Signer<'info>, pub system_program: Program<'info, System>, diff --git a/programs/lb_clmm/src/instructions/initialize_preset_parameters.rs b/programs/lb_clmm/src/instructions/initialize_preset_parameters.rs index c0b1a83..81ad7b8 100644 --- a/programs/lb_clmm/src/instructions/initialize_preset_parameters.rs +++ b/programs/lb_clmm/src/instructions/initialize_preset_parameters.rs @@ -28,6 +28,32 @@ pub struct InitPresetParametersIx { pub protocol_share: u16, } +#[derive(Accounts)] +#[instruction(ix: InitPresetParametersIx)] +pub struct InitializePresetParameterV2<'info> { + #[account( + init, + seeds = [ + PRESET_PARAMETER, + &ix.bin_step.to_le_bytes(), + &ix.base_factor.to_le_bytes(), + ], + bump, + payer = admin, + space = 8 + PresetParameter::INIT_SPACE + )] + pub preset_parameter: Account<'info, PresetParameter>, + + #[account( + mut, + constraint = assert_eq_admin(admin.key()) @ LBError::InvalidAdmin + )] + pub admin: Signer<'info>, + + pub system_program: Program<'info, System>, + pub rent: Sysvar<'info, Rent>, +} + #[derive(Accounts)] #[instruction(ix: InitPresetParametersIx)] pub struct InitializePresetParameter<'info> { @@ -54,6 +80,39 @@ pub struct InitializePresetParameter<'info> { pub rent: Sysvar<'info, Rent>, } -pub fn handle(ctx: Context, ix: InitPresetParametersIx) -> Result<()> { +pub fn handle_v2( + ctx: Context, + ix: InitPresetParametersIx, +) -> Result<()> { + Ok(()) +} + +pub fn handle_v1( + ctx: Context, + ix: InitPresetParametersIx, +) -> Result<()> { + Ok(()) +} + +#[inline(never)] +fn create_preset_parameters( + preset_parameter: &mut Account<'_, PresetParameter>, + ix: InitPresetParametersIx, +) -> Result<()> { + preset_parameter.init( + ix.bin_step, + ix.base_factor, + ix.filter_period, + ix.decay_period, + ix.reduction_factor, + ix.variable_fee_control, + ix.max_volatility_accumulator, + ix.min_bin_id, + ix.max_bin_id, + ix.protocol_share, + ); + + preset_parameter.validate()?; + Ok(()) } diff --git a/programs/lb_clmm/src/instructions/initialize_reward.rs b/programs/lb_clmm/src/instructions/initialize_reward.rs index f577db3..6bdb181 100644 --- a/programs/lb_clmm/src/instructions/initialize_reward.rs +++ b/programs/lb_clmm/src/instructions/initialize_reward.rs @@ -1,5 +1,7 @@ use crate::assert_eq_admin; +use crate::constants::{MAX_REWARD_DURATION, MIN_REWARD_DURATION, NUM_REWARDS}; use crate::errors::LBError; +use crate::events::InitializeReward as InitializeRewardEvent; use crate::state::lb_pair::LbPair; use anchor_lang::prelude::*; use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; @@ -37,6 +39,20 @@ pub struct InitializeReward<'info> { pub rent: Sysvar<'info, Rent>, } +impl<'info> InitializeReward<'info> { + fn validate(&self, reward_index: usize, reward_duration: u64) -> Result<()> { + let lb_pair = self.lb_pair.load()?; + require!(reward_index < NUM_REWARDS, LBError::InvalidRewardIndex); + require!( + reward_duration >= MIN_REWARD_DURATION && reward_duration <= MAX_REWARD_DURATION, + LBError::InvalidRewardDuration + ); + let reward_info = &lb_pair.reward_infos[reward_index]; + require!(!reward_info.initialized(), LBError::RewardInitialized); + Ok(()) + } +} + pub fn handle( ctx: Context, index: u64, diff --git a/programs/lb_clmm/src/instructions/migrate_bin_array.rs b/programs/lb_clmm/src/instructions/migrate_bin_array.rs index cd5bb75..12333e6 100644 --- a/programs/lb_clmm/src/instructions/migrate_bin_array.rs +++ b/programs/lb_clmm/src/instructions/migrate_bin_array.rs @@ -1,6 +1,9 @@ -use crate::state::lb_pair::LbPair; use anchor_lang::prelude::*; +use crate::state::lb_pair::LbPair; +use crate::BinArrayAccount; +use std::collections::{BTreeMap, BTreeSet}; + #[derive(Accounts)] pub struct MigrateBinArray<'info> { pub lb_pair: AccountLoader<'info, LbPair>, diff --git a/programs/lb_clmm/src/instructions/migrate_position.rs b/programs/lb_clmm/src/instructions/migrate_position_from_v1.rs similarity index 64% rename from programs/lb_clmm/src/instructions/migrate_position.rs rename to programs/lb_clmm/src/instructions/migrate_position_from_v1.rs index d6315ac..6809bd5 100644 --- a/programs/lb_clmm/src/instructions/migrate_position.rs +++ b/programs/lb_clmm/src/instructions/migrate_position_from_v1.rs @@ -1,20 +1,21 @@ use anchor_lang::prelude::*; -use crate::state::{ - bin::BinArray, - lb_pair::LbPair, - position::{Position, PositionV2}, +use crate::events::{PositionClose, PositionCreate}; +use crate::state::dynamic_position::{DynamicPositionLoader, PositionV3}; +use crate::{ + manager::bin_array_manager::BinArrayManager, + state::{bin::BinArray, lb_pair::LbPair, position::Position}, }; #[event_cpi] #[derive(Accounts)] -pub struct MigratePosition<'info> { +pub struct MigratePositionFromV1<'info> { #[account( init, payer = owner, - space = 8 + PositionV2::INIT_SPACE, + space = PositionV3::space(position_v1.load()?.width()), )] - pub position_v2: AccountLoader<'info, PositionV2>, + pub position_v3: AccountLoader<'info, PositionV3>, // TODO do we need to check whether it is pda? #[account( @@ -48,6 +49,6 @@ pub struct MigratePosition<'info> { pub rent_receiver: UncheckedAccount<'info>, } -pub fn handle(ctx: Context) -> Result<()> { +pub fn handle(ctx: Context) -> Result<()> { Ok(()) } diff --git a/programs/lb_clmm/src/instructions/migrate_position_from_v2.rs b/programs/lb_clmm/src/instructions/migrate_position_from_v2.rs new file mode 100644 index 0000000..e1d47a0 --- /dev/null +++ b/programs/lb_clmm/src/instructions/migrate_position_from_v2.rs @@ -0,0 +1,53 @@ +use crate::events::{PositionClose, PositionCreate}; +use crate::state::dynamic_position::{DynamicPositionLoader, PositionV3}; +use crate::{ + manager::bin_array_manager::BinArrayManager, + state::{bin::BinArray, lb_pair::LbPair, position::PositionV2}, +}; +use anchor_lang::prelude::*; + +#[event_cpi] +#[derive(Accounts)] +pub struct MigratePositionFromV2<'info> { + #[account( + init, + payer = owner, + space = PositionV3::space(position_v2.load()?.width()), + )] + pub position_v3: AccountLoader<'info, PositionV3>, + + // TODO do we need to check whether it is pda? + #[account( + mut, + has_one = owner, + has_one = lb_pair, + close = rent_receiver + )] + pub position_v2: AccountLoader<'info, PositionV2>, + + pub lb_pair: AccountLoader<'info, LbPair>, + + #[account( + mut, + has_one = lb_pair + )] + pub bin_array_lower: AccountLoader<'info, BinArray>, + #[account( + mut, + has_one = lb_pair + )] + pub bin_array_upper: AccountLoader<'info, BinArray>, + + #[account(mut)] + pub owner: Signer<'info>, + + pub system_program: Program<'info, System>, + + /// CHECK: Account to receive closed account rental SOL + #[account(mut)] + pub rent_receiver: UncheckedAccount<'info>, +} + +pub fn handle(ctx: Context) -> Result<()> { + Ok(()) +} diff --git a/programs/lb_clmm/src/instructions/mod.rs b/programs/lb_clmm/src/instructions/mod.rs index 4f1f937..0c32694 100644 --- a/programs/lb_clmm/src/instructions/mod.rs +++ b/programs/lb_clmm/src/instructions/mod.rs @@ -1,14 +1,13 @@ -pub mod add_liquidity; -pub mod add_liquidity_by_strategy; -pub mod add_liquidity_by_strategy_one_side; -pub mod add_liquidity_by_weight; -pub mod add_liquidity_by_weight_one_side; pub mod claim_fee; pub mod claim_reward; pub mod close_position; pub mod close_preset_parameter; +pub mod decrease_position_length; +mod deposit; pub mod fund_reward; +pub mod go_to_a_bin; pub mod increase_oracle_length; +pub mod increase_position_length; pub mod initialize_bin_array; pub mod initialize_bin_array_bitmap_extension; pub mod initialize_lb_pair; @@ -19,10 +18,12 @@ pub mod initialize_position_pda; pub mod initialize_preset_parameters; pub mod initialize_reward; pub mod migrate_bin_array; -pub mod migrate_position; +pub mod migrate_position_from_v1; +pub mod migrate_position_from_v2; pub mod position_authorize; pub mod remove_all_liquidity; pub mod remove_liquidity; +pub mod remove_liquidity_by_range; pub mod set_activation_slot; pub mod set_lock_release_slot; pub mod set_max_swapped_amount; @@ -37,3 +38,4 @@ pub mod update_reward_funder; pub mod update_whitelisted_wallet; pub mod withdraw_ineligible_reward; pub mod withdraw_protocol_fee; +pub use deposit::*; diff --git a/programs/lb_clmm/src/instructions/position_authorize.rs b/programs/lb_clmm/src/instructions/position_authorize.rs index 5af4802..e85d1ef 100644 --- a/programs/lb_clmm/src/instructions/position_authorize.rs +++ b/programs/lb_clmm/src/instructions/position_authorize.rs @@ -1,8 +1,8 @@ -use crate::{assert_eq_launch_pool_admin, state::position::PositionV2}; +use crate::assert_eq_launch_pool_admin; +use crate::state::dynamic_position::PositionV3; use anchor_lang::prelude::*; - pub fn authorize_modify_position<'info>( - position: &AccountLoader<'info, PositionV2>, + position: &AccountLoader<'info, PositionV3>, sender: Pubkey, ) -> Result { let position = position.load()?; @@ -10,7 +10,7 @@ pub fn authorize_modify_position<'info>( } pub fn authorize_claim_fee_position<'info>( - position: &AccountLoader<'info, PositionV2>, + position: &AccountLoader<'info, PositionV3>, sender: Pubkey, ) -> Result { let position = position.load()?; diff --git a/programs/lb_clmm/src/instructions/remove_all_liquidity.rs b/programs/lb_clmm/src/instructions/remove_all_liquidity.rs index 05b8549..6598881 100644 --- a/programs/lb_clmm/src/instructions/remove_all_liquidity.rs +++ b/programs/lb_clmm/src/instructions/remove_all_liquidity.rs @@ -1,9 +1,19 @@ -use super::add_liquidity::ModifyLiquidity; - +use super::deposit::add_liquidity::ModifyLiquidity; +use crate::errors::LBError; +use crate::instructions::remove_liquidity::RemoveLiquidity; +use crate::manager::bin_array_manager::BinArrayManager; +use crate::state::bin::BinArray; +use crate::state::dynamic_position::DynamicPositionLoader; +use crate::BinArrayAccount; +use crate::PositionLiquidityFlowValidator; +use crate::{events::RemoveLiquidity as RemoveLiquidityEvent, math::safe_math::SafeMath}; use anchor_lang::prelude::*; +use std::collections::{BTreeMap, BTreeSet}; pub fn handle<'a, 'b, 'c, 'info>( ctx: Context<'a, 'b, 'c, 'info, ModifyLiquidity<'info>>, + min_bin_id: i32, + max_bin_id: i32, ) -> Result<()> { Ok(()) } diff --git a/programs/lb_clmm/src/instructions/remove_liquidity.rs b/programs/lb_clmm/src/instructions/remove_liquidity.rs index 1df3d19..8341ba7 100644 --- a/programs/lb_clmm/src/instructions/remove_liquidity.rs +++ b/programs/lb_clmm/src/instructions/remove_liquidity.rs @@ -1,15 +1,91 @@ -use super::add_liquidity::ModifyLiquidity; +use super::deposit::add_liquidity::ModifyLiquidity; use crate::constants::BASIS_POINT_MAX; -use crate::{errors::LBError, math::safe_math::SafeMath, state::position::PositionV2}; +use crate::manager::bin_array_manager::BinArrayManager; +use crate::state::dynamic_position::DynamicPosition; +use crate::state::dynamic_position::DynamicPositionLoader; +use crate::BinArrayAccount; +use crate::PositionLiquidityFlowValidator; +use crate::{ + errors::LBError, events::RemoveLiquidity as RemoveLiquidityEvent, math::safe_math::SafeMath, +}; use anchor_lang::prelude::*; +use anchor_spl::token_2022::TransferChecked; use ruint::aliases::U256; +use std::collections::{BTreeMap, BTreeSet}; #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] pub struct BinLiquidityReduction { pub bin_id: i32, pub bps_to_remove: u16, } -pub fn calculate_shares_to_remove(bps: u16, bin_id: i32, position: &PositionV2) -> Result { +impl<'a, 'b, 'c, 'info> PositionLiquidityFlowValidator for ModifyLiquidity<'info> { + fn validate_outflow_to_ata_of_position_owner(&self, owner: Pubkey) -> Result<()> { + let dest_token_x = anchor_spl::associated_token::get_associated_token_address( + &owner, + &self.token_x_mint.key(), + ); + require!( + dest_token_x == self.user_token_x.key() && self.user_token_x.owner == owner, + LBError::WithdrawToWrongTokenAccount + ); + let dest_token_y = anchor_spl::associated_token::get_associated_token_address( + &owner, + &self.token_y_mint.key(), + ); + require!( + dest_token_y == self.user_token_y.key() && self.user_token_y.owner == owner, + LBError::WithdrawToWrongTokenAccount + ); + Ok(()) + } +} + +pub trait RemoveLiquidity { + fn transfer_to_user(&self, amount_x: u64, amount_y: u64) -> Result<()>; +} + +impl<'a, 'b, 'c, 'info> RemoveLiquidity for Context<'a, 'b, 'c, 'info, ModifyLiquidity<'info>> { + fn transfer_to_user(&self, amount_x: u64, amount_y: u64) -> Result<()> { + let lb_pair = self.accounts.lb_pair.load()?; + let signer_seeds = &[&lb_pair.seeds()?[..]]; + + anchor_spl::token_2022::transfer_checked( + CpiContext::new_with_signer( + self.accounts.token_x_program.to_account_info(), + TransferChecked { + from: self.accounts.reserve_x.to_account_info(), + to: self.accounts.user_token_x.to_account_info(), + authority: self.accounts.lb_pair.to_account_info(), + mint: self.accounts.token_x_mint.to_account_info(), + }, + signer_seeds, + ), + amount_x, + self.accounts.token_x_mint.decimals, + )?; + + anchor_spl::token_2022::transfer_checked( + CpiContext::new_with_signer( + self.accounts.token_y_program.to_account_info(), + TransferChecked { + from: self.accounts.reserve_y.to_account_info(), + to: self.accounts.user_token_y.to_account_info(), + authority: self.accounts.lb_pair.to_account_info(), + mint: self.accounts.token_y_mint.to_account_info(), + }, + signer_seeds, + ), + amount_y, + self.accounts.token_y_mint.decimals, + ) + } +} + +pub fn calculate_shares_to_remove( + bps: u16, + bin_id: i32, + position: &DynamicPosition, +) -> Result { let share_in_bin = U256::from(position.get_liquidity_share_in_bin(bin_id)?); let share_to_remove: u128 = U256::from(bps) @@ -26,3 +102,50 @@ pub fn handle<'a, 'b, 'c, 'info>( ) -> Result<()> { Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ix_size_reduction() { + let max_tx_size = 1232; // 1232 bytes + let signature_size = 64; // 64 bytes + let account_size = 32; // 32 bytes + let anchor_disc_size = 8; + + let remove_liquidity_account_count = 13; + let remove_liquidity_signature_count = 1; + + let base_tx_size = signature_size * remove_liquidity_signature_count + + remove_liquidity_account_count * account_size; + + let bin_ids = vec![0i32]; + let liquidities_to_remove = vec![0u128]; + + let bin_ids_size = borsh::to_vec(&bin_ids).unwrap().len(); + let liquidities_to_remove_size = borsh::to_vec(&liquidities_to_remove).unwrap().len(); + + let ix_data_with_u256 = anchor_disc_size + bin_ids_size + liquidities_to_remove_size; + + let bps_to_remove = vec![0u16]; + let bps_to_remove_size = borsh::to_vec(&bps_to_remove).unwrap().len(); + + let ix_data_with_bps = anchor_disc_size + bin_ids_size + bps_to_remove_size; + + assert_eq!(ix_data_with_bps < ix_data_with_u256, true); + + let delta = ix_data_with_u256 - ix_data_with_bps; + let pct = delta * 100 / ix_data_with_u256; + + println!("Reduced {}%", pct); + + let remaining_size = max_tx_size - base_tx_size - anchor_disc_size; + let no_of_bin_can_fit = remaining_size / (bin_ids_size + bps_to_remove_size); + + println!( + "Estimated number of bins can be withdrawn {}", + no_of_bin_can_fit + ); + } +} diff --git a/programs/lb_clmm/src/instructions/remove_liquidity_by_range.rs b/programs/lb_clmm/src/instructions/remove_liquidity_by_range.rs new file mode 100644 index 0000000..3a2948d --- /dev/null +++ b/programs/lb_clmm/src/instructions/remove_liquidity_by_range.rs @@ -0,0 +1,13 @@ +use crate::constants::BASIS_POINT_MAX; +use crate::errors::LBError; +use crate::{BinLiquidityReduction, ModifyLiquidity}; +use anchor_lang::prelude::*; + +pub fn handle<'a, 'b, 'c, 'info>( + ctx: Context<'a, 'b, 'c, 'info, ModifyLiquidity<'info>>, + min_bin_id: i32, + max_bin_id: i32, + bps: u16, +) -> Result<()> { + Ok(()) +} diff --git a/programs/lb_clmm/src/instructions/set_activation_slot.rs b/programs/lb_clmm/src/instructions/set_activation_slot.rs index f6364c1..712711d 100644 --- a/programs/lb_clmm/src/instructions/set_activation_slot.rs +++ b/programs/lb_clmm/src/instructions/set_activation_slot.rs @@ -1,8 +1,19 @@ -use crate::assert_eq_admin; use crate::errors::LBError; +use crate::math::safe_math::SafeMath; use crate::state::lb_pair::LbPair; +use crate::state::LaunchPadParams; use anchor_lang::prelude::*; +// 1 slot ~500ms +const SLOT_PER_SECOND: u64 = 2; +const SLOT_PER_MINUTE: u64 = SLOT_PER_SECOND * 60; + +#[cfg(feature = "localnet")] +const SLOT_BUFFER: u64 = 0; + +#[cfg(not(feature = "localnet"))] +const SLOT_BUFFER: u64 = SLOT_PER_MINUTE * 60; + #[derive(Accounts)] pub struct SetActivationSlot<'info> { #[account(mut)] @@ -15,6 +26,6 @@ pub struct SetActivationSlot<'info> { pub admin: Signer<'info>, } -pub fn handle(ctx: Context, activation_slot: u64) -> Result<()> { +pub fn handle(ctx: Context, new_activation_slot: u64) -> Result<()> { Ok(()) } diff --git a/programs/lb_clmm/src/instructions/set_lock_release_slot.rs b/programs/lb_clmm/src/instructions/set_lock_release_slot.rs index ddffc89..c431f6b 100644 --- a/programs/lb_clmm/src/instructions/set_lock_release_slot.rs +++ b/programs/lb_clmm/src/instructions/set_lock_release_slot.rs @@ -1,6 +1,8 @@ use crate::assert_eq_launch_pool_admin; use crate::errors::LBError; -use crate::state::{lb_pair::LbPair, position::PositionV2}; +use crate::events::UpdatePositionLockReleaseSlot; +use crate::state::dynamic_position::PositionV3; +use crate::state::LbPair; use anchor_lang::prelude::*; #[event_cpi] @@ -11,7 +13,7 @@ pub struct SetLockReleaseSlot<'info> { mut, has_one = lb_pair )] - pub position: AccountLoader<'info, PositionV2>, + pub position: AccountLoader<'info, PositionV3>, pub lb_pair: AccountLoader<'info, LbPair>, @@ -24,3 +26,196 @@ pub struct SetLockReleaseSlot<'info> { pub fn handle(ctx: Context, lock_release_slot: u64) -> Result<()> { Ok(()) } + +// fn validate_lock_release_lock( +// current_slot: u64, +// existing_lock_release_lock: u64, +// new_lock_release_slot: u64, +// ) -> Result<()> { +// // Can only extend lock release slot into the future +// require!( +// new_lock_release_slot > current_slot && new_lock_release_slot > existing_lock_release_lock, +// LBError::InvalidLockReleaseSlot +// ); + +// Ok(()) +// } + +// fn validate_update_lock_release_slot_for_normal_position( +// current_slot: u64, +// lock_release_slot: u64, +// position: &PositionV2, +// sender: Pubkey, +// ) -> Result<()> { +// // Normal position. Only position owner can update lock release slot +// require!(sender.eq(&position.owner), LBError::UnauthorizedAccess); + +// validate_lock_release_lock(current_slot, position.lock_release_slot, lock_release_slot)?; + +// Ok(()) +// } + +// fn validate_update_lock_release_slot_for_seed_position( +// current_slot: u64, +// lock_release_slot: u64, +// lb_pair: &LbPair, +// position: &PositionV2, +// sender: Pubkey, +// ) -> Result<()> { +// if current_slot >= lb_pair.activation_slot { +// // Treat it as normal position, therefore pool creator is not authorized to update, only position owner +// require!(sender.eq(&position.owner), LBError::UnauthorizedAccess); + +// validate_lock_release_lock(current_slot, position.lock_release_slot, lock_release_slot)?; +// } else { +// // Seed position, only pool creator can update +// require!(sender.eq(&lb_pair.creator), LBError::UnauthorizedAccess); + +// // No validation on lock_release_slot to allow pool creator to withdraw if there's any mistake in seeded liquidity +// } + +// Ok(()) +// } + +// #[cfg(test)] +// mod tests { +// use super::*; + +// #[test] +// fn validate_update_lock_release_slot_for_seed_position_after_activation() { +// let position_owner = Pubkey::new_unique(); +// let pool_creator = Pubkey::new_unique(); + +// let activation_slot = 100; +// let current_slot = activation_slot + 1; +// let lock_release_slot = activation_slot + 100; + +// let lb_pair = LbPair { +// creator: pool_creator, +// activation_slot, +// ..Default::default() +// }; + +// let position = PositionV2 { +// owner: position_owner, +// subjected_to_bootstrap_liquidity_locking: true.into(), +// lock_release_slot, +// ..Default::default() +// }; + +// let new_lock_release_slot = lock_release_slot + 100; + +// // Sender is position owner +// assert!(validate_update_lock_release_slot_for_seed_position( +// current_slot, +// new_lock_release_slot, +// &lb_pair, +// &position, +// position_owner +// ) +// .is_ok()); + +// // After activation, can only extend lock release slot +// assert!(validate_update_lock_release_slot_for_seed_position( +// current_slot, +// current_slot, +// &lb_pair, +// &position, +// position_owner +// ) +// .is_err()); + +// assert!(validate_update_lock_release_slot_for_seed_position( +// current_slot, +// position.lock_release_slot - 10, +// &lb_pair, +// &position, +// position_owner +// ) +// .is_err()); + +// assert!(validate_update_lock_release_slot_for_seed_position( +// current_slot, +// position.lock_release_slot, +// &lb_pair, +// &position, +// position_owner +// ) +// .is_err()); + +// // Error because after activation, only position owner can do that. +// assert!(validate_update_lock_release_slot_for_seed_position( +// current_slot, +// new_lock_release_slot, +// &lb_pair, +// &position, +// pool_creator, +// ) +// .is_err()); +// } + +// #[test] +// fn validate_update_lock_release_slot_for_seed_position_before_activation() { +// let position_owner = Pubkey::new_unique(); +// let pool_creator = Pubkey::new_unique(); + +// let current_slot = 50; +// let activation_slot = current_slot + 50; +// let lock_release_slot = activation_slot + 100; + +// let lb_pair = LbPair { +// creator: pool_creator, +// activation_slot, +// ..Default::default() +// }; + +// let position = PositionV2 { +// owner: position_owner, +// subjected_to_bootstrap_liquidity_locking: true.into(), +// lock_release_slot, +// ..Default::default() +// }; + +// let new_lock_release_slot = lock_release_slot + 100; + +// // Sender is pool creator +// assert!(validate_update_lock_release_slot_for_seed_position( +// current_slot, +// new_lock_release_slot, +// &lb_pair, +// &position, +// pool_creator +// ) +// .is_ok()); + +// // Before activation, allow any value for lock_release_slot +// assert!(validate_update_lock_release_slot_for_seed_position( +// current_slot, +// 0, +// &lb_pair, +// &position, +// pool_creator +// ) +// .is_ok()); + +// // Error because before activation, only pool creator can do that. +// assert!(validate_update_lock_release_slot_for_seed_position( +// current_slot, +// new_lock_release_slot, +// &lb_pair, +// &position, +// position_owner, +// ) +// .is_err()); + +// // Error because before activation, only pool creator can do that. +// assert!(validate_update_lock_release_slot_for_seed_position( +// current_slot, +// 0, +// &lb_pair, +// &position, +// position_owner, +// ) +// .is_err()); +// } +// } diff --git a/programs/lb_clmm/src/instructions/swap.rs b/programs/lb_clmm/src/instructions/swap.rs index adde6ce..66e998c 100644 --- a/programs/lb_clmm/src/instructions/swap.rs +++ b/programs/lb_clmm/src/instructions/swap.rs @@ -1,9 +1,33 @@ +use crate::constants::{HOST_FEE_BPS, MAX_REWARD_BIN_SPLIT, NUM_REWARDS}; use crate::errors::LBError; +use crate::events::Swap as SwapEvent; +use crate::math::safe_math::SafeMath; +use crate::math::u64x64_math::SCALE_OFFSET; +use crate::state::action_access::get_lb_pair_type_access_validator; +use crate::state::bin::SwapResult; use crate::state::bin_array_bitmap_extension::BinArrayBitmapExtension; -use crate::state::lb_pair::*; -use crate::state::oracle::Oracle; +use crate::state::oracle::{Oracle, OracleContentLoader}; +use crate::state::{bin::BinArray, lb_pair::*}; use anchor_lang::prelude::*; +use anchor_spl::token_2022::TransferChecked; use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; +use ruint::aliases::U256; +use std::cell::RefMut; +use std::collections::{BTreeMap, BTreeSet}; + +#[derive(Accounts)] +pub struct BinArrayAccount<'info> { + #[account(mut)] + pub bin_array: AccountLoader<'info, BinArray>, +} + +impl<'info> BinArrayAccount<'info> { + pub fn load_and_validate(&self, lb_pair: Pubkey) -> Result> { + let bin_array = self.bin_array.load_mut()?; + require!(bin_array.lb_pair == lb_pair, LBError::InvalidBinArray); + Ok(bin_array) + } +} #[event_cpi] #[derive(Accounts)] @@ -54,6 +78,107 @@ pub struct Swap<'info> { pub token_y_program: Interface<'info, TokenInterface>, } +impl<'info> Swap<'info> { + fn get_host_fee_bps(&self) -> Option { + if self.host_fee_in.is_some() { + Some(HOST_FEE_BPS) + } else { + None + } + } + + fn swap_transfer(&self, in_amount: u64, out_amount: u64, swap_for_y: bool) -> Result<()> { + let lb_pair = self.lb_pair.load()?; + let signer_seeds = &[&lb_pair.seeds()?[..]]; + self.transfer_to_reserve(in_amount, swap_for_y)?; + self.transfer_to_user(out_amount, swap_for_y, signer_seeds) + } + + fn transfer_from_user_to( + &self, + amount: u64, + swap_for_y: bool, + destination_ai: AccountInfo<'info>, + ) -> Result<()> { + if swap_for_y { + anchor_spl::token_2022::transfer_checked( + CpiContext::new( + self.token_x_program.to_account_info(), + TransferChecked { + from: self.user_token_in.to_account_info(), + to: destination_ai, + authority: self.user.to_account_info(), + mint: self.token_x_mint.to_account_info(), + }, + ), + amount, + self.token_x_mint.decimals, + ) + } else { + anchor_spl::token_2022::transfer_checked( + CpiContext::new( + self.token_y_program.to_account_info(), + TransferChecked { + from: self.user_token_in.to_account_info(), + to: destination_ai, + authority: self.user.to_account_info(), + mint: self.token_y_mint.to_account_info(), + }, + ), + amount, + self.token_y_mint.decimals, + ) + } + } + + fn transfer_to_reserve(&self, amount: u64, swap_for_y: bool) -> Result<()> { + if swap_for_y { + self.transfer_from_user_to(amount, swap_for_y, self.reserve_x.to_account_info()) + } else { + self.transfer_from_user_to(amount, swap_for_y, self.reserve_y.to_account_info()) + } + } + + fn transfer_to_user( + &self, + amount: u64, + swap_for_y: bool, + signer_seeds: &[&[&[u8]]], + ) -> Result<()> { + if swap_for_y { + anchor_spl::token_2022::transfer_checked( + CpiContext::new_with_signer( + self.token_y_program.to_account_info(), + TransferChecked { + from: self.reserve_y.to_account_info(), + to: self.user_token_out.to_account_info(), + authority: self.lb_pair.to_account_info(), + mint: self.token_y_mint.to_account_info(), + }, + signer_seeds, + ), + amount, + self.token_y_mint.decimals, + ) + } else { + anchor_spl::token_2022::transfer_checked( + CpiContext::new_with_signer( + self.token_x_program.to_account_info(), + TransferChecked { + from: self.reserve_x.to_account_info(), + to: self.user_token_out.to_account_info(), + authority: self.lb_pair.to_account_info(), + mint: self.token_x_mint.to_account_info(), + }, + signer_seeds, + ), + amount, + self.token_x_mint.decimals, + ) + } + } +} + pub fn handle<'a, 'b, 'c, 'info>( ctx: Context<'a, 'b, 'c, 'info, Swap<'info>>, amount_in: u64, diff --git a/programs/lb_clmm/src/instructions/toggle_pair_status.rs b/programs/lb_clmm/src/instructions/toggle_pair_status.rs index 5a53485..dc9ee37 100644 --- a/programs/lb_clmm/src/instructions/toggle_pair_status.rs +++ b/programs/lb_clmm/src/instructions/toggle_pair_status.rs @@ -1,6 +1,7 @@ use crate::assert_eq_admin; use crate::errors::LBError; use crate::state::lb_pair::LbPair; +use crate::state::PairStatus; use anchor_lang::prelude::*; #[derive(Accounts)] diff --git a/programs/lb_clmm/src/instructions/update_fee_parameters.rs b/programs/lb_clmm/src/instructions/update_fee_parameters.rs index 85730c1..36f537b 100644 --- a/programs/lb_clmm/src/instructions/update_fee_parameters.rs +++ b/programs/lb_clmm/src/instructions/update_fee_parameters.rs @@ -1,5 +1,6 @@ use crate::assert_eq_admin; use crate::errors::LBError; +use crate::events::FeeParameterUpdate; use crate::state::lb_pair::LbPair; use anchor_lang::prelude::*; diff --git a/programs/lb_clmm/src/instructions/update_fees_and_rewards.rs b/programs/lb_clmm/src/instructions/update_fees_and_rewards.rs index d436b23..4eb3215 100644 --- a/programs/lb_clmm/src/instructions/update_fees_and_rewards.rs +++ b/programs/lb_clmm/src/instructions/update_fees_and_rewards.rs @@ -1,6 +1,13 @@ use crate::authorize_modify_position; -use crate::state::{bin::BinArray, lb_pair::LbPair, position::PositionV2}; +use crate::math::safe_math::SafeMath; +use crate::state::dynamic_position::{DynamicPositionLoader, PositionV3}; +use crate::BinArrayAccount; +use crate::{ + manager::bin_array_manager::BinArrayManager, + state::{bin::BinArray, lb_pair::LbPair}, +}; use anchor_lang::prelude::*; +use std::collections::{BTreeMap, BTreeSet}; #[derive(Accounts)] pub struct UpdateFeesAndRewards<'info> { @@ -9,25 +16,14 @@ pub struct UpdateFeesAndRewards<'info> { has_one = lb_pair, constraint = authorize_modify_position(&position, owner.key())? )] - pub position: AccountLoader<'info, PositionV2>, + pub position: AccountLoader<'info, PositionV3>, #[account(mut)] pub lb_pair: AccountLoader<'info, LbPair>, - #[account( - mut, - has_one = lb_pair - )] - pub bin_array_lower: AccountLoader<'info, BinArray>, - #[account( - mut, - has_one = lb_pair - )] - pub bin_array_upper: AccountLoader<'info, BinArray>, - pub owner: Signer<'info>, } -pub fn handle(ctx: Context) -> Result<()> { +pub fn handle(ctx: Context, min_bin_id: i32, max_bin_id: i32) -> Result<()> { Ok(()) } diff --git a/programs/lb_clmm/src/instructions/update_position_operator.rs b/programs/lb_clmm/src/instructions/update_position_operator.rs index d2333ea..3df87ef 100644 --- a/programs/lb_clmm/src/instructions/update_position_operator.rs +++ b/programs/lb_clmm/src/instructions/update_position_operator.rs @@ -1,10 +1,12 @@ -use crate::state::position::PositionV2; +use crate::errors::LBError; +use crate::events::UpdatePositionOperator as UpdatePositionOperatorEvent; +use crate::state::dynamic_position::PositionV3; use anchor_lang::prelude::*; #[event_cpi] #[derive(Accounts)] pub struct UpdatePositionOperator<'info> { #[account(mut, has_one = owner)] - pub position: AccountLoader<'info, PositionV2>, + pub position: AccountLoader<'info, PositionV3>, pub owner: Signer<'info>, } diff --git a/programs/lb_clmm/src/instructions/update_reward_duration.rs b/programs/lb_clmm/src/instructions/update_reward_duration.rs index 3e34b99..47df066 100644 --- a/programs/lb_clmm/src/instructions/update_reward_duration.rs +++ b/programs/lb_clmm/src/instructions/update_reward_duration.rs @@ -1,5 +1,8 @@ use crate::assert_eq_admin; +use crate::constants::{MAX_REWARD_DURATION, MIN_REWARD_DURATION, NUM_REWARDS}; use crate::errors::LBError; +use crate::events::UpdateRewardDuration as UpdateRewardDurationEvent; +use crate::manager::bin_array_manager::BinArrayManager; use crate::state::bin::BinArray; use crate::state::lb_pair::LbPair; use anchor_lang::prelude::*; @@ -23,6 +26,35 @@ pub struct UpdateRewardDuration<'info> { pub bin_array: AccountLoader<'info, BinArray>, } +impl<'info> UpdateRewardDuration<'info> { + fn validate(&self, reward_index: usize, new_reward_duration: u64) -> Result<()> { + require!(reward_index < NUM_REWARDS, LBError::InvalidRewardIndex); + require!( + new_reward_duration >= MIN_REWARD_DURATION + && new_reward_duration <= MAX_REWARD_DURATION, + LBError::InvalidRewardDuration + ); + + let lb_pair = self.lb_pair.load()?; + let reward_info = &lb_pair.reward_infos[reward_index]; + + require!(reward_info.initialized(), LBError::RewardUninitialized); + require!( + reward_info.reward_duration != new_reward_duration, + LBError::IdenticalRewardDuration, + ); + + let current_time = Clock::get()?.unix_timestamp; + // only allow update reward duration if previous reward has been finished + require!( + reward_info.reward_duration_end < current_time as u64, + LBError::RewardCampaignInProgress, + ); + + Ok(()) + } +} + pub fn handle( ctx: Context, index: u64, diff --git a/programs/lb_clmm/src/instructions/update_reward_funder.rs b/programs/lb_clmm/src/instructions/update_reward_funder.rs index f5b64bc..1a23f22 100644 --- a/programs/lb_clmm/src/instructions/update_reward_funder.rs +++ b/programs/lb_clmm/src/instructions/update_reward_funder.rs @@ -1,6 +1,7 @@ use crate::assert_eq_admin; use crate::constants::NUM_REWARDS; use crate::errors::LBError; +use crate::events::UpdateRewardFunder as UpdateRewardFunderEvent; use crate::state::lb_pair::LbPair; use anchor_lang::prelude::*; @@ -15,6 +16,21 @@ pub struct UpdateRewardFunder<'info> { pub admin: Signer<'info>, } +impl<'info> UpdateRewardFunder<'info> { + fn validate(&self, reward_index: usize, new_funder: Pubkey) -> Result<()> { + require!(reward_index < NUM_REWARDS, LBError::InvalidRewardIndex); + + let lb_pair = self.lb_pair.load()?; + let reward_info = &lb_pair.reward_infos[reward_index]; + + require!(reward_info.initialized(), LBError::RewardUninitialized); + + require!(reward_info.funder != new_funder, LBError::IdenticalFunder,); + + Ok(()) + } +} + pub fn handle(ctx: Context, index: u64, new_funder: Pubkey) -> Result<()> { Ok(()) } diff --git a/programs/lb_clmm/src/instructions/withdraw_ineligible_reward.rs b/programs/lb_clmm/src/instructions/withdraw_ineligible_reward.rs index bf67698..2298dea 100644 --- a/programs/lb_clmm/src/instructions/withdraw_ineligible_reward.rs +++ b/programs/lb_clmm/src/instructions/withdraw_ineligible_reward.rs @@ -1,6 +1,15 @@ -use crate::state::{bin::BinArray, lb_pair::LbPair}; use anchor_lang::prelude::*; -use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface, TransferChecked}; +use ruint::aliases::U256; + +use crate::{ + constants::NUM_REWARDS, + errors::LBError, + events::WithdrawIneligibleReward as WithdrawIneligibleRewardEvent, + manager::bin_array_manager::BinArrayManager, + math::{safe_math::SafeMath, u64x64_math::SCALE_OFFSET}, + state::{bin::BinArray, lb_pair::LbPair}, +}; #[event_cpi] #[derive(Accounts)] @@ -27,6 +36,56 @@ pub struct WithdrawIneligibleReward<'info> { pub token_program: Interface<'info, TokenInterface>, } +impl<'info> WithdrawIneligibleReward<'info> { + fn validate(&self, reward_index: usize) -> Result<()> { + let lb_pair = self.lb_pair.load()?; + require!(reward_index < NUM_REWARDS, LBError::InvalidRewardIndex); + + let reward_info = &lb_pair.reward_infos[reward_index]; + + require!(reward_info.initialized(), LBError::RewardUninitialized); + + require!( + reward_info.vault.eq(&self.reward_vault.key()), + LBError::InvalidRewardVault + ); + + require!( + reward_info.is_valid_funder(self.funder.key()), + LBError::InvalidAdmin + ); + + let current_timestamp = Clock::get()?.unix_timestamp as u64; + + require!( + current_timestamp >= reward_info.reward_duration_end, + LBError::RewardNotEnded + ); + + Ok(()) + } + + fn transfer_from_vault_to_funder(&self, amount: u64) -> Result<()> { + let lb_pair = self.lb_pair.load()?; + let signer_seeds = &[&lb_pair.seeds()?[..]]; + + anchor_spl::token_2022::transfer_checked( + CpiContext::new_with_signer( + self.token_program.to_account_info(), + TransferChecked { + from: self.reward_vault.to_account_info(), + to: self.funder_token_account.to_account_info(), + authority: self.lb_pair.to_account_info(), + mint: self.reward_mint.to_account_info(), + }, + signer_seeds, + ), + amount, + self.reward_mint.decimals, + ) + } +} + pub fn handle(ctx: Context, index: u64) -> Result<()> { Ok(()) } diff --git a/programs/lb_clmm/src/instructions/withdraw_protocol_fee.rs b/programs/lb_clmm/src/instructions/withdraw_protocol_fee.rs index 818144b..7dcd9ed 100644 --- a/programs/lb_clmm/src/instructions/withdraw_protocol_fee.rs +++ b/programs/lb_clmm/src/instructions/withdraw_protocol_fee.rs @@ -1,6 +1,8 @@ -use crate::state::lb_pair::LbPair; use anchor_lang::prelude::*; -use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface, TransferChecked}; + +use crate::errors::LBError; +use crate::state::lb_pair::LbPair; #[derive(Accounts)] pub struct WithdrawProtocolFee<'info> { @@ -32,6 +34,63 @@ pub struct WithdrawProtocolFee<'info> { pub token_y_program: Interface<'info, TokenInterface>, } +impl<'info> WithdrawProtocolFee<'info> { + fn validate(&self, amount_x: u64, amount_y: u64) -> Result<()> { + let lb_pair = self.lb_pair.load()?; + + require!( + lb_pair.fee_owner.eq(&self.fee_owner.key()), + LBError::InvalidFeeOwner + ); + + require!( + amount_x <= lb_pair.protocol_fee.amount_x, + LBError::InvalidFeeWithdrawAmount + ); + + require!( + amount_y <= lb_pair.protocol_fee.amount_y, + LBError::InvalidFeeWithdrawAmount + ); + + Ok(()) + } + + fn withdraw_fee(&self, amount_x: u64, amount_y: u64) -> Result<()> { + let lb_pair = self.lb_pair.load()?; + let signer_seeds = &[&lb_pair.seeds()?[..]]; + anchor_spl::token_2022::transfer_checked( + CpiContext::new_with_signer( + self.token_x_program.to_account_info(), + TransferChecked { + from: self.reserve_x.to_account_info(), + to: self.receiver_token_x.to_account_info(), + authority: self.lb_pair.to_account_info(), + mint: self.token_x_mint.to_account_info(), + }, + signer_seeds, + ), + amount_x, + self.token_x_mint.decimals, + )?; + + anchor_spl::token_2022::transfer_checked( + CpiContext::new_with_signer( + self.token_y_program.to_account_info(), + TransferChecked { + from: self.reserve_y.to_account_info(), + to: self.receiver_token_y.to_account_info(), + authority: self.lb_pair.to_account_info(), + mint: self.token_y_mint.to_account_info(), + }, + signer_seeds, + ), + amount_y, + self.token_y_mint.decimals, + ) + } +} + pub fn handle(ctx: Context, amount_x: u64, amount_y: u64) -> Result<()> { Ok(()) } diff --git a/programs/lb_clmm/src/lib.rs b/programs/lb_clmm/src/lib.rs index def6b50..83db93b 100644 --- a/programs/lb_clmm/src/lib.rs +++ b/programs/lb_clmm/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(warnings)] + use anchor_lang::prelude::*; pub mod constants; @@ -7,6 +9,7 @@ pub mod instructions; pub mod manager; pub mod math; pub mod state; +pub mod tests; pub mod utils; use instructions::add_liquidity::*; @@ -14,12 +17,16 @@ use instructions::add_liquidity_by_strategy::*; use instructions::add_liquidity_by_strategy_one_side::*; use instructions::add_liquidity_by_weight::*; use instructions::add_liquidity_by_weight_one_side::*; +use instructions::add_liquidity_single_side_precise::*; use instructions::claim_fee::*; use instructions::claim_reward::*; use instructions::close_position::*; use instructions::close_preset_parameter::*; +use instructions::decrease_position_length::*; use instructions::fund_reward::*; +use instructions::go_to_a_bin::*; use instructions::increase_oracle_length::*; +use instructions::increase_position_length::*; use instructions::initialize_bin_array::*; use instructions::initialize_bin_array_bitmap_extension::*; use instructions::initialize_lb_pair::*; @@ -30,7 +37,8 @@ use instructions::initialize_position_pda::*; use instructions::initialize_preset_parameters::*; use instructions::initialize_reward::*; use instructions::migrate_bin_array::*; -use instructions::migrate_position::*; +use instructions::migrate_position_from_v1::*; +use instructions::migrate_position_from_v2::*; use instructions::position_authorize::*; use instructions::remove_liquidity::*; use instructions::set_activation_slot::*; @@ -77,11 +85,13 @@ pub mod launch_pool_config_admins { pub const ADMINS: [Pubkey; 1] = [pubkey!("bossj3JvwiNK7pvjr149DqdtJxf2gdygbcmEPTkb2F1")]; #[cfg(not(feature = "localnet"))] - pub const ADMINS: [Pubkey; 4] = [ - pubkey!("4Qo6nr3CqiynvnA3SsbBtzVT3B1pmqQW4dwf2nFmnzYp"), - pubkey!("5unTfT2kssBuNvHPY6LbJfJpLqEcdMxGYLWHwShaeTLi"), - pubkey!("ChSAh3XXTxpp5n2EmgSCm6vVvVPoD1L9VrK3mcQkYz7m"), - pubkey!("DHLXnJdACTY83yKwnUkeoDjqi4QBbsYGa1v8tJL76ViX"), + pub const ADMINS: [Pubkey; 6] = [ + pubkey!("6h43GsVT3TjtLa5nRpsXp15GDpAY4smWCYHgcq58dSPM"), // bin + pubkey!("4U8keyQCV8NFMCevhRJffLawYiUZMyeUrwBjaMcZkGeh"), // soju + pubkey!("4zvTjdpyr3SAgLeSpCnq4KaHvX2j5SbkwxYydzbfqhRQ"), // zhen + pubkey!("5unTfT2kssBuNvHPY6LbJfJpLqEcdMxGYLWHwShaeTLi"), // tian + pubkey!("ChSAh3XXTxpp5n2EmgSCm6vVvVPoD1L9VrK3mcQkYz7m"), // ben + pubkey!("DHLXnJdACTY83yKwnUkeoDjqi4QBbsYGa1v8tJL76ViX"), // andrew ]; } @@ -96,21 +106,20 @@ pub mod fee_owner { declare_id!("6WaLrrRfReGKBYUSkmx2K6AuT21ida4j8at2SUiZdXu8"); } -pub fn assert_eq_admin(admin: Pubkey) -> bool { - crate::admin::ADMINS - .iter() - .any(|predefined_admin| predefined_admin.eq(&admin)) -} - pub fn assert_eq_launch_pool_admin(admin: Pubkey) -> bool { crate::launch_pool_config_admins::ADMINS .iter() .any(|predefined_launch_pool_admin| predefined_launch_pool_admin.eq(&admin)) } +pub fn assert_eq_admin(admin: Pubkey) -> bool { + crate::admin::ADMINS + .iter() + .any(|predefined_admin| predefined_admin.eq(&admin)) +} + #[program] pub mod lb_clmm { - use super::*; pub fn initialize_lb_pair( @@ -144,6 +153,7 @@ pub mod lb_clmm { ) -> Result<()> { instructions::add_liquidity::handle(ctx, liquidity_parameter) } + pub fn add_liquidity_by_weight<'a, 'b, 'c, 'info>( ctx: Context<'a, 'b, 'c, 'info, ModifyLiquidity<'info>>, liquidity_parameter: LiquidityParameterByWeight, @@ -272,12 +282,17 @@ pub mod lb_clmm { instructions::update_reward_duration::handle(ctx, reward_index, new_duration) } - pub fn claim_reward(ctx: Context, reward_index: u64) -> Result<()> { - instructions::claim_reward::handle(ctx, reward_index) + pub fn claim_reward( + ctx: Context, + reward_index: u64, + min_bin_id: i32, + max_bin_id: i32, + ) -> Result<()> { + instructions::claim_reward::handle(ctx, reward_index, min_bin_id, max_bin_id) } - pub fn claim_fee(ctx: Context) -> Result<()> { - instructions::claim_fee::handle(ctx) + pub fn claim_fee(ctx: Context, min_bin_id: i32, max_bin_id: i32) -> Result<()> { + instructions::claim_fee::handle(ctx, min_bin_id, max_bin_id) } pub fn close_position(ctx: Context) -> Result<()> { @@ -298,11 +313,34 @@ pub mod lb_clmm { instructions::increase_oracle_length::handle(ctx, length_to_add) } + pub fn increase_position_length( + ctx: Context, + length_to_add: u16, + side: u8, + ) -> Result<()> { + instructions::increase_position_length::handle(ctx, length_to_add, side) + } + + pub fn decrease_position_length( + ctx: Context, + length_to_remove: u16, + side: u8, + ) -> Result<()> { + instructions::decrease_position_length::handle(ctx, length_to_remove, side) + } + pub fn initialize_preset_parameter( ctx: Context, ix: InitPresetParametersIx, ) -> Result<()> { - instructions::initialize_preset_parameters::handle(ctx, ix) + instructions::initialize_preset_parameters::handle_v1(ctx, ix) + } + + pub fn initialize_preset_parameter_v2( + ctx: Context, + ix: InitPresetParametersIx, + ) -> Result<()> { + instructions::initialize_preset_parameters::handle_v2(ctx, ix) } pub fn close_preset_parameter(ctx: Context) -> Result<()> { @@ -311,8 +349,10 @@ pub mod lb_clmm { pub fn remove_all_liquidity<'a, 'b, 'c, 'info>( ctx: Context<'a, 'b, 'c, 'info, ModifyLiquidity<'info>>, + min_bin_id: i32, + max_bin_id: i32, ) -> Result<()> { - instructions::remove_all_liquidity::handle(ctx) + instructions::remove_all_liquidity::handle(ctx, min_bin_id, max_bin_id) } pub fn toggle_pair_status(ctx: Context) -> Result<()> { @@ -327,16 +367,23 @@ pub mod lb_clmm { instructions::update_whitelisted_wallet::handle(ctx, idx.into(), wallet) } - pub fn migrate_position(ctx: Context) -> Result<()> { - instructions::migrate_position::handle(ctx) + pub fn migrate_position_from_v1(ctx: Context) -> Result<()> { + instructions::migrate_position_from_v1::handle(ctx) + } + pub fn migrate_position_from_v2(ctx: Context) -> Result<()> { + instructions::migrate_position_from_v2::handle(ctx) } pub fn migrate_bin_array(ctx: Context) -> Result<()> { instructions::migrate_bin_array::handle(ctx) } - pub fn update_fees_and_rewards(ctx: Context) -> Result<()> { - instructions::update_fees_and_rewards::handle(ctx) + pub fn update_fees_and_rewards( + ctx: Context, + min_bin_id: i32, + max_bin_id: i32, + ) -> Result<()> { + instructions::update_fees_and_rewards::handle(ctx, min_bin_id, max_bin_id) } pub fn withdraw_ineligible_reward( @@ -352,6 +399,7 @@ pub mod lb_clmm { ) -> Result<()> { instructions::set_activation_slot::handle(ctx, activation_slot) } + pub fn set_max_swapped_amount( ctx: Context, swap_cap_deactivate_slot: u64, @@ -370,4 +418,24 @@ pub mod lb_clmm { ) -> Result<()> { instructions::set_lock_release_slot::handle(ctx, new_lock_release_slot) } + + pub fn remove_liquidity_by_range<'a, 'b, 'c, 'info>( + ctx: Context<'a, 'b, 'c, 'info, ModifyLiquidity<'info>>, + from_bin_id: i32, + to_bin_id: i32, + bps_to_remove: u16, + ) -> Result<()> { + instructions::remove_liquidity_by_range::handle(ctx, from_bin_id, to_bin_id, bps_to_remove) + } + + pub fn add_liquidity_one_side_precise<'a, 'b, 'c, 'info>( + ctx: Context<'a, 'b, 'c, 'info, ModifyLiquidityOneSide<'info>>, + parameter: AddLiquiditySingleSidePreciseParameter, + ) -> Result<()> { + instructions::add_liquidity_single_side_precise::handle(ctx, parameter) + } + + pub fn go_to_a_bin(ctx: Context, bin_id: i32) -> Result<()> { + instructions::go_to_a_bin::handle(ctx, bin_id) + } } diff --git a/programs/lb_clmm/src/manager/bin_array_manager.rs b/programs/lb_clmm/src/manager/bin_array_manager.rs index 984dc12..1b0f737 100644 --- a/programs/lb_clmm/src/manager/bin_array_manager.rs +++ b/programs/lb_clmm/src/manager/bin_array_manager.rs @@ -4,8 +4,7 @@ use crate::state::bin::Bin; use crate::state::lb_pair::LbPair; use crate::{math::safe_math::SafeMath, state::bin::BinArray}; use anchor_lang::prelude::*; -use std::cell::{Ref, RefMut}; - +use std::cell::RefMut; /// A bin arrays container which make sure that the bin array are in continuous form. pub struct BinArrayManager<'a, 'info> { bin_arrays: &'a mut [RefMut<'info, BinArray>], @@ -43,7 +42,6 @@ impl<'a, 'info> BinArrayManager<'a, 'info> { require!(self.bin_arrays.len() > 0, LBError::InvalidInput); let bin_array_0_index = BinArray::bin_id_to_bin_array_index(lower_bin_id)?; - require!( bin_array_0_index as i64 == self.bin_arrays[0].index, LBError::InvalidInput @@ -135,25 +133,3 @@ impl<'a, 'info> BinArrayManager<'a, 'info> { } } } - -pub struct BinArrayManagerReadOnly<'a, 'info> { - bin_arrays: &'a [Ref<'info, BinArray>], -} - -impl<'a, 'info> BinArrayManagerReadOnly<'a, 'info> { - pub fn new(bin_arrays: &'a [Ref<'info, BinArray>]) -> Result { - Ok(BinArrayManagerReadOnly { bin_arrays }) - } - - pub fn get_bin(&self, bin_id: i32) -> Result<&Bin> { - let bin_array_idx = BinArray::bin_id_to_bin_array_index(bin_id)?; - match self - .bin_arrays - .iter() - .find(|ba| ba.index == bin_array_idx as i64) - { - Some(bin_array) => bin_array.get_bin(bin_id), - None => Err(LBError::InvalidBinArray.into()), - } - } -} diff --git a/programs/lb_clmm/src/math/bin_math.rs b/programs/lb_clmm/src/math/bin_math.rs index b7cfe90..36abbce 100644 --- a/programs/lb_clmm/src/math/bin_math.rs +++ b/programs/lb_clmm/src/math/bin_math.rs @@ -22,3 +22,47 @@ pub fn get_liquidity(x: u64, y: u64, price: u128) -> Result { let liquidity = px.safe_add(U256::from(y))?; Ok(liquidity.try_into().map_err(|_| LBError::TypeCastFailed)?) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::constants::BASIS_POINT_MAX; + use crate::math::{ + price_math::get_price_from_id, + u64x64_math::{to_decimal, PRECISION}, + }; + + #[test] + fn test_get_liquidity() { + let x = 21116312; + let y = 122265385; + + let active_id = 5555; + let bin_step = 10; + + let price = get_price_from_id(active_id, bin_step).unwrap(); + println!("price in fixed point {:?}", price); + + let liquidity = get_liquidity(x, y, price).unwrap().try_into().unwrap(); + println!("liquidity in fixed point {:?}", liquidity); + + assert_eq!(liquidity, 102679554235059215585763858120u128); + + let liquidity_conv = (liquidity as f64) / 2.0f64.powi(64); + println!("liquidity converted to float {}", liquidity_conv); + + let liquidity_d = to_decimal(liquidity).unwrap(); + println!("liquidity in decimal {}", liquidity_d); + + let price = (1.0f64 + bin_step as f64 / BASIS_POINT_MAX as f64).powi(active_id); + let liquidity_f = price * x as f64 + y as f64; + println!("liquidity computed in float {:?}", liquidity_f); + + let liquidity_d = liquidity_d / PRECISION; + let liquidity_f = liquidity_f.floor() as u128; + let liquidity_conv = liquidity_conv.floor() as u128; + + assert_eq!(liquidity_d, liquidity_f); + assert_eq!(liquidity_d, liquidity_conv); + } +} diff --git a/programs/lb_clmm/src/math/price_math.rs b/programs/lb_clmm/src/math/price_math.rs index 4056c0c..d06f790 100644 --- a/programs/lb_clmm/src/math/price_math.rs +++ b/programs/lb_clmm/src/math/price_math.rs @@ -17,3 +17,42 @@ pub fn get_price_from_id(active_id: i32, bin_step: u16) -> Result { let base = ONE.safe_add(bps)?; pow(base, active_id).ok_or_else(|| LBError::MathOverflow.into()) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::math::u64x64_math::to_decimal; + + #[test] + fn test_get_price_from_positive_id() { + let active_id = 5555; + let bin_step = 10; + + let price = get_price_from_id(active_id, bin_step).unwrap(); + let decimal_price = to_decimal(price).unwrap(); + println!("Decimal price (10^12 precision) {}", decimal_price); + + assert_eq!(decimal_price.to_string(), "257810379178651"); + + let base = 1.0f64 + bin_step as f64 / BASIS_POINT_MAX as f64; + let price = base.powi(active_id); + println!("Float price {}", price); + } + + #[test] + fn test_get_price_from_negative_id() { + let active_id = -5555; + let bin_step = 10; + + let price = get_price_from_id(active_id, bin_step).unwrap(); + let decimal_price = to_decimal(price).unwrap(); + + println!("Decimal price (10^12 precision) {}", decimal_price); + + assert_eq!(decimal_price.to_string(), "3878819786"); + + let base = 1.0f64 + bin_step as f64 / BASIS_POINT_MAX as f64; + let price = base.powi(active_id); + println!("Float price {}", price); + } +} diff --git a/programs/lb_clmm/src/math/safe_math.rs b/programs/lb_clmm/src/math/safe_math.rs index c429e62..56aaf3b 100644 --- a/programs/lb_clmm/src/math/safe_math.rs +++ b/programs/lb_clmm/src/math/safe_math.rs @@ -112,3 +112,50 @@ checked_impl!(u128, u32); checked_impl!(i128, u32); checked_impl!(usize, u32); checked_impl!(U256, usize); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn safe_add() { + assert_eq!(u64::MAX.safe_add(u64::MAX).is_err(), true); + assert_eq!(100u64.safe_add(100u64).is_ok(), true); + assert_eq!(100u64.safe_add(100u64).unwrap(), 200u64); + } + + #[test] + fn safe_sub() { + assert_eq!(0u64.safe_sub(u64::MAX).is_err(), true); + assert_eq!(200u64.safe_sub(100u64).is_ok(), true); + assert_eq!(200u64.safe_sub(100u64).unwrap(), 100u64); + } + + #[test] + fn safe_mul() { + assert_eq!(u64::MAX.safe_mul(u64::MAX).is_err(), true); + assert_eq!(100u64.safe_mul(100u64).is_ok(), true); + assert_eq!(100u64.safe_mul(100u64).unwrap(), 10000u64); + } + + #[test] + fn safe_div() { + assert_eq!(100u64.safe_div(0u64).is_err(), true); + assert_eq!(200u64.safe_div(100u64).is_ok(), true); + assert_eq!(200u64.safe_div(100u64), Ok(2u64)); + } + + #[test] + fn safe_shl() { + assert_eq!(1u128.safe_shl(8).is_ok(), true); + assert_eq!(100u128.safe_shl(128).is_err(), true); + assert_eq!(100u128.safe_shl(8), Ok(25600)) + } + + #[test] + fn safe_shr() { + assert_eq!(100u128.safe_shr(1).is_ok(), true); + assert_eq!(200u128.safe_shr(129).is_err(), true); + assert_eq!(200u128.safe_shr(1), Ok(100)) + } +} diff --git a/programs/lb_clmm/src/math/u128x128_math.rs b/programs/lb_clmm/src/math/u128x128_math.rs index d26dbf0..f08104d 100644 --- a/programs/lb_clmm/src/math/u128x128_math.rs +++ b/programs/lb_clmm/src/math/u128x128_math.rs @@ -41,3 +41,173 @@ pub fn shl_div(x: u128, y: u128, offset: u8, rounding: Rounding) -> Option let scale = 1u128.checked_shl(offset.into())?; mul_div(x, scale, y, rounding) } + +#[cfg(test)] +mod tests { + use super::*; + use proptest::proptest; + + #[test] + fn test_mul_div() { + assert_eq!(mul_div(33, 3, 2, Rounding::Up), Some(50)); + assert_eq!(mul_div(33, 3, 2, Rounding::Down), Some(49)); + + assert_eq!(mul_div(30, 3, 2, Rounding::Up), Some(45)); + assert_eq!(mul_div(30, 3, 2, Rounding::Down), Some(45)); + + assert_eq!(mul_div(30, 0, 2, Rounding::Up), Some(0)); + assert_eq!(mul_div(30, 0, 2, Rounding::Down), Some(0)); + } + + #[test] + fn test_mul_div_over_underflow() { + assert_eq!(mul_div(33, 3, 0, Rounding::Up), None); + assert_eq!(mul_div(33, 3, 0, Rounding::Down), None); + + assert_eq!( + mul_div(u128::MAX, u128::MAX, u128::MAX - 1, Rounding::Up), + None + ); + assert_eq!( + mul_div(u128::MAX, u128::MAX, u128::MAX - 1, Rounding::Down), + None + ); + } + + #[test] + fn test_mul_div_max() { + assert_eq!( + mul_div(u128::MAX, u128::MAX, u128::MAX, Rounding::Up), + Some(u128::MAX) + ); + assert_eq!( + mul_div(u128::MAX, u128::MAX, u128::MAX, Rounding::Down), + Some(u128::MAX) + ); + } + + #[test] + fn test_mul_shr() { + assert_eq!(mul_shr(33, 3, 1, Rounding::Up), Some(50)); + assert_eq!(mul_shr(33, 3, 1, Rounding::Down), Some(49)); + + assert_eq!(mul_shr(33, 3, 0, Rounding::Up), Some(99)); + assert_eq!(mul_shr(33, 3, 0, Rounding::Down), Some(99)); + } + + #[test] + fn test_mul_shr_overflow() { + assert!(mul_shr( + 240615969200000000000000000000000000000u128, + 240615969200000000000000000000000000000u128, + 127, + Rounding::Up + ) + .is_none()); + assert!(mul_shr( + 240615969200000000000000000000000000000u128, + 240615969200000000000000000000000000000u128, + 127, + Rounding::Down + ) + .is_none()); + } + + #[test] + fn test_shl_div() { + assert_eq!(shl_div(33, 5, 1, Rounding::Up), Some(14)); + assert_eq!(shl_div(33, 5, 1, Rounding::Down), Some(13)); + + assert_eq!(shl_div(33, 5, 0, Rounding::Up), Some(7)); + assert_eq!(shl_div(33, 5, 0, Rounding::Down), Some(6)); + } + + #[test] + fn test_shl_div_overflow() { + assert_eq!(shl_div(u128::MAX, 5, 127, Rounding::Up), None); + assert_eq!(shl_div(u128::MAX, 5, 127, Rounding::Down), None); + } + + #[test] + fn test_shl_div_underflow() { + assert_eq!(shl_div(33, 0, 1, Rounding::Up), None); + assert_eq!(shl_div(33, 0, 1, Rounding::Down), None); + } + + proptest! { + #[test] + fn test_shl_div_range( + x in 0..=u128::MAX, + y in 170141183460469231731687303715884105728..=u128::MAX + ) { + assert!(shl_div(x, y, 127, Rounding::Up).is_some()); + assert!(shl_div(x, y, 127, Rounding::Down).is_some()); + } + + #[test] + fn test_shl_div_underflow_range( + x in 0..=u128::MAX, + offset in 0..=127u8 + ) { + assert!(shl_div(x, 0, offset, Rounding::Up).is_none()); + assert!(shl_div(x, 0, offset, Rounding::Down).is_none()); + } + + #[test] + fn test_shl_div_overflow_range( + x in 2..=u128::MAX + ) { + assert!(shl_div(x, 1, 127, Rounding::Up).is_none()); + assert!(shl_div(x, 1, 127, Rounding::Down).is_none()); + } + } + + proptest! { + #[test] + fn test_mul_shr_range( + x in 0..=240615969100000000000000000000000000000u128, + y in 0..=240615969100000000000000000000000000000u128, + ) { + assert!(mul_shr(x, y, 127, Rounding::Up).is_some()); + assert!(mul_shr(x, y, 127, Rounding::Down).is_some()); + } + + #[test] + fn test_mul_shr_overflow_range( + x in 240615969200000000000000000000000000000u128..=u128::MAX, + y in 240615969200000000000000000000000000000u128..=u128::MAX, + ) { + assert!(mul_div(x, y, 127, Rounding::Up).is_none()); + assert!(mul_div(x, y, 127, Rounding::Down).is_none()); + } + } + + proptest! { + #[test] + fn test_mul_div_range( + x in 0..=u128::MAX, + y in 0..=u128::MAX + ) { + assert!(mul_div(x, y, u128::MAX, Rounding::Up).is_some()); + assert!(mul_div(x, y, u128::MAX, Rounding::Down).is_some()); + } + + #[test] + fn test_mul_div_overflow_range( + x in u64::MAX as u128..=u128::MAX, + y in u64::MAX as u128 + 1..=u128::MAX + ) { + assert!(mul_div(x, y, 1, Rounding::Up).is_none()); + assert!(mul_div(x, y, 1, Rounding::Down).is_none()); + } + + #[test] + fn test_mul_div_underflow_range( + x in 0..=u128::MAX, + y in 0..=u128::MAX + ) { + assert!(mul_div(x, y, 0, Rounding::Up).is_none()); + assert!(mul_div(x, y, 0, Rounding::Down).is_none()); + } + } +} diff --git a/programs/lb_clmm/src/math/u64x64_math.rs b/programs/lb_clmm/src/math/u64x64_math.rs index 61bebd3..2fae387 100644 --- a/programs/lb_clmm/src/math/u64x64_math.rs +++ b/programs/lb_clmm/src/math/u64x64_math.rs @@ -207,3 +207,190 @@ pub fn get_base(bin_step: u32) -> Option { let fraction = quotient.checked_div(BASIS_POINT_MAX as u128)?; ONE.checked_add(fraction) } + +#[cfg(test)] +mod tests { + use super::*; + + const BITS_SUPPORTED: u32 = 64; + const SCALE_OFFSET_F: f64 = 18446744073709551616f64; + + // Get maximum number of bins supported based on BPS and the bits used in u64xu64 math. + // With some bin_step, the range might be smaller due to underflow in pow + // (1 + bps)^n <= u64::MAX, solve for n + fn get_supported_bins(bin_step: u32) -> (i32, i32) { + let base = 1.0f64 + (bin_step as f64 / BASIS_POINT_MAX as f64); + let max_price_supported = 2.0f64.powi(BITS_SUPPORTED as i32); + let n = (max_price_supported.log10() / base.log10()) as i32; + (-n, n) + } + + // Because of pow underflow, the supported range will be smaller than what returned from get_supported_bins. This function search for the actual supported range. + fn find_actual_supported_bins(base: u128, bin_step: u32) -> (i32, i32) { + let (mut min_bin_id, mut max_bin_id) = get_supported_bins(bin_step); + // Try pow and check whether underflow happens + while pow(base, min_bin_id).is_none() { + min_bin_id = min_bin_id + 1; + } + while pow(base, max_bin_id).is_none() { + max_bin_id = max_bin_id - 1; + } + + (min_bin_id, max_bin_id) + } + + // Because of pow behavior of lossy, at the edge of the bin, crossing bin doesn't change price at all. This function find the min and max bin id when crossing bin price changes stopped. + fn find_actual_bin_cross_with_price_changes(base: u128) -> (i32, i32) { + let mut bin_id = 0; + // Crossing towards right + loop { + let cur_answer = pow(base, bin_id); + let next_answer = pow(base, bin_id + 1); + match next_answer { + Some(next) => { + if cur_answer.unwrap() == next { + break; + } + } + None => { + break; + } + } + bin_id = bin_id + 1; + } + let max_bin_id = bin_id; + // Crossing towards left + loop { + let cur_answer = pow(base, bin_id); + let next_answer = pow(base, bin_id - 1); + match next_answer { + Some(next) => { + if cur_answer.unwrap() == next { + break; + } + } + None => { + break; + } + } + bin_id = bin_id - 1; + } + let min_bin_id = bin_id; + + (min_bin_id, max_bin_id) + } + + #[test] + fn test_get_base_with_valid_bin_step() { + for bin_step in 1..=10_000u32 { + let base = get_base(bin_step); + assert!(base.is_some()); + + let dec_base = to_decimal(base.unwrap()).unwrap(); + let f_base = dec_base as f64 / PRECISION as f64; + let expected_f_base = 1.0f64 + bin_step as f64 / BASIS_POINT_MAX as f64; + let diff = expected_f_base - f_base; + + assert!(diff < 0.00001f64); + } + } + + #[test] + fn test_find_actual_bin_cross_with_price_changes() { + for bin_step in 1..=10_000u32 { + let base = get_base(bin_step); + assert!(base.is_some()); + + let (supported_min_bin_id, supported_max_bin_id) = + find_actual_supported_bins(base.unwrap(), bin_step); + let (crossable_min_bin_id, crossable_max_bin_id) = + find_actual_bin_cross_with_price_changes(base.unwrap()); + + assert!(crossable_max_bin_id <= supported_max_bin_id); + assert!(crossable_min_bin_id >= supported_min_bin_id); + + let mut prev_answer: Option = None; + for bin_id in crossable_min_bin_id..=crossable_max_bin_id { + let answer = pow(base.unwrap(), bin_id); + assert!(answer.is_some()); + assert!(answer > Some(0)); + + if prev_answer.is_some() { + let p_answer = prev_answer.unwrap(); + assert!(p_answer < answer.unwrap()); + } + + prev_answer = answer; + } + } + } + + #[test] + fn test_supported_min_max_bin_ids_for_bin_step() { + for bin_step in 1..=10_000u32 { + let base = get_base(bin_step); + assert!(base.is_some()); + + let (min_bin_id, max_bin_id) = find_actual_supported_bins(base.unwrap(), bin_step); + for bin_id in min_bin_id..=max_bin_id { + let answer = pow(base.unwrap(), bin_id); + assert!(answer.is_some()); + assert!(answer > Some(0)); + } + } + } + + #[test] + fn test_actual_max_supported_bins() { + for bin_step in 1..=10_000u32 { + let base = get_base(bin_step); + assert!(base.is_some()); + + let (min_bin_id, max_bin_id) = get_supported_bins(bin_step); + let (actual_min_bin_id, actual_max_bin_id) = + find_actual_supported_bins(base.unwrap(), bin_step); + + // Because of pow underflow, some bin_step might results in smaller range + assert!(actual_min_bin_id >= min_bin_id); + assert!(actual_max_bin_id <= max_bin_id); + } + } + + #[test] + fn test_max_supported_bins() { + let bin_steps = [1, 2, 5, 10, 15, 20, 25, 50, 100, 10000]; + let expected_min_max = [ + (-443636, 443636), + (-221829, 221829), + (-88745, 88745), + (-44383, 44383), + (-29596, 29596), + (-22202, 22202), + (-17766, 17766), + (-8894, 8894), + (-4458, 4458), + (-64, 64), + ]; + for (idx, bin_step) in bin_steps.iter().enumerate() { + let (min, max) = get_supported_bins(*bin_step); + let (e_min, e_max) = expected_min_max[idx]; + assert_eq!(max, e_max); + assert_eq!(min, e_min); + } + } + + #[test] + fn test_pow() { + let bin_step = 15; + let bin_id = 3333; + let base = get_base(bin_step); + assert!(base.is_some()); + + let price = pow(base.unwrap(), bin_id); + assert!(price.is_some()); + assert!(price == Some(2726140093009341558707u128)); + + let price_f = price.unwrap() as f64 / SCALE_OFFSET_F; + assert!(price_f == 147.78435056702816f64); + } +} diff --git a/programs/lb_clmm/src/math/weight_to_amounts.rs b/programs/lb_clmm/src/math/weight_to_amounts.rs index cf07cf8..25cf591 100644 --- a/programs/lb_clmm/src/math/weight_to_amounts.rs +++ b/programs/lb_clmm/src/math/weight_to_amounts.rs @@ -7,11 +7,24 @@ use crate::math::utils_math::safe_mul_div_cast_from_u64_to_u64; use anchor_lang::prelude::*; use ruint::aliases::U256; +#[derive(Debug, Clone, Copy)] +pub struct AmountInBinSingleSide { + pub bin_id: i32, + pub amount: u64, +} + +#[derive(Debug, Clone, Copy)] +pub struct AmountInBin { + pub bin_id: i32, + pub amount_x: u64, + pub amount_y: u64, +} + pub fn to_amount_bid_side( active_id: i32, amount: u64, weights: &[(i32, u16)], -) -> Result> { +) -> Result> { // get sum of weight let mut total_weight = 0u64; for &(bin_id, weight) in weights.iter() { @@ -29,12 +42,12 @@ pub fn to_amount_bid_side( for &(bin_id, weight) in weights.iter() { // skip all ask side if bin_id > active_id { - amounts.push((bin_id, 0)); + amounts.push(AmountInBinSingleSide { bin_id, amount: 0 }); } else { - amounts.push(( + amounts.push(AmountInBinSingleSide { bin_id, - safe_mul_div_cast_from_u64_to_u64(weight.into(), amount, total_weight)?, - )); + amount: safe_mul_div_cast_from_u64_to_u64(weight.into(), amount, total_weight)?, + }); } } Ok(amounts) @@ -45,7 +58,7 @@ pub fn to_amount_ask_side( amount: u64, bin_step: u16, weights: &[(i32, u16)], -) -> Result> { +) -> Result> { // get sum of weight let mut total_weight = U256::ZERO; let mut weight_per_prices = vec![U256::ZERO; weights.len()]; @@ -69,12 +82,16 @@ pub fn to_amount_ask_side( for (i, &(bin_id, _weight)) in weights.iter().enumerate() { // skip all bid side if bin_id < active_id { - amounts.push((bin_id, 0)); + amounts.push(AmountInBinSingleSide { bin_id, amount: 0 }); } else { - amounts.push(( + amounts.push(AmountInBinSingleSide { bin_id, - safe_mul_div_cast_from_u256_to_u64(amount, weight_per_prices[i], total_weight)?, - )); + amount: safe_mul_div_cast_from_u256_to_u64( + amount, + weight_per_prices[i], + total_weight, + )?, + }); } } Ok(amounts) @@ -101,7 +118,37 @@ pub fn to_amount_both_side( total_amount_x: u64, total_amount_y: u64, weights: &[(i32, u16)], -) -> Result> { +) -> Result> { + // only bid side + if active_id > weights[weights.len() - 1].0 { + let amounts = to_amount_bid_side(active_id, total_amount_y, weights)?; + + let amounts = amounts + .iter() + .map(|x| AmountInBin { + bin_id: x.bin_id, + amount_x: 0, + amount_y: x.amount, + }) + .collect::>(); + + return Ok(amounts); + } + // only ask side + if active_id < weights[0].0 { + let amounts = to_amount_ask_side(active_id, total_amount_x, bin_step, weights)?; + + let amounts = amounts + .iter() + .map(|x| AmountInBin { + bin_id: x.bin_id, + amount_x: x.amount, + amount_y: 0, + }) + .collect::>(); + + return Ok(amounts); + } match get_active_bin_index(active_id, weights) { Some(index) => { let (active_bin_id, active_weight) = weights[index]; @@ -179,11 +226,12 @@ pub fn to_amount_both_side( let (amount_y_in_bin, _) = k .safe_mul(U256::from(weight))? .overflowing_shr(SCALE_OFFSET.into()); - amounts.push(( + amounts.push(AmountInBin { bin_id, - 0, - u64::try_from(amount_y_in_bin).map_err(|_| LBError::TypeCastFailed)?, - )); + amount_x: 0, + amount_y: u64::try_from(amount_y_in_bin) + .map_err(|_| LBError::TypeCastFailed)?, + }); continue; } if bin_id > active_id { @@ -191,11 +239,12 @@ pub fn to_amount_both_side( .safe_mul(weight_per_prices[i])? .overflowing_shr((SCALE_OFFSET * 2).into()); - amounts.push(( + amounts.push(AmountInBin { bin_id, - u64::try_from(amount_x_in_bin).map_err(|_| LBError::TypeCastFailed)?, - 0, - )); + amount_x: u64::try_from(amount_x_in_bin) + .map_err(|_| LBError::TypeCastFailed)?, + amount_y: 0, + }); continue; } // else we are in active id @@ -204,11 +253,13 @@ pub fn to_amount_both_side( let (amount_y_in_bin, _) = k.safe_mul(wy0)?.overflowing_shr((SCALE_OFFSET * 2).into()); - amounts.push(( + amounts.push(AmountInBin { bin_id, - u64::try_from(amount_x_in_bin).map_err(|_| LBError::TypeCastFailed)?, - u64::try_from(amount_y_in_bin).map_err(|_| LBError::TypeCastFailed)?, - )); + amount_x: u64::try_from(amount_x_in_bin) + .map_err(|_| LBError::TypeCastFailed)?, + amount_y: u64::try_from(amount_y_in_bin) + .map_err(|_| LBError::TypeCastFailed)?, + }); } return Ok(amounts); } @@ -247,11 +298,12 @@ pub fn to_amount_both_side( let (amount_y_in_bin, _) = k .safe_mul(U256::from(weight))? .overflowing_shr(SCALE_OFFSET.into()); - amounts.push(( + amounts.push(AmountInBin { bin_id, - 0, - u64::try_from(amount_y_in_bin).map_err(|_| LBError::TypeCastFailed)?, - )); + amount_x: 0, + amount_y: u64::try_from(amount_y_in_bin) + .map_err(|_| LBError::TypeCastFailed)?, + }); continue; } if bin_id > active_id { @@ -259,14 +311,520 @@ pub fn to_amount_both_side( .safe_mul(weight_per_prices[i])? .overflowing_shr((SCALE_OFFSET * 2).into()); - amounts.push(( + amounts.push(AmountInBin { bin_id, - u64::try_from(amount_x_in_bin).map_err(|_| LBError::TypeCastFailed)?, - 0, - )); + amount_x: u64::try_from(amount_x_in_bin) + .map_err(|_| LBError::TypeCastFailed)?, + amount_y: 0, + }); } } return Ok(amounts); } } } + +#[cfg(test)] +mod add_liquidity_by_weight_test { + use crate::constants::tests::PRESET_BIN_STEP; + use crate::constants::DEFAULT_BIN_PER_POSITION; + use crate::constants::{MAX_BIN_ID, MIN_BIN_ID}; + use crate::math::u64x64_math::PRECISION; + use crate::BinLiquidityDistributionByWeight; + use crate::LiquidityParameterByWeight; + + use super::*; + use proptest::proptest; + + fn get_supported_bin_range(bin_step: u16) -> Result<(i32, i32)> { + match bin_step { + 1 => Ok((-100000, 100000)), + 2 => Ok((-80000, 80000)), + 4 => Ok((-65000, 65000)), + 5 => Ok((-60000, 60000)), + 8 => Ok((-40000, 40000)), + 10 => Ok((-20000, 20000)), + 15 => Ok((-18000, 18000)), + 20 => Ok((-16000, 16000)), + 25 => Ok((-14000, 14000)), + 50 => Ok((-7000, 7000)), + 60 => Ok((-5800, 5800)), + 100 => Ok((-2900, 2900)), + _ => Err(LBError::InvalidInput.into()), + } + } + + fn new_liquidity_parameter_from_dist( + amount_x: u64, + amount_y: u64, + bin_liquidity_dist: Vec, + ) -> LiquidityParameterByWeight { + LiquidityParameterByWeight { + amount_x, + amount_y, + active_id: 0, + max_active_bin_slippage: i32::MAX, + bin_liquidity_dist, + } + } + + fn get_k( + bin_id: i32, + amount_x: u64, + amount_y: u64, + bin_step: u16, + weight: u16, + ) -> Result { + let price = U256::from(get_price_from_id(bin_id, bin_step)?); + let amount_x = U256::from(amount_x); + let amount_y = U256::from(amount_y).safe_shl(SCALE_OFFSET.into())?; + let weight = U256::from(weight); + + let capital = amount_x + .checked_mul(price) + .unwrap() + .checked_add(amount_y) + .unwrap(); + + let k = capital.checked_div(weight).unwrap(); + + return Ok(k); + } + + fn assert_amount_in_active_bin( + amount_x: u64, + amount_y: u64, + amount_x_in_bin: u64, + amount_y_in_bin: u64, + ) -> Option<()> { + if amount_x == 0 && amount_y == 0 { + return Some(()); + } + if amount_x == 0 { + if amount_x_in_bin != 0 { + return None; + } else { + return Some(()); + } + } + if amount_y == 0 { + if amount_y_in_bin != 0 { + return None; + } else { + return Some(()); + } + } + if amount_y_in_bin == 0 { + // TODO fix this assertion + return Some(()); + } + let amount_x = u128::from(amount_x); + let amount_y = u128::from(amount_y); + let amount_x_in_bin = u128::from(amount_x_in_bin); + let amount_y_in_bin = u128::from(amount_y_in_bin); + + let r1 = amount_x + .checked_mul(PRECISION) + .unwrap() + .checked_div(amount_y) + .unwrap(); + let r2 = amount_x_in_bin + .checked_mul(PRECISION) + .unwrap() + .checked_div(amount_y_in_bin) + .unwrap(); + + return assert_same_value_with_precision(U256::from(r1), U256::from(r2), U256::from(100)); + } + + fn assert_same_value_with_precision(k1: U256, k2: U256, multiplier: U256) -> Option<()> { + // TODO fix this assertion + if k1 == U256::ZERO && k2 == U256::ZERO { + return Some(()); + } + + let ratio = if k1 < k2 { + (k2.checked_sub(k1)?) + .checked_mul(multiplier)? + .checked_div(k1)? + } else { + (k1.checked_sub(k2)?) + .checked_mul(multiplier)? + .checked_div(k2)? + }; + if ratio != U256::ZERO { + return None; + } + Some(()) + } + + fn assert_in_amounts( + liquidity_parameter: &LiquidityParameterByWeight, + in_amounts: &Vec, + active_id: i32, + amount_x: u64, + amount_y: u64, + bin_step: u16, + ) -> Option<()> { + let mut sum_x = 0u64; + let mut sum_y = 0u64; + for val in in_amounts.iter() { + sum_x = sum_x.checked_add(val.amount_x)?; + sum_y = sum_y.checked_add(val.amount_y)?; + } + + if sum_x > liquidity_parameter.amount_x { + return None; + }; + if sum_y > liquidity_parameter.amount_y { + return None; + }; + + // allow precision, must consume all amounts in 1 side + let is_x_full = assert_same_value_with_precision( + U256::from(liquidity_parameter.amount_x), + U256::from(sum_x), + U256::from(1000), + ); + let is_y_full = assert_same_value_with_precision( + U256::from(liquidity_parameter.amount_y), + U256::from(sum_y), + U256::from(1000), + ); + if is_x_full.is_none() && is_y_full.is_none() { + return None; + } + + let weights = liquidity_parameter + .bin_liquidity_dist + .iter() + .map(|x| (x.bin_id, x.weight)) + .collect::>(); + + match get_active_bin_index(active_id, &weights) { + Some(index) => { + let ok = assert_amount_in_active_bin( + amount_x, + amount_y, + in_amounts[index].amount_x, + in_amounts[index].amount_y, + ); + if ok.is_none() { + println!( + "{} {} {} {}", + amount_x, amount_y, in_amounts[index].bin_id, in_amounts[index].amount_x + ); + return None; + } + } + None => {} + } + + // assert distribution + for i in 0..liquidity_parameter.bin_liquidity_dist.len() { + let ki = get_k( + liquidity_parameter.bin_liquidity_dist[i].bin_id, + in_amounts[i].amount_x, + in_amounts[i].amount_y, + bin_step, + liquidity_parameter.bin_liquidity_dist[i].weight, + ) + .unwrap(); + + for j in (i + 1)..liquidity_parameter.bin_liquidity_dist.len() { + let kj = get_k( + liquidity_parameter.bin_liquidity_dist[j].bin_id, + in_amounts[j].amount_x, + in_amounts[j].amount_y, + bin_step, + liquidity_parameter.bin_liquidity_dist[j].weight, + ) + .unwrap(); + + let is_same_ratio = assert_same_value_with_precision(ki, kj, U256::from(100)); + if is_same_ratio.is_none() { + println!("k is not equal {} {}", ki, kj); + return None; + } + } + } + Some(()) + } + + #[test] + fn test_simple_case() { + let amount_x = 100000; + let amount_y = 2000000; + let amount_x_in_active_bin = 100; + let amount_y_in_active_bin = 2000; + + let bin_step = 10; + let bin_liquidity_dist = vec![ + BinLiquidityDistributionByWeight { + bin_id: 1, + weight: 20, + }, + BinLiquidityDistributionByWeight { + bin_id: 3, + weight: 10, + }, + BinLiquidityDistributionByWeight { + bin_id: 5, + weight: 10, + }, + BinLiquidityDistributionByWeight { + bin_id: 7, + weight: 10, + }, + ]; + let liquidity_parameter = + new_liquidity_parameter_from_dist(amount_x, amount_y, bin_liquidity_dist); + + let active_id = 0; + let in_amounts = liquidity_parameter + .to_amounts_into_bin( + active_id, + bin_step, + amount_x_in_active_bin, + amount_y_in_active_bin, + ) + .unwrap(); + + assert_in_amounts( + &liquidity_parameter, + &in_amounts, + active_id, + amount_x_in_active_bin, + amount_y_in_active_bin, + bin_step, + ) + .unwrap(); + + let active_id = 8; + let in_amounts = liquidity_parameter + .to_amounts_into_bin( + active_id, + bin_step, + amount_x_in_active_bin, + amount_y_in_active_bin, + ) + .unwrap(); + println!("bid side {:?}", in_amounts); + + assert_in_amounts( + &liquidity_parameter, + &in_amounts, + active_id, + amount_x_in_active_bin, + amount_y_in_active_bin, + bin_step, + ) + .unwrap(); + + let active_id = 6; + let in_amounts = liquidity_parameter + .to_amounts_into_bin( + active_id, + bin_step, + amount_x_in_active_bin, + amount_y_in_active_bin, + ) + .unwrap(); + println!("active id is not existed {:?}", in_amounts); + + assert_in_amounts( + &liquidity_parameter, + &in_amounts, + active_id, + amount_x_in_active_bin, + amount_y_in_active_bin, + bin_step, + ) + .unwrap(); + + let active_id = 5; + let in_amounts = liquidity_parameter + .to_amounts_into_bin( + active_id, + bin_step, + amount_x_in_active_bin, + amount_y_in_active_bin, + ) + .unwrap(); + println!("active id is existed {:?}", in_amounts); + + assert_in_amounts( + &liquidity_parameter, + &in_amounts, + active_id, + amount_x_in_active_bin, + amount_y_in_active_bin, + bin_step, + ) + .unwrap(); + } + + fn new_liquidity_parameter( + amount_x: u64, + amount_y: u64, + active_id: i32, + num_bin: usize, + side_type: u16, + ) -> LiquidityParameterByWeight { + if side_type == 0 { + // ask side + let mut bin_liquidity_dist = vec![]; + for i in 0..num_bin { + bin_liquidity_dist.push(BinLiquidityDistributionByWeight { + bin_id: active_id + (i as i32) + 1, + weight: u16::MAX, + }) + } + + return LiquidityParameterByWeight { + amount_x, + amount_y, + active_id, + max_active_bin_slippage: i32::MAX, + bin_liquidity_dist, + }; + } + if side_type == 1 { + // bid side + let mut bin_liquidity_dist = vec![]; + for i in 0..num_bin { + bin_liquidity_dist.push(BinLiquidityDistributionByWeight { + bin_id: active_id - ((i as i32) + 1), + weight: u16::MAX, + }) + } + + return LiquidityParameterByWeight { + amount_x, + amount_y, + active_id, + max_active_bin_slippage: i32::MAX, + bin_liquidity_dist, + }; + } + if side_type == 2 { + // active id is not existed + let mut bin_liquidity_dist = vec![]; + for i in 0..num_bin { + let bin_id = active_id + ((i as i32) + 1) - (num_bin as i32 / 2); + if bin_id == active_id { + continue; + } + bin_liquidity_dist.push(BinLiquidityDistributionByWeight { + bin_id: bin_id, + weight: u16::MAX, + }) + } + + return LiquidityParameterByWeight { + amount_x, + amount_y, + active_id, + max_active_bin_slippage: i32::MAX, + bin_liquidity_dist, + }; + } + + if side_type == 3 { + // active id is existed + let mut bin_liquidity_dist = vec![]; + for i in 0..num_bin { + let bin_id = active_id + ((i as i32) + 1) - (num_bin as i32 / 2); + bin_liquidity_dist.push(BinLiquidityDistributionByWeight { + bin_id: bin_id, + weight: u16::MAX, + }) + } + + return LiquidityParameterByWeight { + amount_x, + amount_y, + active_id, + max_active_bin_slippage: i32::MAX, + bin_liquidity_dist, + }; + } + panic!("not supported"); + } + + #[test] + fn test_debug() { + let amount_x = 2554236866980533123; + let amount_y = 169441449402218619; + let amount_x_in_active_bin = 1691977113496464004; + let amount_y_in_active_bin = 2495859837519749078; + let active_id = -4007; + let num_bin = 49; + let bin_step = 100; + let side_type = 2; + let liquidity_parameter = + new_liquidity_parameter(amount_x, amount_y, active_id, num_bin, side_type); + let in_amounts = liquidity_parameter.to_amounts_into_bin( + active_id, + bin_step, + amount_x_in_active_bin, + amount_y_in_active_bin, + ); + println!("{:?}", in_amounts); + } + + proptest! { + #[test] + fn test_in_amounts( + amount_x in 0..u64::MAX / 4, + amount_y in 0..u64::MAX / 4, + amount_x_in_active_bin in 0..u64::MAX/ 4, + amount_y_in_active_bin in 0..u64::MAX/ 4, + active_id in MIN_BIN_ID..=MAX_BIN_ID, + num_bin in 1..DEFAULT_BIN_PER_POSITION, + side_type in 0..4u16, + + ){ + if side_type == 2 || side_type == 3 { + if num_bin < 3 { + return Ok(()); + } + } + for &bin_step in PRESET_BIN_STEP.iter(){ + let (min_bin_id, max_bin_id) = get_supported_bin_range(bin_step).unwrap(); + if active_id < min_bin_id || active_id > max_bin_id { + continue; + } + let liquidity_parameter = new_liquidity_parameter(amount_x, amount_y, active_id, num_bin, side_type); + if !liquidity_parameter.validate(active_id).is_err() { + match liquidity_parameter + .to_amounts_into_bin( + active_id, + bin_step, + amount_x_in_active_bin, + amount_y_in_active_bin, + ) { + Ok(in_amounts) => { + let is_ok = assert_in_amounts( + &liquidity_parameter, + &in_amounts, + active_id, + amount_x_in_active_bin, + amount_y_in_active_bin, + bin_step, + ); + if is_ok.is_none() { + println!("failed case {} {} {} {} {} {} {} {}", amount_x, amount_y, amount_x_in_active_bin, amount_y_in_active_bin, active_id, num_bin, bin_step, side_type); + assert!(false); + } + } + Err(_err) => { + println!("overflow case {} {} {} {} {} {} {} {}", amount_x, amount_y, amount_x_in_active_bin, amount_y_in_active_bin, active_id, num_bin,bin_step, side_type); + assert!(false); + } + } + } + } + } + } +} diff --git a/programs/lb_clmm/src/state/action_access.rs b/programs/lb_clmm/src/state/action_access.rs deleted file mode 100644 index e2501f6..0000000 --- a/programs/lb_clmm/src/state/action_access.rs +++ /dev/null @@ -1,119 +0,0 @@ -use super::lb_pair::{LbPair, PairStatus}; - -use anchor_lang::prelude::*; -use solana_program::pubkey::Pubkey; - -pub trait LbPairTypeActionAccess { - fn validate_add_liquidity_access(&self, wallet: Pubkey) -> bool; - fn validate_initialize_bin_array_access(&self, wallet: Pubkey) -> bool; - fn validate_initialize_position_access(&self, wallet: Pubkey) -> bool; - fn validate_swap_access(&self) -> bool; - fn get_swap_cap_status_and_amount(&self, swap_for_y: bool) -> (bool, u64); -} - -struct PermissionLbPairActionAccess<'a> { - is_enabled: bool, - activated: bool, - throttled: bool, - max_swapped_amount: u64, - whitelisted_wallet: &'a [Pubkey], -} - -impl<'a> PermissionLbPairActionAccess<'a> { - pub fn new(lb_pair: &'a LbPair, current_slot: u64) -> Self { - Self { - whitelisted_wallet: &lb_pair.whitelisted_wallet, - is_enabled: lb_pair.status == Into::::into(PairStatus::Enabled), - activated: current_slot >= lb_pair.activation_slot, - throttled: current_slot <= lb_pair.swap_cap_deactivate_slot, - max_swapped_amount: lb_pair.max_swapped_amount, - } - } -} - -impl<'a> LbPairTypeActionAccess for PermissionLbPairActionAccess<'a> { - fn validate_add_liquidity_access(&self, wallet: Pubkey) -> bool { - // Pair disabled due to emergency mode. Nothing can be deposited. - if !self.is_enabled { - return false; - } - - let is_wallet_whitelisted = is_wallet_in_whitelist(&wallet, &self.whitelisted_wallet); - self.activated || is_wallet_whitelisted - } - - fn validate_initialize_bin_array_access(&self, wallet: Pubkey) -> bool { - self.validate_add_liquidity_access(wallet) - } - - fn validate_initialize_position_access(&self, wallet: Pubkey) -> bool { - self.validate_add_liquidity_access(wallet) - } - - fn validate_swap_access(&self) -> bool { - self.is_enabled && self.activated - } - - fn get_swap_cap_status_and_amount(&self, swap_for_y: bool) -> (bool, u64) { - // no cap when user sell - if swap_for_y { - return (false, u64::MAX); - } - return ( - self.throttled && self.max_swapped_amount < u64::MAX, - self.max_swapped_amount, - ); - } -} - -struct PermissionlessLbPairActionAccess { - is_enabled: bool, -} - -impl PermissionlessLbPairActionAccess { - pub fn new(lb_pair: &LbPair) -> Self { - Self { - is_enabled: lb_pair.status == Into::::into(PairStatus::Enabled), - } - } -} - -impl LbPairTypeActionAccess for PermissionlessLbPairActionAccess { - fn validate_add_liquidity_access(&self, _wallet: Pubkey) -> bool { - self.is_enabled - } - - fn validate_initialize_bin_array_access(&self, _wallet: Pubkey) -> bool { - self.is_enabled - } - - fn validate_initialize_position_access(&self, _wallet: Pubkey) -> bool { - self.is_enabled - } - - fn validate_swap_access(&self) -> bool { - self.is_enabled - } - fn get_swap_cap_status_and_amount(&self, swap_for_y: bool) -> (bool, u64) { - (false, u64::MAX) - } -} - -pub fn get_lb_pair_type_access_validator<'a>( - lb_pair: &'a LbPair, - current_slot: u64, -) -> Result> { - if lb_pair.is_permission_pair()? { - let permission_pair_access_validator = - PermissionLbPairActionAccess::new(&lb_pair, current_slot); - - Ok(Box::new(permission_pair_access_validator)) - } else { - let permissionless_pair_access_validator = PermissionlessLbPairActionAccess::new(&lb_pair); - Ok(Box::new(permissionless_pair_access_validator)) - } -} - -pub fn is_wallet_in_whitelist(wallet: &Pubkey, whitelist: &[Pubkey]) -> bool { - !wallet.eq(&Pubkey::default()) && whitelist.iter().find(|&&w| w.eq(&wallet)).is_some() -} diff --git a/programs/lb_clmm/src/state/bin.rs b/programs/lb_clmm/src/state/bin.rs index 94f5ac1..4243e1f 100644 --- a/programs/lb_clmm/src/state/bin.rs +++ b/programs/lb_clmm/src/state/bin.rs @@ -15,6 +15,7 @@ use crate::{ use anchor_lang::prelude::*; use num_enum::{IntoPrimitive, TryFromPrimitive}; use num_integer::Integer; +use static_assertions::const_assert_eq; /// Calculate out token amount based on liquidity share and supply #[inline] pub fn get_out_amount( @@ -62,6 +63,8 @@ pub struct SwapResult { pub protocol_fee_after_host_fee: u64, /// Part of protocol fee pub host_fee: u64, + /// whether the swap has reached cap, only used in swap_with_cap_function + pub is_reach_cap: bool, } #[zero_copy] @@ -143,6 +146,26 @@ impl Bin { Ok(()) } + pub fn swap_with_cap( + &mut self, + amount_in: u64, + price: u128, + swap_for_y: bool, + lb_pair: &LbPair, + host_fee_bps: Option, + remaining_cap: u64, + ) -> Result { + // Get maximum out token amount can be swapped out from the bin. + let max_amount_out = self.get_max_amount_out(swap_for_y); + if max_amount_out < remaining_cap { + return self.swap(amount_in, price, swap_for_y, lb_pair, host_fee_bps); + } + + let amount_in = amount_in.min(Bin::get_amount_in(remaining_cap, price, swap_for_y)?); + let mut swap_result = self.swap(amount_in, price, swap_for_y, lb_pair, host_fee_bps)?; + swap_result.is_reach_cap = true; + Ok(swap_result) + } /// Swap pub fn swap( &mut self, @@ -176,7 +199,7 @@ impl Bin { // TODO: User possible to bypass fee by swapping small amount ? User do a "normal" swap by just bundling all small swap that bypass fee ? let fee = lb_pair.compute_fee_from_amount(amount_in)?; let amount_in_after_fee = amount_in.safe_sub(fee)?; - let amount_out = Bin::get_amount_out(amount_in_after_fee, price, swap_for_y)?; + let amount_out = self.get_amount_out(amount_in_after_fee, price, swap_for_y)?; ( amount_in, std::cmp::min(amount_out, max_amount_out), @@ -211,6 +234,7 @@ impl Bin { fee, protocol_fee_after_host_fee, host_fee, + is_reach_cap: false, }) } @@ -267,7 +291,7 @@ impl Bin { /// Get out token amount from the bin based in amount in. The result is floor-ed. /// X -> Y: inX * bin_price /// Y -> X: inY / bin_price - pub fn get_amount_out(amount_in: u64, price: u128, swap_for_y: bool) -> Result { + pub fn get_amount_out(&self, amount_in: u64, price: u128, swap_for_y: bool) -> Result { if swap_for_y { // (Q64x64(price) * Q64x0(amount_in)) >> SCALE_OFFSET // price * amount_in = amount_out_token_y (Q64x64) @@ -282,6 +306,22 @@ impl Bin { } } + /// This function reserves amount_in from amount_out, used when user swap with cap limit + pub fn get_amount_in(amount_out: u64, price: u128, swap_for_y: bool) -> Result { + if swap_for_y { + // (amount_y << SCALE_OFFSET) / price + // Convert amount_y into Q64x0, if not the result will always in 0 as price is in Q64x64 + // Division between same Q number format cancel out, result in integer + // amount_y / price = amount_in_token_x (integer [Rounding::Down]) + safe_shl_div_cast(amount_out.into(), price, SCALE_OFFSET, Rounding::Down) + } else { + // (Q64x64(price) * Q64x0(amount_x)) >> SCALE_OFFSET + // price * amount_x = amount_in_token_y (Q64x64) + // amount_in_token_y >> SCALE_OFFSET (convert it back to integer form [Rounding::Down]) + safe_mul_shr_cast(amount_out.into(), price, SCALE_OFFSET, Rounding::Down) + } + } + /// Get maximum token amount needed to deposit into bin, in order to withdraw out all the opposite token from the bin. The result is ceil-ed. /// X -> Y: reserve_y / bin_price /// Y -> X: reserve_x * bin_price @@ -337,6 +377,8 @@ pub struct BinArray { pub bins: [Bin; MAX_BIN_PER_ARRAY], } +const_assert_eq!(std::mem::size_of::(), 10128); + impl BinArray { pub fn is_zero_liquidity(&self) -> bool { for bin in self.bins.iter() { @@ -373,9 +415,12 @@ impl BinArray { Ok(()) } - fn get_bin_index_in_array(&self, bin_id: i32) -> Result { + pub fn get_bin_index_in_array(&self, bin_id: i32) -> Result { self.is_bin_id_within_range(bin_id)?; + self.get_bin_index_internal(bin_id) + } + fn get_bin_index_internal(&self, bin_id: i32) -> Result { let (lower_bin_id, upper_bin_id) = BinArray::get_bin_array_lower_upper_bin_id(self.index as i32)?; @@ -489,4 +534,403 @@ impl BinArray { } Ok(()) } + + // Check whether those bins between from_bin_id to to_bin_id are zero in a binArray + pub fn is_zero_liquidity_in_range(&self, from_bin_id: i32, to_bin_id: i32) -> Result { + self.is_bin_id_within_range(from_bin_id)?; + + let (lower_bin_id, upper_bin_id) = + BinArray::get_bin_array_lower_upper_bin_id(self.index as i32)?; + + let (start_bin_id, end_bin_id) = if from_bin_id > to_bin_id { + let start_bin_id = to_bin_id.max(lower_bin_id); + (start_bin_id, from_bin_id) + } else { + let end_bin_id = to_bin_id.min(upper_bin_id); + (from_bin_id, end_bin_id) + }; + + let start_bin_index = self.get_bin_index_internal(start_bin_id)?; + let end_bin_index = self.get_bin_index_internal(end_bin_id)?; + + for i in start_bin_index..=end_bin_index { + if !self.bins[i].is_zero_liquidity() { + return Ok(false); + } + } + Ok(true) + } +} + +#[cfg(test)] +mod tests { + use crate::state::position::Position; + + use super::*; + use proptest::proptest; + trait BinArrayExt { + fn new(index: i64, lb_pair: Pubkey) -> Self; + } + + impl BinArrayExt for BinArray { + fn new(index: i64, lb_pair: Pubkey) -> Self { + Self { + index, + lb_pair, + bins: [Bin::default(); MAX_BIN_PER_ARRAY], + version: LayoutVersion::V1.into(), + _padding: [0u8; 7], + } + } + } + + struct TestBinArray { + index: i32, + bin_ids: Vec, + } + + proptest! { + #[test] + fn test_is_zero_liquidity_in_range( + from_bin_id in -436704..=436704, + bin_id in -436704..=436704, + flip_id in -436704..=436704) { + let bin_array_index = BinArray::bin_id_to_bin_array_index(flip_id).unwrap(); + let mut bin_array = BinArray::new(bin_array_index.into(), Pubkey::default()); + + // init liquidity + let bin_array_index_offset = bin_array.get_bin_index_in_array(flip_id).unwrap(); + bin_array.bins[bin_array_index_offset].liquidity_supply = 1; + + if bin_array.is_bin_id_within_range(from_bin_id).is_ok() { + let is_zero_liquidity = bin_array.is_zero_liquidity_in_range(from_bin_id, bin_id).unwrap(); + + let min_id = from_bin_id.min(bin_id); + let max_id = from_bin_id.max(bin_id); + if flip_id >= min_id && flip_id <= max_id { + assert!(!is_zero_liquidity); + }else{ + assert!(is_zero_liquidity); + } + } + } + } + #[test] + fn test_bin_id_to_bin_array_index() { + // Populate 4 bin arrays to the left, and right, starting from bin array index 0 + let bin_arrays_delta = 4; + + let mut right_bin_arrays = vec![]; + let mut id = 0; + let mut bin_array_idx = 0; + + for _ in 0..bin_arrays_delta { + let mut bins = vec![]; + for _ in 0..MAX_BIN_PER_ARRAY { + bins.push(id); + id = id + 1; + } + right_bin_arrays.push(TestBinArray { + index: bin_array_idx, + bin_ids: bins, + }); + bin_array_idx = bin_array_idx + 1; + } + + let mut left_bin_arrays = vec![]; + id = 0; + bin_array_idx = 0; + + for _ in 0..bin_arrays_delta { + let mut bins = vec![]; + for _ in 0..MAX_BIN_PER_ARRAY { + id = id - 1; + bins.push(id); + } + bin_array_idx = bin_array_idx - 1; + let reversed_bins = bins.into_iter().rev().collect(); + left_bin_arrays.push(TestBinArray { + index: bin_array_idx, + bin_ids: reversed_bins, + }); + } + + let continuous_bin_arrays: Vec = left_bin_arrays + .into_iter() + .rev() + .chain(right_bin_arrays.into_iter()) + .collect(); + + let mut prev = None; + let mut min_bin_id = i32::MAX; + let mut max_bin_id = i32::MIN; + + for bin_array in continuous_bin_arrays.iter() { + assert_eq!(bin_array.bin_ids.len(), MAX_BIN_PER_ARRAY); + + for bin_id in bin_array.bin_ids.iter() { + if let Some(prev) = prev { + assert_eq!(prev + 1, *bin_id); + } + prev = Some(bin_id); + + if *bin_id < min_bin_id { + min_bin_id = *bin_id; + } + if *bin_id > max_bin_id { + max_bin_id = *bin_id; + } + } + } + + assert_eq!(min_bin_id, -280); + // Because bin id 0 is positioned at positive side + assert_eq!(max_bin_id, 279); + + for i in min_bin_id..=max_bin_id { + let idx = BinArray::bin_id_to_bin_array_index(i).unwrap(); + let bin_array = continuous_bin_arrays + .iter() + .find(|ba| ba.index == idx) + .unwrap(); + let result = bin_array.bin_ids.iter().find(|bid| **bid == i); + if let None = result { + println!("Bin id causing the error {}", i); + } + assert!(result.is_some()); + } + } + + #[test] + fn test_bin_array_lower_upper_bin_id_negative() { + let bin_array_index = -1; + let mut amount: i32 = -1; + + let mut bin_array = BinArray::new(bin_array_index, Pubkey::default()); + + // Amount here is bin id, negate it and use for assertion later + for i in (0..MAX_BIN_PER_ARRAY).rev() { + bin_array.bins[i].amount_x = amount.unsigned_abs() as u64; + bin_array.bins[i].amount_y = amount.unsigned_abs() as u64; + amount -= 1; + } + + let (lower, upper) = + BinArray::get_bin_array_lower_upper_bin_id(bin_array.index as i32).unwrap(); + + println!( + "{} {} {:?} {:?}", + lower, + upper, + bin_array.get_bin(lower).unwrap(), + bin_array.get_bin(upper).unwrap() + ); + + assert_eq!( + lower, + (bin_array.get_bin(lower).unwrap().amount_x as i32).wrapping_neg() + ); + assert_eq!( + upper, + (bin_array.get_bin(upper).unwrap().amount_x as i32).wrapping_neg() + ); + + bin_array.index -= 1; + + for i in (0..MAX_BIN_PER_ARRAY).rev() { + bin_array.bins[i].amount_x = amount.unsigned_abs() as u64; + bin_array.bins[i].amount_y = amount.unsigned_abs() as u64; + amount -= 1; + } + + let (lower, upper) = + BinArray::get_bin_array_lower_upper_bin_id(bin_array.index as i32).unwrap(); + + println!( + "{} {} {:?} {:?}", + lower, + upper, + bin_array.get_bin(lower).unwrap(), + bin_array.get_bin(upper).unwrap() + ); + + assert_eq!( + lower, + (bin_array.get_bin(lower).unwrap().amount_x as i32).wrapping_neg() + ); + assert_eq!( + upper, + (bin_array.get_bin(upper).unwrap().amount_x as i32).wrapping_neg() + ); + } + + #[test] + fn test_bin_array_lower_upper_bin_id_positive() { + let bin_array_index = 0; + let mut amount = 0; + + let mut bin_array = BinArray::new(bin_array_index, Pubkey::default()); + + // Amount here is bin id, used for assertion later + for i in 0..MAX_BIN_PER_ARRAY { + bin_array.bins[i].amount_x = amount; + bin_array.bins[i].amount_y = amount; + amount += 1; + } + + let (lower, upper) = + BinArray::get_bin_array_lower_upper_bin_id(bin_array.index as i32).unwrap(); + println!( + "{} {} {:?} {:?}", + lower, + upper, + bin_array.get_bin(lower).unwrap(), + bin_array.get_bin(upper).unwrap() + ); + + assert_eq!(lower, bin_array.get_bin(lower).unwrap().amount_x as i32); + assert_eq!(upper, bin_array.get_bin(upper).unwrap().amount_x as i32); + + bin_array.index += 1; + + for i in 0..MAX_BIN_PER_ARRAY { + bin_array.bins[i].amount_x = amount; + bin_array.bins[i].amount_y = amount; + amount += 1; + } + + let (lower, upper) = + BinArray::get_bin_array_lower_upper_bin_id(bin_array.index as i32).unwrap(); + + println!( + "{} {} {:?} {:?}", + lower, + upper, + bin_array.get_bin(lower).unwrap(), + bin_array.get_bin(upper).unwrap() + ); + + assert_eq!(lower, bin_array.get_bin(lower).unwrap().amount_x as i32); + assert_eq!(upper, bin_array.get_bin(upper).unwrap().amount_x as i32); + } + + #[test] + fn test_bin_negative() { + let bin_id = -11514; + let bin_array_index = BinArray::bin_id_to_bin_array_index(bin_id).unwrap(); + + let mut bin_arrays = [[Bin::default(); MAX_BIN_PER_ARRAY]; 195]; + let mut amount: i64 = -1; + + for arr in bin_arrays.iter_mut().rev() { + for bin in arr.iter_mut().rev() { + bin.amount_x = amount.unsigned_abs(); + bin.amount_y = amount.unsigned_abs(); + amount -= 1; + } + } + + let mut bin_array = BinArray::new(bin_array_index as i64, Pubkey::default()); + bin_array.bins = bin_arrays[(195 - bin_array_index.abs()) as usize]; + + let bin = bin_array.get_bin(bin_id).unwrap(); + println!("{:?}", bin); + assert_eq!((bin.amount_x as i32).wrapping_neg(), bin_id); + + let bin_id = -1332; + let bin_array_index = BinArray::bin_id_to_bin_array_index(bin_id).unwrap(); + + let mut bin_array = BinArray::new(bin_array_index as i64, Pubkey::default()); + bin_array.bins = bin_arrays[(195 - bin_array_index.abs()) as usize]; + + let bin = bin_array.get_bin(bin_id).unwrap(); + println!("{:?}", bin); + assert_eq!((bin.amount_x as i32).wrapping_neg(), bin_id); + + let bin_id = -66; + let bin_array_index = BinArray::bin_id_to_bin_array_index(bin_id).unwrap(); + + let mut bin_array = BinArray::new(bin_array_index as i64, Pubkey::default()); + bin_array.bins = bin_arrays[(195 - bin_array_index.abs()) as usize]; + + let bin = bin_array.get_bin(bin_id).unwrap(); + println!("{:?}", bin); + assert_eq!((bin.amount_x as i32).wrapping_neg(), bin_id); + } + + #[test] + fn test_bin_positive() { + let bin_id = 5645; + let bin_array_index = BinArray::bin_id_to_bin_array_index(bin_id).unwrap(); + let mut bin_arrays = [[Bin::default(); MAX_BIN_PER_ARRAY]; 195]; + + let mut amount = 0; + + for arr in bin_arrays.iter_mut() { + for bin in arr.iter_mut() { + bin.amount_x = amount; + bin.amount_y = amount; + amount += 1; + } + } + + let mut bin_array = BinArray::new(bin_array_index as i64, Pubkey::default()); + bin_array.bins = bin_arrays[bin_array_index as usize]; + + let bin = bin_array.get_bin(bin_id).unwrap(); + assert_eq!(bin.amount_x, bin_id as u64); + println!("{:?}", bin); + + let bin_id = 10532; + let bin_array_index = BinArray::bin_id_to_bin_array_index(bin_id).unwrap(); + + let mut bin_array = BinArray::new(bin_array_index as i64, Pubkey::default()); + bin_array.bins = bin_arrays[bin_array_index as usize]; + + let bin = bin_array.get_bin(bin_id).unwrap(); + assert_eq!(bin.amount_x, bin_id as u64); + println!("{:?}", bin); + + let bin_id = 252; + let bin_array_index = BinArray::bin_id_to_bin_array_index(bin_id).unwrap(); + + let mut bin_array = BinArray::new(bin_array_index as i64, Pubkey::default()); + bin_array.bins = bin_arrays[bin_array_index as usize]; + + let bin = bin_array.get_bin(bin_id).unwrap(); + assert_eq!(bin.amount_x, bin_id as u64); + println!("{:?}", bin); + } + + #[test] + fn test_bin_array_size() { + let bin_array_size = std::mem::size_of::(); + println!("BinArray size {:?}", bin_array_size); + + let bin_size = std::mem::size_of::(); + println!("Bin size {:?}", bin_size); + + let max_size = 10240; + + println!( + "No of bin can fit into {} bytes, {} bins", + max_size, + (max_size - 32 * 2) / bin_size + ); + + let remaining = max_size - bin_array_size - 8; + println!("remaining bytes {:?}", remaining); + + let bin_size = std::mem::size_of::(); + println!("remaining bins {:?}", remaining / bin_size); + } + + #[test] + fn test_bin_array_and_position_size() { + println!( + "Bin array size {:?} Position size {:?}", + std::mem::size_of::(), + std::mem::size_of::() + ); + } } diff --git a/programs/lb_clmm/src/state/bin_array_bitmap_extension.rs b/programs/lb_clmm/src/state/bin_array_bitmap_extension.rs index 7d2f031..75a648c 100644 --- a/programs/lb_clmm/src/state/bin_array_bitmap_extension.rs +++ b/programs/lb_clmm/src/state/bin_array_bitmap_extension.rs @@ -4,8 +4,10 @@ use crate::math::safe_math::SafeMath; use crate::math::utils_math::one; use anchor_lang::prelude::*; use ruint::aliases::U512; +use static_assertions::const_assert_eq; use std::ops::BitXor; - +use std::ops::Shl; +use std::ops::Shr; #[account(zero_copy)] #[derive(Debug, InitSpace)] pub struct BinArrayBitmapExtension { @@ -16,6 +18,8 @@ pub struct BinArrayBitmapExtension { pub negative_bin_array_bitmap: [[u64; 8]; EXTENSION_BINARRAY_BITMAP_SIZE], } +const_assert_eq!(std::mem::size_of::(), 1568); + impl Default for BinArrayBitmapExtension { #[inline] fn default() -> BinArrayBitmapExtension { @@ -34,7 +38,7 @@ impl BinArrayBitmapExtension { self.negative_bin_array_bitmap = [[0; 8]; EXTENSION_BINARRAY_BITMAP_SIZE]; } - fn get_bitmap_offset(bin_array_index: i32) -> Result { + pub fn get_bitmap_offset(bin_array_index: i32) -> Result { // bin_array_index starts from 512 in positive side and -513 in negative side let offset = if bin_array_index > 0 { bin_array_index / BIN_ARRAY_BITMAP_SIZE - 1 @@ -54,7 +58,7 @@ impl BinArrayBitmapExtension { } } - fn bin_array_offset_in_bitmap(bin_array_index: i32) -> Result { + pub fn bin_array_offset_in_bitmap(bin_array_index: i32) -> Result { if bin_array_index > 0 { Ok(bin_array_index.safe_rem(BIN_ARRAY_BITMAP_SIZE)? as usize) } else { @@ -62,6 +66,15 @@ impl BinArrayBitmapExtension { } } + pub fn get_offset_and_bin_array_offset_in_bitmap( + bin_array_index: i32, + ) -> Result<(usize, usize)> { + let offset = BinArrayBitmapExtension::get_bitmap_offset(bin_array_index)?; + let bin_array_offset = + BinArrayBitmapExtension::bin_array_offset_in_bitmap(bin_array_index)?; + Ok((offset, bin_array_offset)) + } + fn to_bin_array_index( offset: usize, bin_array_offset: usize, @@ -76,6 +89,58 @@ impl BinArrayBitmapExtension { } } + fn is_bin_range_empty( + bin_array_bitmap: &U512, + from_offset: usize, + to_offset: usize, + ) -> Result { + let bin_array_bitmap = bin_array_bitmap + .shr(from_offset) + .shl(511.safe_add(from_offset)?.safe_sub(to_offset)?); + Ok(bin_array_bitmap.eq(&U512::ZERO)) + } + + // bin_array_from_offset and bin_array_to_offset will be offset in the same U512 bitmap, if from_offset and to_offset are the same. Else, it will be index in different U512 bitmap. + pub fn is_bin_array_range_empty( + bin_array_bitmap_extension: &[[u64; 8]; EXTENSION_BINARRAY_BITMAP_SIZE], + from_offset: usize, + to_offset: usize, // to_offset always greater than or equal from_offset + bin_array_from_offset: usize, + bin_array_to_offset: usize, + ) -> Result { + let bin_array_bitmap = U512::from_limbs(bin_array_bitmap_extension[from_offset]); + if from_offset == to_offset { + return BinArrayBitmapExtension::is_bin_range_empty( + &bin_array_bitmap, + bin_array_from_offset, + bin_array_to_offset, + ); + } + if !BinArrayBitmapExtension::is_bin_range_empty( + &bin_array_bitmap, + bin_array_from_offset, + 511, + )? { + return Ok(false); + } + let start_offset = from_offset.safe_add(1)?; + let end_offset = to_offset.safe_sub(1)?; + if start_offset <= end_offset { + for i in start_offset..=end_offset { + let bin_array_bitmap = U512::from_limbs(bin_array_bitmap_extension[i]); + if !bin_array_bitmap.eq(&U512::ZERO) { + return Ok(false); + } + } + } + let bin_array_bitmap = U512::from_limbs(bin_array_bitmap_extension[to_offset]); + if !BinArrayBitmapExtension::is_bin_range_empty(&bin_array_bitmap, 0, bin_array_to_offset)? + { + return Ok(false); + } + Ok(true) + } + /// Flip the value of bin in the bitmap. pub fn flip_bin_array_bit(&mut self, bin_array_index: i32) -> Result<()> { // TODO do we need validate bin_array_index again? @@ -108,11 +173,18 @@ impl BinArrayBitmapExtension { } pub fn iter_bitmap(&self, start_index: i32, end_index: i32) -> Result> { + if start_index == end_index { + if self.bit(start_index)? { + return Ok(Some(start_index)); + } else { + return Ok(None); + } + } let offset: usize = Self::get_bitmap_offset(start_index)?; let bin_array_offset = Self::bin_array_offset_in_bitmap(start_index)?; if start_index < 0 { // iter in negative_bin_array_bitmap - if start_index <= end_index { + if start_index < end_index { for i in (0..=offset).rev() { let mut bin_array_bitmap = U512::from_limbs(self.negative_bin_array_bitmap[i]); @@ -176,7 +248,7 @@ impl BinArrayBitmapExtension { } } else { // iter in possitive_bin_array_bitmap - if start_index <= end_index { + if start_index < end_index { for i in offset..EXTENSION_BINARRAY_BITMAP_SIZE { let mut bin_array_bitmap = U512::from_limbs(self.positive_bin_array_bitmap[i]); if i == offset { @@ -274,3 +346,563 @@ impl BinArrayBitmapExtension { } } } + +#[cfg(test)] +pub mod bin_array_bitmap_extension_test { + use crate::{ + constants::{MAX_BIN_ID, MAX_BIN_PER_ARRAY}, + state::lb_pair::LbPair, + }; + use core::panic; + use proptest::proptest; + use ruint::Uint; + + use super::*; + + #[test] + fn test_flip_bin_array_bit_extension() { + let mut extension = BinArrayBitmapExtension::default(); + let bin_array_index = BIN_ARRAY_BITMAP_SIZE; + extension.flip_bin_array_bit(bin_array_index).unwrap(); + assert_eq!(extension.bit(bin_array_index).unwrap(), true); + extension.flip_bin_array_bit(bin_array_index).unwrap(); + assert_eq!(extension.bit(bin_array_index).unwrap(), false); + + let bin_array_index = -BIN_ARRAY_BITMAP_SIZE - 1; + extension.flip_bin_array_bit(bin_array_index).unwrap(); + assert_eq!(extension.bit(bin_array_index).unwrap(), true); + extension.flip_bin_array_bit(bin_array_index).unwrap(); + assert_eq!(extension.bit(bin_array_index).unwrap(), false); + + // max range + let bin_array_index = MAX_BIN_ID / (MAX_BIN_PER_ARRAY as i32) + 1; + extension.flip_bin_array_bit(bin_array_index).unwrap(); + assert_eq!(extension.bit(bin_array_index).unwrap(), true); + + let bin_array_index = -MAX_BIN_ID / (MAX_BIN_PER_ARRAY as i32) - 1; + extension.flip_bin_array_bit(bin_array_index).unwrap(); + assert_eq!(extension.bit(bin_array_index).unwrap(), true); + } + + #[test] + fn test_flip_all_bin_array_bit_extension() { + let mut extension = BinArrayBitmapExtension::default(); + let max_bin_array_index = MAX_BIN_ID / (MAX_BIN_PER_ARRAY as i32) + 1; + let min_bin_array_index = -MAX_BIN_ID / (MAX_BIN_PER_ARRAY as i32) - 1; + + for i in BIN_ARRAY_BITMAP_SIZE..max_bin_array_index { + extension.flip_bin_array_bit(i).unwrap(); + assert_eq!(extension.bit(i).unwrap(), true); + } + for i in min_bin_array_index..(-BIN_ARRAY_BITMAP_SIZE) { + extension.flip_bin_array_bit(i).unwrap(); + assert_eq!(extension.bit(i).unwrap(), true); + } + + for i in BIN_ARRAY_BITMAP_SIZE..max_bin_array_index { + extension.flip_bin_array_bit(i).unwrap(); + assert_eq!(extension.bit(i).unwrap(), false); + } + for i in min_bin_array_index..(-BIN_ARRAY_BITMAP_SIZE) { + extension.flip_bin_array_bit(i).unwrap(); + assert_eq!(extension.bit(i).unwrap(), false); + } + } + + #[test] + fn test_next_id_to_initialized_bin_array_from_internal_to_extension_swap_for_x() { + let mut extension = BinArrayBitmapExtension::default(); + + let (_, max_bin_array_index) = BinArrayBitmapExtension::bitmap_range(); + let start_index = BIN_ARRAY_BITMAP_SIZE; + + let index = 2000; + // deposit liquidity at index 2000 + extension.flip_bin_array_bit(index).unwrap(); + assert_eq!(extension.bit(index).unwrap(), true); + let (bin_array_id, ok) = extension + .next_bin_array_index_with_liquidity(false, start_index) + .unwrap(); + assert_eq!(index, bin_array_id); + + assert_eq!(ok, true); + // swap for x + let (bin_array_id, ok) = extension + .next_bin_array_index_with_liquidity(false, start_index) + .unwrap(); + assert_eq!(index, bin_array_id); + assert_eq!(ok, true); + // withdraw liquidity at index 2000 + extension.flip_bin_array_bit(index).unwrap(); + + let index = max_bin_array_index; + // deposit liquidity at index max_bin_array_index + extension.flip_bin_array_bit(index).unwrap(); + assert_eq!(extension.bit(index).unwrap(), true); + + // swap for x + let (bin_array_id, ok) = extension + .next_bin_array_index_with_liquidity(false, start_index) + .unwrap(); + assert_eq!(index, bin_array_id); + assert_eq!(ok, true); + + // if we dont find non zero liquidity, then we have to return error + extension.flip_bin_array_bit(index).unwrap(); + assert_eq!(extension.bit(index).unwrap(), false); + + match extension.next_bin_array_index_with_liquidity(false, start_index) { + Ok(_value) => panic!("should panic"), + Err(_err) => {} + }; + } + + #[test] + fn test_next_bin_array_index_with_liquidity_bug() { + let mut extension = BinArrayBitmapExtension::default(); + extension.flip_bin_array_bit(600).unwrap(); + + let (index, flag) = extension + .next_bin_array_index_with_liquidity(false, 550) + .unwrap(); + println!("Swap for X (Index: 550): {} {}", index, flag); // Output: 600 since index increases from 550 + let (index, flag) = extension + .next_bin_array_index_with_liquidity(true, 600) + .unwrap(); + println!("Swap for Y (Index: 650): {} {}", index, flag); // Output: 600 since index decreases from 650 + let (index, flag) = extension + .next_bin_array_index_with_liquidity(true, 513) + .unwrap(); + println!("Swap for Y (Index: 513): {} {}", index, flag); // Output: 511 since index decreases from 513 + + // Vulnerable Case, Swapping for Y: searching index from 512 + let (index, flag) = extension + .next_bin_array_index_with_liquidity(true, 512) + .unwrap(); + assert_eq!(index, 511); + assert_eq!(flag, false); + + let mut extension = BinArrayBitmapExtension::default(); + extension.flip_bin_array_bit(-600).unwrap(); + + let (index, flag) = extension + .next_bin_array_index_with_liquidity(false, -513) + .unwrap(); + assert_eq!(index, -512); + assert_eq!(flag, false); + } + + #[test] + fn test_next_id_to_initialized_bin_array_from_internal_to_extension_swap_for_y() { + let mut extension = BinArrayBitmapExtension::default(); + let (min_bin_array_index, _) = BinArrayBitmapExtension::bitmap_range(); + let start_index = -BIN_ARRAY_BITMAP_SIZE - 1; + let index = -2000; + // deposit liquidity at index -2000 + extension.flip_bin_array_bit(index).unwrap(); + assert_eq!(extension.bit(index).unwrap(), true); + + // swap for x + let (bin_array_id, ok) = extension + .next_bin_array_index_with_liquidity(true, start_index) + .unwrap(); + assert_eq!(index, bin_array_id); + assert_eq!(ok, true); + + // withdraw liquidity at index -2000 + extension.flip_bin_array_bit(index).unwrap(); + assert_eq!(extension.bit(index).unwrap(), false); + + let index = min_bin_array_index; + // deposit liquidity at index min_bin_array_index + extension.flip_bin_array_bit(index).unwrap(); + assert_eq!(extension.bit(index).unwrap(), true); + + // swap for x + let (bin_array_id, ok) = extension + .next_bin_array_index_with_liquidity(true, start_index) + .unwrap(); + + assert_eq!(index, bin_array_id); + assert_eq!(ok, true); + + // if we dont find non zero liquidity, then we have to return error + extension.flip_bin_array_bit(index).unwrap(); + assert_eq!(extension.bit(index).unwrap(), false); + match extension.next_bin_array_index_with_liquidity(true, start_index) { + Ok(_value) => panic!("should panic"), + Err(_err) => {} + }; + } + + #[test] + fn test_next_id_to_initialized_bin_array_from_extension_to_internal_swap_for_y() { + let mut extension = BinArrayBitmapExtension::default(); + let index: i32 = 2000; + let start_index = index - 1; + // deposit liquidity at index 2000 + extension.flip_bin_array_bit(index).unwrap(); + assert_eq!(extension.bit(index).unwrap(), true); + + let (bin_array_id, ok) = extension + .next_bin_array_index_with_liquidity(true, start_index) + .unwrap(); + assert_eq!(ok, false); + assert_eq!(bin_array_id, BIN_ARRAY_BITMAP_SIZE - 1); + } + + #[test] + fn test_next_id_to_initialized_bin_array_from_extension_to_internal_swap_for_x() { + let mut extension = BinArrayBitmapExtension::default(); + let index: i32 = -2000; + let start_index = index + 1; + // deposit liquidity at index 2000 + extension.flip_bin_array_bit(index).unwrap(); + assert_eq!(extension.bit(index).unwrap(), true); + + let (bin_array_id, ok) = extension + .next_bin_array_index_with_liquidity(false, start_index) + .unwrap(); + assert_eq!(ok, false); + assert_eq!(bin_array_id, -BIN_ARRAY_BITMAP_SIZE); + } + + #[test] + fn test_bin_array_offset() { + let (min_bin_id, max_bin_id) = LbPair::bitmap_range(); + + let next_max_bin_id = max_bin_id + 1; + let bitmap_offset = BinArrayBitmapExtension::get_bitmap_offset(next_max_bin_id).unwrap(); + assert_eq!(bitmap_offset, 0); + let bin_array_offset = + BinArrayBitmapExtension::bin_array_offset_in_bitmap(next_max_bin_id).unwrap(); + assert_eq!(bin_array_offset, 0); + + let end_nex_max_bin_id = next_max_bin_id + 511; + let bitmap_offset = BinArrayBitmapExtension::get_bitmap_offset(end_nex_max_bin_id).unwrap(); + assert_eq!(bitmap_offset, 0); + let bin_array_offset = + BinArrayBitmapExtension::bin_array_offset_in_bitmap(end_nex_max_bin_id).unwrap(); + assert_eq!(bin_array_offset, 511); + + let next_min_bin_id = min_bin_id - 1; + let bitmap_offset = BinArrayBitmapExtension::get_bitmap_offset(next_min_bin_id).unwrap(); + assert_eq!(bitmap_offset, 0); + let bin_array_offset = + BinArrayBitmapExtension::bin_array_offset_in_bitmap(next_min_bin_id).unwrap(); + assert_eq!(bin_array_offset, 0); + + let end_nex_min_bin_id = next_min_bin_id - 511; + let bitmap_offset = BinArrayBitmapExtension::get_bitmap_offset(end_nex_min_bin_id).unwrap(); + assert_eq!(bitmap_offset, 0); + let bin_array_offset = + BinArrayBitmapExtension::bin_array_offset_in_bitmap(end_nex_min_bin_id).unwrap(); + assert_eq!(bin_array_offset, 511); + } + + #[test] + fn test_iter_map() { + let mut extension = BinArrayBitmapExtension::default(); + extension.flip_bin_array_bit(-1111).unwrap(); + extension.flip_bin_array_bit(-2222).unwrap(); + extension.flip_bin_array_bit(-2225).unwrap(); + + extension.flip_bin_array_bit(1111).unwrap(); + extension.flip_bin_array_bit(2222).unwrap(); + extension.flip_bin_array_bit(2225).unwrap(); + + assert_eq!(extension.iter_bitmap(-513, -5555).unwrap().unwrap(), -1111); + assert_eq!(extension.iter_bitmap(-5555, -513).unwrap().unwrap(), -2225); + + assert_eq!(extension.iter_bitmap(513, 5555).unwrap().unwrap(), 1111); + assert_eq!(extension.iter_bitmap(5555, 513).unwrap().unwrap(), 2225); + } + + #[test] + fn test_iter_map_ajacent_items_negative_index() { + let mut extension = BinArrayBitmapExtension::default(); + extension.flip_bin_array_bit(-1111).unwrap(); + extension.flip_bin_array_bit(-1113).unwrap(); + extension.flip_bin_array_bit(-1115).unwrap(); + assert_eq!(extension.iter_bitmap(-1115, -1111).unwrap().unwrap(), -1115); + assert_eq!(extension.iter_bitmap(-1114, -1111).unwrap().unwrap(), -1113); + assert_eq!(extension.iter_bitmap(-1111, -1115).unwrap().unwrap(), -1111); + assert_eq!(extension.iter_bitmap(-1112, -1115).unwrap().unwrap(), -1113); + } + + #[test] + fn test_iter_map_ajacent_items_possitive_index() { + let mut extension = BinArrayBitmapExtension::default(); + extension.flip_bin_array_bit(1111).unwrap(); + extension.flip_bin_array_bit(1113).unwrap(); + extension.flip_bin_array_bit(1115).unwrap(); + + assert_eq!(extension.iter_bitmap(1111, 1115).unwrap().unwrap(), 1111); + assert_eq!(extension.iter_bitmap(1112, 1115).unwrap().unwrap(), 1113); + assert_eq!(extension.iter_bitmap(1115, 1111).unwrap().unwrap(), 1115); + assert_eq!(extension.iter_bitmap(1114, 1111).unwrap().unwrap(), 1113); + } + + proptest! { + #[test] + fn test_is_bin_range_empty_extension( + from_offset in 0..=511, + to_offset in 0..=511, + flip_offset in 0..=511) { + + if from_offset > to_offset { + return Ok(()); + } + + // flip + let bin_array_bitmap = U512::from_limbs([0u64;8]); + let mask= one::<512, 8>() << (flip_offset as usize); + let bin_array_bitmap: Uint<512,8> = bin_array_bitmap.bitxor(mask); + + let is_zero_liquidity = BinArrayBitmapExtension::is_bin_range_empty(&bin_array_bitmap, from_offset as usize, to_offset as usize).unwrap(); + + if flip_offset >= from_offset && flip_offset <= to_offset { + assert!(!is_zero_liquidity); + }else{ + assert!(is_zero_liquidity); + } + } + + #[test] + fn test_is_bin_array_range_empty_positive_extension( + from_index in 512..=6655, //(BIN_ARRAY_BITMAP_SIZE * (EXTENSION_BINARRAY_BITMAP_SIZE as i32 + 1) - 1)) + to_index in 512..=6655, + flip_index in 512..=6655) { + + if from_index > to_index { + return Ok(()); + } + + let mut extension = BinArrayBitmapExtension::default(); + extension.flip_bin_array_bit(flip_index).unwrap(); + + let (from_offset, bin_array_from_offset) = + BinArrayBitmapExtension::get_offset_and_bin_array_offset_in_bitmap( + from_index, + ).unwrap(); + + let (to_offset, bin_array_to_offset) = + BinArrayBitmapExtension::get_offset_and_bin_array_offset_in_bitmap( + to_index, + ).unwrap(); + + let is_zero_liquidity = BinArrayBitmapExtension::is_bin_array_range_empty( + &extension.positive_bin_array_bitmap, + from_offset, + to_offset, + bin_array_from_offset, + bin_array_to_offset, + ).unwrap(); + if flip_index >= from_index && flip_index <= to_index { + assert!(!is_zero_liquidity); + }else{ + assert!(is_zero_liquidity); + } + } + + #[test] + fn test_is_bin_array_range_empty_positive_extension_double( + from_index in 512..=6655, //(BIN_ARRAY_BITMAP_SIZE * (EXTENSION_BINARRAY_BITMAP_SIZE as i32 + 1) - 1)) + to_index in 512..=6655, + flip_index_1 in 512..=6655, + flip_index_2 in 512..=6655) { + + if from_index > to_index { + return Ok(()); + } + + let mut extension = BinArrayBitmapExtension::default(); + extension.flip_bin_array_bit(flip_index_1).unwrap(); + if flip_index_1 != flip_index_2 { + extension.flip_bin_array_bit(flip_index_2).unwrap(); + } + + let (from_offset, bin_array_from_offset) = + BinArrayBitmapExtension::get_offset_and_bin_array_offset_in_bitmap( + from_index, + ).unwrap(); + + let (to_offset, bin_array_to_offset) = + BinArrayBitmapExtension::get_offset_and_bin_array_offset_in_bitmap( + to_index, + ).unwrap(); + + let is_zero_liquidity = BinArrayBitmapExtension::is_bin_array_range_empty( + &extension.positive_bin_array_bitmap, + from_offset, + to_offset, + bin_array_from_offset, + bin_array_to_offset, + ).unwrap(); + if (flip_index_1 >= from_index && flip_index_1 <= to_index) || (flip_index_2 >= from_index && flip_index_2 <= to_index) { + assert!(!is_zero_liquidity); + }else{ + assert!(is_zero_liquidity); + } + } + + #[test] + fn test_is_bin_array_range_empty_negative_extension_single( + from_index in -6656..=-512, //(BIN_ARRAY_BITMAP_SIZE * (EXTENSION_BINARRAY_BITMAP_SIZE as i32 + 1) - 1)) + to_index in -6656..=-512, + flip_index in -6656..=-512) { + + if from_index > to_index { + return Ok(()); + } + + let mut extension = BinArrayBitmapExtension::default(); + extension.flip_bin_array_bit(flip_index).unwrap(); + + let (from_offset, bin_array_from_offset) = + BinArrayBitmapExtension::get_offset_and_bin_array_offset_in_bitmap( + to_index, + ).unwrap(); + + let (to_offset, bin_array_to_offset) = + BinArrayBitmapExtension::get_offset_and_bin_array_offset_in_bitmap( + from_index, + ).unwrap(); + + let is_zero_liquidity = BinArrayBitmapExtension::is_bin_array_range_empty( + &extension.negative_bin_array_bitmap, + from_offset, + to_offset, + bin_array_from_offset, + bin_array_to_offset, + ).unwrap(); + if flip_index >= from_index && flip_index <= to_index { + assert!(!is_zero_liquidity); + }else{ + assert!(is_zero_liquidity); + } + } + + #[test] + fn test_is_bin_array_range_empty_negative_extension_double( + from_index in -6656..=-512, //(BIN_ARRAY_BITMAP_SIZE * (EXTENSION_BINARRAY_BITMAP_SIZE as i32 + 1) - 1)) + to_index in -6656..=-512, + flip_index_1 in -6656..=-512, + flip_index_2 in -6656..=-512) { + + if from_index > to_index { + return Ok(()); + } + + let mut extension = BinArrayBitmapExtension::default(); + extension.flip_bin_array_bit(flip_index_1).unwrap(); + if flip_index_2 != flip_index_1 { + extension.flip_bin_array_bit(flip_index_2).unwrap(); + } + + let (from_offset, bin_array_from_offset) = + BinArrayBitmapExtension::get_offset_and_bin_array_offset_in_bitmap( + to_index, + ).unwrap(); + + let (to_offset, bin_array_to_offset) = + BinArrayBitmapExtension::get_offset_and_bin_array_offset_in_bitmap( + from_index, + ).unwrap(); + + let is_zero_liquidity = BinArrayBitmapExtension::is_bin_array_range_empty( + &extension.negative_bin_array_bitmap, + from_offset, + to_offset, + bin_array_from_offset, + bin_array_to_offset, + ).unwrap(); + if (flip_index_1 >= from_index && flip_index_1 <= to_index) || (flip_index_2 >= from_index && flip_index_2 <= to_index){ + assert!(!is_zero_liquidity); + }else{ + assert!(is_zero_liquidity); + } + } + } + + proptest! { + #[test] + fn test_next_possitive_bin_array_index_with_liquidity( + swap_for_y in 0..=1, + start_index in 512..6655, + flip_id in 512..6655 + + ) { + let mut extension = BinArrayBitmapExtension::default(); + extension.flip_bin_array_bit(flip_id).unwrap(); + assert_eq!(extension.bit(flip_id).unwrap(), true); + + let swap_for_y = if swap_for_y == 0 { + false + }else{ + true + }; + let result = extension.next_bin_array_index_with_liquidity(swap_for_y, start_index); + + if swap_for_y { + let (bin_id, ok) = result.unwrap(); + if start_index >= flip_id { + assert_eq!(bin_id, flip_id); + assert_eq!(ok, true); + }else{ + assert_eq!(bin_id, 511); + assert_eq!(ok, false); + } + }else{ + if start_index > flip_id { + assert_eq!(result.is_err(), true); + }else{ + let (bin_id, ok) = result.unwrap(); + assert_eq!(bin_id, flip_id); + assert_eq!(ok, true); + + } + } + } + + #[test] + fn test_next_negative_bin_array_index_with_liquidity( + swap_for_y in 0..=1, + start_index in -6656..-513, + flip_id in -6656..-513 + + ) { + let mut extension = BinArrayBitmapExtension::default(); + extension.flip_bin_array_bit(flip_id).unwrap(); + assert_eq!(extension.bit(flip_id).unwrap(), true); + + let swap_for_y = if swap_for_y == 0 { + false + }else{ + true + }; + let result = extension.next_bin_array_index_with_liquidity(swap_for_y, start_index); + + if swap_for_y { + + if start_index < flip_id { + assert_eq!(result.is_err(), true); + }else{ + let (bin_id, ok) = result.unwrap(); + assert_eq!(bin_id, flip_id); + assert_eq!(ok, true); + } + }else{ + let (bin_id, ok) = result.unwrap(); + if start_index <= flip_id { + assert_eq!(bin_id, flip_id); + assert_eq!(ok, true); + }else{ + assert_eq!(bin_id, -512); + assert_eq!(ok, false); + + } + } + } + } +} diff --git a/programs/lb_clmm/src/state/dynamic_position.rs b/programs/lb_clmm/src/state/dynamic_position.rs new file mode 100644 index 0000000..33b99f4 --- /dev/null +++ b/programs/lb_clmm/src/state/dynamic_position.rs @@ -0,0 +1,583 @@ +use super::position::PositionV2; +use crate::constants::NUM_REWARDS; +use crate::errors::LBError; +use crate::manager::bin_array_manager::BinArrayManager; +use crate::math::safe_math::SafeMath; +use crate::math::u128x128_math::Rounding; +use crate::math::u64x64_math::SCALE_OFFSET; +use crate::math::utils_math::safe_mul_shr_cast; +use crate::state::bin::Bin; +use crate::state::position::Position; +use crate::state::position::{FeeInfo, UserRewardInfo}; +use anchor_lang::prelude::*; +use num_enum::{IntoPrimitive, TryFromPrimitive}; +use num_traits::Zero; +use std::cell::RefMut; +#[derive(Copy, Clone, Debug, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)] +#[repr(u8)] +/// Side of resize, 0 for lower and 1 for upper +pub enum ResizeSide { + Lower, + Upper, +} + +pub fn get_idx(bin_id: i32, lower_bin_id: i32) -> Result { + Ok(bin_id.safe_sub(lower_bin_id)? as usize) +} + +/// Extension trait for loading dynamic-sized data in a zero-copy dynamic position account. +pub trait DynamicPositionLoader<'info> { + fn load_content_mut<'a>(&'a self) -> Result>; + fn load_content_init<'a>(&'a self) -> Result>; + fn load_content<'a>(&'a self) -> Result>; +} + +#[account(zero_copy)] +#[derive(InitSpace, Debug)] +pub struct PositionV3 { + /// The LB pair of this position + pub lb_pair: Pubkey, + /// Owner of the position. Client rely on this to to fetch their positions. + pub owner: Pubkey, + /// Lower bin ID + pub lower_bin_id: i32, + /// Upper bin ID + pub upper_bin_id: i32, + /// Last updated timestamp + pub last_updated_at: i64, + /// Total claimed token fee X + pub total_claimed_fee_x_amount: u64, + /// Total claimed token fee Y + pub total_claimed_fee_y_amount: u64, + /// Total claimed rewards + pub total_claimed_rewards: [u64; 2], + /// Operator of position + pub operator: Pubkey, + /// Slot which the locked liquidity can be withdraw + pub lock_release_slot: u64, + /// Is the position subjected to liquidity locking for the launch pool. + pub subjected_to_bootstrap_liquidity_locking: u8, + /// Padding + pub padding_0: [u8; 7], + /// Address is able to claim fee in this position, only valid for bootstrap_liquidity_position + pub fee_owner: Pubkey, + /// Number of bins + pub length: u64, + /// Reserved space for future use + pub _reserved: [u8; 128], +} + +impl Default for PositionV3 { + fn default() -> Self { + Self { + lb_pair: Pubkey::default(), + owner: Pubkey::default(), + lower_bin_id: 0, + upper_bin_id: 0, + last_updated_at: 0, + total_claimed_fee_x_amount: 0, + total_claimed_fee_y_amount: 0, + total_claimed_rewards: [0u64; 2], + operator: Pubkey::default(), + lock_release_slot: 0, + subjected_to_bootstrap_liquidity_locking: 0, + padding_0: [0u8; 7], + fee_owner: Pubkey::default(), + length: 0, + _reserved: [0u8; 128], + } + } +} + +impl PositionV3 { + pub fn init( + &mut self, + lb_pair: Pubkey, + owner: Pubkey, + operator: Pubkey, + lower_bin_id: i32, + upper_bin_id: i32, + width: i32, + current_time: i64, + seed_liquidity_release_slot: u64, + subjected_to_initial_liquidity_locking: bool, + fee_owner: Pubkey, + ) -> Result<()> { + self.lb_pair = lb_pair; + self.owner = owner; + self.operator = operator; + + self.lower_bin_id = lower_bin_id; + self.upper_bin_id = upper_bin_id; + + self.length = width as u64; + + self.last_updated_at = current_time; + + self.lock_release_slot = seed_liquidity_release_slot; + self.subjected_to_bootstrap_liquidity_locking = + subjected_to_initial_liquidity_locking.into(); + self.fee_owner = fee_owner; + + Ok(()) + } + + pub fn increase_length(&mut self, length_to_increase: u64) -> Result<()> { + self.length = self.length.safe_add(length_to_increase)?; + Ok(()) + } + + pub fn space(bin_count: usize) -> usize { + 8 + PositionV3::INIT_SPACE + bin_count as usize * PositionBinData::INIT_SPACE + } + + pub fn new_space_after_add( + length_to_add: u64, + account_loader: &AccountLoader<'_, PositionV3>, + ) -> Result { + let global_data = account_loader.load()?; + Ok(PositionV3::space( + (global_data.length.safe_add(length_to_add)?) as usize, + )) + } + + pub fn new_space_after_remove( + length_to_remove: u64, + account_loader: &AccountLoader<'_, PositionV3>, + ) -> Result { + let global_data = account_loader.load()?; + Ok(PositionV3::space( + (global_data.length.safe_sub(length_to_remove)?) as usize, + )) + } + + pub fn accumulate_total_claimed_fees(&mut self, fee_x: u64, fee_y: u64) { + self.total_claimed_fee_x_amount = self.total_claimed_fee_x_amount.wrapping_add(fee_x); + self.total_claimed_fee_y_amount = self.total_claimed_fee_y_amount.wrapping_add(fee_y); + } + + pub fn accumulate_total_claimed_rewards(&mut self, reward_index: usize, reward: u64) { + let total_claimed_reward = self.total_claimed_rewards[reward_index]; + self.total_claimed_rewards[reward_index] = total_claimed_reward.wrapping_add(reward); + } + + pub fn is_subjected_to_initial_liquidity_locking(&self) -> bool { + self.subjected_to_bootstrap_liquidity_locking != 0 + } + pub fn set_lock_release_slot(&mut self, lock_release_slot: u64) { + self.lock_release_slot = lock_release_slot; + } +} + +/// An position struct loaded with dynamic sized data type +#[derive(Debug)] +pub struct DynamicPosition<'a> { + global_data: RefMut<'a, PositionV3>, + position_bin_data: RefMut<'a, [PositionBinData]>, +} + +impl<'a> DynamicPosition<'a> { + pub fn increase_length(&mut self, length_to_add: u64, side: ResizeSide) -> Result<()> { + self.global_data.length = self.global_data.length.safe_add(length_to_add)?; + match side { + ResizeSide::Lower => { + self.global_data.lower_bin_id = self + .global_data + .lower_bin_id + .safe_sub(length_to_add as i32)?; + // shift position_bin_data to right + self.position_bin_data.rotate_right(length_to_add as usize); + } + ResizeSide::Upper => { + self.global_data.upper_bin_id = self + .global_data + .upper_bin_id + .safe_add(length_to_add as i32)?; + } + } + Ok(()) + } + + pub fn decrease_length(&mut self, length_to_remove: u64, side: ResizeSide) -> Result<()> { + self.global_data.length = self.global_data.length.safe_sub(length_to_remove)?; + match side { + ResizeSide::Lower => { + self.global_data.lower_bin_id = self + .global_data + .lower_bin_id + .safe_add(length_to_remove as i32)?; + // shift position_bin_data to left + self.position_bin_data + .rotate_left(length_to_remove as usize); + } + ResizeSide::Upper => { + self.global_data.upper_bin_id = self + .global_data + .upper_bin_id + .safe_sub(length_to_remove as i32)?; + } + } + Ok(()) + } + pub fn set_last_updated_at(&mut self, current_time: i64) { + self.global_data.last_updated_at = current_time; + } + /// Update reward + fee earning + pub fn update_earning_per_token_stored( + &mut self, + bin_array_manager: &BinArrayManager, + min_bin_id: i32, + max_bin_id: i32, + ) -> Result<()> { + self.assert_valid_bin_range_for_modify_position(min_bin_id, max_bin_id)?; + let (bin_arrays_lower_bin_id, bin_arrays_upper_bin_id) = + bin_array_manager.get_lower_upper_bin_id()?; + + require!( + min_bin_id >= bin_arrays_lower_bin_id && max_bin_id <= bin_arrays_upper_bin_id, + LBError::InvalidBinArray + ); + + for bin_id in min_bin_id..=max_bin_id { + let bin = bin_array_manager.get_bin(bin_id)?; + let idx = get_idx(bin_id, self.global_data.lower_bin_id)?; + let bin_data = &mut self.position_bin_data[idx]; + bin_data.update_reward_per_token_stored(&bin)?; + bin_data.update_fee_per_token_stored(&bin)?; + } + + Ok(()) + } + + pub fn id_within_position(&self, id: i32) -> Result<()> { + require!( + id >= self.global_data.lower_bin_id && id <= self.global_data.upper_bin_id, + LBError::InvalidPosition + ); + Ok(()) + } + + pub fn deposit(&mut self, bin_id: i32, liquidity_share: u128) -> Result<()> { + self.id_within_position(bin_id)?; + let idx: usize = get_idx(bin_id, self.global_data.lower_bin_id)?; + let bin_data = &mut self.position_bin_data[idx]; + bin_data.liquidity_share = bin_data.liquidity_share.safe_add(liquidity_share)?; + Ok(()) + } + + pub fn claim_fee(&mut self, min_bin_id: i32, max_bin_id: i32) -> Result<(u64, u64)> { + self.assert_valid_bin_range_for_modify_position(min_bin_id, max_bin_id)?; + + let mut fee_x = 0; + let mut fee_y = 0; + let min_idx = get_idx(min_bin_id, self.global_data.lower_bin_id)?; + let max_idx = get_idx(max_bin_id, self.global_data.lower_bin_id)?; + + for idx in min_idx..=max_idx { + let fee_info = &mut self.position_bin_data[idx].fee_info; + fee_x = fee_x.safe_add(fee_info.fee_x_pending)?; + fee_info.fee_x_pending = 0; + + fee_y = fee_y.safe_add(fee_info.fee_y_pending)?; + fee_info.fee_y_pending = 0; + } + Ok((fee_x, fee_y)) + } + + pub fn accumulate_total_claimed_fees(&mut self, fee_x: u64, fee_y: u64) { + self.global_data.accumulate_total_claimed_fees(fee_x, fee_y) + } + + pub fn owner(&self) -> Pubkey { + self.global_data.owner + } + + pub fn fee_owner(&self) -> Pubkey { + self.global_data.fee_owner + } + + pub fn lower_bin_id(&self) -> i32 { + self.global_data.lower_bin_id + } + + pub fn upper_bin_id(&self) -> i32 { + self.global_data.upper_bin_id + } + + pub fn lb_pair(&self) -> Pubkey { + self.global_data.lb_pair + } + + pub fn length(&self) -> u64 { + self.global_data.length + } + + pub fn assert_valid_bin_range_for_modify_position( + &self, + min_bin_id: i32, + max_bin_id: i32, + ) -> Result<()> { + require!( + min_bin_id >= self.global_data.lower_bin_id + && max_bin_id <= self.global_data.upper_bin_id, + LBError::InvalidPosition + ); + Ok(()) + } + + pub fn get_total_reward( + &self, + reward_index: usize, + min_bin_id: i32, + max_bin_id: i32, + ) -> Result { + self.assert_valid_bin_range_for_modify_position(min_bin_id, max_bin_id)?; + let mut total_reward = 0; + + let min_idx = get_idx(min_bin_id, self.global_data.lower_bin_id)?; + let max_idx = get_idx(max_bin_id, self.global_data.lower_bin_id)?; + for idx in min_idx..=max_idx { + total_reward = total_reward + .safe_add(self.position_bin_data[idx].reward_info.reward_pendings[reward_index])?; + } + Ok(total_reward) + } + + pub fn reset_all_pending_reward( + &mut self, + reward_index: usize, + min_bin_id: i32, + max_bin_id: i32, + ) -> Result<()> { + self.assert_valid_bin_range_for_modify_position(min_bin_id, max_bin_id)?; + let min_idx = get_idx(min_bin_id, self.global_data.lower_bin_id)?; + let max_idx = get_idx(max_bin_id, self.global_data.lower_bin_id)?; + for idx in min_idx..=max_idx { + self.position_bin_data[idx].reward_info.reward_pendings[reward_index] = 0; + } + Ok(()) + } + + pub fn accumulate_total_claimed_rewards(&mut self, reward_index: usize, reward: u64) { + self.global_data + .accumulate_total_claimed_rewards(reward_index, reward) + } + + /// Position is empty when rewards is 0, fees is 0, and liquidity share is 0. + pub fn is_empty(&self, min_bin_id: i32, max_bin_id: i32) -> Result { + self.assert_valid_bin_range_for_modify_position(min_bin_id, max_bin_id)?; + let min_idx = get_idx(min_bin_id, self.global_data.lower_bin_id)?; + let max_idx = get_idx(max_bin_id, self.global_data.lower_bin_id)?; + for idx in min_idx..=max_idx { + let position_bin_data = &self.position_bin_data[idx]; + if !position_bin_data.liquidity_share.is_zero() { + return Ok(false); + } + let reward_infos = &position_bin_data.reward_info; + + for reward_pending in reward_infos.reward_pendings { + if !reward_pending.is_zero() { + return Ok(false); + } + } + + let fee_infos = &position_bin_data.fee_info; + if !fee_infos.fee_x_pending.is_zero() || !fee_infos.fee_y_pending.is_zero() { + return Ok(false); + } + } + Ok(true) + } + + pub fn get_liquidity_share_in_bin(&self, bin_id: i32) -> Result { + self.id_within_position(bin_id)?; + let idx = get_idx(bin_id, self.global_data.lower_bin_id)?; + Ok(self.position_bin_data[idx].liquidity_share) + } + + pub fn withdraw(&mut self, bin_id: i32, liquidity_share: u128) -> Result<()> { + self.id_within_position(bin_id)?; + let idx = get_idx(bin_id, self.global_data.lower_bin_id)?; + let position_bin_data = &mut self.position_bin_data[idx]; + position_bin_data.liquidity_share = position_bin_data + .liquidity_share + .safe_sub(liquidity_share)?; + + Ok(()) + } + + pub fn migrate_from_v1(&mut self, position: &Position) -> Result<()> { + self.global_data.lb_pair = position.lb_pair; + self.global_data.owner = position.owner; + self.global_data.lower_bin_id = position.lower_bin_id; + self.global_data.upper_bin_id = position.upper_bin_id; + self.global_data.last_updated_at = position.last_updated_at; + self.global_data.total_claimed_fee_x_amount = position.total_claimed_fee_x_amount; + self.global_data.total_claimed_fee_y_amount = position.total_claimed_fee_y_amount; + self.global_data.total_claimed_rewards = position.total_claimed_rewards; + let width = position.width(); + self.global_data.length = width as u64; + + for i in 0..width { + let position_bin_data = &mut self.position_bin_data[i]; + position_bin_data.liquidity_share = + u128::from(position.liquidity_shares[i]).safe_shl(SCALE_OFFSET.into())?; + position_bin_data.reward_info = position.reward_infos[i]; + position_bin_data.fee_info = position.fee_infos[i]; + } + Ok(()) + } + + pub fn migrate_from_v2(&mut self, position: &PositionV2) -> Result<()> { + self.global_data.lb_pair = position.lb_pair; + self.global_data.owner = position.owner; + self.global_data.lower_bin_id = position.lower_bin_id; + self.global_data.upper_bin_id = position.upper_bin_id; + self.global_data.last_updated_at = position.last_updated_at; + self.global_data.total_claimed_fee_x_amount = position.total_claimed_fee_x_amount; + self.global_data.total_claimed_fee_y_amount = position.total_claimed_fee_y_amount; + self.global_data.total_claimed_rewards = position.total_claimed_rewards; + let width = position.width(); + self.global_data.length = width as u64; + self.global_data.operator = position.operator; + self.global_data.subjected_to_bootstrap_liquidity_locking = + position.subjected_to_bootstrap_liquidity_locking; + self.global_data.lock_release_slot = position.lock_release_slot; + self.global_data.fee_owner = position.fee_owner; + + for i in 0..width { + let position_bin_data = &mut self.position_bin_data[i]; + position_bin_data.liquidity_share = position.liquidity_shares[i]; + position_bin_data.reward_info = position.reward_infos[i]; + position_bin_data.fee_info = position.fee_infos[i]; + } + Ok(()) + } + + pub fn is_liquidity_locked(&self, current_slot: u64) -> bool { + current_slot < self.global_data.lock_release_slot + } +} + +impl PositionBinData { + fn update_fee_per_token_stored(&mut self, bin: &Bin) -> Result<()> { + let fee_infos = &mut self.fee_info; + + let fee_x_per_token_stored = bin.fee_amount_x_per_token_stored; + + let new_fee_x: u64 = safe_mul_shr_cast( + self.liquidity_share + .safe_shr(SCALE_OFFSET.into())? + .try_into() + .map_err(|_| LBError::TypeCastFailed)?, + fee_x_per_token_stored.safe_sub(fee_infos.fee_x_per_token_complete)?, + SCALE_OFFSET, + Rounding::Down, + )?; + + fee_infos.fee_x_pending = new_fee_x.safe_add(fee_infos.fee_x_pending)?; + fee_infos.fee_x_per_token_complete = fee_x_per_token_stored; + + let fee_y_per_token_stored = bin.fee_amount_y_per_token_stored; + + let new_fee_y: u64 = safe_mul_shr_cast( + self.liquidity_share + .safe_shr(SCALE_OFFSET.into())? + .try_into() + .map_err(|_| LBError::TypeCastFailed)?, + fee_y_per_token_stored.safe_sub(fee_infos.fee_y_per_token_complete)?, + SCALE_OFFSET, + Rounding::Down, + )?; + + fee_infos.fee_y_pending = new_fee_y.safe_add(fee_infos.fee_y_pending)?; + fee_infos.fee_y_per_token_complete = fee_y_per_token_stored; + + Ok(()) + } + + fn update_reward_per_token_stored(&mut self, bin: &Bin) -> Result<()> { + let reward_info = &mut self.reward_info; + for reward_idx in 0..NUM_REWARDS { + let reward_per_token_stored = bin.reward_per_token_stored[reward_idx]; + + let new_reward: u64 = safe_mul_shr_cast( + self.liquidity_share + .safe_shr(SCALE_OFFSET.into())? + .try_into() + .map_err(|_| LBError::TypeCastFailed)?, + reward_per_token_stored + .safe_sub(reward_info.reward_per_token_completes[reward_idx])?, + SCALE_OFFSET, + Rounding::Down, + )?; + + reward_info.reward_pendings[reward_idx] = + new_reward.safe_add(reward_info.reward_pendings[reward_idx])?; + reward_info.reward_per_token_completes[reward_idx] = reward_per_token_stored; + } + + Ok(()) + } +} +#[zero_copy] +#[derive(Default, Debug, AnchorDeserialize, AnchorSerialize, InitSpace, PartialEq)] +pub struct PositionBinData { + pub liquidity_share: u128, + pub reward_info: UserRewardInfo, + pub fee_info: FeeInfo, +} + +impl<'a> DynamicPosition<'a> { + pub fn new( + global_data: RefMut<'a, PositionV3>, + position_bin_data: RefMut<'a, [PositionBinData]>, + ) -> DynamicPosition<'a> { + Self { + global_data, + position_bin_data, + } + } +} + +fn position_account_split<'a, 'info>( + position_al: &'a AccountLoader<'info, PositionV3>, +) -> Result> { + let data = position_al.as_ref().try_borrow_mut_data()?; + + let (global_data, position_bin_data) = RefMut::map_split(data, |data| { + let (global_bytes, position_bin_data_bytes) = data.split_at_mut(8 + PositionV3::INIT_SPACE); + let global_data = bytemuck::from_bytes_mut::(&mut global_bytes[8..]); + let position_bin_data = + bytemuck::cast_slice_mut::(position_bin_data_bytes); + (global_data, position_bin_data) + }); + + Ok(DynamicPosition::new(global_data, position_bin_data)) +} + +impl<'info> DynamicPositionLoader<'info> for AccountLoader<'info, PositionV3> { + fn load_content_mut<'a>(&'a self) -> Result> { + { + // Re-use anchor internal validation such as discriminator check + self.load_mut()?; + } + position_account_split(&self) + } + + fn load_content_init<'a>(&'a self) -> Result> { + { + // Re-use anchor internal validation and initialization such as insert of discriminator for new zero copy account + self.load_init()?; + } + position_account_split(&self) + } + + fn load_content<'a>(&'a self) -> Result> { + { + // Re-use anchor internal validation and initialization such as insert of discriminator for new zero copy account + self.load()?; + } + position_account_split(&self) + } +} diff --git a/programs/lb_clmm/src/state/lb_pair.rs b/programs/lb_clmm/src/state/lb_pair.rs deleted file mode 100644 index 46e0114..0000000 --- a/programs/lb_clmm/src/state/lb_pair.rs +++ /dev/null @@ -1,814 +0,0 @@ -use std::cmp::min; - -use crate::assert_eq_admin; -use crate::constants::{ - BASIS_POINT_MAX, BIN_ARRAY_BITMAP_SIZE, FEE_PRECISION, MAX_BIN_ID, MAX_FEE_RATE, - MAX_FEE_UPDATE_WINDOW, MIN_BIN_ID, -}; -use crate::instructions::update_fee_parameters::FeeParameter; -use crate::math::u128x128_math::Rounding; -use crate::math::u64x64_math::SCALE_OFFSET; -use crate::math::utils_math::{one, safe_mul_div_cast, safe_mul_shr_cast, safe_shl_div_cast}; -use crate::state::action_access::get_lb_pair_type_access_validator; -use crate::state::bin::BinArray; -use crate::state::bin_array_bitmap_extension::BinArrayBitmapExtension; -use crate::state::parameters::{StaticParameters, VariableParameters}; -use crate::{errors::LBError, math::safe_math::SafeMath}; -use anchor_lang::prelude::*; -use num_enum::{IntoPrimitive, TryFromPrimitive}; -use ruint::aliases::{U1024, U256}; -use std::ops::BitXor; -use std::ops::Shl; -use std::ops::Shr; - -#[derive(Copy, Clone, Debug, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)] -#[repr(u8)] -/// Type of the Pair. 0 = Permissionless, 1 = Permission. Putting 0 as permissionless for backward compatibility. -pub enum PairType { - Permissionless, - Permission, -} - -pub struct LaunchPadParams { - pub activation_slot: u64, - pub swap_cap_deactivate_slot: u64, - pub max_swapped_amount: u64, -} - -impl PairType { - pub fn get_pair_default_launch_pad_params(&self) -> LaunchPadParams { - match self { - // The slot is unreachable. Therefore by default, the pair will be disabled until admin update the activation slot. - Self::Permission => LaunchPadParams { - activation_slot: u64::MAX, - swap_cap_deactivate_slot: u64::MAX, - max_swapped_amount: u64::MAX, - }, - // Activation slot is not used in permissionless pair. Therefore, default to 0. - Self::Permissionless => LaunchPadParams { - activation_slot: 0, - swap_cap_deactivate_slot: 0, - max_swapped_amount: 0, - }, - } - } -} - -#[derive( - AnchorSerialize, AnchorDeserialize, Debug, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, -)] -#[repr(u8)] -/// Pair status. 0 = Enabled, 1 = Disabled. Putting 0 as enabled for backward compatibility. -pub enum PairStatus { - // Fully enabled. - // Condition: - // Permissionless: PairStatus::Enabled - // Permission: PairStatus::Enabled and clock.slot > activation_slot - Enabled, - // Similar as emergency mode. User can only withdraw (Only outflow). Except whitelisted wallet still have full privileges. - Disabled, -} - -#[zero_copy] -#[derive(InitSpace, Default, Debug)] -pub struct ProtocolFee { - pub amount_x: u64, - pub amount_y: u64, -} - -#[account(zero_copy)] -#[derive(InitSpace, Debug)] -pub struct LbPair { - pub parameters: StaticParameters, - pub v_parameters: VariableParameters, - pub bump_seed: [u8; 1], - /// Bin step signer seed - pub bin_step_seed: [u8; 2], - /// Type of the pair - pub pair_type: u8, - /// Active bin id - pub active_id: i32, - /// Bin step. Represent the price increment / decrement. - pub bin_step: u16, - /// Status of the pair. Check PairStatus enum. - pub status: u8, - pub require_base_factor_seed: u8, - pub base_factor_seed: [u8; 2], - pub _padding1: [u8; 2], - /// Token X mint - pub token_x_mint: Pubkey, - /// Token Y mint - pub token_y_mint: Pubkey, - /// LB token X vault - pub reserve_x: Pubkey, - /// LB token Y vault - pub reserve_y: Pubkey, - /// Uncollected protocol fee - pub protocol_fee: ProtocolFee, - /// Protocol fee owner, - pub fee_owner: Pubkey, - /// Farming reward information - pub reward_infos: [RewardInfo; 2], // TODO: Bug in anchor IDL parser when using InitSpace macro. Temp hardcode it. https://github.com/coral-xyz/anchor/issues/2556 - /// Oracle pubkey - pub oracle: Pubkey, - /// Packed initialized bin array state - pub bin_array_bitmap: [u64; 16], // store default bin id from -512 to 511 (bin id from -35840 to 35840, price from 2.7e-16 to 3.6e15) - /// Last time the pool fee parameter was updated - pub last_updated_at: i64, - /// Whitelisted wallet - pub whitelisted_wallet: [Pubkey; 2], - /// Base keypair. Only required for permission pair - pub base_key: Pubkey, - /// Slot to enable the pair. Only applicable for permission pair. - pub activation_slot: u64, - /// Last slot until pool remove max_swapped_amount for buying - pub swap_cap_deactivate_slot: u64, - /// Max X swapped amount user can swap from y to x between activation_slot and last_slot - pub max_swapped_amount: u64, - /// Liquidity lock duration for positions which created before activate. Only applicable for permission pair. - pub lock_durations_in_slot: u64, - /// Pool creator - pub creator: Pubkey, - /// Reserved space for future use - pub _reserved: [u8; 24], -} - -impl Default for LbPair { - fn default() -> Self { - let LaunchPadParams { - activation_slot, - max_swapped_amount, - swap_cap_deactivate_slot, - } = PairType::Permissionless.get_pair_default_launch_pad_params(); - Self { - active_id: 0, - parameters: StaticParameters::default(), - v_parameters: VariableParameters::default(), - bump_seed: [0u8; 1], - bin_step: 0, - token_x_mint: Pubkey::default(), - token_y_mint: Pubkey::default(), - bin_step_seed: [0u8; 2], - fee_owner: Pubkey::default(), - protocol_fee: ProtocolFee::default(), - reserve_x: Pubkey::default(), - reserve_y: Pubkey::default(), - reward_infos: [RewardInfo::default(); 2], - oracle: Pubkey::default(), - bin_array_bitmap: [0u64; 16], - last_updated_at: 0, - pair_type: PairType::Permissionless.into(), - status: 0, - whitelisted_wallet: [Pubkey::default(); 2], - base_key: Pubkey::default(), - activation_slot, - swap_cap_deactivate_slot, - max_swapped_amount, - creator: Pubkey::default(), - lock_durations_in_slot: 0, - require_base_factor_seed: 0, - base_factor_seed: [0u8; 2], - _padding1: [0u8; 2], - _reserved: [0u8; 24], - } - } -} - -/// Stores the state relevant for tracking liquidity mining rewards -#[zero_copy] -#[derive(InitSpace, Default, Debug, PartialEq)] -pub struct RewardInfo { - /// Reward token mint. - pub mint: Pubkey, - /// Reward vault token account. - pub vault: Pubkey, - /// Authority account that allows to fund rewards - pub funder: Pubkey, - /// TODO check whether we need to store it in pool - pub reward_duration: u64, // 8 - /// TODO check whether we need to store it in pool - pub reward_duration_end: u64, // 8 - /// TODO check whether we need to store it in pool - pub reward_rate: u128, // 8 - /// The last time reward states were updated. - pub last_update_time: u64, // 8 - /// Accumulated seconds where when farm distribute rewards, but the bin is empty. The reward will be accumulated for next reward time window. - pub cumulative_seconds_with_empty_liquidity_reward: u64, -} - -impl RewardInfo { - /// Returns true if this reward is initialized. - /// Once initialized, a reward cannot transition back to uninitialized. - pub fn initialized(&self) -> bool { - self.mint.ne(&Pubkey::default()) - } - - pub fn is_valid_funder(&self, funder: Pubkey) -> bool { - assert_eq_admin(funder) || funder.eq(&self.funder) - } - - pub fn init_reward( - &mut self, - mint: Pubkey, - vault: Pubkey, - funder: Pubkey, - reward_duration: u64, - ) { - self.mint = mint; - self.vault = vault; - self.funder = funder; - self.reward_duration = reward_duration; - } - - pub fn update_last_update_time(&mut self, current_time: u64) { - self.last_update_time = std::cmp::min(current_time, self.reward_duration_end); - } - - pub fn get_seconds_elapsed_since_last_update(&self, current_time: u64) -> Result { - let last_time_reward_applicable = std::cmp::min(current_time, self.reward_duration_end); - let time_period = last_time_reward_applicable.safe_sub(self.last_update_time.into())?; - - Ok(time_period) - } - - // To make it simple we truncate decimals of liquidity_supply for the calculation - pub fn calculate_reward_per_token_stored_since_last_update( - &self, - current_time: u64, - liquidity_supply: u64, - ) -> Result { - let time_period = self.get_seconds_elapsed_since_last_update(current_time)?; - - safe_mul_div_cast( - time_period.into(), - self.reward_rate, - liquidity_supply.into(), - Rounding::Down, - ) - } - - pub fn calculate_reward_accumulated_since_last_update( - &self, - current_time: u64, - ) -> Result { - let last_time_reward_applicable = std::cmp::min(current_time, self.reward_duration_end); - - let time_period = - U256::from(last_time_reward_applicable.safe_sub(self.last_update_time.into())?); - - Ok(time_period.safe_mul(U256::from(self.reward_rate))?) - } - - /// Farming rate after funding - pub fn update_rate_after_funding( - &mut self, - current_time: u64, - funding_amount: u64, - ) -> Result<()> { - let reward_duration_end = self.reward_duration_end; - let total_amount: u64; - - if current_time >= reward_duration_end { - total_amount = funding_amount - } else { - let remaining_seconds = reward_duration_end.safe_sub(current_time)?; - let leftover: u64 = safe_mul_shr_cast( - self.reward_rate, - remaining_seconds.into(), - SCALE_OFFSET, - Rounding::Down, - )?; - - total_amount = leftover.safe_add(funding_amount)?; - } - - self.reward_rate = safe_shl_div_cast( - total_amount.into(), - self.reward_duration.into(), - SCALE_OFFSET, - Rounding::Down, - )?; - self.last_update_time = current_time; - self.reward_duration_end = current_time.safe_add(self.reward_duration)?; - - Ok(()) - } -} - -impl LbPair { - pub fn initialize( - &mut self, - bump: u8, - active_id: i32, - bin_step: u16, - token_mint_x: Pubkey, - token_mint_y: Pubkey, - reserve_x: Pubkey, - reserve_y: Pubkey, - oracle: Pubkey, - static_parameter: StaticParameters, - pair_type: PairType, - pair_status: u8, - base_key: Pubkey, - lock_duration_in_slot: u64, - creator: Pubkey, - ) -> Result<()> { - self.parameters = static_parameter; - self.active_id = active_id; - self.bin_step = bin_step; - self.token_x_mint = token_mint_x; - self.token_y_mint = token_mint_y; - self.reserve_x = reserve_x; - self.reserve_y = reserve_y; - self.fee_owner = crate::fee_owner::ID; - self.bump_seed = [bump]; - self.bin_step_seed = bin_step.to_le_bytes(); - self.oracle = oracle; - self.pair_type = pair_type.into(); - self.base_key = base_key; - self.status = pair_status; - self.creator = creator; - - let LaunchPadParams { - activation_slot, - swap_cap_deactivate_slot, - max_swapped_amount, - } = pair_type.get_pair_default_launch_pad_params(); - - self.activation_slot = activation_slot; - self.swap_cap_deactivate_slot = swap_cap_deactivate_slot; - self.max_swapped_amount = max_swapped_amount; - self.lock_durations_in_slot = lock_duration_in_slot; - - Ok(()) - } - - pub fn get_release_slot(&self, current_slot: u64) -> Result { - let release_slot = match self.pair_type()? { - PairType::Permission => { - if self.lock_durations_in_slot > 0 && current_slot < self.activation_slot { - self.activation_slot - .saturating_add(self.lock_durations_in_slot) - } else { - 0 - } - } - _ => 0, - }; - - Ok(release_slot) - } - - pub fn update_whitelisted_wallet(&mut self, idx: usize, wallet: Pubkey) -> Result<()> { - require!(idx < self.whitelisted_wallet.len(), LBError::InvalidIndex); - self.whitelisted_wallet[idx] = wallet; - - Ok(()) - } - - pub fn add_whitelist_wallet(&mut self, wallet: Pubkey) -> Result<()> { - let mut index = None; - - for (idx, whitelisted) in self.whitelisted_wallet.iter().enumerate() { - if whitelisted.eq(&wallet) { - return Ok(()); // Wallet already exists in the whitelist, returning early - } - if index.is_none() && whitelisted.eq(&Pubkey::default()) { - index = Some(idx); - } - } - - match index { - Some(idx) => { - self.whitelisted_wallet[idx] = wallet; - Ok(()) - } - None => Err(LBError::ExceedMaxWhitelist.into()), - } - } - - pub fn get_swap_cap_status_and_amount( - &self, - current_time: u64, - swap_for_y: bool, - ) -> Result<(bool, u64)> { - let pair_type_access_validator = get_lb_pair_type_access_validator(self, current_time)?; - Ok(pair_type_access_validator.get_swap_cap_status_and_amount(swap_for_y)) - } - - pub fn status(&self) -> Result { - let pair_status: PairStatus = self - .status - .try_into() - .map_err(|_| LBError::TypeCastFailed)?; - - Ok(pair_status) - } - - pub fn pair_type(&self) -> Result { - let pair_type: PairType = self - .pair_type - .try_into() - .map_err(|_| LBError::TypeCastFailed)?; - - Ok(pair_type) - } - - pub fn is_permission_pair(&self) -> Result { - let pair_type = self.pair_type()?; - Ok(pair_type.eq(&PairType::Permission)) - } - - pub fn update_fee_parameters(&mut self, parameter: &FeeParameter) -> Result<()> { - let current_timestamp = Clock::get()?.unix_timestamp; - if self.last_updated_at > 0 { - let second_elapsed = current_timestamp.safe_sub(self.last_updated_at)?; - - require!( - second_elapsed > MAX_FEE_UPDATE_WINDOW, - LBError::ExcessiveFeeUpdate - ); - } - - self.parameters.update(parameter)?; - self.last_updated_at = current_timestamp; - - Ok(()) - } - - pub fn seeds(&self) -> Result> { - let min_key = min(self.token_x_mint, self.token_y_mint); - let (min_key_ref, max_key_ref) = if min_key == self.token_x_mint { - (self.token_x_mint.as_ref(), self.token_y_mint.as_ref()) - } else { - (self.token_y_mint.as_ref(), self.token_x_mint.as_ref()) - }; - if self.is_permission_pair()? { - Ok(vec![ - self.base_key.as_ref(), - min_key_ref, - max_key_ref, - &self.bin_step_seed, - &self.bump_seed, - ]) - } else { - Ok(vec![ - min_key_ref, - max_key_ref, - &self.bin_step_seed, - &self.bump_seed, - ]) - } - } - - #[inline(always)] - pub fn swap_for_y(&self, out_token_mint: Pubkey) -> bool { - out_token_mint.eq(&self.token_y_mint) - } - - /// Plus / Minus 1 to the active bin based on the swap direction - pub fn advance_active_bin(&mut self, swap_for_y: bool) -> Result<()> { - let next_active_bin_id = if swap_for_y { - self.active_id.safe_sub(1)? - } else { - self.active_id.safe_add(1)? - }; - - require!( - next_active_bin_id >= MIN_BIN_ID && next_active_bin_id <= MAX_BIN_ID, - LBError::PairInsufficientLiquidity - ); - - self.active_id = next_active_bin_id; - - Ok(()) - } - - /// Base fee rate = Base fee factor * bin step. This is in 1e9 unit. - pub fn get_base_fee(&self) -> Result { - Ok(u128::from(self.parameters.base_factor) - .safe_mul(self.bin_step.into())? - // Make it to be the same as FEE_PRECISION defined for ceil_div later on. - .safe_mul(10u128)?) - } - - /// Variable fee rate = variable fee factor * (volatility_accumulator * bin_step)^2 - pub fn compute_variable_fee(&self, volatility_accumulator: u32) -> Result { - if self.parameters.variable_fee_control > 0 { - let volatility_accumulator: u128 = volatility_accumulator.into(); - let bin_step: u128 = self.bin_step.into(); - let variable_fee_control: u128 = self.parameters.variable_fee_control.into(); - - let square_vfa_bin = volatility_accumulator - .safe_mul(bin_step)? - .checked_pow(2) - .ok_or(LBError::MathOverflow)?; - - // Variable fee control, volatility accumulator, bin step are in basis point unit (10_000) - // This is 1e20. Which > 1e9. Scale down it to 1e9 unit and ceiling the remaining. - let v_fee = variable_fee_control.safe_mul(square_vfa_bin)?; - - let scaled_v_fee = v_fee.safe_add(99_999_999_999)?.safe_div(100_000_000_000)?; - return Ok(scaled_v_fee); - } - - Ok(0) - } - - /// Variable fee rate = variable_fee_control * (variable_fee_accumulator * bin_step) ^ 2 - pub fn get_variable_fee(&self) -> Result { - self.compute_variable_fee(self.v_parameters.volatility_accumulator) - } - - /// Total fee rate = base_fee_rate + variable_fee_rate - pub fn get_total_fee(&self) -> Result { - let total_fee_rate = self.get_base_fee()?.safe_add(self.get_variable_fee()?)?; - let total_fee_rate_cap = std::cmp::min(total_fee_rate, MAX_FEE_RATE.into()); - Ok(total_fee_rate_cap) - } - - #[cfg(test)] - /// Maximum fee rate - fn get_max_total_fee(&self) -> Result { - let max_total_fee_rate = self - .get_base_fee()? - .safe_add(self.compute_variable_fee(self.parameters.max_volatility_accumulator)?)?; - - let total_fee_rate_cap = std::cmp::min(max_total_fee_rate, MAX_FEE_RATE.into()); - Ok(total_fee_rate_cap) - } - - /// Compute composition fee. Composition_fee = fee_amount * (1 + total fee rate) - pub fn compute_composition_fee(&self, swap_amount: u64) -> Result { - let total_fee_rate = self.get_total_fee()?; - // total_fee_rate 1e9 unit - let fee_amount = u128::from(swap_amount).safe_mul(total_fee_rate)?; - let composition_fee = - fee_amount.safe_mul(u128::from(FEE_PRECISION).safe_add(total_fee_rate)?)?; - // 1e9 unit * 1e9 unit = 1e18, scale back - let scaled_down_fee = composition_fee.safe_div(u128::from(FEE_PRECISION).pow(2))?; - - Ok(scaled_down_fee - .try_into() - .map_err(|_| LBError::TypeCastFailed)?) - } - - /// Compute fee from amount, where fee is part of the amount. The result is ceil-ed. - pub fn compute_fee_from_amount(&self, amount_with_fees: u64) -> Result { - // total_fee_rate 1e9 unit - let total_fee_rate = self.get_total_fee()?; - // Ceil division - let fee_amount = u128::from(amount_with_fees) - .safe_mul(total_fee_rate)? - .safe_add((FEE_PRECISION - 1).into())?; - let scaled_down_fee = fee_amount.safe_div(FEE_PRECISION.into())?; - - Ok(scaled_down_fee - .try_into() - .map_err(|_| LBError::TypeCastFailed)?) - } - - /// Compute fee for the amount. The fee is not part of the amount. This function is used when you do not know the amount_with_fees - /// Solve for fee_amount, equation: (amount + fee_amount) * total_fee_rate / 1e9 = fee_amount - /// fee_amount = (amount * total_fee_rate) / (1e9 - total_fee_rate) - /// The result is ceil-ed. - pub fn compute_fee(&self, amount: u64) -> Result { - let total_fee_rate = self.get_total_fee()?; - let denominator = u128::from(FEE_PRECISION).safe_sub(total_fee_rate)?; - - // Ceil division - let fee = u128::from(amount) - .safe_mul(total_fee_rate)? - .safe_add(denominator)? - .safe_sub(1)?; - - let scaled_down_fee = fee.safe_div(denominator)?; - - Ok(scaled_down_fee - .try_into() - .map_err(|_| LBError::TypeCastFailed)?) - } - - /// Compute protocol fee - pub fn compute_protocol_fee(&self, fee_amount: u64) -> Result { - let protocol_fee = u128::from(fee_amount) - .safe_mul(self.parameters.protocol_share.into())? - .safe_div(BASIS_POINT_MAX as u128)?; - - Ok(protocol_fee - .try_into() - .map_err(|_| LBError::TypeCastFailed)?) - } - - /// Accumulate protocol fee - pub fn accumulate_protocol_fees(&mut self, fee_amount_x: u64, fee_amount_y: u64) -> Result<()> { - self.protocol_fee.amount_x = self.protocol_fee.amount_x.safe_add(fee_amount_x)?; - self.protocol_fee.amount_y = self.protocol_fee.amount_y.safe_add(fee_amount_y)?; - - Ok(()) - } - - /// Update volatility reference and accumulator - pub fn update_volatility_parameters(&mut self, current_timestamp: i64) -> Result<()> { - self.v_parameters.update_volatility_parameter( - self.active_id, - current_timestamp, - &self.parameters, - ) - } - - pub fn update_references(&mut self, current_timestamp: i64) -> Result<()> { - self.v_parameters - .update_references(self.active_id, current_timestamp, &self.parameters) - } - - pub fn update_volatility_accumulator(&mut self) -> Result<()> { - self.v_parameters - .update_volatility_accumulator(self.active_id, &self.parameters) - } - - pub fn withdraw_protocol_fee(&mut self, amount_x: u64, amount_y: u64) -> Result<()> { - self.protocol_fee.amount_x = self.protocol_fee.amount_x.safe_sub(amount_x)?; - self.protocol_fee.amount_y = self.protocol_fee.amount_y.safe_sub(amount_y)?; - - Ok(()) - } - - pub fn set_fee_owner(&mut self, fee_owner: Pubkey) { - self.fee_owner = fee_owner; - } - - pub fn oracle_initialized(&self) -> bool { - self.oracle != Pubkey::default() - } - - pub fn flip_bin_array_bit( - &mut self, - bin_array_bitmap_extension: &Option>, - bin_array_index: i32, - ) -> Result<()> { - if self.is_overflow_default_bin_array_bitmap(bin_array_index) { - match bin_array_bitmap_extension { - Some(bitmap_ext) => { - bitmap_ext.load_mut()?.flip_bin_array_bit(bin_array_index)?; - } - None => return Err(LBError::BitmapExtensionAccountIsNotProvided.into()), - } - } else { - self.flip_bin_array_bit_internal(bin_array_index)?; - } - - Ok(()) - } - - pub fn is_overflow_default_bin_array_bitmap(&self, bin_array_index: i32) -> bool { - let (min_bitmap_id, max_bitmap_id) = LbPair::bitmap_range(); - bin_array_index > max_bitmap_id || bin_array_index < min_bitmap_id - } - - pub fn bitmap_range() -> (i32, i32) { - (-BIN_ARRAY_BITMAP_SIZE, BIN_ARRAY_BITMAP_SIZE - 1) - } - - fn get_bin_array_offset(bin_array_index: i32) -> usize { - (bin_array_index + BIN_ARRAY_BITMAP_SIZE) as usize - } - - fn flip_bin_array_bit_internal(&mut self, bin_array_index: i32) -> Result<()> { - let bin_array_offset = Self::get_bin_array_offset(bin_array_index); - let bin_array_bitmap = U1024::from_limbs(self.bin_array_bitmap); - let mask = one::<1024, 16>() << bin_array_offset; - self.bin_array_bitmap = bin_array_bitmap.bitxor(mask).into_limbs(); - Ok(()) - } - - // return bin_array_index that it's liquidity is non-zero - // if cannot find one, return false - pub fn next_bin_array_index_with_liquidity_internal( - &self, - swap_for_y: bool, - start_array_index: i32, - ) -> Result<(i32, bool)> { - let bin_array_bitmap = U1024::from_limbs(self.bin_array_bitmap); - let array_offset: usize = Self::get_bin_array_offset(start_array_index); - let (min_bitmap_id, max_bitmap_id) = LbPair::bitmap_range(); - if swap_for_y { - let binmap_range: usize = max_bitmap_id - .safe_sub(min_bitmap_id)? - .try_into() - .map_err(|_| LBError::TypeCastFailed)?; - let offset_bit_map = bin_array_bitmap.shl(binmap_range.safe_sub(array_offset)?); - - if offset_bit_map.eq(&U1024::ZERO) { - return Ok((min_bitmap_id.safe_sub(1)?, false)); - } else { - let next_bit = offset_bit_map.leading_zeros(); - return Ok((start_array_index.safe_sub(next_bit as i32)?, true)); - } - } else { - let offset_bit_map = bin_array_bitmap.shr(array_offset); - if offset_bit_map.eq(&U1024::ZERO) { - return Ok((max_bitmap_id.safe_add(1)?, false)); - } else { - let next_bit = offset_bit_map.trailing_zeros(); - return Ok(( - start_array_index.checked_add(next_bit as i32).unwrap(), - true, - )); - }; - } - } - - // shift active until non-zero liquidity bin_array_index - fn shift_active_bin(&mut self, swap_for_y: bool, bin_array_index: i32) -> Result<()> { - // update active id - let (lower_bin_id, upper_bin_id) = - BinArray::get_bin_array_lower_upper_bin_id(bin_array_index)?; - - if swap_for_y { - self.active_id = upper_bin_id; - } else { - self.active_id = lower_bin_id; - } - Ok(()) - } - - fn next_bin_array_index_with_liquidity_from_extension( - swap_for_y: bool, - bin_array_index: i32, - bin_array_bitmap_extension: &Option>, - ) -> Result<(i32, bool)> { - match bin_array_bitmap_extension { - Some(bitmap_ext) => { - return Ok(bitmap_ext - .load()? - .next_bin_array_index_with_liquidity(swap_for_y, bin_array_index)?); - } - None => return Err(LBError::BitmapExtensionAccountIsNotProvided.into()), - } - } - - pub fn next_bin_array_index_from_internal_to_extension( - &mut self, - swap_for_y: bool, - current_array_index: i32, - start_array_index: i32, - bin_array_bitmap_extension: &Option>, - ) -> Result<()> { - let (bin_array_index, is_non_zero_liquidity_flag) = - self.next_bin_array_index_with_liquidity_internal(swap_for_y, start_array_index)?; - if is_non_zero_liquidity_flag { - if current_array_index != bin_array_index { - self.shift_active_bin(swap_for_y, bin_array_index)?; - } - } else { - let (bin_array_index, _) = LbPair::next_bin_array_index_with_liquidity_from_extension( - swap_for_y, - bin_array_index, - bin_array_bitmap_extension, - )?; - // no need to check for flag here, because if we cannot find the non-liquidity bin array id in the extension go from lb_pair state, then extension will return error - if current_array_index != bin_array_index { - self.shift_active_bin(swap_for_y, bin_array_index)?; - } - } - Ok(()) - } - - pub fn next_bin_array_index_with_liquidity( - &mut self, - swap_for_y: bool, - bin_array_bitmap_extension: &Option>, - ) -> Result<()> { - let start_array_index = BinArray::bin_id_to_bin_array_index(self.active_id)?; - - if self.is_overflow_default_bin_array_bitmap(start_array_index) { - let (bin_array_index, is_non_zero_liquidity_flag) = - LbPair::next_bin_array_index_with_liquidity_from_extension( - swap_for_y, - start_array_index, - bin_array_bitmap_extension, - )?; - if is_non_zero_liquidity_flag { - if start_array_index != bin_array_index { - self.shift_active_bin(swap_for_y, bin_array_index)?; - } - } else { - self.next_bin_array_index_from_internal_to_extension( - swap_for_y, - start_array_index, - bin_array_index, - bin_array_bitmap_extension, - )?; - } - } else { - self.next_bin_array_index_from_internal_to_extension( - swap_for_y, - start_array_index, - start_array_index, - bin_array_bitmap_extension, - )?; - } - Ok(()) - } -} diff --git a/programs/lb_clmm/src/state/lb_pair/action_access.rs b/programs/lb_clmm/src/state/lb_pair/action_access.rs new file mode 100644 index 0000000..2275199 --- /dev/null +++ b/programs/lb_clmm/src/state/lb_pair/action_access.rs @@ -0,0 +1,281 @@ +use super::{LbPair, PairStatus}; + +use anchor_lang::prelude::*; +use solana_program::pubkey::Pubkey; + +pub trait LbPairTypeActionAccess { + fn validate_add_liquidity_access(&self, wallet: Pubkey) -> bool; + fn validate_initialize_bin_array_access(&self, wallet: Pubkey) -> bool; + fn validate_initialize_position_access(&self, wallet: Pubkey) -> bool; + fn validate_swap_access(&self) -> bool; + fn get_swap_cap_status_and_amount(&self, swap_for_y: bool) -> (bool, u64); +} + +struct PermissionLbPairActionAccess<'a> { + is_enabled: bool, + activated: bool, + throttled: bool, + max_swapped_amount: u64, + whitelisted_wallet: &'a [Pubkey], +} + +impl<'a> PermissionLbPairActionAccess<'a> { + pub fn new(lb_pair: &'a LbPair, current_slot: u64) -> Self { + Self { + whitelisted_wallet: &lb_pair.whitelisted_wallet, + is_enabled: lb_pair.status == Into::::into(PairStatus::Enabled), + activated: current_slot >= lb_pair.activation_slot, + throttled: current_slot <= lb_pair.swap_cap_deactivate_slot, + max_swapped_amount: lb_pair.max_swapped_amount, + } + } +} + +impl<'a> LbPairTypeActionAccess for PermissionLbPairActionAccess<'a> { + fn validate_add_liquidity_access(&self, wallet: Pubkey) -> bool { + // Pair disabled due to emergency mode. Nothing can be deposited. + if !self.is_enabled { + return false; + } + + let is_wallet_whitelisted = is_wallet_in_whitelist(&wallet, &self.whitelisted_wallet); + self.activated || is_wallet_whitelisted + } + + fn validate_initialize_bin_array_access(&self, wallet: Pubkey) -> bool { + self.validate_add_liquidity_access(wallet) + } + + fn validate_initialize_position_access(&self, wallet: Pubkey) -> bool { + self.validate_add_liquidity_access(wallet) + } + + fn validate_swap_access(&self) -> bool { + self.is_enabled && self.activated + } + + fn get_swap_cap_status_and_amount(&self, swap_for_y: bool) -> (bool, u64) { + // no cap when user sell + if swap_for_y { + return (false, u64::MAX); + } + return ( + self.throttled && self.max_swapped_amount < u64::MAX, + self.max_swapped_amount, + ); + } +} + +struct PermissionlessLbPairActionAccess { + is_enabled: bool, +} + +impl PermissionlessLbPairActionAccess { + pub fn new(lb_pair: &LbPair) -> Self { + Self { + is_enabled: lb_pair.status == Into::::into(PairStatus::Enabled), + } + } +} + +impl LbPairTypeActionAccess for PermissionlessLbPairActionAccess { + fn validate_add_liquidity_access(&self, _wallet: Pubkey) -> bool { + self.is_enabled + } + + fn validate_initialize_bin_array_access(&self, _wallet: Pubkey) -> bool { + self.is_enabled + } + + fn validate_initialize_position_access(&self, _wallet: Pubkey) -> bool { + self.is_enabled + } + + fn validate_swap_access(&self) -> bool { + self.is_enabled + } + fn get_swap_cap_status_and_amount(&self, _swap_for_y: bool) -> (bool, u64) { + (false, u64::MAX) + } +} + +pub fn get_lb_pair_type_access_validator<'a>( + lb_pair: &'a LbPair, + current_slot: u64, +) -> Result> { + if lb_pair.is_permission_pair()? { + let permission_pair_access_validator = + PermissionLbPairActionAccess::new(&lb_pair, current_slot); + + Ok(Box::new(permission_pair_access_validator)) + } else { + let permissionless_pair_access_validator = PermissionlessLbPairActionAccess::new(&lb_pair); + Ok(Box::new(permissionless_pair_access_validator)) + } +} + +pub fn is_wallet_in_whitelist(wallet: &Pubkey, whitelist: &[Pubkey]) -> bool { + !wallet.eq(&Pubkey::default()) && whitelist.iter().find(|&&w| w.eq(&wallet)).is_some() +} + +#[cfg(test)] +mod test { + use super::*; + use crate::state::{parameters::StaticParameters, PairType}; + fn create_lb_pair(is_permission: bool) -> LbPair { + let mut lb_pair = LbPair::default(); + let pair_type = if is_permission { + PairType::Permission + } else { + PairType::Permissionless + }; + + lb_pair + .initialize( + 0, + 0, + 10, + Pubkey::default(), + Pubkey::default(), + Pubkey::default(), + Pubkey::default(), + Pubkey::default(), + StaticParameters::default(), + pair_type, + PairStatus::Enabled.into(), + Pubkey::default(), + 0, + Pubkey::default(), + ) + .unwrap(); + + lb_pair + } + + #[test] + fn test_permission_pair_add_liquidity_access_validation() { + let current_slot = 0; + let mut lb_pair = create_lb_pair(true); + + assert_eq!(lb_pair.activation_slot, u64::MAX); + assert_eq!(lb_pair.status, Into::::into(PairStatus::Enabled)); + + let wallet_1 = Pubkey::new_unique(); + let wallet_2 = Pubkey::new_unique(); + + // Pair is enabled, activation slot not reached. No whitelisted wallet + { + // Access denied because wallet not whitelisted + let access_validator = + get_lb_pair_type_access_validator(&lb_pair, current_slot).unwrap(); + assert!(access_validator.validate_add_liquidity_access(Pubkey::default()) == false); + assert!(access_validator.validate_add_liquidity_access(Pubkey::new_unique()) == false); + assert!(access_validator.validate_add_liquidity_access(wallet_1) == false); + assert!(access_validator.validate_add_liquidity_access(wallet_2) == false); + } + + lb_pair.add_whitelist_wallet(wallet_1).unwrap(); + lb_pair.add_whitelist_wallet(wallet_2).unwrap(); + + // Pair is enabled, activation slot not reached. With whitelisted wallet. + { + // Access granted for only whitelisted wallet. + let access_validator = + get_lb_pair_type_access_validator(&lb_pair, current_slot).unwrap(); + assert!(access_validator.validate_add_liquidity_access(Pubkey::default()) == false); + assert!(access_validator.validate_add_liquidity_access(Pubkey::new_unique()) == false); + assert!(access_validator.validate_add_liquidity_access(wallet_1) == true); + assert!(access_validator.validate_add_liquidity_access(wallet_2) == true); + } + + lb_pair.activation_slot = 0; + + // Pair is enabled, activation slot reached. Access granted for all wallet. + { + let access_validator = + get_lb_pair_type_access_validator(&lb_pair, current_slot).unwrap(); + assert!(access_validator.validate_add_liquidity_access(Pubkey::default()) == true); + assert!(access_validator.validate_add_liquidity_access(Pubkey::new_unique()) == true); + assert!(access_validator.validate_add_liquidity_access(wallet_1) == true); + assert!(access_validator.validate_add_liquidity_access(wallet_2) == true); + } + + lb_pair.status = PairStatus::Disabled.into(); + + // Pair is disabled. Access denied. + { + // Access denied for all wallets. + let access_validator = + get_lb_pair_type_access_validator(&lb_pair, current_slot).unwrap(); + assert!(access_validator.validate_add_liquidity_access(Pubkey::default()) == false); + assert!(access_validator.validate_add_liquidity_access(Pubkey::new_unique()) == false); + assert!(access_validator.validate_add_liquidity_access(wallet_1) == false); + assert!(access_validator.validate_add_liquidity_access(wallet_2) == false); + } + } + + #[test] + fn test_permissionless_pair_add_liquidity_access_validation() { + let current_slot = 0; + let mut lb_pair = create_lb_pair(false); + + assert_eq!(lb_pair.activation_slot, 0); + assert_eq!(lb_pair.status, Into::::into(PairStatus::Enabled)); + + let wallet_1 = Pubkey::new_unique(); + let wallet_2 = Pubkey::new_unique(); + + // Pair enabled. No whitelisted wallet. + { + // Access granted for all wallets. + let access_validator = + get_lb_pair_type_access_validator(&lb_pair, current_slot).unwrap(); + assert!(access_validator.validate_add_liquidity_access(Pubkey::default()) == true); + assert!(access_validator.validate_add_liquidity_access(wallet_1) == true); + assert!(access_validator.validate_add_liquidity_access(wallet_2) == true); + } + + lb_pair.add_whitelist_wallet(wallet_1).unwrap(); + lb_pair.add_whitelist_wallet(wallet_2).unwrap(); + + // Pair enabled with whitelisted wallet. Program endpoint do not allow admin to whitelist wallet in permissionless pair. But just in case, have a unit test. + { + // Access granted for all wallets. + let access_validator = + get_lb_pair_type_access_validator(&lb_pair, current_slot).unwrap(); + assert!(access_validator.validate_add_liquidity_access(Pubkey::default()) == true); + assert!(access_validator.validate_add_liquidity_access(wallet_1) == true); + assert!(access_validator.validate_add_liquidity_access(wallet_2) == true); + } + + lb_pair.status = PairStatus::Disabled.into(); + + // Pair disabled with whitelisted wallet. + { + // Access denied for all wallets. + let access_validator = + get_lb_pair_type_access_validator(&lb_pair, current_slot).unwrap(); + assert!(access_validator.validate_add_liquidity_access(Pubkey::default()) == false); + assert!(access_validator.validate_add_liquidity_access(wallet_1) == false); + assert!(access_validator.validate_add_liquidity_access(wallet_2) == false); + } + } + + #[test] + fn test_is_wallet_in_whitelist() { + let mut lb_pair = create_lb_pair(true); + + let wallet_1 = Pubkey::new_unique(); + let wallet_2 = Pubkey::new_unique(); + + lb_pair.add_whitelist_wallet(wallet_1).unwrap(); + lb_pair.add_whitelist_wallet(wallet_2).unwrap(); + + assert!(is_wallet_in_whitelist(&wallet_1, &lb_pair.whitelisted_wallet) == true); + assert!(is_wallet_in_whitelist(&wallet_2, &lb_pair.whitelisted_wallet) == true); + + assert!( + is_wallet_in_whitelist(&Pubkey::new_unique(), &lb_pair.whitelisted_wallet) == false + ); + } +} diff --git a/programs/lb_clmm/src/state/lb_pair/mod.rs b/programs/lb_clmm/src/state/lb_pair/mod.rs new file mode 100644 index 0000000..2638180 --- /dev/null +++ b/programs/lb_clmm/src/state/lb_pair/mod.rs @@ -0,0 +1,3 @@ +mod state; +pub use state::*; +pub mod action_access; diff --git a/programs/lb_clmm/src/state/lb_pair/state.rs b/programs/lb_clmm/src/state/lb_pair/state.rs new file mode 100644 index 0000000..0c0eaa2 --- /dev/null +++ b/programs/lb_clmm/src/state/lb_pair/state.rs @@ -0,0 +1,1696 @@ +use std::cmp::min; + +use crate::assert_eq_admin; +use crate::constants::{ + BASIS_POINT_MAX, BIN_ARRAY_BITMAP_SIZE, FEE_PRECISION, MAX_BIN_ID, MAX_FEE_RATE, + MAX_FEE_UPDATE_WINDOW, MIN_BIN_ID, +}; +use crate::instructions::update_fee_parameters::FeeParameter; +use crate::manager::bin_array_manager::BinArrayManager; +use crate::math::u128x128_math::Rounding; +use crate::math::u64x64_math::SCALE_OFFSET; +use crate::math::utils_math::{one, safe_mul_div_cast, safe_mul_shr_cast, safe_shl_div_cast}; +use crate::state::action_access::get_lb_pair_type_access_validator; +use crate::state::bin::BinArray; +use crate::state::bin_array_bitmap_extension::BinArrayBitmapExtension; +use crate::state::parameters::{StaticParameters, VariableParameters}; +use crate::{errors::LBError, math::safe_math::SafeMath}; +use anchor_lang::prelude::*; +use num_enum::{IntoPrimitive, TryFromPrimitive}; +use ruint::aliases::{U1024, U256}; +use static_assertions::const_assert_eq; +use std::ops::BitXor; +use std::ops::Shl; +use std::ops::Shr; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)] +#[repr(u8)] +/// Type of the Pair. 0 = Permissionless, 1 = Permission. Putting 0 as permissionless for backward compatibility. +pub enum PairType { + Permissionless, + Permission, +} + +pub struct LaunchPadParams { + pub activation_slot: u64, + pub swap_cap_deactivate_slot: u64, + pub max_swapped_amount: u64, +} + +impl PairType { + pub fn get_pair_default_launch_pad_params(&self) -> LaunchPadParams { + match self { + // The slot is unreachable. Therefore by default, the pair will be disabled until admin update the activation slot. + Self::Permission => LaunchPadParams { + activation_slot: u64::MAX, + swap_cap_deactivate_slot: u64::MAX, + max_swapped_amount: u64::MAX, + }, + // Activation slot is not used in permissionless pair. Therefore, default to 0. + Self::Permissionless => LaunchPadParams { + activation_slot: 0, + swap_cap_deactivate_slot: 0, + max_swapped_amount: 0, + }, + } + } +} + +#[derive( + AnchorSerialize, AnchorDeserialize, Debug, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, +)] +#[repr(u8)] +/// Pair status. 0 = Enabled, 1 = Disabled. Putting 0 as enabled for backward compatibility. +pub enum PairStatus { + // Fully enabled. + // Condition: + // Permissionless: PairStatus::Enabled + // Permission: PairStatus::Enabled and clock.slot > activation_slot + Enabled, + // Similar as emergency mode. User can only withdraw (Only outflow). Except whitelisted wallet still have full privileges. + Disabled, +} + +#[zero_copy] +#[derive(InitSpace, Default, Debug)] +pub struct ProtocolFee { + pub amount_x: u64, + pub amount_y: u64, +} + +#[account(zero_copy)] +#[derive(InitSpace, Debug)] +pub struct LbPair { + pub parameters: StaticParameters, + pub v_parameters: VariableParameters, + pub bump_seed: [u8; 1], + /// Bin step signer seed + pub bin_step_seed: [u8; 2], + /// Type of the pair + pub pair_type: u8, + /// Active bin id + pub active_id: i32, + /// Bin step. Represent the price increment / decrement. + pub bin_step: u16, + /// Status of the pair. Check PairStatus enum. + pub status: u8, + pub _padding1: [u8; 5], + /// Token X mint + pub token_x_mint: Pubkey, + /// Token Y mint + pub token_y_mint: Pubkey, + /// LB token X vault + pub reserve_x: Pubkey, + /// LB token Y vault + pub reserve_y: Pubkey, + /// Uncollected protocol fee + pub protocol_fee: ProtocolFee, + /// Protocol fee owner, + pub fee_owner: Pubkey, + /// Farming reward information + pub reward_infos: [RewardInfo; 2], // TODO: Bug in anchor IDL parser when using InitSpace macro. Temp hardcode it. https://github.com/coral-xyz/anchor/issues/2556 + /// Oracle pubkey + pub oracle: Pubkey, + /// Packed initialized bin array state + pub bin_array_bitmap: [u64; 16], // store default bin id from -512 to 511 (bin id from -35840 to 35840, price from 2.7e-16 to 3.6e15) + /// Last time the pool fee parameter was updated + pub last_updated_at: i64, + /// Whitelisted wallet + pub whitelisted_wallet: [Pubkey; 2], + /// Base keypair. Only required for permission pair + pub base_key: Pubkey, + /// Slot to enable the pair. Only applicable for permission pair. + pub activation_slot: u64, + /// Last slot until pool remove max_swapped_amount for buying + pub swap_cap_deactivate_slot: u64, + /// Max X swapped amount user can swap from y to x between activation_slot and last_slot + pub max_swapped_amount: u64, + /// Liquidity lock duration for positions which created before activate. Only applicable for permission pair. + pub lock_durations_in_slot: u64, + /// Pool creator + pub creator: Pubkey, + /// Reserved space for future use + pub _reserved: [u8; 24], +} + +const_assert_eq!(std::mem::size_of::(), 896); + +impl Default for LbPair { + fn default() -> Self { + let LaunchPadParams { + activation_slot, + max_swapped_amount, + swap_cap_deactivate_slot, + } = PairType::Permissionless.get_pair_default_launch_pad_params(); + + Self { + active_id: 0, + parameters: StaticParameters::default(), + v_parameters: VariableParameters::default(), + bump_seed: [0u8; 1], + bin_step: 0, + token_x_mint: Pubkey::default(), + token_y_mint: Pubkey::default(), + bin_step_seed: [0u8; 2], + fee_owner: Pubkey::default(), + protocol_fee: ProtocolFee::default(), + reserve_x: Pubkey::default(), + reserve_y: Pubkey::default(), + reward_infos: [RewardInfo::default(); 2], + oracle: Pubkey::default(), + bin_array_bitmap: [0u64; 16], + last_updated_at: 0, + pair_type: PairType::Permissionless.into(), + status: 0, + whitelisted_wallet: [Pubkey::default(); 2], + base_key: Pubkey::default(), + activation_slot, + swap_cap_deactivate_slot, + max_swapped_amount, + lock_durations_in_slot: 0, + creator: Pubkey::default(), + _padding1: [0u8; 5], + _reserved: [0u8; 24], + } + } +} + +/// Stores the state relevant for tracking liquidity mining rewards +#[zero_copy] +#[derive(InitSpace, Default, Debug, PartialEq)] +pub struct RewardInfo { + /// Reward token mint. + pub mint: Pubkey, + /// Reward vault token account. + pub vault: Pubkey, + /// Authority account that allows to fund rewards + pub funder: Pubkey, + /// TODO check whether we need to store it in pool + pub reward_duration: u64, // 8 + /// TODO check whether we need to store it in pool + pub reward_duration_end: u64, // 8 + /// TODO check whether we need to store it in pool + pub reward_rate: u128, // 8 + /// The last time reward states were updated. + pub last_update_time: u64, // 8 + /// Accumulated seconds where when farm distribute rewards, but the bin is empty. The reward will be accumulated for next reward time window. + pub cumulative_seconds_with_empty_liquidity_reward: u64, +} + +impl RewardInfo { + /// Returns true if this reward is initialized. + /// Once initialized, a reward cannot transition back to uninitialized. + pub fn initialized(&self) -> bool { + self.mint.ne(&Pubkey::default()) + } + + pub fn is_valid_funder(&self, funder: Pubkey) -> bool { + assert_eq_admin(funder) || funder.eq(&self.funder) + } + + pub fn init_reward( + &mut self, + mint: Pubkey, + vault: Pubkey, + funder: Pubkey, + reward_duration: u64, + ) { + self.mint = mint; + self.vault = vault; + self.funder = funder; + self.reward_duration = reward_duration; + } + + pub fn update_last_update_time(&mut self, current_time: u64) { + self.last_update_time = std::cmp::min(current_time, self.reward_duration_end); + } + + pub fn get_seconds_elapsed_since_last_update(&self, current_time: u64) -> Result { + let last_time_reward_applicable = std::cmp::min(current_time, self.reward_duration_end); + let time_period = last_time_reward_applicable.safe_sub(self.last_update_time.into())?; + + Ok(time_period) + } + + // To make it simple we truncate decimals of liquidity_supply for the calculation + pub fn calculate_reward_per_token_stored_since_last_update( + &self, + current_time: u64, + liquidity_supply: u64, + ) -> Result { + let time_period = self.get_seconds_elapsed_since_last_update(current_time)?; + + safe_mul_div_cast( + time_period.into(), + self.reward_rate, + liquidity_supply.into(), + Rounding::Down, + ) + } + + pub fn calculate_reward_accumulated_since_last_update( + &self, + current_time: u64, + ) -> Result { + let last_time_reward_applicable = std::cmp::min(current_time, self.reward_duration_end); + + let time_period = + U256::from(last_time_reward_applicable.safe_sub(self.last_update_time.into())?); + + Ok(time_period.safe_mul(U256::from(self.reward_rate))?) + } + + /// Farming rate after funding + pub fn update_rate_after_funding( + &mut self, + current_time: u64, + funding_amount: u64, + ) -> Result<()> { + let reward_duration_end = self.reward_duration_end; + let total_amount: u64; + + if current_time >= reward_duration_end { + total_amount = funding_amount + } else { + let remaining_seconds = reward_duration_end.safe_sub(current_time)?; + let leftover: u64 = safe_mul_shr_cast( + self.reward_rate, + remaining_seconds.into(), + SCALE_OFFSET, + Rounding::Down, + )?; + + total_amount = leftover.safe_add(funding_amount)?; + } + + self.reward_rate = safe_shl_div_cast( + total_amount.into(), + self.reward_duration.into(), + SCALE_OFFSET, + Rounding::Down, + )?; + self.last_update_time = current_time; + self.reward_duration_end = current_time.safe_add(self.reward_duration)?; + + Ok(()) + } +} + +impl LbPair { + pub fn initialize( + &mut self, + bump: u8, + active_id: i32, + bin_step: u16, + token_mint_x: Pubkey, + token_mint_y: Pubkey, + reserve_x: Pubkey, + reserve_y: Pubkey, + oracle: Pubkey, + static_parameter: StaticParameters, + pair_type: PairType, + pair_status: u8, + base_key: Pubkey, + lock_duration_in_slot: u64, + creator: Pubkey, + ) -> Result<()> { + self.parameters = static_parameter; + self.active_id = active_id; + self.bin_step = bin_step; + self.token_x_mint = token_mint_x; + self.token_y_mint = token_mint_y; + self.reserve_x = reserve_x; + self.reserve_y = reserve_y; + self.fee_owner = crate::fee_owner::ID; + self.bump_seed = [bump]; + self.bin_step_seed = bin_step.to_le_bytes(); + self.oracle = oracle; + self.pair_type = pair_type.into(); + self.base_key = base_key; + self.status = pair_status; + self.creator = creator; + + let LaunchPadParams { + activation_slot, + swap_cap_deactivate_slot, + max_swapped_amount, + } = pair_type.get_pair_default_launch_pad_params(); + + self.activation_slot = activation_slot; + self.swap_cap_deactivate_slot = swap_cap_deactivate_slot; + self.max_swapped_amount = max_swapped_amount; + self.lock_durations_in_slot = lock_duration_in_slot; + + Ok(()) + } + + pub fn get_release_slot(&self, current_slot: u64) -> Result { + let release_slot = match self.pair_type()? { + PairType::Permission => { + if self.lock_durations_in_slot > 0 && current_slot < self.activation_slot { + self.activation_slot + .saturating_add(self.lock_durations_in_slot) + } else { + 0 + } + } + _ => 0, + }; + + Ok(release_slot) + } + + pub fn update_whitelisted_wallet(&mut self, idx: usize, wallet: Pubkey) -> Result<()> { + require!(idx < self.whitelisted_wallet.len(), LBError::InvalidIndex); + self.whitelisted_wallet[idx] = wallet; + + Ok(()) + } + + pub fn add_whitelist_wallet(&mut self, wallet: Pubkey) -> Result<()> { + let mut index = None; + + for (idx, whitelisted) in self.whitelisted_wallet.iter().enumerate() { + if whitelisted.eq(&wallet) { + return Ok(()); // Wallet already exists in the whitelist, returning early + } + if index.is_none() && whitelisted.eq(&Pubkey::default()) { + index = Some(idx); + } + } + + match index { + Some(idx) => { + self.whitelisted_wallet[idx] = wallet; + Ok(()) + } + None => Err(LBError::ExceedMaxWhitelist.into()), + } + } + + pub fn get_swap_cap_status_and_amount( + &self, + current_time: u64, + swap_for_y: bool, + ) -> Result<(bool, u64)> { + let pair_type_access_validator = get_lb_pair_type_access_validator(self, current_time)?; + Ok(pair_type_access_validator.get_swap_cap_status_and_amount(swap_for_y)) + } + + pub fn status(&self) -> Result { + let pair_status: PairStatus = self + .status + .try_into() + .map_err(|_| LBError::TypeCastFailed)?; + + Ok(pair_status) + } + + pub fn pair_type(&self) -> Result { + let pair_type: PairType = self + .pair_type + .try_into() + .map_err(|_| LBError::TypeCastFailed)?; + + Ok(pair_type) + } + + pub fn is_permission_pair(&self) -> Result { + let pair_type = self.pair_type()?; + Ok(pair_type.eq(&PairType::Permission)) + } + + pub fn update_fee_parameters(&mut self, parameter: &FeeParameter) -> Result<()> { + let current_timestamp = Clock::get()?.unix_timestamp; + if self.last_updated_at > 0 { + let second_elapsed = current_timestamp.safe_sub(self.last_updated_at)?; + + require!( + second_elapsed > MAX_FEE_UPDATE_WINDOW, + LBError::ExcessiveFeeUpdate + ); + } + + self.parameters.update(parameter)?; + self.last_updated_at = current_timestamp; + + Ok(()) + } + + pub fn seeds(&self) -> Result> { + let min_key = min(self.token_x_mint, self.token_y_mint); + let (min_key_ref, max_key_ref) = if min_key == self.token_x_mint { + (self.token_x_mint.as_ref(), self.token_y_mint.as_ref()) + } else { + (self.token_y_mint.as_ref(), self.token_x_mint.as_ref()) + }; + if self.is_permission_pair()? { + Ok(vec![ + self.base_key.as_ref(), + min_key_ref, + max_key_ref, + &self.bin_step_seed, + &self.bump_seed, + ]) + } else { + Ok(vec![ + min_key_ref, + max_key_ref, + &self.bin_step_seed, + &self.bump_seed, + ]) + } + } + + #[inline(always)] + pub fn swap_for_y(&self, out_token_mint: Pubkey) -> bool { + out_token_mint.eq(&self.token_y_mint) + } + + /// Plus / Minus 1 to the active bin based on the swap direction + pub fn advance_active_bin(&mut self, swap_for_y: bool) -> Result<()> { + let next_active_bin_id = if swap_for_y { + self.active_id.safe_sub(1)? + } else { + self.active_id.safe_add(1)? + }; + + require!( + next_active_bin_id >= MIN_BIN_ID && next_active_bin_id <= MAX_BIN_ID, + LBError::PairInsufficientLiquidity + ); + + self.active_id = next_active_bin_id; + + Ok(()) + } + + /// Base fee rate = Base fee factor * bin step. This is in 1e9 unit. + pub fn get_base_fee(&self) -> Result { + Ok(u128::from(self.parameters.base_factor) + .safe_mul(self.bin_step.into())? + // Make it to be the same as FEE_PRECISION defined for ceil_div later on. + .safe_mul(10u128)?) + } + + /// Variable fee rate = variable fee factor * (volatility_accumulator * bin_step)^2 + fn compute_variable_fee(&self, volatility_accumulator: u32) -> Result { + if self.parameters.variable_fee_control > 0 { + let volatility_accumulator: u128 = volatility_accumulator.into(); + let bin_step: u128 = self.bin_step.into(); + let variable_fee_control: u128 = self.parameters.variable_fee_control.into(); + + let square_vfa_bin = volatility_accumulator + .safe_mul(bin_step)? + .checked_pow(2) + .ok_or(LBError::MathOverflow)?; + + // Variable fee control, volatility accumulator, bin step are in basis point unit (10_000) + // This is 1e20. Which > 1e9. Scale down it to 1e9 unit and ceiling the remaining. + let v_fee = variable_fee_control.safe_mul(square_vfa_bin)?; + + let scaled_v_fee = v_fee.safe_add(99_999_999_999)?.safe_div(100_000_000_000)?; + return Ok(scaled_v_fee); + } + + Ok(0) + } + + /// Variable fee rate = variable_fee_control * (variable_fee_accumulator * bin_step) ^ 2 + pub fn get_variable_fee(&self) -> Result { + self.compute_variable_fee(self.v_parameters.volatility_accumulator) + } + + /// Total fee rate = base_fee_rate + variable_fee_rate + pub fn get_total_fee(&self) -> Result { + let total_fee_rate = self.get_base_fee()?.safe_add(self.get_variable_fee()?)?; + let total_fee_rate_cap = std::cmp::min(total_fee_rate, MAX_FEE_RATE.into()); + Ok(total_fee_rate_cap) + } + + #[cfg(test)] + /// Maximum fee rate + fn get_max_total_fee(&self) -> Result { + let max_total_fee_rate = self + .get_base_fee()? + .safe_add(self.compute_variable_fee(self.parameters.max_volatility_accumulator)?)?; + + let total_fee_rate_cap = std::cmp::min(max_total_fee_rate, MAX_FEE_RATE.into()); + Ok(total_fee_rate_cap) + } + + /// Compute composition fee. Composition_fee = fee_amount * (1 + total fee rate) + pub fn compute_composition_fee(&self, swap_amount: u64) -> Result { + let total_fee_rate = self.get_total_fee()?; + // total_fee_rate 1e9 unit + let fee_amount = u128::from(swap_amount).safe_mul(total_fee_rate)?; + let composition_fee = + fee_amount.safe_mul(u128::from(FEE_PRECISION).safe_add(total_fee_rate)?)?; + // 1e9 unit * 1e9 unit = 1e18, scale back + let scaled_down_fee = composition_fee.safe_div(u128::from(FEE_PRECISION).pow(2))?; + + Ok(scaled_down_fee + .try_into() + .map_err(|_| LBError::TypeCastFailed)?) + } + + /// Compute fee from amount, where fee is part of the amount. The result is ceil-ed. + pub fn compute_fee_from_amount(&self, amount_with_fees: u64) -> Result { + // total_fee_rate 1e9 unit + let total_fee_rate = self.get_total_fee()?; + // Ceil division + let fee_amount = u128::from(amount_with_fees) + .safe_mul(total_fee_rate)? + .safe_add((FEE_PRECISION - 1).into())?; + let scaled_down_fee = fee_amount.safe_div(FEE_PRECISION.into())?; + + Ok(scaled_down_fee + .try_into() + .map_err(|_| LBError::TypeCastFailed)?) + } + + /// Compute fee for the amount. The fee is not part of the amount. This function is used when you do not know the amount_with_fees + /// Solve for fee_amount, equation: (amount + fee_amount) * total_fee_rate / 1e9 = fee_amount + /// fee_amount = (amount * total_fee_rate) / (1e9 - total_fee_rate) + /// The result is ceil-ed. + pub fn compute_fee(&self, amount: u64) -> Result { + let total_fee_rate = self.get_total_fee()?; + let denominator = u128::from(FEE_PRECISION).safe_sub(total_fee_rate)?; + + // Ceil division + let fee = u128::from(amount) + .safe_mul(total_fee_rate)? + .safe_add(denominator)? + .safe_sub(1)?; + + let scaled_down_fee = fee.safe_div(denominator)?; + + Ok(scaled_down_fee + .try_into() + .map_err(|_| LBError::TypeCastFailed)?) + } + + /// Compute protocol fee + pub fn compute_protocol_fee(&self, fee_amount: u64) -> Result { + let protocol_fee = u128::from(fee_amount) + .safe_mul(self.parameters.protocol_share.into())? + .safe_div(BASIS_POINT_MAX as u128)?; + + Ok(protocol_fee + .try_into() + .map_err(|_| LBError::TypeCastFailed)?) + } + + /// Accumulate protocol fee + pub fn accumulate_protocol_fees(&mut self, fee_amount_x: u64, fee_amount_y: u64) -> Result<()> { + self.protocol_fee.amount_x = self.protocol_fee.amount_x.safe_add(fee_amount_x)?; + self.protocol_fee.amount_y = self.protocol_fee.amount_y.safe_add(fee_amount_y)?; + + Ok(()) + } + + /// Update volatility reference and accumulator + pub fn update_volatility_parameters(&mut self, current_timestamp: i64) -> Result<()> { + self.v_parameters.update_volatility_parameter( + self.active_id, + current_timestamp, + &self.parameters, + ) + } + + pub fn update_references(&mut self, current_timestamp: i64) -> Result<()> { + self.v_parameters + .update_references(self.active_id, current_timestamp, &self.parameters) + } + + pub fn update_volatility_accumulator(&mut self) -> Result<()> { + self.v_parameters + .update_volatility_accumulator(self.active_id, &self.parameters) + } + + pub fn withdraw_protocol_fee(&mut self, amount_x: u64, amount_y: u64) -> Result<()> { + self.protocol_fee.amount_x = self.protocol_fee.amount_x.safe_sub(amount_x)?; + self.protocol_fee.amount_y = self.protocol_fee.amount_y.safe_sub(amount_y)?; + + Ok(()) + } + + pub fn set_fee_owner(&mut self, fee_owner: Pubkey) { + self.fee_owner = fee_owner; + } + + pub fn oracle_initialized(&self) -> bool { + self.oracle != Pubkey::default() + } + + /// check whether a binArray has liquidity or not + pub fn is_bin_array_has_liquidity( + &self, + bin_array_bitmap_extension: &Option>, + bin_array_index: i32, + ) -> Result { + if self.is_overflow_default_bin_array_bitmap(bin_array_index) { + match bin_array_bitmap_extension { + Some(bitmap_ext) => Ok(bitmap_ext.load_mut()?.bit(bin_array_index)?), + None => return Err(LBError::BitmapExtensionAccountIsNotProvided.into()), + } + } else { + Ok(U1024::from_limbs(self.bin_array_bitmap) + .bit(LbPair::get_bin_array_offset(bin_array_index) as usize)) + } + } + + pub fn flip_bin_arrays( + &mut self, + before_liquidity_flags: &[bool], + bin_array_manager: &BinArrayManager, + bin_array_bitmap_extension: &Option>, + ) -> Result<()> { + let after_liquidity_flags = bin_array_manager.get_zero_liquidity_flags(); + + for (i, &old_flag) in before_liquidity_flags.iter().enumerate() { + if old_flag != after_liquidity_flags[i] { + // flip bin + self.flip_bin_array_bit( + bin_array_bitmap_extension, + bin_array_manager.get_bin_array_index(i)?, + )?; + } + } + Ok(()) + } + + pub fn flip_bin_array_bit( + &mut self, + bin_array_bitmap_extension: &Option>, + bin_array_index: i32, + ) -> Result<()> { + if self.is_overflow_default_bin_array_bitmap(bin_array_index) { + match bin_array_bitmap_extension { + Some(bitmap_ext) => { + bitmap_ext.load_mut()?.flip_bin_array_bit(bin_array_index)?; + } + None => return Err(LBError::BitmapExtensionAccountIsNotProvided.into()), + } + } else { + self.flip_bin_array_bit_internal(bin_array_index)?; + } + + Ok(()) + } + + pub fn is_overflow_default_bin_array_bitmap(&self, bin_array_index: i32) -> bool { + let (min_bitmap_id, max_bitmap_id) = LbPair::bitmap_range(); + bin_array_index > max_bitmap_id || bin_array_index < min_bitmap_id + } + + pub fn bitmap_range() -> (i32, i32) { + (-BIN_ARRAY_BITMAP_SIZE, BIN_ARRAY_BITMAP_SIZE - 1) + } + + fn get_bin_array_offset(bin_array_index: i32) -> usize { + (bin_array_index + BIN_ARRAY_BITMAP_SIZE) as usize + } + + pub fn bit( + &self, + bin_array_index: i32, + bin_array_bitmap_extension: &Option>, + ) -> Result { + if self.is_overflow_default_bin_array_bitmap(bin_array_index) { + match bin_array_bitmap_extension { + Some(bitmap_ext) => Ok(bitmap_ext.load()?.bit(bin_array_index)?), + None => Err(LBError::BitmapExtensionAccountIsNotProvided.into()), + } + } else { + Ok(self.internal_bit(bin_array_index)) + } + } + + fn internal_bit(&self, bin_array_index: i32) -> bool { + U1024::from_limbs(self.bin_array_bitmap).bit(LbPair::get_bin_array_offset(bin_array_index)) + } + + fn flip_bin_array_bit_internal(&mut self, bin_array_index: i32) -> Result<()> { + let bin_array_offset = Self::get_bin_array_offset(bin_array_index); + let bin_array_bitmap = U1024::from_limbs(self.bin_array_bitmap); + let mask = one::<1024, 16>() << bin_array_offset; + self.bin_array_bitmap = bin_array_bitmap.bitxor(mask).into_limbs(); + Ok(()) + } + + // return bin_array_index that it's liquidity is non-zero + // if cannot find one, return false + pub fn get_next_bin_array_index_with_liquidity_internal( + &self, + swap_for_y: bool, + start_array_index: i32, + ) -> Result<(i32, bool)> { + let bin_array_bitmap = U1024::from_limbs(self.bin_array_bitmap); + let array_offset: usize = Self::get_bin_array_offset(start_array_index); + let (min_bitmap_id, max_bitmap_id) = LbPair::bitmap_range(); + if swap_for_y { + let bitmap_range: usize = max_bitmap_id + .safe_sub(min_bitmap_id)? + .try_into() + .map_err(|_| LBError::TypeCastFailed)?; + let offset_bit_map = bin_array_bitmap.shl(bitmap_range.safe_sub(array_offset)?); + + if offset_bit_map.eq(&U1024::ZERO) { + return Ok((min_bitmap_id.safe_sub(1)?, false)); + } else { + let next_bit = offset_bit_map.leading_zeros(); + return Ok((start_array_index.safe_sub(next_bit as i32)?, true)); + } + } else { + let offset_bit_map = bin_array_bitmap.shr(array_offset); + if offset_bit_map.eq(&U1024::ZERO) { + return Ok((max_bitmap_id.safe_add(1)?, false)); + } else { + let next_bit = offset_bit_map.trailing_zeros(); + return Ok(( + start_array_index.checked_add(next_bit as i32).unwrap(), + true, + )); + }; + } + } + + // shift active until non-zero liquidity bin_array_index + fn shift_active_bin(&mut self, swap_for_y: bool, bin_array_index: i32) -> Result<()> { + // update active id + let (lower_bin_id, upper_bin_id) = + BinArray::get_bin_array_lower_upper_bin_id(bin_array_index)?; + + if swap_for_y { + self.active_id = upper_bin_id; + } else { + self.active_id = lower_bin_id; + } + Ok(()) + } + + fn next_bin_array_index_with_liquidity_from_extension( + swap_for_y: bool, + bin_array_index: i32, + bin_array_bitmap_extension: &Option>, + ) -> Result<(i32, bool)> { + match bin_array_bitmap_extension { + Some(bitmap_ext) => { + return Ok(bitmap_ext + .load()? + .next_bin_array_index_with_liquidity(swap_for_y, bin_array_index)?); + } + None => return Err(LBError::BitmapExtensionAccountIsNotProvided.into()), + } + } + + pub fn next_bin_array_index_from_internal_to_extension( + &mut self, + swap_for_y: bool, + current_array_index: i32, + start_array_index: i32, + bin_array_bitmap_extension: &Option>, + ) -> Result<()> { + let (bin_array_index, is_non_zero_liquidity_flag) = + self.get_next_bin_array_index_with_liquidity_internal(swap_for_y, start_array_index)?; + if is_non_zero_liquidity_flag { + if current_array_index != bin_array_index { + self.shift_active_bin(swap_for_y, bin_array_index)?; + } + } else { + let (bin_array_index, _) = LbPair::next_bin_array_index_with_liquidity_from_extension( + swap_for_y, + bin_array_index, + bin_array_bitmap_extension, + )?; + // no need to check for flag here, because if we cannot find the non-liquidity bin array id in the extension go from lb_pair state, then extension will return error + if current_array_index != bin_array_index { + self.shift_active_bin(swap_for_y, bin_array_index)?; + } + } + Ok(()) + } + + pub fn next_bin_array_index_with_liquidity( + &mut self, + swap_for_y: bool, + bin_array_bitmap_extension: &Option>, + ) -> Result<()> { + let start_array_index = BinArray::bin_id_to_bin_array_index(self.active_id)?; + + if self.is_overflow_default_bin_array_bitmap(start_array_index) { + let (bin_array_index, is_non_zero_liquidity_flag) = + LbPair::next_bin_array_index_with_liquidity_from_extension( + swap_for_y, + start_array_index, + bin_array_bitmap_extension, + )?; + if is_non_zero_liquidity_flag { + if start_array_index != bin_array_index { + self.shift_active_bin(swap_for_y, bin_array_index)?; + } + } else { + self.next_bin_array_index_from_internal_to_extension( + swap_for_y, + start_array_index, + bin_array_index, + bin_array_bitmap_extension, + )?; + } + } else { + self.next_bin_array_index_from_internal_to_extension( + swap_for_y, + start_array_index, + start_array_index, + bin_array_bitmap_extension, + )?; + } + Ok(()) + } + + fn get_bin_array_ranges( + start_bin_array_index: i32, + end_bin_array_index: i32, // start_bin_array_index always smaller than or equal end_bin_array_index + ) -> Result { + let (min_bitmap_id, max_bitmap_id) = LbPair::bitmap_range(); + + if end_bin_array_index < min_bitmap_id { + return Ok(BinRangeList { + negative_extension_bin_range: Some(BinRange { + lower_index: start_bin_array_index, + upper_index: end_bin_array_index, + }), + internal_bin_range: None, + positive_extension_bin_range: None, + }); + } + if start_bin_array_index > max_bitmap_id { + return Ok(BinRangeList { + negative_extension_bin_range: None, + internal_bin_range: None, + positive_extension_bin_range: Some(BinRange { + lower_index: start_bin_array_index, + upper_index: end_bin_array_index, + }), + }); + } + + let negative_extension_bin_range = if start_bin_array_index < min_bitmap_id { + Some(BinRange { + lower_index: start_bin_array_index, + upper_index: min_bitmap_id.safe_sub(1)?, + }) + } else { + None + }; + + let positive_extension_bin_range = if end_bin_array_index > max_bitmap_id { + Some(BinRange { + lower_index: max_bitmap_id.safe_add(1)?, + upper_index: end_bin_array_index, + }) + } else { + None + }; + + Ok(BinRangeList { + negative_extension_bin_range, + internal_bin_range: Some(BinRange { + lower_index: start_bin_array_index.max(min_bitmap_id), + upper_index: end_bin_array_index.min(max_bitmap_id), + }), + positive_extension_bin_range, + }) + } + pub fn is_bin_array_range_empty_internal( + &self, + start_bin_array_index: i32, + end_bin_array_index: i32, // lower_index always smaller than or equal upper_index + ) -> Result { + let start_offset: usize = Self::get_bin_array_offset(start_bin_array_index); + let end_offset: usize = Self::get_bin_array_offset(end_bin_array_index); + + let bin_array_bitmap = U1024::from_limbs(self.bin_array_bitmap); + + let (min_bitmap_id, max_bitmap_id) = LbPair::bitmap_range(); + let bitmap_range: usize = max_bitmap_id + .safe_sub(min_bitmap_id)? + .try_into() + .map_err(|_| LBError::TypeCastFailed)?; + + let offset_bit_map = bin_array_bitmap + .shr(start_offset) + .shl(bitmap_range.safe_add(start_offset)?.safe_sub(end_offset)?); + Ok(offset_bit_map.eq(&U1024::ZERO)) + } + pub fn is_bin_array_range_empty( + &self, + start_bin_array_index: i32, + end_bin_array_index: i32, // start_bin_array_index always smaller than or equal end_bin_array_index + bin_array_bitmap_extension: &Option>, + ) -> Result { + if start_bin_array_index > end_bin_array_index { + return Err(LBError::InvalidIndex.into()); + } + let bin_ranges = LbPair::get_bin_array_ranges(start_bin_array_index, end_bin_array_index)?; + if let Some(bin_range) = bin_ranges.internal_bin_range { + if !self + .is_bin_array_range_empty_internal(bin_range.lower_index, bin_range.upper_index)? + { + return Ok(false); + } + } + + if bin_ranges.is_include_extension_bin_range() { + let Some(bitmap_ext) = bin_array_bitmap_extension else { + return Err(LBError::BitmapExtensionAccountIsNotProvided.into()); + }; + let bitmap_ext = bitmap_ext.load()?; + + if let Some(bin_range) = bin_ranges.positive_extension_bin_range { + let (from_offset, bin_array_from_offset) = + BinArrayBitmapExtension::get_offset_and_bin_array_offset_in_bitmap( + bin_range.lower_index, + )?; + + let (to_offset, bin_array_to_offset) = + BinArrayBitmapExtension::get_offset_and_bin_array_offset_in_bitmap( + bin_range.upper_index, + )?; + + if !BinArrayBitmapExtension::is_bin_array_range_empty( + &bitmap_ext.positive_bin_array_bitmap, + from_offset, + to_offset, + bin_array_from_offset, + bin_array_to_offset, + )? { + return Ok(false); + } + } + + if let Some(bin_range) = bin_ranges.negative_extension_bin_range { + let (from_offset, bin_array_from_offset) = + BinArrayBitmapExtension::get_offset_and_bin_array_offset_in_bitmap( + bin_range.upper_index, + )?; + + let (to_offset, bin_array_to_offset) = + BinArrayBitmapExtension::get_offset_and_bin_array_offset_in_bitmap( + bin_range.lower_index, + )?; + + if !BinArrayBitmapExtension::is_bin_array_range_empty( + &bitmap_ext.negative_bin_array_bitmap, + from_offset, + to_offset, + bin_array_from_offset, + bin_array_to_offset, + )? { + return Ok(false); + } + } + } + + Ok(true) + } +} + +#[derive(Copy, Clone, Debug)] +struct BinRange { + lower_index: i32, + upper_index: i32, // lower_index always smaller than or equal upper_index +} + +#[derive(Copy, Clone, Debug)] +struct BinRangeList { + positive_extension_bin_range: Option, + internal_bin_range: Option, + negative_extension_bin_range: Option, +} + +impl BinRangeList { + fn is_include_extension_bin_range(&self) -> bool { + self.positive_extension_bin_range.is_some() || self.negative_extension_bin_range.is_some() + } +} + +#[cfg(test)] +mod lb_pair_test { + use std::collections::HashMap; + + use super::*; + use crate::constants::{tests::get_preset, *}; + use num_traits::Pow; + use proptest::proptest; + + fn create_lb_pair_max() -> LbPair { + LbPair { + parameters: StaticParameters { + base_factor: u16::MAX, + decay_period: u16::MAX, + filter_period: u16::MAX, + max_volatility_accumulator: U24_MAX, + protocol_share: MAX_PROTOCOL_SHARE, + reduction_factor: u16::MAX, + variable_fee_control: U24_MAX, + max_bin_id: i32::MAX, + min_bin_id: i32::MIN, + _padding: [0u8; 6], + }, + bin_step: BASIS_POINT_MAX as u16, + active_id: 0, + bin_step_seed: [0u8; 2], + bump_seed: [0u8; 1], + protocol_fee: ProtocolFee::default(), + token_x_mint: Pubkey::default(), + token_y_mint: Pubkey::default(), + reserve_x: Pubkey::default(), + reserve_y: Pubkey::default(), + v_parameters: VariableParameters { + index_reference: i32::MAX, + last_update_timestamp: i64::MAX, + volatility_accumulator: U24_MAX, + volatility_reference: U24_MAX, + ..VariableParameters::default() + }, + fee_owner: Pubkey::default(), + reward_infos: [RewardInfo::default(); NUM_REWARDS], + ..LbPair::default() + } + } + + #[test] + fn test_get_seed_liquidity_release_slot() { + let lb_pair = LbPair { + pair_type: PairType::Permission.into(), + activation_slot: 100, + lock_durations_in_slot: 100, + ..Default::default() + }; + + let current_slot = 90; + + let release_slot = lb_pair.get_release_slot(current_slot); + assert_eq!(release_slot.unwrap(), 200); + + let lb_pair = LbPair { + pair_type: PairType::Permissionless.into(), + activation_slot: 100, + lock_durations_in_slot: 100, + ..Default::default() + }; + + let release_slot = lb_pair.get_release_slot(current_slot); + assert_eq!(release_slot.unwrap(), 0); + + let lb_pair = LbPair { + pair_type: PairType::Permission.into(), + activation_slot: 100, + lock_durations_in_slot: 0, + ..Default::default() + }; + + let release_slot = lb_pair.get_release_slot(current_slot); + assert_eq!(release_slot.unwrap(), 0); + + let current_slot = lb_pair.activation_slot; + let release_slot = lb_pair.get_release_slot(current_slot); + assert_eq!(release_slot.unwrap(), 0); + } + + #[test] + fn test_update_whitelist_wallet() { + let mut lb_pair = LbPair::default(); + + let wallet_1 = Pubkey::new_unique(); + let wallet_2 = Pubkey::new_unique(); + let wallet_3 = Pubkey::new_unique(); + + assert!(lb_pair.add_whitelist_wallet(wallet_1).is_ok()); + assert!(lb_pair.add_whitelist_wallet(wallet_2).is_ok()); + + assert!(lb_pair.whitelisted_wallet[0].eq(&wallet_1)); + assert!(lb_pair.whitelisted_wallet[1].eq(&wallet_2)); + + let _ = lb_pair.update_whitelisted_wallet(0, Pubkey::default()); + assert!(lb_pair.whitelisted_wallet[0].eq(&Pubkey::default())); + assert!(lb_pair.whitelisted_wallet[1].eq(&wallet_2)); + + assert!(lb_pair + .update_whitelisted_wallet(2, Pubkey::default()) + .is_err()); + assert!(lb_pair.whitelisted_wallet[0].eq(&Pubkey::default())); + assert!(lb_pair.whitelisted_wallet[1].eq(&wallet_2)); + + let _ = lb_pair.update_whitelisted_wallet(1, Pubkey::default()); + assert!(lb_pair.whitelisted_wallet[0].eq(&Pubkey::default())); + assert!(lb_pair.whitelisted_wallet[1].eq(&Pubkey::default())); + + assert!(lb_pair.add_whitelist_wallet(wallet_3).is_ok()); + assert!(lb_pair.whitelisted_wallet[0].eq(&wallet_3)); + assert!(lb_pair.whitelisted_wallet[1].eq(&Pubkey::default())); + } + + #[test] + fn test_whitelist_wallet() { + let mut lb_pair = LbPair::default(); + + let empty_slot = lb_pair + .whitelisted_wallet + .iter() + .filter(|&&p| p.eq(&Pubkey::default())) + .count(); + + // Duplicate pubkey will not error, but nothing will be added + assert!(lb_pair.add_whitelist_wallet(Pubkey::default()).is_ok()); + + assert_eq!(empty_slot, 2); + + let wallet_1 = Pubkey::new_unique(); + let wallet_2 = Pubkey::new_unique(); + let wallet_3 = Pubkey::new_unique(); + let wallet_4 = Pubkey::new_unique(); + + assert!(lb_pair.add_whitelist_wallet(wallet_1).is_ok()); + assert!(lb_pair.whitelisted_wallet[0].eq(&wallet_1)); + assert!(lb_pair.whitelisted_wallet[1].eq(&Pubkey::default())); + + assert!(lb_pair.add_whitelist_wallet(wallet_2).is_ok()); + assert!(lb_pair.whitelisted_wallet[0].eq(&wallet_1)); + assert!(lb_pair.whitelisted_wallet[1].eq(&wallet_2)); + + assert!(lb_pair.add_whitelist_wallet(wallet_3).is_err()); + assert!(lb_pair.whitelisted_wallet[0].eq(&wallet_1)); + assert!(lb_pair.whitelisted_wallet[1].eq(&wallet_2)); + + assert!(lb_pair.add_whitelist_wallet(wallet_4).is_err()); + + assert!(lb_pair.add_whitelist_wallet(wallet_2).is_ok()); + assert!(lb_pair.whitelisted_wallet[0].eq(&wallet_1)); + assert!(lb_pair.whitelisted_wallet[1].eq(&wallet_2)); + } + + #[test] + fn test_num_enum() { + let permissionless_pool_type = 0; + let permission_pool_type = 1; + let unknown_pool_type = 2; + + let converted_type: std::result::Result = permission_pool_type.try_into(); + assert!(converted_type.is_ok()); + assert_eq!(converted_type.unwrap(), PairType::Permission); + + let converted_type: std::result::Result = permissionless_pool_type.try_into(); + assert!(converted_type.is_ok()); + assert_eq!(converted_type.unwrap(), PairType::Permissionless); + + let converted_type: std::result::Result = unknown_pool_type.try_into(); + assert!(converted_type.is_err()); + + assert_eq!(Into::::into(PairType::Permission), permission_pool_type); + assert_eq!( + Into::::into(PairType::Permissionless), + permissionless_pool_type + ); + } + + #[test] + fn test_get_total_fee_rate_cap() { + let total_fee_rate = create_lb_pair_max().get_max_total_fee(); + assert!(total_fee_rate.is_ok()); + assert_eq!(total_fee_rate.unwrap(), MAX_FEE_RATE as u128); + } + + #[test] + fn test_get_base_rate_fits_u128() { + let base_fee_rate = create_lb_pair_max().get_base_fee(); + assert!(base_fee_rate.is_ok()) + } + + #[test] + fn test_get_variable_rate_fits_u128() { + let variable_fee_rate = create_lb_pair_max().get_variable_fee(); + assert!(variable_fee_rate.is_ok()) + } + + #[test] + fn test_get_total_fee_rate_fits_u128() { + let total_fee_rate = create_lb_pair_max().get_max_total_fee(); + assert!(total_fee_rate.is_ok()) + } + + #[test] + fn test_compute_fee_fits_u64() { + let fee_amount = create_lb_pair_max().compute_fee(u64::MAX); + assert!(fee_amount.is_ok()); + } + + #[test] + fn test_compute_fee_from_amount_fits_u64() { + let fee_amount = create_lb_pair_max().compute_fee_from_amount(u64::MAX); + assert!(fee_amount.is_ok()); + } + + #[test] + fn test_compute_composite_fee_amount_fits_u64() { + let fee_amount = create_lb_pair_max().compute_composition_fee(u64::MAX); + assert!(fee_amount.is_ok()); + } + + #[test] + fn test_volatile_fee_rate() { + let bin_step = 10; + + let lb_pair = LbPair { + parameters: get_preset(bin_step).unwrap(), + bin_step, + active_id: 0, + protocol_fee: ProtocolFee::default(), + token_x_mint: Pubkey::default(), + token_y_mint: Pubkey::default(), + reserve_x: Pubkey::default(), + reserve_y: Pubkey::default(), + v_parameters: VariableParameters { + volatility_accumulator: 10000, + ..VariableParameters::default() + }, + fee_owner: Pubkey::default(), + reward_infos: [RewardInfo::default(); NUM_REWARDS], + ..LbPair::default() + }; + + let total_fee_rate = lb_pair.get_total_fee(); + assert!(total_fee_rate.is_ok()); + + let expected_base_fee_rate = + (lb_pair.parameters.base_factor as i32 / BASIS_POINT_MAX) as f64 * bin_step as f64 + / BASIS_POINT_MAX as f64; + let expected_volatile_fee_rate = (lb_pair.parameters.variable_fee_control as f64 + / BASIS_POINT_MAX as f64) + * (lb_pair.v_parameters.volatility_accumulator as f64 / BASIS_POINT_MAX as f64 + * bin_step as f64 + / BASIS_POINT_MAX as f64) + .pow(2); + let expected_total_fee_rate = expected_base_fee_rate + expected_volatile_fee_rate; + let expected_total_fee_rate = (expected_total_fee_rate * FEE_PRECISION as f64) as u128; + + assert_eq!(expected_total_fee_rate, total_fee_rate.unwrap()); + } + + #[test] + fn test_compute_fee_from_amount() { + let swap_amount = u64::MAX; + let bin_step = 10; + + let lb_pair = LbPair { + parameters: get_preset(bin_step).unwrap(), + bin_step, + active_id: 0, + bin_step_seed: [0u8; 2], + bump_seed: [0u8; 1], + protocol_fee: ProtocolFee::default(), + token_x_mint: Pubkey::default(), + token_y_mint: Pubkey::default(), + reserve_x: Pubkey::default(), + reserve_y: Pubkey::default(), + v_parameters: VariableParameters::default(), + fee_owner: Pubkey::default(), + reward_infos: [RewardInfo::default(); NUM_REWARDS], + ..LbPair::default() + }; + + let total_fee_rate = lb_pair.get_total_fee(); + assert!(total_fee_rate.is_ok()); + + let total_fee_rate = total_fee_rate.unwrap() as f64 / FEE_PRECISION as f64; + let expected_fee = (swap_amount as f64 * total_fee_rate).ceil(); + + let fee = lb_pair.compute_fee_from_amount(swap_amount).unwrap(); + assert_eq!(expected_fee as u64, fee); + } + + #[test] + fn test_compute_fee() { + let swap_amount = u64::MAX; + let bin_step = 10; + + let lb_pair = LbPair { + parameters: get_preset(bin_step).unwrap(), + bin_step, + active_id: 0, + bin_step_seed: [0u8; 2], + bump_seed: [0u8; 1], + protocol_fee: ProtocolFee::default(), + token_x_mint: Pubkey::default(), + token_y_mint: Pubkey::default(), + reserve_x: Pubkey::default(), + reserve_y: Pubkey::default(), + v_parameters: VariableParameters::default(), + fee_owner: Pubkey::default(), + reward_infos: [RewardInfo::default(); NUM_REWARDS], + ..LbPair::default() + }; + + let total_fee_rate = lb_pair.get_total_fee(); + assert!(total_fee_rate.is_ok()); + + let total_fee_rate = total_fee_rate.unwrap() as f64 / FEE_PRECISION as f64; + let inverse_total_fee_rate = 1.0f64 - total_fee_rate; + + let expected_fee = (swap_amount as f64 * total_fee_rate / inverse_total_fee_rate).ceil(); + let fee = lb_pair.compute_fee(swap_amount).unwrap(); + + // Precision loss from float, the +1 can be remove if we use smaller swap amount ... + assert_eq!(expected_fee as u64 + 1, fee); + } + + #[test] + fn test_fee_charges() { + let bin_step = 10; + let lb_pair = LbPair { + parameters: get_preset(bin_step).unwrap(), + bin_step, + active_id: 0, + bin_step_seed: [0u8; 2], + bump_seed: [0u8; 1], + protocol_fee: ProtocolFee::default(), + token_x_mint: Pubkey::default(), + token_y_mint: Pubkey::default(), + reserve_x: Pubkey::default(), + reserve_y: Pubkey::default(), + v_parameters: VariableParameters { + volatility_accumulator: 625, + volatility_reference: 625, + index_reference: 0, + last_update_timestamp: 0, + ..VariableParameters::default() + }, + fee_owner: Pubkey::default(), + reward_infos: [RewardInfo::default(); NUM_REWARDS], + ..LbPair::default() + }; + + let amount = 1_234_567; + let fee = lb_pair.compute_fee(amount).unwrap(); + let amount_with_fees = amount + fee; + let fee_amount = lb_pair.compute_fee_from_amount(amount_with_fees).unwrap(); + + println!("{} {}", fee, fee_amount); + } + + #[test] + fn test_flip_bin_array_bit_internal() { + let mut lb_pair = LbPair::default(); + let index = 0; + lb_pair.flip_bin_array_bit_internal(index).unwrap(); + assert_eq!(lb_pair.internal_bit(index), true); + let index = 0; + lb_pair.flip_bin_array_bit_internal(index).unwrap(); + assert_eq!(lb_pair.internal_bit(index), false); + let index = 1; + lb_pair.flip_bin_array_bit_internal(index).unwrap(); + assert_eq!(lb_pair.internal_bit(index), true); + let index = 2; + lb_pair.flip_bin_array_bit_internal(index).unwrap(); + assert_eq!(lb_pair.internal_bit(index), true); + + // max range + let index = BIN_ARRAY_BITMAP_SIZE - 1; + lb_pair.flip_bin_array_bit_internal(index).unwrap(); + assert_eq!(lb_pair.internal_bit(index), true); + + // TODO add test overflow for BIN_ARRAY_BITMAP_SIZE + // TODO add test overflow for -BIN_ARRAY_BITMAP_SIZE-1 + let index = -BIN_ARRAY_BITMAP_SIZE; + lb_pair.flip_bin_array_bit_internal(index).unwrap(); + assert_eq!(lb_pair.internal_bit(index), true); + } + + #[test] + fn test_flip_all_bin_array_bit_internal() { + let mut lb_pair = LbPair::default(); + for i in -BIN_ARRAY_BITMAP_SIZE..BIN_ARRAY_BITMAP_SIZE { + lb_pair.flip_bin_array_bit_internal(i).unwrap(); + assert_eq!(lb_pair.internal_bit(i), true); + } + for i in -BIN_ARRAY_BITMAP_SIZE..BIN_ARRAY_BITMAP_SIZE { + lb_pair.flip_bin_array_bit_internal(i).unwrap(); + assert_eq!(lb_pair.internal_bit(i), false); + } + } + + #[test] + fn test_next_id_to_initialized_bin_array_in_default_range() { + let mut lb_pair = LbPair::default(); + let (min_bin_id, max_bin_id) = LbPair::bitmap_range(); + let index = max_bin_id; + // deposit liquidity + lb_pair.flip_bin_array_bit_internal(index).unwrap(); + assert_eq!(lb_pair.internal_bit(index), true); + // swap for y + lb_pair + .next_bin_array_index_with_liquidity(false, &None) + .unwrap(); + let bin_id = BinArray::bin_id_to_bin_array_index(lb_pair.active_id).unwrap(); + assert_eq!(index, bin_id); + + // withdraw liquidity + lb_pair.flip_bin_array_bit_internal(index).unwrap(); + assert_eq!(lb_pair.internal_bit(index), false); + // swap for x + let index = min_bin_id; + lb_pair.flip_bin_array_bit_internal(index).unwrap(); + assert_eq!(lb_pair.internal_bit(index), true); + lb_pair + .next_bin_array_index_with_liquidity(true, &None) + .unwrap(); + let bin_id = BinArray::bin_id_to_bin_array_index(lb_pair.active_id).unwrap(); + assert_eq!(index, bin_id); + } + + #[test] + fn test_next_id_to_initialized_bin_array_internal() { + let lb_pair = LbPair::default(); + let (min_bitmap_id, max_bitmap_id) = LbPair::bitmap_range(); + let (next_bin_array_id, ok) = lb_pair + .get_next_bin_array_index_with_liquidity_internal(false, 0) + .unwrap(); + assert_eq!(ok, false); + assert_eq!(next_bin_array_id, max_bitmap_id + 1); + + let (next_bin_array_id, ok) = lb_pair + .get_next_bin_array_index_with_liquidity_internal(true, 0) + .unwrap(); + assert_eq!(ok, false); + assert_eq!(next_bin_array_id, min_bitmap_id - 1); + } + + #[test] + fn test_next_id_from_non_zero_liquidity_bin_array() { + let mut lb_pair = LbPair::default(); + let (min_bitmap_id, max_bitmap_id) = LbPair::bitmap_range(); + for i in min_bitmap_id..=max_bitmap_id { + lb_pair.flip_bin_array_bit_internal(i).unwrap(); + let (lower_id, upper_id) = BinArray::get_bin_array_lower_upper_bin_id(i).unwrap(); + for j in lower_id..=upper_id { + lb_pair.active_id = j; + lb_pair + .next_bin_array_index_with_liquidity(false, &None) + .unwrap(); + assert_eq!(lb_pair.active_id, j); + + lb_pair + .next_bin_array_index_with_liquidity(true, &None) + .unwrap(); + assert_eq!(lb_pair.active_id, j); + } + } + } + + proptest! { + #[test] + fn test_get_bin_array_ranges( + start_bin_array_index in -6656..=6655, + end_bin_array_index in -6656..=6655) { + + if start_bin_array_index > end_bin_array_index { + return Ok(()); + } + let bin_range_list = LbPair::get_bin_array_ranges(start_bin_array_index, end_bin_array_index).unwrap(); + + let (min_bitmap_id, max_bitmap_id) = LbPair::bitmap_range(); + + + let mut cover_range = HashMap::new(); + + if let Some(BinRange{lower_index, upper_index}) = bin_range_list.positive_extension_bin_range { + assert!(lower_index<=upper_index); + assert!(end_bin_array_index==upper_index); + for i in lower_index..=upper_index{ + cover_range.insert(i, true); + } + } + if let Some(BinRange{lower_index, upper_index}) = bin_range_list.internal_bin_range { + assert!(lower_index<=upper_index); + assert!(lower_index >= min_bitmap_id); + assert!(upper_index <= max_bitmap_id); + for i in lower_index..=upper_index{ + assert!(cover_range.get(&i).is_none()); // avoid overlap + cover_range.insert(i, true); + } + + } + if let Some(BinRange{lower_index, upper_index}) = bin_range_list.negative_extension_bin_range { + assert!(lower_index<=upper_index); + assert!(start_bin_array_index==lower_index); + for i in lower_index..=upper_index{ + assert!(cover_range.get(&i).is_none()); // avoid overlap + cover_range.insert(i, true); + } + } + + // ensure cover the full range + for i in start_bin_array_index..=end_bin_array_index{ + assert!(cover_range.get(&i).is_some()); + } + } + + } + + proptest! { + #[test] + fn test_compute_composition_fee( + swap_amount in 1..=u32::MAX, + ) { + let bin_steps = [1, 2, 5, 10, 15, 20, 25, 50, 100]; + let active_id = 3333; + + for bin_step in bin_steps { + let mut lb_pair = LbPair::default(); + + let pair_type = PairType::Permissionless; + + lb_pair + .initialize( + 0, + active_id, + bin_step, + Pubkey::default(), + Pubkey::default(), + Pubkey::default(), + Pubkey::default(), + Pubkey::default(), + get_preset(bin_step).unwrap(), + pair_type, + PairStatus::Enabled.into(), + Pubkey::default(), + 0, + Pubkey::default(), + ) + .unwrap(); + + let fee_rate_f64 = lb_pair.get_base_fee().unwrap() as f64 / FEE_PRECISION as f64; + let expected_composition_fee = (swap_amount as f64 * fee_rate_f64 * (1.0 + fee_rate_f64)) as u64; + let composition_fee = lb_pair.compute_composition_fee(swap_amount.into()); + + assert!(composition_fee.is_ok()); + assert!(expected_composition_fee == composition_fee.unwrap()); + } + + } + } + + proptest! { + #[test] + fn test_is_bin_array_range_empty_internal( + lower_index in -512..=511, + upper_index in -512..=511, + flip_index in -512..=511) { + + if lower_index > upper_index { + return Ok(()); + } + let mut lb_pair = LbPair::default(); + lb_pair.flip_bin_array_bit_internal(flip_index).unwrap(); + + let is_zero_liquidity = lb_pair.is_bin_array_range_empty_internal(lower_index, upper_index).unwrap(); + if flip_index >= lower_index && flip_index <= upper_index { + assert!(!is_zero_liquidity); + }else{ + assert!(is_zero_liquidity); + } + } + + #[test] + fn test_is_bin_array_range_empty_internal_double( + lower_index in -512..=511, + upper_index in -512..=511, + flip_index_1 in -512..=511, + flip_index_2 in -512..=511) { + + if lower_index > upper_index { + return Ok(()); + } + if flip_index_1 == flip_index_2 { + return Ok(()); + } + let mut lb_pair = LbPair::default(); + lb_pair.flip_bin_array_bit_internal(flip_index_1).unwrap(); + lb_pair.flip_bin_array_bit_internal(flip_index_2).unwrap(); + + let is_zero_liquidity = lb_pair.is_bin_array_range_empty_internal(lower_index, upper_index).unwrap(); + if (flip_index_1 >= lower_index && flip_index_1 <= upper_index) || (flip_index_2 >= lower_index && flip_index_2 <= upper_index) { + assert!(!is_zero_liquidity); + }else{ + assert!(is_zero_liquidity); + } + } + } + + proptest! { + #[test] + fn test_next_bin_array_index_with_liquidity( + swap_for_y in 0..=1, + start_index in -512..511, + flip_id in -512..511) { + + let mut lb_pair = LbPair::default(); + lb_pair.flip_bin_array_bit_internal(flip_id).unwrap(); + assert_eq!( + lb_pair.internal_bit(flip_id), + true + ); + + let swap_for_y = if swap_for_y == 0 { + false + }else{ + true + }; + + let (next_bin_array_id, ok) = lb_pair + .get_next_bin_array_index_with_liquidity_internal(swap_for_y, start_index) + .unwrap(); + + + if swap_for_y { + if start_index >= flip_id { + assert_eq!(ok, true); + assert_eq!(next_bin_array_id, flip_id); + }else{ + assert_eq!(ok, false); + assert_eq!(next_bin_array_id, -513); + } + }else{ + if start_index <= flip_id { + assert_eq!(ok, true); + assert_eq!(next_bin_array_id, flip_id); + }else{ + assert_eq!(ok, false); + assert_eq!(next_bin_array_id, 512); + } + } + } + } +} diff --git a/programs/lb_clmm/src/state/mod.rs b/programs/lb_clmm/src/state/mod.rs index bb3422b..4c35a29 100644 --- a/programs/lb_clmm/src/state/mod.rs +++ b/programs/lb_clmm/src/state/mod.rs @@ -1,8 +1,9 @@ -pub mod action_access; pub mod bin; pub mod bin_array_bitmap_extension; +pub mod dynamic_position; pub mod lb_pair; pub mod oracle; pub mod parameters; pub mod position; pub mod preset_parameters; +pub use lb_pair::*; diff --git a/programs/lb_clmm/src/state/oracle.rs b/programs/lb_clmm/src/state/oracle.rs index 24ee256..2d3688e 100644 --- a/programs/lb_clmm/src/state/oracle.rs +++ b/programs/lb_clmm/src/state/oracle.rs @@ -254,3 +254,336 @@ impl<'info> OracleContentLoader<'info> for AccountLoader<'info, Oracle> { oracle_account_split(&self) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::constants::SAMPLE_LIFETIME; + use rand::Rng; + use std::cell::RefCell; + + struct OracleAccount { + oracle: RefCell, + observations: RefCell<[Observation; N]>, + } + + impl OracleAccount { + fn dynamic_oracle(&self) -> DynamicOracle<'_> { + DynamicOracle::new(self.oracle.borrow_mut(), self.observations.borrow_mut()) + } + + fn increase_length(self) -> OracleAccount { + let extended_oracle_account = setup_oracle_account::(); + + { + let metadata = self.oracle.borrow(); + let mut extended_metadata = extended_oracle_account.oracle.borrow_mut(); + extended_metadata.idx = metadata.idx; + extended_metadata.length = M as u64; + } + + { + let observations = self.observations.borrow(); + let mut extended_observations = extended_oracle_account.observations.borrow_mut(); + + for (idx, observation) in observations.iter().enumerate() { + let new_observation = &mut extended_observations[idx]; + new_observation.cumulative_active_bin_id = observation.cumulative_active_bin_id; + new_observation.created_at = observation.created_at; + new_observation.last_updated_at = observation.last_updated_at; + } + } + + extended_oracle_account + } + } + + fn setup_oracle_account() -> OracleAccount { + OracleAccount { + oracle: RefCell::new(Oracle { + idx: 0, + active_size: 0, + length: N as u64, + }), + observations: RefCell::new([Observation::default(); N]), + } + } + + #[test] + fn test_dynamic_oracle_samples_in_ascending_order() { + const SIZE: usize = 100; + let mut current_timestamp = 1698225292; + let mut active_id = 5555; + + let oracle_account = setup_oracle_account::(); + let mut dynamic_oracle = oracle_account.dynamic_oracle(); + + let mut rng = rand::thread_rng(); + let circular_count = rng.gen_range(1, 10); + let record_per_iteration = rng.gen_range(SIZE, SIZE + 20); + + for _ in 0..=circular_count { + for _ in 0..=record_per_iteration { + let timestamp_elapsed = rng.gen_range(5, 300); + let active_id_moved = rng.gen_range(-10, 10); + current_timestamp += timestamp_elapsed; + active_id += active_id_moved; + assert!(dynamic_oracle.update(active_id, current_timestamp).is_ok()); + } + } + + let mut start_idx = dynamic_oracle.metadata.idx; + + for _ in 0..SIZE - 1 { + let sample_cur = &dynamic_oracle.observations[start_idx as usize]; + start_idx = if start_idx == 0 { + dynamic_oracle.metadata.length - 1 + } else { + start_idx - 1 + }; + let sample_prev = &dynamic_oracle.observations[start_idx as usize]; + assert!(sample_cur.last_updated_at > sample_prev.last_updated_at); + } + } + + #[test] + fn test_dynamic_oracle_update_extendable_circular() { + let created_timestamp = 1698225292; + let mut current_timestamp = created_timestamp; + let active_id = 5555; + + let oracle_account = setup_oracle_account::<2>(); + let mut dynamic_oracle = oracle_account.dynamic_oracle(); + + current_timestamp += SAMPLE_LIFETIME as i64; + + assert!(dynamic_oracle.update(active_id, current_timestamp).is_ok()); + assert_eq!(dynamic_oracle.metadata.idx, 0); + + current_timestamp += SAMPLE_LIFETIME as i64; + + assert!(dynamic_oracle.update(active_id, current_timestamp).is_ok()); + assert_eq!(dynamic_oracle.metadata.idx, 1); + + drop(dynamic_oracle); + + let oracle_account = oracle_account.increase_length::<5>(); + let mut dynamic_oracle = oracle_account.dynamic_oracle(); + + for i in 2..5 { + current_timestamp += SAMPLE_LIFETIME as i64; + assert!(dynamic_oracle.update(active_id, current_timestamp).is_ok()); + assert_eq!(dynamic_oracle.metadata.idx, i); + } + + current_timestamp += SAMPLE_LIFETIME as i64; + assert!(dynamic_oracle.update(active_id, current_timestamp).is_ok()); + assert_eq!(dynamic_oracle.metadata.idx, 0); + } + + #[test] + fn test_dynamic_oracle_get_earliest_sample_mut() { + let current_timestamp = 1698225292; + let active_id = 5555; + + let oracle_account = setup_oracle_account::<2>(); + let mut dynamic_oracle = oracle_account.dynamic_oracle(); + + let timepoint_0 = current_timestamp; + assert!(dynamic_oracle.update(active_id, timepoint_0).is_ok()); + + let timepoint_1 = timepoint_0 + SAMPLE_LIFETIME as i64; + assert!(dynamic_oracle.update(active_id, timepoint_1).is_ok()); + + // Timepoint 2 overwrite timepoint 0 + let timepoint_2 = timepoint_1 + SAMPLE_LIFETIME as i64; + assert!(dynamic_oracle.update(active_id, timepoint_2).is_ok()); + + let earliest_sample = dynamic_oracle.get_earliest_sample().unwrap(); + assert!(earliest_sample.created_at == timepoint_1); + } + + #[test] + fn test_dynamic_oracle_get_latest_sample_mut() { + let mut current_timestamp = 1698225292; + let active_id = 5555; + + let oracle_account = setup_oracle_account::<2>(); + let mut dynamic_oracle = oracle_account.dynamic_oracle(); + + assert!(dynamic_oracle.update(active_id, current_timestamp).is_ok()); + let latest_sample = dynamic_oracle.get_latest_sample_mut().cloned().unwrap(); + assert_eq!(dynamic_oracle.metadata.idx, 0); + assert_eq!(latest_sample.last_updated_at, current_timestamp); + + current_timestamp += 100; + + assert!(dynamic_oracle.update(active_id, current_timestamp).is_ok()); + let latest_sample = dynamic_oracle.get_latest_sample_mut().cloned().unwrap(); + assert_eq!(dynamic_oracle.metadata.idx, 0); + assert_eq!(latest_sample.last_updated_at, current_timestamp); + + current_timestamp += SAMPLE_LIFETIME as i64; + + assert!(dynamic_oracle.update(active_id, current_timestamp).is_ok()); + let latest_sample = dynamic_oracle.get_latest_sample_mut().cloned().unwrap(); + assert_eq!(dynamic_oracle.metadata.idx, 1); + assert_eq!(latest_sample.last_updated_at, current_timestamp); + + current_timestamp += SAMPLE_LIFETIME as i64; + assert!(dynamic_oracle.update(active_id, current_timestamp).is_ok()); + let latest_sample = dynamic_oracle.get_latest_sample_mut().cloned().unwrap(); + assert_eq!(dynamic_oracle.metadata.idx, 0); + assert_eq!(latest_sample.last_updated_at, current_timestamp); + } + + #[test] + fn test_dynamic_oracle_metadata_active_size() { + let mut current_timestamp = 1698225292; + let active_id = 5555; + + let oracle_account = setup_oracle_account::<3>(); + let mut dynamic_oracle = oracle_account.dynamic_oracle(); + + assert!(dynamic_oracle.update(active_id, current_timestamp).is_ok()); + assert!(dynamic_oracle.metadata.idx == 0); + assert!(dynamic_oracle.metadata.active_size == 1); + + current_timestamp += SAMPLE_LIFETIME as i64; + + assert!(dynamic_oracle.update(active_id, current_timestamp).is_ok()); + assert!(dynamic_oracle.metadata.idx == 1); + assert!(dynamic_oracle.metadata.active_size == 2); + + current_timestamp += SAMPLE_LIFETIME as i64; + + assert!(dynamic_oracle.update(active_id, current_timestamp).is_ok()); + assert!(dynamic_oracle.metadata.idx == 2); + assert!(dynamic_oracle.metadata.active_size == 3); + + current_timestamp += SAMPLE_LIFETIME as i64; + + assert!(dynamic_oracle.update(active_id, current_timestamp).is_ok()); + assert!(dynamic_oracle.metadata.idx == 0); + assert!(dynamic_oracle.metadata.active_size == 3); + } + + #[test] + fn test_dynamic_oracle_next_reset() { + let oracle_account = setup_oracle_account::<2>(); + let mut dynamic_oracle = oracle_account.dynamic_oracle(); + + { + let observations = &mut dynamic_oracle.observations; + let sample_0 = &mut observations[0]; + sample_0.cumulative_active_bin_id = 1; + + let sample_1 = &mut observations[1]; + sample_1.cumulative_active_bin_id = 2; + } + + for observation in dynamic_oracle.observations.iter() { + assert!(observation.cumulative_active_bin_id > 0); + } + + let observation = dynamic_oracle.next_reset().unwrap(); + assert!(observation.cumulative_active_bin_id == 0); + assert!(dynamic_oracle.metadata.idx == 1); + + let observation = dynamic_oracle.next_reset().unwrap(); + assert!(observation.cumulative_active_bin_id == 0); + assert!(dynamic_oracle.metadata.idx == 0); + } + + #[test] + fn test_dynamic_oracle_update_multiple_samples() { + let created_timestamp = 1698225292; + let mut current_timestamp = created_timestamp; + let active_id = 5555; + + let oracle_account = setup_oracle_account::<2>(); + let mut dynamic_oracle = oracle_account.dynamic_oracle(); + + assert!(dynamic_oracle.update(active_id, current_timestamp).is_ok()); + assert_eq!(dynamic_oracle.metadata.idx, 0); + + let sample = dynamic_oracle.get_latest_sample_mut().unwrap(); + assert_eq!(sample.cumulative_active_bin_id, active_id as i128); + assert_eq!(sample.created_at, created_timestamp); + assert_eq!(sample.last_updated_at, current_timestamp); + + current_timestamp += SAMPLE_LIFETIME as i64; + + let accumulated_active_id = active_id as i64 * SAMPLE_LIFETIME as i64; + let cumulative_active_id = sample.cumulative_active_bin_id + accumulated_active_id as i128; + + assert!(dynamic_oracle.update(active_id, current_timestamp).is_ok()); + assert_eq!(dynamic_oracle.metadata.idx, 1); + + let sample = dynamic_oracle.get_latest_sample_mut().unwrap(); + assert_eq!(sample.cumulative_active_bin_id, cumulative_active_id); + assert_eq!(sample.created_at, current_timestamp); + assert_eq!(sample.last_updated_at, current_timestamp); + + current_timestamp += SAMPLE_LIFETIME as i64; + + let cumulative_active_id = sample.cumulative_active_bin_id + accumulated_active_id as i128; + + assert!(dynamic_oracle.update(active_id, current_timestamp).is_ok()); + assert_eq!(dynamic_oracle.metadata.idx, 0); + + let sample = dynamic_oracle.get_latest_sample_mut().unwrap(); + assert_eq!(sample.cumulative_active_bin_id, cumulative_active_id); + assert_eq!(sample.created_at, current_timestamp); + assert_eq!(sample.last_updated_at, current_timestamp); + } + + #[test] + fn test_dynamic_oracle_update_same_sample_if_lifetime_not_expired() { + let created_timestamp = 1698225292; + let mut current_timestamp = created_timestamp; + let mut active_id = 5555; + + let oracle_account = setup_oracle_account::<2>(); + let mut dynamic_oracle = oracle_account.dynamic_oracle(); + + assert!(dynamic_oracle.update(active_id, current_timestamp).is_ok()); + assert_eq!(dynamic_oracle.metadata.idx, 0); + + let sample = dynamic_oracle.get_latest_sample_mut().unwrap(); + assert_eq!(sample.cumulative_active_bin_id, active_id as i128); + assert_eq!(sample.created_at, created_timestamp); + assert_eq!(sample.last_updated_at, current_timestamp); + + let delta_seconds = 5; + let accumulated_active_id = active_id as i64 * delta_seconds; + let cumulative_active_id = sample.cumulative_active_bin_id + accumulated_active_id as i128; + + current_timestamp += delta_seconds; + + assert!(dynamic_oracle.update(active_id, current_timestamp).is_ok()); + active_id += 1; + + assert_eq!(dynamic_oracle.metadata.idx, 0); + + let sample = dynamic_oracle.get_latest_sample_mut().unwrap(); + assert_eq!(sample.cumulative_active_bin_id, cumulative_active_id); + assert_eq!(sample.created_at, created_timestamp); + assert_eq!(sample.last_updated_at, current_timestamp); + + let delta_seconds = 10; + let accumulated_active_id = active_id as i64 * delta_seconds; + let cumulative_active_id = sample.cumulative_active_bin_id + accumulated_active_id as i128; + + current_timestamp += delta_seconds; + assert!(dynamic_oracle.update(active_id, current_timestamp).is_ok()); + // active_id += 5; + + assert_eq!(dynamic_oracle.metadata.idx, 0); + + let sample = dynamic_oracle.get_latest_sample_mut().unwrap(); + assert_eq!(sample.cumulative_active_bin_id, cumulative_active_id); + assert_eq!(sample.created_at, created_timestamp); + assert_eq!(sample.last_updated_at, current_timestamp); + } +} diff --git a/programs/lb_clmm/src/state/parameters.rs b/programs/lb_clmm/src/state/parameters.rs index 36fb495..de9f18a 100644 --- a/programs/lb_clmm/src/state/parameters.rs +++ b/programs/lb_clmm/src/state/parameters.rs @@ -191,3 +191,196 @@ impl VariableParameters { self.update_volatility_accumulator(active_id, static_params) } } + +#[cfg(test)] +mod parameter_tests { + use super::*; + use crate::constants::tests::*; + use crate::state::lb_pair::{LbPair, PairType}; + use crate::state::PairStatus; + use proptest::proptest; + proptest! { + #[test] + fn test_update_volatility_accumulator_range( + max_volatility_accumulator in u32::MIN..=u32::MAX, + index_reference in i32::MIN..=i32::MAX, + volatility_accumulator in u32::MIN..=u32::MAX, + volatility_reference in u32::MIN..=u32::MAX, + ) { + for bin_step in PRESET_BIN_STEP { + let mut params = get_preset(bin_step).unwrap(); + params.max_volatility_accumulator = max_volatility_accumulator; + + let mut v_params = VariableParameters::default(); + v_params.index_reference = index_reference; + v_params.volatility_accumulator = volatility_accumulator; + v_params.volatility_reference = volatility_reference; + + assert!(v_params + .update_volatility_accumulator(i32::MAX, ¶ms) + .is_ok()); + } + + } + } + + #[test] + fn test_total_fee_volatile() { + let mut active_id = 1000; + let bin_step = 10; + let mut last_update_timestamp = 1_000_000; + + let mut lb_pair = LbPair::default(); + let pair_type = PairType::Permissionless; + + lb_pair + .initialize( + 0, + active_id, + bin_step, + Pubkey::default(), + Pubkey::default(), + Pubkey::default(), + Pubkey::default(), + Pubkey::default(), + get_preset(bin_step).unwrap(), + pair_type, + PairStatus::Enabled.into(), + Pubkey::default(), + 0, + Pubkey::default(), + ) + .unwrap(); + + let total_fee: u128 = lb_pair.get_total_fee().unwrap().try_into().unwrap(); + let fee_rate = total_fee as f64 / 10f64.powi(16); + println!("fee_rate {}", fee_rate); + + lb_pair + .v_parameters + .update_references(active_id, last_update_timestamp, &lb_pair.parameters) + .unwrap(); + + active_id += 1; + + lb_pair + .v_parameters + .update_volatility_accumulator(active_id, &lb_pair.parameters) + .unwrap(); + + let total_fee: u128 = lb_pair.get_total_fee().unwrap().try_into().unwrap(); + let fee_rate = total_fee as f64 / 10f64.powi(16); + println!("fee_rate {}", fee_rate); + + // Decay window + last_update_timestamp += 30; + + lb_pair + .v_parameters + .update_references(active_id, last_update_timestamp, &lb_pair.parameters) + .unwrap(); + + lb_pair + .v_parameters + .update_volatility_accumulator(active_id, &lb_pair.parameters) + .unwrap(); + + let total_fee: u128 = lb_pair.get_total_fee().unwrap().try_into().unwrap(); + let fee_rate = total_fee as f64 / 10f64.powi(16); + println!("fee_rate {}", fee_rate); + } + + #[test] + fn test_update_volatility_accumulator() { + let mut active_id = 1000; + let bin_step = 10; + let mut last_update_timestamp = 1_000_000; + + let static_param = get_preset(bin_step).unwrap(); + + let mut var_param = VariableParameters { + last_update_timestamp, + index_reference: active_id, + ..Default::default() + }; + + var_param + .update_references(active_id, last_update_timestamp, &static_param) + .unwrap(); + + active_id += 5; + + var_param + .update_volatility_accumulator(active_id, &static_param) + .unwrap(); + + println!("{:?}", var_param); + // High freq window + for _ in 0..1000 { + last_update_timestamp += 20; + + var_param + .update_references(active_id, last_update_timestamp, &static_param) + .unwrap(); + + var_param + .update_volatility_accumulator(active_id, &static_param) + .unwrap(); + } + + println!("{:?}", var_param); + + // Decay window + last_update_timestamp += 30; + + var_param + .update_references(active_id, last_update_timestamp, &static_param) + .unwrap(); + + active_id += 2; + + var_param + .update_volatility_accumulator(active_id, &static_param) + .unwrap(); + println!("{:?}", var_param); + + // High freq + last_update_timestamp += 10; + + var_param + .update_references(active_id, last_update_timestamp, &static_param) + .unwrap(); + + var_param + .update_volatility_accumulator(active_id, &static_param) + .unwrap(); + println!("{:?}", var_param); + + // Decay window + last_update_timestamp += 30; + + var_param + .update_references(active_id, last_update_timestamp, &static_param) + .unwrap(); + + var_param + .update_volatility_accumulator(active_id, &static_param) + .unwrap(); + println!("{:?}", var_param); + + // High freq + last_update_timestamp += 10; + + active_id += 2; + + var_param + .update_references(active_id, last_update_timestamp, &static_param) + .unwrap(); + + var_param + .update_volatility_accumulator(active_id, &static_param) + .unwrap(); + + println!("{:?}", var_param); + } +} diff --git a/programs/lb_clmm/src/state/position.rs b/programs/lb_clmm/src/state/position.rs index 28b5e47..66b06db 100644 --- a/programs/lb_clmm/src/state/position.rs +++ b/programs/lb_clmm/src/state/position.rs @@ -1,16 +1,9 @@ -use super::bin::Bin; use crate::{ - constants::{MAX_BIN_PER_POSITION, NUM_REWARDS}, - errors::LBError, - manager::bin_array_manager::BinArrayManager, - math::{ - safe_math::SafeMath, u128x128_math::Rounding, u64x64_math::SCALE_OFFSET, - utils_math::safe_mul_shr_cast, - }, + constants::{DEFAULT_BIN_PER_POSITION, NUM_REWARDS}, + math::safe_math::SafeMath, }; use anchor_lang::prelude::*; -use num_traits::Zero; -use std::cell::Ref; +use static_assertions::const_assert_eq; #[account(zero_copy)] #[derive(InitSpace, Debug)] @@ -20,11 +13,11 @@ pub struct Position { /// Owner of the position. Client rely on this to to fetch their positions. pub owner: Pubkey, /// Liquidity shares of this position in bins (lower_bin_id <-> upper_bin_id). This is the same as LP concept. - pub liquidity_shares: [u64; MAX_BIN_PER_POSITION], + pub liquidity_shares: [u64; DEFAULT_BIN_PER_POSITION], /// Farming reward information - pub reward_infos: [UserRewardInfo; MAX_BIN_PER_POSITION], + pub reward_infos: [UserRewardInfo; DEFAULT_BIN_PER_POSITION], /// Swap fee to claim information - pub fee_infos: [FeeInfo; MAX_BIN_PER_POSITION], + pub fee_infos: [FeeInfo; DEFAULT_BIN_PER_POSITION], /// Lower bin ID pub lower_bin_id: i32, /// Upper bin ID @@ -41,6 +34,17 @@ pub struct Position { pub _reserved: [u8; 160], } +impl Position { + // safe to use unwrap here + pub fn width(&self) -> usize { + self.upper_bin_id + .safe_add(1) + .unwrap() + .safe_sub(self.lower_bin_id) + .unwrap() as usize + } +} + #[account(zero_copy)] #[derive(InitSpace, Debug)] pub struct PositionV2 { @@ -49,11 +53,11 @@ pub struct PositionV2 { /// Owner of the position. Client rely on this to to fetch their positions. pub owner: Pubkey, /// Liquidity shares of this position in bins (lower_bin_id <-> upper_bin_id). This is the same as LP concept. - pub liquidity_shares: [u128; MAX_BIN_PER_POSITION], + pub liquidity_shares: [u128; DEFAULT_BIN_PER_POSITION], /// Farming reward information - pub reward_infos: [UserRewardInfo; MAX_BIN_PER_POSITION], + pub reward_infos: [UserRewardInfo; DEFAULT_BIN_PER_POSITION], /// Swap fee to claim information - pub fee_infos: [FeeInfo; MAX_BIN_PER_POSITION], + pub fee_infos: [FeeInfo; DEFAULT_BIN_PER_POSITION], /// Lower bin ID pub lower_bin_id: i32, /// Upper bin ID @@ -78,6 +82,8 @@ pub struct PositionV2 { pub _reserved: [u8; 87], } +const_assert_eq!(std::mem::size_of::(), 8112); + impl Default for PositionV2 { fn default() -> Self { Self { @@ -86,9 +92,9 @@ impl Default for PositionV2 { lower_bin_id: 0, upper_bin_id: 0, last_updated_at: 0, - liquidity_shares: [0u128; MAX_BIN_PER_POSITION], - reward_infos: [UserRewardInfo::default(); MAX_BIN_PER_POSITION], - fee_infos: [FeeInfo::default(); MAX_BIN_PER_POSITION], + liquidity_shares: [0u128; DEFAULT_BIN_PER_POSITION], + reward_infos: [UserRewardInfo::default(); DEFAULT_BIN_PER_POSITION], + fee_infos: [FeeInfo::default(); DEFAULT_BIN_PER_POSITION], total_claimed_fee_x_amount: 0, total_claimed_fee_y_amount: 0, total_claimed_rewards: [0u64; 2], @@ -100,6 +106,18 @@ impl Default for PositionV2 { } } } +impl PositionV2 { + // safe to use unwrap here + pub fn width(&self) -> usize { + self.upper_bin_id + .safe_add(1) + .unwrap() + .safe_sub(self.lower_bin_id) + .unwrap() as usize + } +} + +const_assert_eq!(std::mem::size_of::(), 8112); #[zero_copy] #[derive(Default, Debug, AnchorDeserialize, AnchorSerialize, InitSpace, PartialEq)] @@ -116,260 +134,3 @@ pub struct UserRewardInfo { pub reward_per_token_completes: [u128; NUM_REWARDS], pub reward_pendings: [u64; NUM_REWARDS], } - -impl PositionV2 { - pub fn init( - &mut self, - lb_pair: Pubkey, - owner: Pubkey, - operator: Pubkey, - lower_bin_id: i32, - upper_bin_id: i32, - current_time: i64, - lock_release_slot: u64, - subjected_to_bootstrap_liquidity_locking: bool, - fee_owner: Pubkey, - ) -> Result<()> { - self.lb_pair = lb_pair; - self.owner = owner; - self.operator = operator; - - self.lower_bin_id = lower_bin_id; - self.upper_bin_id = upper_bin_id; - - self.liquidity_shares = [0u128; MAX_BIN_PER_POSITION]; - self.reward_infos = [UserRewardInfo::default(); MAX_BIN_PER_POSITION]; - - self.last_updated_at = current_time; - self.lock_release_slot = lock_release_slot; - self.subjected_to_bootstrap_liquidity_locking = - subjected_to_bootstrap_liquidity_locking.into(); - - if subjected_to_bootstrap_liquidity_locking { - self.fee_owner = fee_owner; - } - - Ok(()) - } - - pub fn migrate_from_v1(&mut self, position: Ref<'_, Position>) -> Result<()> { - self.lb_pair = position.lb_pair; - self.owner = position.owner; - self.reward_infos = position.reward_infos; - self.fee_infos = position.fee_infos; - self.lower_bin_id = position.lower_bin_id; - self.upper_bin_id = position.upper_bin_id; - self.total_claimed_fee_x_amount = position.total_claimed_fee_x_amount; - self.total_claimed_fee_y_amount = position.total_claimed_fee_y_amount; - self.total_claimed_rewards = position.total_claimed_rewards; - self.last_updated_at = position.last_updated_at; - - for (i, &liquidity_share) in position.liquidity_shares.iter().enumerate() { - self.liquidity_shares[i] = u128::from(liquidity_share).safe_shl(SCALE_OFFSET.into())?; - } - Ok(()) - } - - pub fn id_within_position(&self, id: i32) -> Result<()> { - require!( - id >= self.lower_bin_id && id <= self.upper_bin_id, - LBError::InvalidPosition - ); - Ok(()) - } - - /// Return the width of the position. The width is 1 when the position have the same value for upper_bin_id, and lower_bin_id. - pub fn width(&self) -> Result { - Ok(self.upper_bin_id.safe_sub(self.lower_bin_id)?.safe_add(1)?) - } - - pub fn get_idx(&self, bin_id: i32) -> Result { - self.id_within_position(bin_id)?; - Ok(bin_id.safe_sub(self.lower_bin_id)? as usize) - } - - pub fn from_idx_to_bin_id(&self, i: usize) -> Result { - Ok(self.lower_bin_id.safe_add(i as i32)?) - } - - pub fn withdraw(&mut self, bin_id: i32, liquidity_share: u128) -> Result<()> { - let idx = self.get_idx(bin_id)?; - self.liquidity_shares[idx] = self.liquidity_shares[idx].safe_sub(liquidity_share)?; - - Ok(()) - } - - pub fn deposit(&mut self, bin_id: i32, liquidity_share: u128) -> Result<()> { - let idx = self.get_idx(bin_id)?; - self.liquidity_shares[idx] = self.liquidity_shares[idx].safe_add(liquidity_share)?; - - Ok(()) - } - - pub fn get_liquidity_share_in_bin(&self, bin_id: i32) -> Result { - let idx = self.get_idx(bin_id)?; - Ok(self.liquidity_shares[idx]) - } - - pub fn accumulate_total_claimed_rewards(&mut self, reward_index: usize, reward: u64) { - let total_claimed_reward = self.total_claimed_rewards[reward_index]; - self.total_claimed_rewards[reward_index] = total_claimed_reward.wrapping_add(reward); - } - - pub fn accumulate_total_claimed_fees(&mut self, fee_x: u64, fee_y: u64) { - self.total_claimed_fee_x_amount = self.total_claimed_fee_x_amount.wrapping_add(fee_x); - self.total_claimed_fee_y_amount = self.total_claimed_fee_y_amount.wrapping_add(fee_y); - } - - /// Update reward + fee earning - pub fn update_earning_per_token_stored( - &mut self, - bin_array_manager: &BinArrayManager, - ) -> Result<()> { - let (bin_arrays_lower_bin_id, bin_arrays_upper_bin_id) = - bin_array_manager.get_lower_upper_bin_id()?; - - // Make sure that the bin arrays cover all the bins of the position. - // TODO: Should we? Maybe we shall update only the bins the user are interacting with, and allow chunk for claim reward. - require!( - self.lower_bin_id >= bin_arrays_lower_bin_id - && self.upper_bin_id <= bin_arrays_upper_bin_id, - LBError::InvalidBinArray - ); - - for bin_id in self.lower_bin_id..=self.upper_bin_id { - let bin = bin_array_manager.get_bin(bin_id)?; - self.update_reward_per_token_stored(bin_id, &bin)?; - self.update_fee_per_token_stored(bin_id, &bin)?; - } - - Ok(()) - } - - pub fn update_fee_per_token_stored(&mut self, bin_id: i32, bin: &Bin) -> Result<()> { - let idx = self.get_idx(bin_id)?; - - let fee_infos = &mut self.fee_infos[idx]; - - let fee_x_per_token_stored = bin.fee_amount_x_per_token_stored; - - let new_fee_x: u64 = safe_mul_shr_cast( - self.liquidity_shares[idx] - .safe_shr(SCALE_OFFSET.into())? - .try_into() - .map_err(|_| LBError::TypeCastFailed)?, - fee_x_per_token_stored.safe_sub(fee_infos.fee_x_per_token_complete)?, - SCALE_OFFSET, - Rounding::Down, - )?; - - fee_infos.fee_x_pending = new_fee_x.safe_add(fee_infos.fee_x_pending)?; - fee_infos.fee_x_per_token_complete = fee_x_per_token_stored; - - let fee_y_per_token_stored = bin.fee_amount_y_per_token_stored; - - let new_fee_y: u64 = safe_mul_shr_cast( - self.liquidity_shares[idx] - .safe_shr(SCALE_OFFSET.into())? - .try_into() - .map_err(|_| LBError::TypeCastFailed)?, - fee_y_per_token_stored.safe_sub(fee_infos.fee_y_per_token_complete)?, - SCALE_OFFSET, - Rounding::Down, - )?; - - fee_infos.fee_y_pending = new_fee_y.safe_add(fee_infos.fee_y_pending)?; - fee_infos.fee_y_per_token_complete = fee_y_per_token_stored; - - Ok(()) - } - - pub fn update_reward_per_token_stored(&mut self, bin_id: i32, bin: &Bin) -> Result<()> { - let idx = self.get_idx(bin_id)?; - - let reward_info = &mut self.reward_infos[idx]; - for reward_idx in 0..NUM_REWARDS { - let reward_per_token_stored = bin.reward_per_token_stored[reward_idx]; - - let new_reward: u64 = safe_mul_shr_cast( - self.liquidity_shares[idx] - .safe_shr(SCALE_OFFSET.into())? - .try_into() - .map_err(|_| LBError::TypeCastFailed)?, - reward_per_token_stored - .safe_sub(reward_info.reward_per_token_completes[reward_idx])?, - SCALE_OFFSET, - Rounding::Down, - )?; - - reward_info.reward_pendings[reward_idx] = - new_reward.safe_add(reward_info.reward_pendings[reward_idx])?; - reward_info.reward_per_token_completes[reward_idx] = reward_per_token_stored; - } - - Ok(()) - } - - pub fn get_total_reward(&self, reward_index: usize) -> Result { - let mut total_reward = 0; - for val in self.reward_infos.iter() { - total_reward = total_reward.safe_add(val.reward_pendings[reward_index])?; - } - Ok(total_reward) - } - - pub fn reset_all_pending_reward(&mut self, reward_index: usize) { - for val in self.reward_infos.iter_mut() { - val.reward_pendings[reward_index] = 0; - } - } - - pub fn claim_fee(&mut self) -> Result<(u64, u64)> { - let mut fee_x = 0; - let mut fee_y = 0; - - for fee_info in self.fee_infos.iter_mut() { - fee_x = fee_x.safe_add(fee_info.fee_x_pending)?; - fee_info.fee_x_pending = 0; - - fee_y = fee_y.safe_add(fee_info.fee_y_pending)?; - fee_info.fee_y_pending = 0; - } - - Ok((fee_x, fee_y)) - } - - pub fn set_last_updated_at(&mut self, current_time: i64) { - self.last_updated_at = current_time; - } - - /// Position is empty when rewards is 0, fees is 0, and liquidity share is 0. - pub fn is_empty(&self) -> bool { - for (idx, liquidity_share) in self.liquidity_shares.iter().enumerate() { - if !liquidity_share.is_zero() { - return false; - } - let reward_infos = &self.reward_infos[idx]; - - for reward_pending in reward_infos.reward_pendings { - if !reward_pending.is_zero() { - return false; - } - } - - let fee_infos = &self.fee_infos[idx]; - if !fee_infos.fee_x_pending.is_zero() || !fee_infos.fee_y_pending.is_zero() { - return false; - } - } - true - } - - pub fn is_liquidity_locked(&self, current_slot: u64) -> bool { - current_slot < self.lock_release_slot - } - - pub fn is_subjected_to_initial_liquidity_locking(&self) -> bool { - self.subjected_to_bootstrap_liquidity_locking != 0 - } -} diff --git a/programs/lb_clmm/src/state/preset_parameters.rs b/programs/lb_clmm/src/state/preset_parameters.rs index 688b057..c61e5c8 100644 --- a/programs/lb_clmm/src/state/preset_parameters.rs +++ b/programs/lb_clmm/src/state/preset_parameters.rs @@ -1,12 +1,14 @@ use crate::constants::{BASIS_POINT_MAX, MAX_PROTOCOL_SHARE, U24_MAX}; use crate::errors::LBError; use crate::math::price_math::get_price_from_id; +use crate::math::safe_math::SafeMath; use anchor_lang::prelude::*; +use static_assertions::const_assert_eq; use super::parameters::StaticParameters; #[account] -#[derive(InitSpace, Debug)] +#[derive(InitSpace, Debug, Default, Copy)] pub struct PresetParameter { /// Bin step. Represent the price increment / decrement. pub bin_step: u16, @@ -30,6 +32,8 @@ pub struct PresetParameter { pub protocol_share: u16, } +const_assert_eq!(std::mem::size_of::(), 28); + impl PresetParameter { pub fn init( &mut self, @@ -111,23 +115,7 @@ impl PresetParameter { LBError::InvalidInput ); - let max_price = get_price_from_id(self.max_bin_id, self.bin_step); - let min_price = get_price_from_id(self.min_bin_id, self.bin_step); - - require!(max_price.is_ok(), LBError::InvalidInput); - require!(min_price.is_ok(), LBError::InvalidInput); - - // Bin is not swap-able when the price is u128::MAX, and 1. Make sure the min and max price bound is 2**127 - 1, 2 - if let Ok(max_price) = max_price { - require!( - max_price == 170141183460469231731687303715884105727, - LBError::InvalidInput - ); - } - - if let Ok(min_price) = min_price { - require!(min_price == 2, LBError::InvalidInput); - } + validate_min_max_bin_id(self.bin_step, self.min_bin_id, self.max_bin_id)?; Ok(()) } @@ -147,3 +135,49 @@ impl PresetParameter { } } } + +pub fn validate_min_max_bin_id(bin_step: u16, min_bin_id: i32, max_bin_id: i32) -> Result<()> { + require!(min_bin_id < max_bin_id, LBError::InvalidInput); + + let max_price = get_price_from_id(max_bin_id, bin_step); + let min_price = get_price_from_id(min_bin_id, bin_step); + + require!(max_price.is_ok(), LBError::InvalidInput); + require!(min_price.is_ok(), LBError::InvalidInput); + + // Bin is not swap-able when the price is u128::MAX, and 1. Make sure the min and max bin id is +/- 1 from edge min, and max bin id (bin ids with 1, and u128::MAX price). + let next_min_price = get_price_from_id(min_bin_id.safe_sub(1)?, bin_step)?; + require!(next_min_price == 1, LBError::InvalidInput); + + let next_max_price = get_price_from_id(max_bin_id.safe_add(1)?, bin_step)?; + require!(next_max_price == u128::MAX, LBError::InvalidInput); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::validate_min_max_bin_id; + use std::ops::Neg; + + #[test] + fn test_validate_min_max_bin_id() { + // Test case: (bin_step, bin_id) + let test_cases = vec![(1, 436704), (2, 218363), (5, 87358)]; + + for (bin_step, bin_id) in test_cases { + let validation_result = validate_min_max_bin_id(bin_step, bin_id.neg(), bin_id); + assert!(validation_result.is_ok()); + } + } + + #[test] + fn test_validate_min_max_bin_id_not_at_edge() { + let test_cases = vec![(1, 426704), (2, 208363), (5, 86358)]; + + for (bin_step, bin_id) in test_cases { + let validation_result = validate_min_max_bin_id(bin_step, bin_id.neg(), bin_id); + assert!(validation_result.is_err()); + } + } +} diff --git a/programs/lb_clmm/src/tests/mod.rs b/programs/lb_clmm/src/tests/mod.rs new file mode 100644 index 0000000..a4afda5 --- /dev/null +++ b/programs/lb_clmm/src/tests/mod.rs @@ -0,0 +1,8 @@ +#[cfg(test)] +mod reward_integration_tests; + +#[cfg(test)] +mod swap_integration_tests; + +#[cfg(test)] +pub use reward_integration_tests::*; diff --git a/programs/lb_clmm/src/tests/reward_integration_tests.rs b/programs/lb_clmm/src/tests/reward_integration_tests.rs new file mode 100644 index 0000000..ea1aadb --- /dev/null +++ b/programs/lb_clmm/src/tests/reward_integration_tests.rs @@ -0,0 +1,953 @@ +use crate::constants::tests::get_preset; +use crate::constants::*; +use crate::manager::bin_array_manager::BinArrayManager; +use crate::math::u128x128_math::Rounding; +use crate::math::u64x64_math::*; +use crate::math::utils_math::*; +use crate::state::bin::*; +use crate::state::dynamic_position::DynamicPosition; +use crate::state::dynamic_position::PositionBinData; +use crate::state::dynamic_position::PositionV3; +use crate::state::lb_pair::*; +use crate::state::parameters::*; +use anchor_lang::prelude::*; +use proptest::proptest; +use rand::prelude::*; +use std::cell::RefCell; +use std::cell::RefMut; + +#[test] +fn test_fund_reward() { + let active_id = 0; + + // init lb_pair, binArray and position + let lb_pair = init_lb_pair(active_id); + let mut lb_pair = lb_pair.try_borrow_mut().unwrap(); + + let mut bin_array = init_bin_array(active_id); + bin_array.is_bin_id_within_range(active_id).unwrap(); + + // Init reward + let reward_index = 0; + let reward_duration = 10; + init_reward(&mut lb_pair, reward_index, reward_duration); + + // Fund reward first time + let current_time = 100; + let funding_amount = 1000; + fund_reward( + &mut lb_pair, + &mut bin_array, + reward_index, + funding_amount, + current_time, + ); + + assert_lb_pair_reward_first_funding( + &lb_pair, + reward_index, + reward_duration, + current_time, + funding_amount, + ); + + // 3. Fund reward second time + let passed_duration = 5; + let fund_amount_1 = 2000; + + let distributed_reward = distributed_reward(&lb_pair, reward_index, passed_duration); + fund_reward( + &mut lb_pair, + &mut bin_array, + reward_index, + fund_amount_1, + current_time + passed_duration, + ); + + assert_lb_pair_reward( + &lb_pair, + reward_index, + current_time + passed_duration, + funding_amount + fund_amount_1 - distributed_reward, + ); + + // TODO add function clawback in case passed_duration, but no one actually deposit liquidity +} + +#[test] +fn test_claim_reward() { + let active_id = 0; + + // 0. init lb_pair, binArray and position + let lb_pair = init_lb_pair(active_id); + let mut lb_pair = lb_pair.try_borrow_mut().unwrap(); + + let mut bin_array = init_bin_array(active_id); + bin_array.is_bin_id_within_range(active_id).unwrap(); + + let global_data = RefCell::new(PositionV3 { + lower_bin_id: active_id, + upper_bin_id: active_id + DEFAULT_BIN_PER_POSITION as i32 - 1, + ..Default::default() + }); + let position_bin_data = RefCell::new([PositionBinData::default(); DEFAULT_BIN_PER_POSITION]); + let mut position_0 = + DynamicPosition::new(global_data.borrow_mut(), position_bin_data.borrow_mut()); + + position_0.id_within_position(active_id).unwrap(); + + // Deposit + let liquidity_share = 100u128.checked_shl(SCALE_OFFSET.into()).unwrap(); + let current_time = 100; + deposit( + &mut lb_pair, + &mut position_0, + &mut bin_array, + active_id, + liquidity_share, + current_time, + ); + assert_position_liquidity(&mut position_0, active_id, liquidity_share); + assert_bin_liquidity(&mut bin_array, active_id, liquidity_share); + + let reward_index = 0; + let reward_duration = 10; + init_reward(&mut lb_pair, reward_index, reward_duration); + + // Fund reward first time + let funding_amount = 1000; + fund_reward( + &mut lb_pair, + &mut bin_array, + reward_index, + funding_amount, + current_time, + ); + + let passed_duration_0 = 5; + let drop_reward = distributed_reward(&lb_pair, reward_index, passed_duration_0); + let current_time = current_time + passed_duration_0; + + let total_reward_0 = claim_reward( + &mut lb_pair, + &mut bin_array, + &mut position_0, + reward_index, + current_time, + ); + + assert_eq!(total_reward_0, drop_reward); + + position_0 + .reset_all_pending_reward( + reward_index, + position_0.lower_bin_id(), + position_0.upper_bin_id(), + ) + .unwrap(); + let total_reward = position_0 + .get_total_reward( + reward_index, + position_0.lower_bin_id(), + position_0.upper_bin_id(), + ) + .unwrap(); + assert_eq!(total_reward, 0); + + // other user deposit + let global_data = RefCell::new(PositionV3 { + lower_bin_id: active_id, + upper_bin_id: active_id + DEFAULT_BIN_PER_POSITION as i32 - 1, + ..Default::default() + }); + let position_bin_data = RefCell::new([PositionBinData::default(); DEFAULT_BIN_PER_POSITION]); + let mut position_1 = + DynamicPosition::new(global_data.borrow_mut(), position_bin_data.borrow_mut()); + // Deposit + deposit( + &mut lb_pair, + &mut position_1, + &mut bin_array, + active_id, + liquidity_share, + current_time, + ); + let passed_duration_1 = 5; + let current_time = current_time + passed_duration_1; + let drop_reward = distributed_reward( + &lb_pair, + reward_index, + passed_duration_0 + passed_duration_1, + ) - drop_reward; + + let total_reward_0 = claim_reward( + &mut lb_pair, + &mut bin_array, + &mut position_0, + reward_index, + current_time, + ); + let total_reward_1 = claim_reward( + &mut lb_pair, + &mut bin_array, + &mut position_1, + reward_index, + current_time, + ); + assert_eq!(total_reward_0 + total_reward_1, drop_reward); +} + +#[test] +fn test_deposit_after_reward_duration_end() { + let active_id = 0; + + // 0. init lb_pair, binArray and position + let lb_pair = init_lb_pair(active_id); + let mut lb_pair = lb_pair.try_borrow_mut().unwrap(); + + let mut bin_array = init_bin_array(active_id); + bin_array.is_bin_id_within_range(active_id).unwrap(); + + let global_data = RefCell::new(PositionV3 { + lower_bin_id: active_id, + upper_bin_id: active_id + DEFAULT_BIN_PER_POSITION as i32 - 1, + ..Default::default() + }); + let position_bin_data = RefCell::new([PositionBinData::default(); DEFAULT_BIN_PER_POSITION]); + let mut position = + DynamicPosition::new(global_data.borrow_mut(), position_bin_data.borrow_mut()); + + position.id_within_position(active_id).unwrap(); + + let current_time = 100; + + let reward_index = 0; + let reward_duration = 10; + init_reward(&mut lb_pair, reward_index, reward_duration); + + // Fund reward first time + let funding_amount = 1000; + fund_reward( + &mut lb_pair, + &mut bin_array, + reward_index, + funding_amount, + current_time, + ); + + // Deposit after reward duration end + let current_time = current_time + reward_duration + 1; + let liquidity_share = 100u128.checked_shl(SCALE_OFFSET.into()).unwrap(); + deposit( + &mut lb_pair, + &mut position, + &mut bin_array, + active_id, + liquidity_share, + current_time, + ); + let total_reward = claim_reward( + &mut lb_pair, + &mut bin_array, + &mut position, + reward_index, + current_time, + ); + // get nothing because reward will not distribute to bin with empty liquidity + assert_eq!(total_reward, 0); +} + +#[test] +fn test_two_reward_index() { + let active_id = 0; + + // 0. init lb_pair, binArray and position + let lb_pair = init_lb_pair(active_id); + let mut lb_pair = lb_pair.try_borrow_mut().unwrap(); + + let mut bin_array = init_bin_array(active_id); + + let global_data = RefCell::new(PositionV3 { + lower_bin_id: active_id, + upper_bin_id: active_id + DEFAULT_BIN_PER_POSITION as i32 - 1, + ..Default::default() + }); + let position_bin_data = RefCell::new([PositionBinData::default(); DEFAULT_BIN_PER_POSITION]); + let mut position = + DynamicPosition::new(global_data.borrow_mut(), position_bin_data.borrow_mut()); + + // Deposit + let liquidity_share = 100u128.checked_shl(SCALE_OFFSET.into()).unwrap(); + let current_time = 100; + deposit( + &mut lb_pair, + &mut position, + &mut bin_array, + active_id, + liquidity_share, + current_time, + ); + + let reward_index_0 = 0; + let reward_duration_0 = 10; + init_reward(&mut lb_pair, reward_index_0, reward_duration_0); + + // Fund reward first time + let funding_amount_0 = 1000; + fund_reward( + &mut lb_pair, + &mut bin_array, + reward_index_0, + funding_amount_0, + current_time, + ); + + let reward_index_1 = 1; + let reward_duration_1 = 15; + init_reward(&mut lb_pair, reward_index_1, reward_duration_1); + + // Fund reward first time + let funding_amount_1 = 2000; + fund_reward( + &mut lb_pair, + &mut bin_array, + reward_index_1, + funding_amount_1, + current_time, + ); + + let current_time_0 = current_time + reward_duration_0; + let current_time_1 = current_time + reward_duration_1; + + let current_time = if current_time_0 > current_time_1 { + current_time_0 + } else { + current_time_1 + }; + + let total_reward_0 = claim_reward( + &mut lb_pair, + &mut bin_array, + &mut position, + reward_index_0, + current_time, + ); + let total_reward_1 = claim_reward( + &mut lb_pair, + &mut bin_array, + &mut position, + reward_index_1, + current_time, + ); + + assert_eq!(funding_amount_0 - total_reward_0 <= 1, true); // precision + assert_eq!(funding_amount_1 - total_reward_1 <= 1, true); // precision +} + +#[test] +fn test_change_reward_duration() { + let active_id = 0; + + // 0. init lb_pair, binArray and position + let lb_pair = init_lb_pair(active_id); + let mut lb_pair = lb_pair.try_borrow_mut().unwrap(); + + let mut bin_array = init_bin_array(active_id); + + let global_data = RefCell::new(PositionV3 { + lower_bin_id: active_id, + upper_bin_id: active_id + DEFAULT_BIN_PER_POSITION as i32 - 1, + ..Default::default() + }); + let position_bin_data = RefCell::new([PositionBinData::default(); DEFAULT_BIN_PER_POSITION]); + let mut position = + DynamicPosition::new(global_data.borrow_mut(), position_bin_data.borrow_mut()); + + // Deposit + let liquidity_share = 100u128.checked_shl(SCALE_OFFSET.into()).unwrap(); + let current_time = 100; + deposit( + &mut lb_pair, + &mut position, + &mut bin_array, + active_id, + liquidity_share, + current_time, + ); + + let reward_index = 0; + let reward_duration = 10; + init_reward(&mut lb_pair, reward_index, reward_duration); + + // Fund reward first time + let funding_amount_0 = 1000; + fund_reward( + &mut lb_pair, + &mut bin_array, + reward_index, + funding_amount_0, + current_time, + ); + + let reward_duration = 15; + let current_time = current_time + reward_duration + 1; + change_reward_duration( + &mut lb_pair, + &mut bin_array, + reward_index, + reward_duration, + current_time, + ); + // Fund reward second time + let funding_amount_1 = 2000; + fund_reward( + &mut lb_pair, + &mut bin_array, + reward_index, + funding_amount_1, + current_time, + ); + + let current_time = current_time + reward_duration + 1; + let total_reward = claim_reward( + &mut lb_pair, + &mut bin_array, + &mut position, + reward_index, + current_time, + ); + assert_eq!( + funding_amount_0 + funding_amount_1 - total_reward <= 1, + true + ); // precision +} + +#[test] +fn test_reward_cross_multiple_bin_ids() { + let active_id = 0; + let reward_index = 0; + let reward_duration = 10; + + let lb_pair = init_lb_pair(active_id); + let mut lb_pair = lb_pair.try_borrow_mut().unwrap(); + + let mut bin_array = init_bin_array(active_id); + let global_data = RefCell::new(PositionV3 { + lower_bin_id: active_id, + upper_bin_id: active_id + DEFAULT_BIN_PER_POSITION as i32 - 1, + ..Default::default() + }); + let position_bin_data = RefCell::new([PositionBinData::default(); DEFAULT_BIN_PER_POSITION]); + let mut position = + DynamicPosition::new(global_data.borrow_mut(), position_bin_data.borrow_mut()); + + // Deposit + let liquidity_share = 100u128.checked_shl(SCALE_OFFSET.into()).unwrap(); + let current_time = 100; + deposit( + &mut lb_pair, + &mut position, + &mut bin_array, + active_id, + liquidity_share, + current_time, + ); + deposit( + &mut lb_pair, + &mut position, + &mut bin_array, + active_id + 1, + liquidity_share, + current_time, + ); + init_reward(&mut lb_pair, reward_index, reward_duration); + + // Fund reward first time + let funding_amount = 1000; + fund_reward( + &mut lb_pair, + &mut bin_array, + reward_index, + funding_amount, + current_time, + ); + + // swap caused active id change + let passed_duration = 5; + let current_time = current_time + passed_duration; + + swap(&mut lb_pair, &mut bin_array, current_time, active_id + 1); + + let current_time = current_time + reward_duration; + let total_reward = claim_reward( + &mut lb_pair, + &mut bin_array, + &mut position, + reward_index, + current_time, + ); + + assert_eq!(total_reward, funding_amount); +} + +proptest! { + #[test] + fn test_reward_precision( + funding_amount in 100u64..=1_000_000_000_000_000u64, + liquidity_share in 100u128..=1_000_000_000_000_000u128, + step in 10u64..=1000u64, + ) { + let active_id = 0; + let reward_index = 0; + let init_current_time = 100_000; + let mut current_time = init_current_time; + + // 0. init lb_pair, binArray and position + let lb_pair = init_lb_pair(active_id); + let mut lb_pair = lb_pair.try_borrow_mut().unwrap(); + + let mut bin_array = init_bin_array(active_id); + let global_data = RefCell::new(PositionV3 { + lower_bin_id: active_id, + upper_bin_id: active_id + DEFAULT_BIN_PER_POSITION as i32 - 1, + ..Default::default() + }); + let position_bin_data = RefCell::new([PositionBinData::default(); DEFAULT_BIN_PER_POSITION]); + let mut position = + DynamicPosition::new(global_data.borrow_mut(), position_bin_data.borrow_mut()); + + let reward_duration = 10_000; + init_reward(&mut lb_pair, reward_index, reward_duration); + + let mut rng = rand::thread_rng(); + let mut i = 0; + + let mut total_funding_amount = 0; + let mut total_claimed_reward = 0; + + // Deposit first, to ensure there always have reward distribution + deposit( + &mut lb_pair, + &mut position, + &mut bin_array, + active_id, + liquidity_share.checked_shl(SCALE_OFFSET.into()).unwrap(), + current_time, + ); + + while i < step { + let passed_duration = rng.gen_range(0, reward_duration / step); + current_time += passed_duration; + match rng.gen_range(0, 4) { + 0 => { + // simulate fund reward + fund_reward( + &mut lb_pair, + &mut bin_array, + reward_index, + funding_amount, + current_time, + ); + + total_funding_amount += funding_amount; + } + 1 => { + // simulate swap + swap(&mut lb_pair, &mut bin_array, current_time, active_id); + } + 2 => { + // simulate deposit liquidity + deposit( + &mut lb_pair, + &mut position, + &mut bin_array, + active_id, + liquidity_share.checked_shl(SCALE_OFFSET.into()).unwrap(), + current_time, + ) + } + 3 => { + // simulate claim reward + total_claimed_reward += claim_reward( + &mut lb_pair, + &mut bin_array, + &mut position, + reward_index, + current_time, + ); + position.reset_all_pending_reward(reward_index, position.lower_bin_id(), position.upper_bin_id()).unwrap(); + } + _ => panic!("not supported"), + } + i += 1; + } + + // claim everything left + total_claimed_reward += claim_reward( + &mut lb_pair, + &mut bin_array, + &mut position, + reward_index, + current_time + reward_duration, + ); + + // Avoid division 0 when there's no funding randomized + if total_funding_amount > 0 { + assert_eq!( + (total_funding_amount - total_claimed_reward) * 10 + / total_funding_amount, + 0 + ); + } + } + +} + +#[test] +fn test_rand_reward() { + let active_id = 0; + let reward_index = 0; + let init_current_time = 100_000; + let mut current_time = init_current_time; + + // 0. init lb_pair, binArray and position + let lb_pair = init_lb_pair(active_id); + let mut lb_pair = lb_pair.try_borrow_mut().unwrap(); + + let mut bin_array = init_bin_array(active_id); + let global_data = RefCell::new(PositionV3 { + lower_bin_id: active_id, + upper_bin_id: active_id + DEFAULT_BIN_PER_POSITION as i32 - 1, + ..Default::default() + }); + let position_bin_data = RefCell::new([PositionBinData::default(); DEFAULT_BIN_PER_POSITION]); + let mut position = + DynamicPosition::new(global_data.borrow_mut(), position_bin_data.borrow_mut()); + + let reward_duration = 10_000; + init_reward(&mut lb_pair, reward_index, reward_duration); + + let mut rng = rand::thread_rng(); + let mut i = 0; + + let mut total_funding_amount = 0; + let mut total_claimed_reward = 0; + let step = 1000; + let mut count_fund_reward = 0; + let mut count_swap = 0; + let mut count_deposit = 0; + let mut count_claim = 0; + while i < step { + let passed_duration = rng.gen_range(0, reward_duration / step); + current_time += passed_duration; + match rng.gen_range(0, 4) { + 0 => { + count_fund_reward += 1; + // simulate fund reward + let funding_amount = rng.gen_range(100u64, 1_000_000_000_000_000u64); + fund_reward( + &mut lb_pair, + &mut bin_array, + reward_index, + funding_amount, + current_time, + ); + + total_funding_amount += funding_amount; + } + 1 => { + // simulate swap + count_swap += 1; + swap(&mut lb_pair, &mut bin_array, current_time, active_id); + } + 2 => { + // simulate deposit liquidity + count_deposit += 1; + let liquidity_share = rng.gen_range(100u128, 1_000_000_000_000_000u128); + deposit( + &mut lb_pair, + &mut position, + &mut bin_array, + active_id, + liquidity_share.checked_shl(SCALE_OFFSET.into()).unwrap(), + current_time, + ) + } + 3 => { + // simulate claim reward + count_claim += 1; + total_claimed_reward += claim_reward( + &mut lb_pair, + &mut bin_array, + &mut position, + reward_index, + current_time, + ); + position + .reset_all_pending_reward( + reward_index, + position.lower_bin_id(), + position.upper_bin_id(), + ) + .unwrap(); + } + _ => panic!("not supported"), + } + i += 1; + } + + // claim everything left + total_claimed_reward += claim_reward( + &mut lb_pair, + &mut bin_array, + &mut position, + reward_index, + current_time + reward_duration, + ); + + println!( + "{} {} {} {}", + count_fund_reward, count_swap, count_deposit, count_claim + ); + assert_eq!( + (total_funding_amount - total_claimed_reward) * 1000 / total_funding_amount, + 0 + ); +} + +fn claim_reward( + lb_pair: &mut RefMut<'_, LbPair>, + bin_array: &mut BinArray, + position: &mut DynamicPosition, + reward_index: usize, + current_time: u64, +) -> u64 { + bin_array.update_all_rewards(lb_pair, current_time).unwrap(); + + let bin_arra_c = RefCell::new(*bin_array); + let mut bin_arrays = [bin_arra_c.borrow_mut()]; + + let bin_array_manager = BinArrayManager::new(&mut bin_arrays).unwrap(); + + position + .update_earning_per_token_stored( + &bin_array_manager, + position.lower_bin_id(), + position.upper_bin_id(), + ) + .unwrap(); + + position + .get_total_reward( + reward_index, + position.lower_bin_id(), + position.upper_bin_id(), + ) + .unwrap() +} + +fn init_lb_pair(active_id: i32) -> RefCell { + let bin_step = 10; + let lb_pair = LbPair { + parameters: get_preset(bin_step).unwrap(), + bin_step, + active_id: active_id, + bump_seed: [0], + protocol_fee: ProtocolFee::default(), + token_x_mint: Pubkey::default(), + token_y_mint: Pubkey::default(), + reserve_x: Pubkey::default(), + reserve_y: Pubkey::default(), + bin_step_seed: [0u8; 2], + v_parameters: VariableParameters { + volatility_accumulator: 10000, + ..VariableParameters::default() + }, + fee_owner: Pubkey::default(), + reward_infos: [RewardInfo::default(); NUM_REWARDS], + ..Default::default() + }; + RefCell::new(lb_pair) +} + +pub fn init_bin_array(active_id: i32) -> BinArray { + let index = (active_id % (MAX_BIN_PER_ARRAY as i32)) as i64; + BinArray { + index, + lb_pair: Pubkey::default(), + version: LayoutVersion::V1.into(), + _padding: [0u8; 7], + bins: [Bin::default(); MAX_BIN_PER_ARRAY], + } +} + +// pub fn init_position<'a>(position: &mut DynamicPosition, active_id: i32) { + +// position.init +// let lower_bin_id = active_id; +// let upper_bin_id = active_id + DEFAULT_BIN_PER_POSITION as i32 - 1; + +// let global_data = RefCell::new(PositionV3 { +// lower_bin_id, +// upper_bin_id, +// ..Default::default() +// }); +// let position_bin_data = RefCell::new([PositionBinData::default(); DEFAULT_BIN_PER_POSITION]); +// DynamicPosition::new(global_data.borrow_mut(), position_bin_data.borrow_mut()) +// } + +fn change_reward_duration( + lb_pair: &mut RefMut<'_, LbPair>, + bin_array: &mut BinArray, + reward_index: usize, + new_duration: u64, + current_time: u64, +) { + bin_array.update_all_rewards(lb_pair, current_time).unwrap(); + + let reward_info = &mut lb_pair.reward_infos[reward_index]; + reward_info.reward_duration = new_duration; +} + +fn swap( + lb_pair: &mut RefMut<'_, LbPair>, + bin_array: &mut BinArray, + current_timestamp: u64, + new_bin_id: i32, +) { + let active_id = lb_pair.active_id; + if bin_array.is_bin_id_within_range(active_id).is_ok() { + // update rewards + bin_array + .update_all_rewards(lb_pair, current_timestamp) + .unwrap(); + } + // update new bin id + lb_pair.active_id = new_bin_id; +} + +fn deposit( + lb_pair: &mut RefMut<'_, LbPair>, + position: &mut DynamicPosition, + bin_array: &mut BinArray, + bin_id: i32, + liquidity_share: u128, + current_time: u64, +) { + // let active_id = lb_pair.active_id; + bin_array.update_all_rewards(lb_pair, current_time).unwrap(); + + let bin_arra_c = RefCell::new(*bin_array); + let mut bin_arrays = [bin_arra_c.borrow_mut()]; + + let bin_array_manager = BinArrayManager::new(&mut bin_arrays).unwrap(); + + position + .update_earning_per_token_stored( + &bin_array_manager, + position.lower_bin_id(), + position.upper_bin_id(), + ) + .unwrap(); + + position.deposit(bin_id, liquidity_share).unwrap(); + let bin = bin_array.get_bin_mut(bin_id).unwrap(); + bin.deposit(0, 0, liquidity_share).unwrap(); +} + +fn assert_position_liquidity(position: &mut DynamicPosition, bin_id: i32, liquidity_share: u128) { + let position_share = position.get_liquidity_share_in_bin(bin_id).unwrap(); + assert_eq!(position_share, liquidity_share); +} + +fn assert_bin_liquidity(bin_array: &mut BinArray, bin_id: i32, liquidity_share: u128) { + let bin = bin_array.get_bin(bin_id).unwrap(); + assert_eq!(bin.liquidity_supply, liquidity_share); +} + +fn init_reward(lb_pair: &mut LbPair, reward_index: usize, reward_duration: u64) { + let reward_info = &mut lb_pair.reward_infos[reward_index]; + reward_info.init_reward( + Pubkey::new_unique(), + Pubkey::default(), + Pubkey::default(), + reward_duration, + ); +} + +fn fund_reward( + lb_pair: &mut RefMut<'_, LbPair>, + bin_array: &mut BinArray, + reward_index: usize, + amount: u64, + current_time: u64, +) { + bin_array.update_all_rewards(lb_pair, current_time).unwrap(); + + let reward_info = &mut lb_pair.reward_infos[reward_index]; + reward_info + .update_rate_after_funding(current_time, amount) + .unwrap(); +} + +fn assert_lb_pair_reward_first_funding( + lb_pair: &LbPair, + reward_index: usize, + reward_duration: u64, + current_time: u64, + funding_amount: u64, +) { + let reward_info = &lb_pair.reward_infos[reward_index]; + assert_eq!(reward_info.reward_duration, reward_duration); + assert_eq!( + reward_info.reward_duration_end, + reward_duration + current_time + ); + assert_eq!(reward_info.last_update_time, current_time); + + let reward_rate = safe_shl_div_cast( + funding_amount.into(), + reward_duration as u128, + SCALE_OFFSET, + Rounding::Down, + ) + .unwrap(); + + assert_eq!(reward_info.reward_rate, reward_rate); +} + +fn distributed_reward(lb_pair: &LbPair, reward_index: usize, passed_duration: u64) -> u64 { + let reward_info = &lb_pair.reward_infos[reward_index]; + + let distributed_reward: u64 = safe_mul_shr_cast( + reward_info.reward_rate, + passed_duration as u128, + SCALE_OFFSET, + Rounding::Down, + ) + .unwrap(); + return distributed_reward; +} +fn assert_lb_pair_reward( + lb_pair: &LbPair, + reward_index: usize, + current_time: u64, + total_funding_amount: u64, +) { + let reward_info = &lb_pair.reward_infos[reward_index]; + assert_eq!( + reward_info.reward_duration_end, + reward_info.reward_duration + current_time + ); + assert_eq!(reward_info.last_update_time, current_time); + + let reward_rate = safe_shl_div_cast( + total_funding_amount.into(), + reward_info.reward_duration as u128, + SCALE_OFFSET, + Rounding::Down, + ) + .unwrap(); + + assert_eq!(reward_info.reward_rate, reward_rate); +} diff --git a/programs/lb_clmm/src/tests/swap_integration_tests.rs b/programs/lb_clmm/src/tests/swap_integration_tests.rs new file mode 100644 index 0000000..8a216ae --- /dev/null +++ b/programs/lb_clmm/src/tests/swap_integration_tests.rs @@ -0,0 +1,148 @@ +use crate::constants::DEFAULT_BIN_PER_POSITION; +use crate::constants::MAX_BIN_PER_ARRAY; +use crate::manager::bin_array_manager::BinArrayManager; +use crate::math::u64x64_math::SCALE_OFFSET; +use crate::state::bin::BinArray; +use crate::state::dynamic_position::DynamicPosition; +use crate::state::dynamic_position::PositionBinData; +use crate::state::dynamic_position::PositionV3; +use crate::tests::init_bin_array; +use proptest::proptest; +use rand::Rng; +use std::cell::RefCell; + +proptest! { + #[test] + fn test_swap_fee_precision( + fee in 100u64..=1_000_000_000_000_000_000u64, + liquidity_share in 100u128..=1_000_000_000_000_000u128, + step in 10u64..=1000u64, + swap_for_y in 0..=1, + bin_id in 0..MAX_BIN_PER_ARRAY, + bin_offset in 0..MAX_BIN_PER_ARRAY, + ){ + let active_id = 0; + let mut bin_array = init_bin_array(active_id); + + + let global_data = RefCell::new(PositionV3 { + lower_bin_id: active_id, + upper_bin_id: active_id + DEFAULT_BIN_PER_POSITION as i32 - 1, + ..Default::default() + }); + let position_bin_data = RefCell::new([PositionBinData::default(); DEFAULT_BIN_PER_POSITION]); + let mut position = + DynamicPosition::new(global_data.borrow_mut(), position_bin_data.borrow_mut()); + + + let mut rng = rand::thread_rng(); + let mut i = 0; + + let mut total_swap_fee_x_amount = 0u64; + let mut total_swap_fee_y_amount = 0u64; + let mut total_claimed_x_fee = 0u64; + let mut total_claimed_y_fee = 0u64; + while i < step { + match rng.gen_range(0, 3) { + 0 => { + + let bin = & bin_array.bins[bin_offset]; + let swap_for_y = if swap_for_y == 0 { + false + }else{ + true + }; + if !bin.is_empty(swap_for_y) { + if !swap_for_y { + total_swap_fee_y_amount = total_swap_fee_y_amount.checked_add(fee).unwrap(); + }else{ + total_swap_fee_x_amount = total_swap_fee_x_amount.checked_add(fee).unwrap(); + } + swap(fee, false, &mut bin_array, bin_offset); + } + } + 1 => { + deposit( &mut position, &mut bin_array, bin_id as i32, liquidity_share.checked_shl(SCALE_OFFSET.into()).unwrap()); + } + 2 => { + let (fee_x, fee_y) = claim_fee(&mut bin_array, &mut position); + total_claimed_x_fee = total_claimed_x_fee.checked_add(fee_x).unwrap(); + total_claimed_y_fee = total_claimed_y_fee.checked_add(fee_y).unwrap(); + } + _ => panic!("not supported"), + } + i += 1; + } + + // claim everything left + let (fee_x, fee_y) = claim_fee(&mut bin_array, &mut position); + total_claimed_x_fee = total_claimed_x_fee.checked_add(fee_x).unwrap(); + total_claimed_y_fee = total_claimed_y_fee.checked_add(fee_y).unwrap(); + + + if total_swap_fee_x_amount == 0 { + assert_eq!(total_claimed_x_fee, 0); + }else{ + assert_eq!( + (total_swap_fee_x_amount - total_claimed_x_fee) * 10000 / total_swap_fee_x_amount, + 0 + ); + } + + if total_swap_fee_y_amount == 0 { + assert_eq!(total_claimed_y_fee, 0); + }else{ + assert_eq!( + (total_swap_fee_y_amount - total_claimed_y_fee) * 10000 / total_swap_fee_y_amount, + 0 + ); + } + } +} + +fn deposit( + position: &mut DynamicPosition, + bin_array: &mut BinArray, + bin_id: i32, + liquidity_share: u128, +) { + let bin_arra_c = RefCell::new(*bin_array); + let mut bin_arrays = [bin_arra_c.borrow_mut()]; + + let bin_array_manager = BinArrayManager::new(&mut bin_arrays).unwrap(); + + position + .update_earning_per_token_stored( + &bin_array_manager, + position.lower_bin_id(), + position.upper_bin_id(), + ) + .unwrap(); + + position.deposit(bin_id, liquidity_share).unwrap(); + let bin = bin_array.get_bin_mut(bin_id).unwrap(); + bin.deposit(0, 0, liquidity_share).unwrap(); +} +fn swap(fee: u64, swap_for_y: bool, bin_array: &mut BinArray, bin_offset: usize) { + let bin = &mut bin_array.bins[bin_offset]; + bin.update_fee_per_token_stored(fee, swap_for_y).unwrap(); +} + +fn claim_fee(bin_array: &mut BinArray, position: &mut DynamicPosition) -> (u64, u64) { + let bin_arra_c = RefCell::new(*bin_array); + let mut bin_arrays = [bin_arra_c.borrow_mut()]; + + let bin_array_manager = BinArrayManager::new(&mut bin_arrays).unwrap(); + + position + .update_earning_per_token_stored( + &bin_array_manager, + position.lower_bin_id(), + position.upper_bin_id(), + ) + .unwrap(); + + position + .claim_fee(position.lower_bin_id(), position.upper_bin_id()) + .unwrap() +} diff --git a/programs/lb_clmm/src/utils/pda.rs b/programs/lb_clmm/src/utils/pda.rs index b561b2a..ed998f0 100644 --- a/programs/lb_clmm/src/utils/pda.rs +++ b/programs/lb_clmm/src/utils/pda.rs @@ -106,7 +106,7 @@ pub fn derive_preset_parameter_pda(bin_step: u16) -> (Pubkey, u8) { Pubkey::find_program_address(&[PRESET_PARAMETER, &bin_step.to_le_bytes()], &crate::ID) } -pub fn derive_preset_parameter_pda2(bin_step: u16, base_factor: u16) -> (Pubkey, u8) { +pub fn derive_preset_parameter_pda_v2(bin_step: u16, base_factor: u16) -> (Pubkey, u8) { Pubkey::find_program_address( &[ PRESET_PARAMETER, diff --git a/target/idl/lb_clmm.json b/target/idl/lb_clmm.json index 934f5a3..28d060d 100644 --- a/target/idl/lb_clmm.json +++ b/target/idl/lb_clmm.json @@ -15,12 +15,26 @@ "value": "70" }, { - "name": "MAX_BIN_PER_POSITION", + "name": "DEFAULT_BIN_PER_POSITION", "type": { "defined": "usize" }, "value": "70" }, + { + "name": "MAX_RESIZE_LENGTH", + "type": { + "defined": "usize" + }, + "value": "70" + }, + { + "name": "POSITION_MAX_LENGTH", + "type": { + "defined": "usize" + }, + "value": "1400" + }, { "name": "MIN_BIN_ID", "type": "i32", @@ -398,16 +412,6 @@ "isMut": false, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -492,16 +496,6 @@ "isMut": false, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -586,16 +580,6 @@ "isMut": false, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -665,16 +649,6 @@ "isMut": false, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -739,16 +713,6 @@ "isMut": false, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -828,16 +792,6 @@ "isMut": false, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -1489,16 +1443,6 @@ "isMut": true, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -1539,6 +1483,14 @@ { "name": "rewardIndex", "type": "u64" + }, + { + "name": "minBinId", + "type": "i32" + }, + { + "name": "maxBinId", + "type": "i32" } ] }, @@ -1555,16 +1507,6 @@ "isMut": true, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -1616,7 +1558,16 @@ "isSigner": false } ], - "args": [] + "args": [ + { + "name": "minBinId", + "type": "i32" + }, + { + "name": "maxBinId", + "type": "i32" + } + ] }, { "name": "closePosition", @@ -1626,21 +1577,6 @@ "isMut": true, "isSigner": false }, - { - "name": "lbPair", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -1733,6 +1669,101 @@ } ] }, + { + "name": "increasePositionLength", + "accounts": [ + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "name": "lbPair", + "isMut": false, + "isSigner": false + }, + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "lengthToAdd", + "type": "u16" + }, + { + "name": "side", + "type": "u8" + } + ] + }, + { + "name": "decreasePositionLength", + "accounts": [ + { + "name": "rentReceiver", + "isMut": true, + "isSigner": false + }, + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "lengthToRemove", + "type": "u16" + }, + { + "name": "side", + "type": "u8" + } + ] + }, { "name": "initializePresetParameter", "accounts": [ @@ -1766,6 +1797,39 @@ } ] }, + { + "name": "initializePresetParameterV2", + "accounts": [ + { + "name": "presetParameter", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "ix", + "type": { + "defined": "InitPresetParametersIx" + } + } + ] + }, { "name": "closePresetParameter", "accounts": [ @@ -1836,16 +1900,6 @@ "isMut": false, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -1872,9 +1926,18 @@ "isSigner": false } ], - "args": [] - }, - { + "args": [ + { + "name": "minBinId", + "type": "i32" + }, + { + "name": "maxBinId", + "type": "i32" + } + ] + }, + { "name": "togglePairStatus", "accounts": [ { @@ -1916,10 +1979,10 @@ ] }, { - "name": "migratePosition", + "name": "migratePositionFromV1", "accounts": [ { - "name": "positionV2", + "name": "positionV3", "isMut": true, "isSigner": true }, @@ -1971,6 +2034,62 @@ ], "args": [] }, + { + "name": "migratePositionFromV2", + "accounts": [ + { + "name": "positionV3", + "isMut": true, + "isSigner": true + }, + { + "name": "positionV2", + "isMut": true, + "isSigner": false + }, + { + "name": "lbPair", + "isMut": false, + "isSigner": false + }, + { + "name": "binArrayLower", + "isMut": true, + "isSigner": false + }, + { + "name": "binArrayUpper", + "isMut": true, + "isSigner": false + }, + { + "name": "owner", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rentReceiver", + "isMut": true, + "isSigner": false + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "migrateBinArray", "accounts": [ @@ -1995,23 +2114,22 @@ "isMut": true, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "owner", "isMut": false, "isSigner": true } ], - "args": [] + "args": [ + { + "name": "minBinId", + "type": "i32" + }, + { + "name": "maxBinId", + "type": "i32" + } + ] }, { "name": "withdrawIneligibleReward", @@ -2150,6 +2268,204 @@ "type": "u64" } ] + }, + { + "name": "removeLiquidityByRange", + "accounts": [ + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "lbPair", + "isMut": true, + "isSigner": false + }, + { + "name": "binArrayBitmapExtension", + "isMut": true, + "isSigner": false, + "isOptional": true + }, + { + "name": "userTokenX", + "isMut": true, + "isSigner": false + }, + { + "name": "userTokenY", + "isMut": true, + "isSigner": false + }, + { + "name": "reserveX", + "isMut": true, + "isSigner": false + }, + { + "name": "reserveY", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenXMint", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenYMint", + "isMut": false, + "isSigner": false + }, + { + "name": "sender", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenXProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenYProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "fromBinId", + "type": "i32" + }, + { + "name": "toBinId", + "type": "i32" + }, + { + "name": "bpsToRemove", + "type": "u16" + } + ] + }, + { + "name": "addLiquidityOneSidePrecise", + "accounts": [ + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "lbPair", + "isMut": true, + "isSigner": false + }, + { + "name": "binArrayBitmapExtension", + "isMut": true, + "isSigner": false, + "isOptional": true + }, + { + "name": "userToken", + "isMut": true, + "isSigner": false + }, + { + "name": "reserve", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenMint", + "isMut": false, + "isSigner": false + }, + { + "name": "sender", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "parameter", + "type": { + "defined": "AddLiquiditySingleSidePreciseParameter" + } + } + ] + }, + { + "name": "goToABin", + "accounts": [ + { + "name": "lbPair", + "isMut": true, + "isSigner": false + }, + { + "name": "binArrayBitmapExtension", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "fromBinArray", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "toBinArray", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "binId", + "type": "i32" + } + ] } ], "accounts": [ @@ -2159,70 +2475,187 @@ "kind": "struct", "fields": [ { - "name": "lbPair", - "type": "publicKey" + "name": "lbPair", + "type": "publicKey" + }, + { + "name": "positiveBinArrayBitmap", + "docs": [ + "Packed initialized bin array state for start_bin_index is positive" + ], + "type": { + "array": [ + { + "array": [ + "u64", + 8 + ] + }, + 12 + ] + } + }, + { + "name": "negativeBinArrayBitmap", + "docs": [ + "Packed initialized bin array state for start_bin_index is negative" + ], + "type": { + "array": [ + { + "array": [ + "u64", + 8 + ] + }, + 12 + ] + } + } + ] + } + }, + { + "name": "BinArray", + "docs": [ + "An account to contain a range of bin. For example: Bin 100 <-> 200.", + "For example:", + "BinArray index: 0 contains bin 0 <-> 599", + "index: 2 contains bin 600 <-> 1199, ..." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "index", + "type": "i64" + }, + { + "name": "version", + "docs": [ + "Version of binArray" + ], + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 7 + ] + } + }, + { + "name": "lbPair", + "type": "publicKey" + }, + { + "name": "bins", + "type": { + "array": [ + { + "defined": "Bin" + }, + 70 + ] + } + } + ] + } + }, + { + "name": "PositionV3", + "type": { + "kind": "struct", + "fields": [ + { + "name": "lbPair", + "docs": [ + "The LB pair of this position" + ], + "type": "publicKey" + }, + { + "name": "owner", + "docs": [ + "Owner of the position. Client rely on this to to fetch their positions." + ], + "type": "publicKey" + }, + { + "name": "lowerBinId", + "docs": [ + "Lower bin ID" + ], + "type": "i32" + }, + { + "name": "upperBinId", + "docs": [ + "Upper bin ID" + ], + "type": "i32" + }, + { + "name": "lastUpdatedAt", + "docs": [ + "Last updated timestamp" + ], + "type": "i64" + }, + { + "name": "totalClaimedFeeXAmount", + "docs": [ + "Total claimed token fee X" + ], + "type": "u64" }, { - "name": "positiveBinArrayBitmap", + "name": "totalClaimedFeeYAmount", "docs": [ - "Packed initialized bin array state for start_bin_index is positive" + "Total claimed token fee Y" ], - "type": { - "array": [ - { - "array": [ - "u64", - 8 - ] - }, - 12 - ] - } + "type": "u64" }, { - "name": "negativeBinArrayBitmap", + "name": "totalClaimedRewards", "docs": [ - "Packed initialized bin array state for start_bin_index is negative" + "Total claimed rewards" ], "type": { "array": [ - { - "array": [ - "u64", - 8 - ] - }, - 12 + "u64", + 2 ] } - } - ] - } - }, - { - "name": "BinArray", - "docs": [ - "An account to contain a range of bin. For example: Bin 100 <-> 200.", - "For example:", - "BinArray index: 0 contains bin 0 <-> 599", - "index: 2 contains bin 600 <-> 1199, ..." - ], - "type": { - "kind": "struct", - "fields": [ + }, { - "name": "index", - "type": "i64" + "name": "operator", + "docs": [ + "Operator of position" + ], + "type": "publicKey" }, { - "name": "version", + "name": "lockReleaseSlot", "docs": [ - "Version of binArray" + "Slot which the locked liquidity can be withdraw" + ], + "type": "u64" + }, + { + "name": "subjectedToBootstrapLiquidityLocking", + "docs": [ + "Is the position subjected to liquidity locking for the launch pool." ], "type": "u8" }, { - "name": "padding", + "name": "padding0", + "docs": [ + "Padding" + ], "type": { "array": [ "u8", @@ -2231,17 +2664,28 @@ } }, { - "name": "lbPair", + "name": "feeOwner", + "docs": [ + "Address is able to claim fee in this position, only valid for bootstrap_liquidity_position" + ], "type": "publicKey" }, { - "name": "bins", + "name": "length", + "docs": [ + "Number of bins" + ], + "type": "u64" + }, + { + "name": "reserved", + "docs": [ + "Reserved space for future use" + ], "type": { "array": [ - { - "defined": "Bin" - }, - 70 + "u8", + 128 ] } } @@ -3103,6 +3547,42 @@ ] } }, + { + "name": "AddLiquiditySingleSidePreciseParameter", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bins", + "type": { + "vec": { + "defined": "CompressedBinDepositAmount" + } + } + }, + { + "name": "decompressMultiplier", + "type": "u64" + } + ] + } + }, + { + "name": "CompressedBinDepositAmount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "binId", + "type": "i32" + }, + { + "name": "amount", + "type": "u32" + } + ] + } + }, { "name": "BinLiquidityDistribution", "type": { @@ -3389,6 +3869,30 @@ ] } }, + { + "name": "PositionBinData", + "type": { + "kind": "struct", + "fields": [ + { + "name": "liquidityShare", + "type": "u128" + }, + { + "name": "rewardInfo", + "type": { + "defined": "UserRewardInfo" + } + }, + { + "name": "feeInfo", + "type": { + "defined": "FeeInfo" + } + } + ] + } + }, { "name": "ProtocolFee", "type": { @@ -3767,6 +4271,23 @@ ] } }, + { + "name": "ResizeSide", + "docs": [ + "Side of resize, 0 for lower and 1 for upper" + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "Lower" + }, + { + "name": "Upper" + } + ] + } + }, { "name": "PairType", "docs": [ @@ -4193,6 +4714,66 @@ } ] }, + { + "name": "IncreasePositionLength", + "fields": [ + { + "name": "lbPair", + "type": "publicKey", + "index": false + }, + { + "name": "position", + "type": "publicKey", + "index": false + }, + { + "name": "owner", + "type": "publicKey", + "index": false + }, + { + "name": "lengthToAdd", + "type": "u16", + "index": false + }, + { + "name": "side", + "type": "u8", + "index": false + } + ] + }, + { + "name": "DecreasePositionLength", + "fields": [ + { + "name": "lbPair", + "type": "publicKey", + "index": false + }, + { + "name": "position", + "type": "publicKey", + "index": false + }, + { + "name": "owner", + "type": "publicKey", + "index": false + }, + { + "name": "lengthToRemove", + "type": "u16", + "index": false + }, + { + "name": "side", + "type": "u8", + "index": false + } + ] + }, { "name": "FeeParameterUpdate", "fields": [ @@ -4297,6 +4878,26 @@ "index": false } ] + }, + { + "name": "GoToABin", + "fields": [ + { + "name": "lbPair", + "type": "publicKey", + "index": false + }, + { + "name": "fromBinId", + "type": "i32", + "index": false + }, + { + "name": "toBinId", + "type": "i32", + "index": false + } + ] } ], "errors": [ @@ -4542,21 +5143,68 @@ }, { "code": 6048, + "name": "UnauthorizedAddress", + "msg": "Unauthorized address" + }, + { + "code": 6049, + "name": "OperatorsAreTheSame", + "msg": "Cannot update because operators are the same" + }, + { + "code": 6050, + "name": "WithdrawToWrongTokenAccount", + "msg": "Withdraw to wrong token account" + }, + { + "code": 6051, + "name": "WrongRentReceiver", + "msg": "Wrong rent receiver" + }, + { + "code": 6052, + "name": "AlreadyPassActivationSlot", + "msg": "Already activated" + }, + { + "code": 6053, + "name": "LastSlotCannotBeSmallerThanActivateSlot", + "msg": "Last slot cannot be smaller than activate slot" + }, + { + "code": 6054, + "name": "ExceedMaxSwappedAmount", + "msg": "Swapped amount is exceeded max swapped amount" + }, + { + "code": 6055, "name": "InvalidStrategyParameters", "msg": "Invalid strategy parameters" }, { - "code": 6049, + "code": 6056, "name": "LiquidityLocked", "msg": "Liquidity locked" }, { - "code": 6050, + "code": 6057, "name": "InvalidLockReleaseSlot", "msg": "Invalid lock release slot" + }, + { + "code": 6058, + "name": "BinRangeIsNotEmpty", + "msg": "Bin range is not empty" + }, + { + "code": 6059, + "name": "InvalidSide", + "msg": "Invalid side" + }, + { + "code": 6060, + "name": "InvalidResizeLength", + "msg": "Invalid resize length" } - ], - "metadata": { - "address": "GrAkKfEpTKQuVHG2Y97Y2FF4i7y7Q5AHLK94JBy7Y5yv" - } + ] } \ No newline at end of file diff --git a/target/types/lb_clmm.ts b/target/types/lb_clmm.ts index 40fe971..176ad5f 100644 --- a/target/types/lb_clmm.ts +++ b/target/types/lb_clmm.ts @@ -15,12 +15,26 @@ export type LbClmm = { "value": "70" }, { - "name": "MAX_BIN_PER_POSITION", + "name": "DEFAULT_BIN_PER_POSITION", "type": { "defined": "usize" }, "value": "70" }, + { + "name": "MAX_RESIZE_LENGTH", + "type": { + "defined": "usize" + }, + "value": "70" + }, + { + "name": "POSITION_MAX_LENGTH", + "type": { + "defined": "usize" + }, + "value": "1400" + }, { "name": "MIN_BIN_ID", "type": "i32", @@ -398,16 +412,6 @@ export type LbClmm = { "isMut": false, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -492,16 +496,6 @@ export type LbClmm = { "isMut": false, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -586,16 +580,6 @@ export type LbClmm = { "isMut": false, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -665,16 +649,6 @@ export type LbClmm = { "isMut": false, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -739,16 +713,6 @@ export type LbClmm = { "isMut": false, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -828,16 +792,6 @@ export type LbClmm = { "isMut": false, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -1489,16 +1443,6 @@ export type LbClmm = { "isMut": true, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -1539,6 +1483,14 @@ export type LbClmm = { { "name": "rewardIndex", "type": "u64" + }, + { + "name": "minBinId", + "type": "i32" + }, + { + "name": "maxBinId", + "type": "i32" } ] }, @@ -1555,16 +1507,6 @@ export type LbClmm = { "isMut": true, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -1616,7 +1558,16 @@ export type LbClmm = { "isSigner": false } ], - "args": [] + "args": [ + { + "name": "minBinId", + "type": "i32" + }, + { + "name": "maxBinId", + "type": "i32" + } + ] }, { "name": "closePosition", @@ -1626,21 +1577,6 @@ export type LbClmm = { "isMut": true, "isSigner": false }, - { - "name": "lbPair", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -1733,6 +1669,101 @@ export type LbClmm = { } ] }, + { + "name": "increasePositionLength", + "accounts": [ + { + "name": "funder", + "isMut": true, + "isSigner": true + }, + { + "name": "lbPair", + "isMut": false, + "isSigner": false + }, + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "lengthToAdd", + "type": "u16" + }, + { + "name": "side", + "type": "u8" + } + ] + }, + { + "name": "decreasePositionLength", + "accounts": [ + { + "name": "rentReceiver", + "isMut": true, + "isSigner": false + }, + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "lengthToRemove", + "type": "u16" + }, + { + "name": "side", + "type": "u8" + } + ] + }, { "name": "initializePresetParameter", "accounts": [ @@ -1766,6 +1797,39 @@ export type LbClmm = { } ] }, + { + "name": "initializePresetParameterV2", + "accounts": [ + { + "name": "presetParameter", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "ix", + "type": { + "defined": "InitPresetParametersIx" + } + } + ] + }, { "name": "closePresetParameter", "accounts": [ @@ -1836,16 +1900,6 @@ export type LbClmm = { "isMut": false, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -1872,9 +1926,18 @@ export type LbClmm = { "isSigner": false } ], - "args": [] - }, - { + "args": [ + { + "name": "minBinId", + "type": "i32" + }, + { + "name": "maxBinId", + "type": "i32" + } + ] + }, + { "name": "togglePairStatus", "accounts": [ { @@ -1916,10 +1979,10 @@ export type LbClmm = { ] }, { - "name": "migratePosition", + "name": "migratePositionFromV1", "accounts": [ { - "name": "positionV2", + "name": "positionV3", "isMut": true, "isSigner": true }, @@ -1971,6 +2034,62 @@ export type LbClmm = { ], "args": [] }, + { + "name": "migratePositionFromV2", + "accounts": [ + { + "name": "positionV3", + "isMut": true, + "isSigner": true + }, + { + "name": "positionV2", + "isMut": true, + "isSigner": false + }, + { + "name": "lbPair", + "isMut": false, + "isSigner": false + }, + { + "name": "binArrayLower", + "isMut": true, + "isSigner": false + }, + { + "name": "binArrayUpper", + "isMut": true, + "isSigner": false + }, + { + "name": "owner", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rentReceiver", + "isMut": true, + "isSigner": false + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "migrateBinArray", "accounts": [ @@ -1995,23 +2114,22 @@ export type LbClmm = { "isMut": true, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "owner", "isMut": false, "isSigner": true } ], - "args": [] + "args": [ + { + "name": "minBinId", + "type": "i32" + }, + { + "name": "maxBinId", + "type": "i32" + } + ] }, { "name": "withdrawIneligibleReward", @@ -2150,109 +2268,435 @@ export type LbClmm = { "type": "u64" } ] - } - ], - "accounts": [ - { - "name": "binArrayBitmapExtension", - "type": { - "kind": "struct", - "fields": [ - { - "name": "lbPair", - "type": "publicKey" - }, - { - "name": "positiveBinArrayBitmap", - "docs": [ - "Packed initialized bin array state for start_bin_index is positive" - ], - "type": { - "array": [ - { - "array": [ - "u64", - 8 - ] - }, - 12 - ] - } - }, - { - "name": "negativeBinArrayBitmap", - "docs": [ - "Packed initialized bin array state for start_bin_index is negative" - ], - "type": { - "array": [ - { - "array": [ - "u64", - 8 - ] - }, - 12 - ] - } - } - ] - } }, { - "name": "binArray", - "docs": [ - "An account to contain a range of bin. For example: Bin 100 <-> 200.", - "For example:", - "BinArray index: 0 contains bin 0 <-> 599", - "index: 2 contains bin 600 <-> 1199, ..." + "name": "removeLiquidityByRange", + "accounts": [ + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "lbPair", + "isMut": true, + "isSigner": false + }, + { + "name": "binArrayBitmapExtension", + "isMut": true, + "isSigner": false, + "isOptional": true + }, + { + "name": "userTokenX", + "isMut": true, + "isSigner": false + }, + { + "name": "userTokenY", + "isMut": true, + "isSigner": false + }, + { + "name": "reserveX", + "isMut": true, + "isSigner": false + }, + { + "name": "reserveY", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenXMint", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenYMint", + "isMut": false, + "isSigner": false + }, + { + "name": "sender", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenXProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenYProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "index", - "type": "i64" - }, - { - "name": "version", - "docs": [ - "Version of binArray" - ], - "type": "u8" - }, - { - "name": "padding", - "type": { - "array": [ - "u8", - 7 - ] - } - }, - { - "name": "lbPair", - "type": "publicKey" - }, - { - "name": "bins", - "type": { - "array": [ - { - "defined": "Bin" - }, - 70 - ] - } - } - ] - } - }, - { - "name": "lbPair", - "type": { - "kind": "struct", - "fields": [ + "args": [ + { + "name": "fromBinId", + "type": "i32" + }, + { + "name": "toBinId", + "type": "i32" + }, + { + "name": "bpsToRemove", + "type": "u16" + } + ] + }, + { + "name": "addLiquidityOneSidePrecise", + "accounts": [ + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "lbPair", + "isMut": true, + "isSigner": false + }, + { + "name": "binArrayBitmapExtension", + "isMut": true, + "isSigner": false, + "isOptional": true + }, + { + "name": "userToken", + "isMut": true, + "isSigner": false + }, + { + "name": "reserve", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenMint", + "isMut": false, + "isSigner": false + }, + { + "name": "sender", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "parameter", + "type": { + "defined": "AddLiquiditySingleSidePreciseParameter" + } + } + ] + }, + { + "name": "goToABin", + "accounts": [ + { + "name": "lbPair", + "isMut": true, + "isSigner": false + }, + { + "name": "binArrayBitmapExtension", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "fromBinArray", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "toBinArray", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "binId", + "type": "i32" + } + ] + } + ], + "accounts": [ + { + "name": "binArrayBitmapExtension", + "type": { + "kind": "struct", + "fields": [ + { + "name": "lbPair", + "type": "publicKey" + }, + { + "name": "positiveBinArrayBitmap", + "docs": [ + "Packed initialized bin array state for start_bin_index is positive" + ], + "type": { + "array": [ + { + "array": [ + "u64", + 8 + ] + }, + 12 + ] + } + }, + { + "name": "negativeBinArrayBitmap", + "docs": [ + "Packed initialized bin array state for start_bin_index is negative" + ], + "type": { + "array": [ + { + "array": [ + "u64", + 8 + ] + }, + 12 + ] + } + } + ] + } + }, + { + "name": "binArray", + "docs": [ + "An account to contain a range of bin. For example: Bin 100 <-> 200.", + "For example:", + "BinArray index: 0 contains bin 0 <-> 599", + "index: 2 contains bin 600 <-> 1199, ..." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "index", + "type": "i64" + }, + { + "name": "version", + "docs": [ + "Version of binArray" + ], + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 7 + ] + } + }, + { + "name": "lbPair", + "type": "publicKey" + }, + { + "name": "bins", + "type": { + "array": [ + { + "defined": "Bin" + }, + 70 + ] + } + } + ] + } + }, + { + "name": "positionV3", + "type": { + "kind": "struct", + "fields": [ + { + "name": "lbPair", + "docs": [ + "The LB pair of this position" + ], + "type": "publicKey" + }, + { + "name": "owner", + "docs": [ + "Owner of the position. Client rely on this to to fetch their positions." + ], + "type": "publicKey" + }, + { + "name": "lowerBinId", + "docs": [ + "Lower bin ID" + ], + "type": "i32" + }, + { + "name": "upperBinId", + "docs": [ + "Upper bin ID" + ], + "type": "i32" + }, + { + "name": "lastUpdatedAt", + "docs": [ + "Last updated timestamp" + ], + "type": "i64" + }, + { + "name": "totalClaimedFeeXAmount", + "docs": [ + "Total claimed token fee X" + ], + "type": "u64" + }, + { + "name": "totalClaimedFeeYAmount", + "docs": [ + "Total claimed token fee Y" + ], + "type": "u64" + }, + { + "name": "totalClaimedRewards", + "docs": [ + "Total claimed rewards" + ], + "type": { + "array": [ + "u64", + 2 + ] + } + }, + { + "name": "operator", + "docs": [ + "Operator of position" + ], + "type": "publicKey" + }, + { + "name": "lockReleaseSlot", + "docs": [ + "Slot which the locked liquidity can be withdraw" + ], + "type": "u64" + }, + { + "name": "subjectedToBootstrapLiquidityLocking", + "docs": [ + "Is the position subjected to liquidity locking for the launch pool." + ], + "type": "u8" + }, + { + "name": "padding0", + "docs": [ + "Padding" + ], + "type": { + "array": [ + "u8", + 7 + ] + } + }, + { + "name": "feeOwner", + "docs": [ + "Address is able to claim fee in this position, only valid for bootstrap_liquidity_position" + ], + "type": "publicKey" + }, + { + "name": "length", + "docs": [ + "Number of bins" + ], + "type": "u64" + }, + { + "name": "reserved", + "docs": [ + "Reserved space for future use" + ], + "type": { + "array": [ + "u8", + 128 + ] + } + } + ] + } + }, + { + "name": "lbPair", + "type": { + "kind": "struct", + "fields": [ { "name": "parameters", "type": { @@ -3103,6 +3547,42 @@ export type LbClmm = { ] } }, + { + "name": "AddLiquiditySingleSidePreciseParameter", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bins", + "type": { + "vec": { + "defined": "CompressedBinDepositAmount" + } + } + }, + { + "name": "decompressMultiplier", + "type": "u64" + } + ] + } + }, + { + "name": "CompressedBinDepositAmount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "binId", + "type": "i32" + }, + { + "name": "amount", + "type": "u32" + } + ] + } + }, { "name": "BinLiquidityDistribution", "type": { @@ -3389,6 +3869,30 @@ export type LbClmm = { ] } }, + { + "name": "PositionBinData", + "type": { + "kind": "struct", + "fields": [ + { + "name": "liquidityShare", + "type": "u128" + }, + { + "name": "rewardInfo", + "type": { + "defined": "UserRewardInfo" + } + }, + { + "name": "feeInfo", + "type": { + "defined": "FeeInfo" + } + } + ] + } + }, { "name": "ProtocolFee", "type": { @@ -3767,6 +4271,23 @@ export type LbClmm = { ] } }, + { + "name": "ResizeSide", + "docs": [ + "Side of resize, 0 for lower and 1 for upper" + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "Lower" + }, + { + "name": "Upper" + } + ] + } + }, { "name": "PairType", "docs": [ @@ -4193,6 +4714,66 @@ export type LbClmm = { } ] }, + { + "name": "IncreasePositionLength", + "fields": [ + { + "name": "lbPair", + "type": "publicKey", + "index": false + }, + { + "name": "position", + "type": "publicKey", + "index": false + }, + { + "name": "owner", + "type": "publicKey", + "index": false + }, + { + "name": "lengthToAdd", + "type": "u16", + "index": false + }, + { + "name": "side", + "type": "u8", + "index": false + } + ] + }, + { + "name": "DecreasePositionLength", + "fields": [ + { + "name": "lbPair", + "type": "publicKey", + "index": false + }, + { + "name": "position", + "type": "publicKey", + "index": false + }, + { + "name": "owner", + "type": "publicKey", + "index": false + }, + { + "name": "lengthToRemove", + "type": "u16", + "index": false + }, + { + "name": "side", + "type": "u8", + "index": false + } + ] + }, { "name": "FeeParameterUpdate", "fields": [ @@ -4297,6 +4878,26 @@ export type LbClmm = { "index": false } ] + }, + { + "name": "GoToABin", + "fields": [ + { + "name": "lbPair", + "type": "publicKey", + "index": false + }, + { + "name": "fromBinId", + "type": "i32", + "index": false + }, + { + "name": "toBinId", + "type": "i32", + "index": false + } + ] } ], "errors": [ @@ -4536,24 +5137,74 @@ export type LbClmm = { "msg": "Reward not ended" }, { - "code": 6047, - "name": "MustWithdrawnIneligibleReward", - "msg": "Must withdraw ineligible reward" + "code": 6047, + "name": "MustWithdrawnIneligibleReward", + "msg": "Must withdraw ineligible reward" + }, + { + "code": 6048, + "name": "UnauthorizedAddress", + "msg": "Unauthorized address" + }, + { + "code": 6049, + "name": "OperatorsAreTheSame", + "msg": "Cannot update because operators are the same" + }, + { + "code": 6050, + "name": "WithdrawToWrongTokenAccount", + "msg": "Withdraw to wrong token account" + }, + { + "code": 6051, + "name": "WrongRentReceiver", + "msg": "Wrong rent receiver" + }, + { + "code": 6052, + "name": "AlreadyPassActivationSlot", + "msg": "Already activated" + }, + { + "code": 6053, + "name": "LastSlotCannotBeSmallerThanActivateSlot", + "msg": "Last slot cannot be smaller than activate slot" + }, + { + "code": 6054, + "name": "ExceedMaxSwappedAmount", + "msg": "Swapped amount is exceeded max swapped amount" }, { - "code": 6048, + "code": 6055, "name": "InvalidStrategyParameters", "msg": "Invalid strategy parameters" }, { - "code": 6049, + "code": 6056, "name": "LiquidityLocked", "msg": "Liquidity locked" }, { - "code": 6050, + "code": 6057, "name": "InvalidLockReleaseSlot", "msg": "Invalid lock release slot" + }, + { + "code": 6058, + "name": "BinRangeIsNotEmpty", + "msg": "Bin range is not empty" + }, + { + "code": 6059, + "name": "InvalidSide", + "msg": "Invalid side" + }, + { + "code": 6060, + "name": "InvalidResizeLength", + "msg": "Invalid resize length" } ] }; @@ -4575,12 +5226,26 @@ export const IDL: LbClmm = { "value": "70" }, { - "name": "MAX_BIN_PER_POSITION", + "name": "DEFAULT_BIN_PER_POSITION", + "type": { + "defined": "usize" + }, + "value": "70" + }, + { + "name": "MAX_RESIZE_LENGTH", "type": { "defined": "usize" }, "value": "70" }, + { + "name": "POSITION_MAX_LENGTH", + "type": { + "defined": "usize" + }, + "value": "1400" + }, { "name": "MIN_BIN_ID", "type": "i32", @@ -4958,16 +5623,6 @@ export const IDL: LbClmm = { "isMut": false, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -5052,16 +5707,6 @@ export const IDL: LbClmm = { "isMut": false, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -5146,16 +5791,6 @@ export const IDL: LbClmm = { "isMut": false, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -5225,16 +5860,6 @@ export const IDL: LbClmm = { "isMut": false, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -5299,16 +5924,6 @@ export const IDL: LbClmm = { "isMut": false, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -5388,16 +6003,6 @@ export const IDL: LbClmm = { "isMut": false, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -6049,16 +6654,6 @@ export const IDL: LbClmm = { "isMut": true, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -6099,6 +6694,14 @@ export const IDL: LbClmm = { { "name": "rewardIndex", "type": "u64" + }, + { + "name": "minBinId", + "type": "i32" + }, + { + "name": "maxBinId", + "type": "i32" } ] }, @@ -6115,16 +6718,6 @@ export const IDL: LbClmm = { "isMut": true, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -6176,7 +6769,16 @@ export const IDL: LbClmm = { "isSigner": false } ], - "args": [] + "args": [ + { + "name": "minBinId", + "type": "i32" + }, + { + "name": "maxBinId", + "type": "i32" + } + ] }, { "name": "closePosition", @@ -6186,31 +6788,126 @@ export const IDL: LbClmm = { "isMut": true, "isSigner": false }, + { + "name": "sender", + "isMut": false, + "isSigner": true + }, + { + "name": "rentReceiver", + "isMut": true, + "isSigner": false + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateFeeParameters", + "accounts": [ { "name": "lbPair", "isMut": true, "isSigner": false }, { - "name": "binArrayLower", + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "feeParameter", + "type": { + "defined": "FeeParameter" + } + } + ] + }, + { + "name": "increaseOracleLength", + "accounts": [ + { + "name": "oracle", "isMut": true, "isSigner": false }, { - "name": "binArrayUpper", + "name": "funder", "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, "isSigner": false }, { - "name": "sender", + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "lengthToAdd", + "type": "u64" + } + ] + }, + { + "name": "increasePositionLength", + "accounts": [ + { + "name": "funder", + "isMut": true, "isSigner": true }, { - "name": "rentReceiver", + "name": "lbPair", + "isMut": false, + "isSigner": false + }, + { + "name": "position", "isMut": true, "isSigner": false }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, { "name": "eventAuthority", "isMut": false, @@ -6222,21 +6919,40 @@ export const IDL: LbClmm = { "isSigner": false } ], - "args": [] + "args": [ + { + "name": "lengthToAdd", + "type": "u16" + }, + { + "name": "side", + "type": "u8" + } + ] }, { - "name": "updateFeeParameters", + "name": "decreasePositionLength", "accounts": [ { - "name": "lbPair", + "name": "rentReceiver", "isMut": true, "isSigner": false }, { - "name": "admin", + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "owner", "isMut": false, "isSigner": true }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, { "name": "eventAuthority", "isMut": false, @@ -6250,23 +6966,25 @@ export const IDL: LbClmm = { ], "args": [ { - "name": "feeParameter", - "type": { - "defined": "FeeParameter" - } + "name": "lengthToRemove", + "type": "u16" + }, + { + "name": "side", + "type": "u8" } ] }, { - "name": "increaseOracleLength", + "name": "initializePresetParameter", "accounts": [ { - "name": "oracle", + "name": "presetParameter", "isMut": true, "isSigner": false }, { - "name": "funder", + "name": "admin", "isMut": true, "isSigner": true }, @@ -6276,25 +6994,22 @@ export const IDL: LbClmm = { "isSigner": false }, { - "name": "eventAuthority", - "isMut": false, - "isSigner": false - }, - { - "name": "program", + "name": "rent", "isMut": false, "isSigner": false } ], "args": [ { - "name": "lengthToAdd", - "type": "u64" + "name": "ix", + "type": { + "defined": "InitPresetParametersIx" + } } ] }, { - "name": "initializePresetParameter", + "name": "initializePresetParameterV2", "accounts": [ { "name": "presetParameter", @@ -6396,16 +7111,6 @@ export const IDL: LbClmm = { "isMut": false, "isSigner": false }, - { - "name": "binArrayLower", - "isMut": true, - "isSigner": false - }, - { - "name": "binArrayUpper", - "isMut": true, - "isSigner": false - }, { "name": "sender", "isMut": false, @@ -6432,7 +7137,16 @@ export const IDL: LbClmm = { "isSigner": false } ], - "args": [] + "args": [ + { + "name": "minBinId", + "type": "i32" + }, + { + "name": "maxBinId", + "type": "i32" + } + ] }, { "name": "togglePairStatus", @@ -6476,10 +7190,10 @@ export const IDL: LbClmm = { ] }, { - "name": "migratePosition", + "name": "migratePositionFromV1", "accounts": [ { - "name": "positionV2", + "name": "positionV3", "isMut": true, "isSigner": true }, @@ -6531,6 +7245,62 @@ export const IDL: LbClmm = { ], "args": [] }, + { + "name": "migratePositionFromV2", + "accounts": [ + { + "name": "positionV3", + "isMut": true, + "isSigner": true + }, + { + "name": "positionV2", + "isMut": true, + "isSigner": false + }, + { + "name": "lbPair", + "isMut": false, + "isSigner": false + }, + { + "name": "binArrayLower", + "isMut": true, + "isSigner": false + }, + { + "name": "binArrayUpper", + "isMut": true, + "isSigner": false + }, + { + "name": "owner", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rentReceiver", + "isMut": true, + "isSigner": false + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "migrateBinArray", "accounts": [ @@ -6543,7 +7313,175 @@ export const IDL: LbClmm = { "args": [] }, { - "name": "updateFeesAndRewards", + "name": "updateFeesAndRewards", + "accounts": [ + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "lbPair", + "isMut": true, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "minBinId", + "type": "i32" + }, + { + "name": "maxBinId", + "type": "i32" + } + ] + }, + { + "name": "withdrawIneligibleReward", + "accounts": [ + { + "name": "lbPair", + "isMut": true, + "isSigner": false + }, + { + "name": "rewardVault", + "isMut": true, + "isSigner": false + }, + { + "name": "rewardMint", + "isMut": false, + "isSigner": false + }, + { + "name": "funderTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "funder", + "isMut": false, + "isSigner": true + }, + { + "name": "binArray", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "rewardIndex", + "type": "u64" + } + ] + }, + { + "name": "setActivationSlot", + "accounts": [ + { + "name": "lbPair", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + } + ], + "args": [ + { + "name": "activationSlot", + "type": "u64" + } + ] + }, + { + "name": "setMaxSwappedAmount", + "accounts": [ + { + "name": "lbPair", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + } + ], + "args": [ + { + "name": "swapCapDeactivateSlot", + "type": "u64" + }, + { + "name": "maxSwappedAmount", + "type": "u64" + } + ] + }, + { + "name": "setLockReleaseSlot", + "accounts": [ + { + "name": "position", + "isMut": true, + "isSigner": false + }, + { + "name": "lbPair", + "isMut": false, + "isSigner": false + }, + { + "name": "sender", + "isMut": false, + "isSigner": true + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "newLockReleaseSlot", + "type": "u64" + } + ] + }, + { + "name": "removeLiquidityByRange", "accounts": [ { "name": "position", @@ -6556,58 +7494,53 @@ export const IDL: LbClmm = { "isSigner": false }, { - "name": "binArrayLower", + "name": "binArrayBitmapExtension", "isMut": true, - "isSigner": false + "isSigner": false, + "isOptional": true }, { - "name": "binArrayUpper", + "name": "userTokenX", "isMut": true, "isSigner": false }, { - "name": "owner", - "isMut": false, - "isSigner": true - } - ], - "args": [] - }, - { - "name": "withdrawIneligibleReward", - "accounts": [ + "name": "userTokenY", + "isMut": true, + "isSigner": false + }, { - "name": "lbPair", + "name": "reserveX", "isMut": true, "isSigner": false }, { - "name": "rewardVault", + "name": "reserveY", "isMut": true, "isSigner": false }, { - "name": "rewardMint", + "name": "tokenXMint", "isMut": false, "isSigner": false }, { - "name": "funderTokenAccount", - "isMut": true, + "name": "tokenYMint", + "isMut": false, "isSigner": false }, { - "name": "funder", + "name": "sender", "isMut": false, "isSigner": true }, { - "name": "binArray", - "isMut": true, + "name": "tokenXProgram", + "isMut": false, "isSigner": false }, { - "name": "tokenProgram", + "name": "tokenYProgram", "isMut": false, "isSigner": false }, @@ -6624,74 +7557,108 @@ export const IDL: LbClmm = { ], "args": [ { - "name": "rewardIndex", - "type": "u64" + "name": "fromBinId", + "type": "i32" + }, + { + "name": "toBinId", + "type": "i32" + }, + { + "name": "bpsToRemove", + "type": "u16" } ] }, { - "name": "setActivationSlot", + "name": "addLiquidityOneSidePrecise", "accounts": [ { - "name": "lbPair", + "name": "position", "isMut": true, "isSigner": false }, { - "name": "admin", + "name": "lbPair", "isMut": true, - "isSigner": true - } - ], - "args": [ + "isSigner": false + }, { - "name": "activationSlot", - "type": "u64" - } - ] - }, - { - "name": "setMaxSwappedAmount", - "accounts": [ + "name": "binArrayBitmapExtension", + "isMut": true, + "isSigner": false, + "isOptional": true + }, { - "name": "lbPair", + "name": "userToken", "isMut": true, "isSigner": false }, { - "name": "admin", + "name": "reserve", "isMut": true, + "isSigner": false + }, + { + "name": "tokenMint", + "isMut": false, + "isSigner": false + }, + { + "name": "sender", + "isMut": false, "isSigner": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "eventAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false } ], "args": [ { - "name": "swapCapDeactivateSlot", - "type": "u64" - }, - { - "name": "maxSwappedAmount", - "type": "u64" + "name": "parameter", + "type": { + "defined": "AddLiquiditySingleSidePreciseParameter" + } } ] }, { - "name": "setLockReleaseSlot", + "name": "goToABin", "accounts": [ { - "name": "position", + "name": "lbPair", "isMut": true, "isSigner": false }, { - "name": "lbPair", + "name": "binArrayBitmapExtension", "isMut": false, - "isSigner": false + "isSigner": false, + "isOptional": true }, { - "name": "sender", + "name": "fromBinArray", "isMut": false, - "isSigner": true + "isSigner": false, + "isOptional": true + }, + { + "name": "toBinArray", + "isMut": false, + "isSigner": false, + "isOptional": true }, { "name": "eventAuthority", @@ -6706,8 +7673,8 @@ export const IDL: LbClmm = { ], "args": [ { - "name": "newLockReleaseSlot", - "type": "u64" + "name": "binId", + "type": "i32" } ] } @@ -6719,70 +7686,187 @@ export const IDL: LbClmm = { "kind": "struct", "fields": [ { - "name": "lbPair", - "type": "publicKey" + "name": "lbPair", + "type": "publicKey" + }, + { + "name": "positiveBinArrayBitmap", + "docs": [ + "Packed initialized bin array state for start_bin_index is positive" + ], + "type": { + "array": [ + { + "array": [ + "u64", + 8 + ] + }, + 12 + ] + } + }, + { + "name": "negativeBinArrayBitmap", + "docs": [ + "Packed initialized bin array state for start_bin_index is negative" + ], + "type": { + "array": [ + { + "array": [ + "u64", + 8 + ] + }, + 12 + ] + } + } + ] + } + }, + { + "name": "binArray", + "docs": [ + "An account to contain a range of bin. For example: Bin 100 <-> 200.", + "For example:", + "BinArray index: 0 contains bin 0 <-> 599", + "index: 2 contains bin 600 <-> 1199, ..." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "index", + "type": "i64" + }, + { + "name": "version", + "docs": [ + "Version of binArray" + ], + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 7 + ] + } + }, + { + "name": "lbPair", + "type": "publicKey" + }, + { + "name": "bins", + "type": { + "array": [ + { + "defined": "Bin" + }, + 70 + ] + } + } + ] + } + }, + { + "name": "positionV3", + "type": { + "kind": "struct", + "fields": [ + { + "name": "lbPair", + "docs": [ + "The LB pair of this position" + ], + "type": "publicKey" + }, + { + "name": "owner", + "docs": [ + "Owner of the position. Client rely on this to to fetch their positions." + ], + "type": "publicKey" + }, + { + "name": "lowerBinId", + "docs": [ + "Lower bin ID" + ], + "type": "i32" + }, + { + "name": "upperBinId", + "docs": [ + "Upper bin ID" + ], + "type": "i32" + }, + { + "name": "lastUpdatedAt", + "docs": [ + "Last updated timestamp" + ], + "type": "i64" + }, + { + "name": "totalClaimedFeeXAmount", + "docs": [ + "Total claimed token fee X" + ], + "type": "u64" }, { - "name": "positiveBinArrayBitmap", + "name": "totalClaimedFeeYAmount", "docs": [ - "Packed initialized bin array state for start_bin_index is positive" + "Total claimed token fee Y" ], - "type": { - "array": [ - { - "array": [ - "u64", - 8 - ] - }, - 12 - ] - } + "type": "u64" }, { - "name": "negativeBinArrayBitmap", + "name": "totalClaimedRewards", "docs": [ - "Packed initialized bin array state for start_bin_index is negative" + "Total claimed rewards" ], "type": { "array": [ - { - "array": [ - "u64", - 8 - ] - }, - 12 + "u64", + 2 ] } - } - ] - } - }, - { - "name": "binArray", - "docs": [ - "An account to contain a range of bin. For example: Bin 100 <-> 200.", - "For example:", - "BinArray index: 0 contains bin 0 <-> 599", - "index: 2 contains bin 600 <-> 1199, ..." - ], - "type": { - "kind": "struct", - "fields": [ + }, { - "name": "index", - "type": "i64" + "name": "operator", + "docs": [ + "Operator of position" + ], + "type": "publicKey" }, { - "name": "version", + "name": "lockReleaseSlot", "docs": [ - "Version of binArray" + "Slot which the locked liquidity can be withdraw" + ], + "type": "u64" + }, + { + "name": "subjectedToBootstrapLiquidityLocking", + "docs": [ + "Is the position subjected to liquidity locking for the launch pool." ], "type": "u8" }, { - "name": "padding", + "name": "padding0", + "docs": [ + "Padding" + ], "type": { "array": [ "u8", @@ -6791,17 +7875,28 @@ export const IDL: LbClmm = { } }, { - "name": "lbPair", + "name": "feeOwner", + "docs": [ + "Address is able to claim fee in this position, only valid for bootstrap_liquidity_position" + ], "type": "publicKey" }, { - "name": "bins", + "name": "length", + "docs": [ + "Number of bins" + ], + "type": "u64" + }, + { + "name": "reserved", + "docs": [ + "Reserved space for future use" + ], "type": { "array": [ - { - "defined": "Bin" - }, - 70 + "u8", + 128 ] } } @@ -7663,6 +8758,42 @@ export const IDL: LbClmm = { ] } }, + { + "name": "AddLiquiditySingleSidePreciseParameter", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bins", + "type": { + "vec": { + "defined": "CompressedBinDepositAmount" + } + } + }, + { + "name": "decompressMultiplier", + "type": "u64" + } + ] + } + }, + { + "name": "CompressedBinDepositAmount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "binId", + "type": "i32" + }, + { + "name": "amount", + "type": "u32" + } + ] + } + }, { "name": "BinLiquidityDistribution", "type": { @@ -7949,6 +9080,30 @@ export const IDL: LbClmm = { ] } }, + { + "name": "PositionBinData", + "type": { + "kind": "struct", + "fields": [ + { + "name": "liquidityShare", + "type": "u128" + }, + { + "name": "rewardInfo", + "type": { + "defined": "UserRewardInfo" + } + }, + { + "name": "feeInfo", + "type": { + "defined": "FeeInfo" + } + } + ] + } + }, { "name": "ProtocolFee", "type": { @@ -8327,6 +9482,23 @@ export const IDL: LbClmm = { ] } }, + { + "name": "ResizeSide", + "docs": [ + "Side of resize, 0 for lower and 1 for upper" + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "Lower" + }, + { + "name": "Upper" + } + ] + } + }, { "name": "PairType", "docs": [ @@ -8753,6 +9925,66 @@ export const IDL: LbClmm = { } ] }, + { + "name": "IncreasePositionLength", + "fields": [ + { + "name": "lbPair", + "type": "publicKey", + "index": false + }, + { + "name": "position", + "type": "publicKey", + "index": false + }, + { + "name": "owner", + "type": "publicKey", + "index": false + }, + { + "name": "lengthToAdd", + "type": "u16", + "index": false + }, + { + "name": "side", + "type": "u8", + "index": false + } + ] + }, + { + "name": "DecreasePositionLength", + "fields": [ + { + "name": "lbPair", + "type": "publicKey", + "index": false + }, + { + "name": "position", + "type": "publicKey", + "index": false + }, + { + "name": "owner", + "type": "publicKey", + "index": false + }, + { + "name": "lengthToRemove", + "type": "u16", + "index": false + }, + { + "name": "side", + "type": "u8", + "index": false + } + ] + }, { "name": "FeeParameterUpdate", "fields": [ @@ -8857,6 +10089,26 @@ export const IDL: LbClmm = { "index": false } ] + }, + { + "name": "GoToABin", + "fields": [ + { + "name": "lbPair", + "type": "publicKey", + "index": false + }, + { + "name": "fromBinId", + "type": "i32", + "index": false + }, + { + "name": "toBinId", + "type": "i32", + "index": false + } + ] } ], "errors": [ @@ -9102,18 +10354,68 @@ export const IDL: LbClmm = { }, { "code": 6048, + "name": "UnauthorizedAddress", + "msg": "Unauthorized address" + }, + { + "code": 6049, + "name": "OperatorsAreTheSame", + "msg": "Cannot update because operators are the same" + }, + { + "code": 6050, + "name": "WithdrawToWrongTokenAccount", + "msg": "Withdraw to wrong token account" + }, + { + "code": 6051, + "name": "WrongRentReceiver", + "msg": "Wrong rent receiver" + }, + { + "code": 6052, + "name": "AlreadyPassActivationSlot", + "msg": "Already activated" + }, + { + "code": 6053, + "name": "LastSlotCannotBeSmallerThanActivateSlot", + "msg": "Last slot cannot be smaller than activate slot" + }, + { + "code": 6054, + "name": "ExceedMaxSwappedAmount", + "msg": "Swapped amount is exceeded max swapped amount" + }, + { + "code": 6055, "name": "InvalidStrategyParameters", "msg": "Invalid strategy parameters" }, { - "code": 6049, + "code": 6056, "name": "LiquidityLocked", "msg": "Liquidity locked" }, { - "code": 6050, + "code": 6057, "name": "InvalidLockReleaseSlot", "msg": "Invalid lock release slot" + }, + { + "code": 6058, + "name": "BinRangeIsNotEmpty", + "msg": "Bin range is not empty" + }, + { + "code": 6059, + "name": "InvalidSide", + "msg": "Invalid side" + }, + { + "code": 6060, + "name": "InvalidResizeLength", + "msg": "Invalid resize length" } ] };