diff --git a/Cargo.lock b/Cargo.lock index 00566bf98..4be11dafe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,7 +50,7 @@ dependencies = [ [[package]] name = "astroport" -version = "3.3.0" +version = "3.3.1" dependencies = [ "astroport-circular-buffer", "cosmwasm-schema", @@ -422,7 +422,7 @@ dependencies = [ [[package]] name = "astroport-pair-concentrated" -version = "2.0.4" +version = "2.0.5" dependencies = [ "anyhow", "astroport", @@ -445,7 +445,7 @@ dependencies = [ [[package]] name = "astroport-pair-concentrated-injective" -version = "2.0.4" +version = "2.0.5" dependencies = [ "anyhow", "astroport", diff --git a/contracts/pair_concentrated/Cargo.toml b/contracts/pair_concentrated/Cargo.toml index 4746f3fdd..1d4c0991d 100644 --- a/contracts/pair_concentrated/Cargo.toml +++ b/contracts/pair_concentrated/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-pair-concentrated" -version = "2.0.4" +version = "2.0.5" authors = ["Astroport"] edition = "2021" description = "The Astroport concentrated liquidity pair" diff --git a/contracts/pair_concentrated/src/contract.rs b/contracts/pair_concentrated/src/contract.rs index 484c455d6..b4b986d6b 100644 --- a/contracts/pair_concentrated/src/contract.rs +++ b/contracts/pair_concentrated/src/contract.rs @@ -38,9 +38,8 @@ use crate::state::{ }; use crate::utils::{ accumulate_swap_sizes, assert_max_spread, assert_slippage_tolerance, before_swap_check, - calc_last_prices, calc_provide_fee, check_asset_infos, check_assets, check_cw20_in_pool, - check_pair_registered, compute_swap, get_share_in_assets, mint_liquidity_token_message, - query_pools, + calc_provide_fee, check_asset_infos, check_assets, check_cw20_in_pool, check_pair_registered, + compute_swap, get_share_in_assets, mint_liquidity_token_message, query_pools, }; /// Contract name that is used for migration. @@ -398,7 +397,7 @@ pub fn provide_liquidity( .find_position(|pool| pool.equal(&assets[0].info)) .ok_or_else(|| ContractError::InvalidAsset(assets[0].info.to_string()))?; assets.push(Asset { - info: config.pair_info.asset_infos[1 - given_ind].clone(), + info: config.pair_info.asset_infos[1 ^ given_ind].clone(), amount: Uint128::zero(), }); } @@ -471,7 +470,6 @@ pub fn provide_liquidity( let amp_gamma = config.pool_state.get_amp_gamma(&env); let new_d = calc_d(&new_xp, &_gamma)?; - let mut old_price = config.pool_state.price_state.last_price; let share = if total_share.is_zero() { let xcp = get_xcp(new_d, config.pool_state.price_state.price_scale); @@ -499,7 +497,6 @@ pub fn provide_liquidity( mint_amount } else { let mut old_xp = pools.iter().map(|a| a.amount).collect_vec(); - old_price = calc_last_prices(&old_xp, &config, &env)?; old_xp[1] *= config.pool_state.price_state.price_scale; let old_d = calc_d(&old_xp, &_gamma)?; let share = (total_share * new_d / old_d).saturating_sub(total_share); @@ -521,18 +518,18 @@ pub fn provide_liquidity( deposits[1].diff(balanced_share[1]), ]; - let tmp_xp = vec![ - new_xp[0], - new_xp[1] / config.pool_state.price_state.price_scale, - ]; - let new_price = calc_last_prices(&tmp_xp, &config, &env)?; + let mut slippage = Decimal256::zero(); - // if assets_diff[1] is zero then deposits are balanced thus no need to update price + // if assets_diff[1] is zero then deposits are balanced thus no need to update price and check slippage if !assets_diff[1].is_zero() { - let last_price = assets_diff[0] / assets_diff[1]; - - assert_slippage_tolerance(old_price, new_price, slippage_tolerance)?; + slippage = assert_slippage_tolerance( + &deposits, + share, + &config.pool_state.price_state, + slippage_tolerance, + )?; + let last_price = assets_diff[0] / assets_diff[1]; config.pool_state.update_price( &config.pool_params, &env, @@ -578,6 +575,7 @@ pub fn provide_liquidity( attr("receiver", receiver), attr("assets", format!("{}, {}", &assets[0], &assets[1])), attr("share", share_uint128), + attr("slippage", slippage.to_string()), ]; Ok(Response::new().add_messages(messages).add_attributes(attrs)) @@ -894,7 +892,7 @@ pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { BufferManager::init(deps.storage, OBSERVATIONS, OBSERVATIONS_SIZE)?; } - "2.0.3" => {} + "2.0.3" | "2.0.4" => {} _ => return Err(ContractError::MigrationError {}), }, _ => return Err(ContractError::MigrationError {}), diff --git a/contracts/pair_concentrated/src/utils.rs b/contracts/pair_concentrated/src/utils.rs index f69d6bf53..4074a5b23 100644 --- a/contracts/pair_concentrated/src/utils.rs +++ b/contracts/pair_concentrated/src/utils.rs @@ -13,10 +13,10 @@ use astroport_circular_buffer::error::BufferResult; use astroport_circular_buffer::BufferManager; use astroport_factory::state::pair_key; -use crate::consts::{DEFAULT_SLIPPAGE, MAX_ALLOWED_SLIPPAGE, N, OFFER_PERCENT}; +use crate::consts::{DEFAULT_SLIPPAGE, MAX_ALLOWED_SLIPPAGE, N, OFFER_PERCENT, TWO}; use crate::error::ContractError; use crate::math::{calc_d, calc_y}; -use crate::state::{Config, PoolParams, Precisions, OBSERVATIONS}; +use crate::state::{Config, PoolParams, Precisions, PriceState, OBSERVATIONS}; /// Helper function to check the given asset infos are valid. pub(crate) fn check_asset_infos( @@ -373,12 +373,13 @@ pub fn calc_provide_fee( deposits[0].diff(avg) * params.fee(xp) / sum } -/// This is an internal function that enforces slippage tolerance for swaps. +/// This is an internal function that enforces slippage tolerance for provides. Returns actual slippage. pub fn assert_slippage_tolerance( - old_price: Decimal256, - new_price: Decimal256, + deposits: &[Decimal256], + actual_share: Decimal256, + price_state: &PriceState, slippage_tolerance: Option, -) -> Result<(), ContractError> { +) -> Result { let slippage_tolerance = slippage_tolerance .map(Into::into) .unwrap_or(DEFAULT_SLIPPAGE); @@ -386,12 +387,17 @@ pub fn assert_slippage_tolerance( return Err(ContractError::AllowedSpreadAssertion {}); } - // Ensure price was not changed more than the slippage tolerance allows - if Decimal256::one().diff(new_price / old_price) > slippage_tolerance { + let deposit_value = deposits[0] + deposits[1] * price_state.price_scale; + let lp_expected = (deposit_value / TWO * deposit_value / (TWO * price_state.price_scale)) + .sqrt() + / price_state.xcp_profit_real; + let slippage = lp_expected.saturating_sub(actual_share) / lp_expected; + + if slippage > slippage_tolerance { return Err(ContractError::MaxSpreadAssertion {}); } - Ok(()) + Ok(slippage) } // Checks whether the pair is registered in the factory or not. @@ -481,12 +487,14 @@ pub fn accumulate_swap_sizes( #[cfg(test)] mod tests { - use super::*; - use cosmwasm_std::testing::{mock_env, MockStorage}; use std::error::Error; use std::fmt::Display; use std::str::FromStr; + use cosmwasm_std::testing::{mock_env, MockStorage}; + + use super::*; + pub fn f64_to_dec(val: f64) -> T where T: FromStr, diff --git a/contracts/pair_concentrated/tests/pair_concentrated_integration.rs b/contracts/pair_concentrated/tests/pair_concentrated_integration.rs index c05dbb8a8..e331911f4 100644 --- a/contracts/pair_concentrated/tests/pair_concentrated_integration.rs +++ b/contracts/pair_concentrated/tests/pair_concentrated_integration.rs @@ -1475,3 +1475,67 @@ fn provide_withdraw_provide() { .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.5))) .unwrap(); } + +#[test] +fn provide_withdraw_slippage() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![TestCoin::native("uusd"), TestCoin::native("uluna")]; + + let params = ConcentratedPoolParams { + amp: f64_to_dec(10f64), + gamma: f64_to_dec(0.000145), + mid_fee: f64_to_dec(0.0026), + out_fee: f64_to_dec(0.0045), + fee_gamma: f64_to_dec(0.00023), + repeg_profit_threshold: f64_to_dec(0.000002), + min_price_scale_delta: f64_to_dec(0.000146), + price_scale: Decimal::from_ratio(10u8, 1u8), + ma_half_time: 600, + track_asset_balances: None, + }; + + let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); + + // Fully balanced provide + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(10_000000u128), + helper.assets[&test_coins[1]].with_balance(1_000000u128), + ]; + helper + .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.02))) + .unwrap(); + + // Imbalanced provide. Slippage is more than 2% while we enforce 2% max slippage + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(5_000000u128), + helper.assets[&test_coins[1]].with_balance(1_000000u128), + ]; + let err = helper + .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.02))) + .unwrap_err(); + assert_eq!( + ContractError::MaxSpreadAssertion {}, + err.downcast().unwrap(), + ); + // With 3% slippage it should work + helper + .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.03))) + .unwrap(); + + // Provide with a huge imbalance. Slippage is ~42.2% + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(1000_000000u128), + helper.assets[&test_coins[1]].with_balance(1000_000000u128), + ]; + let err = helper + .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.02))) + .unwrap_err(); + assert_eq!( + ContractError::MaxSpreadAssertion {}, + err.downcast().unwrap(), + ); + helper + .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.5))) + .unwrap(); +} diff --git a/contracts/pair_concentrated_inj/Cargo.toml b/contracts/pair_concentrated_inj/Cargo.toml index a785eea4c..226f4bbdb 100644 --- a/contracts/pair_concentrated_inj/Cargo.toml +++ b/contracts/pair_concentrated_inj/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-pair-concentrated-injective" -version = "2.0.4" +version = "2.0.5" authors = ["Astroport"] edition = "2021" description = "The Astroport concentrated liquidity pair which supports Injective orderbook integration" diff --git a/contracts/pair_concentrated_inj/src/contract.rs b/contracts/pair_concentrated_inj/src/contract.rs index 4ff7f17be..eec92932f 100644 --- a/contracts/pair_concentrated_inj/src/contract.rs +++ b/contracts/pair_concentrated_inj/src/contract.rs @@ -41,9 +41,8 @@ use crate::state::{ }; use crate::utils::{ accumulate_swap_sizes, assert_max_spread, assert_slippage_tolerance, before_swap_check, - calc_last_prices, calc_provide_fee, check_asset_infos, check_assets, check_pair_registered, - compute_swap, get_share_in_assets, mint_liquidity_token_message, query_contract_balances, - query_pools, + calc_provide_fee, check_asset_infos, check_assets, check_pair_registered, compute_swap, + get_share_in_assets, mint_liquidity_token_message, query_contract_balances, query_pools, }; /// Contract name that is used for migration. @@ -479,7 +478,6 @@ where let amp_gamma = config.pool_state.get_amp_gamma(&env); let new_d = calc_d(&new_xp, &_gamma)?; - let mut old_price = config.pool_state.price_state.last_price; let share = if total_share.is_zero() { let xcp = get_xcp(new_d, config.pool_state.price_state.price_scale); @@ -507,7 +505,6 @@ where mint_amount } else { let mut old_xp = xs.clone(); - old_price = calc_last_prices(&old_xp, &config, &env)?; old_xp[1] *= config.pool_state.price_state.price_scale; let old_d = calc_d(&old_xp, &_gamma)?; let share = (total_share * new_d / old_d).saturating_sub(total_share); @@ -529,18 +526,18 @@ where deposits[1].diff(balanced_share[1]), ]; + let mut slippage = Decimal256::zero(); + // if assets_diff[1] is zero then deposits are balanced thus no need to update price if !assets_diff[1].is_zero() { - let last_price = assets_diff[0] / assets_diff[1]; - - let tmp_xp = vec![ - new_xp[0], - new_xp[1] / config.pool_state.price_state.price_scale, - ]; - let new_price = calc_last_prices(&tmp_xp, &config, &env)?; - - assert_slippage_tolerance(old_price, new_price, slippage_tolerance)?; + slippage = assert_slippage_tolerance( + &deposits, + share, + &config.pool_state.price_state, + slippage_tolerance, + )?; + let last_price = assets_diff[0] / assets_diff[1]; config.pool_state.update_price( &config.pool_params, &env, @@ -574,6 +571,7 @@ where attr("receiver", receiver), attr("assets", format!("{}, {}", &assets[0], &assets[1])), attr("share", share_uint128), + attr("slippage", slippage.to_string()), ]; Ok(Response::new().add_messages(messages).add_attributes(attrs)) diff --git a/contracts/pair_concentrated_inj/src/migrate.rs b/contracts/pair_concentrated_inj/src/migrate.rs index 7e3b8695d..2d2710b16 100644 --- a/contracts/pair_concentrated_inj/src/migrate.rs +++ b/contracts/pair_concentrated_inj/src/migrate.rs @@ -13,7 +13,7 @@ use astroport_pair_concentrated::state::Config as CLConfig; use crate::state::{AmpGamma, Config, PoolParams, PoolState, PriceState, CONFIG}; const MIGRATE_FROM: &str = "astroport-pair-concentrated"; -const MIGRATION_VERSION: &str = "2.0.4"; +const MIGRATION_VERSION: &str = "2.0.5"; /// Manages the contract migration. #[cfg_attr(not(feature = "library"), entry_point)] @@ -57,7 +57,7 @@ pub fn migrate( let contract_info = cw2::get_contract_version(deps.storage)?; match contract_info.contract.as_str() { CONTRACT_NAME => match contract_info.version.as_str() { - "2.0.3" => {} + "2.0.3" | "2.0.4" => {} _ => { return Err(StdError::generic_err(format!( "Can't migrate from {} {}", diff --git a/contracts/pair_concentrated_inj/src/utils.rs b/contracts/pair_concentrated_inj/src/utils.rs index b153401e8..606304117 100644 --- a/contracts/pair_concentrated_inj/src/utils.rs +++ b/contracts/pair_concentrated_inj/src/utils.rs @@ -14,12 +14,12 @@ use astroport_circular_buffer::error::BufferResult; use astroport_circular_buffer::BufferManager; use astroport_factory::state::pair_key; -use crate::consts::{DEFAULT_SLIPPAGE, MAX_ALLOWED_SLIPPAGE, N, OFFER_PERCENT}; +use crate::consts::{DEFAULT_SLIPPAGE, MAX_ALLOWED_SLIPPAGE, N, OFFER_PERCENT, TWO}; use crate::error::ContractError; use crate::math::{calc_d, calc_y}; use crate::orderbook::state::OrderbookState; use crate::orderbook::utils::get_subaccount_balances_dec; -use crate::state::{Config, PoolParams, Precisions, OBSERVATIONS}; +use crate::state::{Config, PoolParams, Precisions, PriceState, OBSERVATIONS}; /// Helper function to check the given asset infos are valid. pub(crate) fn check_asset_infos(asset_infos: &[AssetInfo]) -> Result<(), ContractError> { @@ -475,12 +475,13 @@ pub fn calc_provide_fee( deviation * params.fee(xp) / (sum * N) } -/// This is an internal function that enforces slippage tolerance for swaps. +/// This is an internal function that enforces slippage tolerance for provides. Returns actual slippage. pub fn assert_slippage_tolerance( - old_price: Decimal256, - new_price: Decimal256, + deposits: &[Decimal256], + actual_share: Decimal256, + price_state: &PriceState, slippage_tolerance: Option, -) -> Result<(), ContractError> { +) -> Result { let slippage_tolerance = slippage_tolerance .map(Into::into) .unwrap_or(DEFAULT_SLIPPAGE); @@ -488,12 +489,17 @@ pub fn assert_slippage_tolerance( return Err(ContractError::AllowedSpreadAssertion {}); } - // Ensure price was not changed more than the slippage tolerance allows - if Decimal256::one().diff(new_price / old_price) > slippage_tolerance { + let deposit_value = deposits[0] + deposits[1] * price_state.price_scale; + let lp_expected = (deposit_value / TWO * deposit_value / (TWO * price_state.price_scale)) + .sqrt() + / price_state.xcp_profit_real; + let slippage = lp_expected.saturating_sub(actual_share) / lp_expected; + + if slippage > slippage_tolerance { return Err(ContractError::MaxSpreadAssertion {}); } - Ok(()) + Ok(slippage) } /// Checks whether the pair is registered in the factory or not. diff --git a/contracts/pair_concentrated_inj/tests/pair_inj_concentrated_integration.rs b/contracts/pair_concentrated_inj/tests/pair_inj_concentrated_integration.rs index 740fa9b70..62d32b472 100644 --- a/contracts/pair_concentrated_inj/tests/pair_inj_concentrated_integration.rs +++ b/contracts/pair_concentrated_inj/tests/pair_inj_concentrated_integration.rs @@ -2028,3 +2028,67 @@ fn provide_withdraw_provide() { .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.5))) .unwrap(); } + +#[test] +fn provide_withdraw_slippage() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![TestCoin::native("uusd"), TestCoin::native("uluna")]; + + let params = ConcentratedPoolParams { + amp: f64_to_dec(10f64), + gamma: f64_to_dec(0.000145), + mid_fee: f64_to_dec(0.0026), + out_fee: f64_to_dec(0.0045), + fee_gamma: f64_to_dec(0.00023), + repeg_profit_threshold: f64_to_dec(0.000002), + min_price_scale_delta: f64_to_dec(0.000146), + price_scale: Decimal::from_ratio(10u8, 1u8), + ma_half_time: 600, + track_asset_balances: None, + }; + + let mut helper = Helper::new(&owner, test_coins.clone(), params, true).unwrap(); + + // Fully balanced provide + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(10_000000u128), + helper.assets[&test_coins[1]].with_balance(1_000000u128), + ]; + helper + .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.02))) + .unwrap(); + + // Imbalanced provide. Slippage is more than 2% while we enforce 2% max slippage + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(5_000000u128), + helper.assets[&test_coins[1]].with_balance(1_000000u128), + ]; + let err = helper + .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.02))) + .unwrap_err(); + assert_eq!( + ContractError::MaxSpreadAssertion {}, + err.downcast().unwrap() + ); + // With 3% slippage it should work + helper + .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.03))) + .unwrap(); + + // Provide with a huge imbalance. Slippage is ~42.2% + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(1000_000000u128), + helper.assets[&test_coins[1]].with_balance(1000_000000u128), + ]; + let err = helper + .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.02))) + .unwrap_err(); + assert_eq!( + ContractError::MaxSpreadAssertion {}, + err.downcast().unwrap(), + ); + helper + .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.5))) + .unwrap(); +} diff --git a/packages/astroport/Cargo.toml b/packages/astroport/Cargo.toml index 691e2c458..ea94cb272 100644 --- a/packages/astroport/Cargo.toml +++ b/packages/astroport/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport" -version = "3.3.0" +version = "3.3.1" authors = ["Astroport"] edition = "2021" description = "Common Astroport types, queriers and other utils"