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/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/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/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..e1205852 --- /dev/null +++ b/programs/cp-amm/src/base_fee/fee_rate_limiter.rs @@ -0,0 +1,381 @@ +use crate::{ + activation_handler::ActivationType, + constants::{ + 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}, + safe_math::SafeMath, + state::{fee::PoolFeesStruct, CollectFeeMode}, + u128x128_math::Rounding, + utils_math::{safe_mul_div_cast_u64, sqrt_u256}, + 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_included_fee_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) + } + + 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 { + 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 <= get_max_fee_bps(CURRENT_POOL_VERSION)?, + 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_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 <= get_max_fee_numerator(CURRENT_POOL_VERSION)?, + PoolError::InvalidFeeRateLimiter + ); + + Ok(()) + } + + 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, + excluded_fee_amount: u64, + ) -> Result { + if self.is_rate_limiter_applied(current_point, activation_point, trade_direction)? { + 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 new file mode 100644 index 00000000..019a0d8d --- /dev/null +++ b/programs/cp-amm/src/base_fee/fee_scheduler.rs @@ -0,0 +1,129 @@ +use crate::{ + activation_handler::ActivationType, + 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}, + 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) + } + } + } + + 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 { + 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 <= get_max_fee_numerator(CURRENT_POOL_VERSION)?, + PoolError::ExceedMaxFeeBps + ); + Ok(()) + } + + fn get_base_fee_numerator_from_included_fee_amount( + &self, + current_point: u64, + activation_point: u64, + _trade_direction: TradeDirection, + _included_fee_amount: u64, + ) -> Result { + 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 new file mode 100644 index 00000000..e32a1230 --- /dev/null +++ b/programs/cp-amm/src/base_fee/mod.rs @@ -0,0 +1,76 @@ +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_from_included_fee_amount( + &self, + current_point: u64, + activation_point: u64, + trade_direction: TradeDirection, + 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; +} + +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..a19e140a 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 @@ -74,13 +82,18 @@ 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; /// 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 +102,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!( @@ -107,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 { @@ -120,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/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 b6144e6e..9706638f 100644 --- a/programs/cp-amm/src/error.rs +++ b/programs/cp-amm/src/error.rs @@ -145,4 +145,28 @@ 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, + + #[msg("Undetermined error")] + UndeterminedError, + + #[msg("Invalid pool version")] + InvalidPoolVersion, + + #[msg("Invalid token mint")] + InvalidTokenMint, + + #[msg("Invalid fee owner")] + InvalidFeeOwner, } 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/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_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/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/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 deleted file mode 100644 index cbe0f805..00000000 --- a/programs/cp-amm/src/instructions/ix_swap.rs +++ /dev/null @@ -1,205 +0,0 @@ -use anchor_lang::prelude::*; -use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; - -use crate::{ - activation_handler::ActivationHandler, - const_pda, get_pool_access_validator, - params::swap::TradeDirection, - state::{fee::FeeMode, Pool}, - token::{calculate_transfer_fee_excluded_amount, transfer_from_pool, transfer_from_user}, - EvtSwap, PoolError, -}; - -#[derive(AnchorSerialize, AnchorDeserialize)] -pub struct SwapParameters { - pub amount_in: u64, - pub minimum_amount_out: u64, -} - -#[event_cpi] -#[derive(Accounts)] -pub struct SwapCtx<'info> { - /// CHECK: pool authority - #[account( - address = const_pda::pool_authority::ID - )] - pub pool_authority: UncheckedAccount<'info>, - - /// Pool account - #[account(mut, has_one = token_a_vault, has_one = token_b_vault)] - pub pool: AccountLoader<'info, Pool>, - - /// The user token account for input token - #[account(mut)] - pub input_token_account: Box>, - - /// The user token account for output token - #[account(mut)] - pub output_token_account: Box>, - - /// The vault token account for input token - #[account(mut, token::token_program = token_a_program, token::mint = token_a_mint)] - pub token_a_vault: Box>, - - /// The vault token account for output token - #[account(mut, token::token_program = token_b_program, token::mint = token_b_mint)] - pub token_b_vault: Box>, - - /// The mint of token a - pub token_a_mint: Box>, - - /// The mint of token b - pub token_b_mint: Box>, - - /// The user performing the swap - pub payer: Signer<'info>, - - /// Token a program - pub token_a_program: Interface<'info, TokenInterface>, - - /// Token b program - pub token_b_program: Interface<'info, TokenInterface>, - - /// referral token account - #[account(mut)] - pub referral_token_account: Option>>, -} - -impl<'info> SwapCtx<'info> { - /// Get the trading direction of the current swap. Eg: USDT -> USDC - pub fn get_trade_direction(&self) -> TradeDirection { - if self.input_token_account.mint == self.token_a_mint.key() { - return TradeDirection::AtoB; - } - TradeDirection::BtoA - } -} - -// TODO impl swap exact out -pub fn handle_swap(ctx: Context, params: SwapParameters) -> Result<()> { - { - let pool = ctx.accounts.pool.load()?; - let access_validator = get_pool_access_validator(&pool)?; - require!( - access_validator.can_swap(&ctx.accounts.payer.key()), - PoolError::PoolDisabled - ); - } - - let SwapParameters { - amount_in, - minimum_amount_out, - } = params; - - let trade_direction = ctx.accounts.get_trade_direction(); - let ( - token_in_mint, - token_out_mint, - input_vault_account, - output_vault_account, - input_program, - output_program, - ) = match trade_direction { - TradeDirection::AtoB => ( - &ctx.accounts.token_a_mint, - &ctx.accounts.token_b_mint, - &ctx.accounts.token_a_vault, - &ctx.accounts.token_b_vault, - &ctx.accounts.token_a_program, - &ctx.accounts.token_b_program, - ), - TradeDirection::BtoA => ( - &ctx.accounts.token_b_mint, - &ctx.accounts.token_a_mint, - &ctx.accounts.token_b_vault, - &ctx.accounts.token_a_vault, - &ctx.accounts.token_b_program, - &ctx.accounts.token_a_program, - ), - }; - - 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); - - let has_referral = ctx.accounts.referral_token_account.is_some(); - - let mut pool = ctx.accounts.pool.load_mut()?; - - // 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( - transfer_fee_excluded_amount_in, - 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)?; - - // send to reserve - transfer_from_user( - &ctx.accounts.payer, - token_in_mint, - &ctx.accounts.input_token_account, - &input_vault_account, - input_program, - amount_in, - )?; - // send to user - transfer_from_pool( - ctx.accounts.pool_authority.to_account_info(), - &token_out_mint, - &output_vault_account, - &ctx.accounts.output_token_account, - output_program, - swap_result.output_amount, - )?; - // send to referral - if has_referral { - if fee_mode.fees_on_token_a { - transfer_from_pool( - ctx.accounts.pool_authority.to_account_info(), - &ctx.accounts.token_a_mint, - &ctx.accounts.token_a_vault, - &ctx.accounts.referral_token_account.clone().unwrap(), - &ctx.accounts.token_a_program, - swap_result.referral_fee, - )?; - } else { - transfer_from_pool( - ctx.accounts.pool_authority.to_account_info(), - &ctx.accounts.token_b_mint, - &ctx.accounts.token_b_vault, - &ctx.accounts.referral_token_account.clone().unwrap(), - &ctx.accounts.token_b_program, - swap_result.referral_fee, - )?; - } - } - - emit_cpi!(EvtSwap { - pool: ctx.accounts.pool.key(), - trade_direction: trade_direction.into(), - params, - swap_result, - has_referral, - actual_amount_in: transfer_fee_excluded_amount_in, - current_timestamp, - }); - - Ok(()) -} 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/swap/ix_swap.rs b/programs/cp-amm/src/instructions/swap/ix_swap.rs new file mode 100644 index 00000000..1876dec1 --- /dev/null +++ b/programs/cp-amm/src/instructions/swap/ix_swap.rs @@ -0,0 +1,342 @@ +use crate::{ + activation_handler::ActivationHandler, + 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, SwapResult2}, + swap::{ProcessSwapParams, ProcessSwapResult}, + token::{transfer_from_pool, transfer_from_user}, + EvtSwap, EvtSwap2, 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}; +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 { + pub amount_in: u64, + 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> { + /// CHECK: pool authority + #[account( + address = const_pda::pool_authority::ID + )] + pub pool_authority: UncheckedAccount<'info>, + + /// Pool account + #[account(mut, has_one = token_a_vault, has_one = token_b_vault)] + pub pool: AccountLoader<'info, Pool>, + + /// The user token account for input token + #[account(mut)] + pub input_token_account: Box>, + + /// The user token account for output token + #[account(mut)] + pub output_token_account: Box>, + + /// The vault token account for input token + #[account(mut, token::token_program = token_a_program, token::mint = token_a_mint)] + pub token_a_vault: Box>, + + /// The vault token account for output token + #[account(mut, token::token_program = token_b_program, token::mint = token_b_mint)] + pub token_b_vault: Box>, + + /// The mint of token a + pub token_a_mint: Box>, + + /// The mint of token b + pub token_b_mint: Box>, + + /// The user performing the swap + pub payer: Signer<'info>, + + /// Token a program + pub token_a_program: Interface<'info, TokenInterface>, + + /// Token b program + pub token_b_program: Interface<'info, TokenInterface>, + + /// referral token account + #[account(mut)] + pub referral_token_account: Option>>, +} + +impl<'info> SwapCtx<'info> { + /// Get the trading direction of the current swap. Eg: USDT -> USDC + pub fn get_trade_direction(&self) -> TradeDirection { + if self.input_token_account.mint == self.token_a_mint.key() { + return TradeDirection::AtoB; + } + TradeDirection::BtoA + } +} + +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)?; + require!( + access_validator.can_swap(&ctx.accounts.payer.key()), + PoolError::PoolDisabled + ); + } + + 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, + input_vault_account, + output_vault_account, + input_program, + output_program, + ) = match trade_direction { + TradeDirection::AtoB => ( + &ctx.accounts.token_a_mint, + &ctx.accounts.token_b_mint, + &ctx.accounts.token_a_vault, + &ctx.accounts.token_b_vault, + &ctx.accounts.token_a_program, + &ctx.accounts.token_b_program, + ), + TradeDirection::BtoA => ( + &ctx.accounts.token_b_mint, + &ctx.accounts.token_a_mint, + &ctx.accounts.token_b_vault, + &ctx.accounts.token_a_vault, + &ctx.accounts.token_b_program, + &ctx.accounts.token_a_program, + ), + }; + + // 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)?; + + // 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 fee_mode = FeeMode::get_fee_mode(pool.collect_fee_mode, trade_direction, has_referral)?; + + 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 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_program, + included_transfer_fee_amount_in, + )?; + + // send to user + transfer_from_pool( + ctx.accounts.pool_authority.to_account_info(), + token_out_mint, + output_vault_account, + &ctx.accounts.output_token_account, + output_program, + included_transfer_fee_amount_out, + )?; + + // send to referral + if has_referral { + if fee_mode.fees_on_token_a { + transfer_from_pool( + ctx.accounts.pool_authority.to_account_info(), + &ctx.accounts.token_a_mint, + &ctx.accounts.token_a_vault, + &ctx.accounts.referral_token_account.clone().unwrap(), + &ctx.accounts.token_a_program, + referral_fee, + )?; + } else { + transfer_from_pool( + ctx.accounts.pool_authority.to_account_info(), + &ctx.accounts.token_b_mint, + &ctx.accounts.token_b_vault, + &ctx.accounts.referral_token_account.clone().unwrap(), + &ctx.accounts.token_b_program, + referral_fee, + )?; + } + } + + 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, + current_timestamp, + included_transfer_fee_amount_in, + included_transfer_fee_amount_out, + excluded_transfer_fee_amount_out, + }); + + 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/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..7d125499 --- /dev/null +++ b/programs/cp-amm/src/instructions/swap/swap_exact_in.rs @@ -0,0 +1,52 @@ +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( + 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; + + 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..2927839b --- /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( + 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; + + 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 46a56094..b427ea95 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; @@ -133,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>( @@ -197,7 +216,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/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..2b414f38 100644 --- a/programs/cp-amm/src/state/fee.rs +++ b/programs/cp-amm/src/state/fee.rs @@ -1,15 +1,10 @@ -use std::u64; - use anchor_lang::prelude::*; 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, BaseFeeHandler, FeeRateLimiter}, + constants::{fee::FEE_DENOMINATOR, BASIS_POINT_MAX, ONE_Q64}, params::swap::TradeDirection, safe_math::SafeMath, u128x128_math::Rounding, @@ -19,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, @@ -43,11 +37,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,101 +84,183 @@ 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_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_min_base_fee_numerator(&self) -> Result { - // trick to force current_point < activation_point - self.get_current_base_fee_numerator(0, 1) + + 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, + ) } - pub fn get_current_base_fee_numerator( +} + +impl PoolFeesStruct { + fn get_total_fee_numerator( &self, - current_point: u64, - activation_point: u64, + base_fee_numerator: u64, + max_fee_numerator: u64, ) -> 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) + 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)?; - 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) - } + if total_fee_numerator > max_fee_numerator { + Ok(max_fee_numerator) + } else { + Ok(total_fee_numerator) } } -} -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)?; - let total_fee_numerator = self - .dynamic_fee - .get_variable_fee()? - .safe_add(base_fee_numerator.into())?; - Ok(total_fee_numerator) + pub fn get_total_trading_fee_from_included_fee_amount( + &self, + current_point: u64, + activation_point: u64, + included_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_included_fee_amount( + current_point, + activation_point, + trade_direction, + included_fee_amount, + )?; + + self.get_total_fee_numerator(base_fee_numerator, max_fee_numerator) + } + + pub fn get_total_trading_fee_from_excluded_fee_amount( + &self, + current_point: u64, + activation_point: u64, + 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, - current_point: u64, - activation_point: u64, has_partner: bool, ) -> 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 - } 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 - let amount = amount.safe_sub(lp_fee)?; + 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, + }) + } + 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( @@ -210,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, }) } } @@ -281,7 +358,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 @@ -361,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/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/pool.rs b/programs/cp-amm/src/state/pool.rs index acdec390..9fec39a0 100644 --- a/programs/cp-amm/src/state/pool.rs +++ b/programs/cp-amm/src/state/pool.rs @@ -1,11 +1,13 @@ 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::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::{LIQUIDITY_SCALE, NUM_REWARDS, REWARD_INDEX_0, REWARD_INDEX_1, REWARD_RATE_SCALE}, @@ -16,7 +18,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}, @@ -78,6 +80,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 +145,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,41 +413,154 @@ impl Pool { self.sqrt_price = sqrt_price; self.collect_fee_mode = collect_fee_mode; self.pool_type = pool_type; + 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_swap_result( + pub fn get_swap_result_from_exact_output( + &self, + amount_out: 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 = get_max_fee_numerator(self.version)?; + + let included_fee_amount_out = if fee_mode.fees_on_input { + amount_out + } else { + 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 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 { + ) -> 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 actual_amount_in = if fee_mode.fees_on_input { + let max_fee_numerator = get_max_fee_numerator(self.version)?; + + 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(), )?; 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; @@ -436,47 +569,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 = 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 + // (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, + 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)?; @@ -493,13 +889,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)?; @@ -515,25 +912,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; @@ -874,9 +1271,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/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/programs/cp-amm/src/tests/fee_scheduler_tests.rs b/programs/cp-amm/src/tests/fee_scheduler_tests.rs index 581cb164..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,17 +18,3 @@ proptest! { assert_eq!(fee_numerator, cliff_fee_numerator) } } - -#[test] -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 - ..Default::default() - }; - let current_fee = base_fee.get_current_base_fee_numerator(100, 0).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 00000000..77622f93 Binary files /dev/null and b/programs/cp-amm/src/tests/fixtures/config_account.bin differ 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 00000000..5727eee1 Binary files /dev/null and b/programs/cp-amm/src/tests/fixtures/pool_account.bin differ 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/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/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 new file mode 100644 index 00000000..f9843425 --- /dev/null +++ b/programs/cp-amm/src/tests/test_rate_limiter.rs @@ -0,0 +1,283 @@ +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_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); + } + + { + let fee_numerator = rate_limiter + .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_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 + } + + { + let fee_numerator = rate_limiter + .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 + } + + { + let fee_numerator = rate_limiter + .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 + } + + { + let fee_numerator = rate_limiter + .get_fee_numerator_from_included_fee_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_from_included_fee_amount(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_from_included_fee_amount( + 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_from_included_fee_amount( + (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_from_included_fee_amount( + 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/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/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/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 00000000..0d378d7b Binary files /dev/null and b/rust-sdk/fixtures/CGPxT5d1uf9a8cKVJuZaJAU76t2EfLGbTmRbfvLLZp5j.bin differ 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 ea381514..3a307f87 100644 --- a/rust-sdk/src/lib.rs +++ b/rust-sdk/src/lib.rs @@ -1,4 +1,8 @@ -pub mod quote; +pub mod calculate_init_sqrt_price; +pub mod quote_exact_in; +pub mod quote_exact_out; +pub mod quote_partial_fill_in; +mod utils; #[cfg(test)] mod tests; diff --git a/rust-sdk/src/quote.rs b/rust-sdk/src/quote.rs deleted file mode 100644 index 6b709554..00000000 --- a/rust-sdk/src/quote.rs +++ /dev/null @@ -1,63 +0,0 @@ -use anyhow::{ensure, Context, Ok, Result}; -use cp_amm::{ - params::swap::TradeDirection, - state::{fee::FeeMode, Pool, SwapResult}, - ActivationType, -}; - -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 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..28f850bb 100644 --- a/rust-sdk/src/tests/mod.rs +++ b/rust-sdk/src/tests/mod.rs @@ -1,11 +1,17 @@ +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; +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_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 + ); +} 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/addLiquidity.test.ts b/tests/addLiquidity.test.ts index 2941157c..356c3d05 100644 --- a/tests/addLiquidity.test.ts +++ b/tests/addLiquidity.test.ts @@ -1,8 +1,9 @@ import { AccountLayout } from "@solana/spl-token"; +import { expect } from "chai"; +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, @@ -19,7 +20,6 @@ import { mintSplTokenTo, U64_MAX, } from "./bankrun-utils"; -import { generateKpAndFund, randomID, startTest } from "./bankrun-utils/common"; import { createToken2022, createTransferFeeExtensionWithInstruction, @@ -94,10 +94,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 +335,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..8b5348ac --- /dev/null +++ b/tests/alphaVaultWithSniperTax.test.ts @@ -0,0 +1,279 @@ +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(500_000_000), // 50 % + firstFactor: 100, // 100 periods + secondFactor: convertToByteArray(new BN(1)), + thirdFactor: new BN(4875000), + 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, + padding: [], + 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/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/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..632c4e73 100644 --- a/tests/bankrun-utils/common.ts +++ b/tests/bankrun-utils/common.ts @@ -6,8 +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 { TRANSFER_HOOK_COUNTER_PROGRAM_ID } from "./transferHook"; + +import CpAmmIdl from "../../target/idl/cp_amm.json"; export async function startTest(root: Keypair) { // Program name need to match fixtures program name @@ -18,6 +21,14 @@ 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), + }, ], [ { @@ -85,6 +96,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 +121,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..8652714e 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, @@ -48,6 +52,7 @@ import { deriveRewardVaultAddress, deriveTokenBadgeAddress, deriveTokenVaultAddress, + deriveWhitelistProtocolFeeReceiver, } from "./accounts"; import { processTransactionMaybeThrow } from "./common"; import { CP_AMM_PROGRAM_ID } from "./constants"; @@ -104,15 +109,15 @@ 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 = { baseFee: BaseFee; - padding: number[], + padding: number[]; dynamicFee: DynamicFee | null; }; @@ -205,27 +210,22 @@ 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(configState.poolFees.protocolFeePercent).eq( - 20 - ); - expect(configState.poolFees.partnerFeePercent).eq( - 0 - ); - expect(configState.poolFees.referralFeePercent).eq( - 20 + + 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.configType).eq(0); // ConfigType: Static return config; @@ -452,6 +452,8 @@ export async function claimProtocolFee( operator: operator.publicKey, tokenAProgram, tokenBProgram, + whitelistProtocolFeeReceiverA: null, + whitelistProtocolFeeReceiverB: null }) .transaction(); @@ -782,11 +784,11 @@ export async function setPoolStatus( export type PoolFeesParams = { baseFee: BaseFee; - padding: number[], + padding: number[]; dynamicFee: DynamicFee | null; }; -export type InitializeCustomizeablePoolParams = { +export type InitializeCustomizablePoolParams = { payer: Keypair; creator: PublicKey; tokenAMint: PublicKey; @@ -802,9 +804,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 +844,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 +1623,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,15 +1682,166 @@ export async function swap(banksClient: BanksClient, params: SwapParams) { 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 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]; - transaction.sign(payer); + transaction.sign(params.payer); await processTransactionMaybeThrow(banksClient, transaction); } -export type ClaimpositionFeeParams = { +export type ClaimPositionFeeParams = { owner: Keypair; pool: PublicKey; position: PublicKey; @@ -1688,7 +1849,7 @@ export type ClaimpositionFeeParams = { export async function claimPositionFee( banksClient: BanksClient, - params: ClaimpositionFeeParams + params: ClaimPositionFeeParams ) { const { owner, pool, position } = params; @@ -1780,7 +1941,6 @@ export async function splitPosition( reward1Percentage, } = params; const program = createCpAmmProgram(); - const poolAuthority = derivePoolAuthority(); const transaction = await program.methods .splitPosition({ permanentLockedLiquidityPercentage, @@ -1807,6 +1967,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 @@ -1865,3 +2072,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/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/bankrun-utils/token2022.ts b/tests/bankrun-utils/token2022.ts index b11a58dd..a57aedf5 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,7 +22,9 @@ import { import { BanksClient } from "solana-bankrun"; import { DECIMALS } from "./constants"; import { getOrCreateAssociatedTokenAccount } from "./token"; -const rawAmount = 1_000_000 * 10 ** DECIMALS; // 1 millions +import { TRANSFER_HOOK_COUNTER_PROGRAM_ID } from "./transferHook"; +import { processTransactionMaybeThrow } from "./common"; +const rawAmount = 1_000_000_000 * 10 ** DECIMALS; // 1 millions interface ExtensionWithInstruction { extension: ExtensionType; @@ -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/claimFee.test.ts b/tests/claimFee.test.ts index 96a76743..76be9cfa 100644 --- a/tests/claimFee.test.ts +++ b/tests/claimFee.test.ts @@ -1,5 +1,10 @@ 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, @@ -12,7 +17,7 @@ import { MIN_LP_AMOUNT, MAX_SQRT_PRICE, MIN_SQRT_PRICE, - swap, + swapExactIn, SwapParams, createClaimFeeOperator, claimProtocolFee, @@ -102,10 +107,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, @@ -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, { @@ -290,10 +295,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, @@ -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 a3678e4f..e1c13058 100644 --- a/tests/claimPositionFee.test.ts +++ b/tests/claimPositionFee.test.ts @@ -1,5 +1,9 @@ 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, @@ -13,7 +17,7 @@ import { MIN_LP_AMOUNT, MAX_SQRT_PRICE, MIN_SQRT_PRICE, - swap, + swapExactIn, SwapParams, createToken, mintSplTokenTo, @@ -88,10 +92,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, @@ -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/claimProtocolFee.test.ts b/tests/claimProtocolFee.test.ts new file mode 100644 index 00000000..48151664 --- /dev/null +++ b/tests/claimProtocolFee.test.ts @@ -0,0 +1,183 @@ +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"; + +describe.skip("Whitelist protocol fee receiver", () => { + let context: ProgramTestContext; + + let user: Keypair; + let creator: Keypair; + let protocolFeeReceiver: Keypair; + let admin: Keypair; + let config: PublicKey; + let pool: PublicKey; + let position: PublicKey; + let tokenAMint: PublicKey; + let tokenBMint: PublicKey; + const configId = Math.floor(Math.random() * 1000); + + 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); + admin = 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); + }); +}); 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 00000000..56e03d38 Binary files /dev/null and b/tests/fixtures/alpha_vault.so differ diff --git a/tests/fixtures/transfer_hook_counter.so b/tests/fixtures/transfer_hook_counter.so new file mode 100755 index 00000000..e5249833 Binary files /dev/null and b/tests/fixtures/transfer_hook_counter.so differ 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..d9b8a60d 100644 --- a/tests/lockPosition.test.ts +++ b/tests/lockPosition.test.ts @@ -23,10 +23,11 @@ import { mintSplTokenTo, permanentLockPosition, refreshVestings, - swap, + swapExactIn, 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, @@ -239,7 +240,7 @@ describe("Lock position", () => { referralTokenAccount: null, }; - await swap(context.banksClient, swapParams); + await swapExactIn(context.banksClient, swapParams); const claimParams = { owner: user, @@ -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, @@ -567,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/permissionLessTransferHook.test.ts b/tests/permissionLessTransferHook.test.ts new file mode 100644 index 00000000..ef1f04bb --- /dev/null +++ b/tests/permissionLessTransferHook.test.ts @@ -0,0 +1,166 @@ +import { expect } from "chai"; +import { ProgramTestContext } from "solana-bankrun"; +import { + convertToByteArray, + expectThrowsAsync, + generateKpAndFund, + startTest, +} from "./bankrun-utils/common"; +import { Keypair, PublicKey } from "@solana/web3.js"; +import { + createConfigIx, + CreateConfigParams, + getPool, + initializePool, + InitializePoolParams, + MIN_LP_AMOUNT, + MAX_SQRT_PRICE, + MIN_SQRT_PRICE, + setPoolStatus, + createToken, + mintSplTokenTo, + getCpAmmProgramErrorCodeHexString, +} from "./bankrun-utils"; +import BN from "bn.js"; +import { + createToken2022, + createTransferHookExtensionWithInstruction, + mintToToken2022, + revokeAuthorityAndProgramIdTransferHook, +} from "./bankrun-utils/token2022"; +import { createExtraAccountMetaListAndCounter } from "./bankrun-utils/transferHook"; + +describe("Permissionless transfer hook", () => { + 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 new file mode 100644 index 00000000..e66f4412 --- /dev/null +++ b/tests/rateLimiter.test.ts @@ -0,0 +1,293 @@ +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, + swapExactIn, + 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 + }, + padding: [], + 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 swapExactIn(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 swapExactIn(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 swapExactIn(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 + }, + padding: [], + 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..2c137953 100644 --- a/tests/splitPosition.test.ts +++ b/tests/splitPosition.test.ts @@ -25,7 +25,8 @@ import { permanentLockPosition, U64_MAX, addLiquidity, - swap, + swapExactIn, + 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), @@ -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 20b742d9..f7e509f2 100644 --- a/tests/swap.test.ts +++ b/tests/swap.test.ts @@ -1,5 +1,10 @@ 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, @@ -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", () => { @@ -95,10 +112,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, @@ -163,7 +180,7 @@ describe("Swap token", () => { referralTokenAccount: null, }; - await swap(context.banksClient, swapParams); + await swapExactIn(context.banksClient, swapParams); }); }); @@ -251,10 +268,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, @@ -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; + } + }); + }); }); }); });