From 670566f8421ad3949fb398270e210b10176e9618 Mon Sep 17 00:00:00 2001 From: defi0x1 <34453681+defi0x1@users.noreply.github.com> Date: Fri, 1 Aug 2025 11:01:58 +0700 Subject: [PATCH 01/10] feat rate limiter (#54) * feat rate limiter * update test * update test * pool versioning (#57) * fix * revert pool v0 (#67) * add test (#58) * fix test disable cpi * test alpha vault backward compatible with rate limiter * remove unused funcs * pushed fixs base comments * fixed typo * fix comment * max fee rate limiter (#68) * max fee rate limiter * add assertion * add test * add test layout backward compatiable * update test * fix test after resolve conflicts * fix test after rebase * fix * fix * fix --------- Co-authored-by: andrewsource147 <31321699+andrewsource147@users.noreply.github.com> Co-authored-by: Andrew Nguyen --- programs/cp-amm/Cargo.toml | 2 +- .../cp-amm/src/base_fee/fee_rate_limiter.rs | 226 + programs/cp-amm/src/base_fee/fee_scheduler.rs | 111 + programs/cp-amm/src/base_fee/mod.rs | 69 + programs/cp-amm/src/constants.rs | 24 +- programs/cp-amm/src/error.rs | 12 + .../admin/ix_create_static_config.rs | 20 +- .../ix_initialize_customizable_pool.rs | 9 +- programs/cp-amm/src/instructions/ix_swap.rs | 91 +- programs/cp-amm/src/lib.rs | 1 + programs/cp-amm/src/params/fee_parameters.rs | 97 +- programs/cp-amm/src/state/config.rs | 26 +- programs/cp-amm/src/state/fee.rs | 126 +- programs/cp-amm/src/state/pool.rs | 38 +- .../cp-amm/src/tests/fee_scheduler_tests.rs | 17 +- .../src/tests/fixtures/config_account.bin | Bin 0 -> 328 bytes .../src/tests/fixtures/pool_account.bin | Bin 0 -> 1112 bytes programs/cp-amm/src/tests/layout_test.rs | 52 + programs/cp-amm/src/tests/mod.rs | 6 + .../cp-amm/src/tests/test_rate_limiter.rs | 278 ++ tests/addLiquidity.test.ts | 19 +- tests/alphaVaultWithSniperTax.test.ts | 280 ++ tests/bankrun-utils/alphaVault.ts | 328 ++ tests/bankrun-utils/common.ts | 225 +- tests/bankrun-utils/constants.ts | 13 +- tests/bankrun-utils/cpAmm.ts | 69 +- tests/bankrun-utils/idl/alpha_vault.json | 3875 ++++++++++++++++ tests/bankrun-utils/idl/alpha_vault.ts | 3881 +++++++++++++++++ tests/bankrun-utils/math.ts | 14 + tests/claimFee.test.ts | 18 +- tests/claimPositionFee.test.ts | 10 +- tests/createConfig.test.ts | 18 +- tests/createCustomizePool.test.ts | 34 +- tests/createPool.test.ts | 20 +- tests/createPosition.test.ts | 18 +- tests/dynamicConfig.test.ts | 16 +- tests/fixtures/alpha_vault.so | Bin 0 -> 784480 bytes tests/frozenRewardVault.test.ts | 9 +- tests/layoutCompatiable.test.ts | 57 + tests/lockPosition.test.ts | 17 +- tests/rateLimiter.test.ts | 289 ++ tests/removeLiquidity.test.ts | 23 +- tests/rewardByAdmin.test.ts | 17 +- tests/rewardByCreator.test.ts | 17 +- tests/splitPosition.test.ts | 14 +- tests/swap.test.ts | 18 +- 46 files changed, 10022 insertions(+), 482 deletions(-) create mode 100644 programs/cp-amm/src/base_fee/fee_rate_limiter.rs create mode 100644 programs/cp-amm/src/base_fee/fee_scheduler.rs create mode 100644 programs/cp-amm/src/base_fee/mod.rs create mode 100644 programs/cp-amm/src/tests/fixtures/config_account.bin create mode 100644 programs/cp-amm/src/tests/fixtures/pool_account.bin create mode 100644 programs/cp-amm/src/tests/layout_test.rs create mode 100644 programs/cp-amm/src/tests/test_rate_limiter.rs create mode 100644 tests/alphaVaultWithSniperTax.test.ts create mode 100644 tests/bankrun-utils/alphaVault.ts create mode 100644 tests/bankrun-utils/idl/alpha_vault.json create mode 100644 tests/bankrun-utils/idl/alpha_vault.ts create mode 100755 tests/fixtures/alpha_vault.so create mode 100644 tests/layoutCompatiable.test.ts create mode 100644 tests/rateLimiter.test.ts diff --git a/programs/cp-amm/Cargo.toml b/programs/cp-amm/Cargo.toml index afe1f31c..85ddd04d 100644 --- a/programs/cp-amm/Cargo.toml +++ b/programs/cp-amm/Cargo.toml @@ -31,4 +31,4 @@ spl-token-metadata-interface = { version = "=0.6.0" } const-crypto = "0.3.0" [dev-dependencies] -proptest = "1.2.0" +proptest = "1.2.0" \ No newline at end of file diff --git a/programs/cp-amm/src/base_fee/fee_rate_limiter.rs b/programs/cp-amm/src/base_fee/fee_rate_limiter.rs new file mode 100644 index 00000000..cfc87554 --- /dev/null +++ b/programs/cp-amm/src/base_fee/fee_rate_limiter.rs @@ -0,0 +1,226 @@ +use crate::{ + activation_handler::ActivationType, + constants::{ + fee::{FEE_DENOMINATOR, MAX_FEE_BPS_V1, MAX_FEE_NUMERATOR_V1, MIN_FEE_NUMERATOR}, + MAX_RATE_LIMITER_DURATION_IN_SECONDS, MAX_RATE_LIMITER_DURATION_IN_SLOTS, + }, + params::{fee_parameters::to_numerator, swap::TradeDirection}, + safe_math::SafeMath, + state::CollectFeeMode, + u128x128_math::Rounding, + utils_math::safe_mul_div_cast_u64, + PoolError, +}; + +use super::BaseFeeHandler; +use anchor_lang::prelude::*; +use num::Integer; +use ruint::aliases::U256; + +/// we denote reference_amount = x0, cliff_fee_numerator = c, fee_increment = i +/// if input_amount <= x0, then fee = input_amount * c +/// +/// if input_amount > x0, then input_amount = x0 + (a * x0 + b) +/// if a < max_index +/// then fee = x0 * c + x0 * (c + i) + .... + x0 * (c + i*a) + b * (c + i * (a+1)) +/// then fee = x0 * (c + c*a + i*a*(a+1)/2) + b * (c + i * (a+1)) +/// +/// if a >= max_index +/// if a = max_index + d, input_amount = x0 + max_index * x0 + (d * x0 + b) +/// then fee = x0 * (c + c*max_index + i*max_index*(max_index+1)/2) + (d * x0 + b) * MAX_FEE +#[derive(Debug, Default)] +pub struct FeeRateLimiter { + pub cliff_fee_numerator: u64, + pub fee_increment_bps: u16, + pub max_limiter_duration: u32, + pub max_fee_bps: u32, + pub reference_amount: u64, +} + +impl FeeRateLimiter { + pub fn is_rate_limiter_applied( + &self, + current_point: u64, + activation_point: u64, + trade_direction: TradeDirection, + ) -> Result { + if self.is_zero_rate_limiter() { + return Ok(false); + } + + // only handle for the case B to A and collect fee mode in token B + if trade_direction == TradeDirection::AtoB { + return Ok(false); + } + + // it means whitelisted vault is buying + if current_point < activation_point { + return Ok(false); + } + + let last_effective_rate_limiter_point = + u128::from(activation_point).safe_add(self.max_limiter_duration.into())?; + if u128::from(current_point) > last_effective_rate_limiter_point { + return Ok(false); + } + Ok(true) + } + + fn is_zero_rate_limiter(&self) -> bool { + self.reference_amount == 0 + && self.max_limiter_duration == 0 + && self.max_fee_bps == 0 + && self.fee_increment_bps == 0 + } + + fn is_non_zero_rate_limiter(&self) -> bool { + self.reference_amount != 0 + && self.max_limiter_duration != 0 + && self.max_fee_bps != 0 + && self.fee_increment_bps != 0 + } + + pub fn get_max_index(&self) -> Result { + let max_fee_numerator = to_numerator(self.max_fee_bps.into(), FEE_DENOMINATOR.into())?; + let delta_numerator = max_fee_numerator.safe_sub(self.cliff_fee_numerator)?; + let fee_increment_numerator = + to_numerator(self.fee_increment_bps.into(), FEE_DENOMINATOR.into())?; + let max_index = delta_numerator.safe_div(fee_increment_numerator)?; + Ok(max_index) + } + + // export function for testing + pub fn get_fee_numerator_from_amount(&self, input_amount: u64) -> Result { + let fee_numerator = if input_amount <= self.reference_amount { + self.cliff_fee_numerator + } else { + let max_fee_numerator = to_numerator(self.max_fee_bps.into(), FEE_DENOMINATOR.into())?; + + let c = U256::from(self.cliff_fee_numerator); + let (a, b) = input_amount + .safe_sub(self.reference_amount)? + .div_rem(&self.reference_amount); + let a = U256::from(a); + let b = U256::from(b); + let max_index = U256::from(self.get_max_index()?); + let i = U256::from(to_numerator( + self.fee_increment_bps.into(), + FEE_DENOMINATOR.into(), + )?); + let x0 = U256::from(self.reference_amount); + let one = U256::ONE; + let two = U256::from(2); + // because we all calculate in U256, so it is safe to avoid safe math + let trading_fee_numerator = if a < max_index { + let numerator_1 = c + c * a + i * a * (a + one) / two; + let numerator_2 = c + i * (a + one); + let first_fee = x0 * numerator_1; + let second_fee = b * numerator_2; + first_fee + second_fee + } else { + let numerator_1 = c + c * max_index + i * max_index * (max_index + one) / two; + let numerator_2 = U256::from(max_fee_numerator); + let first_fee = x0 * numerator_1; + + let d = a - max_index; + let left_amount = d * x0 + b; + let second_fee = left_amount * numerator_2; + first_fee + second_fee + }; + + let denominator = U256::from(FEE_DENOMINATOR); + let trading_fee = (trading_fee_numerator + denominator - one) / denominator; + let trading_fee = trading_fee + .try_into() + .map_err(|_| PoolError::TypeCastFailed)?; + + // reverse to fee numerator + // input_amount * numerator / FEE_DENOMINATOR = trading_fee + // then numerator = trading_fee * FEE_DENOMINATOR / input_amount + let fee_numerator = + safe_mul_div_cast_u64(trading_fee, FEE_DENOMINATOR, input_amount, Rounding::Up)?; + fee_numerator + }; + + Ok(fee_numerator) + } +} + +impl BaseFeeHandler for FeeRateLimiter { + fn validate( + &self, + collect_fee_mode: CollectFeeMode, + activation_type: ActivationType, + ) -> Result<()> { + // can only be applied in OnlyB collect fee mode + require!( + collect_fee_mode == CollectFeeMode::OnlyB, + PoolError::InvalidFeeRateLimiter + ); + + if self.is_zero_rate_limiter() { + return Ok(()); + } + + require!( + self.is_non_zero_rate_limiter(), + PoolError::InvalidFeeRateLimiter + ); + + let max_limiter_duration = match activation_type { + ActivationType::Slot => MAX_RATE_LIMITER_DURATION_IN_SLOTS, + ActivationType::Timestamp => MAX_RATE_LIMITER_DURATION_IN_SECONDS, + }; + + require!( + self.max_limiter_duration <= max_limiter_duration, + PoolError::InvalidFeeRateLimiter + ); + + let fee_increment_numerator = + to_numerator(self.fee_increment_bps.into(), FEE_DENOMINATOR.into())?; + require!( + fee_increment_numerator < FEE_DENOMINATOR, + PoolError::InvalidFeeRateLimiter + ); + + let max_fee_bps_u64 = + u64::try_from(self.max_fee_bps).map_err(|_| PoolError::TypeCastFailed)?; + require!( + max_fee_bps_u64 <= MAX_FEE_BPS_V1, + PoolError::InvalidFeeRateLimiter + ); + + let max_fee_numerator_from_bps = + to_numerator(self.max_fee_bps.into(), FEE_DENOMINATOR.into())?; + // this condition is redundant, but is is safe to add this + require!( + self.cliff_fee_numerator >= MIN_FEE_NUMERATOR + && self.cliff_fee_numerator <= max_fee_numerator_from_bps, + PoolError::InvalidFeeRateLimiter + ); + + // validate max fee (more amount, then more fee) + let min_fee_numerator = self.get_fee_numerator_from_amount(0)?; + let max_fee_numerator = self.get_fee_numerator_from_amount(u64::MAX)?; + require!( + min_fee_numerator >= MIN_FEE_NUMERATOR && max_fee_numerator <= MAX_FEE_NUMERATOR_V1, + PoolError::InvalidFeeRateLimiter + ); + + Ok(()) + } + fn get_base_fee_numerator( + &self, + current_point: u64, + activation_point: u64, + trade_direction: TradeDirection, + input_amount: u64, + ) -> Result { + if self.is_rate_limiter_applied(current_point, activation_point, trade_direction)? { + self.get_fee_numerator_from_amount(input_amount) + } else { + Ok(self.cliff_fee_numerator) + } + } +} diff --git a/programs/cp-amm/src/base_fee/fee_scheduler.rs b/programs/cp-amm/src/base_fee/fee_scheduler.rs new file mode 100644 index 00000000..607e4892 --- /dev/null +++ b/programs/cp-amm/src/base_fee/fee_scheduler.rs @@ -0,0 +1,111 @@ +use crate::{ + activation_handler::ActivationType, + constants::fee::{FEE_DENOMINATOR, MAX_FEE_NUMERATOR_V1, MIN_FEE_NUMERATOR}, + fee_math::get_fee_in_period, + math::safe_math::SafeMath, + params::{fee_parameters::validate_fee_fraction, swap::TradeDirection}, + state::CollectFeeMode, + PoolError, +}; +use anchor_lang::prelude::*; +use num_enum::{IntoPrimitive, TryFromPrimitive}; + +use super::BaseFeeHandler; + +// https://www.desmos.com/calculator/oxdndn2xdx +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)] +pub enum FeeSchedulerMode { + // fee = cliff_fee_numerator - passed_period * reduction_factor + Linear, + // fee = cliff_fee_numerator * (1-reduction_factor/10_000)^passed_period + Exponential, +} + +#[derive(Debug, Default)] +pub struct FeeScheduler { + pub cliff_fee_numerator: u64, + pub number_of_period: u16, + pub period_frequency: u64, + pub reduction_factor: u64, + pub fee_scheduler_mode: u8, +} + +impl FeeScheduler { + pub fn get_max_base_fee_numerator(&self) -> u64 { + self.cliff_fee_numerator + } + + pub fn get_min_base_fee_numerator(&self) -> Result { + self.get_base_fee_numerator_by_period(self.number_of_period.into()) + } + + fn get_base_fee_numerator_by_period(&self, period: u64) -> Result { + let period = period.min(self.number_of_period.into()); + + let base_fee_mode = FeeSchedulerMode::try_from(self.fee_scheduler_mode) + .map_err(|_| PoolError::TypeCastFailed)?; + + match base_fee_mode { + FeeSchedulerMode::Linear => { + let fee_numerator = self + .cliff_fee_numerator + .safe_sub(self.reduction_factor.safe_mul(period)?)?; + Ok(fee_numerator) + } + FeeSchedulerMode::Exponential => { + let period = u16::try_from(period).map_err(|_| PoolError::MathOverflow)?; + let fee_numerator = + get_fee_in_period(self.cliff_fee_numerator, self.reduction_factor, period)?; + Ok(fee_numerator) + } + } + } +} + +impl BaseFeeHandler for FeeScheduler { + fn validate( + &self, + _collect_fee_mode: CollectFeeMode, + _activation_type: ActivationType, + ) -> Result<()> { + if self.period_frequency != 0 || self.number_of_period != 0 || self.reduction_factor != 0 { + require!( + self.number_of_period != 0 + && self.period_frequency != 0 + && self.reduction_factor != 0, + PoolError::InvalidFeeScheduler + ); + } + let min_fee_numerator = self.get_min_base_fee_numerator()?; + let max_fee_numerator = self.get_max_base_fee_numerator(); + validate_fee_fraction(min_fee_numerator, FEE_DENOMINATOR)?; + validate_fee_fraction(max_fee_numerator, FEE_DENOMINATOR)?; + require!( + min_fee_numerator >= MIN_FEE_NUMERATOR && max_fee_numerator <= MAX_FEE_NUMERATOR_V1, + PoolError::ExceedMaxFeeBps + ); + Ok(()) + } + fn get_base_fee_numerator( + &self, + current_point: u64, + activation_point: u64, + _trade_direction: TradeDirection, + _input_amount: u64, + ) -> Result { + if self.period_frequency == 0 { + return Ok(self.cliff_fee_numerator); + } + // it means alpha-vault is buying + let period = if current_point < activation_point { + self.number_of_period.into() + } else { + let period = current_point + .safe_sub(activation_point)? + .safe_div(self.period_frequency)?; + period.min(self.number_of_period.into()) + }; + self.get_base_fee_numerator_by_period(period) + } +} diff --git a/programs/cp-amm/src/base_fee/mod.rs b/programs/cp-amm/src/base_fee/mod.rs new file mode 100644 index 00000000..9a504b32 --- /dev/null +++ b/programs/cp-amm/src/base_fee/mod.rs @@ -0,0 +1,69 @@ +pub mod fee_scheduler; +pub use fee_scheduler::*; +pub mod fee_rate_limiter; +pub use fee_rate_limiter::*; + +use anchor_lang::prelude::*; + +use crate::{ + activation_handler::ActivationType, + params::swap::TradeDirection, + state::{fee::BaseFeeMode, CollectFeeMode}, + PoolError, +}; + +pub trait BaseFeeHandler { + fn validate( + &self, + collect_fee_mode: CollectFeeMode, + activation_type: ActivationType, + ) -> Result<()>; + fn get_base_fee_numerator( + &self, + current_point: u64, + activation_point: u64, + trade_direction: TradeDirection, + input_amount: u64, + ) -> Result; +} + +pub fn get_base_fee_handler( + cliff_fee_numerator: u64, + first_factor: u16, + second_factor: [u8; 8], + third_factor: u64, + base_fee_mode: u8, +) -> Result> { + let base_fee_mode = + BaseFeeMode::try_from(base_fee_mode).map_err(|_| PoolError::InvalidBaseFeeMode)?; + match base_fee_mode { + BaseFeeMode::FeeSchedulerLinear | BaseFeeMode::FeeSchedulerExponential => { + let fee_scheduler = FeeScheduler { + cliff_fee_numerator, + number_of_period: first_factor, + period_frequency: u64::from_le_bytes(second_factor), + reduction_factor: third_factor, + fee_scheduler_mode: base_fee_mode.into(), + }; + Ok(Box::new(fee_scheduler)) + } + BaseFeeMode::RateLimiter => { + let fee_rate_limiter = FeeRateLimiter { + cliff_fee_numerator, + fee_increment_bps: first_factor, + max_limiter_duration: u32::from_le_bytes( + second_factor[0..4] + .try_into() + .map_err(|_| PoolError::TypeCastFailed)?, + ), + max_fee_bps: u32::from_le_bytes( + second_factor[4..8] + .try_into() + .map_err(|_| PoolError::TypeCastFailed)?, + ), + reference_amount: third_factor, + }; + Ok(Box::new(fee_rate_limiter)) + } + } +} diff --git a/programs/cp-amm/src/constants.rs b/programs/cp-amm/src/constants.rs index ef389552..4226379a 100644 --- a/programs/cp-amm/src/constants.rs +++ b/programs/cp-amm/src/constants.rs @@ -37,6 +37,14 @@ pub const MIN_REWARD_DURATION: u64 = 24 * 60 * 60; // 1 day pub const MAX_REWARD_DURATION: u64 = 31536000; // 1 year = 365 * 24 * 3600 +pub const MAX_RATE_LIMITER_DURATION_IN_SECONDS: u32 = 60 * 60 * 12; // 12 hours +pub const MAX_RATE_LIMITER_DURATION_IN_SLOTS: u32 = 108000; // 12 hours + +static_assertions::const_assert_eq!( + MAX_RATE_LIMITER_DURATION_IN_SECONDS * 1000 / 400, + MAX_RATE_LIMITER_DURATION_IN_SLOTS +); + pub mod activation { #[cfg(not(feature = "local"))] pub const SLOT_BUFFER: u64 = 9000; // 1 slot = 400 mls => 1 hour @@ -79,8 +87,11 @@ pub mod fee { pub const FEE_DENOMINATOR: u64 = 1_000_000_000; /// Max fee BPS - pub const MAX_FEE_BPS: u64 = 5000; // 50% - pub const MAX_FEE_NUMERATOR: u64 = 500_000_000; // 50% + pub const MAX_FEE_BPS_V0: u64 = 5000; // 50% + pub const MAX_FEE_NUMERATOR_V0: u64 = 500_000_000; // 50% + + pub const MAX_FEE_BPS_V1: u64 = 9900; // 99% + pub const MAX_FEE_NUMERATOR_V1: u64 = 990_000_000; // 99% /// Max basis point. 100% in pct pub const MAX_BASIS_POINT: u64 = 10000; @@ -89,8 +100,13 @@ pub mod fee { pub const MIN_FEE_NUMERATOR: u64 = 100_000; static_assertions::const_assert_eq!( - MAX_FEE_BPS * FEE_DENOMINATOR / MAX_BASIS_POINT, - MAX_FEE_NUMERATOR + MAX_FEE_BPS_V0 * FEE_DENOMINATOR / MAX_BASIS_POINT, + MAX_FEE_NUMERATOR_V0 + ); + + static_assertions::const_assert_eq!( + MAX_FEE_BPS_V1 * FEE_DENOMINATOR / MAX_BASIS_POINT, + MAX_FEE_NUMERATOR_V1 ); static_assertions::const_assert_eq!( diff --git a/programs/cp-amm/src/error.rs b/programs/cp-amm/src/error.rs index b6144e6e..d9d3852f 100644 --- a/programs/cp-amm/src/error.rs +++ b/programs/cp-amm/src/error.rs @@ -145,4 +145,16 @@ pub enum PoolError { #[msg("Same position")] SamePosition, + + #[msg("Invalid base fee mode")] + InvalidBaseFeeMode, + + #[msg("Invalid fee rate limiter")] + InvalidFeeRateLimiter, + + #[msg("Fail to validate single swap instruction in rate limiter")] + FailToValidateSingleSwapInstruction, + + #[msg("Invalid fee scheduler")] + InvalidFeeScheduler, } diff --git a/programs/cp-amm/src/instructions/admin/ix_create_static_config.rs b/programs/cp-amm/src/instructions/admin/ix_create_static_config.rs index 6bdfe6bd..a22383c0 100644 --- a/programs/cp-amm/src/instructions/admin/ix_create_static_config.rs +++ b/programs/cp-amm/src/instructions/admin/ix_create_static_config.rs @@ -1,7 +1,7 @@ use anchor_lang::prelude::*; use crate::{ - activation_handler::ActivationHandler, + activation_handler::{ActivationHandler, ActivationType}, assert_eq_admin, constants::{seeds::CONFIG_PREFIX, MAX_SQRT_PRICE, MIN_SQRT_PRICE}, event, @@ -65,15 +65,6 @@ pub fn handle_create_static_config( PoolError::InvalidPriceRange ); - // validate collect fee mode - require!( - CollectFeeMode::try_from(collect_fee_mode).is_ok(), - PoolError::InvalidCollectFeeMode - ); - - // validate fee - pool_fees.validate()?; - let has_alpha_vault = vault_config_key.ne(&Pubkey::default()); let activation_point = Some(ActivationHandler::get_max_activation_point( @@ -87,6 +78,13 @@ pub fn handle_create_static_config( }; activation_params.validate()?; + let pool_activation_type = + ActivationType::try_from(activation_type).map_err(|_| PoolError::InvalidActivationType)?; + + let pool_collect_fee_mode = + CollectFeeMode::try_from(collect_fee_mode).map_err(|_| PoolError::InvalidCollectFeeMode)?; + pool_fees.validate(pool_collect_fee_mode, pool_activation_type)?; + let mut config = ctx.accounts.config.load_init()?; config.init_static_config( index, @@ -96,7 +94,7 @@ pub fn handle_create_static_config( activation_type, sqrt_min_price, sqrt_max_price, - collect_fee_mode.into(), + collect_fee_mode, ); emit_cpi!(event::EvtCreateConfig { diff --git a/programs/cp-amm/src/instructions/initialize_pool/ix_initialize_customizable_pool.rs b/programs/cp-amm/src/instructions/initialize_pool/ix_initialize_customizable_pool.rs index a767c14d..827f44d5 100644 --- a/programs/cp-amm/src/instructions/initialize_pool/ix_initialize_customizable_pool.rs +++ b/programs/cp-amm/src/instructions/initialize_pool/ix_initialize_customizable_pool.rs @@ -5,7 +5,7 @@ use anchor_spl::{ }; use crate::{ - activation_handler::ActivationHandler, + activation_handler::{ActivationHandler, ActivationType}, alpha_vault::alpha_vault, const_pda, constants::{ @@ -68,11 +68,12 @@ impl InitializeCustomizablePoolParameters { require!(self.liquidity > 0, PoolError::InvalidMinimumLiquidity); + let activation_type = ActivationType::try_from(self.activation_type) + .map_err(|_| PoolError::InvalidActivationType)?; // validate fee - self.pool_fees.validate()?; - - CollectFeeMode::try_from(self.collect_fee_mode) + let collect_fee_mode = CollectFeeMode::try_from(self.collect_fee_mode) .map_err(|_| PoolError::InvalidCollectFeeMode)?; + self.pool_fees.validate(collect_fee_mode, activation_type)?; // validate activation let activation_params = ActivationParams { diff --git a/programs/cp-amm/src/instructions/ix_swap.rs b/programs/cp-amm/src/instructions/ix_swap.rs index cbe0f805..7d26893b 100644 --- a/programs/cp-amm/src/instructions/ix_swap.rs +++ b/programs/cp-amm/src/instructions/ix_swap.rs @@ -1,14 +1,19 @@ -use anchor_lang::prelude::*; -use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; - use crate::{ activation_handler::ActivationHandler, const_pda, get_pool_access_validator, + instruction::Swap as SwapInstruction, params::swap::TradeDirection, + safe_math::SafeMath, state::{fee::FeeMode, Pool}, token::{calculate_transfer_fee_excluded_amount, transfer_from_pool, transfer_from_user}, EvtSwap, PoolError, }; +use anchor_lang::solana_program::sysvar; +use anchor_lang::{ + prelude::*, + solana_program::instruction::{get_processed_sibling_instruction, get_stack_height}, +}; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; #[derive(AnchorSerialize, AnchorDeserialize)] pub struct SwapParameters { @@ -126,12 +131,24 @@ pub fn handle_swap(ctx: Context, params: SwapParameters) -> Result<()> let has_referral = ctx.accounts.referral_token_account.is_some(); let mut pool = ctx.accounts.pool.load_mut()?; + let current_point = ActivationHandler::get_current_point(pool.activation_type)?; + + // another validation to prevent snipers to craft multiple swap instructions in 1 tx + // (if we dont do this, they are able to concat 16 swap instructions in 1 tx) + if let Ok(rate_limiter) = pool.pool_fees.base_fee.get_fee_rate_limiter() { + if rate_limiter.is_rate_limiter_applied( + current_point, + pool.activation_point, + trade_direction, + )? { + validate_single_swap_instruction(&ctx.accounts.pool.key(), ctx.remaining_accounts)?; + } + } // update for dynamic fee reference let current_timestamp = Clock::get()?.unix_timestamp as u64; pool.update_pre_swap(current_timestamp)?; - let current_point = ActivationHandler::get_current_point(pool.activation_type)?; let fee_mode = &FeeMode::get_fee_mode(pool.collect_fee_mode, trade_direction, has_referral)?; let swap_result = pool.get_swap_result( @@ -203,3 +220,69 @@ pub fn handle_swap(ctx: Context, params: SwapParameters) -> Result<()> Ok(()) } + +pub fn validate_single_swap_instruction<'c, 'info>( + pool: &Pubkey, + remaining_accounts: &'c [AccountInfo<'info>], +) -> Result<()> { + let instruction_sysvar_account_info = remaining_accounts + .get(0) + .ok_or_else(|| PoolError::FailToValidateSingleSwapInstruction)?; + + // get current index of instruction + let current_index = + sysvar::instructions::load_current_index_checked(instruction_sysvar_account_info)?; + let current_instruction = sysvar::instructions::load_instruction_at_checked( + current_index.into(), + instruction_sysvar_account_info, + )?; + + if current_instruction.program_id != crate::ID { + // check if current instruction is CPI + // disable any stack height greater than 2 + if get_stack_height() > 2 { + return Err(PoolError::FailToValidateSingleSwapInstruction.into()); + } + // check for any sibling instruction + let mut sibling_index = 0; + while let Some(sibling_instruction) = get_processed_sibling_instruction(sibling_index) { + if sibling_instruction.program_id == crate::ID + && sibling_instruction.data[..8].eq(SwapInstruction::DISCRIMINATOR) + { + if sibling_instruction.accounts[1].pubkey.eq(pool) { + return Err(PoolError::FailToValidateSingleSwapInstruction.into()); + } + } + sibling_index = sibling_index.safe_add(1)?; + } + } + + if current_index == 0 { + // skip for first instruction + return Ok(()); + } + for i in 0..current_index { + let instruction = sysvar::instructions::load_instruction_at_checked( + i.into(), + instruction_sysvar_account_info, + )?; + + if instruction.program_id != crate::ID { + // we treat any instruction including that pool address is other swap ix + for i in 0..instruction.accounts.len() { + if instruction.accounts[i].pubkey.eq(pool) { + msg!("Multiple swaps not allowed"); + return Err(PoolError::FailToValidateSingleSwapInstruction.into()); + } + } + } else if instruction.data[..8].eq(SwapInstruction::DISCRIMINATOR) { + if instruction.accounts[1].pubkey.eq(pool) { + // otherwise, we just need to search swap instruction discriminator, so creator can still bundle initialzing pool and swap at 1 tx + msg!("Multiple swaps not allowed"); + return Err(PoolError::FailToValidateSingleSwapInstruction.into()); + } + } + } + + Ok(()) +} diff --git a/programs/cp-amm/src/lib.rs b/programs/cp-amm/src/lib.rs index 46a56094..d5e5bbaf 100644 --- a/programs/cp-amm/src/lib.rs +++ b/programs/cp-amm/src/lib.rs @@ -16,6 +16,7 @@ pub mod event; pub use event::*; pub mod utils; pub use utils::*; +pub mod base_fee; pub mod math; pub use math::*; pub mod curve; diff --git a/programs/cp-amm/src/params/fee_parameters.rs b/programs/cp-amm/src/params/fee_parameters.rs index b9442491..c15d8cf0 100644 --- a/programs/cp-amm/src/params/fee_parameters.rs +++ b/programs/cp-amm/src/params/fee_parameters.rs @@ -1,14 +1,14 @@ //! Fees module includes information about fee charges +use crate::activation_handler::ActivationType; +use crate::base_fee::get_base_fee_handler; use crate::constants::fee::{ - FEE_DENOMINATOR, HOST_FEE_PERCENT, MAX_BASIS_POINT, MAX_FEE_NUMERATOR, MIN_FEE_NUMERATOR, - PARTNER_FEE_PERCENT, PROTOCOL_FEE_PERCENT, + HOST_FEE_PERCENT, MAX_BASIS_POINT, PARTNER_FEE_PERCENT, PROTOCOL_FEE_PERCENT, }; use crate::constants::{BASIS_POINT_MAX, BIN_STEP_BPS_DEFAULT, BIN_STEP_BPS_U128_DEFAULT, U24_MAX}; use crate::error::PoolError; -use crate::fee_math::get_fee_in_period; use crate::safe_math::SafeMath; -use crate::state::fee::{BaseFeeStruct, DynamicFeeStruct, FeeSchedulerMode, PoolFeesStruct}; -use crate::state::{BaseFeeConfig, DynamicFeeConfig, PoolFeesConfig}; +use crate::state::fee::{BaseFeeStruct, DynamicFeeStruct, PoolFeesStruct}; +use crate::state::{BaseFeeConfig, CollectFeeMode, DynamicFeeConfig, PoolFeesConfig}; use anchor_lang::prelude::*; /// Information regarding fee charges @@ -25,56 +25,37 @@ pub struct PoolFeeParameters { #[derive(Copy, Clone, Debug, AnchorSerialize, AnchorDeserialize, InitSpace, Default)] pub struct BaseFeeParameters { pub cliff_fee_numerator: u64, - pub number_of_period: u16, - pub period_frequency: u64, - pub reduction_factor: u64, - pub fee_scheduler_mode: u8, + pub first_factor: u16, + pub second_factor: [u8; 8], + pub third_factor: u64, + pub base_fee_mode: u8, } impl BaseFeeParameters { - pub fn get_max_base_fee_numerator(&self) -> u64 { - self.cliff_fee_numerator - } - pub fn get_min_base_fee_numerator(&self) -> Result { - let fee_scheduler_mode = FeeSchedulerMode::try_from(self.fee_scheduler_mode) - .map_err(|_| PoolError::TypeCastFailed)?; - match fee_scheduler_mode { - FeeSchedulerMode::Linear => { - let fee_numerator = self.cliff_fee_numerator.safe_sub( - self.reduction_factor - .safe_mul(self.number_of_period.into())?, - )?; - Ok(fee_numerator) - } - FeeSchedulerMode::Exponential => { - let fee_numerator = get_fee_in_period( - self.cliff_fee_numerator, - self.reduction_factor, - self.number_of_period, - )?; - Ok(fee_numerator) - } - } - } + fn validate( + &self, + collect_fee_mode: CollectFeeMode, + activation_type: ActivationType, + ) -> Result<()> { + let base_fee_handler = get_base_fee_handler( + self.cliff_fee_numerator, + self.first_factor, + self.second_factor, + self.third_factor, + self.base_fee_mode, + )?; + base_fee_handler.validate(collect_fee_mode, activation_type)?; - fn validate(&self) -> Result<()> { - let min_fee_numerator = self.get_min_base_fee_numerator()?; - let max_fee_numerator = self.get_max_base_fee_numerator(); - validate_fee_fraction(min_fee_numerator, FEE_DENOMINATOR)?; - validate_fee_fraction(max_fee_numerator, FEE_DENOMINATOR)?; - require!( - min_fee_numerator >= MIN_FEE_NUMERATOR && max_fee_numerator <= MAX_FEE_NUMERATOR, - PoolError::ExceedMaxFeeBps - ); Ok(()) } + fn to_base_fee_struct(&self) -> BaseFeeStruct { BaseFeeStruct { cliff_fee_numerator: self.cliff_fee_numerator, - number_of_period: self.number_of_period, - period_frequency: self.period_frequency, - reduction_factor: self.reduction_factor, - fee_scheduler_mode: self.fee_scheduler_mode, + first_factor: self.first_factor, + second_factor: self.second_factor, + third_factor: self.third_factor, + base_fee_mode: self.base_fee_mode, ..Default::default() } } @@ -82,10 +63,10 @@ impl BaseFeeParameters { pub fn to_base_fee_config(&self) -> BaseFeeConfig { BaseFeeConfig { cliff_fee_numerator: self.cliff_fee_numerator, - number_of_period: self.number_of_period, - period_frequency: self.period_frequency, - reduction_factor: self.reduction_factor, - fee_scheduler_mode: self.fee_scheduler_mode, + first_factor: self.first_factor, + second_factor: self.second_factor, + third_factor: self.third_factor, + base_fee_mode: self.base_fee_mode, ..Default::default() } } @@ -255,11 +236,21 @@ pub fn to_bps(numerator: u128, denominator: u128) -> Option { bps.try_into().ok() } +pub fn to_numerator(bps: u128, denominator: u128) -> Result { + let numerator = bps + .safe_mul(denominator.into())? + .safe_div(MAX_BASIS_POINT.into())?; + Ok(u64::try_from(numerator).map_err(|_| PoolError::TypeCastFailed)?) +} + impl PoolFeeParameters { /// Validate that the fees are reasonable - pub fn validate(&self) -> Result<()> { - self.base_fee.validate()?; - + pub fn validate( + &self, + collect_fee_mode: CollectFeeMode, + activation_type: ActivationType, + ) -> Result<()> { + self.base_fee.validate(collect_fee_mode, activation_type)?; if let Some(dynamic_fee) = self.dynamic_fee { dynamic_fee.validate()?; } diff --git a/programs/cp-amm/src/state/config.rs b/programs/cp-amm/src/state/config.rs index aa1b9f0a..a89de158 100644 --- a/programs/cp-amm/src/state/config.rs +++ b/programs/cp-amm/src/state/config.rs @@ -53,11 +53,13 @@ const_assert_eq!(PoolFeesConfig::INIT_SPACE, 128); #[derive(Debug, InitSpace, Default)] pub struct BaseFeeConfig { pub cliff_fee_numerator: u64, - pub fee_scheduler_mode: u8, + // In fee scheduler first_factor: number_of_period, second_factor: period_frequency, third_factor: reduction_factor + // in rate limiter: first_factor: fee_increment_bps, second_factor: max_limiter_duration, max_fee_bps, third_factor: reference_amount + pub base_fee_mode: u8, pub padding: [u8; 5], - pub number_of_period: u16, - pub period_frequency: u64, - pub reduction_factor: u64, + pub first_factor: u16, + pub second_factor: [u8; 8], + pub third_factor: u64, } const_assert_eq!(BaseFeeConfig::INIT_SPACE, 32); @@ -66,20 +68,20 @@ impl BaseFeeConfig { fn to_base_fee_parameters(&self) -> BaseFeeParameters { BaseFeeParameters { cliff_fee_numerator: self.cliff_fee_numerator, - number_of_period: self.number_of_period, - period_frequency: self.period_frequency, - reduction_factor: self.reduction_factor, - fee_scheduler_mode: self.fee_scheduler_mode, + first_factor: self.first_factor, + second_factor: self.second_factor, + third_factor: self.third_factor, + base_fee_mode: self.base_fee_mode, } } fn to_base_fee_struct(&self) -> BaseFeeStruct { BaseFeeStruct { cliff_fee_numerator: self.cliff_fee_numerator, - number_of_period: self.number_of_period, - period_frequency: self.period_frequency, - reduction_factor: self.reduction_factor, - fee_scheduler_mode: self.fee_scheduler_mode, + first_factor: self.first_factor, + second_factor: self.second_factor, + third_factor: self.third_factor, + base_fee_mode: self.base_fee_mode, ..Default::default() } } diff --git a/programs/cp-amm/src/state/fee.rs b/programs/cp-amm/src/state/fee.rs index c7c54e66..ceaa4cee 100644 --- a/programs/cp-amm/src/state/fee.rs +++ b/programs/cp-amm/src/state/fee.rs @@ -5,11 +5,8 @@ use num_enum::{IntoPrimitive, TryFromPrimitive}; use static_assertions::const_assert_eq; use crate::{ - constants::{ - fee::{FEE_DENOMINATOR, MAX_FEE_NUMERATOR}, - BASIS_POINT_MAX, ONE_Q64, - }, - fee_math::get_fee_in_period, + base_fee::{get_base_fee_handler, FeeRateLimiter}, + constants::{fee::FEE_DENOMINATOR, BASIS_POINT_MAX, ONE_Q64}, params::swap::TradeDirection, safe_math::SafeMath, u128x128_math::Rounding, @@ -43,11 +40,13 @@ pub struct FeeOnAmountResult { )] // https://www.desmos.com/calculator/oxdndn2xdx -pub enum FeeSchedulerMode { +pub enum BaseFeeMode { // fee = cliff_fee_numerator - passed_period * reduction_factor - Linear, + FeeSchedulerLinear, // fee = cliff_fee_numerator * (1-reduction_factor/10_000)^passed_period - Exponential, + FeeSchedulerExponential, + // rate limiter + RateLimiter, } #[zero_copy] @@ -88,67 +87,82 @@ const_assert_eq!(PoolFeesStruct::INIT_SPACE, 160); #[derive(Debug, InitSpace, Default)] pub struct BaseFeeStruct { pub cliff_fee_numerator: u64, - pub fee_scheduler_mode: u8, + // In fee scheduler first_factor: number_of_period, second_factor: period_frequency, third_factor: reduction_factor + // in rate limiter: first_factor: fee_increment_bps, second_factor: max_limiter_duration, max_fee_bps, third_factor: reference_amount + pub base_fee_mode: u8, pub padding_0: [u8; 5], - pub number_of_period: u16, - pub period_frequency: u64, - pub reduction_factor: u64, + pub first_factor: u16, + pub second_factor: [u8; 8], + pub third_factor: u64, pub padding_1: u64, } const_assert_eq!(BaseFeeStruct::INIT_SPACE, 40); impl BaseFeeStruct { - pub fn get_max_base_fee_numerator(&self) -> u64 { - self.cliff_fee_numerator - } - pub fn get_min_base_fee_numerator(&self) -> Result { - // trick to force current_point < activation_point - self.get_current_base_fee_numerator(0, 1) + pub fn get_fee_rate_limiter(&self) -> Result { + let base_fee_mode = + BaseFeeMode::try_from(self.base_fee_mode).map_err(|_| PoolError::InvalidBaseFeeMode)?; + if base_fee_mode == BaseFeeMode::RateLimiter { + Ok(FeeRateLimiter { + cliff_fee_numerator: self.cliff_fee_numerator, + fee_increment_bps: self.first_factor, + max_limiter_duration: u32::from_le_bytes( + self.second_factor[0..4] + .try_into() + .map_err(|_| PoolError::TypeCastFailed)?, + ), + max_fee_bps: u32::from_le_bytes( + self.second_factor[4..8] + .try_into() + .map_err(|_| PoolError::TypeCastFailed)?, + ), + reference_amount: self.third_factor, + }) + } else { + Err(PoolError::InvalidFeeRateLimiter.into()) + } } + pub fn get_current_base_fee_numerator( &self, current_point: u64, activation_point: u64, + amount: u64, + trade_direction: TradeDirection, ) -> Result { - if self.period_frequency == 0 { - return Ok(self.cliff_fee_numerator); - } - // can trade before activation point, so it is alpha-vault, we use min fee - let period = if current_point < activation_point { - self.number_of_period.into() - } else { - let period = current_point - .safe_sub(activation_point)? - .safe_div(self.period_frequency)?; - period.min(self.number_of_period.into()) - }; - let fee_scheduler_mode = FeeSchedulerMode::try_from(self.fee_scheduler_mode) - .map_err(|_| PoolError::TypeCastFailed)?; - - match fee_scheduler_mode { - FeeSchedulerMode::Linear => { - let fee_numerator = self - .cliff_fee_numerator - .safe_sub(period.safe_mul(self.reduction_factor.into())?)?; - Ok(fee_numerator) - } - FeeSchedulerMode::Exponential => { - let period = u16::try_from(period).map_err(|_| PoolError::MathOverflow)?; - let fee_numerator = - get_fee_in_period(self.cliff_fee_numerator, self.reduction_factor, period)?; - Ok(fee_numerator) - } - } + let base_fee_handler = get_base_fee_handler( + self.cliff_fee_numerator, + self.first_factor, + self.second_factor, + self.third_factor, + self.base_fee_mode, + )?; + + base_fee_handler.get_base_fee_numerator( + current_point, + activation_point, + trade_direction, + amount, + ) } } impl PoolFeesStruct { // in numerator - pub fn get_total_trading_fee(&self, current_point: u64, activation_point: u64) -> Result { - let base_fee_numerator = self - .base_fee - .get_current_base_fee_numerator(current_point, activation_point)?; + pub fn get_total_trading_fee( + &self, + current_point: u64, + activation_point: u64, + amount: u64, + trade_direction: TradeDirection, + ) -> Result { + let base_fee_numerator = self.base_fee.get_current_base_fee_numerator( + current_point, + activation_point, + amount, + trade_direction, + )?; let total_fee_numerator = self .dynamic_fee .get_variable_fee()? @@ -163,13 +177,17 @@ impl PoolFeesStruct { current_point: u64, activation_point: u64, has_partner: bool, + trade_direction: TradeDirection, + max_fee_numerator: u64, ) -> Result { - let trade_fee_numerator = self.get_total_trading_fee(current_point, activation_point)?; - let trade_fee_numerator = if trade_fee_numerator > MAX_FEE_NUMERATOR.into() { - MAX_FEE_NUMERATOR + let trade_fee_numerator = + self.get_total_trading_fee(current_point, activation_point, amount, trade_direction)?; + let trade_fee_numerator = if trade_fee_numerator > max_fee_numerator.into() { + max_fee_numerator } else { trade_fee_numerator.try_into().unwrap() }; + let lp_fee: u64 = safe_mul_div_cast_u64(amount, trade_fee_numerator, FEE_DENOMINATOR, Rounding::Up)?; // update amount diff --git a/programs/cp-amm/src/state/pool.rs b/programs/cp-amm/src/state/pool.rs index acdec390..1ac51b0d 100644 --- a/programs/cp-amm/src/state/pool.rs +++ b/programs/cp-amm/src/state/pool.rs @@ -8,7 +8,10 @@ use num_enum::{IntoPrimitive, TryFromPrimitive}; use crate::{ assert_eq_admin, - constants::{LIQUIDITY_SCALE, NUM_REWARDS, REWARD_INDEX_0, REWARD_INDEX_1, REWARD_RATE_SCALE}, + constants::{ + fee::{MAX_FEE_NUMERATOR_V0, MAX_FEE_NUMERATOR_V1}, + LIQUIDITY_SCALE, NUM_REWARDS, REWARD_INDEX_0, REWARD_INDEX_1, REWARD_RATE_SCALE, + }, curve::{ get_delta_amount_a_unsigned, get_delta_amount_a_unsigned_unchecked, get_delta_amount_b_unsigned, get_next_sqrt_price_from_input, @@ -78,6 +81,22 @@ pub enum PoolType { Customizable, } +#[repr(u8)] +#[derive( + Clone, + Copy, + Debug, + PartialEq, + IntoPrimitive, + TryFromPrimitive, + AnchorDeserialize, + AnchorSerialize, +)] +pub enum PoolVersion { + V0, // 0 + V1, // 1 +} + #[account(zero_copy)] #[derive(InitSpace, Debug, Default)] pub struct Pool { @@ -127,8 +146,10 @@ pub struct Pool { pub collect_fee_mode: u8, /// pool type pub pool_type: u8, + /// pool version, 0: max_fee is still capped at 50%, 1: max_fee is capped at 99% + pub version: u8, /// padding - pub _padding_0: [u8; 2], + pub _padding_0: u8, /// cumulative pub fee_a_per_liquidity: [u8; 32], // U256 /// cumulative @@ -393,6 +414,7 @@ impl Pool { self.sqrt_price = sqrt_price; self.collect_fee_mode = collect_fee_mode; self.pool_type = pool_type; + self.version = PoolVersion::V0.into(); // still use v0 for now } pub fn pool_reward_initialized(&self) -> bool { @@ -411,6 +433,14 @@ impl Pool { let mut actual_referral_fee = 0; let mut actual_partner_fee = 0; + let pool_version = + PoolVersion::try_from(self.version).map_err(|_| PoolError::TypeCastFailed)?; + let max_fee_numerator = if pool_version == PoolVersion::V0 { + MAX_FEE_NUMERATOR_V0 + } else { + MAX_FEE_NUMERATOR_V1 + }; + let actual_amount_in = if fee_mode.fees_on_input { let FeeOnAmountResult { amount, @@ -424,6 +454,8 @@ impl Pool { current_point, self.activation_point, self.has_partner(), + trade_direction, + max_fee_numerator, )?; actual_protocol_fee = protocol_fee; @@ -459,6 +491,8 @@ impl Pool { current_point, self.activation_point, self.has_partner(), + trade_direction, + max_fee_numerator, )?; actual_protocol_fee = protocol_fee; actual_lp_fee = lp_fee; diff --git a/programs/cp-amm/src/tests/fee_scheduler_tests.rs b/programs/cp-amm/src/tests/fee_scheduler_tests.rs index 581cb164..8c853540 100644 --- a/programs/cp-amm/src/tests/fee_scheduler_tests.rs +++ b/programs/cp-amm/src/tests/fee_scheduler_tests.rs @@ -23,12 +23,19 @@ proptest! { fn test_base_fee() { let base_fee = BaseFeeStruct { cliff_fee_numerator: 100_000, - fee_scheduler_mode: 1, - number_of_period: 50, - period_frequency: 1, - reduction_factor: 500, // 5% each second + base_fee_mode: 1, + first_factor: 50, + second_factor: 1u64.to_le_bytes(), + third_factor: 500, // 5% each second ..Default::default() }; - let current_fee = base_fee.get_current_base_fee_numerator(100, 0).unwrap(); + let current_fee = base_fee + .get_current_base_fee_numerator( + 100, + 0, + 1_000_000, + crate::params::swap::TradeDirection::AtoB, + ) + .unwrap(); println!("{}", current_fee) } diff --git a/programs/cp-amm/src/tests/fixtures/config_account.bin b/programs/cp-amm/src/tests/fixtures/config_account.bin new file mode 100644 index 0000000000000000000000000000000000000000..77622f93e3ad0c31d363152c49fc8d7cb7273461 GIT binary patch literal 328 zcmbQuv+9A|uQN>yl!DZ=vLGWE!RAyj*g(x&2oZqth^IstM2J@mRLKanM-8egz?u>2 Z7Kp&?@JzpzQZX$XkNo>jlEFkO1^^;r4%q+z literal 0 HcmV?d00001 diff --git a/programs/cp-amm/src/tests/fixtures/pool_account.bin b/programs/cp-amm/src/tests/fixtures/pool_account.bin new file mode 100644 index 0000000000000000000000000000000000000000..5727eee1adb3ca79c32c8e2114a2afde8ea7a329 GIT binary patch literal 1112 zcmex3E0;xZW9}Y?)U&b-K)?th7%CWSz%&B`Cqw|sV_*;gLJ)unCoH+cP{|JBa{+aA z2tU6Um;sVMEpU9-j$Sqp6Ab=$vIzy$=fedS@QK2th@v;VabFSXcINlVWJ6U&+qfAO z!Zq^x&zu{=Kg!kYP@E;nHoGJI-|EJe-x>AEZW0GfZym9`BlX%kbjE@YAoCelUPvtn zTy0b0%)U8n+nWLowQNJhlNk?%Y~nJV{#EGTV6g9a_Cl{I;--k(?D&&!RSbPn_H@5$ z(u!74pRn+#5+6~H#G`1Rib{s;8qc)y3DOr}VTVn5<)y2)u!#p)Gs43hBtJVm({H6z zOv}b2|Nevc5AV%%sTNCthws-qM{qoV?80XsNG}MyJk^t|$({dXOPR6g Q!sVX?ENVxEhEE6p031qfBLDyZ literal 0 HcmV?d00001 diff --git a/programs/cp-amm/src/tests/layout_test.rs b/programs/cp-amm/src/tests/layout_test.rs new file mode 100644 index 00000000..3bd8d750 --- /dev/null +++ b/programs/cp-amm/src/tests/layout_test.rs @@ -0,0 +1,52 @@ +use crate::state::{Config, Pool}; + +use std::fs; + +#[test] +fn config_account_layout_backward_compatible() { + // config account: TBuzuEMMQizTjpZhRLaUPavALhZmD8U1hwiw1pWSCSq + let config_account_data = + fs::read(&"./src/tests/fixtures/config_account.bin").expect("Failed to read account data"); + + let mut data_without_discriminator = config_account_data[8..].to_vec(); + let config_state: &mut Config = bytemuck::from_bytes_mut(&mut data_without_discriminator); + + // Test backward compatibility + // https://solscan.io/account/TBuzuEMMQizTjpZhRLaUPavALhZmD8U1hwiw1pWSCSq#anchorData + let period_frequency = 60u64; + let period_frequency_from_bytes = + u64::from_le_bytes(config_state.pool_fees.base_fee.second_factor); + assert_eq!( + period_frequency, period_frequency_from_bytes, + "Second factor layout should be backward compatible" + ); + let period_to_bytes = period_frequency.to_le_bytes(); + assert_eq!( + period_to_bytes, + config_state.pool_fees.base_fee.second_factor, + ); +} + +#[test] +fn pool_account_layout_backward_compatible() { + // pool account: E8zRkDw3UdzRc8qVWmqyQ9MLj7jhgZDHSroYud5t25A7 + let pool_account_data = + fs::read(&"./src/tests/fixtures/pool_account.bin").expect("Failed to read account data"); + + let mut data_without_discriminator = pool_account_data[8..].to_vec(); + let pool_state: &mut Pool = bytemuck::from_bytes_mut(&mut data_without_discriminator); + + // Test backward compatibility + // https://solscan.io/account/E8zRkDw3UdzRc8qVWmqyQ9MLj7jhgZDHSroYud5t25A7#anchorData + let period_frequency = 60u64; + let period_frequency_from_bytes = + u64::from_le_bytes(pool_state.pool_fees.base_fee.second_factor); + + assert_eq!( + period_frequency, period_frequency_from_bytes, + "Second factor layout should be backward compatible" + ); + + let period_to_bytes = period_frequency.to_le_bytes(); + assert_eq!(period_to_bytes, pool_state.pool_fees.base_fee.second_factor,); +} diff --git a/programs/cp-amm/src/tests/mod.rs b/programs/cp-amm/src/tests/mod.rs index 466a7bed..a157e8e5 100644 --- a/programs/cp-amm/src/tests/mod.rs +++ b/programs/cp-amm/src/tests/mod.rs @@ -27,3 +27,9 @@ mod fee_scheduler_tests; #[cfg(test)] mod test_volatility_accumulate; + +#[cfg(test)] +mod test_rate_limiter; + +#[cfg(test)] +mod layout_test; diff --git a/programs/cp-amm/src/tests/test_rate_limiter.rs b/programs/cp-amm/src/tests/test_rate_limiter.rs new file mode 100644 index 00000000..52c9393c --- /dev/null +++ b/programs/cp-amm/src/tests/test_rate_limiter.rs @@ -0,0 +1,278 @@ +use crate::{ + activation_handler::ActivationType, + base_fee::{BaseFeeHandler, FeeRateLimiter}, + constants::fee::{FEE_DENOMINATOR, MAX_FEE_NUMERATOR_V1, MIN_FEE_NUMERATOR}, + params::{ + fee_parameters::{to_bps, to_numerator, BaseFeeParameters, PoolFeeParameters}, + swap::TradeDirection, + }, + state::CollectFeeMode, + u128x128_math::Rounding, + utils_math::safe_mul_div_cast_u64, +}; + +#[test] +fn test_validate_rate_limiter() { + // validate collect fee mode + { + let rate_limiter = FeeRateLimiter { + cliff_fee_numerator: 10_0000, + reference_amount: 1_000_000_000, // 1SOL + max_limiter_duration: 60, // 60 seconds + max_fee_bps: 5000, // 50 % + fee_increment_bps: 10, // 10 bps + }; + assert!(rate_limiter + .validate(CollectFeeMode::try_from(0).unwrap(), ActivationType::Slot) + .is_err()); + assert!(rate_limiter + .validate(CollectFeeMode::try_from(1).unwrap(), ActivationType::Slot) + .is_ok()); + } + + // validate zero rate limiter + { + let rate_limiter = FeeRateLimiter { + cliff_fee_numerator: 10_0000, + reference_amount: 1, // 1SOL + max_limiter_duration: 0, // 60 seconds + max_fee_bps: 5000, // 50 % + fee_increment_bps: 0, // 10 bps + }; + assert!(rate_limiter + .validate(CollectFeeMode::try_from(0).unwrap(), ActivationType::Slot) + .is_err()); + let rate_limiter = FeeRateLimiter { + cliff_fee_numerator: 10_0000, + reference_amount: 0, // 1SOL + max_limiter_duration: 1, // 60 seconds + max_fee_bps: 5000, // 50 % + fee_increment_bps: 0, // 10 bps + }; + assert!(rate_limiter + .validate(CollectFeeMode::try_from(0).unwrap(), ActivationType::Slot) + .is_err()); + let rate_limiter = FeeRateLimiter { + cliff_fee_numerator: 10_0000, + reference_amount: 0, // 1SOL + max_limiter_duration: 0, // 60 seconds + max_fee_bps: 5000, // 50 % + fee_increment_bps: 1, // 10 bps + }; + assert!(rate_limiter + .validate(CollectFeeMode::try_from(0).unwrap(), ActivationType::Slot) + .is_err()); + } + + // validate cliff fee numerator + { + let rate_limiter = FeeRateLimiter { + cliff_fee_numerator: MIN_FEE_NUMERATOR - 1, + reference_amount: 1_000_000_000, // 1SOL + max_limiter_duration: 60, // 60 seconds + max_fee_bps: 5000, // 50 % + fee_increment_bps: 10, // 10 bps + }; + assert!(rate_limiter + .validate(CollectFeeMode::try_from(0).unwrap(), ActivationType::Slot) + .is_err()); + let rate_limiter = FeeRateLimiter { + cliff_fee_numerator: MAX_FEE_NUMERATOR_V1 + 1, + reference_amount: 1_000_000_000, // 1SOL + max_limiter_duration: 60, // 60 seconds + max_fee_bps: 5000, // 50 % + fee_increment_bps: 10, // 10 bps + }; + assert!(rate_limiter + .validate(CollectFeeMode::try_from(0).unwrap(), ActivationType::Slot) + .is_err()); + } +} + +#[test] +fn test_rate_limiter_from_pool_fee_params() { + let max_limiter_duration: u32 = 60u32; + let max_fee_bps: u32 = 5000u32; + let mut second_factor = [0u8; 8]; + second_factor[0..4].copy_from_slice(&max_limiter_duration.to_le_bytes()); + second_factor[4..8].copy_from_slice(&max_fee_bps.to_le_bytes()); + + let base_fee = BaseFeeParameters { + cliff_fee_numerator: 10_0000, + first_factor: 10, // fee increasement bps + second_factor, + third_factor: 1_000_000_000, // reference_amount 1SOL + base_fee_mode: 2, + }; + + let pool_fees = PoolFeeParameters { + base_fee, + dynamic_fee: None, + ..Default::default() + }; + + let base_fee_struct = pool_fees.to_pool_fees_struct().base_fee; + let rate_limiter = base_fee_struct.get_fee_rate_limiter().unwrap(); + + assert_eq!(rate_limiter.max_fee_bps, max_fee_bps); + assert_eq!(rate_limiter.max_limiter_duration, max_limiter_duration); +} +// that test show that more amount, then more fee numerator +#[test] +fn test_rate_limiter_behavior() { + let base_fee_bps = 100u64; // 1% + let reference_amount = 1_000_000_000; // 1 sol + let fee_increment_bps = 100; // 1% + let cliff_fee_numerator = to_numerator(base_fee_bps.into(), FEE_DENOMINATOR.into()).unwrap(); + + let rate_limiter = FeeRateLimiter { + cliff_fee_numerator, + reference_amount, // 1SOL + max_limiter_duration: 60, // 60 seconds + max_fee_bps: 5000, // 50 % + fee_increment_bps, // 10 bps + }; + assert!(rate_limiter + .validate(CollectFeeMode::try_from(1).unwrap(), ActivationType::Slot) + .is_ok()); + + { + let fee_numerator = rate_limiter + .get_fee_numerator_from_amount(reference_amount) + .unwrap(); + let fee_bps = to_bps(fee_numerator.into(), FEE_DENOMINATOR.into()).unwrap(); + assert_eq!(fee_bps, base_fee_bps); + } + + { + let fee_numerator = rate_limiter + .get_fee_numerator_from_amount(reference_amount * 3 / 2) + .unwrap(); + let fee_bps = to_bps(fee_numerator.into(), FEE_DENOMINATOR.into()).unwrap(); + assert_eq!(fee_bps, 133); + + let fee_numerator = rate_limiter + .get_fee_numerator_from_amount(reference_amount * 2) + .unwrap(); + let fee_bps = to_bps(fee_numerator.into(), FEE_DENOMINATOR.into()).unwrap(); + assert_eq!(fee_bps, 150); // 1.5%, (1+1+1) / 2 + } + + { + let fee_numerator = rate_limiter + .get_fee_numerator_from_amount(reference_amount * 3) + .unwrap(); + let fee_bps = to_bps(fee_numerator.into(), FEE_DENOMINATOR.into()).unwrap(); + assert_eq!(fee_bps, 200); // 2%, (1+1+1+1) / 2 + } + + { + let fee_numerator = rate_limiter + .get_fee_numerator_from_amount(reference_amount * 4) + .unwrap(); + let fee_bps = to_bps(fee_numerator.into(), FEE_DENOMINATOR.into()).unwrap(); + assert_eq!(fee_bps, 250); // 2.5% (1+1+1+1+1) / 2 + } + + { + let fee_numerator = rate_limiter + .get_fee_numerator_from_amount(u64::MAX) + .unwrap(); + let fee_bps = to_bps(fee_numerator.into(), FEE_DENOMINATOR.into()).unwrap(); + + assert_eq!(fee_bps, u64::from(rate_limiter.max_fee_bps)); // fee_bps cap equal max_fee_bps + } +} + +fn calculate_output_amount(rate_limiter: &FeeRateLimiter, input_amount: u64) -> u64 { + let trade_fee_numerator = rate_limiter + .get_base_fee_numerator(0, 0, TradeDirection::BtoA, input_amount) + .unwrap(); + let trading_fee: u64 = safe_mul_div_cast_u64( + input_amount, + trade_fee_numerator, + FEE_DENOMINATOR, + Rounding::Up, + ) + .unwrap(); + input_amount.checked_sub(trading_fee).unwrap() +} +// that test show that, more input amount, then more output amount +#[test] +fn test_rate_limiter_routing_friendly() { + let base_fee_bps = 100u64; // 1% + let reference_amount = 1_000_000_000; // 1 sol + let fee_increment_bps = 100; // 1% + let cliff_fee_numerator = to_numerator(base_fee_bps.into(), FEE_DENOMINATOR.into()).unwrap(); + + let rate_limiter = FeeRateLimiter { + cliff_fee_numerator, + reference_amount, // 1SOL + max_limiter_duration: 60, // 60 seconds + max_fee_bps: 5000, // 50 % + fee_increment_bps, // 10 bps + }; + + let mut input_amount = reference_amount - 10; + let mut currrent_output_amount = calculate_output_amount(&rate_limiter, input_amount); + + for _i in 0..500 { + input_amount = input_amount + reference_amount / 2; + let output_amount = calculate_output_amount(&rate_limiter, input_amount); + assert!(output_amount > currrent_output_amount); + currrent_output_amount = output_amount + } +} + +#[test] +fn test_rate_limiter_base_fee_numerator() { + let base_fee_bps = 100u64; // 1% + let reference_amount = 1_000_000_000; // 1 sol + let fee_increment_bps = 100; // 1% + let cliff_fee_numerator = to_numerator(base_fee_bps.into(), FEE_DENOMINATOR.into()).unwrap(); + + let rate_limiter = FeeRateLimiter { + cliff_fee_numerator, + reference_amount, // 1SOL + max_limiter_duration: 60, // 60 seconds + max_fee_bps: 5000, // 50 % + fee_increment_bps, // 10 bps + }; + + { + // trade from base to quote + let fee_numerator = rate_limiter + .get_base_fee_numerator(0, 0, TradeDirection::AtoB, 2_000_000_000) + .unwrap(); + + assert_eq!(fee_numerator, rate_limiter.cliff_fee_numerator); + } + + { + // trade pass last effective point + let fee_numerator = rate_limiter + .get_base_fee_numerator( + (rate_limiter.max_limiter_duration + 1).into(), + 0, + TradeDirection::BtoA, + 2_000_000_000, + ) + .unwrap(); + + assert_eq!(fee_numerator, rate_limiter.cliff_fee_numerator); + } + + { + // trade in effective point + let fee_numerator = rate_limiter + .get_base_fee_numerator( + rate_limiter.max_limiter_duration.into(), + 0, + TradeDirection::BtoA, + 2_000_000_000, + ) + .unwrap(); + + assert!(fee_numerator > rate_limiter.cliff_fee_numerator); + } +} diff --git a/tests/addLiquidity.test.ts b/tests/addLiquidity.test.ts index 2941157c..8524997f 100644 --- a/tests/addLiquidity.test.ts +++ b/tests/addLiquidity.test.ts @@ -1,4 +1,7 @@ import { AccountLayout } from "@solana/spl-token"; +import { expect } from "chai"; +import { BanksClient, ProgramTestContext } from "solana-bankrun"; +import { convertToByteArray, generateKpAndFund, randomID, startTest } from "./bankrun-utils/common"; import { Keypair, PublicKey } from "@solana/web3.js"; import BN from "bn.js"; import { expect } from "chai"; @@ -94,10 +97,10 @@ describe("Add liquidity", () => { poolFees: { baseFee: { cliffFeeNumerator: new BN(2_500_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: null, @@ -335,10 +338,10 @@ describe("Add liquidity", () => { poolFees: { baseFee: { cliffFeeNumerator: new BN(2_500_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: null, diff --git a/tests/alphaVaultWithSniperTax.test.ts b/tests/alphaVaultWithSniperTax.test.ts new file mode 100644 index 00000000..2046b000 --- /dev/null +++ b/tests/alphaVaultWithSniperTax.test.ts @@ -0,0 +1,280 @@ +import { BanksClient, ProgramTestContext } from "solana-bankrun"; +import { + convertToByteArray, + convertToRateLimiterSecondFactor, + generateKpAndFund, + startTest, + warpSlotBy, +} from "./bankrun-utils/common"; +import { Keypair, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js"; +import { + MIN_LP_AMOUNT, + MAX_SQRT_PRICE, + MIN_SQRT_PRICE, + createToken, + mintSplTokenTo, + InitializeCustomizablePoolParams, + initializeCustomizablePool, + getPool, + FEE_DENOMINATOR, + BaseFee, +} from "./bankrun-utils"; +import BN from "bn.js"; +import { + depositAlphaVault, + fillDammV2, + getVaultState, + setupProrataAlphaVault, +} from "./bankrun-utils/alphaVault"; +import { NATIVE_MINT } from "@solana/spl-token"; +import { mulDiv, Rounding } from "./bankrun-utils/math"; +import { expect } from "chai"; + +describe("Alpha vault with sniper tax", () => { + describe("Fee Scheduler", () => { + let context: ProgramTestContext; + let user: Keypair; + let creator: Keypair; + let tokenAMint: PublicKey; + let tokenBMint: PublicKey; + + beforeEach(async () => { + const root = Keypair.generate(); + context = await startTest(root); + + user = await generateKpAndFund(context.banksClient, context.payer); + creator = await generateKpAndFund(context.banksClient, context.payer); + + tokenAMint = await createToken( + context.banksClient, + context.payer, + context.payer.publicKey + ); + tokenBMint = NATIVE_MINT; + + await mintSplTokenTo( + context.banksClient, + context.payer, + tokenAMint, + context.payer, + creator.publicKey + ); + }); + + it("Alpha vault can buy before activation point with minimum fee", async () => { + const baseFee = { + cliffFeeNumerator: new BN(990_000_000), // 99 % + firstFactor: 100, // 100 periods + secondFactor: convertToByteArray(new BN(1)), + thirdFactor: new BN(9875000), + baseFeeMode: 0, // fee scheduler Linear mode + }; + const { pool, alphaVault } = await alphaVaultWithSniperTaxFullflow( + context, + user, + creator, + tokenAMint, + tokenBMint, + baseFee + ); + + const alphaVaultState = await getVaultState( + context.banksClient, + alphaVault + ); + const poolState = await getPool(context.banksClient, pool); + let totalTradingFee = poolState.metrics.totalLpBFee.add( + poolState.metrics.totalProtocolBFee + ); + const totalDeposit = new BN(alphaVaultState.totalDeposit); + + // flat base fee + // linear fee scheduler + const feeNumerator = poolState.poolFees.baseFee.cliffFeeNumerator.sub( + new BN(poolState.poolFees.baseFee.firstFactor).mul( + poolState.poolFees.baseFee.thirdFactor + ) + ); + + const lpFee = mulDiv( + totalDeposit, + feeNumerator, + new BN(FEE_DENOMINATOR), + Rounding.Up + ); + // alpha vault can buy with minimum fee (fee scheduler don't applied) + // expect total trading fee equal minimum base fee + expect(totalTradingFee.toNumber()).eq(lpFee.toNumber()); + }); + }); + + describe("Rate limiter", () => { + let context: ProgramTestContext; + let user: Keypair; + let creator: Keypair; + let tokenAMint: PublicKey; + let tokenBMint: PublicKey; + + beforeEach(async () => { + const root = Keypair.generate(); + context = await startTest(root); + + user = await generateKpAndFund(context.banksClient, context.payer); + creator = await generateKpAndFund(context.banksClient, context.payer); + + tokenAMint = await createToken( + context.banksClient, + context.payer, + context.payer.publicKey + ); + tokenBMint = NATIVE_MINT; + + await mintSplTokenTo( + context.banksClient, + context.payer, + tokenAMint, + context.payer, + creator.publicKey + ); + }); + + it("Alpha vault can buy before activation point with minimum fee", async () => { + let referenceAmount = new BN(LAMPORTS_PER_SOL); // 1 SOL + let maxRateLimiterDuration = new BN(10); + let maxFeeBps = new BN(5000); + + let rateLimiterSecondFactor = convertToRateLimiterSecondFactor( + maxRateLimiterDuration, + maxFeeBps + ); + + const baseFee = { + cliffFeeNumerator: new BN(10_000_000), // 100bps + firstFactor: 10, // 10 bps + secondFactor: rateLimiterSecondFactor, + thirdFactor: referenceAmount, // 1 sol + baseFeeMode: 2, // rate limiter mode + }; + const { pool, alphaVault } = await alphaVaultWithSniperTaxFullflow( + context, + user, + creator, + tokenAMint, + tokenBMint, + baseFee + ); + + const alphaVaultState = await getVaultState( + context.banksClient, + alphaVault + ); + const poolState = await getPool(context.banksClient, pool); + let totalTradingFee = poolState.metrics.totalLpBFee.add( + poolState.metrics.totalProtocolBFee + ); + const totalDeposit = new BN(alphaVaultState.totalDeposit); + const feeNumerator = poolState.poolFees.baseFee.cliffFeeNumerator; + + const lpFee = mulDiv( + totalDeposit, + feeNumerator, + new BN(FEE_DENOMINATOR), + Rounding.Up + ); + // alpha vault can buy with minimum fee (rate limiter don't applied) + // expect total trading fee equal minimum base fee + expect(totalTradingFee.toNumber()).eq(lpFee.toNumber()); + }); + }); +}); + +const alphaVaultWithSniperTaxFullflow = async ( + context: ProgramTestContext, + user: Keypair, + creator: Keypair, + tokenAMint: PublicKey, + tokenBMint: PublicKey, + baseFee: BaseFee +): Promise<{ pool: PublicKey; alphaVault: PublicKey }> => { + let activationPointDiff = 20; + let startVestingPointDiff = 25; + let endVestingPointDiff = 30; + + let currentSlot = await context.banksClient.getSlot("processed"); + let activationPoint = new BN(Number(currentSlot) + activationPointDiff); + + console.log("setup permission pool"); + + const params: InitializeCustomizablePoolParams = { + payer: creator, + creator: creator.publicKey, + tokenAMint, + tokenBMint, + liquidity: MIN_LP_AMOUNT, + sqrtPrice: MIN_SQRT_PRICE, + sqrtMinPrice: MIN_SQRT_PRICE, + sqrtMaxPrice: MAX_SQRT_PRICE, + hasAlphaVault: true, + activationPoint, + poolFees: { + baseFee, + protocolFeePercent: 20, + partnerFeePercent: 0, + referralFeePercent: 20, + dynamicFee: null, + }, + activationType: 0, // slot + collectFeeMode: 1, // onlyB + }; + const { pool } = await initializeCustomizablePool( + context.banksClient, + params + ); + + console.log("setup prorata vault"); + let startVestingPoint = new BN(Number(currentSlot) + startVestingPointDiff); + let endVestingPoint = new BN(Number(currentSlot) + endVestingPointDiff); + let maxBuyingCap = new BN(10 * LAMPORTS_PER_SOL); + + let alphaVault = await setupProrataAlphaVault(context.banksClient, { + baseMint: tokenAMint, + quoteMint: tokenBMint, + pool, + poolType: 2, // 0: DLMM, 1: Dynamic Pool, 2: DammV2 + startVestingPoint, + endVestingPoint, + maxBuyingCap, + payer: creator, + escrowFee: new BN(0), + whitelistMode: 0, // Permissionless + baseKeypair: creator, + }); + + console.log("User deposit in alpha vault"); + let depositAmount = new BN(10 * LAMPORTS_PER_SOL); + await depositAlphaVault(context.banksClient, { + amount: depositAmount, + ownerKeypair: user, + alphaVault, + payer: user, + }); + + // warp slot to pre-activation point + // alpha vault can buy before activation point + const preactivationPoint = activationPoint.sub(new BN(5)); + await warpSlotBy(context, preactivationPoint); + + console.log("fill damm v2"); + await fillDammV2( + context.banksClient, + pool, + alphaVault, + creator, + maxBuyingCap + ); + + return { + pool, + alphaVault, + }; +}; diff --git a/tests/bankrun-utils/alphaVault.ts b/tests/bankrun-utils/alphaVault.ts new file mode 100644 index 00000000..9c0cf32b --- /dev/null +++ b/tests/bankrun-utils/alphaVault.ts @@ -0,0 +1,328 @@ +import { + AnchorProvider, + BN, + IdlAccounts, + IdlTypes, + Program, + Wallet, +} from "@coral-xyz/anchor"; +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + getAssociatedTokenAddressSync, + MintLayout, + NATIVE_MINT, + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; +import { + clusterApiUrl, + ComputeBudgetProgram, + Connection, + Keypair, + PublicKey, + SystemProgram, +} from "@solana/web3.js"; + +import AlphaVaultIDL from "./idl/alpha_vault.json"; +import { AlphaVault } from "./idl/alpha_vault"; +import { ALPHA_VAULT_PROGRAM_ID, FEE_DENOMINATOR } from "./constants"; +import { expect } from "chai"; +import { createCpAmmProgram, getPool } from "./cpAmm"; +import { BanksClient } from "solana-bankrun"; +import { derivePoolAuthority } from "./accounts"; +import { getOrCreateAssociatedTokenAccount, wrapSOL } from "./token"; +import { processTransactionMaybeThrow } from "./common"; +import { mulDiv, Rounding } from "./math"; + +export const ALPHA_VAULT_TREASURY_ID = new PublicKey( + "BJQbRiRWhJCyTYZcAuAL3ngDCx3AyFQGKDq8zhiZAKUw" +); + +export interface DepositAlphaVaultParams { + amount: BN; + alphaVault: PublicKey; + ownerKeypair: Keypair; + payer: Keypair; +} + +export type WhitelistMode = 0 | 1 | 2; + +export interface SetupProrataAlphaVaultParams { + quoteMint: PublicKey; + baseMint: PublicKey; + pool: PublicKey; + poolType: number; + maxBuyingCap: BN; + startVestingPoint: BN; + endVestingPoint: BN; + escrowFee: BN; + payer: Keypair; + whitelistMode: WhitelistMode; + baseKeypair: Keypair; +} + +export function deriveAlphaVaultEscrow( + alphaVault: PublicKey, + owner: PublicKey +) { + return PublicKey.findProgramAddressSync( + [Buffer.from("escrow"), alphaVault.toBuffer(), owner.toBuffer()], + ALPHA_VAULT_PROGRAM_ID + ); +} + +export function deriveAlphaVault(base: PublicKey, lbPair: PublicKey) { + return PublicKey.findProgramAddressSync( + [Buffer.from("vault"), base.toBuffer(), lbPair.toBuffer()], + ALPHA_VAULT_PROGRAM_ID + ); +} + +export function deriveCrankFeeWhitelist(owner: PublicKey) { + return PublicKey.findProgramAddressSync( + [Buffer.from("crank_fee_whitelist"), owner.toBuffer()], + ALPHA_VAULT_PROGRAM_ID + ); +} + +export function createAlphaVaultProgram() { + const wallet = new Wallet(Keypair.generate()); + const provider = new AnchorProvider( + new Connection(clusterApiUrl("devnet")), + wallet, + {} + ); + const program = new Program( + AlphaVaultIDL as AlphaVault, + provider + ); + return program; +} + +export async function getVaultState(banksClient: BanksClient, alphaVault: PublicKey): Promise{ + const alphaVaultProgram = createAlphaVaultProgram(); + + const alphaVaultAccount = await banksClient.getAccount(alphaVault); + return alphaVaultProgram.coder.accounts.decode( + "vault", + Buffer.from(alphaVaultAccount.data) + ); + +} + +export async function setupProrataAlphaVault( + banksClient: BanksClient, + params: SetupProrataAlphaVaultParams +): Promise { + let { + quoteMint, + baseMint, + pool, + poolType, + maxBuyingCap, + startVestingPoint, + endVestingPoint, + payer, + escrowFee, + whitelistMode, + baseKeypair, + } = params; + + const alphaVaultProgram = createAlphaVaultProgram(); + + const baseMintAccount = await banksClient.getAccount(baseMint); + const quoteMintAccount = await banksClient.getAccount(quoteMint); + + let [alphaVault] = deriveAlphaVault(baseKeypair.publicKey, pool); + + await getOrCreateAssociatedTokenAccount( + banksClient, + payer, + quoteMint, + alphaVault, + quoteMintAccount.owner + ); + + await getOrCreateAssociatedTokenAccount( + banksClient, + payer, + baseMint, + alphaVault, + baseMintAccount.owner + ); + + const transaction = await alphaVaultProgram.methods + .initializeProrataVault({ + poolType, + quoteMint, + baseMint, + maxBuyingCap, + depositingPoint: new BN(0), + startVestingPoint, + endVestingPoint, + escrowFee, + whitelistMode, + }) + .accountsPartial({ + vault: alphaVault, + pool, + funder: payer.publicKey, + base: baseKeypair.publicKey, + systemProgram: SystemProgram.programId, + }) + .transaction(); + + transaction.recentBlockhash = (await banksClient.getLatestBlockhash())[0]; + transaction.feePayer = payer.publicKey; + transaction.sign(payer, baseKeypair); + + await processTransactionMaybeThrow(banksClient, transaction); + + return alphaVault; +} + +export async function depositAlphaVault( + banksClient: BanksClient, + params: DepositAlphaVaultParams +) { + let { amount, ownerKeypair, alphaVault, payer } = params; + const alphaVaultProgram = createAlphaVaultProgram(); + + const alphaVaultAccount = await banksClient.getAccount(alphaVault); + let alphaVaultState = alphaVaultProgram.coder.accounts.decode( + "vault", + Buffer.from(alphaVaultAccount.data) + ); + const quoteMintAccount = await banksClient.getAccount( + alphaVaultState.quoteMint + ); + + let [escrow] = deriveAlphaVaultEscrow(alphaVault, ownerKeypair.publicKey); + + const escrowData = await banksClient.getAccount(escrow); + if (!escrowData) { + const createEscrowTx = await alphaVaultProgram.methods + .createNewEscrow() + .accountsPartial({ + owner: ownerKeypair.publicKey, + vault: alphaVault, + pool: alphaVaultState.pool, + payer: payer.publicKey, + systemProgram: SystemProgram.programId, + escrow, + escrowFeeReceiver: ALPHA_VAULT_TREASURY_ID, + }) + .transaction(); + + createEscrowTx.recentBlockhash = ( + await banksClient.getLatestBlockhash() + )[0]; + createEscrowTx.feePayer = payer.publicKey; + createEscrowTx.sign(payer); + + + await processTransactionMaybeThrow(banksClient, createEscrowTx); + } + + if (alphaVaultState.quoteMint.equals(NATIVE_MINT)) { + await wrapSOL(banksClient, payer, amount); + } + + let sourceToken = getAssociatedTokenAddressSync( + alphaVaultState.quoteMint, + ownerKeypair.publicKey, + true, + quoteMintAccount.owner, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + + const transaction = await alphaVaultProgram.methods + .deposit(amount) + .accountsPartial({ + vault: alphaVault, + pool: alphaVaultState.pool, + escrow, + sourceToken, + tokenVault: alphaVaultState.tokenVault, + tokenMint: alphaVaultState.quoteMint, + tokenProgram: quoteMintAccount.owner, + owner: ownerKeypair.publicKey, + }) + .transaction(); + + transaction.recentBlockhash = (await banksClient.getLatestBlockhash())[0]; + transaction.feePayer = ownerKeypair.publicKey; + transaction.sign(ownerKeypair); + + await processTransactionMaybeThrow(banksClient, transaction); +} + +export async function fillDammV2( + banksClient: BanksClient, + pool: PublicKey, + alphaVault: PublicKey, + onwer: Keypair, + maxAmount: BN +) { + const alphaVaultProgram = createAlphaVaultProgram(); + const ammProgram = createCpAmmProgram(); + const alphaVaultAccount = await banksClient.getAccount(alphaVault); + let alphaVaultState = alphaVaultProgram.coder.accounts.decode( + "vault", + Buffer.from(alphaVaultAccount.data) + ); + + let poolState = await getPool(banksClient, pool); + const tokenAProgram = + poolState.tokenAFlag == 0 ? TOKEN_PROGRAM_ID : TOKEN_2022_PROGRAM_ID; + const tokenBProgram = + poolState.tokenBFlag == 0 ? TOKEN_PROGRAM_ID : TOKEN_2022_PROGRAM_ID; + + const dammEventAuthority = PublicKey.findProgramAddressSync( + [Buffer.from("__event_authority")], + ammProgram.programId + )[0]; + + const [crankFeeWhitelist] = deriveCrankFeeWhitelist(onwer.publicKey); + const crankFeeWhitelistAccount = await banksClient.getAccount( + crankFeeWhitelist + ); + + const transaction = await alphaVaultProgram.methods + .fillDammV2(maxAmount) + .accountsPartial({ + vault: alphaVault, + tokenVault: alphaVaultState.tokenVault, + tokenOutVault: alphaVaultState.tokenOutVault, + ammProgram: ammProgram.programId, + poolAuthority: derivePoolAuthority(), + pool, + tokenAVault: poolState.tokenAVault, + tokenBVault: poolState.tokenBVault, + tokenAMint: poolState.tokenAMint, + tokenBMint: poolState.tokenBMint, + tokenAProgram, + tokenBProgram, + dammEventAuthority, + cranker: onwer.publicKey, + crankFeeWhitelist: crankFeeWhitelistAccount + ? crankFeeWhitelist + : ALPHA_VAULT_PROGRAM_ID, + crankFeeReceiver: crankFeeWhitelistAccount + ? ALPHA_VAULT_PROGRAM_ID + : ALPHA_VAULT_TREASURY_ID, + systemProgram: SystemProgram.programId, + }) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ + units: 1_400_000, + }), + ]) + .transaction(); + + transaction.recentBlockhash = (await banksClient.getLatestBlockhash())[0]; + transaction.feePayer = onwer.publicKey; + transaction.sign(onwer); + + await processTransactionMaybeThrow(banksClient, transaction); +} diff --git a/tests/bankrun-utils/common.ts b/tests/bankrun-utils/common.ts index e30a7dbc..322a733e 100644 --- a/tests/bankrun-utils/common.ts +++ b/tests/bankrun-utils/common.ts @@ -6,9 +6,11 @@ import { Transaction, } from "@solana/web3.js"; import { BanksClient, ProgramTestContext, startAnchor } from "solana-bankrun"; -import { CP_AMM_PROGRAM_ID } from "./constants"; +import { ALPHA_VAULT_PROGRAM_ID, CP_AMM_PROGRAM_ID } from "./constants"; import BN from "bn.js"; +import CpAmmIdl from "../../target/idl/cp_amm.json"; + export async function startTest(root: Keypair) { // Program name need to match fixtures program name return startAnchor( @@ -18,6 +20,10 @@ export async function startTest(root: Keypair) { name: "cp_amm", programId: new PublicKey(CP_AMM_PROGRAM_ID), }, + { + name: "alpha_vault", + programId: new PublicKey(ALPHA_VAULT_PROGRAM_ID), + }, ], [ { @@ -85,6 +91,22 @@ export async function expectThrowsAsync( throw new Error("Expected an error but didn't get one"); } +export function getCpAmmProgramErrorCodeHexString(errorMessage: String) { + const error = CpAmmIdl.errors.find( + (e) => + e.name.toLowerCase() === errorMessage.toLowerCase() || + e.msg.toLowerCase() === errorMessage.toLowerCase() + ); + + if (!error) { + throw new Error( + `Unknown stake for fee error message / name: ${errorMessage}` + ); + } + + return "0x" + error.code.toString(16); +} + export async function generateKpAndFund( banksClient: BanksClient, rootKeypair: Keypair @@ -94,199 +116,30 @@ export async function generateKpAndFund( banksClient, rootKeypair, kp.publicKey, - new BN(LAMPORTS_PER_SOL) + new BN(100 * LAMPORTS_PER_SOL) ); return kp; } -// async function createAndFundToken2022( -// banksClient: BanksClient, -// rootKeypair: Keypair, -// extensions: ExtensionType[], -// accounts: PublicKey[] -// ) { -// const tokenAMintKeypair = Keypair.generate(); -// const tokenBMintKeypair = Keypair.generate(); -// const rewardMintKeypair = Keypair.generate(); -// await createToken2022( -// banksClient, -// rootKeypair, -// tokenAMintKeypair, -// extensions -// ); -// await createToken2022( -// banksClient, -// rootKeypair, -// tokenBMintKeypair, -// extensions -// ); -// await createToken2022( -// banksClient, -// rootKeypair, -// rewardMintKeypair, -// extensions -// ); -// // Mint token A to payer & user -// for (const account of accounts) { -// await mintToToken2022( -// banksClient, -// rootKeypair, -// rootKeypair, -// tokenAMintKeypair.publicKey, -// account, -// BigInt(rawAmount) -// ); - -// await mintToToken2022( -// banksClient, -// rootKeypair, -// rootKeypair, -// tokenBMintKeypair.publicKey, -// account, -// BigInt(rawAmount) -// ); - -// await mintToToken2022( -// banksClient, -// rootKeypair, -// rootKeypair, -// rewardMintKeypair.publicKey, -// account, -// BigInt(rawAmount) -// ); - -// await mintToToken2022( -// banksClient, -// rootKeypair, -// rootKeypair, -// rewardMintKeypair.publicKey, -// account, -// BigInt(rawAmount) -// ); -// } -// return { -// tokenAMint: tokenAMintKeypair.publicKey, -// tokenBMint: tokenBMintKeypair, -// rewardMint: rewardMintKeypair.publicKey, -// }; -// } - -// async function createAndFundSplToken( -// banksClient: BanksClient, -// rootKeypair: Keypair, -// accounts: PublicKey[] -// ) { -// const tokenAMintKeypair = Keypair.generate(); -// const tokenBMintKeypair = Keypair.generate(); -// const rewardMintKeypair = Keypair.generate(); -// await createToken( -// banksClient, -// rootKeypair, -// tokenAMintKeypair, -// rootKeypair.publicKey -// ); -// await createToken( -// banksClient, -// rootKeypair, -// tokenBMintKeypair, -// rootKeypair.publicKey -// ); -// await createToken( -// banksClient, -// rootKeypair, -// rewardMintKeypair, -// rootKeypair.publicKey -// ); -// // Mint token A to payer & user -// for (const account of accounts) { -// mintTo( -// banksClient, -// rootKeypair, -// tokenAMintKeypair.publicKey, -// rootKeypair, -// account, -// BigInt(rawAmount) -// ); - -// mintTo( -// banksClient, -// rootKeypair, -// tokenBMintKeypair.publicKey, -// rootKeypair, -// account, -// BigInt(rawAmount) -// ); - -// await mintTo( -// banksClient, -// rootKeypair, -// rewardMintKeypair.publicKey, -// rootKeypair, -// account, -// BigInt(rawAmount) -// ); - -// await mintTo( -// banksClient, -// rootKeypair, -// rewardMintKeypair.publicKey, -// rootKeypair, -// account, -// BigInt(rawAmount) -// ); -// } - -// return { -// tokenAMint: tokenAMintKeypair.publicKey, -// tokenBMint: tokenBMintKeypair, -// rewardMint: rewardMintKeypair.publicKey, -// }; -// } - -// export async function setupTestContext( -// banksClient: BanksClient, -// rootKeypair: Keypair, -// token2022: boolean, -// extensions?: ExtensionType[] -// ) { -// const accounts = await generateKpAndFund(banksClient, rootKeypair, 7); -// const accountPubkeys = accounts.map((item) => item.publicKey); -// // -// let tokens; -// if (token2022) { -// tokens = await createAndFundToken2022( -// banksClient, -// rootKeypair, -// extensions, -// accountPubkeys -// ); -// } else { -// tokens = await createAndFundSplToken( -// banksClient, -// rootKeypair, -// accountPubkeys -// ); -// } - -// return { -// admin: accounts[0], -// payer: accounts[1], -// poolCreator: accounts[2], -// funder: accounts[3], -// user: accounts[4], -// operator: accounts[5], -// partner: accounts[6], -// tokenAMint: tokens.tokenAMint, -// tokenBMint: tokens.tokenBMint, -// rewardMint: tokens.rewardMint, -// }; -// } - export function randomID(min = 0, max = 10000) { return Math.floor(Math.random() * (max - min) + min); } export async function warpSlotBy(context: ProgramTestContext, slots: BN) { const clock = await context.banksClient.getClock(); - context.warpToSlot(clock.slot + BigInt(slots.toString())); + context.warpToSlot(clock.slot + BigInt(slots.toString())); +} + +export function convertToByteArray(value: BN): number[] { + return Array.from(value.toArrayLike(Buffer, "le", 8)); +} + +export function convertToRateLimiterSecondFactor( + maxLimiterDuration: BN, + maxFeeBps: BN +): number[] { + const buffer1 = maxLimiterDuration.toArrayLike(Buffer, "le", 4); + const buffer2 = maxFeeBps.toArrayLike(Buffer, "le", 4); + const buffer = Buffer.concat([buffer1, buffer2]); + return Array.from(buffer); } diff --git a/tests/bankrun-utils/constants.ts b/tests/bankrun-utils/constants.ts index 0eb6f872..53a4b7ea 100644 --- a/tests/bankrun-utils/constants.ts +++ b/tests/bankrun-utils/constants.ts @@ -5,6 +5,10 @@ export const CP_AMM_PROGRAM_ID = new PublicKey( "cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG" ); +export const ALPHA_VAULT_PROGRAM_ID = new PublicKey( + "SNPmGgnywBvvrAKMLundzG6StojyHTHDLu7T4sdhP4k" +); + export const TREASURY = new PublicKey( "4EWqcx3aNZmMetCnxwLYwyNjan6XLGp3Ca2W316vrSjv" ); @@ -14,10 +18,17 @@ export const MAX_SQRT_PRICE = new BN("79226673521066979257578248091"); export const LIQUIDITY_MAX = new BN("34028236692093846346337460743"); export const MIN_LP_AMOUNT = new BN("1844674407370955161600"); -export const DECIMALS = 6; +export const DECIMALS = 9; export const BASIS_POINT_MAX = 10_000; export const OFFSET = 64; export const U64_MAX = new BN("18446744073709551615"); +export const MAX_FEE_BPS = 9900; +export const MAX_FEE_NUMERATOR = 990_000_000; +export const MIN_FEE_NUMERATOR = 100_000 +export const FEE_DENOMINATOR = 1_000_000_000; + +export const MAX_RATE_LIMITER_DURATION_IN_SECONDS = 60 * 60 * 12; // 12 hours +export const MAX_RATE_LIMITER_DURATION_IN_SLOTS = 108000; // 12 hours // Set the decimals, fee basis points, and maximum fee export const FEE_BASIS_POINT = 100; // 1% diff --git a/tests/bankrun-utils/cpAmm.ts b/tests/bankrun-utils/cpAmm.ts index 2cc1ffad..d7bb5e77 100644 --- a/tests/bankrun-utils/cpAmm.ts +++ b/tests/bankrun-utils/cpAmm.ts @@ -22,6 +22,7 @@ import { MintCloseAuthorityLayout, MetadataPointerLayout, unpackAccount, + NATIVE_MINT, } from "@solana/spl-token"; import { unpack } from "@solana/spl-token-metadata"; import { @@ -30,13 +31,16 @@ import { ComputeBudgetProgram, Connection, Keypair, + LAMPORTS_PER_SOL, PublicKey, SystemProgram, + SYSVAR_INSTRUCTIONS_PUBKEY, + Transaction, } from "@solana/web3.js"; import { BanksClient } from "solana-bankrun"; import CpAmmIDL from "../../target/idl/cp_amm.json"; import { CpAmm } from "../../target/types/cp_amm"; -import { getOrCreateAssociatedTokenAccount } from "./token"; +import { getOrCreateAssociatedTokenAccount, wrapSOL } from "./token"; import { deriveClaimFeeOperatorAddress, deriveConfigAddress, @@ -104,10 +108,10 @@ export type DynamicFee = { export type BaseFee = { cliffFeeNumerator: BN; - numberOfPeriod: number; - periodFrequency: BN; - reductionFactor: BN; - feeSchedulerMode: number; + firstFactor: number; + secondFactor: number[]; + thirdFactor: BN; + baseFeeMode: number; }; export type PoolFees = { @@ -205,17 +209,18 @@ export async function createConfigIx( expect(configState.poolFees.baseFee.cliffFeeNumerator.toNumber()).eq( params.poolFees.baseFee.cliffFeeNumerator.toNumber() ); - expect(configState.poolFees.baseFee.numberOfPeriod).eq( - params.poolFees.baseFee.numberOfPeriod + expect(configState.poolFees.baseFee.firstFactor).eq( + params.poolFees.baseFee.firstFactor ); - expect(configState.poolFees.baseFee.reductionFactor.toNumber()).eq( - params.poolFees.baseFee.reductionFactor.toNumber() + expect(configState.poolFees.baseFee.thirdFactor.toNumber()).eq( + params.poolFees.baseFee.thirdFactor.toNumber() ); - expect(configState.poolFees.baseFee.feeSchedulerMode).eq( - params.poolFees.baseFee.feeSchedulerMode + expect(configState.poolFees.baseFee.baseFeeMode).eq( + params.poolFees.baseFee.baseFeeMode ); - expect(configState.poolFees.baseFee.periodFrequency.toNumber()).eq( - params.poolFees.baseFee.periodFrequency.toNumber() + + expect(Buffer.from(configState.poolFees.baseFee.secondFactor).toString()).eq( + Buffer.from(params.poolFees.baseFee.secondFactor).toString() ); expect(configState.poolFees.protocolFeePercent).eq( 20 @@ -786,7 +791,7 @@ export type PoolFeesParams = { dynamicFee: DynamicFee | null; }; -export type InitializeCustomizeablePoolParams = { +export type InitializeCustomizablePoolParams = { payer: Keypair; creator: PublicKey; tokenAMint: PublicKey; @@ -802,9 +807,9 @@ export type InitializeCustomizeablePoolParams = { activationPoint: BN | null; }; -export async function initializeCustomizeablePool( +export async function initializeCustomizablePool( banksClient: BanksClient, - params: InitializeCustomizeablePoolParams + params: InitializeCustomizablePoolParams ): Promise<{ pool: PublicKey; position: PublicKey }> { const { tokenAMint, @@ -842,13 +847,18 @@ export async function initializeCustomizeablePool( true, tokenAProgram ); - const payerTokenB = getAssociatedTokenAddressSync( + const payerTokenB = await getOrCreateAssociatedTokenAccount( + banksClient, + payer, tokenBMint, payer.publicKey, - true, tokenBProgram ); + if (tokenBMint.equals(NATIVE_MINT)) { + await wrapSOL(banksClient, payer, new BN(LAMPORTS_PER_SOL)); + } + const transaction = await program.methods .initializeCustomizablePool({ poolFees, @@ -1616,7 +1626,10 @@ export type SwapParams = { referralTokenAccount: PublicKey | null; }; -export async function swap(banksClient: BanksClient, params: SwapParams) { +export async function swapInstruction( + banksClient: BanksClient, + params: SwapParams +): Promise { const { payer, pool, @@ -1672,10 +1685,26 @@ export async function swap(banksClient: BanksClient, params: SwapParams) { tokenBMint, referralTokenAccount, }) + .remainingAccounts( + // TODO should check condition to add this in remaning accounts + [ + { + isSigner: false, + isWritable: false, + pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, + }, + ] + ) .transaction(); + return transaction; +} + +export async function swap(banksClient: BanksClient, params: SwapParams) { + const transaction = await swapInstruction(banksClient, params); + transaction.recentBlockhash = (await banksClient.getLatestBlockhash())[0]; - transaction.sign(payer); + transaction.sign(params.payer); await processTransactionMaybeThrow(banksClient, transaction); } diff --git a/tests/bankrun-utils/idl/alpha_vault.json b/tests/bankrun-utils/idl/alpha_vault.json new file mode 100644 index 00000000..d0862813 --- /dev/null +++ b/tests/bankrun-utils/idl/alpha_vault.json @@ -0,0 +1,3875 @@ +{ + "address": "SNPmGgnywBvvrAKMLundzG6StojyHTHDLu7T4sdhP4k", + "metadata": { + "name": "alpha_vault", + "version": "0.4.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "instructions": [ + { + "name": "claim_token", + "discriminator": [ + 116, + 206, + 27, + 191, + 166, + 19, + 0, + 73 + ], + "accounts": [ + { + "name": "vault", + "writable": true, + "relations": [ + "escrow" + ] + }, + { + "name": "escrow", + "writable": true + }, + { + "name": "token_out_vault", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "destination_token", + "writable": true + }, + { + "name": "token_mint" + }, + { + "name": "token_program" + }, + { + "name": "owner", + "signer": true, + "relations": [ + "escrow" + ] + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "close_crank_fee_whitelist", + "discriminator": [ + 189, + 166, + 73, + 241, + 81, + 12, + 246, + 170 + ], + "accounts": [ + { + "name": "crank_fee_whitelist", + "writable": true + }, + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "rent_receiver", + "writable": true + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "close_escrow", + "discriminator": [ + 139, + 171, + 94, + 146, + 191, + 91, + 144, + 50 + ], + "accounts": [ + { + "name": "vault", + "writable": true, + "relations": [ + "escrow" + ] + }, + { + "name": "escrow", + "writable": true + }, + { + "name": "owner", + "signer": true, + "relations": [ + "escrow" + ] + }, + { + "name": "rent_receiver", + "writable": true + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "close_fcfs_config", + "discriminator": [ + 48, + 178, + 212, + 101, + 23, + 138, + 233, + 90 + ], + "accounts": [ + { + "name": "config", + "writable": true + }, + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "rent_receiver", + "writable": true + } + ], + "args": [] + }, + { + "name": "close_prorata_config", + "discriminator": [ + 84, + 140, + 103, + 57, + 178, + 155, + 57, + 26 + ], + "accounts": [ + { + "name": "config", + "writable": true + }, + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "rent_receiver", + "writable": true + } + ], + "args": [] + }, + { + "name": "create_crank_fee_whitelist", + "discriminator": [ + 120, + 91, + 25, + 162, + 211, + 27, + 100, + 199 + ], + "accounts": [ + { + "name": "crank_fee_whitelist", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 114, + 97, + 110, + 107, + 95, + 102, + 101, + 101, + 95, + 119, + 104, + 105, + 116, + 101, + 108, + 105, + 115, + 116 + ] + }, + { + "kind": "account", + "path": "cranker" + } + ] + } + }, + { + "name": "cranker" + }, + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "create_fcfs_config", + "discriminator": [ + 7, + 255, + 242, + 242, + 1, + 99, + 179, + 12 + ], + "accounts": [ + { + "name": "config", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 99, + 102, + 115, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "arg", + "path": "config_parameters.index" + } + ] + } + }, + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "config_parameters", + "type": { + "defined": { + "name": "FcfsConfigParameters" + } + } + } + ] + }, + { + "name": "create_merkle_root_config", + "discriminator": [ + 55, + 243, + 253, + 240, + 78, + 186, + 232, + 166 + ], + "accounts": [ + { + "name": "vault" + }, + { + "name": "merkle_root_config", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 109, + 101, + 114, + 107, + 108, + 101, + 95, + 114, + 111, + 111, + 116 + ] + }, + { + "kind": "account", + "path": "vault" + }, + { + "kind": "arg", + "path": "params.version" + } + ] + } + }, + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "CreateMerkleRootConfigParams" + } + } + } + ] + }, + { + "name": "create_new_escrow", + "discriminator": [ + 60, + 154, + 170, + 202, + 252, + 109, + 83, + 199 + ], + "accounts": [ + { + "name": "vault", + "writable": true + }, + { + "name": "pool", + "relations": [ + "vault" + ] + }, + { + "name": "escrow", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119 + ] + }, + { + "kind": "account", + "path": "vault" + }, + { + "kind": "account", + "path": "owner" + } + ] + } + }, + { + "name": "owner" + }, + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "escrow_fee_receiver", + "writable": true, + "optional": true, + "address": "BJQbRiRWhJCyTYZcAuAL3ngDCx3AyFQGKDq8zhiZAKUw" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "create_permissioned_escrow", + "discriminator": [ + 60, + 166, + 36, + 85, + 96, + 137, + 132, + 184 + ], + "accounts": [ + { + "name": "vault", + "writable": true, + "relations": [ + "merkle_root_config" + ] + }, + { + "name": "pool", + "relations": [ + "vault" + ] + }, + { + "name": "escrow", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119 + ] + }, + { + "kind": "account", + "path": "vault" + }, + { + "kind": "account", + "path": "owner" + } + ] + } + }, + { + "name": "owner" + }, + { + "name": "merkle_root_config", + "docs": [ + "merkle_root_config" + ] + }, + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "escrow_fee_receiver", + "writable": true, + "optional": true, + "address": "BJQbRiRWhJCyTYZcAuAL3ngDCx3AyFQGKDq8zhiZAKUw" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "max_cap", + "type": "u64" + }, + { + "name": "proof", + "type": { + "vec": { + "array": [ + "u8", + 32 + ] + } + } + } + ] + }, + { + "name": "create_permissioned_escrow_with_authority", + "discriminator": [ + 211, + 231, + 194, + 69, + 65, + 11, + 123, + 93 + ], + "accounts": [ + { + "name": "vault", + "writable": true + }, + { + "name": "pool", + "relations": [ + "vault" + ] + }, + { + "name": "escrow", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119 + ] + }, + { + "kind": "account", + "path": "vault" + }, + { + "kind": "account", + "path": "owner" + } + ] + } + }, + { + "name": "owner" + }, + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "max_cap", + "type": "u64" + } + ] + }, + { + "name": "create_prorata_config", + "discriminator": [ + 38, + 203, + 72, + 231, + 103, + 29, + 195, + 61 + ], + "accounts": [ + { + "name": "config", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 114, + 111, + 114, + 97, + 116, + 97, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "arg", + "path": "config_parameters.index" + } + ] + } + }, + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "config_parameters", + "type": { + "defined": { + "name": "ProrataConfigParameters" + } + } + } + ] + }, + { + "name": "deposit", + "discriminator": [ + 242, + 35, + 198, + 137, + 82, + 225, + 242, + 182 + ], + "accounts": [ + { + "name": "vault", + "writable": true, + "relations": [ + "escrow" + ] + }, + { + "name": "pool", + "relations": [ + "vault" + ] + }, + { + "name": "escrow", + "writable": true + }, + { + "name": "source_token", + "writable": true + }, + { + "name": "token_vault", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "token_mint" + }, + { + "name": "token_program" + }, + { + "name": "owner", + "signer": true, + "relations": [ + "escrow" + ] + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "max_amount", + "type": "u64" + } + ] + }, + { + "name": "fill_damm_v2", + "discriminator": [ + 221, + 175, + 108, + 48, + 19, + 204, + 125, + 23 + ], + "accounts": [ + { + "name": "vault", + "writable": true + }, + { + "name": "token_vault", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "token_out_vault", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "amm_program", + "address": "cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG" + }, + { + "name": "pool_authority" + }, + { + "name": "pool", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "token_a_vault", + "writable": true + }, + { + "name": "token_b_vault", + "writable": true + }, + { + "name": "token_a_mint" + }, + { + "name": "token_b_mint" + }, + { + "name": "token_a_program" + }, + { + "name": "token_b_program" + }, + { + "name": "damm_event_authority" + }, + { + "name": "crank_fee_whitelist", + "optional": true + }, + { + "name": "crank_fee_receiver", + "writable": true, + "optional": true, + "address": "BJQbRiRWhJCyTYZcAuAL3ngDCx3AyFQGKDq8zhiZAKUw" + }, + { + "name": "cranker", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "max_amount", + "type": "u64" + } + ] + }, + { + "name": "fill_dlmm", + "discriminator": [ + 1, + 108, + 141, + 11, + 4, + 126, + 251, + 222 + ], + "accounts": [ + { + "name": "vault", + "writable": true + }, + { + "name": "token_vault", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "token_out_vault", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "amm_program", + "address": "LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo" + }, + { + "name": "pool", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "bin_array_bitmap_extension" + }, + { + "name": "reserve_x", + "writable": true + }, + { + "name": "reserve_y", + "writable": true + }, + { + "name": "token_x_mint" + }, + { + "name": "token_y_mint" + }, + { + "name": "oracle", + "writable": true + }, + { + "name": "token_x_program" + }, + { + "name": "token_y_program" + }, + { + "name": "dlmm_event_authority" + }, + { + "name": "crank_fee_whitelist", + "optional": true + }, + { + "name": "crank_fee_receiver", + "writable": true, + "optional": true, + "address": "BJQbRiRWhJCyTYZcAuAL3ngDCx3AyFQGKDq8zhiZAKUw" + }, + { + "name": "cranker", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "memo_program" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "max_amount", + "type": "u64" + }, + { + "name": "remaining_accounts_info", + "type": { + "defined": { + "name": "RemainingAccountsInfo" + } + } + } + ] + }, + { + "name": "fill_dynamic_amm", + "discriminator": [ + 224, + 226, + 223, + 80, + 36, + 50, + 70, + 231 + ], + "accounts": [ + { + "name": "vault", + "writable": true + }, + { + "name": "token_vault", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "token_out_vault", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "amm_program", + "address": "Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB" + }, + { + "name": "pool", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "a_vault", + "writable": true + }, + { + "name": "b_vault", + "writable": true + }, + { + "name": "a_token_vault", + "writable": true + }, + { + "name": "b_token_vault", + "writable": true + }, + { + "name": "a_vault_lp_mint", + "writable": true + }, + { + "name": "b_vault_lp_mint", + "writable": true + }, + { + "name": "a_vault_lp", + "writable": true + }, + { + "name": "b_vault_lp", + "writable": true + }, + { + "name": "admin_token_fee", + "writable": true + }, + { + "name": "vault_program" + }, + { + "name": "token_program" + }, + { + "name": "crank_fee_whitelist", + "optional": true + }, + { + "name": "crank_fee_receiver", + "writable": true, + "optional": true, + "address": "BJQbRiRWhJCyTYZcAuAL3ngDCx3AyFQGKDq8zhiZAKUw" + }, + { + "name": "cranker", + "writable": true, + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "max_amount", + "type": "u64" + } + ] + }, + { + "name": "initialize_fcfs_vault", + "discriminator": [ + 163, + 205, + 69, + 145, + 235, + 71, + 47, + 21 + ], + "accounts": [ + { + "name": "vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "base" + }, + { + "kind": "account", + "path": "pool" + } + ] + } + }, + { + "name": "pool" + }, + { + "name": "funder", + "writable": true, + "signer": true + }, + { + "name": "base", + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "InitializeFcfsVaultParams" + } + } + } + ] + }, + { + "name": "initialize_prorata_vault", + "discriminator": [ + 178, + 180, + 176, + 247, + 128, + 186, + 43, + 9 + ], + "accounts": [ + { + "name": "vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "base" + }, + { + "kind": "account", + "path": "pool" + } + ] + } + }, + { + "name": "pool" + }, + { + "name": "funder", + "writable": true, + "signer": true + }, + { + "name": "base", + "signer": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "InitializeProrataVaultParams" + } + } + } + ] + }, + { + "name": "initialize_vault_with_fcfs_config", + "discriminator": [ + 189, + 251, + 92, + 104, + 235, + 21, + 81, + 182 + ], + "accounts": [ + { + "name": "vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "config" + }, + { + "kind": "account", + "path": "pool" + } + ] + } + }, + { + "name": "pool" + }, + { + "name": "quote_mint" + }, + { + "name": "funder", + "writable": true, + "signer": true + }, + { + "name": "config" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "InitializeVaultWithConfigParams" + } + } + } + ] + }, + { + "name": "initialize_vault_with_prorata_config", + "discriminator": [ + 155, + 216, + 34, + 162, + 103, + 242, + 236, + 211 + ], + "accounts": [ + { + "name": "vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "config" + }, + { + "kind": "account", + "path": "pool" + } + ] + } + }, + { + "name": "pool" + }, + { + "name": "quote_mint" + }, + { + "name": "funder", + "writable": true, + "signer": true + }, + { + "name": "config" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "InitializeVaultWithConfigParams" + } + } + } + ] + }, + { + "name": "transfer_vault_authority", + "discriminator": [ + 139, + 35, + 83, + 88, + 52, + 186, + 162, + 110 + ], + "accounts": [ + { + "name": "vault", + "writable": true + }, + { + "name": "vault_authority", + "signer": true, + "relations": [ + "vault" + ] + } + ], + "args": [ + { + "name": "new_authority", + "type": "pubkey" + } + ] + }, + { + "name": "update_fcfs_vault_parameters", + "discriminator": [ + 172, + 23, + 13, + 143, + 18, + 133, + 104, + 174 + ], + "accounts": [ + { + "name": "vault", + "writable": true + }, + { + "name": "pool", + "relations": [ + "vault" + ] + }, + { + "name": "admin", + "signer": true + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdateFcfsVaultParams" + } + } + } + ] + }, + { + "name": "update_prorata_vault_parameters", + "discriminator": [ + 177, + 39, + 151, + 50, + 253, + 249, + 5, + 74 + ], + "accounts": [ + { + "name": "vault", + "writable": true + }, + { + "name": "pool", + "relations": [ + "vault" + ] + }, + { + "name": "admin", + "signer": true + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdateProrataVaultParams" + } + } + } + ] + }, + { + "name": "withdraw", + "discriminator": [ + 183, + 18, + 70, + 156, + 148, + 109, + 161, + 34 + ], + "accounts": [ + { + "name": "vault", + "writable": true, + "relations": [ + "escrow" + ] + }, + { + "name": "pool", + "relations": [ + "vault" + ] + }, + { + "name": "escrow", + "writable": true + }, + { + "name": "destination_token", + "writable": true + }, + { + "name": "token_vault", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "token_mint" + }, + { + "name": "token_program" + }, + { + "name": "owner", + "signer": true, + "relations": [ + "escrow" + ] + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "withdraw_remaining_quote", + "discriminator": [ + 54, + 253, + 188, + 34, + 100, + 145, + 59, + 127 + ], + "accounts": [ + { + "name": "vault", + "writable": true, + "relations": [ + "escrow" + ] + }, + { + "name": "pool", + "relations": [ + "vault" + ] + }, + { + "name": "escrow", + "writable": true + }, + { + "name": "token_vault", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "destination_token", + "writable": true + }, + { + "name": "token_mint" + }, + { + "name": "token_program" + }, + { + "name": "owner", + "signer": true, + "relations": [ + "escrow" + ] + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + } + ], + "accounts": [ + { + "name": "CrankFeeWhitelist", + "discriminator": [ + 39, + 105, + 184, + 30, + 248, + 231, + 176, + 133 + ] + }, + { + "name": "Escrow", + "discriminator": [ + 31, + 213, + 123, + 187, + 186, + 22, + 218, + 155 + ] + }, + { + "name": "FcfsVaultConfig", + "discriminator": [ + 99, + 243, + 252, + 122, + 160, + 175, + 130, + 52 + ] + }, + { + "name": "MerkleRootConfig", + "discriminator": [ + 103, + 2, + 222, + 217, + 73, + 50, + 187, + 39 + ] + }, + { + "name": "ProrataVaultConfig", + "discriminator": [ + 93, + 214, + 205, + 104, + 119, + 9, + 51, + 152 + ] + }, + { + "name": "Vault", + "discriminator": [ + 211, + 8, + 232, + 43, + 2, + 152, + 117, + 119 + ] + } + ], + "events": [ + { + "name": "CrankFeeWhitelistClosed", + "discriminator": [ + 157, + 171, + 85, + 155, + 37, + 20, + 41, + 114 + ] + }, + { + "name": "CrankFeeWhitelistCreated", + "discriminator": [ + 176, + 138, + 32, + 77, + 129, + 74, + 137, + 244 + ] + }, + { + "name": "EscrowClaimToken", + "discriminator": [ + 179, + 72, + 71, + 30, + 59, + 19, + 170, + 3 + ] + }, + { + "name": "EscrowClosed", + "discriminator": [ + 109, + 20, + 57, + 51, + 217, + 118, + 3, + 173 + ] + }, + { + "name": "EscrowCreated", + "discriminator": [ + 70, + 127, + 105, + 102, + 92, + 97, + 7, + 173 + ] + }, + { + "name": "EscrowDeposit", + "discriminator": [ + 43, + 90, + 49, + 176, + 134, + 148, + 50, + 32 + ] + }, + { + "name": "EscrowRemainingWithdraw", + "discriminator": [ + 113, + 14, + 156, + 89, + 113, + 79, + 88, + 178 + ] + }, + { + "name": "EscrowWithdraw", + "discriminator": [ + 171, + 17, + 164, + 116, + 122, + 66, + 183, + 34 + ] + }, + { + "name": "FcfsVaultCreated", + "discriminator": [ + 73, + 153, + 165, + 103, + 151, + 182, + 184, + 136 + ] + }, + { + "name": "FcfsVaultParametersUpdated", + "discriminator": [ + 78, + 112, + 112, + 62, + 193, + 209, + 231, + 226 + ] + }, + { + "name": "MerkleRootConfigCreated", + "discriminator": [ + 121, + 112, + 42, + 76, + 144, + 131, + 142, + 90 + ] + }, + { + "name": "ProrataVaultCreated", + "discriminator": [ + 181, + 255, + 162, + 226, + 203, + 199, + 193, + 6 + ] + }, + { + "name": "ProrataVaultParametersUpdated", + "discriminator": [ + 24, + 147, + 160, + 237, + 132, + 87, + 15, + 206 + ] + }, + { + "name": "SwapFill", + "discriminator": [ + 116, + 212, + 73, + 222, + 33, + 244, + 134, + 148 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "TimePointNotInFuture", + "msg": "Time point is not in future" + }, + { + "code": 6001, + "name": "IncorrectTokenMint", + "msg": "Token mint is incorrect" + }, + { + "code": 6002, + "name": "IncorrectPairType", + "msg": "Pair is not permissioned" + }, + { + "code": 6003, + "name": "PoolHasStarted", + "msg": "Pool has started" + }, + { + "code": 6004, + "name": "NotPermitThisActionInThisTimePoint", + "msg": "This action is not permitted in this time point" + }, + { + "code": 6005, + "name": "TheSaleIsOngoing", + "msg": "The sale is on going, cannot withdraw" + }, + { + "code": 6006, + "name": "EscrowIsNotClosable", + "msg": "Escrow is not closable" + }, + { + "code": 6007, + "name": "TimePointOrdersAreIncorrect", + "msg": "Time point orders are incorrect" + }, + { + "code": 6008, + "name": "EscrowHasRefuned", + "msg": "Escrow has refunded" + }, + { + "code": 6009, + "name": "MathOverflow", + "msg": "Math operation overflow" + }, + { + "code": 6010, + "name": "MaxBuyingCapIsZero", + "msg": "Max buying cap is zero" + }, + { + "code": 6011, + "name": "MaxAmountIsTooSmall", + "msg": "Max amount is too small" + }, + { + "code": 6012, + "name": "PoolTypeIsNotSupported", + "msg": "Pool type is not supported" + }, + { + "code": 6013, + "name": "InvalidAdmin", + "msg": "Invalid admin" + }, + { + "code": 6014, + "name": "VaultModeIsIncorrect", + "msg": "Vault mode is incorrect" + }, + { + "code": 6015, + "name": "MaxDepositingCapIsInValid", + "msg": "Max depositing cap is invalid" + }, + { + "code": 6016, + "name": "VestingDurationIsInValid", + "msg": "Vesting duration is invalid" + }, + { + "code": 6017, + "name": "DepositAmountIsZero", + "msg": "Deposit amount is zero" + }, + { + "code": 6018, + "name": "PoolOwnerIsMismatched", + "msg": "Pool owner is mismatched" + }, + { + "code": 6019, + "name": "RefundAmountIsZero", + "msg": "Refund amount is zero" + }, + { + "code": 6020, + "name": "DepositingDurationIsInvalid", + "msg": "Depositing duration is invalid" + }, + { + "code": 6021, + "name": "DepositingTimePointIsInvalid", + "msg": "Depositing time point is invalid" + }, + { + "code": 6022, + "name": "IndividualDepositingCapIsZero", + "msg": "Individual depositing cap is zero" + }, + { + "code": 6023, + "name": "InvalidFeeReceiverAccount", + "msg": "Invalid fee receiver account" + }, + { + "code": 6024, + "name": "NotPermissionedVault", + "msg": "Not permissioned vault" + }, + { + "code": 6025, + "name": "NotPermitToDoThisAction", + "msg": "Not permit to do this action" + }, + { + "code": 6026, + "name": "InvalidProof", + "msg": "Invalid Merkle proof" + }, + { + "code": 6027, + "name": "InvalidActivationType", + "msg": "Invalid activation type" + }, + { + "code": 6028, + "name": "ActivationTypeIsMismatched", + "msg": "Activation type is mismatched" + }, + { + "code": 6029, + "name": "InvalidPool", + "msg": "Pool is not connected to the alpha vault" + }, + { + "code": 6030, + "name": "InvalidCreator", + "msg": "Invalid creator" + }, + { + "code": 6031, + "name": "PermissionedVaultCannotChargeEscrowFee", + "msg": "Permissioned vault cannot charge escrow fee" + }, + { + "code": 6032, + "name": "EscrowFeeTooHigh", + "msg": "Escrow fee too high" + }, + { + "code": 6033, + "name": "LockDurationInvalid", + "msg": "Lock duration is invalid" + }, + { + "code": 6034, + "name": "MaxBuyingCapIsTooSmall", + "msg": "Max buying cap is too small" + }, + { + "code": 6035, + "name": "MaxDepositingCapIsTooSmall", + "msg": "Max depositing cap is too small" + }, + { + "code": 6036, + "name": "InvalidWhitelistWalletMode", + "msg": "Invalid whitelist wallet mode" + }, + { + "code": 6037, + "name": "InvalidCrankFeeWhitelist", + "msg": "Invalid crank fee whitelist" + }, + { + "code": 6038, + "name": "MissingFeeReceiver", + "msg": "Missing fee receiver" + }, + { + "code": 6039, + "name": "DiscriminatorIsMismatched", + "msg": "Discriminator is mismatched" + } + ], + "types": [ + { + "name": "AccountsType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "TransferHookX" + }, + { + "name": "TransferHookY" + }, + { + "name": "TransferHookReward" + } + ] + } + }, + { + "name": "CrankFeeWhitelist", + "serialization": "bytemuck", + "repr": { + "kind": "c" + }, + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "padding", + "type": { + "array": [ + "u128", + 5 + ] + } + } + ] + } + }, + { + "name": "CrankFeeWhitelistClosed", + "type": { + "kind": "struct", + "fields": [ + { + "name": "cranker", + "type": "pubkey" + } + ] + } + }, + { + "name": "CrankFeeWhitelistCreated", + "type": { + "kind": "struct", + "fields": [ + { + "name": "cranker", + "type": "pubkey" + } + ] + } + }, + { + "name": "CreateMerkleRootConfigParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "root", + "docs": [ + "The 256-bit merkle root." + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "version", + "docs": [ + "version" + ], + "type": "u64" + } + ] + } + }, + { + "name": "Escrow", + "serialization": "bytemuck", + "repr": { + "kind": "c" + }, + "type": { + "kind": "struct", + "fields": [ + { + "name": "vault", + "docs": [ + "vault address" + ], + "type": "pubkey" + }, + { + "name": "owner", + "docs": [ + "owner" + ], + "type": "pubkey" + }, + { + "name": "total_deposit", + "docs": [ + "total deposited quote token" + ], + "type": "u64" + }, + { + "name": "claimed_token", + "docs": [ + "Total token that escrow has claimed" + ], + "type": "u64" + }, + { + "name": "last_claimed_point", + "docs": [ + "Last claimed timestamp" + ], + "type": "u64" + }, + { + "name": "refunded", + "docs": [ + "Whether owner has claimed for remaining quote token" + ], + "type": "u8" + }, + { + "name": "padding_1", + "docs": [ + "padding 1" + ], + "type": { + "array": [ + "u8", + 7 + ] + } + }, + { + "name": "max_cap", + "docs": [ + "Only has meaning in permissioned vault" + ], + "type": "u64" + }, + { + "name": "padding_2", + "docs": [ + "padding 2" + ], + "type": { + "array": [ + "u8", + 8 + ] + } + }, + { + "name": "padding", + "type": { + "array": [ + "u128", + 1 + ] + } + } + ] + } + }, + { + "name": "EscrowClaimToken", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vault", + "type": "pubkey" + }, + { + "name": "escrow", + "type": "pubkey" + }, + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "vault_total_claimed_token", + "type": "u64" + } + ] + } + }, + { + "name": "EscrowClosed", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vault", + "type": "pubkey" + }, + { + "name": "escrow", + "type": "pubkey" + }, + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "vault_total_escrow", + "type": "u64" + } + ] + } + }, + { + "name": "EscrowCreated", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vault", + "type": "pubkey" + }, + { + "name": "escrow", + "type": "pubkey" + }, + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "vault_total_escrow", + "type": "u64" + }, + { + "name": "escrow_fee", + "type": "u64" + } + ] + } + }, + { + "name": "EscrowDeposit", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vault", + "type": "pubkey" + }, + { + "name": "escrow", + "type": "pubkey" + }, + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "vault_total_deposit", + "type": "u64" + } + ] + } + }, + { + "name": "EscrowRemainingWithdraw", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vault", + "type": "pubkey" + }, + { + "name": "escrow", + "type": "pubkey" + }, + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "vault_remaining_deposit", + "type": "u64" + } + ] + } + }, + { + "name": "EscrowWithdraw", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vault", + "type": "pubkey" + }, + { + "name": "escrow", + "type": "pubkey" + }, + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "vault_total_deposit", + "type": "u64" + } + ] + } + }, + { + "name": "FcfsConfigParameters", + "type": { + "kind": "struct", + "fields": [ + { + "name": "max_depositing_cap", + "type": "u64" + }, + { + "name": "start_vesting_duration", + "type": "u64" + }, + { + "name": "end_vesting_duration", + "type": "u64" + }, + { + "name": "depositing_duration_until_last_join_point", + "type": "u64" + }, + { + "name": "individual_depositing_cap", + "type": "u64" + }, + { + "name": "escrow_fee", + "type": "u64" + }, + { + "name": "activation_type", + "type": "u8" + }, + { + "name": "index", + "type": "u64" + } + ] + } + }, + { + "name": "FcfsVaultConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "max_depositing_cap", + "type": "u64" + }, + { + "name": "start_vesting_duration", + "type": "u64" + }, + { + "name": "end_vesting_duration", + "type": "u64" + }, + { + "name": "depositing_duration_until_last_join_point", + "type": "u64" + }, + { + "name": "individual_depositing_cap", + "type": "u64" + }, + { + "name": "escrow_fee", + "type": "u64" + }, + { + "name": "activation_type", + "type": "u8" + }, + { + "name": "_padding", + "type": { + "array": [ + "u8", + 175 + ] + } + } + ] + } + }, + { + "name": "FcfsVaultCreated", + "type": { + "kind": "struct", + "fields": [ + { + "name": "base_mint", + "type": "pubkey" + }, + { + "name": "quote_mint", + "type": "pubkey" + }, + { + "name": "start_vesting_point", + "type": "u64" + }, + { + "name": "end_vesting_point", + "type": "u64" + }, + { + "name": "max_depositing_cap", + "type": "u64" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "pool_type", + "type": "u8" + }, + { + "name": "depositing_point", + "type": "u64" + }, + { + "name": "individual_depositing_cap", + "type": "u64" + }, + { + "name": "escrow_fee", + "type": "u64" + }, + { + "name": "activation_type", + "type": "u8" + } + ] + } + }, + { + "name": "FcfsVaultParametersUpdated", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vault", + "type": "pubkey" + }, + { + "name": "max_depositing_cap", + "type": "u64" + }, + { + "name": "start_vesting_point", + "type": "u64" + }, + { + "name": "end_vesting_point", + "type": "u64" + }, + { + "name": "depositing_point", + "type": "u64" + }, + { + "name": "individual_depositing_cap", + "type": "u64" + } + ] + } + }, + { + "name": "InitializeFcfsVaultParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pool_type", + "type": "u8" + }, + { + "name": "quote_mint", + "type": "pubkey" + }, + { + "name": "base_mint", + "type": "pubkey" + }, + { + "name": "depositing_point", + "type": "u64" + }, + { + "name": "start_vesting_point", + "type": "u64" + }, + { + "name": "end_vesting_point", + "type": "u64" + }, + { + "name": "max_depositing_cap", + "type": "u64" + }, + { + "name": "individual_depositing_cap", + "type": "u64" + }, + { + "name": "escrow_fee", + "type": "u64" + }, + { + "name": "whitelist_mode", + "type": "u8" + } + ] + } + }, + { + "name": "InitializeProrataVaultParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pool_type", + "type": "u8" + }, + { + "name": "quote_mint", + "type": "pubkey" + }, + { + "name": "base_mint", + "type": "pubkey" + }, + { + "name": "depositing_point", + "type": "u64" + }, + { + "name": "start_vesting_point", + "type": "u64" + }, + { + "name": "end_vesting_point", + "type": "u64" + }, + { + "name": "max_buying_cap", + "type": "u64" + }, + { + "name": "escrow_fee", + "type": "u64" + }, + { + "name": "whitelist_mode", + "type": "u8" + } + ] + } + }, + { + "name": "InitializeVaultWithConfigParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pool_type", + "type": "u8" + }, + { + "name": "quote_mint", + "type": "pubkey" + }, + { + "name": "base_mint", + "type": "pubkey" + }, + { + "name": "whitelist_mode", + "type": "u8" + } + ] + } + }, + { + "name": "MerkleRootConfig", + "serialization": "bytemuck", + "repr": { + "kind": "c" + }, + "type": { + "kind": "struct", + "fields": [ + { + "name": "root", + "docs": [ + "The 256-bit merkle root." + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "vault", + "docs": [ + "vault pubkey that config is belong" + ], + "type": "pubkey" + }, + { + "name": "version", + "docs": [ + "version" + ], + "type": "u64" + }, + { + "name": "_padding", + "docs": [ + "padding for further use" + ], + "type": { + "array": [ + "u64", + 8 + ] + } + } + ] + } + }, + { + "name": "MerkleRootConfigCreated", + "type": { + "kind": "struct", + "fields": [ + { + "name": "admin", + "type": "pubkey" + }, + { + "name": "config", + "type": "pubkey" + }, + { + "name": "vault", + "type": "pubkey" + }, + { + "name": "version", + "type": "u64" + }, + { + "name": "root", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "ProrataConfigParameters", + "type": { + "kind": "struct", + "fields": [ + { + "name": "max_buying_cap", + "type": "u64" + }, + { + "name": "start_vesting_duration", + "type": "u64" + }, + { + "name": "end_vesting_duration", + "type": "u64" + }, + { + "name": "escrow_fee", + "type": "u64" + }, + { + "name": "activation_type", + "type": "u8" + }, + { + "name": "index", + "type": "u64" + } + ] + } + }, + { + "name": "ProrataVaultConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "max_buying_cap", + "type": "u64" + }, + { + "name": "start_vesting_duration", + "type": "u64" + }, + { + "name": "end_vesting_duration", + "type": "u64" + }, + { + "name": "escrow_fee", + "type": "u64" + }, + { + "name": "activation_type", + "type": "u8" + }, + { + "name": "_padding", + "type": { + "array": [ + "u8", + 191 + ] + } + } + ] + } + }, + { + "name": "ProrataVaultCreated", + "type": { + "kind": "struct", + "fields": [ + { + "name": "base_mint", + "type": "pubkey" + }, + { + "name": "quote_mint", + "type": "pubkey" + }, + { + "name": "start_vesting_point", + "type": "u64" + }, + { + "name": "end_vesting_point", + "type": "u64" + }, + { + "name": "max_buying_cap", + "type": "u64" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "pool_type", + "type": "u8" + }, + { + "name": "escrow_fee", + "type": "u64" + }, + { + "name": "activation_type", + "type": "u8" + } + ] + } + }, + { + "name": "ProrataVaultParametersUpdated", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vault", + "type": "pubkey" + }, + { + "name": "max_buying_cap", + "type": "u64" + }, + { + "name": "start_vesting_point", + "type": "u64" + }, + { + "name": "end_vesting_point", + "type": "u64" + } + ] + } + }, + { + "name": "RemainingAccountsInfo", + "type": { + "kind": "struct", + "fields": [ + { + "name": "slices", + "type": { + "vec": { + "defined": { + "name": "RemainingAccountsSlice" + } + } + } + } + ] + } + }, + { + "name": "RemainingAccountsSlice", + "type": { + "kind": "struct", + "fields": [ + { + "name": "accounts_type", + "type": { + "defined": { + "name": "AccountsType" + } + } + }, + { + "name": "length", + "type": "u8" + } + ] + } + }, + { + "name": "SwapFill", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vault", + "type": "pubkey" + }, + { + "name": "pair", + "type": "pubkey" + }, + { + "name": "fill_amount", + "type": "u64" + }, + { + "name": "purchased_amount", + "type": "u64" + }, + { + "name": "unfilled_amount", + "type": "u64" + } + ] + } + }, + { + "name": "UpdateFcfsVaultParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "max_depositing_cap", + "type": "u64" + }, + { + "name": "depositing_point", + "type": "u64" + }, + { + "name": "individual_depositing_cap", + "type": "u64" + }, + { + "name": "start_vesting_point", + "type": "u64" + }, + { + "name": "end_vesting_point", + "type": "u64" + } + ] + } + }, + { + "name": "UpdateProrataVaultParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "max_buying_cap", + "type": "u64" + }, + { + "name": "start_vesting_point", + "type": "u64" + }, + { + "name": "end_vesting_point", + "type": "u64" + } + ] + } + }, + { + "name": "Vault", + "serialization": "bytemuck", + "repr": { + "kind": "c" + }, + "type": { + "kind": "struct", + "fields": [ + { + "name": "pool", + "docs": [ + "pool" + ], + "type": "pubkey" + }, + { + "name": "token_vault", + "docs": [ + "reserve quote token" + ], + "type": "pubkey" + }, + { + "name": "token_out_vault", + "docs": [ + "reserve base token" + ], + "type": "pubkey" + }, + { + "name": "quote_mint", + "docs": [ + "quote token" + ], + "type": "pubkey" + }, + { + "name": "base_mint", + "docs": [ + "base token" + ], + "type": "pubkey" + }, + { + "name": "base", + "docs": [ + "base key" + ], + "type": "pubkey" + }, + { + "name": "owner", + "docs": [ + "owner key, deprecated field, can re-use in the future" + ], + "type": "pubkey" + }, + { + "name": "max_buying_cap", + "docs": [ + "max buying cap" + ], + "type": "u64" + }, + { + "name": "total_deposit", + "docs": [ + "total deposited quote token" + ], + "type": "u64" + }, + { + "name": "total_escrow", + "docs": [ + "total user deposit" + ], + "type": "u64" + }, + { + "name": "swapped_amount", + "docs": [ + "swapped_amount" + ], + "type": "u64" + }, + { + "name": "bought_token", + "docs": [ + "total bought token" + ], + "type": "u64" + }, + { + "name": "total_refund", + "docs": [ + "Total quote refund" + ], + "type": "u64" + }, + { + "name": "total_claimed_token", + "docs": [ + "Total claimed_token" + ], + "type": "u64" + }, + { + "name": "start_vesting_point", + "docs": [ + "Start vesting ts" + ], + "type": "u64" + }, + { + "name": "end_vesting_point", + "docs": [ + "End vesting ts" + ], + "type": "u64" + }, + { + "name": "bump", + "docs": [ + "bump" + ], + "type": "u8" + }, + { + "name": "pool_type", + "docs": [ + "pool type" + ], + "type": "u8" + }, + { + "name": "vault_mode", + "docs": [ + "vault mode" + ], + "type": "u8" + }, + { + "name": "padding_0", + "docs": [ + "padding 0" + ], + "type": { + "array": [ + "u8", + 5 + ] + } + }, + { + "name": "max_depositing_cap", + "docs": [ + "max depositing cap" + ], + "type": "u64" + }, + { + "name": "individual_depositing_cap", + "docs": [ + "individual depositing cap" + ], + "type": "u64" + }, + { + "name": "depositing_point", + "docs": [ + "depositing point" + ], + "type": "u64" + }, + { + "name": "escrow_fee", + "docs": [ + "flat fee when user open an escrow" + ], + "type": "u64" + }, + { + "name": "total_escrow_fee", + "docs": [ + "total escrow fee just for statistic" + ], + "type": "u64" + }, + { + "name": "whitelist_mode", + "docs": [ + "deposit whitelist mode" + ], + "type": "u8" + }, + { + "name": "activation_type", + "docs": [ + "activation type" + ], + "type": "u8" + }, + { + "name": "padding_1", + "docs": [ + "padding 1" + ], + "type": { + "array": [ + "u8", + 6 + ] + } + }, + { + "name": "vault_authority", + "docs": [ + "vault authority normally is vault creator, will be able to create merkle root config" + ], + "type": "pubkey" + }, + { + "name": "padding", + "type": { + "array": [ + "u128", + 5 + ] + } + } + ] + } + } + ] +} \ No newline at end of file diff --git a/tests/bankrun-utils/idl/alpha_vault.ts b/tests/bankrun-utils/idl/alpha_vault.ts new file mode 100644 index 00000000..63e4803f --- /dev/null +++ b/tests/bankrun-utils/idl/alpha_vault.ts @@ -0,0 +1,3881 @@ +/** + * Program IDL in camelCase format in order to be used in JS/TS. + * + * Note that this is only a type helper and is not the actual IDL. The original + * IDL can be found at `target/idl/alpha_vault.json`. + */ +export type AlphaVault = { + "address": "SNPmGgnywBvvrAKMLundzG6StojyHTHDLu7T4sdhP4k", + "metadata": { + "name": "alphaVault", + "version": "0.4.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "instructions": [ + { + "name": "claimToken", + "discriminator": [ + 116, + 206, + 27, + 191, + 166, + 19, + 0, + 73 + ], + "accounts": [ + { + "name": "vault", + "writable": true, + "relations": [ + "escrow" + ] + }, + { + "name": "escrow", + "writable": true + }, + { + "name": "tokenOutVault", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "destinationToken", + "writable": true + }, + { + "name": "tokenMint" + }, + { + "name": "tokenProgram" + }, + { + "name": "owner", + "signer": true, + "relations": [ + "escrow" + ] + }, + { + "name": "eventAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "closeCrankFeeWhitelist", + "discriminator": [ + 189, + 166, + 73, + 241, + 81, + 12, + 246, + 170 + ], + "accounts": [ + { + "name": "crankFeeWhitelist", + "writable": true + }, + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "rentReceiver", + "writable": true + }, + { + "name": "eventAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "closeEscrow", + "discriminator": [ + 139, + 171, + 94, + 146, + 191, + 91, + 144, + 50 + ], + "accounts": [ + { + "name": "vault", + "writable": true, + "relations": [ + "escrow" + ] + }, + { + "name": "escrow", + "writable": true + }, + { + "name": "owner", + "signer": true, + "relations": [ + "escrow" + ] + }, + { + "name": "rentReceiver", + "writable": true + }, + { + "name": "eventAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "closeFcfsConfig", + "discriminator": [ + 48, + 178, + 212, + 101, + 23, + 138, + 233, + 90 + ], + "accounts": [ + { + "name": "config", + "writable": true + }, + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "rentReceiver", + "writable": true + } + ], + "args": [] + }, + { + "name": "closeProrataConfig", + "discriminator": [ + 84, + 140, + 103, + 57, + 178, + 155, + 57, + 26 + ], + "accounts": [ + { + "name": "config", + "writable": true + }, + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "rentReceiver", + "writable": true + } + ], + "args": [] + }, + { + "name": "createCrankFeeWhitelist", + "discriminator": [ + 120, + 91, + 25, + 162, + 211, + 27, + 100, + 199 + ], + "accounts": [ + { + "name": "crankFeeWhitelist", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 114, + 97, + 110, + 107, + 95, + 102, + 101, + 101, + 95, + 119, + 104, + 105, + 116, + 101, + 108, + 105, + 115, + 116 + ] + }, + { + "kind": "account", + "path": "cranker" + } + ] + } + }, + { + "name": "cranker" + }, + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + }, + { + "name": "eventAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "createFcfsConfig", + "discriminator": [ + 7, + 255, + 242, + 242, + 1, + 99, + 179, + 12 + ], + "accounts": [ + { + "name": "config", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 102, + 99, + 102, + 115, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "arg", + "path": "config_parameters.index" + } + ] + } + }, + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "configParameters", + "type": { + "defined": { + "name": "fcfsConfigParameters" + } + } + } + ] + }, + { + "name": "createMerkleRootConfig", + "discriminator": [ + 55, + 243, + 253, + 240, + 78, + 186, + 232, + 166 + ], + "accounts": [ + { + "name": "vault" + }, + { + "name": "merkleRootConfig", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 109, + 101, + 114, + 107, + 108, + 101, + 95, + 114, + 111, + 111, + 116 + ] + }, + { + "kind": "account", + "path": "vault" + }, + { + "kind": "arg", + "path": "params.version" + } + ] + } + }, + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + }, + { + "name": "eventAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "createMerkleRootConfigParams" + } + } + } + ] + }, + { + "name": "createNewEscrow", + "discriminator": [ + 60, + 154, + 170, + 202, + 252, + 109, + 83, + 199 + ], + "accounts": [ + { + "name": "vault", + "writable": true + }, + { + "name": "pool", + "relations": [ + "vault" + ] + }, + { + "name": "escrow", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119 + ] + }, + { + "kind": "account", + "path": "vault" + }, + { + "kind": "account", + "path": "owner" + } + ] + } + }, + { + "name": "owner" + }, + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "escrowFeeReceiver", + "writable": true, + "optional": true, + "address": "BJQbRiRWhJCyTYZcAuAL3ngDCx3AyFQGKDq8zhiZAKUw" + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + }, + { + "name": "eventAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + }, + { + "name": "createPermissionedEscrow", + "discriminator": [ + 60, + 166, + 36, + 85, + 96, + 137, + 132, + 184 + ], + "accounts": [ + { + "name": "vault", + "writable": true, + "relations": [ + "merkleRootConfig" + ] + }, + { + "name": "pool", + "relations": [ + "vault" + ] + }, + { + "name": "escrow", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119 + ] + }, + { + "kind": "account", + "path": "vault" + }, + { + "kind": "account", + "path": "owner" + } + ] + } + }, + { + "name": "owner" + }, + { + "name": "merkleRootConfig", + "docs": [ + "merkleRootConfig" + ] + }, + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "escrowFeeReceiver", + "writable": true, + "optional": true, + "address": "BJQbRiRWhJCyTYZcAuAL3ngDCx3AyFQGKDq8zhiZAKUw" + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + }, + { + "name": "eventAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "maxCap", + "type": "u64" + }, + { + "name": "proof", + "type": { + "vec": { + "array": [ + "u8", + 32 + ] + } + } + } + ] + }, + { + "name": "createPermissionedEscrowWithAuthority", + "discriminator": [ + 211, + 231, + 194, + 69, + 65, + 11, + 123, + 93 + ], + "accounts": [ + { + "name": "vault", + "writable": true + }, + { + "name": "pool", + "relations": [ + "vault" + ] + }, + { + "name": "escrow", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119 + ] + }, + { + "kind": "account", + "path": "vault" + }, + { + "kind": "account", + "path": "owner" + } + ] + } + }, + { + "name": "owner" + }, + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + }, + { + "name": "eventAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "maxCap", + "type": "u64" + } + ] + }, + { + "name": "createProrataConfig", + "discriminator": [ + 38, + 203, + 72, + 231, + 103, + 29, + 195, + 61 + ], + "accounts": [ + { + "name": "config", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 114, + 111, + 114, + 97, + 116, + 97, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "arg", + "path": "config_parameters.index" + } + ] + } + }, + { + "name": "admin", + "writable": true, + "signer": true + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "configParameters", + "type": { + "defined": { + "name": "prorataConfigParameters" + } + } + } + ] + }, + { + "name": "deposit", + "discriminator": [ + 242, + 35, + 198, + 137, + 82, + 225, + 242, + 182 + ], + "accounts": [ + { + "name": "vault", + "writable": true, + "relations": [ + "escrow" + ] + }, + { + "name": "pool", + "relations": [ + "vault" + ] + }, + { + "name": "escrow", + "writable": true + }, + { + "name": "sourceToken", + "writable": true + }, + { + "name": "tokenVault", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "tokenMint" + }, + { + "name": "tokenProgram" + }, + { + "name": "owner", + "signer": true, + "relations": [ + "escrow" + ] + }, + { + "name": "eventAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "maxAmount", + "type": "u64" + } + ] + }, + { + "name": "fillDammV2", + "discriminator": [ + 221, + 175, + 108, + 48, + 19, + 204, + 125, + 23 + ], + "accounts": [ + { + "name": "vault", + "writable": true + }, + { + "name": "tokenVault", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "tokenOutVault", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "ammProgram", + "address": "cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG" + }, + { + "name": "poolAuthority" + }, + { + "name": "pool", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "tokenAVault", + "writable": true + }, + { + "name": "tokenBVault", + "writable": true + }, + { + "name": "tokenAMint" + }, + { + "name": "tokenBMint" + }, + { + "name": "tokenAProgram" + }, + { + "name": "tokenBProgram" + }, + { + "name": "dammEventAuthority" + }, + { + "name": "crankFeeWhitelist", + "optional": true + }, + { + "name": "crankFeeReceiver", + "writable": true, + "optional": true, + "address": "BJQbRiRWhJCyTYZcAuAL3ngDCx3AyFQGKDq8zhiZAKUw" + }, + { + "name": "cranker", + "writable": true, + "signer": true + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + }, + { + "name": "eventAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "maxAmount", + "type": "u64" + } + ] + }, + { + "name": "fillDlmm", + "discriminator": [ + 1, + 108, + 141, + 11, + 4, + 126, + 251, + 222 + ], + "accounts": [ + { + "name": "vault", + "writable": true + }, + { + "name": "tokenVault", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "tokenOutVault", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "ammProgram", + "address": "LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo" + }, + { + "name": "pool", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "binArrayBitmapExtension" + }, + { + "name": "reserveX", + "writable": true + }, + { + "name": "reserveY", + "writable": true + }, + { + "name": "tokenXMint" + }, + { + "name": "tokenYMint" + }, + { + "name": "oracle", + "writable": true + }, + { + "name": "tokenXProgram" + }, + { + "name": "tokenYProgram" + }, + { + "name": "dlmmEventAuthority" + }, + { + "name": "crankFeeWhitelist", + "optional": true + }, + { + "name": "crankFeeReceiver", + "writable": true, + "optional": true, + "address": "BJQbRiRWhJCyTYZcAuAL3ngDCx3AyFQGKDq8zhiZAKUw" + }, + { + "name": "cranker", + "writable": true, + "signer": true + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + }, + { + "name": "memoProgram" + }, + { + "name": "eventAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "maxAmount", + "type": "u64" + }, + { + "name": "remainingAccountsInfo", + "type": { + "defined": { + "name": "remainingAccountsInfo" + } + } + } + ] + }, + { + "name": "fillDynamicAmm", + "discriminator": [ + 224, + 226, + 223, + 80, + 36, + 50, + 70, + 231 + ], + "accounts": [ + { + "name": "vault", + "writable": true + }, + { + "name": "tokenVault", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "tokenOutVault", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "ammProgram", + "address": "Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB" + }, + { + "name": "pool", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "aVault", + "writable": true + }, + { + "name": "bVault", + "writable": true + }, + { + "name": "aTokenVault", + "writable": true + }, + { + "name": "bTokenVault", + "writable": true + }, + { + "name": "aVaultLpMint", + "writable": true + }, + { + "name": "bVaultLpMint", + "writable": true + }, + { + "name": "aVaultLp", + "writable": true + }, + { + "name": "bVaultLp", + "writable": true + }, + { + "name": "adminTokenFee", + "writable": true + }, + { + "name": "vaultProgram" + }, + { + "name": "tokenProgram" + }, + { + "name": "crankFeeWhitelist", + "optional": true + }, + { + "name": "crankFeeReceiver", + "writable": true, + "optional": true, + "address": "BJQbRiRWhJCyTYZcAuAL3ngDCx3AyFQGKDq8zhiZAKUw" + }, + { + "name": "cranker", + "writable": true, + "signer": true + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + }, + { + "name": "eventAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "maxAmount", + "type": "u64" + } + ] + }, + { + "name": "initializeFcfsVault", + "discriminator": [ + 163, + 205, + 69, + 145, + 235, + 71, + 47, + 21 + ], + "accounts": [ + { + "name": "vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "base" + }, + { + "kind": "account", + "path": "pool" + } + ] + } + }, + { + "name": "pool" + }, + { + "name": "funder", + "writable": true, + "signer": true + }, + { + "name": "base", + "signer": true + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + }, + { + "name": "eventAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "initializeFcfsVaultParams" + } + } + } + ] + }, + { + "name": "initializeProrataVault", + "discriminator": [ + 178, + 180, + 176, + 247, + 128, + 186, + 43, + 9 + ], + "accounts": [ + { + "name": "vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "base" + }, + { + "kind": "account", + "path": "pool" + } + ] + } + }, + { + "name": "pool" + }, + { + "name": "funder", + "writable": true, + "signer": true + }, + { + "name": "base", + "signer": true + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + }, + { + "name": "eventAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "initializeProrataVaultParams" + } + } + } + ] + }, + { + "name": "initializeVaultWithFcfsConfig", + "discriminator": [ + 189, + 251, + 92, + 104, + 235, + 21, + 81, + 182 + ], + "accounts": [ + { + "name": "vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "config" + }, + { + "kind": "account", + "path": "pool" + } + ] + } + }, + { + "name": "pool" + }, + { + "name": "quoteMint" + }, + { + "name": "funder", + "writable": true, + "signer": true + }, + { + "name": "config" + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + }, + { + "name": "eventAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "initializeVaultWithConfigParams" + } + } + } + ] + }, + { + "name": "initializeVaultWithProrataConfig", + "discriminator": [ + 155, + 216, + 34, + 162, + 103, + 242, + 236, + 211 + ], + "accounts": [ + { + "name": "vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "config" + }, + { + "kind": "account", + "path": "pool" + } + ] + } + }, + { + "name": "pool" + }, + { + "name": "quoteMint" + }, + { + "name": "funder", + "writable": true, + "signer": true + }, + { + "name": "config" + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111" + }, + { + "name": "eventAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "initializeVaultWithConfigParams" + } + } + } + ] + }, + { + "name": "transferVaultAuthority", + "discriminator": [ + 139, + 35, + 83, + 88, + 52, + 186, + 162, + 110 + ], + "accounts": [ + { + "name": "vault", + "writable": true + }, + { + "name": "vaultAuthority", + "signer": true, + "relations": [ + "vault" + ] + } + ], + "args": [ + { + "name": "newAuthority", + "type": "pubkey" + } + ] + }, + { + "name": "updateFcfsVaultParameters", + "discriminator": [ + 172, + 23, + 13, + 143, + 18, + 133, + 104, + 174 + ], + "accounts": [ + { + "name": "vault", + "writable": true + }, + { + "name": "pool", + "relations": [ + "vault" + ] + }, + { + "name": "admin", + "signer": true + }, + { + "name": "eventAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "updateFcfsVaultParams" + } + } + } + ] + }, + { + "name": "updateProrataVaultParameters", + "discriminator": [ + 177, + 39, + 151, + 50, + 253, + 249, + 5, + 74 + ], + "accounts": [ + { + "name": "vault", + "writable": true + }, + { + "name": "pool", + "relations": [ + "vault" + ] + }, + { + "name": "admin", + "signer": true + }, + { + "name": "eventAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "updateProrataVaultParams" + } + } + } + ] + }, + { + "name": "withdraw", + "discriminator": [ + 183, + 18, + 70, + 156, + 148, + 109, + 161, + 34 + ], + "accounts": [ + { + "name": "vault", + "writable": true, + "relations": [ + "escrow" + ] + }, + { + "name": "pool", + "relations": [ + "vault" + ] + }, + { + "name": "escrow", + "writable": true + }, + { + "name": "destinationToken", + "writable": true + }, + { + "name": "tokenVault", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "tokenMint" + }, + { + "name": "tokenProgram" + }, + { + "name": "owner", + "signer": true, + "relations": [ + "escrow" + ] + }, + { + "name": "eventAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "withdrawRemainingQuote", + "discriminator": [ + 54, + 253, + 188, + 34, + 100, + 145, + 59, + 127 + ], + "accounts": [ + { + "name": "vault", + "writable": true, + "relations": [ + "escrow" + ] + }, + { + "name": "pool", + "relations": [ + "vault" + ] + }, + { + "name": "escrow", + "writable": true + }, + { + "name": "tokenVault", + "writable": true, + "relations": [ + "vault" + ] + }, + { + "name": "destinationToken", + "writable": true + }, + { + "name": "tokenMint" + }, + { + "name": "tokenProgram" + }, + { + "name": "owner", + "signer": true, + "relations": [ + "escrow" + ] + }, + { + "name": "eventAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [] + } + ], + "accounts": [ + { + "name": "crankFeeWhitelist", + "discriminator": [ + 39, + 105, + 184, + 30, + 248, + 231, + 176, + 133 + ] + }, + { + "name": "escrow", + "discriminator": [ + 31, + 213, + 123, + 187, + 186, + 22, + 218, + 155 + ] + }, + { + "name": "fcfsVaultConfig", + "discriminator": [ + 99, + 243, + 252, + 122, + 160, + 175, + 130, + 52 + ] + }, + { + "name": "merkleRootConfig", + "discriminator": [ + 103, + 2, + 222, + 217, + 73, + 50, + 187, + 39 + ] + }, + { + "name": "prorataVaultConfig", + "discriminator": [ + 93, + 214, + 205, + 104, + 119, + 9, + 51, + 152 + ] + }, + { + "name": "vault", + "discriminator": [ + 211, + 8, + 232, + 43, + 2, + 152, + 117, + 119 + ] + } + ], + "events": [ + { + "name": "crankFeeWhitelistClosed", + "discriminator": [ + 157, + 171, + 85, + 155, + 37, + 20, + 41, + 114 + ] + }, + { + "name": "crankFeeWhitelistCreated", + "discriminator": [ + 176, + 138, + 32, + 77, + 129, + 74, + 137, + 244 + ] + }, + { + "name": "escrowClaimToken", + "discriminator": [ + 179, + 72, + 71, + 30, + 59, + 19, + 170, + 3 + ] + }, + { + "name": "escrowClosed", + "discriminator": [ + 109, + 20, + 57, + 51, + 217, + 118, + 3, + 173 + ] + }, + { + "name": "escrowCreated", + "discriminator": [ + 70, + 127, + 105, + 102, + 92, + 97, + 7, + 173 + ] + }, + { + "name": "escrowDeposit", + "discriminator": [ + 43, + 90, + 49, + 176, + 134, + 148, + 50, + 32 + ] + }, + { + "name": "escrowRemainingWithdraw", + "discriminator": [ + 113, + 14, + 156, + 89, + 113, + 79, + 88, + 178 + ] + }, + { + "name": "escrowWithdraw", + "discriminator": [ + 171, + 17, + 164, + 116, + 122, + 66, + 183, + 34 + ] + }, + { + "name": "fcfsVaultCreated", + "discriminator": [ + 73, + 153, + 165, + 103, + 151, + 182, + 184, + 136 + ] + }, + { + "name": "fcfsVaultParametersUpdated", + "discriminator": [ + 78, + 112, + 112, + 62, + 193, + 209, + 231, + 226 + ] + }, + { + "name": "merkleRootConfigCreated", + "discriminator": [ + 121, + 112, + 42, + 76, + 144, + 131, + 142, + 90 + ] + }, + { + "name": "prorataVaultCreated", + "discriminator": [ + 181, + 255, + 162, + 226, + 203, + 199, + 193, + 6 + ] + }, + { + "name": "prorataVaultParametersUpdated", + "discriminator": [ + 24, + 147, + 160, + 237, + 132, + 87, + 15, + 206 + ] + }, + { + "name": "swapFill", + "discriminator": [ + 116, + 212, + 73, + 222, + 33, + 244, + 134, + 148 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "timePointNotInFuture", + "msg": "Time point is not in future" + }, + { + "code": 6001, + "name": "incorrectTokenMint", + "msg": "Token mint is incorrect" + }, + { + "code": 6002, + "name": "incorrectPairType", + "msg": "Pair is not permissioned" + }, + { + "code": 6003, + "name": "poolHasStarted", + "msg": "Pool has started" + }, + { + "code": 6004, + "name": "notPermitThisActionInThisTimePoint", + "msg": "This action is not permitted in this time point" + }, + { + "code": 6005, + "name": "theSaleIsOngoing", + "msg": "The sale is on going, cannot withdraw" + }, + { + "code": 6006, + "name": "escrowIsNotClosable", + "msg": "Escrow is not closable" + }, + { + "code": 6007, + "name": "timePointOrdersAreIncorrect", + "msg": "Time point orders are incorrect" + }, + { + "code": 6008, + "name": "escrowHasRefuned", + "msg": "Escrow has refunded" + }, + { + "code": 6009, + "name": "mathOverflow", + "msg": "Math operation overflow" + }, + { + "code": 6010, + "name": "maxBuyingCapIsZero", + "msg": "Max buying cap is zero" + }, + { + "code": 6011, + "name": "maxAmountIsTooSmall", + "msg": "Max amount is too small" + }, + { + "code": 6012, + "name": "poolTypeIsNotSupported", + "msg": "Pool type is not supported" + }, + { + "code": 6013, + "name": "invalidAdmin", + "msg": "Invalid admin" + }, + { + "code": 6014, + "name": "vaultModeIsIncorrect", + "msg": "Vault mode is incorrect" + }, + { + "code": 6015, + "name": "maxDepositingCapIsInValid", + "msg": "Max depositing cap is invalid" + }, + { + "code": 6016, + "name": "vestingDurationIsInValid", + "msg": "Vesting duration is invalid" + }, + { + "code": 6017, + "name": "depositAmountIsZero", + "msg": "Deposit amount is zero" + }, + { + "code": 6018, + "name": "poolOwnerIsMismatched", + "msg": "Pool owner is mismatched" + }, + { + "code": 6019, + "name": "refundAmountIsZero", + "msg": "Refund amount is zero" + }, + { + "code": 6020, + "name": "depositingDurationIsInvalid", + "msg": "Depositing duration is invalid" + }, + { + "code": 6021, + "name": "depositingTimePointIsInvalid", + "msg": "Depositing time point is invalid" + }, + { + "code": 6022, + "name": "individualDepositingCapIsZero", + "msg": "Individual depositing cap is zero" + }, + { + "code": 6023, + "name": "invalidFeeReceiverAccount", + "msg": "Invalid fee receiver account" + }, + { + "code": 6024, + "name": "notPermissionedVault", + "msg": "Not permissioned vault" + }, + { + "code": 6025, + "name": "notPermitToDoThisAction", + "msg": "Not permit to do this action" + }, + { + "code": 6026, + "name": "invalidProof", + "msg": "Invalid Merkle proof" + }, + { + "code": 6027, + "name": "invalidActivationType", + "msg": "Invalid activation type" + }, + { + "code": 6028, + "name": "activationTypeIsMismatched", + "msg": "Activation type is mismatched" + }, + { + "code": 6029, + "name": "invalidPool", + "msg": "Pool is not connected to the alpha vault" + }, + { + "code": 6030, + "name": "invalidCreator", + "msg": "Invalid creator" + }, + { + "code": 6031, + "name": "permissionedVaultCannotChargeEscrowFee", + "msg": "Permissioned vault cannot charge escrow fee" + }, + { + "code": 6032, + "name": "escrowFeeTooHigh", + "msg": "Escrow fee too high" + }, + { + "code": 6033, + "name": "lockDurationInvalid", + "msg": "Lock duration is invalid" + }, + { + "code": 6034, + "name": "maxBuyingCapIsTooSmall", + "msg": "Max buying cap is too small" + }, + { + "code": 6035, + "name": "maxDepositingCapIsTooSmall", + "msg": "Max depositing cap is too small" + }, + { + "code": 6036, + "name": "invalidWhitelistWalletMode", + "msg": "Invalid whitelist wallet mode" + }, + { + "code": 6037, + "name": "invalidCrankFeeWhitelist", + "msg": "Invalid crank fee whitelist" + }, + { + "code": 6038, + "name": "missingFeeReceiver", + "msg": "Missing fee receiver" + }, + { + "code": 6039, + "name": "discriminatorIsMismatched", + "msg": "Discriminator is mismatched" + } + ], + "types": [ + { + "name": "accountsType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "transferHookX" + }, + { + "name": "transferHookY" + }, + { + "name": "transferHookReward" + } + ] + } + }, + { + "name": "crankFeeWhitelist", + "serialization": "bytemuck", + "repr": { + "kind": "c" + }, + "type": { + "kind": "struct", + "fields": [ + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "padding", + "type": { + "array": [ + "u128", + 5 + ] + } + } + ] + } + }, + { + "name": "crankFeeWhitelistClosed", + "type": { + "kind": "struct", + "fields": [ + { + "name": "cranker", + "type": "pubkey" + } + ] + } + }, + { + "name": "crankFeeWhitelistCreated", + "type": { + "kind": "struct", + "fields": [ + { + "name": "cranker", + "type": "pubkey" + } + ] + } + }, + { + "name": "createMerkleRootConfigParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "root", + "docs": [ + "The 256-bit merkle root." + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "version", + "docs": [ + "version" + ], + "type": "u64" + } + ] + } + }, + { + "name": "escrow", + "serialization": "bytemuck", + "repr": { + "kind": "c" + }, + "type": { + "kind": "struct", + "fields": [ + { + "name": "vault", + "docs": [ + "vault address" + ], + "type": "pubkey" + }, + { + "name": "owner", + "docs": [ + "owner" + ], + "type": "pubkey" + }, + { + "name": "totalDeposit", + "docs": [ + "total deposited quote token" + ], + "type": "u64" + }, + { + "name": "claimedToken", + "docs": [ + "Total token that escrow has claimed" + ], + "type": "u64" + }, + { + "name": "lastClaimedPoint", + "docs": [ + "Last claimed timestamp" + ], + "type": "u64" + }, + { + "name": "refunded", + "docs": [ + "Whether owner has claimed for remaining quote token" + ], + "type": "u8" + }, + { + "name": "padding1", + "docs": [ + "padding 1" + ], + "type": { + "array": [ + "u8", + 7 + ] + } + }, + { + "name": "maxCap", + "docs": [ + "Only has meaning in permissioned vault" + ], + "type": "u64" + }, + { + "name": "padding2", + "docs": [ + "padding 2" + ], + "type": { + "array": [ + "u8", + 8 + ] + } + }, + { + "name": "padding", + "type": { + "array": [ + "u128", + 1 + ] + } + } + ] + } + }, + { + "name": "escrowClaimToken", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vault", + "type": "pubkey" + }, + { + "name": "escrow", + "type": "pubkey" + }, + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "vaultTotalClaimedToken", + "type": "u64" + } + ] + } + }, + { + "name": "escrowClosed", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vault", + "type": "pubkey" + }, + { + "name": "escrow", + "type": "pubkey" + }, + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "vaultTotalEscrow", + "type": "u64" + } + ] + } + }, + { + "name": "escrowCreated", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vault", + "type": "pubkey" + }, + { + "name": "escrow", + "type": "pubkey" + }, + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "vaultTotalEscrow", + "type": "u64" + }, + { + "name": "escrowFee", + "type": "u64" + } + ] + } + }, + { + "name": "escrowDeposit", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vault", + "type": "pubkey" + }, + { + "name": "escrow", + "type": "pubkey" + }, + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "vaultTotalDeposit", + "type": "u64" + } + ] + } + }, + { + "name": "escrowRemainingWithdraw", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vault", + "type": "pubkey" + }, + { + "name": "escrow", + "type": "pubkey" + }, + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "vaultRemainingDeposit", + "type": "u64" + } + ] + } + }, + { + "name": "escrowWithdraw", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vault", + "type": "pubkey" + }, + { + "name": "escrow", + "type": "pubkey" + }, + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "vaultTotalDeposit", + "type": "u64" + } + ] + } + }, + { + "name": "fcfsConfigParameters", + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxDepositingCap", + "type": "u64" + }, + { + "name": "startVestingDuration", + "type": "u64" + }, + { + "name": "endVestingDuration", + "type": "u64" + }, + { + "name": "depositingDurationUntilLastJoinPoint", + "type": "u64" + }, + { + "name": "individualDepositingCap", + "type": "u64" + }, + { + "name": "escrowFee", + "type": "u64" + }, + { + "name": "activationType", + "type": "u8" + }, + { + "name": "index", + "type": "u64" + } + ] + } + }, + { + "name": "fcfsVaultConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxDepositingCap", + "type": "u64" + }, + { + "name": "startVestingDuration", + "type": "u64" + }, + { + "name": "endVestingDuration", + "type": "u64" + }, + { + "name": "depositingDurationUntilLastJoinPoint", + "type": "u64" + }, + { + "name": "individualDepositingCap", + "type": "u64" + }, + { + "name": "escrowFee", + "type": "u64" + }, + { + "name": "activationType", + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 175 + ] + } + } + ] + } + }, + { + "name": "fcfsVaultCreated", + "type": { + "kind": "struct", + "fields": [ + { + "name": "baseMint", + "type": "pubkey" + }, + { + "name": "quoteMint", + "type": "pubkey" + }, + { + "name": "startVestingPoint", + "type": "u64" + }, + { + "name": "endVestingPoint", + "type": "u64" + }, + { + "name": "maxDepositingCap", + "type": "u64" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "poolType", + "type": "u8" + }, + { + "name": "depositingPoint", + "type": "u64" + }, + { + "name": "individualDepositingCap", + "type": "u64" + }, + { + "name": "escrowFee", + "type": "u64" + }, + { + "name": "activationType", + "type": "u8" + } + ] + } + }, + { + "name": "fcfsVaultParametersUpdated", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vault", + "type": "pubkey" + }, + { + "name": "maxDepositingCap", + "type": "u64" + }, + { + "name": "startVestingPoint", + "type": "u64" + }, + { + "name": "endVestingPoint", + "type": "u64" + }, + { + "name": "depositingPoint", + "type": "u64" + }, + { + "name": "individualDepositingCap", + "type": "u64" + } + ] + } + }, + { + "name": "initializeFcfsVaultParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "poolType", + "type": "u8" + }, + { + "name": "quoteMint", + "type": "pubkey" + }, + { + "name": "baseMint", + "type": "pubkey" + }, + { + "name": "depositingPoint", + "type": "u64" + }, + { + "name": "startVestingPoint", + "type": "u64" + }, + { + "name": "endVestingPoint", + "type": "u64" + }, + { + "name": "maxDepositingCap", + "type": "u64" + }, + { + "name": "individualDepositingCap", + "type": "u64" + }, + { + "name": "escrowFee", + "type": "u64" + }, + { + "name": "whitelistMode", + "type": "u8" + } + ] + } + }, + { + "name": "initializeProrataVaultParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "poolType", + "type": "u8" + }, + { + "name": "quoteMint", + "type": "pubkey" + }, + { + "name": "baseMint", + "type": "pubkey" + }, + { + "name": "depositingPoint", + "type": "u64" + }, + { + "name": "startVestingPoint", + "type": "u64" + }, + { + "name": "endVestingPoint", + "type": "u64" + }, + { + "name": "maxBuyingCap", + "type": "u64" + }, + { + "name": "escrowFee", + "type": "u64" + }, + { + "name": "whitelistMode", + "type": "u8" + } + ] + } + }, + { + "name": "initializeVaultWithConfigParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "poolType", + "type": "u8" + }, + { + "name": "quoteMint", + "type": "pubkey" + }, + { + "name": "baseMint", + "type": "pubkey" + }, + { + "name": "whitelistMode", + "type": "u8" + } + ] + } + }, + { + "name": "merkleRootConfig", + "serialization": "bytemuck", + "repr": { + "kind": "c" + }, + "type": { + "kind": "struct", + "fields": [ + { + "name": "root", + "docs": [ + "The 256-bit merkle root." + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "vault", + "docs": [ + "vault pubkey that config is belong" + ], + "type": "pubkey" + }, + { + "name": "version", + "docs": [ + "version" + ], + "type": "u64" + }, + { + "name": "padding", + "docs": [ + "padding for further use" + ], + "type": { + "array": [ + "u64", + 8 + ] + } + } + ] + } + }, + { + "name": "merkleRootConfigCreated", + "type": { + "kind": "struct", + "fields": [ + { + "name": "admin", + "type": "pubkey" + }, + { + "name": "config", + "type": "pubkey" + }, + { + "name": "vault", + "type": "pubkey" + }, + { + "name": "version", + "type": "u64" + }, + { + "name": "root", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "prorataConfigParameters", + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxBuyingCap", + "type": "u64" + }, + { + "name": "startVestingDuration", + "type": "u64" + }, + { + "name": "endVestingDuration", + "type": "u64" + }, + { + "name": "escrowFee", + "type": "u64" + }, + { + "name": "activationType", + "type": "u8" + }, + { + "name": "index", + "type": "u64" + } + ] + } + }, + { + "name": "prorataVaultConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxBuyingCap", + "type": "u64" + }, + { + "name": "startVestingDuration", + "type": "u64" + }, + { + "name": "endVestingDuration", + "type": "u64" + }, + { + "name": "escrowFee", + "type": "u64" + }, + { + "name": "activationType", + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 191 + ] + } + } + ] + } + }, + { + "name": "prorataVaultCreated", + "type": { + "kind": "struct", + "fields": [ + { + "name": "baseMint", + "type": "pubkey" + }, + { + "name": "quoteMint", + "type": "pubkey" + }, + { + "name": "startVestingPoint", + "type": "u64" + }, + { + "name": "endVestingPoint", + "type": "u64" + }, + { + "name": "maxBuyingCap", + "type": "u64" + }, + { + "name": "pool", + "type": "pubkey" + }, + { + "name": "poolType", + "type": "u8" + }, + { + "name": "escrowFee", + "type": "u64" + }, + { + "name": "activationType", + "type": "u8" + } + ] + } + }, + { + "name": "prorataVaultParametersUpdated", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vault", + "type": "pubkey" + }, + { + "name": "maxBuyingCap", + "type": "u64" + }, + { + "name": "startVestingPoint", + "type": "u64" + }, + { + "name": "endVestingPoint", + "type": "u64" + } + ] + } + }, + { + "name": "remainingAccountsInfo", + "type": { + "kind": "struct", + "fields": [ + { + "name": "slices", + "type": { + "vec": { + "defined": { + "name": "remainingAccountsSlice" + } + } + } + } + ] + } + }, + { + "name": "remainingAccountsSlice", + "type": { + "kind": "struct", + "fields": [ + { + "name": "accountsType", + "type": { + "defined": { + "name": "accountsType" + } + } + }, + { + "name": "length", + "type": "u8" + } + ] + } + }, + { + "name": "swapFill", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vault", + "type": "pubkey" + }, + { + "name": "pair", + "type": "pubkey" + }, + { + "name": "fillAmount", + "type": "u64" + }, + { + "name": "purchasedAmount", + "type": "u64" + }, + { + "name": "unfilledAmount", + "type": "u64" + } + ] + } + }, + { + "name": "updateFcfsVaultParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxDepositingCap", + "type": "u64" + }, + { + "name": "depositingPoint", + "type": "u64" + }, + { + "name": "individualDepositingCap", + "type": "u64" + }, + { + "name": "startVestingPoint", + "type": "u64" + }, + { + "name": "endVestingPoint", + "type": "u64" + } + ] + } + }, + { + "name": "updateProrataVaultParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxBuyingCap", + "type": "u64" + }, + { + "name": "startVestingPoint", + "type": "u64" + }, + { + "name": "endVestingPoint", + "type": "u64" + } + ] + } + }, + { + "name": "vault", + "serialization": "bytemuck", + "repr": { + "kind": "c" + }, + "type": { + "kind": "struct", + "fields": [ + { + "name": "pool", + "docs": [ + "pool" + ], + "type": "pubkey" + }, + { + "name": "tokenVault", + "docs": [ + "reserve quote token" + ], + "type": "pubkey" + }, + { + "name": "tokenOutVault", + "docs": [ + "reserve base token" + ], + "type": "pubkey" + }, + { + "name": "quoteMint", + "docs": [ + "quote token" + ], + "type": "pubkey" + }, + { + "name": "baseMint", + "docs": [ + "base token" + ], + "type": "pubkey" + }, + { + "name": "base", + "docs": [ + "base key" + ], + "type": "pubkey" + }, + { + "name": "owner", + "docs": [ + "owner key, deprecated field, can re-use in the future" + ], + "type": "pubkey" + }, + { + "name": "maxBuyingCap", + "docs": [ + "max buying cap" + ], + "type": "u64" + }, + { + "name": "totalDeposit", + "docs": [ + "total deposited quote token" + ], + "type": "u64" + }, + { + "name": "totalEscrow", + "docs": [ + "total user deposit" + ], + "type": "u64" + }, + { + "name": "swappedAmount", + "docs": [ + "swappedAmount" + ], + "type": "u64" + }, + { + "name": "boughtToken", + "docs": [ + "total bought token" + ], + "type": "u64" + }, + { + "name": "totalRefund", + "docs": [ + "Total quote refund" + ], + "type": "u64" + }, + { + "name": "totalClaimedToken", + "docs": [ + "Total claimed_token" + ], + "type": "u64" + }, + { + "name": "startVestingPoint", + "docs": [ + "Start vesting ts" + ], + "type": "u64" + }, + { + "name": "endVestingPoint", + "docs": [ + "End vesting ts" + ], + "type": "u64" + }, + { + "name": "bump", + "docs": [ + "bump" + ], + "type": "u8" + }, + { + "name": "poolType", + "docs": [ + "pool type" + ], + "type": "u8" + }, + { + "name": "vaultMode", + "docs": [ + "vault mode" + ], + "type": "u8" + }, + { + "name": "padding0", + "docs": [ + "padding 0" + ], + "type": { + "array": [ + "u8", + 5 + ] + } + }, + { + "name": "maxDepositingCap", + "docs": [ + "max depositing cap" + ], + "type": "u64" + }, + { + "name": "individualDepositingCap", + "docs": [ + "individual depositing cap" + ], + "type": "u64" + }, + { + "name": "depositingPoint", + "docs": [ + "depositing point" + ], + "type": "u64" + }, + { + "name": "escrowFee", + "docs": [ + "flat fee when user open an escrow" + ], + "type": "u64" + }, + { + "name": "totalEscrowFee", + "docs": [ + "total escrow fee just for statistic" + ], + "type": "u64" + }, + { + "name": "whitelistMode", + "docs": [ + "deposit whitelist mode" + ], + "type": "u8" + }, + { + "name": "activationType", + "docs": [ + "activation type" + ], + "type": "u8" + }, + { + "name": "padding1", + "docs": [ + "padding 1" + ], + "type": { + "array": [ + "u8", + 6 + ] + } + }, + { + "name": "vaultAuthority", + "docs": [ + "vault authority normally is vault creator, will be able to create merkle root config" + ], + "type": "pubkey" + }, + { + "name": "padding", + "type": { + "array": [ + "u128", + 5 + ] + } + } + ] + } + } + ] +}; diff --git a/tests/bankrun-utils/math.ts b/tests/bankrun-utils/math.ts index 5329f2d1..06c6c986 100644 --- a/tests/bankrun-utils/math.ts +++ b/tests/bankrun-utils/math.ts @@ -1,6 +1,20 @@ import { BN } from "@coral-xyz/anchor"; +export enum Rounding { + Up, + Down, + } + export function shlDiv(x: BN, y: BN, offset: number) { return x.shln(offset).div(y); } +export function mulDiv(x: BN, y: BN, denominator: BN, rounding: Rounding): BN { + const { div, mod } = x.mul(y).divmod(denominator); + + if (rounding == Rounding.Up && !mod.isZero()) { + return div.add(new BN(1)); + } + return div; + } + diff --git a/tests/claimFee.test.ts b/tests/claimFee.test.ts index 96a76743..9114100c 100644 --- a/tests/claimFee.test.ts +++ b/tests/claimFee.test.ts @@ -1,5 +1,5 @@ import { ProgramTestContext } from "solana-bankrun"; -import { generateKpAndFund, randomID, startTest } from "./bankrun-utils/common"; +import { convertToByteArray, generateKpAndFund, randomID, startTest } from "./bankrun-utils/common"; import { Keypair, PublicKey } from "@solana/web3.js"; import { addLiquidity, @@ -102,10 +102,10 @@ describe("Claim fee", () => { poolFees: { baseFee: { cliffFeeNumerator: new BN(2_500_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: null, @@ -290,10 +290,10 @@ describe("Claim fee", () => { poolFees: { baseFee: { cliffFeeNumerator: new BN(2_500_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: null, diff --git a/tests/claimPositionFee.test.ts b/tests/claimPositionFee.test.ts index a3678e4f..57f57118 100644 --- a/tests/claimPositionFee.test.ts +++ b/tests/claimPositionFee.test.ts @@ -1,5 +1,5 @@ import { ProgramTestContext } from "solana-bankrun"; -import { generateKpAndFund, startTest } from "./bankrun-utils/common"; +import { convertToByteArray, generateKpAndFund, startTest } from "./bankrun-utils/common"; import { Keypair, PublicKey } from "@solana/web3.js"; import { addLiquidity, @@ -88,10 +88,10 @@ describe("Claim position fee", () => { poolFees: { baseFee: { cliffFeeNumerator: new BN(2_500_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: null, diff --git a/tests/createConfig.test.ts b/tests/createConfig.test.ts index d93d31ec..cef45efb 100644 --- a/tests/createConfig.test.ts +++ b/tests/createConfig.test.ts @@ -1,5 +1,5 @@ import { ProgramTestContext } from "solana-bankrun"; -import { generateKpAndFund, randomID, startTest } from "./bankrun-utils/common"; +import { convertToByteArray, generateKpAndFund, randomID, startTest } from "./bankrun-utils/common"; import { Keypair, PublicKey } from "@solana/web3.js"; import { BASIS_POINT_MAX, @@ -27,10 +27,10 @@ describe("Admin function: Create config", () => { poolFees: { baseFee: { cliffFeeNumerator: new BN(2_500_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: null, @@ -74,10 +74,10 @@ describe("Admin function: Create config", () => { poolFees: { baseFee: { cliffFeeNumerator: new BN(2_500_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: { diff --git a/tests/createCustomizePool.test.ts b/tests/createCustomizePool.test.ts index d5fbfe14..00ce027b 100644 --- a/tests/createCustomizePool.test.ts +++ b/tests/createCustomizePool.test.ts @@ -1,14 +1,15 @@ import { ProgramTestContext } from "solana-bankrun"; -import { generateKpAndFund, startTest } from "./bankrun-utils/common"; +import { convertToByteArray, generateKpAndFund, startTest } from "./bankrun-utils/common"; import { Keypair, PublicKey } from "@solana/web3.js"; import { - InitializeCustomizeablePoolParams, - initializeCustomizeablePool, + InitializeCustomizablePoolParams, + initializeCustomizablePool, MIN_LP_AMOUNT, MAX_SQRT_PRICE, MIN_SQRT_PRICE, mintSplTokenTo, createToken, + getPool, } from "./bankrun-utils"; import BN from "bn.js"; import { ExtensionType } from "@solana/spl-token"; @@ -17,6 +18,7 @@ import { createTransferFeeExtensionWithInstruction, mintToToken2022, } from "./bankrun-utils/token2022"; +import { expect } from "chai"; describe("Initialize customizable pool", () => { describe("SPL-Token", () => { @@ -59,7 +61,7 @@ describe("Initialize customizable pool", () => { }); it("Initialize customizeable pool with spl token", async () => { - const params: InitializeCustomizeablePoolParams = { + const params: InitializeCustomizablePoolParams = { payer: creator, creator: creator.publicKey, tokenAMint, @@ -73,10 +75,10 @@ describe("Initialize customizable pool", () => { poolFees: { baseFee: { cliffFeeNumerator: new BN(2_500_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: null, @@ -85,7 +87,7 @@ describe("Initialize customizable pool", () => { collectFeeMode: 0, }; - await initializeCustomizeablePool(context.banksClient, params); + await initializeCustomizablePool(context.banksClient, params); }); }); @@ -144,7 +146,7 @@ describe("Initialize customizable pool", () => { }); it("Initialize customizeable pool with spl token", async () => { - const params: InitializeCustomizeablePoolParams = { + const params: InitializeCustomizablePoolParams = { payer: creator, creator: creator.publicKey, tokenAMint, @@ -158,10 +160,10 @@ describe("Initialize customizable pool", () => { poolFees: { baseFee: { cliffFeeNumerator: new BN(2_500_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: null, @@ -170,7 +172,9 @@ describe("Initialize customizable pool", () => { collectFeeMode: 0, }; - await initializeCustomizeablePool(context.banksClient, params); + const { pool } = await initializeCustomizablePool(context.banksClient, params); + const poolState = await getPool(context.banksClient, pool); + expect(poolState.version).eq(0); }); }); }); diff --git a/tests/createPool.test.ts b/tests/createPool.test.ts index e928cea5..21ba01d6 100644 --- a/tests/createPool.test.ts +++ b/tests/createPool.test.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import { ProgramTestContext } from "solana-bankrun"; -import { generateKpAndFund, startTest } from "./bankrun-utils/common"; +import { convertToByteArray, generateKpAndFund, startTest } from "./bankrun-utils/common"; import { Keypair, PublicKey } from "@solana/web3.js"; import { createConfigIx, @@ -72,10 +72,10 @@ describe("Initialize pool", () => { poolFees: { baseFee: { cliffFeeNumerator: new BN(2_500_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: null, @@ -124,6 +124,7 @@ describe("Initialize pool", () => { }); const poolState = await getPool(context.banksClient, pool); expect(poolState.poolStatus).eq(newStatus); + expect(poolState.version).eq(0); }); }); @@ -192,10 +193,10 @@ describe("Initialize pool", () => { poolFees: { baseFee: { cliffFeeNumerator: new BN(2_500_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: null, @@ -244,6 +245,7 @@ describe("Initialize pool", () => { }); const poolState = await getPool(context.banksClient, pool); expect(poolState.poolStatus).eq(newStatus); + expect(poolState.version).eq(0); }); }); }); diff --git a/tests/createPosition.test.ts b/tests/createPosition.test.ts index 71cd67a3..ffea666c 100644 --- a/tests/createPosition.test.ts +++ b/tests/createPosition.test.ts @@ -1,5 +1,5 @@ import { ProgramTestContext } from "solana-bankrun"; -import { generateKpAndFund, randomID, startTest } from "./bankrun-utils/common"; +import { convertToByteArray, generateKpAndFund, randomID, startTest } from "./bankrun-utils/common"; import { Keypair, PublicKey } from "@solana/web3.js"; import { createConfigIx, @@ -74,10 +74,10 @@ describe("Create position", () => { poolFees: { baseFee: { cliffFeeNumerator: new BN(2_500_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: null, @@ -185,10 +185,10 @@ describe("Create position", () => { poolFees: { baseFee: { cliffFeeNumerator: new BN(2_500_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: null, diff --git a/tests/dynamicConfig.test.ts b/tests/dynamicConfig.test.ts index 284d38ca..d6ef3432 100644 --- a/tests/dynamicConfig.test.ts +++ b/tests/dynamicConfig.test.ts @@ -1,5 +1,5 @@ import { ProgramTestContext } from "solana-bankrun"; -import { generateKpAndFund, startTest } from "./bankrun-utils/common"; +import { convertToByteArray, generateKpAndFund, startTest } from "./bankrun-utils/common"; import { Keypair, PublicKey } from "@solana/web3.js"; import { MIN_LP_AMOUNT, @@ -11,8 +11,10 @@ import { CreateDynamicConfigParams, InitializePoolWithCustomizeConfigParams, initializePoolWithCustomizeConfig, + getPool, } from "./bankrun-utils"; import BN from "bn.js"; +import { expect } from "chai"; describe("Dynamic config test", () => { let context: ProgramTestContext; @@ -87,10 +89,10 @@ describe("Dynamic config test", () => { poolFees: { baseFee: { cliffFeeNumerator: new BN(2_500_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: null, @@ -99,6 +101,8 @@ describe("Dynamic config test", () => { collectFeeMode: 0, }; - await initializePoolWithCustomizeConfig(context.banksClient, params); + const { pool } = await initializePoolWithCustomizeConfig(context.banksClient, params); + const poolState = await getPool(context.banksClient, pool); + expect(poolState.version).eq(0); }); }); diff --git a/tests/fixtures/alpha_vault.so b/tests/fixtures/alpha_vault.so new file mode 100755 index 0000000000000000000000000000000000000000..56e03d38bd44e8838d2d536fe553ca05213d5334 GIT binary patch literal 784480 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ΠTry&ole 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
H9>^V!f~n^ycTO{hiO>DCw-n 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 A4*&oF literal 0 HcmV?d00001 diff --git a/tests/frozenRewardVault.test.ts b/tests/frozenRewardVault.test.ts index 271dd35f..6e68cfea 100644 --- a/tests/frozenRewardVault.test.ts +++ b/tests/frozenRewardVault.test.ts @@ -28,6 +28,7 @@ import { U64_MAX, getCpAmmProgramErrorCodeHexString, getPosition, + convertToByteArray, } from "./bankrun-utils"; import BN from "bn.js"; import { describe } from "mocha"; @@ -125,10 +126,10 @@ describe("Frozen reward vault", () => { poolFees: { baseFee: { cliffFeeNumerator: new BN(2_500_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: null, diff --git a/tests/layoutCompatiable.test.ts b/tests/layoutCompatiable.test.ts new file mode 100644 index 00000000..f7ce5698 --- /dev/null +++ b/tests/layoutCompatiable.test.ts @@ -0,0 +1,57 @@ +import { expect } from "chai"; +import { convertToByteArray } from "./bankrun-utils/common"; +import { createCpAmmProgram } from "./bankrun-utils"; +import BN from "bn.js"; +import fs from "fs" + +describe("Account Layout backward compatible", () => { + it("Config account", async () => { + const program = createCpAmmProgram(); + + const accountData = fs.readFileSync("./programs/cp-amm/src/tests/fixtures/config_account.bin"); + // https://solscan.io/account/TBuzuEMMQizTjpZhRLaUPavALhZmD8U1hwiw1pWSCSq#anchorData + const periodFrequency = 60; + const configState = program.coder.accounts.decode( + "config", + Buffer.from(accountData) + ); + const secondFactorByNewLayout = configState.poolFees.baseFee.secondFactor; + // validate convert from le bytes array to number + const valueFromBytesArray = new BN( + Buffer.from(secondFactorByNewLayout).reverse() // reverse() because BN constructor use Big-Endian bytes. + ).toNumber(); + expect(valueFromBytesArray).eq(periodFrequency); + + const periodFrequencyInbyte = convertToByteArray(new BN(periodFrequency)); + expect(secondFactorByNewLayout.length).eq(periodFrequencyInbyte.length); + + for (let i = 0; i < secondFactorByNewLayout.length; i++) { + expect(periodFrequencyInbyte[i]).eq(secondFactorByNewLayout[i]); + } + }); + + it("Pool account", async () => { + const program = createCpAmmProgram(); + + const accountData = fs.readFileSync("./programs/cp-amm/src/tests/fixtures/pool_account.bin"); + // https://solscan.io/account/E8zRkDw3UdzRc8qVWmqyQ9MLj7jhgZDHSroYud5t25A7#anchorData + const periodFrequency = 60; + const poolState = program.coder.accounts.decode( + "pool", + Buffer.from(accountData) + ); + const secondFactorByNewLayout = poolState.poolFees.baseFee.secondFactor; + // validate convert from le bytes array to number + const valueFromBytesArray = new BN( + Buffer.from(secondFactorByNewLayout).reverse() // reverse because BN constructor use Big-Endian bytes. + ).toNumber(); + expect(valueFromBytesArray).eq(periodFrequency); + + const periodFrequencyInbyte = convertToByteArray(new BN(periodFrequency)); + expect(secondFactorByNewLayout.length).eq(periodFrequencyInbyte.length); + + for (let i = 0; i < secondFactorByNewLayout.length; i++) { + expect(periodFrequencyInbyte[i]).eq(secondFactorByNewLayout[i]); + } + }); +}); diff --git a/tests/lockPosition.test.ts b/tests/lockPosition.test.ts index 1823ef29..ccbd3d98 100644 --- a/tests/lockPosition.test.ts +++ b/tests/lockPosition.test.ts @@ -27,6 +27,7 @@ import { SwapParams, } from "./bankrun-utils"; import { + convertToByteArray, generateKpAndFund, startTest, warpSlotBy, @@ -110,10 +111,10 @@ describe("Lock position", () => { poolFees: { baseFee: { cliffFeeNumerator: new BN(10_000_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: null, @@ -438,10 +439,10 @@ describe("Lock position", () => { poolFees: { baseFee: { cliffFeeNumerator: new BN(10_000_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: null, diff --git a/tests/rateLimiter.test.ts b/tests/rateLimiter.test.ts new file mode 100644 index 00000000..d918353a --- /dev/null +++ b/tests/rateLimiter.test.ts @@ -0,0 +1,289 @@ +import { ProgramTestContext } from "solana-bankrun"; +import { + convertToRateLimiterSecondFactor, + expectThrowsAsync, + generateKpAndFund, + getCpAmmProgramErrorCodeHexString, + processTransactionMaybeThrow, + randomID, + startTest, + warpSlotBy, +} from "./bankrun-utils/common"; +import { + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + Transaction, +} from "@solana/web3.js"; +import { + InitializeCustomizablePoolParams, + initializeCustomizablePool, + MIN_LP_AMOUNT, + MAX_SQRT_PRICE, + MIN_SQRT_PRICE, + mintSplTokenTo, + createToken, + CreateConfigParams, + createConfigIx, + InitializePoolParams, + initializePool, + getPool, + swap, + swapInstruction, +} from "./bankrun-utils"; +import BN from "bn.js"; +import { assert, expect } from "chai"; + +describe("Rate limiter", () => { + let context: ProgramTestContext; + let admin: Keypair; + let operator: Keypair; + let partner: Keypair; + let user: Keypair; + let poolCreator: Keypair; + let tokenA: PublicKey; + let tokenB: PublicKey; + + before(async () => { + const root = Keypair.generate(); + context = await startTest(root); + admin = context.payer; + operator = await generateKpAndFund(context.banksClient, context.payer); + partner = await generateKpAndFund(context.banksClient, context.payer); + user = await generateKpAndFund(context.banksClient, context.payer); + poolCreator = await generateKpAndFund(context.banksClient, context.payer); + + tokenA = await createToken( + context.banksClient, + context.payer, + context.payer.publicKey + ); + tokenB = await createToken( + context.banksClient, + context.payer, + context.payer.publicKey + ); + + await mintSplTokenTo( + context.banksClient, + context.payer, + tokenA, + context.payer, + user.publicKey + ); + + await mintSplTokenTo( + context.banksClient, + context.payer, + tokenB, + context.payer, + user.publicKey + ); + + await mintSplTokenTo( + context.banksClient, + context.payer, + tokenA, + context.payer, + poolCreator.publicKey + ); + + await mintSplTokenTo( + context.banksClient, + context.payer, + tokenB, + context.payer, + poolCreator.publicKey + ); + }); + + it("Rate limiter", async () => { + let referenceAmount = new BN(LAMPORTS_PER_SOL); // 1 SOL + let maxRateLimiterDuration = new BN(10); + let maxFeeBps = new BN(5000); + + let rateLimiterSecondFactor = convertToRateLimiterSecondFactor(maxRateLimiterDuration, maxFeeBps) + + const createConfigParams: CreateConfigParams = { + poolFees: { + baseFee: { + cliffFeeNumerator: new BN(10_000_000), // 100bps + firstFactor: 10, // 10 bps + secondFactor: rateLimiterSecondFactor, // combined(maxRateLimiterDuration, maxFeeBps) + thirdFactor: referenceAmount, // 1 sol + baseFeeMode: 2, // rate limiter mode + }, + protocolFeePercent: 10, + partnerFeePercent: 0, + referralFeePercent: 0, + dynamicFee: null, + }, + sqrtMinPrice: new BN(MIN_SQRT_PRICE), + sqrtMaxPrice: new BN(MAX_SQRT_PRICE), + vaultConfigKey: PublicKey.default, + poolCreatorAuthority: PublicKey.default, + activationType: 0, + collectFeeMode: 1, // onlyB + }; + + let config = await createConfigIx( + context.banksClient, + admin, + new BN(randomID()), + createConfigParams + ); + const liquidity = new BN(MIN_LP_AMOUNT); + const sqrtPrice = new BN(MIN_SQRT_PRICE.muln(2)); + + const initPoolParams: InitializePoolParams = { + payer: poolCreator, + creator: poolCreator.publicKey, + config, + tokenAMint: tokenA, + tokenBMint: tokenB, + liquidity, + sqrtPrice, + activationPoint: null, + }; + const { pool } = await initializePool(context.banksClient, initPoolParams); + let poolState = await getPool(context.banksClient, pool); + + // swap with 1 SOL + + await swap(context.banksClient, { + payer: poolCreator, + pool, + inputTokenMint: tokenB, + outputTokenMint: tokenA, + amountIn: referenceAmount, + minimumAmountOut: new BN(0), + referralTokenAccount: null, + }); + + poolState = await getPool(context.banksClient, pool); + + let totalTradingFee = poolState.metrics.totalLpBFee.add( + poolState.metrics.totalProtocolBFee + ); + + expect(totalTradingFee.toNumber()).eq( + referenceAmount.div(new BN(100)).toNumber() + ); + + // swap with 2 SOL + + await swap(context.banksClient, { + payer: poolCreator, + pool, + inputTokenMint: tokenB, + outputTokenMint: tokenA, + amountIn: referenceAmount.mul(new BN(2)), + minimumAmountOut: new BN(0), + referralTokenAccount: null, + }); + + poolState = await getPool(context.banksClient, pool); + + let totalTradingFee1 = poolState.metrics.totalLpBFee.add( + poolState.metrics.totalProtocolBFee + ); + let deltaTradingFee = totalTradingFee1.sub(totalTradingFee); + + expect(deltaTradingFee.toNumber()).gt( + referenceAmount.mul(new BN(2)).div(new BN(100)).toNumber() + ); + + // wait until time pass the 10 slot + await warpSlotBy(context, maxRateLimiterDuration.add(new BN(1))); + + // swap with 2 SOL + + await swap(context.banksClient, { + payer: poolCreator, + pool, + inputTokenMint: tokenB, + outputTokenMint: tokenA, + amountIn: referenceAmount.mul(new BN(2)), + minimumAmountOut: new BN(0), + referralTokenAccount: null, + }); + + poolState = await getPool(context.banksClient, pool); + + let totalTradingFee2 = poolState.metrics.totalLpBFee.add( + poolState.metrics.totalProtocolBFee + ); + let deltaTradingFee1 = totalTradingFee2.sub(totalTradingFee1); + expect(deltaTradingFee1.toNumber()).eq( + referenceAmount.mul(new BN(2)).div(new BN(100)).toNumber() + ); + }); + + it("Try to send multiple instructions", async () => { + let referenceAmount = new BN(LAMPORTS_PER_SOL); // 1 SOL + let maxRateLimiterDuration = new BN(10); + let maxFeeBps = new BN(5000); + + let rateLimiterSecondFactor = convertToRateLimiterSecondFactor(maxRateLimiterDuration, maxFeeBps) + const liquidity = new BN(MIN_LP_AMOUNT); + const sqrtPrice = new BN(MIN_SQRT_PRICE.muln(2)); + + const initPoolParams: InitializeCustomizablePoolParams = { + payer: poolCreator, + creator: poolCreator.publicKey, + tokenAMint: tokenA, + tokenBMint: tokenB, + poolFees: { + baseFee: { + cliffFeeNumerator: new BN(10_000_000), // 100bps + firstFactor: 10, // 10 bps + secondFactor: rateLimiterSecondFactor, + thirdFactor: referenceAmount, // 1 sol + baseFeeMode: 2, // rate limiter mode + }, + protocolFeePercent: 20, + partnerFeePercent: 0, + referralFeePercent: 20, + dynamicFee: null, + }, + sqrtMinPrice: new BN(MIN_SQRT_PRICE), + sqrtMaxPrice: new BN(MAX_SQRT_PRICE), + liquidity, + sqrtPrice, + hasAlphaVault: false, + activationType: 0, + collectFeeMode: 1, // onlyB + activationPoint: null, + }; + const { pool } = await initializeCustomizablePool( + context.banksClient, + initPoolParams + ); + + // swap with 1 SOL + const swapIx = await swapInstruction(context.banksClient, { + payer: poolCreator, + pool, + inputTokenMint: tokenB, + outputTokenMint: tokenA, + amountIn: referenceAmount, + minimumAmountOut: new BN(0), + referralTokenAccount: null, + }); + + let transaction = new Transaction(); + for (let i = 0; i < 2; i++) { + transaction.add(swapIx); + } + + transaction.recentBlockhash = ( + await context.banksClient.getLatestBlockhash() + )[0]; + transaction.sign(poolCreator); + + const errorCode = getCpAmmProgramErrorCodeHexString("FailToValidateSingleSwapInstruction") + await expectThrowsAsync(async ()=>{ + await processTransactionMaybeThrow(context.banksClient, transaction); + }, errorCode) + }); +}); diff --git a/tests/removeLiquidity.test.ts b/tests/removeLiquidity.test.ts index cc9dad98..f8d5e0d5 100644 --- a/tests/removeLiquidity.test.ts +++ b/tests/removeLiquidity.test.ts @@ -1,5 +1,5 @@ import { ProgramTestContext } from "solana-bankrun"; -import { generateKpAndFund, randomID, startTest } from "./bankrun-utils/common"; +import { convertToByteArray, generateKpAndFund, randomID, startTest } from "./bankrun-utils/common"; import { Keypair, PublicKey } from "@solana/web3.js"; import { addLiquidity, @@ -15,6 +15,7 @@ import { createToken, removeAllLiquidity, closePosition, + CreateConfigParams, } from "./bankrun-utils"; import BN from "bn.js"; import { ExtensionType } from "@solana/spl-token"; @@ -88,14 +89,14 @@ describe("Remove liquidity", () => { ); // create config - const createConfigParams = { + const createConfigParams: CreateConfigParams = { poolFees: { baseFee: { cliffFeeNumerator: new BN(2_500_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: null, @@ -255,14 +256,14 @@ describe("Remove liquidity", () => { ); // create config - const createConfigParams = { + const createConfigParams: CreateConfigParams = { poolFees: { baseFee: { cliffFeeNumerator: new BN(2_500_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: null, diff --git a/tests/rewardByAdmin.test.ts b/tests/rewardByAdmin.test.ts index bdaca6d6..07512d84 100644 --- a/tests/rewardByAdmin.test.ts +++ b/tests/rewardByAdmin.test.ts @@ -23,6 +23,7 @@ import { updateRewardDuration, updateRewardFunder, withdrawIneligibleReward, + convertToByteArray, } from "./bankrun-utils"; import { generateKpAndFund, startTest } from "./bankrun-utils/common"; import { @@ -124,10 +125,10 @@ describe("Reward by admin", () => { poolFees: { baseFee: { cliffFeeNumerator: new BN(2_500_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: null, @@ -438,10 +439,10 @@ describe("Reward by admin", () => { poolFees: { baseFee: { cliffFeeNumerator: new BN(2_500_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: null, diff --git a/tests/rewardByCreator.test.ts b/tests/rewardByCreator.test.ts index 0bd1d85a..1cad89ca 100644 --- a/tests/rewardByCreator.test.ts +++ b/tests/rewardByCreator.test.ts @@ -27,6 +27,7 @@ import { createToken, mintSplTokenTo, getCpAmmProgramErrorCodeHexString, + convertToByteArray, } from "./bankrun-utils"; import BN from "bn.js"; import { describe } from "mocha"; @@ -129,10 +130,10 @@ describe("Reward by creator", () => { poolFees: { baseFee: { cliffFeeNumerator: new BN(2_500_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: null, @@ -430,10 +431,10 @@ describe("Reward by creator", () => { poolFees: { baseFee: { cliffFeeNumerator: new BN(2_500_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: null, diff --git a/tests/splitPosition.test.ts b/tests/splitPosition.test.ts index a6bfe25e..fc986c54 100644 --- a/tests/splitPosition.test.ts +++ b/tests/splitPosition.test.ts @@ -26,6 +26,7 @@ import { U64_MAX, addLiquidity, swap, + convertToByteArray } from "./bankrun-utils"; import BN from "bn.js"; @@ -93,18 +94,17 @@ describe("Split position", () => { user.publicKey ); // create config + const createConfigParams: CreateConfigParams = { poolFees: { baseFee: { cliffFeeNumerator: new BN(2_500_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, - protocolFeePercent: 10, - partnerFeePercent: 0, - referralFeePercent: 0, + padding: [], dynamicFee: null, }, sqrtMinPrice: new BN(MIN_SQRT_PRICE), diff --git a/tests/swap.test.ts b/tests/swap.test.ts index 20b742d9..c2cfd17b 100644 --- a/tests/swap.test.ts +++ b/tests/swap.test.ts @@ -1,5 +1,5 @@ import { ProgramTestContext } from "solana-bankrun"; -import { generateKpAndFund, randomID, startTest } from "./bankrun-utils/common"; +import { convertToByteArray, generateKpAndFund, randomID, startTest } from "./bankrun-utils/common"; import { Keypair, PublicKey } from "@solana/web3.js"; import { addLiquidity, @@ -95,10 +95,10 @@ describe("Swap token", () => { poolFees: { baseFee: { cliffFeeNumerator: new BN(2_500_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: null, @@ -251,10 +251,10 @@ describe("Swap token", () => { poolFees: { baseFee: { cliffFeeNumerator: new BN(2_500_000), - numberOfPeriod: 0, - reductionFactor: new BN(0), - periodFrequency: new BN(0), - feeSchedulerMode: 0, + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, }, padding: [], dynamicFee: null, From 0faa07de8f2bd14508ff2831e1b23d03a8a34c5b Mon Sep 17 00:00:00 2001 From: defi0x1 <34453681+defi0x1@users.noreply.github.com> Date: Fri, 1 Aug 2025 16:04:26 +0700 Subject: [PATCH 02/10] Feat/permissionless transfer hook (#96) * WIP * feat: permissionless transfer hook * fix after cherry picked * fix test --- package.json | 2 +- pnpm-lock.yaml | 10 +- programs/cp-amm/src/utils/token.rs | 27 +- tests/bankrun-utils/common.ts | 5 + tests/bankrun-utils/token2022.ts | 50 ++ tests/bankrun-utils/transferHook/idl.ts | 229 +++++++++ .../transferHook/idl/transfer_hook.json | 130 +++++ .../transferHook/idl/transfer_hook.ts | 136 +++++ tests/bankrun-utils/transferHook/index.ts | 84 ++++ .../transferHook/transferHookUtils.ts | 468 ++++++++++++++++++ tests/fixtures/transfer_hook_counter.so | Bin 0 -> 244888 bytes tests/permissionLessTransferHook.test.ts | 166 +++++++ tests/rateLimiter.test.ts | 8 +- 13 files changed, 1298 insertions(+), 17 deletions(-) create mode 100644 tests/bankrun-utils/transferHook/idl.ts create mode 100644 tests/bankrun-utils/transferHook/idl/transfer_hook.json create mode 100644 tests/bankrun-utils/transferHook/idl/transfer_hook.ts create mode 100644 tests/bankrun-utils/transferHook/index.ts create mode 100644 tests/bankrun-utils/transferHook/transferHookUtils.ts create mode 100755 tests/fixtures/transfer_hook_counter.so create mode 100644 tests/permissionLessTransferHook.test.ts diff --git a/package.json b/package.json index a8222871..d2dee1c2 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "typescript": "^4.3.5", "prettier": "^2.6.2", "@coral-xyz/anchor-errors": "^0.30.1", - "@solana/spl-token": "^0.4.8", + "@solana/spl-token": "^0.4.13", "@solana/web3.js": "^1.95.3", "solana-bankrun": "^0.3.1", "@solana/spl-token-metadata": "^0.1.6" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e1552ca3..ebb6118b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,8 +16,8 @@ importers: specifier: ^0.30.1 version: 0.30.1 '@solana/spl-token': - specifier: ^0.4.8 - version: 0.4.12(@solana/web3.js@1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@4.9.5)(utf-8-validate@5.0.10) + specifier: ^0.4.13 + version: 0.4.13(@solana/web3.js@1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@4.9.5)(utf-8-validate@5.0.10) '@solana/spl-token-metadata': specifier: ^0.1.6 version: 0.1.6(@solana/web3.js@1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@4.9.5) @@ -141,8 +141,8 @@ packages: peerDependencies: '@solana/web3.js': ^1.95.3 - '@solana/spl-token@0.4.12': - resolution: {integrity: sha512-K6CxzSoO1vC+WBys25zlSDaW0w4UFZO/IvEZquEI35A/PjqXNQHeVigmDCZYEJfESvYarKwsr8tYr/29lPtvaw==} + '@solana/spl-token@0.4.13': + resolution: {integrity: sha512-cite/pYWQZZVvLbg5lsodSovbetK/eA24gaR0eeUeMuBAMNrT8XFCwaygKy0N2WSg3gSyjjNpIeAGBAKZaY/1w==} engines: {node: '>=16'} peerDependencies: '@solana/web3.js': ^1.95.5 @@ -942,7 +942,7 @@ snapshots: - fastestsmallesttextencoderdecoder - typescript - '@solana/spl-token@0.4.12(@solana/web3.js@1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@4.9.5)(utf-8-validate@5.0.10)': + '@solana/spl-token@0.4.13(@solana/web3.js@1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@4.9.5)(utf-8-validate@5.0.10)': dependencies: '@solana/buffer-layout': 4.0.1 '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) diff --git a/programs/cp-amm/src/utils/token.rs b/programs/cp-amm/src/utils/token.rs index 3af27d41..4d0f8bdf 100644 --- a/programs/cp-amm/src/utils/token.rs +++ b/programs/cp-amm/src/utils/token.rs @@ -230,12 +230,29 @@ pub fn is_supported_mint(mint_account: &InterfaceAccount) -> Result let mint_data = mint_info.try_borrow_data()?; let mint = StateWithExtensions::::unpack(&mint_data)?; let extensions = mint.get_extension_types()?; + for e in extensions { - if e != ExtensionType::TransferFeeConfig - && e != ExtensionType::MetadataPointer - && e != ExtensionType::TokenMetadata - { - return Ok(false); + match e { + ExtensionType::TransferFeeConfig + | ExtensionType::MetadataPointer + | ExtensionType::TokenMetadata => { + // permissionless supported + } + ExtensionType::TransferHook => { + if let Ok(transfer_hook) = + mint.get_extension::() + { + let transfer_hook_program_id = Option::::from(transfer_hook.program_id); + let transfer_hook_authority = Option::::from(transfer_hook.authority); + if transfer_hook_program_id.is_some() || transfer_hook_authority.is_some() { + return Ok(false); + } + } else { + return Ok(false); + } + } + + _ => return Ok(false), } } Ok(true) diff --git a/tests/bankrun-utils/common.ts b/tests/bankrun-utils/common.ts index 322a733e..632c4e73 100644 --- a/tests/bankrun-utils/common.ts +++ b/tests/bankrun-utils/common.ts @@ -8,6 +8,7 @@ import { import { BanksClient, ProgramTestContext, startAnchor } from "solana-bankrun"; import { ALPHA_VAULT_PROGRAM_ID, CP_AMM_PROGRAM_ID } from "./constants"; import BN from "bn.js"; +import { TRANSFER_HOOK_COUNTER_PROGRAM_ID } from "./transferHook"; import CpAmmIdl from "../../target/idl/cp_amm.json"; @@ -20,6 +21,10 @@ export async function startTest(root: Keypair) { name: "cp_amm", programId: new PublicKey(CP_AMM_PROGRAM_ID), }, + { + name: "transfer_hook_counter", + programId: TRANSFER_HOOK_COUNTER_PROGRAM_ID, + }, { name: "alpha_vault", programId: new PublicKey(ALPHA_VAULT_PROGRAM_ID), diff --git a/tests/bankrun-utils/token2022.ts b/tests/bankrun-utils/token2022.ts index b11a58dd..23fc8c57 100644 --- a/tests/bankrun-utils/token2022.ts +++ b/tests/bankrun-utils/token2022.ts @@ -7,6 +7,10 @@ import { createInitializeMetadataPointerInstruction, createMintToInstruction, createInitializePermanentDelegateInstruction, + createInitializeTransferHookInstruction, + createUpdateTransferHookInstruction, + createSetAuthorityInstruction, + AuthorityType, } from "@solana/spl-token"; import { Keypair, @@ -18,6 +22,8 @@ import { import { BanksClient } from "solana-bankrun"; import { DECIMALS } from "./constants"; import { getOrCreateAssociatedTokenAccount } from "./token"; +import { TRANSFER_HOOK_COUNTER_PROGRAM_ID } from "./transferHook"; +import { processTransactionMaybeThrow } from "./common"; const rawAmount = 1_000_000 * 10 ** DECIMALS; // 1 millions interface ExtensionWithInstruction { @@ -63,6 +69,21 @@ export function createTransferFeeExtensionWithInstruction( }; } +export function createTransferHookExtensionWithInstruction( + mint: PublicKey, + authority: PublicKey +): ExtensionWithInstruction { + return { + extension: ExtensionType.TransferHook, + instruction: createInitializeTransferHookInstruction( + mint, + authority, + TRANSFER_HOOK_COUNTER_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID + ), + }; +} + export async function createToken2022( banksClient: BanksClient, payer: Keypair, @@ -131,3 +152,32 @@ export async function mintToToken2022( await banksClient.processTransaction(transaction); } + +export async function revokeAuthorityAndProgramIdTransferHook( + bankClient: BanksClient, + authority: Keypair, + mint: PublicKey +) { + const transaction = new Transaction().add( + createUpdateTransferHookInstruction( + mint, + authority.publicKey, + PublicKey.default, + [], + TOKEN_2022_PROGRAM_ID + ), + createSetAuthorityInstruction( + mint, + authority.publicKey, + AuthorityType.TransferHookProgramId, + null, + [], + TOKEN_2022_PROGRAM_ID + ) + ); + + transaction.recentBlockhash = (await bankClient.getLatestBlockhash())[0]; + transaction.sign(authority); + + await processTransactionMaybeThrow(bankClient, transaction); +} diff --git a/tests/bankrun-utils/transferHook/idl.ts b/tests/bankrun-utils/transferHook/idl.ts new file mode 100644 index 00000000..c358d210 --- /dev/null +++ b/tests/bankrun-utils/transferHook/idl.ts @@ -0,0 +1,229 @@ +export type TransferHookCounter = { + address: "EBZDYx7599krFc4m2govwBdZcicr4GgepqC78m71nsHS"; + metadata: { + name: "transfer_hook_counter"; + version: "0.1.0"; + spec: "0.1.0"; + description: "Created with Anchor"; + }; + instructions: [ + { + name: "initializeExtraAccountMetaList"; + accounts: [ + { + name: "payer"; + isMut: true; + isSigner: true; + }, + { + name: "extraAccountMetaList"; + isMut: true; + isSigner: false; + }, + { + name: "mint"; + isMut: false; + isSigner: false; + }, + { + name: "counterAccount"; + isMut: true; + isSigner: false; + }, + { + name: "tokenProgram"; + isMut: false; + isSigner: false; + }, + { + name: "associatedTokenProgram"; + isMut: false; + isSigner: false; + }, + { + name: "systemProgram"; + isMut: false; + isSigner: false; + } + ]; + args: []; + }, + { + name: "transferHook"; + accounts: [ + { + name: "sourceToken"; + isMut: false; + isSigner: false; + }, + { + name: "mint"; + isMut: false; + isSigner: false; + }, + { + name: "destinationToken"; + isMut: false; + isSigner: false; + }, + { + name: "owner"; + isMut: false; + isSigner: false; + }, + { + name: "extraAccountMetaList"; + isMut: false; + isSigner: false; + }, + { + name: "counterAccount"; + isMut: true; + isSigner: false; + } + ]; + args: [ + { + name: "amount"; + type: "u64"; + } + ]; + } + ]; + accounts: [ + { + name: "counterAccount"; + type: { + kind: "struct"; + fields: [ + { + name: "counter"; + type: "u32"; + } + ]; + }; + } + ]; + errors: [ + { + code: 6000; + name: "AmountTooBig"; + msg: "The amount is too big"; + } + ]; +}; + +export const IDL: TransferHookCounter = { + address: "EBZDYx7599krFc4m2govwBdZcicr4GgepqC78m71nsHS", + metadata: { + name: "transfer_hook_counter", + version: "0.1.0", + spec: "0.1.0", + description: "Created with Anchor", + }, + instructions: [ + { + name: "initializeExtraAccountMetaList", + accounts: [ + { + name: "payer", + isMut: true, + isSigner: true, + }, + { + name: "extraAccountMetaList", + isMut: true, + isSigner: false, + }, + { + name: "mint", + isMut: false, + isSigner: false, + }, + { + name: "counterAccount", + isMut: true, + isSigner: false, + }, + { + name: "tokenProgram", + isMut: false, + isSigner: false, + }, + { + name: "associatedTokenProgram", + isMut: false, + isSigner: false, + }, + { + name: "systemProgram", + isMut: false, + isSigner: false, + }, + ], + args: [], + }, + { + name: "transferHook", + accounts: [ + { + name: "sourceToken", + isMut: false, + isSigner: false, + }, + { + name: "mint", + isMut: false, + isSigner: false, + }, + { + name: "destinationToken", + isMut: false, + isSigner: false, + }, + { + name: "owner", + isMut: false, + isSigner: false, + }, + { + name: "extraAccountMetaList", + isMut: false, + isSigner: false, + }, + { + name: "counterAccount", + isMut: true, + isSigner: false, + }, + ], + args: [ + { + name: "amount", + type: "u64", + }, + ], + }, + ], + accounts: [ + { + name: "counterAccount", + type: { + kind: "struct", + fields: [ + { + name: "counter", + type: "u32", + }, + ], + }, + }, + ], + errors: [ + { + code: 6000, + name: "AmountTooBig", + msg: "The amount is too big", + }, + ], +}; diff --git a/tests/bankrun-utils/transferHook/idl/transfer_hook.json b/tests/bankrun-utils/transferHook/idl/transfer_hook.json new file mode 100644 index 00000000..2e86a201 --- /dev/null +++ b/tests/bankrun-utils/transferHook/idl/transfer_hook.json @@ -0,0 +1,130 @@ +{ + "address": "EBZDYx7599krFc4m2govwBdZcicr4GgepqC78m71nsHS", + "metadata": { + "name": "transfer_hook_counter", + "version": "0.1.0", + "spec": "0.1.0" + }, + "instructions": [ + { + "name": "initialize_extra_account_meta_list", + "discriminator": [ + 92, + 197, + 174, + 197, + 41, + 124, + 19, + 3 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "extra_account_meta_list", + "writable": true + }, + { + "name": "mint" + }, + { + "name": "counter_account", + "writable": true + }, + { + "name": "token_program" + }, + { + "name": "associated_token_program" + }, + { + "name": "system_program" + } + ], + "args": [] + }, + { + "name": "transfer_hook", + "discriminator": [ + 220, + 57, + 220, + 152, + 126, + 125, + 97, + 168 + ], + "accounts": [ + { + "name": "source_token" + }, + { + "name": "mint" + }, + { + "name": "destination_token" + }, + { + "name": "owner" + }, + { + "name": "extra_account_meta_list" + }, + { + "name": "counter_account", + "writable": true + }, + { + "name": "account_order_verifier" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + } + ], + "accounts": [ + { + "name": "CounterAccount", + "discriminator": [ + 164, + 8, + 153, + 71, + 8, + 44, + 93, + 22 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "AmountTooBig", + "msg": "The amount is too big" + } + ], + "types": [ + { + "name": "CounterAccount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "counter", + "type": "u32" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/tests/bankrun-utils/transferHook/idl/transfer_hook.ts b/tests/bankrun-utils/transferHook/idl/transfer_hook.ts new file mode 100644 index 00000000..95bf073f --- /dev/null +++ b/tests/bankrun-utils/transferHook/idl/transfer_hook.ts @@ -0,0 +1,136 @@ +/** + * Program IDL in camelCase format in order to be used in JS/TS. + * + * Note that this is only a type helper and is not the actual IDL. The original + * IDL can be found at `target/idl/transfer_hook_counter.json`. + */ +export type TransferHookCounter = { + "address": "EBZDYx7599krFc4m2govwBdZcicr4GgepqC78m71nsHS", + "metadata": { + "name": "transferHookCounter", + "version": "0.1.0", + "spec": "0.1.0" + }, + "instructions": [ + { + "name": "initializeExtraAccountMetaList", + "discriminator": [ + 92, + 197, + 174, + 197, + 41, + 124, + 19, + 3 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "extraAccountMetaList", + "writable": true + }, + { + "name": "mint" + }, + { + "name": "counterAccount", + "writable": true + }, + { + "name": "tokenProgram" + }, + { + "name": "associatedTokenProgram" + }, + { + "name": "systemProgram" + } + ], + "args": [] + }, + { + "name": "transferHook", + "discriminator": [ + 220, + 57, + 220, + 152, + 126, + 125, + 97, + 168 + ], + "accounts": [ + { + "name": "sourceToken" + }, + { + "name": "mint" + }, + { + "name": "destinationToken" + }, + { + "name": "owner" + }, + { + "name": "extraAccountMetaList" + }, + { + "name": "counterAccount", + "writable": true + }, + { + "name": "accountOrderVerifier" + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + } + ], + "accounts": [ + { + "name": "counterAccount", + "discriminator": [ + 164, + 8, + 153, + 71, + 8, + 44, + 93, + 22 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "amountTooBig", + "msg": "The amount is too big" + } + ], + "types": [ + { + "name": "counterAccount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "counter", + "type": "u32" + } + ] + } + } + ] +}; diff --git a/tests/bankrun-utils/transferHook/index.ts b/tests/bankrun-utils/transferHook/index.ts new file mode 100644 index 00000000..d340d597 --- /dev/null +++ b/tests/bankrun-utils/transferHook/index.ts @@ -0,0 +1,84 @@ +import { AnchorProvider, Program, Wallet, web3 } from "@coral-xyz/anchor"; +import TransferHookIdl from "./idl/transfer_hook.json"; +import { TransferHookCounter } from "./idl/transfer_hook"; +import { + clusterApiUrl, + Connection, + Keypair, + PublicKey, + Signer, + SystemProgram, + Transaction, +} from "@solana/web3.js"; +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID, +} from "@solana/spl-token"; +import { BanksClient } from "solana-bankrun"; +import { processTransactionMaybeThrow } from "../common"; + +export const TRANSFER_HOOK_COUNTER_PROGRAM_ID = new web3.PublicKey( + "EBZDYx7599krFc4m2govwBdZcicr4GgepqC78m71nsHS" +); + +export function createTransferHookCounterProgram(): Program { + const wallet = new Wallet(Keypair.generate()); + const provider = new AnchorProvider( + new Connection(clusterApiUrl("devnet")), + wallet, + {} + ); + + const program = new Program( + TransferHookIdl as TransferHookCounter, + provider + ); + + return program; +} + +export function deriveExtraAccountMetaList(mint: PublicKey) { + const [extraAccountMetaListPda] = PublicKey.findProgramAddressSync( + [Buffer.from("extra-account-metas"), mint.toBuffer()], + TRANSFER_HOOK_COUNTER_PROGRAM_ID + ); + + return extraAccountMetaListPda; +} + +export async function createExtraAccountMetaListAndCounter( + bankClient: BanksClient, + payer: Keypair, + mint: web3.PublicKey +) { + const program = createTransferHookCounterProgram(); + const extraAccountMetaList = deriveExtraAccountMetaList(mint); + const counterAccount = deriveCounter(mint, program.programId); + + const transaction = await program.methods + .initializeExtraAccountMetaList() + .accountsPartial({ + mint, + counterAccount, + extraAccountMetaList, + payer: payer.publicKey, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .transaction(); + transaction.recentBlockhash = (await bankClient.getLatestBlockhash())[0]; + transaction.sign(payer); + await processTransactionMaybeThrow(bankClient, transaction); + + return [extraAccountMetaList, counterAccount]; +} + +export function deriveCounter(mint: web3.PublicKey, programId: web3.PublicKey) { + const [counter] = web3.PublicKey.findProgramAddressSync( + [Buffer.from("counter"), mint.toBuffer()], + programId + ); + + return counter; +} \ No newline at end of file diff --git a/tests/bankrun-utils/transferHook/transferHookUtils.ts b/tests/bankrun-utils/transferHook/transferHookUtils.ts new file mode 100644 index 00000000..e65528a0 --- /dev/null +++ b/tests/bankrun-utils/transferHook/transferHookUtils.ts @@ -0,0 +1,468 @@ +import { + createExecuteInstruction, + createTransferCheckedInstruction, + ExtraAccountMeta, + getExtraAccountMetaAddress, + getExtraAccountMetas, + getTransferHook, + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, + TokenTransferHookAccountDataNotFound, + TokenTransferHookAccountNotFound, + TokenTransferHookInvalidPubkeyData, + TokenTransferHookInvalidSeed, + TokenTransferHookPubkeyDataTooSmall, + unpackMint, +} from "@solana/spl-token"; +import { + AccountMeta, + PUBLIC_KEY_LENGTH, + PublicKey, + Signer, + TransactionInstruction, +} from "@solana/web3.js"; +import { BanksClient } from "solana-bankrun"; + +export async function getExtraAccountMetasForTransferHook( + bankClient: BanksClient, + mint: PublicKey +) { + const info = await bankClient.getAccount(mint); + + if (info.owner.equals(TOKEN_PROGRAM_ID)) { + return []; + } + + const accountInfoWithBuffer = { + ...info, + data: Buffer.from(info.data), + }; + const mintInfo = unpackMint( + mint, + accountInfoWithBuffer, + TOKEN_2022_PROGRAM_ID + ); + + const transferHook = getTransferHook(mintInfo); + if (!transferHook) { + return []; + } else { + const transferWithHookIx = await createTransferCheckedWithTransferHookInstruction( + bankClient, + PublicKey.default, + mint, + PublicKey.default, + PublicKey.default, + BigInt(0), + mintInfo.decimals, + [], + TOKEN_2022_PROGRAM_ID + ); + + // Only 4 keys needed if it's single signer. https://github.com/solana-labs/solana-program-library/blob/d72289c79a04411c69a8bf1054f7156b6196f9b3/token/js/src/extensions/transferFee/instructions.ts#L251 + return transferWithHookIx.keys.slice(4); + } +} + +export async function createTransferCheckedWithTransferHookInstruction( + bankClient: BanksClient, + source: PublicKey, + mint: PublicKey, + destination: PublicKey, + owner: PublicKey, + amount: bigint, + decimals: number, + multiSigners: (Signer | PublicKey)[] = [], + programId = TOKEN_2022_PROGRAM_ID +) { + const instruction = createTransferCheckedInstruction( + source, + mint, + destination, + owner, + amount, + decimals, + multiSigners, + programId + ); + const info = await bankClient.getAccount(mint); + const accountInfoWithBuffer = { + ...info, + data: Buffer.from(info.data), + }; + const mintInfo = unpackMint( + mint, + accountInfoWithBuffer, + TOKEN_2022_PROGRAM_ID + ); + + const transferHook = getTransferHook(mintInfo); + + if (transferHook) { + addExtraAccountMetasForExecute( + bankClient, + instruction, + transferHook.programId, + source, + mint, + destination, + owner, + amount + ); + } + + return instruction; +} + +function deEscalateAccountMeta( + accountMeta: AccountMeta, + accountMetas: AccountMeta[] +): AccountMeta { + const maybeHighestPrivileges = accountMetas + .filter((x) => x.pubkey.equals(accountMeta.pubkey)) + .reduce<{ isSigner: boolean; isWritable: boolean } | undefined>( + (acc, x) => { + if (!acc) return { isSigner: x.isSigner, isWritable: x.isWritable }; + return { + isSigner: acc.isSigner || x.isSigner, + isWritable: acc.isWritable || x.isWritable, + }; + }, + undefined + ); + if (maybeHighestPrivileges) { + const { isSigner, isWritable } = maybeHighestPrivileges; + if (!isSigner && isSigner !== accountMeta.isSigner) { + accountMeta.isSigner = false; + } + if (!isWritable && isWritable !== accountMeta.isWritable) { + accountMeta.isWritable = false; + } + } + return accountMeta; +} + +export async function addExtraAccountMetasForExecute( + bankClient: BanksClient, + instruction: TransactionInstruction, + programId: PublicKey, + source: PublicKey, + mint: PublicKey, + destination: PublicKey, + owner: PublicKey, + amount: number | bigint +) { + const validateStatePubkey = getExtraAccountMetaAddress(mint, programId); + const validateStateAccount = await bankClient.getAccount(validateStatePubkey); + if (validateStateAccount == null) { + return instruction; + } + const accountInfoWithBuffer = { + ...validateStateAccount, + data: Buffer.from(validateStateAccount.data), + }; + const validateStateData = getExtraAccountMetas(accountInfoWithBuffer); + + // Check to make sure the provided keys are in the instruction + if ( + ![source, mint, destination, owner].every((key) => + instruction.keys.some((meta) => meta.pubkey.equals(key)) + ) + ) { + throw new Error("Missing required account in instruction"); + } + + const executeInstruction = createExecuteInstruction( + programId, + source, + mint, + destination, + owner, + validateStatePubkey, + BigInt(amount) + ); + + for (const extraAccountMeta of validateStateData) { + executeInstruction.keys.push( + deEscalateAccountMeta( + await resolveExtraAccountMeta( + bankClient, + extraAccountMeta, + executeInstruction.keys, + executeInstruction.data, + executeInstruction.programId + ), + executeInstruction.keys + ) + ); + } + + // Add only the extra accounts resolved from the validation state + instruction.keys.push(...executeInstruction.keys.slice(5)); + + // Add the transfer hook program ID and the validation state account + instruction.keys.push({ + pubkey: programId, + isSigner: false, + isWritable: false, + }); + instruction.keys.push({ + pubkey: validateStatePubkey, + isSigner: false, + isWritable: false, + }); +} + +export async function resolveExtraAccountMeta( + bankClient: BanksClient, + extraMeta: ExtraAccountMeta, + previousMetas: AccountMeta[], + instructionData: Buffer, + transferHookProgramId: PublicKey +): Promise { + if (extraMeta.discriminator === 0) { + return { + pubkey: new PublicKey(extraMeta.addressConfig), + isSigner: extraMeta.isSigner, + isWritable: extraMeta.isWritable, + }; + } else if (extraMeta.discriminator === 2) { + const pubkey = await unpackPubkeyData( + bankClient, + extraMeta.addressConfig, + previousMetas, + instructionData + ); + return { + pubkey, + isSigner: extraMeta.isSigner, + isWritable: extraMeta.isWritable, + }; + } + + let programId = PublicKey.default; + + if (extraMeta.discriminator === 1) { + programId = transferHookProgramId; + } else { + const accountIndex = extraMeta.discriminator - (1 << 7); + if (previousMetas.length <= accountIndex) { + throw new TokenTransferHookAccountNotFound(); + } + programId = previousMetas[accountIndex].pubkey; + } + + const seeds = await unpackSeeds( + bankClient, + extraMeta.addressConfig, + previousMetas, + instructionData + ); + const pubkey = PublicKey.findProgramAddressSync(seeds, programId)[0]; + + return { + pubkey, + isSigner: extraMeta.isSigner, + isWritable: extraMeta.isWritable, + }; +} + +async function unpackPubkeyData( + bankClient: BanksClient, + keyDataConfig: Uint8Array, + previousMetas: AccountMeta[], + instructionData: Buffer +): Promise { + const [discriminator, ...rest] = keyDataConfig; + const remaining = new Uint8Array(rest); + switch (discriminator) { + case 1: + return unpackPubkeyDataFromInstructionData(remaining, instructionData); + case 2: + return await unpackPubkeyDataFromAccountData(bankClient, remaining, previousMetas); + default: + throw new TokenTransferHookInvalidPubkeyData(); + } +} + +async function unpackSeeds( + bankClient: BanksClient, + seeds: Uint8Array, + previousMetas: AccountMeta[], + instructionData: Buffer +): Promise { + const unpackedSeeds: Buffer[] = []; + let i = 0; + while (i < 32) { + const seed = await unpackFirstSeed( + bankClient, + seeds.slice(i), + previousMetas, + instructionData + ); + if (seed == null) { + break; + } + unpackedSeeds.push(seed.data); + i += seed.packedLength; + } + return unpackedSeeds; +} + +async function unpackFirstSeed( + bankClient: BanksClient, + seeds: Uint8Array, + previousMetas: AccountMeta[], + instructionData: Buffer +): Promise { + const [discriminator, ...rest] = seeds; + const remaining = new Uint8Array(rest); + switch (discriminator) { + case 0: + return null; + case 1: + return unpackSeedLiteral(remaining); + case 2: + return unpackSeedInstructionArg(remaining, instructionData); + case 3: + return unpackSeedAccountKey(remaining, previousMetas); + case 4: + return await unpackSeedAccountData(bankClient, remaining, previousMetas); + default: + throw new TokenTransferHookInvalidSeed(); + } +} + +interface Seed { + data: Buffer; + packedLength: number; +} + +const DISCRIMINATOR_SPAN = 1; +const LITERAL_LENGTH_SPAN = 1; +const INSTRUCTION_ARG_OFFSET_SPAN = 1; +const INSTRUCTION_ARG_LENGTH_SPAN = 1; +const ACCOUNT_KEY_INDEX_SPAN = 1; +const ACCOUNT_DATA_ACCOUNT_INDEX_SPAN = 1; +const ACCOUNT_DATA_OFFSET_SPAN = 1; +const ACCOUNT_DATA_LENGTH_SPAN = 1; + +function unpackSeedLiteral(seeds: Uint8Array): Seed { + if (seeds.length < 1) { + throw new TokenTransferHookInvalidSeed(); + } + const [length, ...rest] = seeds; + if (rest.length < length) { + throw new TokenTransferHookInvalidSeed(); + } + return { + data: Buffer.from(rest.slice(0, length)), + packedLength: DISCRIMINATOR_SPAN + LITERAL_LENGTH_SPAN + length, + }; +} + +function unpackSeedInstructionArg( + seeds: Uint8Array, + instructionData: Buffer +): Seed { + if (seeds.length < 2) { + throw new TokenTransferHookInvalidSeed(); + } + const [index, length] = seeds; + if (instructionData.length < length + index) { + throw new TokenTransferHookInvalidSeed(); + } + return { + data: instructionData.subarray(index, index + length), + packedLength: + DISCRIMINATOR_SPAN + + INSTRUCTION_ARG_OFFSET_SPAN + + INSTRUCTION_ARG_LENGTH_SPAN, + }; +} + +function unpackSeedAccountKey( + seeds: Uint8Array, + previousMetas: AccountMeta[] +): Seed { + if (seeds.length < 1) { + throw new TokenTransferHookInvalidSeed(); + } + const [index] = seeds; + if (previousMetas.length <= index) { + throw new TokenTransferHookInvalidSeed(); + } + return { + data: previousMetas[index].pubkey.toBuffer(), + packedLength: DISCRIMINATOR_SPAN + ACCOUNT_KEY_INDEX_SPAN, + }; +} + +async function unpackSeedAccountData( + bankClient: BanksClient, + seeds: Uint8Array, + previousMetas: AccountMeta[] +): Promise { + if (seeds.length < 3) { + throw new TokenTransferHookInvalidSeed(); + } + const [accountIndex, dataIndex, length] = seeds; + if (previousMetas.length <= accountIndex) { + throw new TokenTransferHookInvalidSeed(); + } + const accountInfo = await bankClient.getAccount(previousMetas[accountIndex].pubkey); + if (accountInfo == null) { + throw new TokenTransferHookAccountDataNotFound(); + } + if (accountInfo.data.length < dataIndex + length) { + throw new TokenTransferHookInvalidSeed(); + } + return { + data: Buffer.from(accountInfo.data).subarray(dataIndex, dataIndex + length), + packedLength: + DISCRIMINATOR_SPAN + + ACCOUNT_DATA_ACCOUNT_INDEX_SPAN + + ACCOUNT_DATA_OFFSET_SPAN + + ACCOUNT_DATA_LENGTH_SPAN, + }; +} + +function unpackPubkeyDataFromInstructionData( + remaining: Uint8Array, + instructionData: Buffer +): PublicKey { + if (remaining.length < 1) { + throw new TokenTransferHookInvalidPubkeyData(); + } + const dataIndex = remaining[0]; + if (instructionData.length < dataIndex + PUBLIC_KEY_LENGTH) { + throw new TokenTransferHookPubkeyDataTooSmall(); + } + return new PublicKey( + instructionData.subarray(dataIndex, dataIndex + PUBLIC_KEY_LENGTH) + ); +} + +async function unpackPubkeyDataFromAccountData( + bankClient: BanksClient, + remaining: Uint8Array, + previousMetas: AccountMeta[] +): Promise { + if (remaining.length < 2) { + throw new TokenTransferHookInvalidPubkeyData(); + } + const [accountIndex, dataIndex] = remaining; + if (previousMetas.length <= accountIndex) { + throw new TokenTransferHookAccountDataNotFound(); + } + const accountInfo = await bankClient.getAccount(previousMetas[accountIndex].pubkey); + if (accountInfo == null) { + throw new TokenTransferHookAccountNotFound(); + } + if (accountInfo.data.length < dataIndex + PUBLIC_KEY_LENGTH) { + throw new TokenTransferHookPubkeyDataTooSmall(); + } + return new PublicKey( + accountInfo.data.subarray(dataIndex, dataIndex + PUBLIC_KEY_LENGTH) + ); +} diff --git a/tests/fixtures/transfer_hook_counter.so b/tests/fixtures/transfer_hook_counter.so new file mode 100755 index 0000000000000000000000000000000000000000..e524983362fe036b2c04d9cc70f0f51f0eceaefa GIT binary patch literal 244888 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 Q*C{FF~(7=%w%3 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 { + let context: ProgramTestContext; + let creator: Keypair; + let config: PublicKey; + + let tokenAMint: PublicKey; + let tokenBMint: PublicKey; + + let liquidity: BN; + let sqrtPrice: BN; + let admin: Keypair; + const configId = Math.floor(Math.random() * 1000); + + beforeEach(async () => { + const root = Keypair.generate(); + context = await startTest(root); + + const tokenAMintKeypair = Keypair.generate(); + const tokenBMintKeypair = Keypair.generate(); + + tokenAMint = tokenAMintKeypair.publicKey; + + const tokenAExtensions = [ + createTransferHookExtensionWithInstruction( + tokenAMintKeypair.publicKey, + context.payer.publicKey + ), + ]; + + creator = await generateKpAndFund(context.banksClient, context.payer); + admin = await generateKpAndFund(context.banksClient, context.payer); + + await createToken2022( + context.banksClient, + context.payer, + tokenAExtensions, + tokenAMintKeypair + ); + tokenBMint = await createToken( + context.banksClient, + context.payer, + context.payer.publicKey + ); + + await mintToToken2022( + context.banksClient, + context.payer, + tokenAMint, + context.payer, + creator.publicKey + ); + + await mintSplTokenTo( + context.banksClient, + context.payer, + tokenBMint, + context.payer, + creator.publicKey + ); + + await createExtraAccountMetaListAndCounter( + context.banksClient, + admin, + tokenAMint + ); + // create config + const createConfigParams: CreateConfigParams = { + poolFees: { + baseFee: { + cliffFeeNumerator: new BN(2_500_000), + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, + }, + padding: [], + dynamicFee: null, + }, + sqrtMinPrice: new BN(MIN_SQRT_PRICE), + sqrtMaxPrice: new BN(MAX_SQRT_PRICE), + vaultConfigKey: PublicKey.default, + poolCreatorAuthority: PublicKey.default, + activationType: 0, + collectFeeMode: 0, + }; + + config = await createConfigIx( + context.banksClient, + admin, + new BN(configId), + createConfigParams + ); + }); + + it("Initialize pool with permission less transfer hook", async () => { + liquidity = new BN(MIN_LP_AMOUNT); + sqrtPrice = new BN(MIN_SQRT_PRICE); + + const initPoolParams: InitializePoolParams = { + payer: creator, + creator: creator.publicKey, + config, + tokenAMint, + tokenBMint, + liquidity, + sqrtPrice, + activationPoint: null, + }; + + const errorCode = getCpAmmProgramErrorCodeHexString("InvalidTokenBadge"); + await expectThrowsAsync(async () => { + await initializePool(context.banksClient, initPoolParams); + }, errorCode); + + // revoke program id + + await revokeAuthorityAndProgramIdTransferHook( + context.banksClient, + context.payer, + tokenAMint + ); + + const { pool } = await initializePool(context.banksClient, initPoolParams); + + const newStatus = 1; + await setPoolStatus(context.banksClient, { + admin, + pool, + status: newStatus, + }); + const poolState = await getPool(context.banksClient, pool); + expect(poolState.poolStatus).eq(newStatus); + }); +}); diff --git a/tests/rateLimiter.test.ts b/tests/rateLimiter.test.ts index d918353a..69a309eb 100644 --- a/tests/rateLimiter.test.ts +++ b/tests/rateLimiter.test.ts @@ -113,9 +113,7 @@ describe("Rate limiter", () => { thirdFactor: referenceAmount, // 1 sol baseFeeMode: 2, // rate limiter mode }, - protocolFeePercent: 10, - partnerFeePercent: 0, - referralFeePercent: 0, + padding: [], dynamicFee: null, }, sqrtMinPrice: new BN(MIN_SQRT_PRICE), @@ -241,9 +239,7 @@ describe("Rate limiter", () => { thirdFactor: referenceAmount, // 1 sol baseFeeMode: 2, // rate limiter mode }, - protocolFeePercent: 20, - partnerFeePercent: 0, - referralFeePercent: 20, + padding: [], dynamicFee: null, }, sqrtMinPrice: new BN(MIN_SQRT_PRICE), From ee874d81171969e2fe8032d8373c04f9cfd89137 Mon Sep 17 00:00:00 2001 From: andrewsource147 <31321699+andrewsource147@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:22:00 +0700 Subject: [PATCH 03/10] fix lock (#99) --- programs/cp-amm/src/state/fee.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/programs/cp-amm/src/state/fee.rs b/programs/cp-amm/src/state/fee.rs index ceaa4cee..17dbe195 100644 --- a/programs/cp-amm/src/state/fee.rs +++ b/programs/cp-amm/src/state/fee.rs @@ -299,7 +299,10 @@ impl DynamicFeeStruct { sqrt_price_current: u128, current_timestamp: u64, ) -> Result<()> { - let elapsed = current_timestamp.safe_sub(self.last_update_timestamp)?; + // it is fine to use saturating_sub, because never a chance current_timestamp is lesser than last_update_timestamp on-chain + // but that can benefit off-chain components for simulation when clock is not synced and pool is high frequency trading + // furthermore, the function doesn't update fee in pre-swap, so quoting won't be affected + let elapsed = current_timestamp.saturating_sub(self.last_update_timestamp); // Not high frequency trade if elapsed >= self.filter_period as u64 { // Update sqrt of last transaction From 1a39d2d8d41f8d2aea58c1ded8d8199628c92c50 Mon Sep 17 00:00:00 2001 From: kampung-tech Date: Sun, 17 Aug 2025 19:14:50 +0800 Subject: [PATCH 04/10] Swap exact out + partial fill (#98) * feat: swap exact out * chore: refactor and add tests * chore: ci check * feat: rust-sdk quote partial fill, exact out * feat: rust sdk ci test * chore: fixes based on comments * chore: more fix * chore: more fix * chore: more fix * refactor (#101) * refactor * refactor sdk --------- Co-authored-by: andrewsource147 <31321699+andrewsource147@users.noreply.github.com> --- .github/workflows/ci.yml | 40 +- .../cp-amm/src/base_fee/fee_rate_limiter.rs | 167 +++++- programs/cp-amm/src/base_fee/fee_scheduler.rs | 45 +- programs/cp-amm/src/base_fee/mod.rs | 11 +- programs/cp-amm/src/curve.rs | 98 +++- programs/cp-amm/src/error.rs | 3 + programs/cp-amm/src/event.rs | 19 +- programs/cp-amm/src/instructions/mod.rs | 4 +- .../src/instructions/{ => swap}/ix_swap.rs | 128 +++-- programs/cp-amm/src/instructions/swap/mod.rs | 37 ++ .../src/instructions/swap/swap_exact_in.rs | 48 ++ .../src/instructions/swap/swap_exact_out.rs | 58 +++ .../instructions/swap/swap_partial_fill.rs | 58 +++ programs/cp-amm/src/lib.rs | 13 +- programs/cp-amm/src/math/safe_math.rs | 10 +- programs/cp-amm/src/math/utils_math.rs | 25 + programs/cp-amm/src/state/fee.rs | 170 +++++-- programs/cp-amm/src/state/pool.rs | 480 ++++++++++++++++-- .../cp-amm/src/tests/fee_scheduler_tests.rs | 23 +- .../cp-amm/src/tests/integration_tests.rs | 2 +- programs/cp-amm/src/tests/swap_tests.rs | 16 +- .../cp-amm/src/tests/test_rate_limiter.rs | 25 +- ...kdjv1hAeGQwAkZMxjb4otV5yvW7g72uviCaZZ.bin} | Bin ...d1uf9a8cKVJuZaJAU76t2EfLGbTmRbfvLLZp5j.bin | Bin 0 -> 1112 bytes rust-sdk/src/lib.rs | 5 +- rust-sdk/src/quote.rs | 63 --- rust-sdk/src/quote_exact_in.rs | 36 ++ rust-sdk/src/quote_exact_out.rs | 37 ++ rust-sdk/src/quote_partial_fill_in.rs | 38 ++ rust-sdk/src/tests/mod.rs | 11 +- rust-sdk/src/tests/test_quote_exact_in.rs | 41 +- rust-sdk/src/tests/test_quote_exact_out.rs | 41 ++ .../src/tests/test_quote_partial_fill_in.rs | 47 ++ rust-sdk/src/utils.rs | 26 + tests/bankrun-utils/cpAmm.ts | 159 +++++- tests/bankrun-utils/token2022.ts | 2 +- tests/claimFee.test.ts | 13 +- tests/claimPositionFee.test.ts | 10 +- tests/lockPosition.test.ts | 6 +- tests/rateLimiter.test.ts | 28 +- tests/splitPosition.test.ts | 8 +- tests/swap.test.ts | 224 +++++++- 42 files changed, 1929 insertions(+), 346 deletions(-) rename programs/cp-amm/src/instructions/{ => swap}/ix_swap.rs (74%) create mode 100644 programs/cp-amm/src/instructions/swap/mod.rs create mode 100644 programs/cp-amm/src/instructions/swap/swap_exact_in.rs create mode 100644 programs/cp-amm/src/instructions/swap/swap_exact_out.rs create mode 100644 programs/cp-amm/src/instructions/swap/swap_partial_fill.rs rename rust-sdk/fixtures/{pool.bin => 3u2BK3ykdjv1hAeGQwAkZMxjb4otV5yvW7g72uviCaZZ.bin} (100%) create mode 100644 rust-sdk/fixtures/CGPxT5d1uf9a8cKVJuZaJAU76t2EfLGbTmRbfvLLZp5j.bin delete mode 100644 rust-sdk/src/quote.rs create mode 100644 rust-sdk/src/quote_exact_in.rs create mode 100644 rust-sdk/src/quote_exact_out.rs create mode 100644 rust-sdk/src/quote_partial_fill_in.rs create mode 100644 rust-sdk/src/tests/test_quote_exact_out.rs create mode 100644 rust-sdk/src/tests/test_quote_partial_fill_in.rs create mode 100644 rust-sdk/src/utils.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6982faad..b7af32dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: pull_request: branches: - main - - staging + - release_0.1.4 env: SOLANA_CLI_VERSION: 2.1.0 @@ -28,6 +28,21 @@ jobs: files: | programs/cp-amm + rust_sdk_changed_files: + runs-on: ubuntu-latest + outputs: + rust_sdk: ${{steps.changed-files-specific.outputs.any_changed}} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Get specific changed files + id: changed-files-specific + uses: tj-actions/changed-files@v18.6 + with: + files: | + rust-sdk + anchor_build: runs-on: ubuntu-latest needs: program_changed_files @@ -39,7 +54,7 @@ jobs: - uses: ./.github/actions/setup-solana - uses: ./.github/actions/setup-dep - uses: ./.github/actions/setup-anchor - # Cache rust, cargo + # Cache rust, cargo - uses: Swatinem/rust-cache@v2 with: cache-targets: "true" @@ -51,7 +66,22 @@ jobs: - run: anchor build shell: bash - cargo_test: + rust_sdk_unit_test: + runs-on: ubuntu-latest + needs: [program_changed_files, rust_sdk_changed_files] + if: needs.program_changed_files.outputs.program == 'true' || needs.rust_sdk_changed_files.outputs.rust_sdk == 'true' + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ env.TOOLCHAIN }} + components: clippy + # Cache rust, cargo + - uses: Swatinem/rust-cache@v1 + - run: cargo test --package rust-sdk + shell: bash + + program_unit_test: runs-on: ubuntu-latest needs: program_changed_files if: needs.program_changed_files.outputs.program == 'true' @@ -66,7 +96,7 @@ jobs: - run: cargo test --package cp-amm shell: bash - program_test: + program_integration_test: runs-on: ubuntu-latest needs: program_changed_files if: needs.program_changed_files.outputs.program == 'true' @@ -83,7 +113,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ env.TOOLCHAIN }} - components: clippy + components: clippy # Cache rust, cargo - uses: Swatinem/rust-cache@v1 # Cache node_modules diff --git a/programs/cp-amm/src/base_fee/fee_rate_limiter.rs b/programs/cp-amm/src/base_fee/fee_rate_limiter.rs index cfc87554..cbc5bc0f 100644 --- a/programs/cp-amm/src/base_fee/fee_rate_limiter.rs +++ b/programs/cp-amm/src/base_fee/fee_rate_limiter.rs @@ -6,9 +6,9 @@ use crate::{ }, params::{fee_parameters::to_numerator, swap::TradeDirection}, safe_math::SafeMath, - state::CollectFeeMode, + state::{fee::PoolFeesStruct, CollectFeeMode}, u128x128_math::Rounding, - utils_math::safe_mul_div_cast_u64, + utils_math::{safe_mul_div_cast_u64, sqrt_u256}, PoolError, }; @@ -90,7 +90,7 @@ impl FeeRateLimiter { } // export function for testing - pub fn get_fee_numerator_from_amount(&self, input_amount: u64) -> Result { + pub fn get_fee_numerator_from_included_fee_amount(&self, input_amount: u64) -> Result { let fee_numerator = if input_amount <= self.reference_amount { self.cliff_fee_numerator } else { @@ -144,6 +144,142 @@ impl FeeRateLimiter { Ok(fee_numerator) } + + pub fn get_checked_amounts(&self) -> Result<(u64, u64, bool)> { + let max_index = self.get_max_index()?; + let x0 = U256::from(self.reference_amount); + let one = U256::from(1); + let max_index_input_amount = (U256::from(max_index) + one) * x0; + if max_index_input_amount <= U256::from(u64::MAX) { + let checked_included_fee_amount = max_index_input_amount + .try_into() + .map_err(|_| PoolError::TypeCastFailed)?; + let checked_output_amount = + self.get_excluded_fee_amount(checked_included_fee_amount)?; + Ok((checked_output_amount, checked_included_fee_amount, false)) + } else { + let checked_excluded_fee_amount = self.get_excluded_fee_amount(u64::MAX)?; + Ok((checked_excluded_fee_amount, u64::MAX, true)) + } + } + + pub fn get_excluded_fee_amount(&self, included_fee_amount: u64) -> Result { + let fee_numerator = self.get_fee_numerator_from_included_fee_amount(included_fee_amount)?; + let (excluded_fee_amount, _fee) = + PoolFeesStruct::get_excluded_fee_amount(fee_numerator, included_fee_amount)?; + Ok(excluded_fee_amount) + } + + // Ref: https://github.com/MeteoraAg/dynamic-bonding-curve/blob/dd40b7d4d53bf2254f395b9f52eb7f6850d24236/programs/dynamic-bonding-curve/src/base_fee/fee_rate_limiter.rs#L103 + pub fn get_fee_numerator_from_excluded_fee_amount( + &self, + excluded_fee_amount: u64, + ) -> Result { + let excluded_fee_reference_amount = self.get_excluded_fee_amount(self.reference_amount)?; + if excluded_fee_amount <= excluded_fee_reference_amount { + return Ok(self.cliff_fee_numerator); + } + let (checked_excluded_fee_amount, checked_included_fee_amount, is_overflow) = + self.get_checked_amounts()?; + // add the early check + if excluded_fee_amount == checked_excluded_fee_amount { + return self.get_fee_numerator_from_included_fee_amount(checked_included_fee_amount); + } + let included_fee_amount = if excluded_fee_amount < checked_excluded_fee_amount { + let two = U256::from(2); + let four = U256::from(4); + // d: fee denominator + // ex: excluded_fee_amount + // input_amount = x0 + (a * x0) + // fee = x0 * (c + c*a + i*a*(a+1)/2) / d + // fee = x0 * (a+1) * (c + i*a/2) / d + // fee = input_amount * (c + i * (input_amount/x0-1)/2) / d + // ex = input_amount - fee + // ex = input_amount - input_amount * (c + i * (input_amount/x0-1)/2) / d + // ex * d * 2 = input_amount * d * 2 - input_amount * (2 * c + i * (input_amount/x0-1)) + // ex * d * 2 * x0 = input_amount * d * 2 * x0 - input_amount * (2 * c * x0 + i * (input_amount-x0)) + // ex * d * 2 * x0 = input_amount * d * 2 * x0 - input_amount * (2 * c * x0 + i * input_amount- i*x0) + // ex * d * 2 * x0 = input_amount * d * 2 * x0 - input_amount * 2 * c * x0 - i * input_amount ^ 2 + input_amount * i*x0 + // i * input_amount ^ 2 - input_amount * (-2 * c * x0 + i*x0 + d * 2 * x0) + ex * d * 2 * x0 = 0 + // equation: x * input_amount ^ 2 - y * input_amount + z = 0 + // x = i, y = (-2 * c * x0 + i*x0 + d * 2 * x0), z = ex * d * 2 * x0 + // input_amount = (y +(-) sqrt(y^2 - 4xz)) / 2x + let i = U256::from(to_numerator( + self.fee_increment_bps.into(), + FEE_DENOMINATOR.into(), + )?); + let x0 = U256::from(self.reference_amount); + let d = U256::from(FEE_DENOMINATOR); + let c = U256::from(self.cliff_fee_numerator); + let ex = U256::from(excluded_fee_amount); + + let x = i; // x > 0 + let y = two * d * x0 + i * x0 - two * c * x0; // y is always greater than zero + let z = two * ex * d * x0; + + // solve quaratic equation + let included_fee_amount = (y - sqrt_u256(y * y - four * x * z) + .ok_or_else(|| PoolError::MathOverflow)?) + / (two * x); + let a_plus_one = included_fee_amount.safe_div(x0)?; + + let first_excluded_fee_amount = self.get_excluded_fee_amount( + included_fee_amount + .try_into() + .map_err(|_| PoolError::TypeCastFailed)?, + )?; + let excluded_fee_remaining_amount = + excluded_fee_amount.safe_sub(first_excluded_fee_amount)?; + + let remaining_amount_fee_numerator = c + i * a_plus_one; + + let (included_fee_remaining_amount, _) = PoolFeesStruct::get_included_fee_amount( + remaining_amount_fee_numerator + .try_into() + .map_err(|_| PoolError::TypeCastFailed)?, + excluded_fee_remaining_amount, + )?; + + let total_in_amount = + included_fee_amount.safe_add(U256::from(included_fee_remaining_amount))?; + let total_in_amount = total_in_amount + .try_into() + .map_err(|_| PoolError::TypeCastFailed)?; + total_in_amount + } else { + // excluded_fee_amount > checked_excluded_fee_amount + if is_overflow { + return Err(PoolError::MathOverflow.into()); + } + let excluded_fee_remaining_amount = + excluded_fee_amount.safe_sub(checked_excluded_fee_amount)?; + // remaining_amount should take the max fee + let (included_fee_remaining_amount, _) = PoolFeesStruct::get_included_fee_amount( + to_numerator(self.max_fee_bps.into(), FEE_DENOMINATOR.into())?, + excluded_fee_remaining_amount, + )?; + + let total_amount_in = + included_fee_remaining_amount.safe_add(checked_included_fee_amount)?; + total_amount_in + }; + + let trading_fee = included_fee_amount.safe_sub(excluded_fee_amount)?; + + let fee_numerator = safe_mul_div_cast_u64( + trading_fee, + FEE_DENOMINATOR, + included_fee_amount, + Rounding::Up, + )?; + + // sanity check + require!( + fee_numerator >= self.cliff_fee_numerator, + PoolError::UndeterminedError + ); + Ok(fee_numerator) + } } impl BaseFeeHandler for FeeRateLimiter { @@ -201,8 +337,8 @@ impl BaseFeeHandler for FeeRateLimiter { ); // validate max fee (more amount, then more fee) - let min_fee_numerator = self.get_fee_numerator_from_amount(0)?; - let max_fee_numerator = self.get_fee_numerator_from_amount(u64::MAX)?; + let min_fee_numerator = self.get_fee_numerator_from_included_fee_amount(0)?; + let max_fee_numerator = self.get_fee_numerator_from_included_fee_amount(u64::MAX)?; require!( min_fee_numerator >= MIN_FEE_NUMERATOR && max_fee_numerator <= MAX_FEE_NUMERATOR_V1, PoolError::InvalidFeeRateLimiter @@ -210,15 +346,30 @@ impl BaseFeeHandler for FeeRateLimiter { Ok(()) } - fn get_base_fee_numerator( + + fn get_base_fee_numerator_from_included_fee_amount( + &self, + current_point: u64, + activation_point: u64, + trade_direction: TradeDirection, + included_fee_amount: u64, + ) -> Result { + if self.is_rate_limiter_applied(current_point, activation_point, trade_direction)? { + self.get_fee_numerator_from_included_fee_amount(included_fee_amount) + } else { + Ok(self.cliff_fee_numerator) + } + } + + fn get_base_fee_numerator_from_excluded_fee_amount( &self, current_point: u64, activation_point: u64, trade_direction: TradeDirection, - input_amount: u64, + excluded_fee_amount: u64, ) -> Result { if self.is_rate_limiter_applied(current_point, activation_point, trade_direction)? { - self.get_fee_numerator_from_amount(input_amount) + self.get_fee_numerator_from_excluded_fee_amount(excluded_fee_amount) } else { Ok(self.cliff_fee_numerator) } diff --git a/programs/cp-amm/src/base_fee/fee_scheduler.rs b/programs/cp-amm/src/base_fee/fee_scheduler.rs index 607e4892..e41decd0 100644 --- a/programs/cp-amm/src/base_fee/fee_scheduler.rs +++ b/programs/cp-amm/src/base_fee/fee_scheduler.rs @@ -61,6 +61,22 @@ impl FeeScheduler { } } } + + pub fn get_base_fee_numerator(&self, current_point: u64, activation_point: u64) -> Result { + if self.period_frequency == 0 { + return Ok(self.cliff_fee_numerator); + } + // it means alpha-vault is buying + let period = if current_point < activation_point { + self.number_of_period.into() + } else { + let period = current_point + .safe_sub(activation_point)? + .safe_div(self.period_frequency)?; + period.min(self.number_of_period.into()) + }; + self.get_base_fee_numerator_by_period(period) + } } impl BaseFeeHandler for FeeScheduler { @@ -87,25 +103,24 @@ impl BaseFeeHandler for FeeScheduler { ); Ok(()) } - fn get_base_fee_numerator( + + fn get_base_fee_numerator_from_included_fee_amount( &self, current_point: u64, activation_point: u64, _trade_direction: TradeDirection, - _input_amount: u64, + _included_fee_amount: u64, ) -> Result { - if self.period_frequency == 0 { - return Ok(self.cliff_fee_numerator); - } - // it means alpha-vault is buying - let period = if current_point < activation_point { - self.number_of_period.into() - } else { - let period = current_point - .safe_sub(activation_point)? - .safe_div(self.period_frequency)?; - period.min(self.number_of_period.into()) - }; - self.get_base_fee_numerator_by_period(period) + self.get_base_fee_numerator(current_point, activation_point) + } + + fn get_base_fee_numerator_from_excluded_fee_amount( + &self, + current_point: u64, + activation_point: u64, + _trade_direction: TradeDirection, + _excluded_fee_amount: u64, + ) -> Result { + self.get_base_fee_numerator(current_point, activation_point) } } diff --git a/programs/cp-amm/src/base_fee/mod.rs b/programs/cp-amm/src/base_fee/mod.rs index 9a504b32..e32a1230 100644 --- a/programs/cp-amm/src/base_fee/mod.rs +++ b/programs/cp-amm/src/base_fee/mod.rs @@ -18,12 +18,19 @@ pub trait BaseFeeHandler { collect_fee_mode: CollectFeeMode, activation_type: ActivationType, ) -> Result<()>; - fn get_base_fee_numerator( + fn get_base_fee_numerator_from_included_fee_amount( &self, current_point: u64, activation_point: u64, trade_direction: TradeDirection, - input_amount: u64, + included_fee_amount: u64, + ) -> Result; + fn get_base_fee_numerator_from_excluded_fee_amount( + &self, + current_point: u64, + activation_point: u64, + trade_direction: TradeDirection, + excluded_fee_amount: u64, ) -> Result; } diff --git a/programs/cp-amm/src/curve.rs b/programs/cp-amm/src/curve.rs index 7cf2c7c8..1a2230f4 100644 --- a/programs/cp-amm/src/curve.rs +++ b/programs/cp-amm/src/curve.rs @@ -65,7 +65,7 @@ pub fn get_delta_amount_a_unsigned_unchecked( } /// Gets the delta amount_b for given liquidity and price range -/// * `Δb = L (√P_upper - √P_lower)` +/// Δb = L * (√P_upper - √P_lower) pub fn get_delta_amount_b_unsigned( lower_sqrt_price: u128, upper_sqrt_price: u128, @@ -82,7 +82,7 @@ pub fn get_delta_amount_b_unsigned( return Ok(result.try_into().map_err(|_| PoolError::TypeCastFailed)?); } -//Δb = L (√P_upper - √P_lower) +// Δb = L * (√P_upper - √P_lower) pub fn get_delta_amount_b_unsigned_unchecked( lower_sqrt_price: u128, upper_sqrt_price: u128, @@ -119,41 +119,60 @@ pub fn get_next_sqrt_price_from_input( // round to make sure that we don't pass the target price if a_for_b { - get_next_sqrt_price_from_amount_a_rounding_up(sqrt_price, liquidity, amount_in) + get_next_sqrt_price_from_amount_in_a_rounding_up(sqrt_price, liquidity, amount_in) } else { - get_next_sqrt_price_from_amount_b_rounding_down(sqrt_price, liquidity, amount_in) + get_next_sqrt_price_from_amount_in_b_rounding_down(sqrt_price, liquidity, amount_in) + } +} + +/// Gets the next sqrt price given an output amount of token_a or token_b +/// Throws if price or liquidity are 0, or if the next price is out of bounds +pub fn get_next_sqrt_price_from_output( + sqrt_price: u128, + liquidity: u128, + amount_out: u64, + a_for_b: bool, +) -> Result { + assert!(sqrt_price > 0); + assert!(liquidity > 0); + + // round to make sure that we don't pass the target price + if a_for_b { + get_next_sqrt_price_from_amount_out_b_rounding_down(sqrt_price, liquidity, amount_out) + } else { + get_next_sqrt_price_from_amount_out_a_rounding_up(sqrt_price, liquidity, amount_out) } } /// Gets the next sqrt price √P' given a delta of token_a /// /// Always round up because -/// 1. In the exact output case, token 0 supply decreases leading to price increase. +/// 1. In the exact output case, token_a supply decreases leading to price increase. /// Move price up so that exact output is met. -/// 2. In the exact input case, token 0 supply increases leading to price decrease. +/// 2. In the exact input case, token_a supply increases leading to price decrease. /// Do not round down to minimize price impact. We only need to meet input /// change and not guarantee exact output. /// -/// Use function for exact input or exact output swaps for token 0 +/// Use function for exact input or exact output swaps for token_a /// /// # Formula /// -/// * `√P' = √P * L / (L + Δx * √P)` -/// * If Δx * √P overflows, use alternate form `√P' = L / (L/√P + Δx)` +/// * `√P' = √P * L / (L + Δa * √P)` +/// * If Δa * √P overflows, use alternate form `√P' = L / (L/√P + Δa)` /// /// # Proof /// /// For constant L, /// -/// L = x * √P -/// x' = x + Δx -/// x' * √P' = x * √P -/// (x + Δx) * √P' = x * √P -/// √P' = (x * √P) / (x + Δx) -/// x = L/√P -/// √P' = √P * L / (L + Δx * √P) +/// L = a * √P +/// a' = a + Δa +/// a' * √P' = a * √P +/// (a + Δa) * √P' = a * √P +/// √P' = (a * √P) / (a + Δa) +/// a = L/√P +/// √P' = √P * L / (L + Δa * √P) /// -pub fn get_next_sqrt_price_from_amount_a_rounding_up( +pub fn get_next_sqrt_price_from_amount_in_a_rounding_up( sqrt_price: u128, liquidity: u128, amount: u64, @@ -171,21 +190,40 @@ pub fn get_next_sqrt_price_from_amount_a_rounding_up( return Ok(result.try_into().map_err(|_| PoolError::TypeCastFailed)?); } +/// √P' = √P * L / (L - Δa * √P) +pub fn get_next_sqrt_price_from_amount_out_a_rounding_up( + sqrt_price: u128, + liquidity: u128, + amount: u64, +) -> Result { + if amount == 0 { + return Ok(sqrt_price); + } + let sqrt_price = U256::from(sqrt_price); + let liquidity = U256::from(liquidity); + + let product = U256::from(amount).safe_mul(sqrt_price)?; + let denominator = liquidity.safe_sub(U256::from(product))?; + let result = mul_div_u256(liquidity, sqrt_price, denominator, Rounding::Up) + .ok_or_else(|| PoolError::MathOverflow)?; + return Ok(result.try_into().map_err(|_| PoolError::TypeCastFailed)?); +} + /// Gets the next sqrt price given a delta of token_b /// /// Always round down because -/// 1. In the exact output case, token 1 supply decreases leading to price decrease. -/// Move price down by rounding down so that exact output of token 0 is met. -/// 2. In the exact input case, token 1 supply increases leading to price increase. +/// 1. In the exact output case, token_b supply decreases leading to price decrease. +/// Move price down by rounding down so that exact output of token_a is met. +/// 2. In the exact input case, token_b supply increases leading to price increase. /// Do not round down to minimize price impact. We only need to meet input -/// change and not gurantee exact output for token 0. +/// change and not guarantee exact output for token_a. /// /// /// # Formula /// -/// * `√P' = √P + Δy / L` +/// * `√P' = √P + Δb / L` /// -pub fn get_next_sqrt_price_from_amount_b_rounding_down( +pub fn get_next_sqrt_price_from_amount_in_b_rounding_down( sqrt_price: u128, liquidity: u128, amount: u64, @@ -197,3 +235,17 @@ pub fn get_next_sqrt_price_from_amount_b_rounding_down( let result = U256::from(sqrt_price).safe_add(quotient)?; Ok(result.try_into().map_err(|_| PoolError::TypeCastFailed)?) } + +/// `√P' = √P - Δb / L` +pub fn get_next_sqrt_price_from_amount_out_b_rounding_down( + sqrt_price: u128, + liquidity: u128, + amount: u64, +) -> Result { + let quotient = U256::from(amount) + .safe_shl((RESOLUTION * 2) as usize)? + .div_ceil(U256::from(liquidity)); + + let result = U256::from(sqrt_price).safe_sub(quotient)?; + Ok(result.try_into().map_err(|_| PoolError::TypeCastFailed)?) +} diff --git a/programs/cp-amm/src/error.rs b/programs/cp-amm/src/error.rs index d9d3852f..2e0de378 100644 --- a/programs/cp-amm/src/error.rs +++ b/programs/cp-amm/src/error.rs @@ -157,4 +157,7 @@ pub enum PoolError { #[msg("Invalid fee scheduler")] InvalidFeeScheduler, + + #[msg("Undetermined error")] + UndeterminedError, } diff --git a/programs/cp-amm/src/event.rs b/programs/cp-amm/src/event.rs index 88f972c9..49ee2b5b 100644 --- a/programs/cp-amm/src/event.rs +++ b/programs/cp-amm/src/event.rs @@ -3,8 +3,9 @@ use anchor_lang::prelude::*; use crate::{ params::fee_parameters::PoolFeeParameters, - state::{SplitAmountInfo, SplitPositionInfo, SwapResult}, + state::{SplitAmountInfo, SplitPositionInfo, SwapResult, SwapResult2}, AddLiquidityParameters, RemoveLiquidityParameters, SplitPositionParameters, SwapParameters, + SwapParameters2, }; /// Close config @@ -140,6 +141,22 @@ pub struct EvtSwap { pub current_timestamp: u64, } +#[derive(Clone, Copy)] +#[event] +pub struct EvtSwap2 { + pub pool: Pubkey, + pub trade_direction: u8, + pub collect_fee_mode: u8, + pub has_referral: bool, + pub params: SwapParameters2, + // excluded_transfer_fee_amount_in is swap_result.included_fee_amount_in + pub swap_result: SwapResult2, + pub included_transfer_fee_amount_in: u64, + pub included_transfer_fee_amount_out: u64, + pub excluded_transfer_fee_amount_out: u64, + pub current_timestamp: u64, +} + #[event] pub struct EvtLockPosition { pub pool: Pubkey, diff --git a/programs/cp-amm/src/instructions/mod.rs b/programs/cp-amm/src/instructions/mod.rs index a9892964..2b5fed40 100644 --- a/programs/cp-amm/src/instructions/mod.rs +++ b/programs/cp-amm/src/instructions/mod.rs @@ -1,7 +1,7 @@ pub mod admin; pub use admin::*; -pub mod ix_swap; -pub use ix_swap::*; +pub mod swap; +pub use swap::*; pub mod ix_add_liquidity; pub use ix_add_liquidity::*; pub mod ix_create_position; diff --git a/programs/cp-amm/src/instructions/ix_swap.rs b/programs/cp-amm/src/instructions/swap/ix_swap.rs similarity index 74% rename from programs/cp-amm/src/instructions/ix_swap.rs rename to programs/cp-amm/src/instructions/swap/ix_swap.rs index 7d26893b..1876dec1 100644 --- a/programs/cp-amm/src/instructions/ix_swap.rs +++ b/programs/cp-amm/src/instructions/swap/ix_swap.rs @@ -3,10 +3,12 @@ use crate::{ const_pda, get_pool_access_validator, instruction::Swap as SwapInstruction, params::swap::TradeDirection, + process_swap_exact_in, process_swap_exact_out, process_swap_partial_fill, safe_math::SafeMath, - state::{fee::FeeMode, Pool}, - token::{calculate_transfer_fee_excluded_amount, transfer_from_pool, transfer_from_user}, - EvtSwap, PoolError, + state::{fee::FeeMode, Pool, SwapResult2}, + swap::{ProcessSwapParams, ProcessSwapResult}, + token::{transfer_from_pool, transfer_from_user}, + EvtSwap, EvtSwap2, PoolError, }; use anchor_lang::solana_program::sysvar; use anchor_lang::{ @@ -14,6 +16,18 @@ use anchor_lang::{ solana_program::instruction::{get_processed_sibling_instruction, get_stack_height}, }; use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; +use num_enum::{FromPrimitive, IntoPrimitive}; + +#[repr(u8)] +#[derive( + Clone, Copy, Debug, PartialEq, IntoPrimitive, FromPrimitive, AnchorDeserialize, AnchorSerialize, +)] +pub enum SwapMode { + #[num_enum(default)] + ExactIn, + PartialFill, + ExactOut, +} #[derive(AnchorSerialize, AnchorDeserialize)] pub struct SwapParameters { @@ -21,6 +35,16 @@ pub struct SwapParameters { pub minimum_amount_out: u64, } +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy)] +pub struct SwapParameters2 { + /// When it's exact in, partial fill, this will be amount_in. When it's exact out, this will be amount_out + pub amount_0: u64, + /// When it's exact in, partial fill, this will be minimum_amount_out. When it's exact out, this will be maximum_amount_in + pub amount_1: u64, + /// Swap mode, refer [SwapMode] + pub swap_mode: u8, +} + #[event_cpi] #[derive(Accounts)] pub struct SwapCtx<'info> { @@ -80,8 +104,14 @@ impl<'info> SwapCtx<'info> { } } -// TODO impl swap exact out -pub fn handle_swap(ctx: Context, params: SwapParameters) -> Result<()> { +pub fn handle_swap_wrapper(ctx: &Context, params: SwapParameters2) -> Result<()> { + let SwapParameters2 { + amount_0, + amount_1, + swap_mode, + .. + } = params; + { let pool = ctx.accounts.pool.load()?; let access_validator = get_pool_access_validator(&pool)?; @@ -91,12 +121,9 @@ pub fn handle_swap(ctx: Context, params: SwapParameters) -> Result<()> ); } - let SwapParameters { - amount_in, - minimum_amount_out, - } = params; - + let swap_mode = SwapMode::try_from(swap_mode).map_err(|_| PoolError::InvalidInput)?; let trade_direction = ctx.accounts.get_trade_direction(); + let ( token_in_mint, token_out_mint, @@ -123,13 +150,10 @@ pub fn handle_swap(ctx: Context, params: SwapParameters) -> Result<()> ), }; - let transfer_fee_excluded_amount_in = - calculate_transfer_fee_excluded_amount(&token_in_mint, amount_in)?.amount; - - require!(transfer_fee_excluded_amount_in > 0, PoolError::AmountIsZero); + // redundant validation, but we can just keep it + require!(amount_0 > 0, PoolError::AmountIsZero); let has_referral = ctx.accounts.referral_token_account.is_some(); - let mut pool = ctx.accounts.pool.load_mut()?; let current_point = ActivationHandler::get_current_point(pool.activation_type)?; @@ -149,42 +173,59 @@ pub fn handle_swap(ctx: Context, params: SwapParameters) -> Result<()> let current_timestamp = Clock::get()?.unix_timestamp as u64; pool.update_pre_swap(current_timestamp)?; - let fee_mode = &FeeMode::get_fee_mode(pool.collect_fee_mode, trade_direction, has_referral)?; + let fee_mode = FeeMode::get_fee_mode(pool.collect_fee_mode, trade_direction, has_referral)?; - let swap_result = pool.get_swap_result( - transfer_fee_excluded_amount_in, - fee_mode, + let process_swap_params = ProcessSwapParams { + pool: &pool, + token_in_mint, + token_out_mint, + amount_0, + amount_1, + fee_mode: &fee_mode, trade_direction, current_point, - )?; - - let transfer_fee_excluded_amount_out = - calculate_transfer_fee_excluded_amount(&token_out_mint, swap_result.output_amount)?.amount; - require!( - transfer_fee_excluded_amount_out >= minimum_amount_out, - PoolError::ExceededSlippage - ); + }; - pool.apply_swap_result(&swap_result, fee_mode, current_timestamp)?; + let ProcessSwapResult { + swap_in_parameters, + swap_result, + included_transfer_fee_amount_in, + excluded_transfer_fee_amount_out, + included_transfer_fee_amount_out, + } = match swap_mode { + SwapMode::ExactIn => process_swap_exact_in(process_swap_params), + SwapMode::PartialFill => process_swap_partial_fill(process_swap_params), + SwapMode::ExactOut => process_swap_exact_out(process_swap_params), + }?; + + pool.apply_swap_result(&swap_result, &fee_mode, current_timestamp)?; + + let SwapResult2 { + included_fee_input_amount, + referral_fee, + .. + } = swap_result; // send to reserve transfer_from_user( &ctx.accounts.payer, token_in_mint, &ctx.accounts.input_token_account, - &input_vault_account, + input_vault_account, input_program, - amount_in, + included_transfer_fee_amount_in, )?; + // send to user transfer_from_pool( ctx.accounts.pool_authority.to_account_info(), - &token_out_mint, - &output_vault_account, + token_out_mint, + output_vault_account, &ctx.accounts.output_token_account, output_program, - swap_result.output_amount, + included_transfer_fee_amount_out, )?; + // send to referral if has_referral { if fee_mode.fees_on_token_a { @@ -194,7 +235,7 @@ pub fn handle_swap(ctx: Context, params: SwapParameters) -> Result<()> &ctx.accounts.token_a_vault, &ctx.accounts.referral_token_account.clone().unwrap(), &ctx.accounts.token_a_program, - swap_result.referral_fee, + referral_fee, )?; } else { transfer_from_pool( @@ -203,7 +244,7 @@ pub fn handle_swap(ctx: Context, params: SwapParameters) -> Result<()> &ctx.accounts.token_b_vault, &ctx.accounts.referral_token_account.clone().unwrap(), &ctx.accounts.token_b_program, - swap_result.referral_fee, + referral_fee, )?; } } @@ -211,11 +252,24 @@ pub fn handle_swap(ctx: Context, params: SwapParameters) -> Result<()> emit_cpi!(EvtSwap { pool: ctx.accounts.pool.key(), trade_direction: trade_direction.into(), + has_referral, + params: swap_in_parameters, + swap_result: swap_result.into(), + actual_amount_in: included_fee_input_amount, + current_timestamp + }); + + emit_cpi!(EvtSwap2 { + pool: ctx.accounts.pool.key(), + trade_direction: trade_direction.into(), + collect_fee_mode: pool.collect_fee_mode, + has_referral, params, swap_result, - has_referral, - actual_amount_in: transfer_fee_excluded_amount_in, current_timestamp, + included_transfer_fee_amount_in, + included_transfer_fee_amount_out, + excluded_transfer_fee_amount_out, }); Ok(()) diff --git a/programs/cp-amm/src/instructions/swap/mod.rs b/programs/cp-amm/src/instructions/swap/mod.rs new file mode 100644 index 00000000..8db749b1 --- /dev/null +++ b/programs/cp-amm/src/instructions/swap/mod.rs @@ -0,0 +1,37 @@ +pub mod ix_swap; +use anchor_lang::prelude::InterfaceAccount; +use anchor_spl::token_interface::Mint; +pub use ix_swap::*; + +pub mod swap_exact_in; +pub use swap_exact_in::*; + +pub mod swap_partial_fill; +pub use swap_partial_fill::*; + +pub mod swap_exact_out; +pub use swap_exact_out::*; + +use crate::{ + params::swap::TradeDirection, + state::{fee::FeeMode, Pool, SwapResult2}, +}; + +pub struct ProcessSwapParams<'a, 'b, 'info> { + pub pool: &'a Pool, + pub token_in_mint: &'b InterfaceAccount<'info, Mint>, + pub token_out_mint: &'b InterfaceAccount<'info, Mint>, + pub fee_mode: &'a FeeMode, + pub trade_direction: TradeDirection, + pub current_point: u64, + pub amount_0: u64, + pub amount_1: u64, +} + +pub struct ProcessSwapResult { + swap_result: SwapResult2, + swap_in_parameters: SwapParameters, + included_transfer_fee_amount_in: u64, + included_transfer_fee_amount_out: u64, + excluded_transfer_fee_amount_out: u64, +} diff --git a/programs/cp-amm/src/instructions/swap/swap_exact_in.rs b/programs/cp-amm/src/instructions/swap/swap_exact_in.rs new file mode 100644 index 00000000..43120e93 --- /dev/null +++ b/programs/cp-amm/src/instructions/swap/swap_exact_in.rs @@ -0,0 +1,48 @@ +use crate::{ + swap::{ProcessSwapParams, ProcessSwapResult}, + token::calculate_transfer_fee_excluded_amount, + PoolError, SwapParameters, +}; +use anchor_lang::prelude::*; + +pub fn process_swap_exact_in<'a, 'b, 'info>( + params: ProcessSwapParams<'a, 'b, 'info>, +) -> Result { + let ProcessSwapParams { + amount_0: amount_in, + amount_1: minimum_amount_out, + pool, + token_in_mint, + token_out_mint, + fee_mode, + trade_direction, + current_point, + } = params; + + let excluded_transfer_fee_amount_in = + calculate_transfer_fee_excluded_amount(token_in_mint, amount_in)?.amount; + + require!(excluded_transfer_fee_amount_in > 0, PoolError::AmountIsZero); + + let swap_result = + pool.get_swap_result_from_exact_input(amount_in, fee_mode, trade_direction, current_point)?; + + let excluded_transfer_fee_amount_out = + calculate_transfer_fee_excluded_amount(token_out_mint, swap_result.output_amount)?.amount; + + require!( + excluded_transfer_fee_amount_out >= minimum_amount_out, + PoolError::ExceededSlippage + ); + + Ok(ProcessSwapResult { + swap_result, + swap_in_parameters: SwapParameters { + amount_in, + minimum_amount_out, + }, + included_transfer_fee_amount_in: amount_in, + included_transfer_fee_amount_out: swap_result.output_amount, + excluded_transfer_fee_amount_out, + }) +} diff --git a/programs/cp-amm/src/instructions/swap/swap_exact_out.rs b/programs/cp-amm/src/instructions/swap/swap_exact_out.rs new file mode 100644 index 00000000..cfa90b9d --- /dev/null +++ b/programs/cp-amm/src/instructions/swap/swap_exact_out.rs @@ -0,0 +1,58 @@ +use anchor_lang::prelude::*; + +use crate::{ + swap::{ProcessSwapParams, ProcessSwapResult}, + token::calculate_transfer_fee_included_amount, + PoolError, SwapParameters, +}; + +pub fn process_swap_exact_out<'a, 'b, 'info>( + params: ProcessSwapParams<'a, 'b, 'info>, +) -> Result { + let ProcessSwapParams { + pool, + token_in_mint, + token_out_mint, + fee_mode, + trade_direction, + current_point, + amount_0: amount_out, + amount_1: maximum_amount_in, + } = params; + + let included_transfer_fee_amount_out = + calculate_transfer_fee_included_amount(token_out_mint, amount_out)?.amount; + require!( + included_transfer_fee_amount_out > 0, + PoolError::AmountIsZero + ); + + let swap_result = pool.get_swap_result_from_exact_output( + included_transfer_fee_amount_out, + fee_mode, + trade_direction, + current_point, + )?; + + let included_transfer_fee_amount_in = calculate_transfer_fee_included_amount( + token_in_mint, + swap_result.included_fee_input_amount, + )? + .amount; + + require!( + included_transfer_fee_amount_in <= maximum_amount_in, + PoolError::ExceededSlippage + ); + + Ok(ProcessSwapResult { + swap_result, + swap_in_parameters: SwapParameters { + amount_in: included_transfer_fee_amount_in, + minimum_amount_out: amount_out, + }, + included_transfer_fee_amount_in, + included_transfer_fee_amount_out, + excluded_transfer_fee_amount_out: amount_out, + }) +} diff --git a/programs/cp-amm/src/instructions/swap/swap_partial_fill.rs b/programs/cp-amm/src/instructions/swap/swap_partial_fill.rs new file mode 100644 index 00000000..9fafc3d5 --- /dev/null +++ b/programs/cp-amm/src/instructions/swap/swap_partial_fill.rs @@ -0,0 +1,58 @@ +use crate::{ + swap::{ProcessSwapParams, ProcessSwapResult}, + token::{calculate_transfer_fee_excluded_amount, calculate_transfer_fee_included_amount}, + PoolError, SwapParameters, +}; +use anchor_lang::prelude::*; + +pub fn process_swap_partial_fill<'a, 'b, 'info>( + params: ProcessSwapParams<'a, 'b, 'info>, +) -> Result { + let ProcessSwapParams { + pool, + token_in_mint, + token_out_mint, + amount_0: amount_in, + amount_1: minimum_amount_out, + fee_mode, + trade_direction, + current_point, + } = params; + + let excluded_transfer_fee_amount_in = + calculate_transfer_fee_excluded_amount(token_in_mint, amount_in)?.amount; + + require!(excluded_transfer_fee_amount_in > 0, PoolError::AmountIsZero); + + let swap_result = pool.get_swap_result_from_partial_input( + amount_in, + fee_mode, + trade_direction, + current_point, + )?; + + let excluded_transfer_fee_amount_out = + calculate_transfer_fee_excluded_amount(token_out_mint, swap_result.output_amount)?.amount; + + require!( + excluded_transfer_fee_amount_out >= minimum_amount_out, + PoolError::ExceededSlippage + ); + + let transfer_fee_included_consumed_in_amount = calculate_transfer_fee_included_amount( + token_in_mint, + swap_result.included_fee_input_amount, + )? + .amount; + + Ok(ProcessSwapResult { + swap_result, + swap_in_parameters: SwapParameters { + amount_in: transfer_fee_included_consumed_in_amount, + minimum_amount_out, + }, + included_transfer_fee_amount_in: transfer_fee_included_consumed_in_amount, + included_transfer_fee_amount_out: swap_result.output_amount, + excluded_transfer_fee_amount_out, + }) +} diff --git a/programs/cp-amm/src/lib.rs b/programs/cp-amm/src/lib.rs index d5e5bbaf..9b02c429 100644 --- a/programs/cp-amm/src/lib.rs +++ b/programs/cp-amm/src/lib.rs @@ -198,7 +198,18 @@ pub mod cp_amm { } pub fn swap(ctx: Context, params: SwapParameters) -> Result<()> { - instructions::handle_swap(ctx, params) + instructions::swap::handle_swap_wrapper( + &ctx, + SwapParameters2 { + amount_0: params.amount_in, + amount_1: params.minimum_amount_out, + swap_mode: SwapMode::ExactIn.into(), + }, + ) + } + + pub fn swap2(ctx: Context, params: SwapParameters2) -> Result<()> { + instructions::swap::handle_swap_wrapper(&ctx, params) } pub fn claim_position_fee(ctx: Context) -> Result<()> { diff --git a/programs/cp-amm/src/math/safe_math.rs b/programs/cp-amm/src/math/safe_math.rs index 45549642..2794bbde 100644 --- a/programs/cp-amm/src/math/safe_math.rs +++ b/programs/cp-amm/src/math/safe_math.rs @@ -17,7 +17,7 @@ pub trait SafeMath: Sized { macro_rules! checked_impl { ($t:ty, $offset:ty) => { impl SafeMath<$offset> for $t { - #[inline(always)] + #[track_caller] fn safe_add(self, v: $t) -> Result<$t, PoolError> { match self.checked_add(v) { Some(result) => Ok(result), @@ -29,7 +29,7 @@ macro_rules! checked_impl { } } - #[inline(always)] + #[track_caller] fn safe_sub(self, v: $t) -> Result<$t, PoolError> { match self.checked_sub(v) { Some(result) => Ok(result), @@ -41,7 +41,7 @@ macro_rules! checked_impl { } } - #[inline(always)] + #[track_caller] fn safe_mul(self, v: $t) -> Result<$t, PoolError> { match self.checked_mul(v) { Some(result) => Ok(result), @@ -77,7 +77,7 @@ macro_rules! checked_impl { } } - #[inline(always)] + #[track_caller] fn safe_shl(self, v: $offset) -> Result<$t, PoolError> { match self.checked_shl(v) { Some(result) => Ok(result), @@ -89,7 +89,7 @@ macro_rules! checked_impl { } } - #[inline(always)] + #[track_caller] fn safe_shr(self, v: $offset) -> Result<$t, PoolError> { match self.checked_shr(v) { Some(result) => Ok(result), diff --git a/programs/cp-amm/src/math/utils_math.rs b/programs/cp-amm/src/math/utils_math.rs index 6a259e7b..fee4cd84 100644 --- a/programs/cp-amm/src/math/utils_math.rs +++ b/programs/cp-amm/src/math/utils_math.rs @@ -54,3 +54,28 @@ pub fn safe_shl_div_cast( T::from_u128(shl_div(x, y, offset, rounding).ok_or_else(|| PoolError::MathOverflow)?) .ok_or_else(|| PoolError::TypeCastFailed.into()) } + +// Ref: https://github.com/MeteoraAg/dynamic-bonding-curve/blob/dd40b7d4d53bf2254f395b9f52eb7f6850d24236/programs/dynamic-bonding-curve/src/math/utils_math.rs#L74 +pub fn sqrt_u256(radicand: U256) -> Option { + if radicand == U256::ZERO { + return Some(U256::ZERO); + } + // Compute bit, the largest power of 4 <= n + let max_shift = U256::ZERO.leading_zeros() - 1; + let shift = (max_shift - radicand.leading_zeros()) & !1; + let mut bit = U256::ONE.checked_shl(shift)?; + + let mut n = radicand; + let mut result = U256::ZERO; + while bit != U256::ZERO { + let result_with_bit = result.checked_add(bit)?; + if n >= result_with_bit { + n = n.checked_sub(result_with_bit)?; + result = result.checked_shr(1)?.checked_add(bit)?; + } else { + result = result.checked_shr(1)?; + } + (bit, _) = bit.overflowing_shr(2); + } + Some(result) +} diff --git a/programs/cp-amm/src/state/fee.rs b/programs/cp-amm/src/state/fee.rs index 17dbe195..2b414f38 100644 --- a/programs/cp-amm/src/state/fee.rs +++ b/programs/cp-amm/src/state/fee.rs @@ -1,11 +1,9 @@ -use std::u64; - use anchor_lang::prelude::*; use num_enum::{IntoPrimitive, TryFromPrimitive}; use static_assertions::const_assert_eq; use crate::{ - base_fee::{get_base_fee_handler, FeeRateLimiter}, + base_fee::{get_base_fee_handler, BaseFeeHandler, FeeRateLimiter}, constants::{fee::FEE_DENOMINATOR, BASIS_POINT_MAX, ONE_Q64}, params::swap::TradeDirection, safe_math::SafeMath, @@ -16,11 +14,10 @@ use crate::{ use super::CollectFeeMode; -/// Encodes all results of swapping #[derive(Debug, PartialEq)] pub struct FeeOnAmountResult { pub amount: u64, - pub lp_fee: u64, + pub trading_fee: u64, pub protocol_fee: u64, pub partner_fee: u64, pub referral_fee: u64, @@ -124,83 +121,146 @@ impl BaseFeeStruct { } } - pub fn get_current_base_fee_numerator( - &self, - current_point: u64, - activation_point: u64, - amount: u64, - trade_direction: TradeDirection, - ) -> Result { - let base_fee_handler = get_base_fee_handler( + pub fn get_base_fee_handler(&self) -> Result> { + get_base_fee_handler( self.cliff_fee_numerator, self.first_factor, self.second_factor, self.third_factor, self.base_fee_mode, - )?; - - base_fee_handler.get_base_fee_numerator( - current_point, - activation_point, - trade_direction, - amount, ) } } impl PoolFeesStruct { + fn get_total_fee_numerator( + &self, + base_fee_numerator: u64, + max_fee_numerator: u64, + ) -> Result { + let dynamic_fee = self.dynamic_fee.get_variable_fee()?; + let total_fee_numerator = dynamic_fee.safe_add(base_fee_numerator.into())?; + let total_fee_numerator: u64 = total_fee_numerator + .try_into() + .map_err(|_| PoolError::TypeCastFailed)?; + + if total_fee_numerator > max_fee_numerator { + Ok(max_fee_numerator) + } else { + Ok(total_fee_numerator) + } + } + // in numerator - pub fn get_total_trading_fee( + pub fn get_total_trading_fee_from_included_fee_amount( &self, current_point: u64, activation_point: u64, - amount: u64, + included_fee_amount: u64, trade_direction: TradeDirection, - ) -> Result { - let base_fee_numerator = self.base_fee.get_current_base_fee_numerator( + max_fee_numerator: u64, + ) -> Result { + let base_fee_handler = self.base_fee.get_base_fee_handler()?; + + let base_fee_numerator = base_fee_handler.get_base_fee_numerator_from_included_fee_amount( current_point, activation_point, - amount, trade_direction, + included_fee_amount, )?; - let total_fee_numerator = self - .dynamic_fee - .get_variable_fee()? - .safe_add(base_fee_numerator.into())?; - Ok(total_fee_numerator) + + self.get_total_fee_numerator(base_fee_numerator, max_fee_numerator) } - pub fn get_fee_on_amount( + pub fn get_total_trading_fee_from_excluded_fee_amount( &self, - amount: u64, - has_referral: bool, current_point: u64, activation_point: u64, - has_partner: bool, + excluded_fee_amount: u64, trade_direction: TradeDirection, max_fee_numerator: u64, + ) -> Result { + let base_fee_handler = self.base_fee.get_base_fee_handler()?; + + let base_fee_numerator = base_fee_handler.get_base_fee_numerator_from_excluded_fee_amount( + current_point, + activation_point, + trade_direction, + excluded_fee_amount, + )?; + + self.get_total_fee_numerator(base_fee_numerator, max_fee_numerator) + } + + pub fn get_fee_on_amount( + &self, + amount: u64, + trade_fee_numerator: u64, + has_referral: bool, + has_partner: bool, ) -> Result { - let trade_fee_numerator = - self.get_total_trading_fee(current_point, activation_point, amount, trade_direction)?; - let trade_fee_numerator = if trade_fee_numerator > max_fee_numerator.into() { - max_fee_numerator - } else { - trade_fee_numerator.try_into().unwrap() - }; + let (amount, trading_fee) = + PoolFeesStruct::get_excluded_fee_amount(trade_fee_numerator, amount)?; + + let SplitFees { + trading_fee, + protocol_fee, + referral_fee, + partner_fee, + } = self.split_fees(trading_fee, has_referral, has_partner)?; + + Ok(FeeOnAmountResult { + amount, + trading_fee, + protocol_fee, + partner_fee, + referral_fee, + }) + } - let lp_fee: u64 = - safe_mul_div_cast_u64(amount, trade_fee_numerator, FEE_DENOMINATOR, Rounding::Up)?; - // update amount - let amount = amount.safe_sub(lp_fee)?; + pub fn get_excluded_fee_amount( + trade_fee_numerator: u64, + included_fee_amount: u64, + ) -> Result<(u64, u64)> { + let trading_fee: u64 = safe_mul_div_cast_u64( + included_fee_amount, + trade_fee_numerator, + FEE_DENOMINATOR, + Rounding::Up, + )?; + let excluded_fee_amount = included_fee_amount.safe_sub(trading_fee)?; + Ok((excluded_fee_amount, trading_fee)) + } + + pub fn get_included_fee_amount( + trade_fee_numerator: u64, + excluded_fee_amount: u64, + ) -> Result<(u64, u64)> { + let included_fee_amount: u64 = safe_mul_div_cast_u64( + excluded_fee_amount, + FEE_DENOMINATOR, + FEE_DENOMINATOR.safe_sub(trade_fee_numerator)?, + Rounding::Up, + )?; + let fee_amount = included_fee_amount.safe_sub(excluded_fee_amount)?; + Ok((included_fee_amount, fee_amount)) + } + pub fn split_fees( + &self, + fee_amount: u64, + has_referral: bool, + has_partner: bool, + ) -> Result { let protocol_fee = safe_mul_div_cast_u64( - lp_fee, + fee_amount, self.protocol_fee_percent.into(), 100, Rounding::Down, )?; - // update lp fee - let lp_fee = lp_fee.safe_sub(protocol_fee)?; + + // update trading fee + let trading_fee: u64 = fee_amount.safe_sub(protocol_fee)?; let referral_fee = if has_referral { safe_mul_div_cast_u64( @@ -228,12 +288,11 @@ impl PoolFeesStruct { let protocol_fee = protocol_fee_after_referral_fee.safe_sub(partner_fee)?; - Ok(FeeOnAmountResult { - amount, - lp_fee, + Ok(SplitFees { + trading_fee, protocol_fee, - partner_fee, referral_fee, + partner_fee, }) } } @@ -382,6 +441,13 @@ impl FeeMode { } } +pub struct SplitFees { + pub trading_fee: u64, + pub protocol_fee: u64, + pub referral_fee: u64, + pub partner_fee: u64, +} + #[cfg(test)] mod tests { use crate::{params::swap::TradeDirection, state::CollectFeeMode}; diff --git a/programs/cp-amm/src/state/pool.rs b/programs/cp-amm/src/state/pool.rs index 1ac51b0d..25b0690f 100644 --- a/programs/cp-amm/src/state/pool.rs +++ b/programs/cp-amm/src/state/pool.rs @@ -1,11 +1,12 @@ use ruint::aliases::U256; use static_assertions::const_assert_eq; use std::cmp::min; -use std::u64; use anchor_lang::prelude::*; use num_enum::{IntoPrimitive, TryFromPrimitive}; +use crate::curve::get_next_sqrt_price_from_output; +use crate::state::fee::{FeeOnAmountResult, SplitFees}; use crate::{ assert_eq_admin, constants::{ @@ -19,7 +20,7 @@ use crate::{ params::swap::TradeDirection, safe_math::SafeMath, state::{ - fee::{DynamicFeeStruct, FeeOnAmountResult, PoolFeesStruct}, + fee::{DynamicFeeStruct, PoolFeesStruct}, Position, SplitFeeAmount, }, u128x128_math::{shl_div_256, Rounding}, @@ -421,45 +422,157 @@ impl Pool { self.reward_infos[0].initialized() || self.reward_infos[1].initialized() } - pub fn get_swap_result( + pub fn get_max_fee_numerator(&self) -> Result { + let pool_version = + PoolVersion::try_from(self.version).map_err(|_| PoolError::TypeCastFailed)?; + if pool_version == PoolVersion::V0 { + Ok(MAX_FEE_NUMERATOR_V0) + } else { + Ok(MAX_FEE_NUMERATOR_V1) + } + } + + pub fn get_swap_result_from_exact_output( &self, - amount_in: u64, + amount_out: u64, fee_mode: &FeeMode, trade_direction: TradeDirection, current_point: u64, - ) -> Result { + ) -> Result { let mut actual_protocol_fee = 0; - let mut actual_lp_fee = 0; + let mut actual_trading_fee = 0; let mut actual_referral_fee = 0; let mut actual_partner_fee = 0; - let pool_version = - PoolVersion::try_from(self.version).map_err(|_| PoolError::TypeCastFailed)?; - let max_fee_numerator = if pool_version == PoolVersion::V0 { - MAX_FEE_NUMERATOR_V0 + let max_fee_numerator = self.get_max_fee_numerator()?; + + let included_fee_amount_out = if fee_mode.fees_on_input { + amount_out } else { - MAX_FEE_NUMERATOR_V1 + let trade_fee_numerator = self + .pool_fees + .get_total_trading_fee_from_excluded_fee_amount( + current_point, + self.activation_point, + amount_out, + trade_direction, + max_fee_numerator, + )?; + + let (included_fee_amount_out, fee_amount) = + PoolFeesStruct::get_included_fee_amount(trade_fee_numerator, amount_out)?; + + let SplitFees { + trading_fee, + protocol_fee, + referral_fee, + partner_fee, + } = self + .pool_fees + .split_fees(fee_amount, fee_mode.has_referral, self.has_partner())?; + + actual_protocol_fee = protocol_fee; + actual_trading_fee = trading_fee; + actual_referral_fee = referral_fee; + actual_partner_fee = partner_fee; + + included_fee_amount_out }; - let actual_amount_in = if fee_mode.fees_on_input { + let SwapAmountFromOutput { + input_amount, + next_sqrt_price, + } = match trade_direction { + TradeDirection::AtoB => self.calculate_a_to_b_from_amount_out(included_fee_amount_out), + TradeDirection::BtoA => self.calculate_b_to_a_from_amount_out(included_fee_amount_out), + }?; + + let included_fee_input_amount = if fee_mode.fees_on_input { + let trade_fee_numerator = self + .pool_fees + .get_total_trading_fee_from_excluded_fee_amount( + current_point, + self.activation_point, + input_amount, + trade_direction, + max_fee_numerator, + )?; + + let (included_fee_input_amount, fee_amount) = + PoolFeesStruct::get_included_fee_amount(trade_fee_numerator, input_amount)?; + + let SplitFees { + trading_fee, + protocol_fee, + referral_fee, + partner_fee, + } = self + .pool_fees + .split_fees(fee_amount, fee_mode.has_referral, self.has_partner())?; + + actual_protocol_fee = protocol_fee; + actual_trading_fee = trading_fee; + actual_referral_fee = referral_fee; + actual_partner_fee = partner_fee; + + included_fee_input_amount + } else { + input_amount + }; + + Ok(SwapResult2 { + amount_left: 0, + included_fee_input_amount, + excluded_fee_input_amount: input_amount, + output_amount: amount_out, + next_sqrt_price, + trading_fee: actual_trading_fee, + protocol_fee: actual_protocol_fee, + partner_fee: actual_partner_fee, + referral_fee: actual_referral_fee, + }) + } + + pub fn get_swap_result_from_partial_input( + &self, + amount_in: u64, + fee_mode: &FeeMode, + trade_direction: TradeDirection, + current_point: u64, + ) -> Result { + let mut actual_protocol_fee = 0; + let mut actual_trading_fee = 0; + let mut actual_referral_fee = 0; + let mut actual_partner_fee = 0; + + let max_fee_numerator = self.get_max_fee_numerator()?; + + let trade_fee_numerator = self + .pool_fees + .get_total_trading_fee_from_included_fee_amount( + current_point, + self.activation_point, + amount_in, + trade_direction, + max_fee_numerator, + )?; + + let mut actual_amount_in = if fee_mode.fees_on_input { let FeeOnAmountResult { amount, - lp_fee, + trading_fee, protocol_fee, partner_fee, referral_fee, } = self.pool_fees.get_fee_on_amount( amount_in, + trade_fee_numerator, fee_mode.has_referral, - current_point, - self.activation_point, self.has_partner(), - trade_direction, - max_fee_numerator, )?; actual_protocol_fee = protocol_fee; - actual_lp_fee = lp_fee; + actual_trading_fee = trading_fee; actual_referral_fee = referral_fee; actual_partner_fee = partner_fee; @@ -468,49 +581,310 @@ impl Pool { amount_in }; - let SwapAmount { + let SwapAmountFromInput { + amount_left, output_amount, next_sqrt_price, } = match trade_direction { - TradeDirection::AtoB => self.get_swap_result_from_a_to_b(actual_amount_in), - TradeDirection::BtoA => self.get_swap_result_from_b_to_a(actual_amount_in), + TradeDirection::AtoB => self.calculate_a_to_b_from_partial_amount_in(actual_amount_in), + TradeDirection::BtoA => self.calculate_b_to_a_from_partial_amount_in(actual_amount_in), }?; + let included_fee_input_amount = if amount_left > 0 { + actual_amount_in = actual_amount_in.safe_sub(amount_left)?; + + if fee_mode.fees_on_input { + let trade_fee_numerator = self + .pool_fees + .get_total_trading_fee_from_excluded_fee_amount( + current_point, + self.activation_point, + actual_amount_in, + trade_direction, + max_fee_numerator, + )?; + + let (included_fee_amount_in, fee_amount) = + PoolFeesStruct::get_included_fee_amount(trade_fee_numerator, actual_amount_in)?; + + let SplitFees { + trading_fee, + protocol_fee, + referral_fee, + partner_fee, + } = self.pool_fees.split_fees( + fee_amount, + fee_mode.has_referral, + self.has_partner(), + )?; + + actual_protocol_fee = protocol_fee; + actual_trading_fee = trading_fee; + actual_referral_fee = referral_fee; + actual_partner_fee = partner_fee; + + included_fee_amount_in + } else { + actual_amount_in + } + } else { + amount_in + }; + let actual_amount_out = if fee_mode.fees_on_input { output_amount } else { let FeeOnAmountResult { amount, - lp_fee, + trading_fee, protocol_fee, partner_fee, referral_fee, } = self.pool_fees.get_fee_on_amount( output_amount, + trade_fee_numerator, fee_mode.has_referral, + self.has_partner(), + )?; + + actual_protocol_fee = protocol_fee; + actual_trading_fee = trading_fee; + actual_referral_fee = referral_fee; + actual_partner_fee = partner_fee; + + amount + }; + + Ok(SwapResult2 { + included_fee_input_amount, + excluded_fee_input_amount: actual_amount_in, + amount_left, + output_amount: actual_amount_out, + next_sqrt_price, + trading_fee: actual_trading_fee, + protocol_fee: actual_protocol_fee, + partner_fee: actual_partner_fee, + referral_fee: actual_referral_fee, + }) + } + + pub fn get_swap_result_from_exact_input( + &self, + amount_in: u64, + fee_mode: &FeeMode, + trade_direction: TradeDirection, + current_point: u64, + ) -> Result { + let mut actual_protocol_fee = 0; + let mut actual_trading_fee = 0; + let mut actual_referral_fee = 0; + let mut actual_partner_fee = 0; + + let max_fee_numerator = self.get_max_fee_numerator()?; + + // We can compute the trade_fee_numerator here. Instead of separately for amount_in, and amount_out. + // This is because FeeRateLimiter (fee rate scale based on amount) only applied when fee_mode.fees_on_input + // (a.k.a TradeDirection::QuoteToBase + CollectFeeMode::QuoteToken) + // For the rest of the time, the fee rate is not dependent on amount. + let trade_fee_numerator = self + .pool_fees + .get_total_trading_fee_from_included_fee_amount( current_point, self.activation_point, - self.has_partner(), + amount_in, trade_direction, max_fee_numerator, )?; + + let actual_amount_in = if fee_mode.fees_on_input { + let FeeOnAmountResult { + amount, + trading_fee, + protocol_fee, + partner_fee, + referral_fee, + } = self.pool_fees.get_fee_on_amount( + amount_in, + trade_fee_numerator, + fee_mode.has_referral, + self.has_partner(), + )?; + + actual_protocol_fee = protocol_fee; + actual_trading_fee = trading_fee; + actual_referral_fee = referral_fee; + actual_partner_fee = partner_fee; + + amount + } else { + amount_in + }; + + let SwapAmountFromInput { + output_amount, + next_sqrt_price, + amount_left, + } = match trade_direction { + TradeDirection::AtoB => self.calculate_a_to_b_from_amount_in(actual_amount_in), + TradeDirection::BtoA => self.calculate_b_to_a_from_amount_in(actual_amount_in), + }?; + + let actual_amount_out = if fee_mode.fees_on_input { + output_amount + } else { + let FeeOnAmountResult { + amount, + trading_fee, + protocol_fee, + partner_fee, + referral_fee, + } = self.pool_fees.get_fee_on_amount( + output_amount, + trade_fee_numerator, + fee_mode.has_referral, + self.has_partner(), + )?; + actual_protocol_fee = protocol_fee; - actual_lp_fee = lp_fee; + actual_trading_fee = trading_fee; actual_referral_fee = referral_fee; actual_partner_fee = partner_fee; + amount }; - Ok(SwapResult { + Ok(SwapResult2 { + amount_left, + included_fee_input_amount: amount_in, + excluded_fee_input_amount: actual_amount_in, output_amount: actual_amount_out, next_sqrt_price, - lp_fee: actual_lp_fee, + trading_fee: actual_trading_fee, protocol_fee: actual_protocol_fee, partner_fee: actual_partner_fee, referral_fee: actual_referral_fee, }) } - fn get_swap_result_from_a_to_b(&self, amount_in: u64) -> Result { + + pub fn calculate_b_to_a_from_amount_out( + &self, + amount_out: u64, + ) -> Result { + let next_sqrt_price = + get_next_sqrt_price_from_output(self.sqrt_price, self.liquidity, amount_out, false)?; + + if next_sqrt_price > self.sqrt_max_price { + return Err(PoolError::PriceRangeViolation.into()); + } + + let in_amount = get_delta_amount_b_unsigned( + self.sqrt_price, + next_sqrt_price, + self.liquidity, + Rounding::Up, + )?; + + Ok(SwapAmountFromOutput { + input_amount: in_amount, + next_sqrt_price, + }) + } + + pub fn calculate_a_to_b_from_amount_out( + &self, + amount_out: u64, + ) -> Result { + let next_sqrt_price = + get_next_sqrt_price_from_output(self.sqrt_price, self.liquidity, amount_out, true)?; + + if next_sqrt_price < self.sqrt_min_price { + return Err(PoolError::PriceRangeViolation.into()); + } + + let in_amount = get_delta_amount_a_unsigned( + next_sqrt_price, + self.sqrt_price, + self.liquidity, + Rounding::Up, + )?; + + Ok(SwapAmountFromOutput { + input_amount: in_amount, + next_sqrt_price, + }) + } + + pub fn calculate_b_to_a_from_partial_amount_in( + &self, + amount_in: u64, + ) -> Result { + let max_amount_in = get_delta_amount_b_unsigned( + self.sqrt_price, + self.sqrt_max_price, + self.liquidity, + Rounding::Up, + )?; + + let (consumed_in_amount, next_sqrt_price) = if amount_in >= max_amount_in { + (max_amount_in, self.sqrt_max_price) + } else { + let next_sqrt_price = + get_next_sqrt_price_from_input(self.sqrt_price, self.liquidity, amount_in, false)?; + (amount_in, next_sqrt_price) + }; + + let output_amount = get_delta_amount_a_unsigned( + self.sqrt_price, + next_sqrt_price, + self.liquidity, + Rounding::Down, + )?; + + let amount_left = amount_in.safe_sub(consumed_in_amount)?; + + Ok(SwapAmountFromInput { + output_amount, + next_sqrt_price, + amount_left, + }) + } + + pub fn calculate_a_to_b_from_partial_amount_in( + &self, + amount_in: u64, + ) -> Result { + let max_amount_in = get_delta_amount_a_unsigned( + self.sqrt_min_price, + self.sqrt_price, + self.liquidity, + Rounding::Up, + )?; + + let (consumed_in_amount, next_sqrt_price) = if amount_in >= max_amount_in { + (max_amount_in, self.sqrt_min_price) + } else { + let next_sqrt_price = + get_next_sqrt_price_from_input(self.sqrt_price, self.liquidity, amount_in, true)?; + (amount_in, next_sqrt_price) + }; + + let output_amount = get_delta_amount_b_unsigned( + next_sqrt_price, + self.sqrt_price, + self.liquidity, + Rounding::Down, + )?; + + let amount_left = amount_in.safe_sub(consumed_in_amount)?; + + Ok(SwapAmountFromInput { + output_amount, + next_sqrt_price, + amount_left, + }) + } + + fn calculate_a_to_b_from_amount_in(&self, amount_in: u64) -> Result { // finding new target price let next_sqrt_price = get_next_sqrt_price_from_input(self.sqrt_price, self.liquidity, amount_in, true)?; @@ -527,13 +901,14 @@ impl Pool { Rounding::Down, )?; - Ok(SwapAmount { + Ok(SwapAmountFromInput { output_amount, next_sqrt_price, + amount_left: 0, }) } - fn get_swap_result_from_b_to_a(&self, amount_in: u64) -> Result { + fn calculate_b_to_a_from_amount_in(&self, amount_in: u64) -> Result { // finding new target price let next_sqrt_price = get_next_sqrt_price_from_input(self.sqrt_price, self.liquidity, amount_in, false)?; @@ -549,25 +924,25 @@ impl Pool { Rounding::Down, )?; - Ok(SwapAmount { + Ok(SwapAmountFromInput { output_amount, next_sqrt_price, + amount_left: 0, }) } pub fn apply_swap_result( &mut self, - swap_result: &SwapResult, + swap_result: &SwapResult2, fee_mode: &FeeMode, current_timestamp: u64, ) -> Result<()> { - let &SwapResult { - output_amount: _output_amount, - lp_fee, + let &SwapResult2 { + trading_fee: lp_fee, next_sqrt_price, protocol_fee, partner_fee, - referral_fee: _referral_fee, + .. } = swap_result; let old_sqrt_price = self.sqrt_price; @@ -908,9 +1283,42 @@ pub struct SwapResult { pub referral_fee: u64, } -pub struct SwapAmount { +impl From for SwapResult { + fn from(value: SwapResult2) -> Self { + Self { + output_amount: value.output_amount, + next_sqrt_price: value.next_sqrt_price, + lp_fee: value.trading_fee, + protocol_fee: value.protocol_fee, + partner_fee: value.partner_fee, + referral_fee: value.referral_fee, + } + } +} + +#[derive(Debug, PartialEq, AnchorDeserialize, AnchorSerialize, Clone, Copy)] +pub struct SwapResult2 { + // This is excluded_transfer_fee_amount_in + pub included_fee_input_amount: u64, + pub excluded_fee_input_amount: u64, + pub amount_left: u64, + pub output_amount: u64, + pub next_sqrt_price: u128, + pub trading_fee: u64, + pub protocol_fee: u64, + pub partner_fee: u64, + pub referral_fee: u64, +} + +pub struct SwapAmountFromInput { output_amount: u64, next_sqrt_price: u128, + amount_left: u64, +} + +pub struct SwapAmountFromOutput { + input_amount: u64, + next_sqrt_price: u128, } #[derive(Debug, PartialEq)] diff --git a/programs/cp-amm/src/tests/fee_scheduler_tests.rs b/programs/cp-amm/src/tests/fee_scheduler_tests.rs index 8c853540..9459232e 100644 --- a/programs/cp-amm/src/tests/fee_scheduler_tests.rs +++ b/programs/cp-amm/src/tests/fee_scheduler_tests.rs @@ -1,4 +1,4 @@ -use crate::{fee_math::get_fee_in_period, state::fee::BaseFeeStruct}; +use crate::fee_math::get_fee_in_period; use proptest::prelude::*; proptest! { @@ -18,24 +18,3 @@ proptest! { assert_eq!(fee_numerator, cliff_fee_numerator) } } - -#[test] -fn test_base_fee() { - let base_fee = BaseFeeStruct { - cliff_fee_numerator: 100_000, - base_fee_mode: 1, - first_factor: 50, - second_factor: 1u64.to_le_bytes(), - third_factor: 500, // 5% each second - ..Default::default() - }; - let current_fee = base_fee - .get_current_base_fee_numerator( - 100, - 0, - 1_000_000, - crate::params::swap::TradeDirection::AtoB, - ) - .unwrap(); - println!("{}", current_fee) -} diff --git a/programs/cp-amm/src/tests/integration_tests.rs b/programs/cp-amm/src/tests/integration_tests.rs index 9abbf483..dc287eab 100644 --- a/programs/cp-amm/src/tests/integration_tests.rs +++ b/programs/cp-amm/src/tests/integration_tests.rs @@ -210,7 +210,7 @@ fn execute_swap_liquidity( let fee_mode = &FeeMode::get_fee_mode(pool.collect_fee_mode, trade_direction, has_referral).unwrap(); let swap_result = pool - .get_swap_result(amount_in, fee_mode, trade_direction, 0) + .get_swap_result_from_exact_input(amount_in, fee_mode, trade_direction, 0) .unwrap(); pool.apply_swap_result(&swap_result, fee_mode, 0).unwrap(); diff --git a/programs/cp-amm/src/tests/swap_tests.rs b/programs/cp-amm/src/tests/swap_tests.rs index 6a5cfaac..ef1dc7f1 100644 --- a/programs/cp-amm/src/tests/swap_tests.rs +++ b/programs/cp-amm/src/tests/swap_tests.rs @@ -34,14 +34,14 @@ proptest! { let max_amount_in = pool.get_max_amount_in(trade_direction).unwrap(); if amount_in <= max_amount_in { let swap_result_0 = pool - .get_swap_result(amount_in, fee_mode, trade_direction, 0) + .get_swap_result_from_exact_input(amount_in, fee_mode, trade_direction, 0) .unwrap(); pool.apply_swap_result(&swap_result_0, fee_mode, 0).unwrap(); // swap back let swap_result_1 = pool - .get_swap_result(swap_result_0.output_amount, fee_mode, TradeDirection::BtoA, 0) + .get_swap_result_from_exact_input(swap_result_0.output_amount, fee_mode, TradeDirection::BtoA, 0) .unwrap(); assert!(swap_result_1.output_amount < amount_in); @@ -70,14 +70,14 @@ proptest! { let max_amount_in = pool.get_max_amount_in(trade_direction).unwrap(); if amount_in <= max_amount_in { let swap_result_0 = pool - .get_swap_result(amount_in, fee_mode, trade_direction, 0) + .get_swap_result_from_exact_input(amount_in, fee_mode, trade_direction, 0) .unwrap(); pool.apply_swap_result(&swap_result_0, fee_mode, 0).unwrap(); // swap back let swap_result_1 = pool - .get_swap_result(swap_result_0.output_amount, fee_mode, TradeDirection::AtoB, 0) + .get_swap_result_from_exact_input(swap_result_0.output_amount, fee_mode, TradeDirection::AtoB, 0) .unwrap(); assert!(swap_result_1.output_amount < amount_in); @@ -133,7 +133,7 @@ fn test_reserve_wont_lost_when_swap_from_b_to_a_single() { }; let fee_mode = &FeeMode::get_fee_mode(pool.collect_fee_mode, trade_direction, false).unwrap(); let swap_result_0 = pool - .get_swap_result(amount_in, fee_mode, trade_direction, 0) + .get_swap_result_from_exact_input(amount_in, fee_mode, trade_direction, 0) .unwrap(); println!("{:?}", swap_result_0); @@ -141,7 +141,7 @@ fn test_reserve_wont_lost_when_swap_from_b_to_a_single() { pool.apply_swap_result(&swap_result_0, fee_mode, 0).unwrap(); let swap_result_1 = pool - .get_swap_result( + .get_swap_result_from_exact_input( swap_result_0.output_amount, fee_mode, TradeDirection::AtoB, @@ -195,7 +195,7 @@ fn test_swap_basic() { let fee_mode = &FeeMode::get_fee_mode(pool.collect_fee_mode, trade_direction, false).unwrap(); let swap_result = pool - .get_swap_result(amount_in, fee_mode, trade_direction, 0) + .get_swap_result_from_exact_input(amount_in, fee_mode, trade_direction, 0) .unwrap(); println!("result {:?}", swap_result); @@ -205,7 +205,7 @@ fn test_swap_basic() { pool.apply_swap_result(&swap_result, fee_mode, 0).unwrap(); let swap_result_referse = pool - .get_swap_result(swap_result.output_amount, fee_mode, TradeDirection::BtoA, 0) + .get_swap_result_from_exact_input(swap_result.output_amount, fee_mode, TradeDirection::BtoA, 0) .unwrap(); println!("reverse {:?}", swap_result_referse); diff --git a/programs/cp-amm/src/tests/test_rate_limiter.rs b/programs/cp-amm/src/tests/test_rate_limiter.rs index 52c9393c..f9843425 100644 --- a/programs/cp-amm/src/tests/test_rate_limiter.rs +++ b/programs/cp-amm/src/tests/test_rate_limiter.rs @@ -138,7 +138,7 @@ fn test_rate_limiter_behavior() { { let fee_numerator = rate_limiter - .get_fee_numerator_from_amount(reference_amount) + .get_fee_numerator_from_included_fee_amount(reference_amount) .unwrap(); let fee_bps = to_bps(fee_numerator.into(), FEE_DENOMINATOR.into()).unwrap(); assert_eq!(fee_bps, base_fee_bps); @@ -146,13 +146,13 @@ fn test_rate_limiter_behavior() { { let fee_numerator = rate_limiter - .get_fee_numerator_from_amount(reference_amount * 3 / 2) + .get_fee_numerator_from_included_fee_amount(reference_amount * 3 / 2) .unwrap(); let fee_bps = to_bps(fee_numerator.into(), FEE_DENOMINATOR.into()).unwrap(); assert_eq!(fee_bps, 133); let fee_numerator = rate_limiter - .get_fee_numerator_from_amount(reference_amount * 2) + .get_fee_numerator_from_included_fee_amount(reference_amount * 2) .unwrap(); let fee_bps = to_bps(fee_numerator.into(), FEE_DENOMINATOR.into()).unwrap(); assert_eq!(fee_bps, 150); // 1.5%, (1+1+1) / 2 @@ -160,7 +160,7 @@ fn test_rate_limiter_behavior() { { let fee_numerator = rate_limiter - .get_fee_numerator_from_amount(reference_amount * 3) + .get_fee_numerator_from_included_fee_amount(reference_amount * 3) .unwrap(); let fee_bps = to_bps(fee_numerator.into(), FEE_DENOMINATOR.into()).unwrap(); assert_eq!(fee_bps, 200); // 2%, (1+1+1+1) / 2 @@ -168,7 +168,7 @@ fn test_rate_limiter_behavior() { { let fee_numerator = rate_limiter - .get_fee_numerator_from_amount(reference_amount * 4) + .get_fee_numerator_from_included_fee_amount(reference_amount * 4) .unwrap(); let fee_bps = to_bps(fee_numerator.into(), FEE_DENOMINATOR.into()).unwrap(); assert_eq!(fee_bps, 250); // 2.5% (1+1+1+1+1) / 2 @@ -176,7 +176,7 @@ fn test_rate_limiter_behavior() { { let fee_numerator = rate_limiter - .get_fee_numerator_from_amount(u64::MAX) + .get_fee_numerator_from_included_fee_amount(u64::MAX) .unwrap(); let fee_bps = to_bps(fee_numerator.into(), FEE_DENOMINATOR.into()).unwrap(); @@ -186,7 +186,7 @@ fn test_rate_limiter_behavior() { fn calculate_output_amount(rate_limiter: &FeeRateLimiter, input_amount: u64) -> u64 { let trade_fee_numerator = rate_limiter - .get_base_fee_numerator(0, 0, TradeDirection::BtoA, input_amount) + .get_base_fee_numerator_from_included_fee_amount(0, 0, TradeDirection::BtoA, input_amount) .unwrap(); let trading_fee: u64 = safe_mul_div_cast_u64( input_amount, @@ -242,7 +242,12 @@ fn test_rate_limiter_base_fee_numerator() { { // trade from base to quote let fee_numerator = rate_limiter - .get_base_fee_numerator(0, 0, TradeDirection::AtoB, 2_000_000_000) + .get_base_fee_numerator_from_included_fee_amount( + 0, + 0, + TradeDirection::AtoB, + 2_000_000_000, + ) .unwrap(); assert_eq!(fee_numerator, rate_limiter.cliff_fee_numerator); @@ -251,7 +256,7 @@ fn test_rate_limiter_base_fee_numerator() { { // trade pass last effective point let fee_numerator = rate_limiter - .get_base_fee_numerator( + .get_base_fee_numerator_from_included_fee_amount( (rate_limiter.max_limiter_duration + 1).into(), 0, TradeDirection::BtoA, @@ -265,7 +270,7 @@ fn test_rate_limiter_base_fee_numerator() { { // trade in effective point let fee_numerator = rate_limiter - .get_base_fee_numerator( + .get_base_fee_numerator_from_included_fee_amount( rate_limiter.max_limiter_duration.into(), 0, TradeDirection::BtoA, diff --git a/rust-sdk/fixtures/pool.bin b/rust-sdk/fixtures/3u2BK3ykdjv1hAeGQwAkZMxjb4otV5yvW7g72uviCaZZ.bin similarity index 100% rename from rust-sdk/fixtures/pool.bin rename to rust-sdk/fixtures/3u2BK3ykdjv1hAeGQwAkZMxjb4otV5yvW7g72uviCaZZ.bin diff --git a/rust-sdk/fixtures/CGPxT5d1uf9a8cKVJuZaJAU76t2EfLGbTmRbfvLLZp5j.bin b/rust-sdk/fixtures/CGPxT5d1uf9a8cKVJuZaJAU76t2EfLGbTmRbfvLLZp5j.bin new file mode 100644 index 0000000000000000000000000000000000000000..0d378d7b7251b50552b5de99f53ec98803fbde9f GIT binary patch literal 1112 zcmex3E0;xZWA2^>s%i|x01*Zeurfxd;)Er47~TWfj0{{16$~B1AD++307;z|IKFE~ zFB?=90|T#-h~(bKirH`h5*ci>JHr31ZfyCTQJ?H4alrJ}5z9MLudPF8EcgI2iSgL4 zs?YmwueGvG{hYP>WQpO%-A@i(`u5;S%`zrtSN+(;gO7SWs`L0N1UGKDaK>##Lh}A! zYowaCRGq7<%3tz`efp<}XZuo46-YNEnXS3Zr#mfmmhZnfOPakaM?H z&zpYn(L~*x6UYBAXtP!M|EVRu^>Wsgq>GLoilQ9~l01I9@}ySJb Result { - ensure!(actual_amount_in > 0, "amount is zero"); - - let result = get_internal_quote( - pool, - current_timestamp, - current_slot, - actual_amount_in, - a_to_b, - has_referral, - ) - .unwrap(); - Ok(result) -} - -fn get_internal_quote( - pool: &Pool, - current_timestamp: u64, - current_slot: u64, - actual_amount_in: u64, - a_to_b: bool, - has_referral: bool, -) -> Result { - let activation_type = - ActivationType::try_from(pool.activation_type).context("invalid activation type")?; - - let current_point = match activation_type { - ActivationType::Slot => current_slot, - ActivationType::Timestamp => current_timestamp, - }; - - ensure!( - pool.pool_status == 0 && pool.activation_point <= current_point, - "Swap is disabled" - ); - - let trade_direction = if a_to_b { - TradeDirection::AtoB - } else { - TradeDirection::BtoA - }; - - let fee_mode = &FeeMode::get_fee_mode(pool.collect_fee_mode, trade_direction, has_referral)?; - - let swap_result = - pool.get_swap_result(actual_amount_in, fee_mode, trade_direction, current_point)?; - - Ok(swap_result) -} diff --git a/rust-sdk/src/quote_exact_in.rs b/rust-sdk/src/quote_exact_in.rs new file mode 100644 index 00000000..c697350f --- /dev/null +++ b/rust-sdk/src/quote_exact_in.rs @@ -0,0 +1,36 @@ +use crate::utils::*; +use anyhow::{ensure, Ok, Result}; +use cp_amm::{ + params::swap::TradeDirection, + state::{fee::FeeMode, Pool, SwapResult2}, +}; + +pub fn get_quote( + pool: &Pool, + current_timestamp: u64, + current_slot: u64, + actual_amount_in: u64, + a_to_b: bool, + has_referral: bool, +) -> Result { + ensure!(actual_amount_in > 0, "amount is zero"); + + let current_point = get_current_point(pool.activation_type, current_slot, current_timestamp)?; + + ensure!(is_swap_enable(pool, current_point)?, "Swap is disabled"); + + let trade_direction = if a_to_b { + TradeDirection::AtoB + } else { + TradeDirection::BtoA + }; + + let fee_mode = &FeeMode::get_fee_mode(pool.collect_fee_mode, trade_direction, has_referral)?; + + Ok(pool.get_swap_result_from_exact_input( + actual_amount_in, + fee_mode, + trade_direction, + current_point, + )?) +} diff --git a/rust-sdk/src/quote_exact_out.rs b/rust-sdk/src/quote_exact_out.rs new file mode 100644 index 00000000..1096651d --- /dev/null +++ b/rust-sdk/src/quote_exact_out.rs @@ -0,0 +1,37 @@ +use crate::utils::*; +use anyhow::{ensure, Ok, Result}; +use cp_amm::{ + params::swap::TradeDirection, + state::{fee::FeeMode, Pool, SwapResult2}, +}; + +pub fn get_quote( + pool: &Pool, + current_timestamp: u64, + current_slot: u64, + actual_amount_out: u64, + a_to_b: bool, + has_referral: bool, +) -> Result { + ensure!(actual_amount_out > 0, "amount is zero"); + + let current_point = get_current_point(pool.activation_type, current_slot, current_timestamp)?; + ensure!(is_swap_enable(pool, current_point)?, "Swap is disabled"); + + let trade_direction = if a_to_b { + TradeDirection::AtoB + } else { + TradeDirection::BtoA + }; + + let fee_mode = &FeeMode::get_fee_mode(pool.collect_fee_mode, trade_direction, has_referral)?; + + let swap_result = pool.get_swap_result_from_exact_output( + actual_amount_out, + fee_mode, + trade_direction, + current_point, + )?; + + Ok(swap_result) +} diff --git a/rust-sdk/src/quote_partial_fill_in.rs b/rust-sdk/src/quote_partial_fill_in.rs new file mode 100644 index 00000000..d2035b91 --- /dev/null +++ b/rust-sdk/src/quote_partial_fill_in.rs @@ -0,0 +1,38 @@ +use crate::utils::*; +use anyhow::{ensure, Ok, Result}; +use cp_amm::{ + params::swap::TradeDirection, + state::{fee::FeeMode, Pool, SwapResult2}, +}; + +pub fn get_quote( + pool: &Pool, + current_timestamp: u64, + current_slot: u64, + actual_amount_in: u64, + a_to_b: bool, + has_referral: bool, +) -> Result { + ensure!(actual_amount_in > 0, "amount is zero"); + + let current_point = get_current_point(pool.activation_type, current_slot, current_timestamp)?; + + ensure!(is_swap_enable(pool, current_point)?, "Swap is disabled"); + + let trade_direction = if a_to_b { + TradeDirection::AtoB + } else { + TradeDirection::BtoA + }; + + let fee_mode = &FeeMode::get_fee_mode(pool.collect_fee_mode, trade_direction, has_referral)?; + + let swap_result = pool.get_swap_result_from_partial_input( + actual_amount_in, + fee_mode, + trade_direction, + current_point, + )?; + + Ok(swap_result) +} diff --git a/rust-sdk/src/tests/mod.rs b/rust-sdk/src/tests/mod.rs index 3e338f9a..efbe8a6d 100644 --- a/rust-sdk/src/tests/mod.rs +++ b/rust-sdk/src/tests/mod.rs @@ -1,11 +1,16 @@ pub mod test_quote_exact_in; +pub mod test_quote_exact_out; +pub mod test_quote_partial_fill_in; +use cp_amm::state::Pool; use std::fs; -use cp_amm::state::Pool; +pub const MACK_USDC_ADDRESS: &str = "3u2BK3ykdjv1hAeGQwAkZMxjb4otV5yvW7g72uviCaZZ"; +pub const SOL_USDC_CL_ADDRESS: &str = "CGPxT5d1uf9a8cKVJuZaJAU76t2EfLGbTmRbfvLLZp5j"; -fn get_pool_account() -> Pool { - let account_data = fs::read(&"./fixtures/pool.bin").expect("Failed to read account data"); +fn get_pool_account(pool_address: &str) -> Pool { + let path = format!("./fixtures/{}.bin", pool_address); + let account_data = fs::read(&path).expect("Failed to read account data"); let mut data_without_discriminator = account_data[8..].to_vec(); let &pool: &Pool = bytemuck::from_bytes(&mut data_without_discriminator); diff --git a/rust-sdk/src/tests/test_quote_exact_in.rs b/rust-sdk/src/tests/test_quote_exact_in.rs index cc97e1cb..5ca20bc7 100644 --- a/rust-sdk/src/tests/test_quote_exact_in.rs +++ b/rust-sdk/src/tests/test_quote_exact_in.rs @@ -1,10 +1,11 @@ -use std::u64; - -use crate::{quote, tests::get_pool_account}; +use crate::{ + quote_exact_in, + tests::{get_pool_account, MACK_USDC_ADDRESS}, +}; #[test] fn test_quote_exact_in() { - let pool = get_pool_account(); + let pool = get_pool_account(MACK_USDC_ADDRESS); let current_timestamp: u64 = 1_753_751_761; let current_slot: u64 = 356410171; @@ -14,7 +15,7 @@ fn test_quote_exact_in() { let actual_amount_in = u64::MAX; - let swap_result = quote::get_quote( + let swap_result = quote_exact_in::get_quote( &pool, current_timestamp, current_slot, @@ -23,5 +24,35 @@ fn test_quote_exact_in() { has_referral, ) .unwrap(); + + assert!( + swap_result.output_amount > 0, + "Expected output amount to be greater than 0" + ); + println!("swap_result {} {:?}", actual_amount_in, swap_result); } + +#[test] +fn test_quote_exact_in_swap_disabled() { + let pool = get_pool_account(MACK_USDC_ADDRESS); + + let current_timestamp: u64 = 0; + let current_slot: u64 = 0; + + let a_to_b: bool = false; + let has_referral: bool = false; + + let actual_amount_in = u64::MAX; + + let swap_result = quote_exact_in::get_quote( + &pool, + current_timestamp, + current_slot, + actual_amount_in, + a_to_b, + has_referral, + ); + + assert!(swap_result.is_err(), "Expected error when swap is disabled"); +} diff --git a/rust-sdk/src/tests/test_quote_exact_out.rs b/rust-sdk/src/tests/test_quote_exact_out.rs new file mode 100644 index 00000000..a349a86d --- /dev/null +++ b/rust-sdk/src/tests/test_quote_exact_out.rs @@ -0,0 +1,41 @@ +use crate::{ + quote_exact_out, + tests::{get_pool_account, MACK_USDC_ADDRESS}, +}; + +#[test] +fn test_quote_exact_out() { + let pool = get_pool_account(MACK_USDC_ADDRESS); + + let current_timestamp: u64 = 1_753_751_761; + let current_slot: u64 = 356410171; + + let a_to_b: bool = false; + let has_referral: bool = false; + + let actual_amount_out = 1_000_000; + + let swap_result = quote_exact_out::get_quote( + &pool, + current_timestamp, + current_slot, + actual_amount_out, + a_to_b, + has_referral, + ) + .unwrap(); + + assert!( + swap_result.included_fee_input_amount > 0, + "Expected amount 0 to be greater than 0" + ); + assert_eq!( + swap_result.output_amount, actual_amount_out, + "Expected output amount to be equals" + ); + + println!( + "swap_result {} {:?}", + swap_result.included_fee_input_amount, swap_result + ); +} diff --git a/rust-sdk/src/tests/test_quote_partial_fill_in.rs b/rust-sdk/src/tests/test_quote_partial_fill_in.rs new file mode 100644 index 00000000..d662464f --- /dev/null +++ b/rust-sdk/src/tests/test_quote_partial_fill_in.rs @@ -0,0 +1,47 @@ +use crate::{ + quote_partial_fill_in, + tests::{get_pool_account, SOL_USDC_CL_ADDRESS}, +}; + +#[test] +fn test_quote_partial_fill_in() { + let pool = get_pool_account(SOL_USDC_CL_ADDRESS); + + let current_timestamp: u64 = 1_753_751_761; + let current_slot: u64 = 356410171; + + let a_to_b: bool = false; + let has_referral: bool = false; + + let amount_in = u64::MAX; + + let swap_result = quote_partial_fill_in::get_quote( + &pool, + current_timestamp, + current_slot, + amount_in, + a_to_b, + has_referral, + ) + .unwrap(); + + assert!( + swap_result.output_amount > 0, + "Expected output amount to be greater than 0" + ); + + assert!( + swap_result.included_fee_input_amount < amount_in, + "Expected consumed input amount to be less than input amount" + ); + + assert_eq!( + pool.sqrt_max_price, swap_result.next_sqrt_price, + "Expected next sqrt price to match pool's max price" + ); + + println!( + "swap_result {} {:?}", + swap_result.included_fee_input_amount, swap_result + ); +} diff --git a/rust-sdk/src/utils.rs b/rust-sdk/src/utils.rs new file mode 100644 index 00000000..d621cfb4 --- /dev/null +++ b/rust-sdk/src/utils.rs @@ -0,0 +1,26 @@ +use anyhow::{Context, Result}; +use cp_amm::{ + state::{Pool, PoolStatus}, + ActivationType, +}; + +pub fn get_current_point( + activation_type: u8, + current_slot: u64, + current_timestamp: u64, +) -> Result { + let activation_type = + ActivationType::try_from(activation_type).context("invalid activation type")?; + + let current_point = match activation_type { + ActivationType::Slot => current_slot, + ActivationType::Timestamp => current_timestamp, + }; + + Ok(current_point) +} + +pub fn is_swap_enable(pool: &Pool, current_point: u64) -> Result { + let pool_status = PoolStatus::try_from(pool.pool_status).context("invalid pool status")?; + Ok(pool_status == PoolStatus::Enable && current_point >= pool.activation_point) +} diff --git a/tests/bankrun-utils/cpAmm.ts b/tests/bankrun-utils/cpAmm.ts index d7bb5e77..66277db4 100644 --- a/tests/bankrun-utils/cpAmm.ts +++ b/tests/bankrun-utils/cpAmm.ts @@ -116,7 +116,7 @@ export type BaseFee = { export type PoolFees = { baseFee: BaseFee; - padding: number[], + padding: number[]; dynamicFee: DynamicFee | null; }; @@ -222,15 +222,9 @@ export async function createConfigIx( expect(Buffer.from(configState.poolFees.baseFee.secondFactor).toString()).eq( Buffer.from(params.poolFees.baseFee.secondFactor).toString() ); - expect(configState.poolFees.protocolFeePercent).eq( - 20 - ); - expect(configState.poolFees.partnerFeePercent).eq( - 0 - ); - expect(configState.poolFees.referralFeePercent).eq( - 20 - ); + expect(configState.poolFees.protocolFeePercent).eq(20); + expect(configState.poolFees.partnerFeePercent).eq(0); + expect(configState.poolFees.referralFeePercent).eq(20); expect(configState.configType).eq(0); // ConfigType: Static return config; @@ -787,7 +781,7 @@ export async function setPoolStatus( export type PoolFeesParams = { baseFee: BaseFee; - padding: number[], + padding: number[]; dynamicFee: DynamicFee | null; }; @@ -1686,7 +1680,7 @@ export async function swapInstruction( referralTokenAccount, }) .remainingAccounts( - // TODO should check condition to add this in remaning accounts + // TODO should check condition to add this in remaining accounts [ { isSigner: false, @@ -1700,7 +1694,142 @@ export async function swapInstruction( return transaction; } -export async function swap(banksClient: BanksClient, params: SwapParams) { +export enum SwapMode { + ExactIn, + PartialFillIn, + ExactOut, +} + +export type Swap2Params = { + payer: Keypair; + pool: PublicKey; + inputTokenMint: PublicKey; + outputTokenMint: PublicKey; + amount0: BN; + amount1: BN; + swapMode: SwapMode; + referralTokenAccount: PublicKey | null; +}; + +export async function swap2Instruction( + banksClient: BanksClient, + params: Swap2Params +) { + const { + payer, + pool, + inputTokenMint, + outputTokenMint, + amount0, + amount1, + swapMode, + referralTokenAccount, + } = params; + + const program = createCpAmmProgram(); + const poolState = await getPool(banksClient, pool); + + const poolAuthority = derivePoolAuthority(); + const tokenAProgram = (await banksClient.getAccount(poolState.tokenAMint)) + .owner; + + const tokenBProgram = (await banksClient.getAccount(poolState.tokenBMint)) + .owner; + const inputTokenAccount = getAssociatedTokenAddressSync( + inputTokenMint, + payer.publicKey, + true, + tokenAProgram + ); + const outputTokenAccount = getAssociatedTokenAddressSync( + outputTokenMint, + payer.publicKey, + true, + tokenBProgram + ); + const tokenAVault = poolState.tokenAVault; + const tokenBVault = poolState.tokenBVault; + const tokenAMint = poolState.tokenAMint; + const tokenBMint = poolState.tokenBMint; + + const transaction = await program.methods + .swap2({ + amount0, + amount1, + swapMode, + }) + .accountsPartial({ + poolAuthority, + pool, + payer: payer.publicKey, + inputTokenAccount, + outputTokenAccount, + tokenAVault, + tokenBVault, + tokenAProgram, + tokenBProgram, + tokenAMint, + tokenBMint, + referralTokenAccount, + }) + .remainingAccounts( + // TODO should check condition to add this in remaining accounts + [ + { + isSigner: false, + isWritable: false, + pubkey: SYSVAR_INSTRUCTIONS_PUBKEY, + }, + ] + ) + .transaction(); + + return transaction; +} + +export async function swap2ExactIn( + banksClient: BanksClient, + params: Omit +) { + const swapIx = await swap2Instruction(banksClient, { + ...params, + swapMode: SwapMode.ExactIn, + }); + swapIx.recentBlockhash = (await banksClient.getLatestBlockhash())[0]; + swapIx.sign(params.payer); + await processTransactionMaybeThrow(banksClient, swapIx); +} + +export async function swap2ExactOut( + banksClient: BanksClient, + params: Omit +) { + const swapIx = await swap2Instruction(banksClient, { + ...params, + swapMode: SwapMode.ExactOut, + }); + swapIx.recentBlockhash = (await banksClient.getLatestBlockhash())[0]; + swapIx.sign(params.payer); + await processTransactionMaybeThrow(banksClient, swapIx); +} + +export async function swap2PartialFillIn( + banksClient: BanksClient, + params: Omit +) { + const swapIx = await swap2Instruction(banksClient, { + ...params, + swapMode: SwapMode.PartialFillIn, + }); + swapIx.recentBlockhash = (await banksClient.getLatestBlockhash())[0]; + swapIx.sign(params.payer); + await processTransactionMaybeThrow(banksClient, swapIx); +} + +export async function swapExactIn( + banksClient: BanksClient, + params: SwapParams +) { const transaction = await swapInstruction(banksClient, params); transaction.recentBlockhash = (await banksClient.getLatestBlockhash())[0]; @@ -1709,7 +1838,7 @@ export async function swap(banksClient: BanksClient, params: SwapParams) { await processTransactionMaybeThrow(banksClient, transaction); } -export type ClaimpositionFeeParams = { +export type ClaimPositionFeeParams = { owner: Keypair; pool: PublicKey; position: PublicKey; @@ -1717,7 +1846,7 @@ export type ClaimpositionFeeParams = { export async function claimPositionFee( banksClient: BanksClient, - params: ClaimpositionFeeParams + params: ClaimPositionFeeParams ) { const { owner, pool, position } = params; diff --git a/tests/bankrun-utils/token2022.ts b/tests/bankrun-utils/token2022.ts index 23fc8c57..a57aedf5 100644 --- a/tests/bankrun-utils/token2022.ts +++ b/tests/bankrun-utils/token2022.ts @@ -24,7 +24,7 @@ import { DECIMALS } from "./constants"; import { getOrCreateAssociatedTokenAccount } from "./token"; import { TRANSFER_HOOK_COUNTER_PROGRAM_ID } from "./transferHook"; import { processTransactionMaybeThrow } from "./common"; -const rawAmount = 1_000_000 * 10 ** DECIMALS; // 1 millions +const rawAmount = 1_000_000_000 * 10 ** DECIMALS; // 1 millions interface ExtensionWithInstruction { extension: ExtensionType; diff --git a/tests/claimFee.test.ts b/tests/claimFee.test.ts index 9114100c..76be9cfa 100644 --- a/tests/claimFee.test.ts +++ b/tests/claimFee.test.ts @@ -1,5 +1,10 @@ import { ProgramTestContext } from "solana-bankrun"; -import { convertToByteArray, generateKpAndFund, randomID, startTest } from "./bankrun-utils/common"; +import { + convertToByteArray, + generateKpAndFund, + randomID, + startTest, +} from "./bankrun-utils/common"; import { Keypair, PublicKey } from "@solana/web3.js"; import { addLiquidity, @@ -12,7 +17,7 @@ import { MIN_LP_AMOUNT, MAX_SQRT_PRICE, MIN_SQRT_PRICE, - swap, + swapExactIn, SwapParams, createClaimFeeOperator, claimProtocolFee, @@ -176,7 +181,7 @@ describe("Claim fee", () => { referralTokenAccount: null, }; - await swap(context.banksClient, swapParams); + await swapExactIn(context.banksClient, swapParams); // claim protocol fee await claimProtocolFee(context.banksClient, { @@ -364,7 +369,7 @@ describe("Claim fee", () => { referralTokenAccount: null, }; - await swap(context.banksClient, swapParams); + await swapExactIn(context.banksClient, swapParams); // claim protocol fee await claimProtocolFee(context.banksClient, { diff --git a/tests/claimPositionFee.test.ts b/tests/claimPositionFee.test.ts index 57f57118..e1c13058 100644 --- a/tests/claimPositionFee.test.ts +++ b/tests/claimPositionFee.test.ts @@ -1,5 +1,9 @@ import { ProgramTestContext } from "solana-bankrun"; -import { convertToByteArray, generateKpAndFund, startTest } from "./bankrun-utils/common"; +import { + convertToByteArray, + generateKpAndFund, + startTest, +} from "./bankrun-utils/common"; import { Keypair, PublicKey } from "@solana/web3.js"; import { addLiquidity, @@ -13,7 +17,7 @@ import { MIN_LP_AMOUNT, MAX_SQRT_PRICE, MIN_SQRT_PRICE, - swap, + swapExactIn, SwapParams, createToken, mintSplTokenTo, @@ -153,7 +157,7 @@ describe("Claim position fee", () => { referralTokenAccount: null, }; - await swap(context.banksClient, swapParams); + await swapExactIn(context.banksClient, swapParams); // claim position fee const claimParams = { diff --git a/tests/lockPosition.test.ts b/tests/lockPosition.test.ts index ccbd3d98..d9b8a60d 100644 --- a/tests/lockPosition.test.ts +++ b/tests/lockPosition.test.ts @@ -23,7 +23,7 @@ import { mintSplTokenTo, permanentLockPosition, refreshVestings, - swap, + swapExactIn, SwapParams, } from "./bankrun-utils"; import { @@ -240,7 +240,7 @@ describe("Lock position", () => { referralTokenAccount: null, }; - await swap(context.banksClient, swapParams); + await swapExactIn(context.banksClient, swapParams); const claimParams = { owner: user, @@ -568,7 +568,7 @@ describe("Lock position", () => { referralTokenAccount: null, }; - await swap(context.banksClient, swapParams); + await swapExactIn(context.banksClient, swapParams); const claimParams = { owner: user, diff --git a/tests/rateLimiter.test.ts b/tests/rateLimiter.test.ts index 69a309eb..e66f4412 100644 --- a/tests/rateLimiter.test.ts +++ b/tests/rateLimiter.test.ts @@ -28,7 +28,7 @@ import { InitializePoolParams, initializePool, getPool, - swap, + swapExactIn, swapInstruction, } from "./bankrun-utils"; import BN from "bn.js"; @@ -102,7 +102,10 @@ describe("Rate limiter", () => { let maxRateLimiterDuration = new BN(10); let maxFeeBps = new BN(5000); - let rateLimiterSecondFactor = convertToRateLimiterSecondFactor(maxRateLimiterDuration, maxFeeBps) + let rateLimiterSecondFactor = convertToRateLimiterSecondFactor( + maxRateLimiterDuration, + maxFeeBps + ); const createConfigParams: CreateConfigParams = { poolFees: { @@ -148,7 +151,7 @@ describe("Rate limiter", () => { // swap with 1 SOL - await swap(context.banksClient, { + await swapExactIn(context.banksClient, { payer: poolCreator, pool, inputTokenMint: tokenB, @@ -170,7 +173,7 @@ describe("Rate limiter", () => { // swap with 2 SOL - await swap(context.banksClient, { + await swapExactIn(context.banksClient, { payer: poolCreator, pool, inputTokenMint: tokenB, @@ -196,7 +199,7 @@ describe("Rate limiter", () => { // swap with 2 SOL - await swap(context.banksClient, { + await swapExactIn(context.banksClient, { payer: poolCreator, pool, inputTokenMint: tokenB, @@ -222,7 +225,10 @@ describe("Rate limiter", () => { let maxRateLimiterDuration = new BN(10); let maxFeeBps = new BN(5000); - let rateLimiterSecondFactor = convertToRateLimiterSecondFactor(maxRateLimiterDuration, maxFeeBps) + let rateLimiterSecondFactor = convertToRateLimiterSecondFactor( + maxRateLimiterDuration, + maxFeeBps + ); const liquidity = new BN(MIN_LP_AMOUNT); const sqrtPrice = new BN(MIN_SQRT_PRICE.muln(2)); @@ -277,9 +283,11 @@ describe("Rate limiter", () => { )[0]; transaction.sign(poolCreator); - const errorCode = getCpAmmProgramErrorCodeHexString("FailToValidateSingleSwapInstruction") - await expectThrowsAsync(async ()=>{ - await processTransactionMaybeThrow(context.banksClient, transaction); - }, errorCode) + const errorCode = getCpAmmProgramErrorCodeHexString( + "FailToValidateSingleSwapInstruction" + ); + await expectThrowsAsync(async () => { + await processTransactionMaybeThrow(context.banksClient, transaction); + }, errorCode); }); }); diff --git a/tests/splitPosition.test.ts b/tests/splitPosition.test.ts index fc986c54..2c137953 100644 --- a/tests/splitPosition.test.ts +++ b/tests/splitPosition.test.ts @@ -25,8 +25,8 @@ import { permanentLockPosition, U64_MAX, addLiquidity, - swap, - convertToByteArray + swapExactIn, + convertToByteArray, } from "./bankrun-utils"; import BN from "bn.js"; @@ -220,7 +220,7 @@ describe("Split position", () => { it("Split position into two position", async () => { // swap - await swap(context.banksClient, { + await swapExactIn(context.banksClient, { payer: user, pool, inputTokenMint: tokenAMint, @@ -230,7 +230,7 @@ describe("Split position", () => { referralTokenAccount: null, }); - await swap(context.banksClient, { + await swapExactIn(context.banksClient, { payer: user, pool, inputTokenMint: tokenBMint, diff --git a/tests/swap.test.ts b/tests/swap.test.ts index c2cfd17b..f7e509f2 100644 --- a/tests/swap.test.ts +++ b/tests/swap.test.ts @@ -1,5 +1,10 @@ import { ProgramTestContext } from "solana-bankrun"; -import { convertToByteArray, generateKpAndFund, randomID, startTest } from "./bankrun-utils/common"; +import { + convertToByteArray, + generateKpAndFund, + randomID, + startTest, +} from "./bankrun-utils/common"; import { Keypair, PublicKey } from "@solana/web3.js"; import { addLiquidity, @@ -12,18 +17,30 @@ import { MIN_LP_AMOUNT, MAX_SQRT_PRICE, MIN_SQRT_PRICE, - swap, + swapExactIn, SwapParams, createToken, mintSplTokenTo, + swap2ExactIn, + U64_MAX, + swap2PartialFillIn, + swap2ExactOut, + OFFSET, } from "./bankrun-utils"; import BN from "bn.js"; -import { ExtensionType } from "@solana/spl-token"; +import { + ExtensionType, + getAssociatedTokenAddressSync, + TOKEN_2022_PROGRAM_ID, + unpackAccount, +} from "@solana/spl-token"; import { createToken2022, createTransferFeeExtensionWithInstruction, mintToToken2022, } from "./bankrun-utils/token2022"; +import { expect } from "chai"; +import { on } from "events"; describe("Swap token", () => { describe("SPL Token", () => { @@ -163,7 +180,7 @@ describe("Swap token", () => { referralTokenAccount: null, }; - await swap(context.banksClient, swapParams); + await swapExactIn(context.banksClient, swapParams); }); }); @@ -275,7 +292,7 @@ describe("Swap token", () => { ); liquidity = new BN(MIN_LP_AMOUNT); - sqrtPrice = new BN(MIN_SQRT_PRICE.muln(2)); + sqrtPrice = new BN(1).shln(OFFSET); const initPoolParams: InitializePoolParams = { payer: creator, @@ -319,7 +336,202 @@ describe("Swap token", () => { referralTokenAccount: null, }; - await swap(context.banksClient, swapParams); + await swapExactIn(context.banksClient, swapParams); + }); + + describe("Swap2", () => { + describe("SwapExactIn", () => { + it("Swap successfully", async () => { + const tokenPermutation = [ + [inputTokenMint, outputTokenMint], + [outputTokenMint, inputTokenMint], + ]; + + for (const [inputTokenMint, outputTokenMint] of tokenPermutation) { + const addLiquidityParams: AddLiquidityParams = { + owner: user, + pool, + position, + liquidityDelta: new BN(MIN_SQRT_PRICE.muln(30)), + tokenAAmountThreshold: new BN(200), + tokenBAmountThreshold: new BN(200), + }; + await addLiquidity(context.banksClient, addLiquidityParams); + + const amountIn = new BN(10); + + const userInputAta = getAssociatedTokenAddressSync( + inputTokenMint, + user.publicKey, + true, + TOKEN_2022_PROGRAM_ID + ); + + const beforeUserInputRawAccount = + await context.banksClient.getAccount(userInputAta); + + const beforeBalance = unpackAccount( + userInputAta, + // @ts-ignore + beforeUserInputRawAccount, + TOKEN_2022_PROGRAM_ID + ).amount; + + await swap2ExactIn(context.banksClient, { + payer: user, + pool, + inputTokenMint, + outputTokenMint, + amount0: amountIn, + amount1: new BN(0), + referralTokenAccount: null, + }); + + const afterUserInputRawAccount = + await context.banksClient.getAccount(userInputAta); + + const afterUserInputTokenAccount = unpackAccount( + userInputAta, + // @ts-ignore + afterUserInputRawAccount, + TOKEN_2022_PROGRAM_ID + ); + + const afterBalance = afterUserInputTokenAccount.amount; + const exactInputAmount = beforeBalance - afterBalance; + expect(Number(exactInputAmount)).to.be.equal(amountIn.toNumber()); + } + }); + }); + + describe("SwapPartialFill", () => { + it("Swap successfully", async () => { + const tokenPermutation = [ + [inputTokenMint, outputTokenMint], + [outputTokenMint, inputTokenMint], + ]; + + for (const [inputTokenMint, outputTokenMint] of tokenPermutation) { + const addLiquidityParams: AddLiquidityParams = { + owner: user, + pool, + position, + liquidityDelta: new BN(MIN_SQRT_PRICE.muln(30)), + tokenAAmountThreshold: new BN(200), + tokenBAmountThreshold: new BN(200), + }; + await addLiquidity(context.banksClient, addLiquidityParams); + + const amountIn = new BN("10000000000000"); + + const userInputAta = getAssociatedTokenAddressSync( + inputTokenMint, + user.publicKey, + true, + TOKEN_2022_PROGRAM_ID + ); + + const beforeUserInputRawAccount = + await context.banksClient.getAccount(userInputAta); + + const beforeBalance = unpackAccount( + userInputAta, + // @ts-ignore + beforeUserInputRawAccount, + TOKEN_2022_PROGRAM_ID + ).amount; + + await swap2PartialFillIn(context.banksClient, { + payer: user, + pool, + inputTokenMint, + outputTokenMint, + amount0: amountIn, + amount1: new BN(0), + referralTokenAccount: null, + }); + + const afterUserInputRawAccount = + await context.banksClient.getAccount(userInputAta); + + const afterUserInputTokenAccount = unpackAccount( + userInputAta, + // @ts-ignore + afterUserInputRawAccount, + TOKEN_2022_PROGRAM_ID + ); + + const afterBalance = afterUserInputTokenAccount.amount; + const exactInputAmount = beforeBalance - afterBalance; + expect(new BN(exactInputAmount.toString()).lt(amountIn)).to.be.true; + } + }); + }); + + describe("SwapExactOut", () => { + it("Swap successfully", async () => { + const tokenPermutation = [ + [inputTokenMint, outputTokenMint], + [outputTokenMint, inputTokenMint], + ]; + + for (const [inputTokenMint, outputTokenMint] of tokenPermutation) { + const addLiquidityParams: AddLiquidityParams = { + owner: user, + pool, + position, + liquidityDelta: new BN("10000000000").shln(OFFSET), + tokenAAmountThreshold: U64_MAX, + tokenBAmountThreshold: U64_MAX, + }; + await addLiquidity(context.banksClient, addLiquidityParams); + + const amountOut = new BN(1000); + + const userOutputAta = getAssociatedTokenAddressSync( + outputTokenMint, + user.publicKey, + true, + TOKEN_2022_PROGRAM_ID + ); + + const beforeUserOutputRawAccount = + await context.banksClient.getAccount(userOutputAta); + + const beforeBalance = unpackAccount( + userOutputAta, + // @ts-ignore + beforeUserOutputRawAccount, + TOKEN_2022_PROGRAM_ID + ).amount; + + await swap2ExactOut(context.banksClient, { + payer: user, + pool, + inputTokenMint, + outputTokenMint, + amount0: amountOut, + amount1: new BN("100000000"), + referralTokenAccount: null, + }); + + const afterUserOutputRawAccount = + await context.banksClient.getAccount(userOutputAta); + + const afterUserInputTokenAccount = unpackAccount( + userOutputAta, + // @ts-ignore + afterUserOutputRawAccount, + TOKEN_2022_PROGRAM_ID + ); + + const afterBalance = afterUserInputTokenAccount.amount; + const exactOutputAmount = afterBalance - beforeBalance; + expect(new BN(exactOutputAmount.toString()).eq(amountOut)).to.be + .true; + } + }); + }); }); }); }); From fc122978de4b069a99fe3f28fd11c7b047d2fdb2 Mon Sep 17 00:00:00 2001 From: kampung-tech Date: Sat, 23 Aug 2025 10:22:17 +0800 Subject: [PATCH 05/10] fix: swap in invalid amount in (#102) --- programs/cp-amm/src/instructions/swap/swap_exact_in.rs | 8 ++++++-- .../cp-amm/src/instructions/swap/swap_partial_fill.rs | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/programs/cp-amm/src/instructions/swap/swap_exact_in.rs b/programs/cp-amm/src/instructions/swap/swap_exact_in.rs index 43120e93..7d125499 100644 --- a/programs/cp-amm/src/instructions/swap/swap_exact_in.rs +++ b/programs/cp-amm/src/instructions/swap/swap_exact_in.rs @@ -24,8 +24,12 @@ pub fn process_swap_exact_in<'a, 'b, 'info>( require!(excluded_transfer_fee_amount_in > 0, PoolError::AmountIsZero); - let swap_result = - pool.get_swap_result_from_exact_input(amount_in, fee_mode, trade_direction, current_point)?; + let swap_result = pool.get_swap_result_from_exact_input( + excluded_transfer_fee_amount_in, + fee_mode, + trade_direction, + current_point, + )?; let excluded_transfer_fee_amount_out = calculate_transfer_fee_excluded_amount(token_out_mint, swap_result.output_amount)?.amount; diff --git a/programs/cp-amm/src/instructions/swap/swap_partial_fill.rs b/programs/cp-amm/src/instructions/swap/swap_partial_fill.rs index 9fafc3d5..2927839b 100644 --- a/programs/cp-amm/src/instructions/swap/swap_partial_fill.rs +++ b/programs/cp-amm/src/instructions/swap/swap_partial_fill.rs @@ -25,7 +25,7 @@ pub fn process_swap_partial_fill<'a, 'b, 'info>( require!(excluded_transfer_fee_amount_in > 0, PoolError::AmountIsZero); let swap_result = pool.get_swap_result_from_partial_input( - amount_in, + excluded_transfer_fee_amount_in, fee_mode, trade_direction, current_point, From abf28511083dbb51fdc4d6dba5df539aae6e139e Mon Sep 17 00:00:00 2001 From: kampung-tech Date: Sat, 23 Aug 2025 11:30:25 +0800 Subject: [PATCH 06/10] Fix inconsistency max fee numerator (#103) * fix: validate fee based on pool version * refactor (#104) --------- Co-authored-by: andrewsource147 <31321699+andrewsource147@users.noreply.github.com> --- .../cp-amm/src/base_fee/fee_rate_limiter.rs | 10 +++++--- programs/cp-amm/src/base_fee/fee_scheduler.rs | 7 ++++-- programs/cp-amm/src/constants.rs | 20 ++++++++++++++++ programs/cp-amm/src/error.rs | 3 +++ programs/cp-amm/src/state/pool.rs | 24 +++++-------------- tests/alphaVaultWithSniperTax.test.ts | 9 ++++--- 6 files changed, 45 insertions(+), 28 deletions(-) diff --git a/programs/cp-amm/src/base_fee/fee_rate_limiter.rs b/programs/cp-amm/src/base_fee/fee_rate_limiter.rs index cbc5bc0f..e1205852 100644 --- a/programs/cp-amm/src/base_fee/fee_rate_limiter.rs +++ b/programs/cp-amm/src/base_fee/fee_rate_limiter.rs @@ -1,7 +1,10 @@ use crate::{ activation_handler::ActivationType, constants::{ - fee::{FEE_DENOMINATOR, MAX_FEE_BPS_V1, MAX_FEE_NUMERATOR_V1, MIN_FEE_NUMERATOR}, + fee::{ + get_max_fee_bps, get_max_fee_numerator, CURRENT_POOL_VERSION, FEE_DENOMINATOR, + MIN_FEE_NUMERATOR, + }, MAX_RATE_LIMITER_DURATION_IN_SECONDS, MAX_RATE_LIMITER_DURATION_IN_SLOTS, }, params::{fee_parameters::to_numerator, swap::TradeDirection}, @@ -323,7 +326,7 @@ impl BaseFeeHandler for FeeRateLimiter { let max_fee_bps_u64 = u64::try_from(self.max_fee_bps).map_err(|_| PoolError::TypeCastFailed)?; require!( - max_fee_bps_u64 <= MAX_FEE_BPS_V1, + max_fee_bps_u64 <= get_max_fee_bps(CURRENT_POOL_VERSION)?, PoolError::InvalidFeeRateLimiter ); @@ -340,7 +343,8 @@ impl BaseFeeHandler for FeeRateLimiter { let min_fee_numerator = self.get_fee_numerator_from_included_fee_amount(0)?; let max_fee_numerator = self.get_fee_numerator_from_included_fee_amount(u64::MAX)?; require!( - min_fee_numerator >= MIN_FEE_NUMERATOR && max_fee_numerator <= MAX_FEE_NUMERATOR_V1, + min_fee_numerator >= MIN_FEE_NUMERATOR + && max_fee_numerator <= get_max_fee_numerator(CURRENT_POOL_VERSION)?, PoolError::InvalidFeeRateLimiter ); diff --git a/programs/cp-amm/src/base_fee/fee_scheduler.rs b/programs/cp-amm/src/base_fee/fee_scheduler.rs index e41decd0..019a0d8d 100644 --- a/programs/cp-amm/src/base_fee/fee_scheduler.rs +++ b/programs/cp-amm/src/base_fee/fee_scheduler.rs @@ -1,6 +1,8 @@ use crate::{ activation_handler::ActivationType, - constants::fee::{FEE_DENOMINATOR, MAX_FEE_NUMERATOR_V1, MIN_FEE_NUMERATOR}, + constants::fee::{ + get_max_fee_numerator, CURRENT_POOL_VERSION, FEE_DENOMINATOR, MIN_FEE_NUMERATOR, + }, fee_math::get_fee_in_period, math::safe_math::SafeMath, params::{fee_parameters::validate_fee_fraction, swap::TradeDirection}, @@ -98,7 +100,8 @@ impl BaseFeeHandler for FeeScheduler { validate_fee_fraction(min_fee_numerator, FEE_DENOMINATOR)?; validate_fee_fraction(max_fee_numerator, FEE_DENOMINATOR)?; require!( - min_fee_numerator >= MIN_FEE_NUMERATOR && max_fee_numerator <= MAX_FEE_NUMERATOR_V1, + min_fee_numerator >= MIN_FEE_NUMERATOR + && max_fee_numerator <= get_max_fee_numerator(CURRENT_POOL_VERSION)?, PoolError::ExceedMaxFeeBps ); Ok(()) diff --git a/programs/cp-amm/src/constants.rs b/programs/cp-amm/src/constants.rs index 4226379a..4df28e7f 100644 --- a/programs/cp-amm/src/constants.rs +++ b/programs/cp-amm/src/constants.rs @@ -82,6 +82,8 @@ pub mod activation { /// Store constants related to fees pub mod fee { + use crate::PoolError; + use anchor_lang::prelude::*; /// Default fee denominator. DO NOT simply update it as it will break logic that depends on it as default value. pub const FEE_DENOMINATOR: u64 = 1_000_000_000; @@ -123,6 +125,24 @@ pub mod fee { static_assertions::const_assert!(PROTOCOL_FEE_PERCENT <= 50); static_assertions::const_assert!(HOST_FEE_PERCENT <= 50); static_assertions::const_assert!(PARTNER_FEE_PERCENT <= 50); + + pub const CURRENT_POOL_VERSION: u8 = 0; + + pub fn get_max_fee_numerator(pool_version: u8) -> Result { + match pool_version { + 0 => Ok(MAX_FEE_NUMERATOR_V0), + 1 => Ok(MAX_FEE_NUMERATOR_V1), + _ => Err(PoolError::InvalidPoolVersion.into()), + } + } + + pub fn get_max_fee_bps(pool_version: u8) -> Result { + match pool_version { + 0 => Ok(MAX_FEE_BPS_V0), + 1 => Ok(MAX_FEE_BPS_V1), + _ => Err(PoolError::InvalidPoolVersion.into()), + } + } } pub mod seeds { diff --git a/programs/cp-amm/src/error.rs b/programs/cp-amm/src/error.rs index 2e0de378..bbf3636a 100644 --- a/programs/cp-amm/src/error.rs +++ b/programs/cp-amm/src/error.rs @@ -160,4 +160,7 @@ pub enum PoolError { #[msg("Undetermined error")] UndeterminedError, + + #[msg("Invalid pool version")] + InvalidPoolVersion, } diff --git a/programs/cp-amm/src/state/pool.rs b/programs/cp-amm/src/state/pool.rs index 25b0690f..9fec39a0 100644 --- a/programs/cp-amm/src/state/pool.rs +++ b/programs/cp-amm/src/state/pool.rs @@ -5,14 +5,12 @@ use std::cmp::min; use anchor_lang::prelude::*; use num_enum::{IntoPrimitive, TryFromPrimitive}; +use crate::constants::fee::{get_max_fee_numerator, CURRENT_POOL_VERSION}; use crate::curve::get_next_sqrt_price_from_output; use crate::state::fee::{FeeOnAmountResult, SplitFees}; use crate::{ assert_eq_admin, - constants::{ - fee::{MAX_FEE_NUMERATOR_V0, MAX_FEE_NUMERATOR_V1}, - LIQUIDITY_SCALE, NUM_REWARDS, REWARD_INDEX_0, REWARD_INDEX_1, REWARD_RATE_SCALE, - }, + constants::{LIQUIDITY_SCALE, NUM_REWARDS, REWARD_INDEX_0, REWARD_INDEX_1, REWARD_RATE_SCALE}, curve::{ get_delta_amount_a_unsigned, get_delta_amount_a_unsigned_unchecked, get_delta_amount_b_unsigned, get_next_sqrt_price_from_input, @@ -415,23 +413,13 @@ impl Pool { self.sqrt_price = sqrt_price; self.collect_fee_mode = collect_fee_mode; self.pool_type = pool_type; - self.version = PoolVersion::V0.into(); // still use v0 for now + self.version = CURRENT_POOL_VERSION; // still use v0 now, after notify integrators will pump to v1 to allow higher fee numerator constraint } pub fn pool_reward_initialized(&self) -> bool { self.reward_infos[0].initialized() || self.reward_infos[1].initialized() } - pub fn get_max_fee_numerator(&self) -> Result { - let pool_version = - PoolVersion::try_from(self.version).map_err(|_| PoolError::TypeCastFailed)?; - if pool_version == PoolVersion::V0 { - Ok(MAX_FEE_NUMERATOR_V0) - } else { - Ok(MAX_FEE_NUMERATOR_V1) - } - } - pub fn get_swap_result_from_exact_output( &self, amount_out: u64, @@ -444,7 +432,7 @@ impl Pool { let mut actual_referral_fee = 0; let mut actual_partner_fee = 0; - let max_fee_numerator = self.get_max_fee_numerator()?; + let max_fee_numerator = get_max_fee_numerator(self.version)?; let included_fee_amount_out = if fee_mode.fees_on_input { amount_out @@ -545,7 +533,7 @@ impl Pool { let mut actual_referral_fee = 0; let mut actual_partner_fee = 0; - let max_fee_numerator = self.get_max_fee_numerator()?; + let max_fee_numerator = get_max_fee_numerator(self.version)?; let trade_fee_numerator = self .pool_fees @@ -680,7 +668,7 @@ impl Pool { let mut actual_referral_fee = 0; let mut actual_partner_fee = 0; - let max_fee_numerator = self.get_max_fee_numerator()?; + let max_fee_numerator = get_max_fee_numerator(self.version)?; // We can compute the trade_fee_numerator here. Instead of separately for amount_in, and amount_out. // This is because FeeRateLimiter (fee rate scale based on amount) only applied when fee_mode.fees_on_input diff --git a/tests/alphaVaultWithSniperTax.test.ts b/tests/alphaVaultWithSniperTax.test.ts index 2046b000..8b5348ac 100644 --- a/tests/alphaVaultWithSniperTax.test.ts +++ b/tests/alphaVaultWithSniperTax.test.ts @@ -63,10 +63,10 @@ describe("Alpha vault with sniper tax", () => { it("Alpha vault can buy before activation point with minimum fee", async () => { const baseFee = { - cliffFeeNumerator: new BN(990_000_000), // 99 % + cliffFeeNumerator: new BN(500_000_000), // 50 % firstFactor: 100, // 100 periods secondFactor: convertToByteArray(new BN(1)), - thirdFactor: new BN(9875000), + thirdFactor: new BN(4875000), baseFeeMode: 0, // fee scheduler Linear mode }; const { pool, alphaVault } = await alphaVaultWithSniperTaxFullflow( @@ -218,9 +218,7 @@ const alphaVaultWithSniperTaxFullflow = async ( activationPoint, poolFees: { baseFee, - protocolFeePercent: 20, - partnerFeePercent: 0, - referralFeePercent: 20, + padding: [], dynamicFee: null, }, activationType: 0, // slot @@ -231,6 +229,7 @@ const alphaVaultWithSniperTaxFullflow = async ( params ); + console.log("setup prorata vault"); let startVestingPoint = new BN(Number(currentSlot) + startVestingPointDiff); let endVestingPoint = new BN(Number(currentSlot) + endVestingPointDiff); From 5cb5b935ec3ed83595961eec15243419da77e69f Mon Sep 17 00:00:00 2001 From: defi0x1 <34453681+defi0x1@users.noreply.github.com> Date: Mon, 25 Aug 2025 08:42:16 +0700 Subject: [PATCH 07/10] Feat/sdk calculate init price (#97) * sdk: calculate init price * fix comments * refactor * fix & test --------- Co-authored-by: Andrew Nguyen --- Cargo.lock | 1 + rust-sdk/Cargo.toml | 1 + rust-sdk/src/calculate_init_sqrt_price.rs | 45 ++++ rust-sdk/src/lib.rs | 1 + rust-sdk/src/tests/mod.rs | 1 + .../tests/test_calculate_init_sqrt_price.rs | 242 ++++++++++++++++++ 6 files changed, 291 insertions(+) create mode 100644 rust-sdk/src/calculate_init_sqrt_price.rs create mode 100644 rust-sdk/src/tests/test_calculate_init_sqrt_price.rs diff --git a/Cargo.lock b/Cargo.lock index de9c83bf..266c9612 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1741,6 +1741,7 @@ dependencies = [ "anyhow", "bytemuck", "cp-amm", + "ruint", ] [[package]] diff --git a/rust-sdk/Cargo.toml b/rust-sdk/Cargo.toml index ee86545d..07efae07 100644 --- a/rust-sdk/Cargo.toml +++ b/rust-sdk/Cargo.toml @@ -8,6 +8,7 @@ authors = ["minh "] [dependencies] anyhow = "1.0.71" cp-amm = { path = "../programs/cp-amm" } +ruint = "1.3.0" [dev-dependencies] bytemuck = { workspace = true} \ No newline at end of file diff --git a/rust-sdk/src/calculate_init_sqrt_price.rs b/rust-sdk/src/calculate_init_sqrt_price.rs new file mode 100644 index 00000000..a8e6f6d4 --- /dev/null +++ b/rust-sdk/src/calculate_init_sqrt_price.rs @@ -0,0 +1,45 @@ +use anyhow::{ensure, Ok, Result}; +use cp_amm::{safe_math::SafeMath, utils_math::sqrt_u256}; +use ruint::aliases::U256; + +// a = L * (1/s - 1/pb) +// b = L * (s - pa) +// b/a = (s - pa) / (1/s - 1/pb) +// With: x = 1 / pb and y = b/a +// => s ^ 2 + s * (-pa + x * y) - y = 0 +// s = [(pa - xy) + √((xy - pa)² + 4y)]/2, // pa: min_sqrt_price, pb: max_sqrt_price +// s = [(pa - b << 128 / a / pb) + sqrt((b << 128 / a / pb - pa)² + 4 * b << 128 / a)] / 2 +pub fn calculate_init_price( + token_a_amount: u64, + token_b_amount: u64, + min_sqrt_price: u128, + max_sqrt_price: u128, +) -> Result { + ensure!( + token_a_amount != 0 && token_b_amount != 0, + "Token amounts must be non-zero" + ); + + let a = U256::from(token_a_amount); + let b = U256::from(token_b_amount) + .safe_shl(128) + .map_err(|_| anyhow::anyhow!("Math overflow"))?; + let pa = U256::from(min_sqrt_price); + let pb = U256::from(max_sqrt_price); + + let four = U256::from(4); + let two = U256::from(2); + + let s = if b / a > pa * pb { + let delta = b / a / pb - pa; + let sqrt_value = sqrt_u256(delta * delta + four * b / a) + .ok_or_else(|| anyhow::anyhow!("Type cast failed"))?; + (sqrt_value - delta) / two + } else { + let delta = pa - b / a / pb; + let sqrt_value = sqrt_u256(delta * delta + four * b / a) + .ok_or_else(|| anyhow::anyhow!("Type cast failed"))?; + (sqrt_value + delta) / two + }; + Ok(u128::try_from(s).map_err(|_| anyhow::anyhow!("Type cast failed"))?) +} diff --git a/rust-sdk/src/lib.rs b/rust-sdk/src/lib.rs index 77e56e43..3a307f87 100644 --- a/rust-sdk/src/lib.rs +++ b/rust-sdk/src/lib.rs @@ -1,3 +1,4 @@ +pub mod calculate_init_sqrt_price; pub mod quote_exact_in; pub mod quote_exact_out; pub mod quote_partial_fill_in; diff --git a/rust-sdk/src/tests/mod.rs b/rust-sdk/src/tests/mod.rs index efbe8a6d..28f850bb 100644 --- a/rust-sdk/src/tests/mod.rs +++ b/rust-sdk/src/tests/mod.rs @@ -1,3 +1,4 @@ +pub mod test_calculate_init_sqrt_price; pub mod test_quote_exact_in; pub mod test_quote_exact_out; pub mod test_quote_partial_fill_in; diff --git a/rust-sdk/src/tests/test_calculate_init_sqrt_price.rs b/rust-sdk/src/tests/test_calculate_init_sqrt_price.rs new file mode 100644 index 00000000..c6732b7a --- /dev/null +++ b/rust-sdk/src/tests/test_calculate_init_sqrt_price.rs @@ -0,0 +1,242 @@ +use std::u64; + +use cp_amm::{ + constants::{MAX_SQRT_PRICE, MIN_SQRT_PRICE}, + math::safe_math::SafeMath, + state::{ModifyLiquidityResult, Pool}, +}; +use ruint::aliases::{U256, U512}; + +use crate::calculate_init_sqrt_price::calculate_init_price; +use anyhow::{Ok, Result}; + +// Δa = L * (1 / √P_lower - 1 / √P_upper) => L = Δa / (1 / √P_lower - 1 / √P_upper) +fn get_initial_liquidity_from_amount_a( + base_amount: u64, + sqrt_max_price: u128, + sqrt_price: u128, +) -> Result { + let price_delta = U512::from(sqrt_max_price.safe_sub(sqrt_price).unwrap()); + let prod = U512::from(base_amount) + .safe_mul(U512::from(sqrt_price)) + .unwrap() + .safe_mul(U512::from(sqrt_max_price)) + .unwrap(); + let liquidity = prod.safe_div(price_delta).unwrap(); // round down + Ok(liquidity) +} + +// Δb = L (√P_upper - √P_lower) => L = Δb / (√P_upper - √P_lower) +fn get_initial_liquidity_from_amount_b( + quote_amount: u64, + sqrt_min_price: u128, + sqrt_price: u128, +) -> Result { + let price_delta = U256::from(sqrt_price.safe_sub(sqrt_min_price).unwrap()); + let quote_amount = U256::from(quote_amount).safe_shl(128).unwrap(); + let liquidity = quote_amount.safe_div(price_delta).unwrap(); // round down + return Ok(liquidity + .try_into() + .map_err(|_| anyhow::anyhow!("Type cast failed"))?); +} + +fn get_liquidity_for_adding_liquidity( + base_amount: u64, + quote_amount: u64, + sqrt_price: u128, + min_sqrt_price: u128, + max_sqrt_price: u128, +) -> Result { + let liquidity_from_base = + get_initial_liquidity_from_amount_a(base_amount, max_sqrt_price, sqrt_price)?; + let liquidity_from_quote = + get_initial_liquidity_from_amount_b(quote_amount, min_sqrt_price, sqrt_price)?; + if liquidity_from_base > U512::from(liquidity_from_quote) { + Ok(liquidity_from_quote) + } else { + Ok(liquidity_from_base + .try_into() + .map_err(|_| anyhow::anyhow!("Type cast failed"))?) + } +} + +#[test] +fn test_b_div_a_larger_than_pa_mul_pb() { + let token_a_in_amount = U256::from(1_000_000 * 1_000_000_000u64); + let token_b_in_amount = + (U256::from(MAX_SQRT_PRICE) * U256::from(MIN_SQRT_PRICE) * token_a_in_amount) + .safe_shr(128) + .unwrap(); + + let a = u64::try_from(token_a_in_amount).unwrap(); + let b = u64::try_from(token_b_in_amount) + .unwrap() + .safe_add(1) + .unwrap(); + + let init_sqrt_price = calculate_init_price(a, b, MIN_SQRT_PRICE, MAX_SQRT_PRICE).unwrap(); + + println!("init_sqrt_price: {:?}", init_sqrt_price); + + let pool = Pool { + sqrt_min_price: MIN_SQRT_PRICE, + sqrt_max_price: MAX_SQRT_PRICE, + sqrt_price: init_sqrt_price, + ..Default::default() + }; + + let liquidity_delta = get_liquidity_for_adding_liquidity( + a, + b, + init_sqrt_price, + pool.sqrt_min_price, + pool.sqrt_max_price, + ) + .unwrap(); + + let ModifyLiquidityResult { + token_a_amount, + token_b_amount, + } = pool + .get_amounts_for_modify_liquidity(liquidity_delta, cp_amm::u128x128_math::Rounding::Up) + .unwrap(); + + // The small difference in token_a is expected due to rounding in liquidity calculations + let token_a_diff = (token_a_amount as i128 - a as i128).abs(); + assert!( + token_a_diff <= 10, // Allow up to 10 lamport difference + "token_a_amount difference {} is too large (got: {}, expected: {})", + token_a_diff, + token_a_amount, + token_a_in_amount + ); + + // The small difference in token_b is expected due to rounding in liquidity calculations + let token_b_diff = (token_b_amount as i128 - b as i128).abs(); + assert!( + token_b_diff <= 10, // Allow up to 10 lamport difference + "token_b_amount difference {} is too large (got: {}, expected: {})", + token_b_diff, + token_b_amount, + token_b_in_amount + ); +} + +#[test] +fn test_b_div_a_less_than_pa_mul_pb() { + let token_a_in_amount = U256::from(1_000_000 * 1_000_000_000u64); + let token_b_in_amount = + (U256::from(MAX_SQRT_PRICE) * U256::from(MIN_SQRT_PRICE) * token_a_in_amount) + .safe_shr(128) + .unwrap(); + + let a = u64::try_from(token_a_in_amount).unwrap(); + let b = u64::try_from(token_b_in_amount) + .unwrap() + .safe_sub(1) + .unwrap(); + + let init_sqrt_price = calculate_init_price(a, b, MIN_SQRT_PRICE, MAX_SQRT_PRICE).unwrap(); + + println!("init_sqrt_price: {:?}", init_sqrt_price); + + let pool = Pool { + sqrt_min_price: MIN_SQRT_PRICE, + sqrt_max_price: MAX_SQRT_PRICE, + sqrt_price: init_sqrt_price, + ..Default::default() + }; + + let liquidity_delta = get_liquidity_for_adding_liquidity( + a, + b, + init_sqrt_price, + pool.sqrt_min_price, + pool.sqrt_max_price, + ) + .unwrap(); + + let ModifyLiquidityResult { + token_a_amount, + token_b_amount, + } = pool + .get_amounts_for_modify_liquidity(liquidity_delta, cp_amm::u128x128_math::Rounding::Up) + .unwrap(); + + // The small difference in token_a is expected due to rounding in liquidity calculations + let token_a_diff = (token_a_amount as i128 - a as i128).abs(); + assert!( + token_a_diff <= 10, // Allow up to 10 lamport difference + "token_a_amount difference {} is too large (got: {}, expected: {})", + token_a_diff, + token_a_amount, + token_a_in_amount + ); + + // The small difference in token_b is expected due to rounding in liquidity calculations + let token_b_diff = (token_b_amount as i128 - b as i128).abs(); + assert!( + token_b_diff <= 10, // Allow up to 10 lamport difference + "token_b_amount difference {} is too large (got: {}, expected: {})", + token_b_diff, + token_b_amount, + token_b_in_amount + ); +} + +#[test] +fn test_b_div_a_equal_pa_mul_pb() { + let b = 1_000_000u64; + let token_b_in_amount = U256::from(b).safe_shl(128).unwrap(); + let token_a_in_amount = token_b_in_amount + .safe_div(U256::from(MAX_SQRT_PRICE) * U256::from(MIN_SQRT_PRICE)) + .unwrap(); + + let a = u64::try_from(token_a_in_amount).unwrap(); + let init_sqrt_price = calculate_init_price(a, b, MIN_SQRT_PRICE, MAX_SQRT_PRICE).unwrap(); + + println!("init_sqrt_price: {:?}", init_sqrt_price); + + let pool = Pool { + sqrt_min_price: MIN_SQRT_PRICE, + sqrt_max_price: MAX_SQRT_PRICE, + sqrt_price: init_sqrt_price, + ..Default::default() + }; + + let liquidity_delta = get_liquidity_for_adding_liquidity( + a, + b, + init_sqrt_price, + pool.sqrt_min_price, + pool.sqrt_max_price, + ) + .unwrap(); + + let ModifyLiquidityResult { + token_a_amount, + token_b_amount, + } = pool + .get_amounts_for_modify_liquidity(liquidity_delta, cp_amm::u128x128_math::Rounding::Up) + .unwrap(); + + // The small difference in token_a is expected due to rounding in liquidity calculations + let token_a_diff = (token_a_amount as i128 - a as i128).abs(); + assert!( + token_a_diff <= 10, // Allow up to 10 lamport difference + "token_a_amount difference {} is too large (got: {}, expected: {})", + token_a_diff, + token_a_amount, + token_a_in_amount + ); + + // The small difference in token_b is expected due to rounding in liquidity calculations + let token_b_diff = (token_b_amount as i128 - b as i128).abs(); + assert!( + token_b_diff <= 10, // Allow up to 10 lamport difference + "token_b_amount difference {} is too large (got: {}, expected: {})", + token_b_diff, + token_b_amount, + token_b_in_amount + ); +} From b28dba60f1aa407d030443500d9e7d9c0128972e Mon Sep 17 00:00:00 2001 From: defi0x1 Date: Tue, 9 Sep 2025 10:49:54 +0700 Subject: [PATCH 08/10] whitelist protocol fee receiver --- package.json | 2 +- programs/cp-amm/Cargo.toml | 1 + programs/cp-amm/src/constants.rs | 1 + programs/cp-amm/src/error.rs | 6 + ...approve_whitelist_protocol_fee_receiver.rs | 20 ++ .../admin/ix_claim_protocol_fee.rs | 58 +++++- ...x_close_whitelist_protocol_fee_receiver.rs | 26 +++ ..._create_whitelist_protocol_fee_receiver.rs | 43 ++++ programs/cp-amm/src/instructions/admin/mod.rs | 6 + programs/cp-amm/src/lib.rs | 18 ++ programs/cp-amm/src/state/mod.rs | 2 + .../whitelisted_protocol_fee_receiver.rs | 62 ++++++ tests/addLiquidity.test.ts | 5 +- tests/bankrun-utils/accounts.ts | 10 + tests/bankrun-utils/cpAmm.ts | 58 +++++- tests/claimProtocolFee.test.ts | 190 ++++++++++++++++++ 16 files changed, 497 insertions(+), 11 deletions(-) create mode 100644 programs/cp-amm/src/instructions/admin/ix_approve_whitelist_protocol_fee_receiver.rs create mode 100644 programs/cp-amm/src/instructions/admin/ix_close_whitelist_protocol_fee_receiver.rs create mode 100644 programs/cp-amm/src/instructions/admin/ix_create_whitelist_protocol_fee_receiver.rs create mode 100644 programs/cp-amm/src/state/whitelisted_protocol_fee_receiver.rs create mode 100644 tests/claimProtocolFee.test.ts diff --git a/package.json b/package.json index d2dee1c2..051c389b 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "scripts": { "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check", - "test": "anchor build -- --features local && yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/*.test.ts" + "test": "anchor build -- --features local && yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/*.test.ts", }, "dependencies": { "@coral-xyz/anchor": "^0.31.0" diff --git a/programs/cp-amm/Cargo.toml b/programs/cp-amm/Cargo.toml index 85ddd04d..f40da491 100644 --- a/programs/cp-amm/Cargo.toml +++ b/programs/cp-amm/Cargo.toml @@ -17,6 +17,7 @@ default = [] local = [] idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] devnet = [] +local_admin = [] [dependencies] anchor-lang = { workspace = true, features = ["event-cpi"] } diff --git a/programs/cp-amm/src/constants.rs b/programs/cp-amm/src/constants.rs index 4df28e7f..a19e140a 100644 --- a/programs/cp-amm/src/constants.rs +++ b/programs/cp-amm/src/constants.rs @@ -156,6 +156,7 @@ pub mod seeds { pub const TOKEN_BADGE_PREFIX: &[u8] = b"token_badge"; pub const REWARD_VAULT_PREFIX: &[u8] = b"reward_vault"; pub const CLAIM_FEE_OPERATOR_PREFIX: &[u8] = b"cf_operator"; + pub const WHITELISTED_PROTOCOL_FEE_RECEIVER: &[u8] = b"pf_receiver"; } pub mod treasury { diff --git a/programs/cp-amm/src/error.rs b/programs/cp-amm/src/error.rs index bbf3636a..9706638f 100644 --- a/programs/cp-amm/src/error.rs +++ b/programs/cp-amm/src/error.rs @@ -163,4 +163,10 @@ pub enum PoolError { #[msg("Invalid pool version")] InvalidPoolVersion, + + #[msg("Invalid token mint")] + InvalidTokenMint, + + #[msg("Invalid fee owner")] + InvalidFeeOwner, } diff --git a/programs/cp-amm/src/instructions/admin/ix_approve_whitelist_protocol_fee_receiver.rs b/programs/cp-amm/src/instructions/admin/ix_approve_whitelist_protocol_fee_receiver.rs new file mode 100644 index 00000000..432a65c4 --- /dev/null +++ b/programs/cp-amm/src/instructions/admin/ix_approve_whitelist_protocol_fee_receiver.rs @@ -0,0 +1,20 @@ +use anchor_lang::prelude::*; + +use crate::state::WhitelistedProtocolFeeReceiver; + +#[derive(Accounts)] +pub struct ApproveWhitelistProtocolFeeReceiver<'info> { + #[account(mut)] + pub whitelist_protocol_fee_receiver: Account<'info, WhitelistedProtocolFeeReceiver>, + + pub admin: Signer<'info>, +} + +pub fn handle_approve_whitelist_protocol_fee_receiver( + ctx: Context, +) -> Result<()> { + let whitelist_protocol_fee_receiver = &mut ctx.accounts.whitelist_protocol_fee_receiver; + whitelist_protocol_fee_receiver.approve(ctx.accounts.admin.key())?; + + Ok(()) +} diff --git a/programs/cp-amm/src/instructions/admin/ix_claim_protocol_fee.rs b/programs/cp-amm/src/instructions/admin/ix_claim_protocol_fee.rs index c15c4e77..08949c5a 100644 --- a/programs/cp-amm/src/instructions/admin/ix_claim_protocol_fee.rs +++ b/programs/cp-amm/src/instructions/admin/ix_claim_protocol_fee.rs @@ -3,10 +3,10 @@ use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; use crate::{ const_pda, - constants::treasury, - state::{ClaimFeeOperator, Pool}, + constants::{treasury, DEFAULT_QUOTE_MINTS}, + state::{ClaimFeeOperator, Pool, WhitelistedProtocolFeeReceiver}, token::transfer_from_pool, - EvtClaimProtocolFee, + EvtClaimProtocolFee, PoolError, }; /// Accounts for withdraw protocol fees @@ -37,7 +37,7 @@ pub struct ClaimProtocolFeesCtx<'info> { /// The treasury token a account #[account( mut, - associated_token::authority = treasury::ID, + associated_token::authority = whitelist_protocol_fee_receiver_a.as_ref().map(|account| account.address).unwrap_or(treasury::ID), associated_token::mint = token_a_mint, associated_token::token_program = token_a_program, )] @@ -46,7 +46,7 @@ pub struct ClaimProtocolFeesCtx<'info> { /// The treasury token b account #[account( mut, - associated_token::authority = treasury::ID, + associated_token::authority = whitelist_protocol_fee_receiver_b.as_ref().map(|account| account.address).unwrap_or(treasury::ID), associated_token::mint = token_b_mint, associated_token::token_program = token_b_program, )] @@ -64,6 +64,44 @@ pub struct ClaimProtocolFeesCtx<'info> { /// Token b program pub token_b_program: Interface<'info, TokenInterface>, + + /// + pub whitelist_protocol_fee_receiver_a: Option>, + /// + pub whitelist_protocol_fee_receiver_b: Option>, +} + +fn validate_protocol_fee_receiver<'info>( + receiver_token: &TokenAccount, + whitelist_protocol_fee_receiver: Option<&Account<'info, WhitelistedProtocolFeeReceiver>>, +) -> Result<()> { + // If the receiver is the Meteora treasury, it's always allowed + if receiver_token.owner.eq(&treasury::ID) { + return Ok(()); + } + + // If not the Meteora treasury, must be whitelisted + require!( + whitelist_protocol_fee_receiver.is_some(), + PoolError::InvalidFeeOwner + ); + + // Must be approved by all admins + let whitelisted_protocol_fee_receiver = whitelist_protocol_fee_receiver.unwrap(); + require!( + whitelisted_protocol_fee_receiver.approved(), + PoolError::InvalidFeeOwner + ); + + // And, must not claim SOL/USDC + require!( + !DEFAULT_QUOTE_MINTS + .iter() + .any(|&m| m.eq(&receiver_token.mint)), + PoolError::InvalidTokenMint + ); + + Ok(()) } /// Withdraw protocol fees. Permissionless. @@ -76,6 +114,16 @@ pub fn handle_claim_protocol_fee( let (token_a_amount, token_b_amount) = pool.claim_protocol_fee(max_amount_a, max_amount_b)?; + validate_protocol_fee_receiver( + ctx.accounts.token_a_account.as_ref(), + ctx.accounts.whitelist_protocol_fee_receiver_a.as_ref(), + )?; + + validate_protocol_fee_receiver( + ctx.accounts.token_b_account.as_ref(), + ctx.accounts.whitelist_protocol_fee_receiver_b.as_ref(), + )?; + if token_a_amount > 0 { transfer_from_pool( ctx.accounts.pool_authority.to_account_info(), diff --git a/programs/cp-amm/src/instructions/admin/ix_close_whitelist_protocol_fee_receiver.rs b/programs/cp-amm/src/instructions/admin/ix_close_whitelist_protocol_fee_receiver.rs new file mode 100644 index 00000000..ed1f3075 --- /dev/null +++ b/programs/cp-amm/src/instructions/admin/ix_close_whitelist_protocol_fee_receiver.rs @@ -0,0 +1,26 @@ +use crate::{assert_eq_admin, state::WhitelistedProtocolFeeReceiver}; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct CloseWhitelistProtocolFeeReceiver<'info> { + #[account( + mut, + close = rent_receiver + )] + pub whitelist_protocol_fee_receiver: Account<'info, WhitelistedProtocolFeeReceiver>, + + /// CHECK: This is not dangerous. We are only transferring lamports to this account. + #[account(mut)] + pub rent_receiver: UncheckedAccount<'info>, + + #[account( + constraint = assert_eq_admin(admin.key()) + )] + pub admin: Signer<'info>, +} + +pub fn handle_close_whitelist_protocol_fee_receiver( + _ctx: Context, +) -> Result<()> { + Ok(()) +} diff --git a/programs/cp-amm/src/instructions/admin/ix_create_whitelist_protocol_fee_receiver.rs b/programs/cp-amm/src/instructions/admin/ix_create_whitelist_protocol_fee_receiver.rs new file mode 100644 index 00000000..46683ba9 --- /dev/null +++ b/programs/cp-amm/src/instructions/admin/ix_create_whitelist_protocol_fee_receiver.rs @@ -0,0 +1,43 @@ +use crate::assert_eq_admin; +use crate::auth::admin::ADMINS; +use crate::constants::seeds::WHITELISTED_PROTOCOL_FEE_RECEIVER; +use crate::state::WhitelistedProtocolFeeReceiver; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct CreateWhitelistProtocolFeeReceiver<'info> { + #[account( + init, + payer = payer, + space = WhitelistedProtocolFeeReceiver::space(ADMINS.len()), + seeds = [WHITELISTED_PROTOCOL_FEE_RECEIVER, protocol_fee_receiver.key().as_ref()], + bump + )] + pub whitelist_protocol_fee_receiver: Account<'info, WhitelistedProtocolFeeReceiver>, + + /// CHECK: This is not dangerous. We are only storing the key of this account. + pub protocol_fee_receiver: UncheckedAccount<'info>, + + #[account(mut)] + pub payer: Signer<'info>, + + #[account( + constraint = assert_eq_admin(admin.key()) + )] + pub admin: Signer<'info>, + pub system_program: Program<'info, System>, +} + +pub fn handle_create_whitelist_protocol_fee_receiver( + ctx: Context, +) -> Result<()> { + let whitelist_protocol_fee_receiver = &mut ctx.accounts.whitelist_protocol_fee_receiver; + + whitelist_protocol_fee_receiver.init( + ctx.accounts.protocol_fee_receiver.key(), + ADMINS.to_vec(), + ctx.accounts.admin.key(), + )?; + + Ok(()) +} diff --git a/programs/cp-amm/src/instructions/admin/mod.rs b/programs/cp-amm/src/instructions/admin/mod.rs index d12346a3..c3036923 100644 --- a/programs/cp-amm/src/instructions/admin/mod.rs +++ b/programs/cp-amm/src/instructions/admin/mod.rs @@ -24,3 +24,9 @@ pub mod ix_update_reward_duration; pub use ix_update_reward_duration::*; pub mod ix_close_token_badge; pub use ix_close_token_badge::*; +pub mod ix_create_whitelist_protocol_fee_receiver; +pub use ix_create_whitelist_protocol_fee_receiver::*; +pub mod ix_close_whitelist_protocol_fee_receiver; +pub use ix_close_whitelist_protocol_fee_receiver::*; +pub mod ix_approve_whitelist_protocol_fee_receiver; +pub use ix_approve_whitelist_protocol_fee_receiver::*; diff --git a/programs/cp-amm/src/lib.rs b/programs/cp-amm/src/lib.rs index 9b02c429..b427ea95 100644 --- a/programs/cp-amm/src/lib.rs +++ b/programs/cp-amm/src/lib.rs @@ -134,6 +134,24 @@ pub mod cp_amm { Ok(()) } + pub fn create_whitelist_protocol_fee_receiver( + ctx: Context, + ) -> Result<()> { + instructions::handle_create_whitelist_protocol_fee_receiver(ctx) + } + + pub fn close_whitelist_protocol_fee_receiver( + ctx: Context, + ) -> Result<()> { + instructions::handle_close_whitelist_protocol_fee_receiver(ctx) + } + + pub fn approve_whitelist_protocol_fee_receiver( + ctx: Context, + ) -> Result<()> { + instructions::handle_approve_whitelist_protocol_fee_receiver(ctx) + } + /// USER FUNCTIONS //// pub fn initialize_pool<'c: 'info, 'info>( diff --git a/programs/cp-amm/src/state/mod.rs b/programs/cp-amm/src/state/mod.rs index c60b0e8c..d986d478 100644 --- a/programs/cp-amm/src/state/mod.rs +++ b/programs/cp-amm/src/state/mod.rs @@ -11,3 +11,5 @@ pub mod vesting; pub use vesting::*; pub mod claim_fee_operator; pub use claim_fee_operator::*; +pub mod whitelisted_protocol_fee_receiver; +pub use whitelisted_protocol_fee_receiver::*; diff --git a/programs/cp-amm/src/state/whitelisted_protocol_fee_receiver.rs b/programs/cp-amm/src/state/whitelisted_protocol_fee_receiver.rs new file mode 100644 index 00000000..83655c67 --- /dev/null +++ b/programs/cp-amm/src/state/whitelisted_protocol_fee_receiver.rs @@ -0,0 +1,62 @@ +use anchor_lang::prelude::*; + +use crate::PoolError; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, InitSpace)] + +pub struct AdminApproval { + pub admin: Pubkey, + pub approved: u8, +} + +#[account] +pub struct WhitelistedProtocolFeeReceiver { + pub address: Pubkey, + pub admin_approval_list: Vec, +} + +impl WhitelistedProtocolFeeReceiver { + pub fn space(admin_count: usize) -> usize { + 8 + // discriminator + std::mem::size_of::() + // address + 4 + (admin_count * AdminApproval::INIT_SPACE) // admin_approval_list (Vec) + } + + pub fn init( + &mut self, + address: Pubkey, + admin_list: Vec, + executing_admin: Pubkey, + ) -> Result<()> { + self.address = address; + self.admin_approval_list = admin_list + .into_iter() + .map(|admin| AdminApproval { + admin, + approved: if admin.eq(&executing_admin) { 1 } else { 0 }, + }) + .collect(); + + Ok(()) + } + + pub fn approve(&mut self, admin: Pubkey) -> Result<()> { + let admin_approval = self + .admin_approval_list + .iter_mut() + .find(|x| x.admin.eq(&admin)) + .ok_or(PoolError::InvalidAdmin)?; + + if admin_approval.approved == 1 { + return Ok(()); + } + + admin_approval.approved = 1; + + Ok(()) + } + + pub fn approved(&self) -> bool { + self.admin_approval_list.iter().all(|x| x.approved == 1) + } +} diff --git a/tests/addLiquidity.test.ts b/tests/addLiquidity.test.ts index 8524997f..356c3d05 100644 --- a/tests/addLiquidity.test.ts +++ b/tests/addLiquidity.test.ts @@ -1,11 +1,9 @@ import { AccountLayout } from "@solana/spl-token"; import { expect } from "chai"; -import { BanksClient, ProgramTestContext } from "solana-bankrun"; +import { ProgramTestContext } from "solana-bankrun"; import { convertToByteArray, generateKpAndFund, randomID, startTest } from "./bankrun-utils/common"; import { Keypair, PublicKey } from "@solana/web3.js"; import BN from "bn.js"; -import { expect } from "chai"; -import { ProgramTestContext } from "solana-bankrun"; import { addLiquidity, AddLiquidityParams, @@ -22,7 +20,6 @@ import { mintSplTokenTo, U64_MAX, } from "./bankrun-utils"; -import { generateKpAndFund, randomID, startTest } from "./bankrun-utils/common"; import { createToken2022, createTransferFeeExtensionWithInstruction, diff --git a/tests/bankrun-utils/accounts.ts b/tests/bankrun-utils/accounts.ts index 9b06ec65..7d0e8a00 100644 --- a/tests/bankrun-utils/accounts.ts +++ b/tests/bankrun-utils/accounts.ts @@ -96,3 +96,13 @@ export function derivePositionNftAccount( CP_AMM_PROGRAM_ID )[0]; } + +export function deriveWhitelistProtocolFeeReceiver( + feeReceiver: PublicKey, +): PublicKey { + + return PublicKey.findProgramAddressSync( + [Buffer.from("pf_receiver"), feeReceiver.toBuffer()], + CP_AMM_PROGRAM_ID + )[0]; +} diff --git a/tests/bankrun-utils/cpAmm.ts b/tests/bankrun-utils/cpAmm.ts index 66277db4..8bbaaee0 100644 --- a/tests/bankrun-utils/cpAmm.ts +++ b/tests/bankrun-utils/cpAmm.ts @@ -52,6 +52,7 @@ import { deriveRewardVaultAddress, deriveTokenBadgeAddress, deriveTokenVaultAddress, + deriveWhitelistProtocolFeeReceiver, } from "./accounts"; import { processTransactionMaybeThrow } from "./common"; import { CP_AMM_PROGRAM_ID } from "./constants"; @@ -1938,7 +1939,6 @@ export async function splitPosition( reward1Percentage, } = params; const program = createCpAmmProgram(); - const poolAuthority = derivePoolAuthority(); const transaction = await program.methods .splitPosition({ permanentLockedLiquidityPercentage, @@ -1965,6 +1965,53 @@ export async function splitPosition( await processTransactionMaybeThrow(banksClient, transaction); } +export type CreateWhitelistProtocolFeeReceiverParams = { + admin: Keypair; + protocolFeeReceiver: PublicKey +} +export async function createWhitelistProtocolFeeReceiver(banksClient: BanksClient, params: CreateWhitelistProtocolFeeReceiverParams): Promise{ + const {admin, protocolFeeReceiver} = params; + + const program = createCpAmmProgram(); + + const transaction = await program.methods.createWhitelistProtocolFeeReceiver().accountsPartial({ + whitelistProtocolFeeReceiver: deriveWhitelistProtocolFeeReceiver(protocolFeeReceiver), + protocolFeeReceiver, + payer: admin.publicKey, + admin: admin.publicKey, + }).transaction() + + transaction.recentBlockhash = (await banksClient.getLatestBlockhash())[0]; + transaction.sign(admin); + + await processTransactionMaybeThrow(banksClient, transaction); + + const whitelistedState = await getWhitelistedProtocolFeeReceiver(banksClient, protocolFeeReceiver); + console.log(whitelistedState) + +} + +export type ApproveWhitelistProtocolFeeReceiverParams = { + admin: Keypair; + protocolFeeReceiver: PublicKey +} +export async function approveWhitelistProtocolFeeReceiver(banksClient: BanksClient, params: ApproveWhitelistProtocolFeeReceiverParams): Promise{ + const {admin, protocolFeeReceiver} = params; + + const program = createCpAmmProgram(); + + const transaction = await program.methods.approveWhitelistProtocolFeeReceiver().accountsPartial({ + whitelistProtocolFeeReceiver: deriveWhitelistProtocolFeeReceiver(protocolFeeReceiver), + admin: admin.publicKey, + }).transaction() + + transaction.recentBlockhash = (await banksClient.getLatestBlockhash())[0]; + transaction.sign(admin); + + await processTransactionMaybeThrow(banksClient, transaction); + +} + export async function getPool( banksClient: BanksClient, pool: PublicKey @@ -2023,3 +2070,12 @@ export async function getTokenBadge( const account = await banksClient.getAccount(tokenBadge); return program.coder.accounts.decode("tokenBadge", Buffer.from(account.data)); } + +export async function getWhitelistedProtocolFeeReceiver( + banksClient: BanksClient, + feeReceiver: PublicKey +): Promise { + const program = createCpAmmProgram(); + const account = await banksClient.getAccount(deriveWhitelistProtocolFeeReceiver(feeReceiver)); + return program.coder.accounts.decode("whitelistedProtocolFeeReceiver", Buffer.from(account.data)); +} diff --git a/tests/claimProtocolFee.test.ts b/tests/claimProtocolFee.test.ts new file mode 100644 index 00000000..0efc9106 --- /dev/null +++ b/tests/claimProtocolFee.test.ts @@ -0,0 +1,190 @@ +import { AccountInfoBytes, BanksClient, ProgramTestContext } from "solana-bankrun"; +import { + convertToByteArray, + generateKpAndFund, + startTest, +} from "./bankrun-utils/common"; +import { Keypair, PublicKey, SystemProgram } from "@solana/web3.js"; +import { + addLiquidity, + AddLiquidityParams, + claimPositionFee, + createConfigIx, + CreateConfigParams, + initializePool, + InitializePoolParams, + MIN_LP_AMOUNT, + MAX_SQRT_PRICE, + MIN_SQRT_PRICE, + swapExactIn, + SwapParams, + createToken, + mintSplTokenTo, + createWhitelistProtocolFeeReceiver, + approveWhitelistProtocolFeeReceiver, +} from "./bankrun-utils"; +import BN from "bn.js"; +import fs from "fs"; + +describe.skip("Whitelist protocol fee receiver", () => { + let context: ProgramTestContext; + + let user: Keypair; + let creator: Keypair; + let protocolFeeReceiver: Keypair; + let config: PublicKey; + let pool: PublicKey; + let position: PublicKey; + let tokenAMint: PublicKey; + let tokenBMint: PublicKey; + const configId = Math.floor(Math.random() * 1000); + + const res = fs.readFileSync( + process.cwd() + + "/keys/localnet/admin-bossj3JvwiNK7pvjr149DqdtJxf2gdygbcmEPTkb2F1.json", + "utf8" +); + + const admin = Keypair.fromSecretKey(new Uint8Array(JSON.parse(res))); + + before(async () => { + const root = Keypair.generate(); + context = await startTest(root); + + user = await generateKpAndFund(context.banksClient, context.payer); + creator = await generateKpAndFund(context.banksClient, context.payer); + protocolFeeReceiver = await generateKpAndFund(context.banksClient, context.payer); + + tokenAMint = await createToken( + context.banksClient, + context.payer, + context.payer.publicKey + ); + tokenBMint = await createToken( + context.banksClient, + context.payer, + context.payer.publicKey + ); + + await mintSplTokenTo( + context.banksClient, + context.payer, + tokenAMint, + context.payer, + user.publicKey + ); + + await mintSplTokenTo( + context.banksClient, + context.payer, + tokenBMint, + context.payer, + user.publicKey + ); + + await mintSplTokenTo( + context.banksClient, + context.payer, + tokenAMint, + context.payer, + creator.publicKey + ); + + await mintSplTokenTo( + context.banksClient, + context.payer, + tokenBMint, + context.payer, + creator.publicKey + ); + + // create config + const createConfigParams: CreateConfigParams = { + poolFees: { + baseFee: { + cliffFeeNumerator: new BN(10_000_000), + firstFactor: 0, + secondFactor: convertToByteArray(new BN(0)), + thirdFactor: new BN(0), + baseFeeMode: 0, + }, + padding: [], + dynamicFee: null, + }, + sqrtMinPrice: new BN(MIN_SQRT_PRICE), + sqrtMaxPrice: new BN(MAX_SQRT_PRICE), + vaultConfigKey: PublicKey.default, + poolCreatorAuthority: PublicKey.default, + activationType: 0, + collectFeeMode: 0, + }; + + config = await createConfigIx( + context.banksClient, + admin, + new BN(configId), + createConfigParams + ); + + const initPoolParams: InitializePoolParams = { + payer: creator, + creator: creator.publicKey, + config, + tokenAMint, + tokenBMint, + liquidity: new BN(MIN_LP_AMOUNT), + sqrtPrice: new BN(MIN_SQRT_PRICE.muln(2)), + activationPoint: null, + }; + + const result = await initializePool(context.banksClient, initPoolParams); + pool = result.pool; + }); + + it("Whitelist protocol fee receiver", async()=>{ + await createWhitelistProtocolFeeReceiver(context.banksClient, { + admin, + protocolFeeReceiver: protocolFeeReceiver.publicKey + }) + }) + + it("Approve whitelist protocol fee receiver", async ()=>{ + await approveWhitelistProtocolFeeReceiver(context.banksClient, { + admin, + protocolFeeReceiver: protocolFeeReceiver.publicKey + }) + + }) + + it.skip("User claim position fee", async () => { + const addLiquidityParams: AddLiquidityParams = { + owner: user, + pool, + position, + liquidityDelta: new BN(MIN_SQRT_PRICE.muln(30)), + tokenAAmountThreshold: new BN(200), + tokenBAmountThreshold: new BN(200), + }; + await addLiquidity(context.banksClient, addLiquidityParams); + + const swapParams: SwapParams = { + payer: user, + pool, + inputTokenMint: tokenAMint, + outputTokenMint: tokenBMint, + amountIn: new BN(10), + minimumAmountOut: new BN(0), + referralTokenAccount: null, + }; + + await swapExactIn(context.banksClient, swapParams); + + // claim position fee + const claimParams = { + owner: user, + pool, + position, + }; + await claimPositionFee(context.banksClient, claimParams); + }); +}); From f4a57418ecec7343d78f7b0ddb8a9f443d0cea7c Mon Sep 17 00:00:00 2001 From: defi0x1 Date: Tue, 9 Sep 2025 10:52:14 +0700 Subject: [PATCH 09/10] minor --- programs/cp-amm/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/programs/cp-amm/Cargo.toml b/programs/cp-amm/Cargo.toml index f40da491..85ddd04d 100644 --- a/programs/cp-amm/Cargo.toml +++ b/programs/cp-amm/Cargo.toml @@ -17,7 +17,6 @@ default = [] local = [] idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] devnet = [] -local_admin = [] [dependencies] anchor-lang = { workspace = true, features = ["event-cpi"] } From 53b6ba45c1053242225faa82c4f9c8b0ec3063dd Mon Sep 17 00:00:00 2001 From: defi0x1 Date: Tue, 9 Sep 2025 11:49:39 +0700 Subject: [PATCH 10/10] fix --- package.json | 2 +- tests/bankrun-utils/cpAmm.ts | 2 ++ tests/claimProtocolFee.test.ts | 11 ++--------- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 051c389b..d2dee1c2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "scripts": { "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check", - "test": "anchor build -- --features local && yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/*.test.ts", + "test": "anchor build -- --features local && yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/*.test.ts" }, "dependencies": { "@coral-xyz/anchor": "^0.31.0" diff --git a/tests/bankrun-utils/cpAmm.ts b/tests/bankrun-utils/cpAmm.ts index 8bbaaee0..8652714e 100644 --- a/tests/bankrun-utils/cpAmm.ts +++ b/tests/bankrun-utils/cpAmm.ts @@ -452,6 +452,8 @@ export async function claimProtocolFee( operator: operator.publicKey, tokenAProgram, tokenBProgram, + whitelistProtocolFeeReceiverA: null, + whitelistProtocolFeeReceiverB: null }) .transaction(); diff --git a/tests/claimProtocolFee.test.ts b/tests/claimProtocolFee.test.ts index 0efc9106..48151664 100644 --- a/tests/claimProtocolFee.test.ts +++ b/tests/claimProtocolFee.test.ts @@ -24,7 +24,6 @@ import { approveWhitelistProtocolFeeReceiver, } from "./bankrun-utils"; import BN from "bn.js"; -import fs from "fs"; describe.skip("Whitelist protocol fee receiver", () => { let context: ProgramTestContext; @@ -32,6 +31,7 @@ describe.skip("Whitelist protocol fee receiver", () => { let user: Keypair; let creator: Keypair; let protocolFeeReceiver: Keypair; + let admin: Keypair; let config: PublicKey; let pool: PublicKey; let position: PublicKey; @@ -39,14 +39,6 @@ describe.skip("Whitelist protocol fee receiver", () => { let tokenBMint: PublicKey; const configId = Math.floor(Math.random() * 1000); - const res = fs.readFileSync( - process.cwd() + - "/keys/localnet/admin-bossj3JvwiNK7pvjr149DqdtJxf2gdygbcmEPTkb2F1.json", - "utf8" -); - - const admin = Keypair.fromSecretKey(new Uint8Array(JSON.parse(res))); - before(async () => { const root = Keypair.generate(); context = await startTest(root); @@ -54,6 +46,7 @@ describe.skip("Whitelist protocol fee receiver", () => { user = await generateKpAndFund(context.banksClient, context.payer); creator = await generateKpAndFund(context.banksClient, context.payer); protocolFeeReceiver = await generateKpAndFund(context.banksClient, context.payer); + admin = await generateKpAndFund(context.banksClient, context.payer); tokenAMint = await createToken( context.banksClient,