Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pool: Split deposit should target the new pool ratio #271

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
115 changes: 59 additions & 56 deletions contracts/pool/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ use crate::{
token_contract,
};
use decimal::Decimal;
use phoenix::{
utils::{is_approx_ratio, LiquidityPoolInitInfo},
validate_bps, validate_int_parameters,
};
use phoenix::{utils::LiquidityPoolInitInfo, validate_bps, validate_int_parameters};

// Metadata that is added on to the WASM custom section
contractmeta!(
Expand Down Expand Up @@ -295,7 +292,7 @@ impl LiquidityPoolTrait for LiquidityPool {
a,
&config.token_a,
);
do_swap(
let actual_b_from_swap = do_swap(
env.clone(),
sender.clone(),
// FIXM: Disable Referral struct
Expand All @@ -307,7 +304,16 @@ impl LiquidityPoolTrait for LiquidityPool {
None,
);
// return: rest of Token A amount, simulated result of swap of portion A
(a - a_for_swap, b_from_swap)
if (actual_b_from_swap - b_from_swap).abs() > 1 {
log!(
&env,
"Pool: ProvideLiquidity: B token off by more than rounding error! actual: {}, predicted: {}",
actual_b_from_swap,
b_from_swap
);
panic_with_error!(env, ContractError::OffByMoreThanRoundingError)
}
(a - a_for_swap, actual_b_from_swap)
}
// Only token B is provided
(None, Some(b)) if b > 0 => {
Expand All @@ -319,7 +325,7 @@ impl LiquidityPoolTrait for LiquidityPool {
b,
&config.token_b,
);
do_swap(
let actual_a_from_swap = do_swap(
env.clone(),
sender.clone(),
// FIXM: Disable Referral struct
Expand All @@ -331,7 +337,16 @@ impl LiquidityPoolTrait for LiquidityPool {
None,
);
// return: simulated result of swap of portion B, rest of Token B amount
(a_from_swap, b - b_for_swap)
if (actual_a_from_swap - a_from_swap).abs() > 1 {
log!(
&env,
"Pool: ProvideLiquidity: A token off by more than rounding error! actual: {}, predicted: {}",
actual_a_from_swap,
a_from_swap
);
panic_with_error!(env, ContractError::OffByMoreThanRoundingError)
}
(actual_a_from_swap, b - b_for_swap)
}
// None or invalid amounts are provided
_ => {
Expand Down Expand Up @@ -845,10 +860,10 @@ fn do_swap(

/// This function divides the deposit in such a way that when swapping it for the other token,
/// the resulting amounts of tokens maintain the current pool's ratio.
/// * `config` - The configuration of the liquidity pool.
/// * `a_pool` - The current amount of Token A in the liquidity pool.
/// * `b_pool` - The current amount of Token B in the liquidity pool.
/// * `deposit` - The total amount of tokens that the user wants to deposit into the liquidity pool.
/// * `sell_a` - A boolean that indicates whether the deposit is in Token A (if true) or in Token B (if false).
/// # Returns
/// * A tuple `(final_offer_amount, final_ask_amount)`, where `final_offer_amount` is the amount of deposit tokens
/// to be swapped, and `final_ask_amount` is the amount of the other tokens that will be received in return.
Expand Down Expand Up @@ -878,57 +893,45 @@ fn split_deposit_based_on_pool_ratio(
);
}

// Calculate the current ratio in the pool
let target_ratio = Decimal::from_ratio(b_pool, a_pool);
// Define boundaries for binary search algorithm
let mut low = 0;
let mut high = deposit;

// Tolerance is the smallest difference in deposit that we care about
let tolerance = 500;
// get pool fee rate
let fee = config.protocol_fee_rate();
// determine which pool is offer and which is ask
let (offer_pool, ask_pool) = if offer_asset == &config.token_a {
(a_pool, b_pool)
} else {
(b_pool, a_pool)
};

let mut final_offer_amount = deposit; // amount of deposit tokens to be swapped
let mut final_ask_amount = 0; // amount of other tokens to be received
// formula to calculate final_offer_amount
let final_offer_amount = {
let numerator = deposit * fee - 2 * offer_pool
+ (deposit * deposit * fee * fee
+ 4 * deposit * offer_pool
+ 4 * offer_pool * offer_pool)
.sqrt();
let denominator = 2 * (fee + Decimal::one());
numerator / denominator
};

while high - low > tolerance {
let mid = (low + high) / 2; // Calculate middle point
// formula to calculate final_ask_amount
// we need to handle the fee here as well
// we don't change ask_pool offer_pool values based on the fee prior to this method
let final_ask_amount = {
let numerator = ask_pool * final_offer_amount;
let denominator = offer_pool + final_offer_amount;
numerator / denominator
};

// Simulate swap to get amount of other tokens to be received for `mid` amount of deposit tokens
let SimulateSwapResponse {
ask_amount,
spread_amount: _,
commission_amount: _,
total_return: _,
} = LiquidityPool::simulate_swap(env.clone(), offer_asset.clone(), mid);

// Update final amounts
final_offer_amount = mid;
final_ask_amount = ask_amount;

// Calculate the ratio that would result from swapping `mid` deposit tokens
let ratio = if offer_asset == &config.token_a {
Decimal::from_ratio(ask_amount, deposit - mid)
} else {
Decimal::from_ratio(deposit - mid, ask_amount)
};
log!(
&env,
"log",
a_pool,
b_pool,
deposit,
final_offer_amount,
final_ask_amount
);

// If the resulting ratio is approximately equal (1%) to the target ratio, break the loop
if is_approx_ratio(ratio, target_ratio, Decimal::percent(1)) {
break;
}
// Update boundaries for the next iteration of the binary search
if ratio > target_ratio {
if offer_asset == &config.token_a {
high = mid;
} else {
low = mid;
}
} else if offer_asset == &config.token_a {
low = mid;
} else {
high = mid;
};
}
(final_offer_amount, final_ask_amount)
}

Expand Down
2 changes: 1 addition & 1 deletion contracts/pool/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ pub enum ContractError {
TokenABiggerThanTokenB = 18,
InvalidBps = 19,
SlippageInvalid = 20,

SwapMinReceivedBiggerThanReturn = 21,
OffByMoreThanRoundingError = 22,
}
3 changes: 2 additions & 1 deletion contracts/pool/src/tests/liquidity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ fn provide_liqudity_single_asset_equal_with_fees() {
let stake_manager = Address::generate(&env);
let stake_owner = Address::generate(&env);

let swap_fees = 1_000i64; // 10% bps
let swap_fees = 1; // 10% bps
let pool = deploy_liquidity_pool_contract(
&env,
None,
Expand Down Expand Up @@ -562,6 +562,7 @@ fn provide_liqudity_single_asset_one_third_with_fees() {
token2.mint(&user1, &100_000);
// providing liquidity with a single asset - token2
pool.provide_liquidity(&user1, &None, &None, &Some(100_000), &None, &None);
soroban_sdk::testutils::arbitrary::std::dbg!("after2");
// before swap : A(10_000_000), B(30_000_000)
// since pool is 1/3 algorithm will split it around 15794/52734
// swap 47_226k B for A = 17_548 (-10% fee = 15_793)
Expand Down
71 changes: 70 additions & 1 deletion contracts/pool/src/tests/swap.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
extern crate std;
use phoenix::assert_approx_eq;
use pretty_assertions::assert_eq;
use soroban_sdk::{
symbol_short,
Expand All @@ -8,7 +9,10 @@ use soroban_sdk::{
use test_case::test_case;

use super::setup::{deploy_liquidity_pool_contract, deploy_token_contract};
use crate::storage::{Asset, PoolResponse, SimulateReverseSwapResponse, SimulateSwapResponse};
use crate::{
storage::{Asset, PoolResponse, SimulateReverseSwapResponse, SimulateSwapResponse},
token_contract,
};
use decimal::Decimal;

#[test]
Expand Down Expand Up @@ -1032,3 +1036,68 @@ fn test_should_fail_when_invalid_ask_asset_min_amount() {
let spread = 100i64; // 1% maximum spread allowed
pool.swap(&user, &token1.address, &1, &Some(10), &Some(spread));
}

#[test_case(0; "when fee is 0 percent")]
#[test_case(1_000; "when fee is 10 percent")]
fn provide_liqudity_single_asset_poc_split_good_target(swap_fees: i64) {
let env = Env::default();
env.mock_all_auths();
env.budget().reset_unlimited();

let mut admin1 = Address::generate(&env);
let mut admin2 = Address::generate(&env);

let mut token1 = deploy_token_contract(&env, &admin1);
let mut token2 = deploy_token_contract(&env, &admin2);
if token2.address < token1.address {
std::mem::swap(&mut token1, &mut token2);
std::mem::swap(&mut admin1, &mut admin2);
}
let user1 = Address::generate(&env);
let user2 = Address::generate(&env);

let pool = deploy_liquidity_pool_contract(
&env,
None,
(&token1.address, &token2.address),
swap_fees,
None,
None,
None,
Address::generate(&env),
Address::generate(&env),
);

token1.mint(&user1, &10_000_000);
token2.mint(&user1, &10_000_000);

pool.provide_liquidity(
&user1,
&Some(10_000_000),
&Some(10_000_000),
&Some(10_000_000),
&Some(10_000_000),
&None,
);
assert_eq!(token1.balance(&pool.address), 10_000_000);
assert_eq!(token2.balance(&pool.address), 10_000_000);

token1.mint(&user2, &500_000);

let user2_token_a_balance_before = token1.balance(&user2);

pool.provide_liquidity(&user2, &Some(500_000), &None, &None, &None, &None);

let share_token = pool.query_share_token_address();

let user2_lp_balance = token_contract::Client::new(&env, &share_token).balance(&user2);

// @audit User 2 withdraw the liquidity by burning all its lp token balance.
let (_, amount_b) = pool.withdraw_liquidity(&user2, &user2_lp_balance, &1, &1);

pool.swap(&user2, &token2.address, &amount_b, &None, &None);

let user2_token_a_balance_after = token1.balance(&user2);

assert_approx_eq!(user2_token_a_balance_before, user2_token_a_balance_after, 5);
}
30 changes: 30 additions & 0 deletions packages/phoenix/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,36 @@ pub fn is_approx_ratio(a: Decimal, b: Decimal, tolerance: Decimal) -> bool {
diff <= tolerance
}

#[macro_export]
macro_rules! assert_approx_eq {
($a:expr, $b:expr) => {{
let eps = 1.0e-6;
let (a, b) = (&$a, &$b);
assert!(
(*a - *b).abs() < eps,
"assertion failed: `(left !== right)` \
(left: `{:?}`, right: `{:?}`, expect diff: `{:?}`, real diff: `{:?}`)",
*a,
*b,
eps,
(*a - *b).abs()
);
}};
($a:expr, $b:expr, $eps:expr) => {{
let (a, b) = (&$a, &$b);
let eps = $eps;
assert!(
(*a - *b).abs() < eps,
"assertion failed: `(left !== right)` \
(left: `{:?}`, right: `{:?}`, expect diff: `{:?}`, real diff: `{:?}`)",
*a,
*b,
eps,
(*a - *b).abs()
);
}};
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TokenInitInfo {
Expand Down
Loading