diff --git a/contracts/pool/src/contract.rs b/contracts/pool/src/contract.rs index ca9e8d652..4c2fca169 100644 --- a/contracts/pool/src/contract.rs +++ b/contracts/pool/src/contract.rs @@ -9,9 +9,12 @@ use crate::{ stake_contract, storage::{ get_config, get_default_slippage_bps, save_config, save_default_slippage_bps, - utils::{self, get_admin, is_initialized, set_initialized}, + utils::{ + self, calculate_swap_fee, get_admin, get_variable_fee_info, is_initialized, + save_variable_fee_info, set_initialized, + }, Asset, ComputeSwap, Config, LiquidityPoolInfo, PairType, PoolResponse, - SimulateReverseSwapResponse, SimulateSwapResponse, + SimulateReverseSwapResponse, SimulateSwapResponse, VariableFeeInfo, VARIABLE_FEE_INFO, }, token_contract, }; @@ -24,6 +27,14 @@ use soroban_decimal::Decimal; /// Minimum initial LP share const MINIMUM_LIQUIDITY_AMOUNT: i128 = 1_000i128; +/// The smallest fee allowed +const SMALLEST_ALLOWED_FEE: u128 = 10u128; // 0.1% in bps +/// The largest fee allowed +const LARGEST_ALLOWED_FEE: u128 = 200u128; // 2.0% in bps +/// The amount after which the users get higher discount +const MAXIMUM_ALLOWED_STAKING_BREAKPOINT: u128 = 1_000u128; // we shouldn't ask for more than 1_000 + // staked $PHO for the users get + // significantly cheaper swap fees // Metadata that is added on to the WASM custom section contractmeta!( @@ -111,6 +122,10 @@ pub trait LiquidityPoolTrait { max_referral_bps: Option, ); + fn update_variable_fee(env: Env, variable_fee_info: VariableFeeInfo); + + fn delete_variable_fee(env: Env); + // Migration entrypoint fn upgrade(e: Env, new_wasm_hash: BytesN<32>, new_default_slippage_bps: i64); @@ -580,6 +595,47 @@ impl LiquidityPoolTrait for LiquidityPool { save_config(&env, config); } + fn update_variable_fee(env: Env, variable_fee_info: VariableFeeInfo) { + let admin = get_admin(&env); + admin.require_auth(); + + if variable_fee_info.min_fee < SMALLEST_ALLOWED_FEE { + log!( + env, + "Pool: Enable Variable Fee: Invalid minimum fee - less than 0.1%" + ); + panic_with_error!(env, ContractError::InvalidMinimumFeeBps); + } + + if variable_fee_info.max_fee > LARGEST_ALLOWED_FEE { + log!( + env, + "Pool: Enable Variable Fee: Invalid maximum fee - over 2.0%" + ); + panic_with_error!(env, ContractError::InvalidMinimumFeeBps); + } + + if variable_fee_info.staking_breakpoint > MAXIMUM_ALLOWED_STAKING_BREAKPOINT { + log!( + env, + "Pool: Enable Variable Fee: Invalid staking breakpoint - over 1000" + ); + panic_with_error!(env, ContractError::InvalidStakingBreakpoint); + } + + // TODO: add a way to verify that the address provided is really `$PHO` staking contract's + // probably add a new query msg in staking that verifies that + + save_variable_fee_info(&env, variable_fee_info); + } + + fn delete_variable_fee(env: Env) { + let admin = get_admin(&env); + admin.require_auth(); + + env.storage().instance().remove(&VARIABLE_FEE_INFO); + } + fn upgrade(env: Env, new_wasm_hash: BytesN<32>, new_default_slippage_bps: i64) { let admin: Address = utils::get_admin(&env); admin.require_auth(); @@ -841,13 +897,24 @@ fn do_swap( // }; let referral_fee_bps = 0; - // 1. We calculate the referral_fee below. If none referral fee will be 0 + let swap_fee = if env.storage().instance().has(&VARIABLE_FEE_INFO) { + let variable_fee_info = get_variable_fee_info(&env); + let user_staked = + stake_contract::Client::new(&env, &variable_fee_info.pho_token_staking_addr) + .query_staked(&sender) + .total_stake; + + calculate_swap_fee(convert_i128_to_u128(user_staked), variable_fee_info) + } else { + config.protocol_fee_rate() + }; + let compute_swap: ComputeSwap = compute_swap( &env, pool_balance_sell, pool_balance_buy, offer_amount, - config.protocol_fee_rate(), + swap_fee, referral_fee_bps, ); diff --git a/contracts/pool/src/error.rs b/contracts/pool/src/error.rs index a7237e952..b6f86c47b 100644 --- a/contracts/pool/src/error.rs +++ b/contracts/pool/src/error.rs @@ -35,4 +35,7 @@ pub enum ContractError { SwapFeeBpsOverLimit = 25, NotEnoughSharesToBeMinted = 26, NotEnoughLiquidityProvided = 27, + VariableFeeNotSet = 28, + InvalidMinimumFeeBps = 29, + InvalidStakingBreakpoint = 30, } diff --git a/contracts/pool/src/storage.rs b/contracts/pool/src/storage.rs index 633a0aec1..8397154a5 100644 --- a/contracts/pool/src/storage.rs +++ b/contracts/pool/src/storage.rs @@ -32,6 +32,22 @@ pub enum PairType { Xyk = 0, } +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VariableFeeInfo { + /// minimum fee that the users can pay on each swap in `bps` + pub min_fee: u128, + /// maximum fee that the users can pay on each swap in `bps` + pub max_fee: u128, + /// The $PHO token staking address + pub pho_token_staking_addr: Address, + /// value representing the number of tokens that the user must have staked in order for the + /// `total_fee_bps` to start becoming significantly cheaper + pub staking_breakpoint: u128, +} + +pub const VARIABLE_FEE_INFO: Symbol = symbol_short!("VAR_INFO"); + #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct Config { @@ -51,6 +67,7 @@ pub struct Config { /// The maximum allowed percentage (in bps) for referral fee pub max_referral_bps: i64, } + const CONFIG: Symbol = symbol_short!("CONFIG"); const DEFAULT_SLIPPAGE_BPS: Symbol = symbol_short!("DSLIPBPS"); @@ -180,6 +197,7 @@ pub struct SimulateReverseSwapResponse { } pub mod utils { + use phoenix::ttl::{INSTANCE_BUMP_AMOUNT, INSTANCE_LIFETIME_THRESHOLD}; use soroban_sdk::String; use super::*; @@ -451,13 +469,52 @@ pub mod utils { PERSISTENT_BUMP_AMOUNT, ); } + + pub fn save_variable_fee_info(env: &Env, variable_fee_info: VariableFeeInfo) { + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + + env.storage() + .instance() + .set(&VARIABLE_FEE_INFO, &variable_fee_info); + } + + pub fn get_variable_fee_info(env: &Env) -> VariableFeeInfo { + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + + env.storage() + .instance() + .get(&VARIABLE_FEE_INFO) + .unwrap_or_else(|| { + log!(env, "Pool: Get Variable Fee Info: Fee not set"); + panic_with_error!(env, ContractError::VariableFeeNotSet); + }) + } + + // Swap Fee = fee_min + (fee_max−fee_min) × (staking_breakpoint / (staking_breakpoint+staked)) + pub fn calculate_swap_fee(user_staked: u128, variable_fee_info: VariableFeeInfo) -> Decimal { + let fee_min = Decimal::bps(variable_fee_info.min_fee as i64); + let fee_max = Decimal::bps(variable_fee_info.max_fee as i64); + + let staked = Decimal::new(user_staked as i128); + let breakpoint = Decimal::new(variable_fee_info.staking_breakpoint as i128); + + let ratio = breakpoint / (breakpoint + staked); + + fee_min + (fee_max - fee_min) * ratio + } } #[cfg(test)] mod tests { use super::*; + use phoenix::utils::is_approx_ratio; use soroban_sdk::testutils::Address as _; use test_case::test_case; + use utils::calculate_swap_fee; #[test] #[should_panic] @@ -712,4 +769,68 @@ mod tests { "Max allowed slippage should be 1%." ); } + + #[test] + fn swap_fee_should_be_one_percent_no_tokens_staked() { + let env = Env::default(); + + let variable_fee_info = VariableFeeInfo { + min_fee: 10, // 0.1% in bps + max_fee: 100, // 1.0% in bps + pho_token_staking_addr: Address::generate(&env), + staking_breakpoint: 800, + }; + + let result = calculate_swap_fee(0, variable_fee_info); + // should be around 100 bps +/- 1 bps + assert!(is_approx_ratio(result, Decimal::bps(100), Decimal::bps(1))); + } + + #[test] + fn swap_fee_should_be_half_percent_percent_staked_1000() { + let env = Env::default(); + + let variable_fee_info = VariableFeeInfo { + min_fee: 10, // 0.1% + max_fee: 100, // 1.0% + pho_token_staking_addr: Address::generate(&env), + staking_breakpoint: 800, + }; + + let result = calculate_swap_fee(1000, variable_fee_info); + // should be around 50 bps +/- 1 bps + assert!(is_approx_ratio(result, Decimal::bps(50), Decimal::bps(1))); + } + + #[test] + fn swap_fee_should_be_zero_one_percent_large_stake() { + let env = Env::default(); + + let variable_fee_info = VariableFeeInfo { + min_fee: 10, // 0.1% + max_fee: 100, // 1.0% + pho_token_staking_addr: Address::generate(&env), + staking_breakpoint: 800, + }; + + // very large stake + let result = calculate_swap_fee(1_000_000_000_000u128, variable_fee_info); + // should be around 10 bps +/- 1 bps + assert!(is_approx_ratio(result, Decimal::bps(10), Decimal::bps(1))); + } + + #[test] + fn swap_fee_should_be_one_and_half_percent() { + let env = Env::default(); + + let variable_fee_info = VariableFeeInfo { + min_fee: 10, // 0.1% + max_fee: 150, // 1.5% + pho_token_staking_addr: Address::generate(&env), + staking_breakpoint: 800, + }; + + let result = calculate_swap_fee(0, variable_fee_info); + assert_eq!(result, Decimal::bps(150)); + } } diff --git a/contracts/pool/src/tests/config.rs b/contracts/pool/src/tests/config.rs index ff1fcada7..a3adb6dec 100644 --- a/contracts/pool/src/tests/config.rs +++ b/contracts/pool/src/tests/config.rs @@ -86,6 +86,7 @@ fn update_config() { let user1 = Address::generate(&env); let stake_manager = Address::generate(&env); let stake_owner = Address::generate(&env); + let swap_fees = 0i64; let pool = deploy_liquidity_pool_contract( &env, @@ -321,6 +322,7 @@ fn update_liquidity_pool_works() { let user1 = Address::generate(&env); let stake_manager = Address::generate(&env); let stake_owner = Address::generate(&env); + let swap_fees = 0i64; let pool = deploy_liquidity_pool_contract( &env, @@ -380,6 +382,7 @@ fn update_configs_all_bps_values_should_work() { let user1 = Address::generate(&env); let stake_manager = Address::generate(&env); let stake_owner = Address::generate(&env); + let swap_fees = 0i64; let pool = deploy_liquidity_pool_contract( &env,