From b8358edea696bce77de0a30821cc5fb92417f9d9 Mon Sep 17 00:00:00 2001 From: Kerber0x <94062656+kerber0x@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:45:17 +0000 Subject: [PATCH] chore: add LP mint calculation for stableswap pools --- Cargo.lock | 2 +- .../pool-network/terraswap_pair/Cargo.toml | 2 +- .../terraswap_pair/src/commands.rs | 159 ++++++++++++------ .../terraswap_pair/src/helpers.rs | 121 ++++++++++++- .../pool-network/terraswap_pair/src/lib.rs | 3 +- .../deployment/deploy_env/mainnets/terra.env | 2 +- 6 files changed, 233 insertions(+), 56 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ad3f93ab7..ffd00a836 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1607,7 +1607,7 @@ dependencies = [ [[package]] name = "terraswap-pair" -version = "1.3.7" +version = "1.3.8" dependencies = [ "anybuf", "cosmwasm-schema", diff --git a/contracts/liquidity_hub/pool-network/terraswap_pair/Cargo.toml b/contracts/liquidity_hub/pool-network/terraswap_pair/Cargo.toml index c44756d82..956189f5a 100644 --- a/contracts/liquidity_hub/pool-network/terraswap_pair/Cargo.toml +++ b/contracts/liquidity_hub/pool-network/terraswap_pair/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "terraswap-pair" -version = "1.3.7" +version = "1.3.8" authors = [ "Terraform Labs, PTE.", "DELIGHT LABS", diff --git a/contracts/liquidity_hub/pool-network/terraswap_pair/src/commands.rs b/contracts/liquidity_hub/pool-network/terraswap_pair/src/commands.rs index 9e922544b..bce174df3 100644 --- a/contracts/liquidity_hub/pool-network/terraswap_pair/src/commands.rs +++ b/contracts/liquidity_hub/pool-network/terraswap_pair/src/commands.rs @@ -11,7 +11,8 @@ use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg}; #[cfg(any(feature = "osmosis_token_factory", feature = "injective"))] use white_whale_std::pool_network::asset::is_factory_token; use white_whale_std::pool_network::asset::{ - get_total_share, Asset, AssetInfo, AssetInfoRaw, PairInfoRaw, MINIMUM_LIQUIDITY_AMOUNT, + get_total_share, Asset, AssetInfo, AssetInfoRaw, PairInfoRaw, PairType, + MINIMUM_LIQUIDITY_AMOUNT, }; #[cfg(feature = "injective")] use white_whale_std::pool_network::denom_injective::{Coin, MsgBurn, MsgMint}; @@ -22,7 +23,9 @@ use white_whale_std::pool_network::{swap, U256}; use crate::error::ContractError; use crate::helpers; -use crate::helpers::get_protocol_fee_for_asset; +use crate::helpers::{ + compute_d, compute_lp_mint_amount_for_stableswap_deposit, get_protocol_fee_for_asset, +}; use crate::state::{ store_fee, ALL_TIME_BURNED_FEES, ALL_TIME_COLLECTED_PROTOCOL_FEES, COLLECTED_PROTOCOL_FEES, CONFIG, PAIR_INFO, @@ -194,56 +197,112 @@ pub fn provide_liquidity( }; let total_share = get_total_share(&deps.as_ref(), liquidity_token.clone())?; - let share = if total_share == Uint128::zero() { - // Make sure at least MINIMUM_LIQUIDITY_AMOUNT is deposited to mitigate the risk of the first - // depositor preventing small liquidity providers from joining the pool - let share = Uint128::new( - (U256::from(deposits[0].u128()) - .checked_mul(U256::from(deposits[1].u128())) - .ok_or::(ContractError::LiquidityShareComputation {}))? - .integer_sqrt() - .as_u128(), - ) - .checked_sub(MINIMUM_LIQUIDITY_AMOUNT) - .map_err(|_| ContractError::InvalidInitialLiquidityAmount(MINIMUM_LIQUIDITY_AMOUNT))?; - - messages.append(&mut mint_lp_token_msg( - liquidity_token.clone(), - env.contract.address.to_string(), - env.contract.address.to_string(), - MINIMUM_LIQUIDITY_AMOUNT, - )?); - - // share should be above zero after subtracting the MINIMUM_LIQUIDITY_AMOUNT - if share.is_zero() { - return Err(ContractError::InvalidInitialLiquidityAmount( - MINIMUM_LIQUIDITY_AMOUNT, - )); - } - share - } else { - // min(1, 2) - // 1. sqrt(deposit_0 * exchange_rate_0_to_1 * deposit_0) * (total_share / sqrt(pool_0 * pool_1)) - // == deposit_0 * total_share / pool_0 - // 2. sqrt(deposit_1 * exchange_rate_1_to_0 * deposit_1) * (total_share / sqrt(pool_1 * pool_1)) - // == deposit_1 * total_share / pool_1 - let amount = std::cmp::min( - deposits[0].multiply_ratio(total_share, pools[0].amount), - deposits[1].multiply_ratio(total_share, pools[1].amount), - ); - - // assert slippage tolerance - helpers::assert_slippage_tolerance( - &slippage_tolerance, - &deposits, - &pools, - pair_info.pair_type, - amount, - total_share, - )?; + let share = match &pair_info.pair_type { + PairType::StableSwap { amp } => { + if total_share == Uint128::zero() { + // Make sure at least MINIMUM_LIQUIDITY_AMOUNT is deposited to mitigate the risk of the first + // depositor preventing small liquidity providers from joining the pool + let min_lp_token_amount = MINIMUM_LIQUIDITY_AMOUNT * Uint128::from(2u8); + + let share = Uint128::try_from(compute_d(amp, deposits[0], deposits[1]).unwrap())? + .saturating_sub(min_lp_token_amount); + + messages.append(&mut mint_lp_token_msg( + liquidity_token.clone(), + env.contract.address.to_string(), + env.contract.address.to_string(), + min_lp_token_amount, + )?); + + // share should be above zero after subtracting the min_lp_token_amount + if share.is_zero() { + return Err(ContractError::InvalidInitialLiquidityAmount( + min_lp_token_amount, + )); + } - amount + share + } else { + let amount = compute_lp_mint_amount_for_stableswap_deposit( + amp, + deposits[0], + deposits[1], + pools[0].amount, + pools[1].amount, + total_share, + ) + .unwrap(); + + // assert slippage tolerance + helpers::assert_slippage_tolerance( + &slippage_tolerance, + &deposits, + &pools, + pair_info.pair_type, + amount, + total_share, + )?; + + amount + } + } + PairType::ConstantProduct => { + if total_share == Uint128::zero() { + // Make sure at least MINIMUM_LIQUIDITY_AMOUNT is deposited to mitigate the risk of the first + // depositor preventing small liquidity providers from joining the pool + let share = Uint128::new( + (U256::from(deposits[0].u128()) + .checked_mul(U256::from(deposits[1].u128())) + .ok_or::(ContractError::LiquidityShareComputation {}))? + .integer_sqrt() + .as_u128(), + ) + .checked_sub(MINIMUM_LIQUIDITY_AMOUNT) + .map_err(|_| { + ContractError::InvalidInitialLiquidityAmount(MINIMUM_LIQUIDITY_AMOUNT) + })?; + + messages.append(&mut mint_lp_token_msg( + liquidity_token.clone(), + env.contract.address.to_string(), + env.contract.address.to_string(), + MINIMUM_LIQUIDITY_AMOUNT, + )?); + + // share should be above zero after subtracting the MINIMUM_LIQUIDITY_AMOUNT + if share.is_zero() { + return Err(ContractError::InvalidInitialLiquidityAmount( + MINIMUM_LIQUIDITY_AMOUNT, + )); + } + + share + } else { + // min(1, 2) + // 1. sqrt(deposit_0 * exchange_rate_0_to_1 * deposit_0) * (total_share / sqrt(pool_0 * pool_1)) + // == deposit_0 * total_share / pool_0 + // 2. sqrt(deposit_1 * exchange_rate_1_to_0 * deposit_1) * (total_share / sqrt(pool_1 * pool_1)) + // == deposit_1 * total_share / pool_1 + //todo fix the index stuff here + let amount = std::cmp::min( + deposits[0].multiply_ratio(total_share, pools[0].amount), + deposits[1].multiply_ratio(total_share, pools[1].amount), + ); + + // assert slippage tolerance + helpers::assert_slippage_tolerance( + &slippage_tolerance, + &deposits, + &pools, + pair_info.pair_type, + amount, + total_share, + )?; + + amount + } + } }; // mint LP token to sender diff --git a/contracts/liquidity_hub/pool-network/terraswap_pair/src/helpers.rs b/contracts/liquidity_hub/pool-network/terraswap_pair/src/helpers.rs index 19474b7ac..1c3278b9b 100644 --- a/contracts/liquidity_hub/pool-network/terraswap_pair/src/helpers.rs +++ b/contracts/liquidity_hub/pool-network/terraswap_pair/src/helpers.rs @@ -5,7 +5,7 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::CosmosMsg; use cosmwasm_std::{ to_json_binary, Decimal, Decimal256, DepsMut, Env, ReplyOn, Response, StdError, StdResult, - Storage, SubMsg, Uint128, Uint256, WasmMsg, + Storage, SubMsg, Uint128, Uint256, Uint512, WasmMsg, }; use cw20::MinterResponse; use cw_storage_plus::Item; @@ -99,6 +99,95 @@ pub enum StableSwapDirection { ReverseSimulate, } +/// Computes the Stable Swap invariant (D). +/// +/// The invariant is defined as follows: +/// +/// ```text +/// A * sum(x_i) * n**n + D = A * D * n**n + D**(n+1) / (n**n * prod(x_i)) +/// ``` +/// +/// # Arguments +/// +/// - `amount_a` - The amount of token A owned by the LP pool. (i.e. token A reserves) +/// - `amount_b` - The amount of token B owned by the LP pool. (i.e. token B reserves) +/// +#[allow(clippy::unwrap_used)] +pub fn compute_d(amp_factor: &u64, amount_a: Uint128, amount_b: Uint128) -> Option { + let sum_x = amount_a.checked_add(amount_b).unwrap(); // sum(x_i), a.k.a S + + // a and b + let n_coins = Uint128::new(2); + + if sum_x == Uint128::zero() { + Some(Uint512::zero()) + } else { + let amount_a_times_coins = amount_a.checked_mul(n_coins).unwrap(); + let amount_b_times_coins = amount_b.checked_mul(n_coins).unwrap(); + + // Newton's method to approximate D + let mut d_prev: Uint512; + let mut d: Uint512 = sum_x.into(); + for _ in 0..256 { + let mut d_prod = d; + d_prod = d_prod + .checked_mul(d) + .unwrap() + .checked_div(amount_a_times_coins.into()) + .unwrap(); + d_prod = d_prod + .checked_mul(d) + .unwrap() + .checked_div(amount_b_times_coins.into()) + .unwrap(); + d_prev = d; + d = compute_next_d(amp_factor, d, d_prod, sum_x, n_coins).unwrap(); + // Equality with the precision of 1 + if d > d_prev { + if d.checked_sub(d_prev).unwrap() <= Uint512::one() { + break; + } + } else if d_prev.checked_sub(d).unwrap() <= Uint512::one() { + break; + } + } + + Some(d) + } +} + +#[allow(clippy::unwrap_used)] +fn compute_next_d( + amp_factor: &u64, + d_init: Uint512, + d_prod: Uint512, + sum_x: Uint128, + n_coins: Uint128, +) -> Option { + let ann = amp_factor.checked_mul(n_coins.u128() as u64)?; + let leverage = Uint512::from(sum_x).checked_mul(ann.into()).unwrap(); + // d = (ann * sum_x + d_prod * n_coins) * d / ((ann - 1) * d + (n_coins + 1) * d_prod) + let numerator = d_init + .checked_mul( + d_prod + .checked_mul(n_coins.into()) + .unwrap() + .checked_add(leverage) + .unwrap(), + ) + .unwrap(); + let denominator = d_init + .checked_mul(ann.checked_sub(1)?.into()) + .unwrap() + .checked_add( + d_prod + .checked_mul((n_coins.checked_add(1u128.into()).unwrap()).into()) + .unwrap(), + ) + .unwrap(); + Some(numerator.checked_div(denominator).unwrap()) +} + /// Calculates the new pool amount given the current pools and swap size. pub fn calculate_stableswap_y( offer_pool: Decimal256, @@ -151,6 +240,36 @@ pub fn calculate_stableswap_y( Err(ContractError::ConvergeError {}) } +/// Computes the amount of pool tokens to mint after a deposit. +#[allow(clippy::unwrap_used, clippy::too_many_arguments)] +pub fn compute_lp_mint_amount_for_stableswap_deposit( + amp_factor: &u64, + deposit_amount_a: Uint128, + deposit_amount_b: Uint128, + swap_amount_a: Uint128, + swap_amount_b: Uint128, + pool_token_supply: Uint128, +) -> Option { + // Initial invariant + let d_0 = compute_d(amp_factor, swap_amount_a, swap_amount_b)?; + let new_balances = [ + swap_amount_a.checked_add(deposit_amount_a).unwrap(), + swap_amount_b.checked_add(deposit_amount_b).unwrap(), + ]; + // Invariant after change + let d_1 = compute_d(amp_factor, new_balances[0], new_balances[1])?; + if d_1 <= d_0 { + None + } else { + let amount = Uint512::from(pool_token_supply) + .checked_mul(d_1.checked_sub(d_0).unwrap()) + .unwrap() + .checked_div(d_0) + .unwrap(); + Some(Uint128::try_from(amount).unwrap()) + } +} + pub fn compute_swap( offer_pool: Uint128, ask_pool: Uint128, diff --git a/contracts/liquidity_hub/pool-network/terraswap_pair/src/lib.rs b/contracts/liquidity_hub/pool-network/terraswap_pair/src/lib.rs index 8257b9ed0..14075237a 100644 --- a/contracts/liquidity_hub/pool-network/terraswap_pair/src/lib.rs +++ b/contracts/liquidity_hub/pool-network/terraswap_pair/src/lib.rs @@ -7,10 +7,9 @@ pub mod state; mod error; mod helpers; mod math; +mod migrations; mod queries; mod response; - -mod migrations; #[cfg(test)] #[cfg(not(target_arch = "wasm32"))] pub mod tests; diff --git a/scripts/deployment/deploy_env/mainnets/terra.env b/scripts/deployment/deploy_env/mainnets/terra.env index 4cad7b0af..10cea715a 100644 --- a/scripts/deployment/deploy_env/mainnets/terra.env +++ b/scripts/deployment/deploy_env/mainnets/terra.env @@ -1,4 +1,4 @@ export CHAIN_ID="phoenix-1" export DENOM="uluna" export BINARY="terrad" -export RPC="https://ww-terra-rpc.polkachu.com:443" +export RPC="https://rpc.lavenderfive.com:443/terra2"