diff --git a/contracts/pool/src/contract.rs b/contracts/pool/src/contract.rs index 8a1d0a4f5..c497cd5df 100644 --- a/contracts/pool/src/contract.rs +++ b/contracts/pool/src/contract.rs @@ -3,7 +3,7 @@ use soroban_sdk::{contract, contractimpl, contractmeta, log, Address, BytesN, En use num_integer::Roots; use crate::storage::utils::{is_initialized, set_initialized}; -use crate::storage::LiquidityPoolInfo; +use crate::storage::{LiquidityPoolInfo, Referral}; use crate::{ stake_contract, storage::{ @@ -63,6 +63,7 @@ pub trait LiquidityPoolTrait { fn swap( env: Env, sender: Address, + referral: Option, offer_asset: Address, offer_amount: i128, belief_price: Option, @@ -265,6 +266,7 @@ impl LiquidityPoolTrait for LiquidityPool { do_swap( env.clone(), sender.clone(), + None, config.clone().token_a, a_for_swap, None, @@ -286,6 +288,7 @@ impl LiquidityPoolTrait for LiquidityPool { do_swap( env.clone(), sender.clone(), + None, config.clone().token_b, b_for_swap, None, @@ -352,6 +355,7 @@ impl LiquidityPoolTrait for LiquidityPool { fn swap( env: Env, sender: Address, + referral: Option, offer_asset: Address, offer_amount: i128, belief_price: Option, @@ -364,6 +368,7 @@ impl LiquidityPoolTrait for LiquidityPool { do_swap( env, sender, + referral, offer_asset, offer_amount, belief_price, @@ -555,11 +560,12 @@ impl LiquidityPoolTrait for LiquidityPool { (pool_balance_b, pool_balance_a) }; - let (ask_amount, spread_amount, commission_amount) = compute_swap( + let (ask_amount, spread_amount, commission_amount, _referral_fee_amount) = compute_swap( pool_balance_offer, pool_balance_ask, offer_amount, config.protocol_fee_rate(), + 0i64, ); let total_return = ask_amount + commission_amount + spread_amount; @@ -605,6 +611,7 @@ impl LiquidityPoolTrait for LiquidityPool { fn do_swap( env: Env, sender: Address, + referral: Option, offer_asset: Address, offer_amount: i128, belief_price: Option, @@ -624,11 +631,18 @@ fn do_swap( (pool_balance_b, pool_balance_a) }; - let (return_amount, spread_amount, commission_amount) = compute_swap( + let referral_fee_bps = match referral { + Some(ref referral) => referral.clone().fee, + None => 0, + }; + + // 1. We calculate the referral_fee below. If none referral fee will be 0 + let (return_amount, spread_amount, commission_amount, referral_fee_amount) = compute_swap( pool_balance_sell, pool_balance_buy, offer_amount, config.protocol_fee_rate(), + referral_fee_bps, ); assert_max_spread( @@ -668,6 +682,18 @@ fn do_swap( &commission_amount, ); + // 2. If referral is present and return amount is larger than 0 we send referral fee commision + // to fee recipient + if let Some(Referral { address, fee }) = referral { + if fee > 0 { + token_contract::Client::new(&env, &buy_token).transfer( + &env.current_contract_address(), + &address, + &referral_fee_amount, + ); + } + } + // user is offering to sell A, so they will receive B // A balance is bigger, B balance is smaller let (balance_a, balance_b) = if offer_asset == config.token_a { @@ -875,17 +901,20 @@ pub fn assert_max_spread( /// - `ask_pool`: Total amount of ask assets in the pool. /// - `offer_amount`: Amount of offer assets to swap. /// - `commission_rate`: Total amount of fees charged for the swap. +/// - `referral_fee`: Amount of fee for the referral /// /// Returns a tuple containing the following values: /// - The resulting amount of ask assets after the swap. /// - The spread amount, representing the difference between the expected and actual swap amounts. /// - The commission amount, representing the fees charged for the swap. +/// - The referral comission fee. pub fn compute_swap( offer_pool: i128, ask_pool: i128, offer_amount: i128, commission_rate: Decimal, -) -> (i128, i128, i128) { + referral_fee: i64, +) -> (i128, i128, i128, i128) { // Calculate the cross product of offer_pool and ask_pool let cp: i128 = offer_pool * ask_pool; @@ -893,7 +922,6 @@ pub fn compute_swap( // Return amount calculation based on the AMM model's invariant, // which ensures the product of the amounts of the two assets remains constant before and after a trade. let return_amount: i128 = ask_pool - (cp / (offer_pool + offer_amount)); - // Calculate the spread amount, representing the difference between the expected and actual swap amounts let spread_amount: i128 = (offer_amount * ask_pool / offer_pool) - return_amount; @@ -901,8 +929,16 @@ pub fn compute_swap( // Deduct the commission (minus the part that goes to the protocol) from the return amount let return_amount: i128 = return_amount - commission_amount; + let referral_fee_amount: i128 = return_amount * Decimal::bps(referral_fee); + + let return_amount: i128 = return_amount - referral_fee_amount; - (return_amount, spread_amount, commission_amount) + ( + return_amount, + spread_amount, + commission_amount, + referral_fee_amount, + ) } /// Returns an amount of offer assets for a specified amount of ask assets. @@ -1046,14 +1082,14 @@ mod tests { #[test] fn test_compute_swap_pass() { - let result = compute_swap(1000, 2000, 100, Decimal::percent(10)); // 10% commission rate - assert_eq!(result, (164, 18, 18)); // Expected return amount, spread, and commission + let result = compute_swap(1000, 2000, 100, Decimal::percent(10), 500i64); // 10% commission rate + assert_eq!(result, (156, 18, 18, 8)); // Expected return amount, spread, commission and referral fee commission } #[test] fn test_compute_swap_full_commission() { - let result = compute_swap(1000, 2000, 100, Decimal::one()); // 100% commission rate should lead to return_amount being 0 - assert_eq!(result, (0, 18, 182)); + let result = compute_swap(1000, 2000, 100, Decimal::one(), 0i64); // 100% commission rate should lead to return_amount being 0 + assert_eq!(result, (0, 18, 182, 0)); } #[test] diff --git a/contracts/pool/src/tests/swap.rs b/contracts/pool/src/tests/swap.rs index 93cbf471d..754608cf7 100644 --- a/contracts/pool/src/tests/swap.rs +++ b/contracts/pool/src/tests/swap.rs @@ -5,7 +5,9 @@ use soroban_sdk::testutils::{AuthorizedFunction, AuthorizedInvocation}; use soroban_sdk::{symbol_short, testutils::Address as _, Address, Env, IntoVal}; use super::setup::{deploy_liquidity_pool_contract, deploy_token_contract}; -use crate::storage::{Asset, PoolResponse, SimulateReverseSwapResponse, SimulateSwapResponse}; +use crate::storage::{ + Asset, PoolResponse, Referral, SimulateReverseSwapResponse, SimulateSwapResponse, +}; use decimal::Decimal; #[test] @@ -49,7 +51,14 @@ fn simple_swap() { // true means "selling A token" // selling just one token with 1% max spread allowed let spread = 100i64; // 1% maximum spread allowed - pool.swap(&user1, &token1.address, &1, &None, &Some(spread)); + pool.swap( + &user1, + &None::, + &token1.address, + &1, + &None, + &Some(spread), + ); assert_eq!( env.auths(), [( @@ -58,7 +67,15 @@ fn simple_swap() { function: AuthorizedFunction::Contract(( pool.address.clone(), symbol_short!("swap"), - (&user1, token1.address.clone(), 1_i128, None::, spread).into_val(&env) + ( + &user1, + None::, + token1.address.clone(), + 1_i128, + None::, + spread + ) + .into_val(&env) )), sub_invocations: std::vec![ (AuthorizedInvocation { @@ -98,7 +115,14 @@ fn simple_swap() { // false means selling B token // this time 100 units - let output_amount = pool.swap(&user1, &token2.address, &1_000, &None, &Some(spread)); + let output_amount = pool.swap( + &user1, + &None::, + &token2.address, + &1_000, + &None, + &Some(spread), + ); let result = pool.query_pool_info(); assert_eq!( result, @@ -167,7 +191,14 @@ fn swap_with_high_fee() { let spread = 1_000; // 10% maximum spread allowed // let's swap 100_000 units of Token 1 in 1:1 pool with 10% protocol fee - pool.swap(&user1, &token1.address, &100_000, &None, &Some(spread)); + pool.swap( + &user1, + &None, + &token1.address, + &100_000, + &None, + &Some(spread), + ); // This is XYK LP with constant product formula // Y_new = (X_in * Y_old) / (X_in + X_old)