diff --git a/Cargo.lock b/Cargo.lock index 11c31d012..c52ddc13b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,7 +40,7 @@ dependencies = [ [[package]] name = "astroport" -version = "2.8.5" +version = "2.8.6" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -308,7 +308,7 @@ dependencies = [ [[package]] name = "astroport-pair-concentrated" -version = "1.2.5" +version = "1.2.6" dependencies = [ "anyhow", "astroport", diff --git a/contracts/pair_concentrated/Cargo.toml b/contracts/pair_concentrated/Cargo.toml index a51753500..87b7676cb 100644 --- a/contracts/pair_concentrated/Cargo.toml +++ b/contracts/pair_concentrated/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-pair-concentrated" -version = "1.2.5" +version = "1.2.6" 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 849545531..9551b36b5 100644 --- a/contracts/pair_concentrated/src/contract.rs +++ b/contracts/pair_concentrated/src/contract.rs @@ -17,7 +17,7 @@ use astroport::asset::{ use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; use astroport::cosmwasm_ext::{AbsDiff, DecimalToInteger, IntegerToDecimal}; use astroport::factory::PairType; -use astroport::pair::{Cw20HookMsg, ExecuteMsg, InstantiateMsg}; +use astroport::pair::{Cw20HookMsg, ExecuteMsg, InstantiateMsg, MIN_TRADE_SIZE}; use astroport::pair_concentrated::{ ConcentratedPoolParams, ConcentratedPoolUpdateParams, MigrateMsg, UpdatePoolParams, }; @@ -527,8 +527,8 @@ pub fn provide_liquidity( let mut slippage = Decimal256::zero(); - // 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() { + // If deposit doesn't diverge too much from the balanced share, we don't update the price + if assets_diff[0] >= MIN_TRADE_SIZE && assets_diff[1] >= MIN_TRADE_SIZE { slippage = assert_slippage_tolerance( &deposits, share, @@ -776,13 +776,19 @@ fn swap( let total_share = query_supply(&deps.querier, &config.pair_info.liquidity_token)? .to_decimal256(LP_TOKEN_PRECISION)?; - let last_price = swap_result.calc_last_prices(offer_asset_dec.amount, offer_ind); - - // update_price() works only with internal representation - xs[1] *= config.pool_state.price_state.price_scale; - config - .pool_state - .update_price(&config.pool_params, &env, total_share, &xs, last_price)?; + // Skip very small trade sizes which could significantly mess up the price due to rounding errors, + // especially if token precisions are 18. + if (swap_result.dy + swap_result.maker_fee) >= MIN_TRADE_SIZE + && offer_asset_dec.amount >= MIN_TRADE_SIZE + { + let last_price = swap_result.calc_last_prices(offer_asset_dec.amount, offer_ind); + + // update_price() works only with internal representation + xs[1] *= config.pool_state.price_state.price_scale; + config + .pool_state + .update_price(&config.pool_params, &env, total_share, &xs, last_price)?; + } let receiver = to.unwrap_or_else(|| sender.clone()); @@ -792,10 +798,11 @@ fn swap( } .into_msg(&receiver)?]; + // Send the maker fee let mut maker_fee = Uint128::zero(); if let Some(fee_address) = fee_info.fee_address { - if !swap_result.maker_fee.is_zero() { - maker_fee = swap_result.maker_fee.to_uint(ask_asset_prec)?; + maker_fee = swap_result.maker_fee.to_uint(ask_asset_prec)?; + if !maker_fee.is_zero() { let fee = Asset { info: pools[ask_ind].info.clone(), amount: maker_fee, @@ -900,7 +907,7 @@ pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result match contract_version.version.as_ref() { "1.0.0" | "1.1.0" | "1.1.1" | "1.1.2" => migrate_config(deps.storage)?, "1.1.4" => migrate_config_from_v140(deps.storage)?, - "1.2.0" | "1.2.1" | "1.2.2" | "1.2.4" => {} + "1.2.0" | "1.2.1" | "1.2.2" | "1.2.4" | "1.2.5" => {} _ => return Err(ContractError::MigrationError {}), }, _ => return Err(ContractError::MigrationError {}), diff --git a/contracts/pair_concentrated/tests/helper.rs b/contracts/pair_concentrated/tests/helper.rs index 6adeff322..0a2e253bb 100644 --- a/contracts/pair_concentrated/tests/helper.rs +++ b/contracts/pair_concentrated/tests/helper.rs @@ -16,8 +16,8 @@ use anyhow::Result as AnyResult; use astroport::asset::{native_asset_info, token_asset_info, Asset, AssetInfo, PairInfo}; use astroport::factory::{PairConfig, PairType}; use astroport::pair::{ - ConfigResponse, CumulativePricesResponse, Cw20HookMsg, ExecuteMsg, ReverseSimulationResponse, - SimulationResponse, + ConfigResponse, CumulativePricesResponse, Cw20HookMsg, ExecuteMsg, PoolResponse, + ReverseSimulationResponse, SimulationResponse, }; use astroport::pair_concentrated::{ ConcentratedPoolParams, ConcentratedPoolUpdateParams, QueryMsg, @@ -304,6 +304,16 @@ impl Helper { sender: &Addr, offer_asset: &Asset, max_spread: Option, + ) -> AnyResult { + self.swap_full_params(sender, offer_asset, max_spread, None) + } + + pub fn swap_full_params( + &mut self, + sender: &Addr, + offer_asset: &Asset, + max_spread: Option, + belief_price: Option, ) -> AnyResult { match &offer_asset.info { AssetInfo::Token { contract_addr } => { @@ -312,7 +322,7 @@ impl Helper { amount: offer_asset.amount, msg: to_binary(&Cw20HookMsg::Swap { ask_asset_info: None, - belief_price: None, + belief_price, max_spread, to: None, }) @@ -333,7 +343,7 @@ impl Helper { let msg = ExecuteMsg::Swap { offer_asset: offer_asset.clone(), ask_asset_info: None, - belief_price: None, + belief_price, max_spread, to: None, }; @@ -455,6 +465,12 @@ impl Helper { from_slice(&binary) } + pub fn query_pool(&self) -> StdResult { + self.app + .wrap() + .query_wasm_smart(&self.pair_addr, &QueryMsg::Pool {}) + } + pub fn query_lp_price(&self) -> StdResult { self.app .wrap() diff --git a/contracts/pair_concentrated/tests/pair_concentrated_integration.rs b/contracts/pair_concentrated/tests/pair_concentrated_integration.rs index c8e2d5706..46b7b2982 100644 --- a/contracts/pair_concentrated/tests/pair_concentrated_integration.rs +++ b/contracts/pair_concentrated/tests/pair_concentrated_integration.rs @@ -1,12 +1,11 @@ -use cosmwasm_std::{Addr, Decimal, Uint128}; - +use cosmwasm_std::{Addr, Decimal, Decimal256, Uint128}; use cw_multi_test::{next_block, Executor}; -use itertools::Itertools; +use itertools::{max, Itertools}; use astroport::asset::{ native_asset_info, Asset, AssetInfo, AssetInfoExt, MINIMUM_LIQUIDITY_AMOUNT, }; - +use astroport::cosmwasm_ext::IntegerToDecimal; use astroport::pair::{ExecuteMsg, PoolResponse}; use astroport::pair_concentrated::{ ConcentratedPoolParams, ConcentratedPoolUpdateParams, PromoteParams, QueryMsg, UpdatePoolParams, @@ -1401,3 +1400,177 @@ fn provide_withdraw_slippage() { .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.5))) .unwrap(); } + +#[test] +fn check_small_trades() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![TestCoin::native("uusd"), TestCoin::native("uluna")]; + + let params = ConcentratedPoolParams { + price_scale: f64_to_dec(4.360000915600192), + 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), + ma_half_time: 600, + track_asset_balances: None, + }; + + let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); + + // Fully balanced but small provide + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(8_000000u128), + helper.assets[&test_coins[1]].with_balance(1_834862u128), + ]; + helper.provide_liquidity(&owner, &assets).unwrap(); + + // Trying to mess the last price with lowest possible swap + for _ in 0..1000 { + helper.app.next_block(30); + let offer_asset = helper.assets[&test_coins[1]].with_balance(1u8); + helper + .swap_full_params(&owner, &offer_asset, None, Some(Decimal::MAX)) + .unwrap(); + } + + // Check that after price scale adjustments (even they are small) internal value is still nearly balanced + let config = helper.query_config().unwrap(); + let pool = helper + .query_pool() + .unwrap() + .assets + .into_iter() + .map(|asset| asset.amount.to_decimal256(6u8).unwrap()) + .collect_vec(); + + let ixs = [pool[0], pool[1] * config.pool_state.price_state.price_scale]; + let relative_diff = ixs[0].abs_diff(ixs[1]) / max(&ixs).unwrap(); + + assert!( + relative_diff < Decimal256::percent(3), + "Internal PCL value is off. Relative_diff: {}", + relative_diff + ); + + // Trying to mess the last price with lowest possible provide + for _ in 0..1000 { + helper.app.next_block(30); + let assets = vec![helper.assets[&test_coins[1]].with_balance(1u8)]; + helper + .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.5))) + .unwrap(); + } + + // Check that after price scale adjustments (even they are small) internal value is still nearly balanced + let config = helper.query_config().unwrap(); + let pool = helper + .query_pool() + .unwrap() + .assets + .into_iter() + .map(|asset| asset.amount.to_decimal256(6u8).unwrap()) + .collect_vec(); + + let ixs = [pool[0], pool[1] * config.pool_state.price_state.price_scale]; + let relative_diff = ixs[0].abs_diff(ixs[1]) / max(&ixs).unwrap(); + + assert!( + relative_diff < Decimal256::percent(3), + "Internal PCL value is off. Relative_diff: {}", + relative_diff + ); +} + +#[test] +fn check_small_trades_18decimals() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![ + TestCoin::cw20precise("ETH", 18), + TestCoin::cw20precise("USD", 18), + ]; + + let params = ConcentratedPoolParams { + price_scale: f64_to_dec(4.360000915600192), + 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), + ma_half_time: 600, + track_asset_balances: None, + }; + + let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); + + // Fully balanced but small provide + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(8e18 as u128), + helper.assets[&test_coins[1]].with_balance(1_834862000000000000u128), + ]; + helper.provide_liquidity(&owner, &assets).unwrap(); + + // Trying to mess the last price with lowest possible swap + for _ in 0..1000 { + helper.app.next_block(30); + let offer_asset = helper.assets[&test_coins[1]].with_balance(1u8); + helper + .swap_full_params(&owner, &offer_asset, None, Some(Decimal::MAX)) + .unwrap(); + } + + // Check that after price scale adjustments (even they are small) internal value is still nearly balanced + let config = helper.query_config().unwrap(); + let pool = helper + .query_pool() + .unwrap() + .assets + .into_iter() + .map(|asset| asset.amount.to_decimal256(6u8).unwrap()) + .collect_vec(); + + let ixs = [pool[0], pool[1] * config.pool_state.price_state.price_scale]; + let relative_diff = ixs[0].abs_diff(ixs[1]) / max(&ixs).unwrap(); + + assert!( + relative_diff < Decimal256::percent(3), + "Internal PCL value is off. Relative_diff: {}", + relative_diff + ); + + // Trying to mess the last price with lowest possible provide + for _ in 0..1000 { + helper.app.next_block(30); + // 0.000001 USD. minimum provide is limited to LP token precision which is 6 decimals. + let assets = vec![helper.assets[&test_coins[1]].with_balance(1000000000000u128)]; + helper + .provide_liquidity_with_slip_tolerance(&owner, &assets, Some(f64_to_dec(0.5))) + .unwrap(); + } + + // Check that after price scale adjustments (even they are small) internal value is still nearly balanced + let config = helper.query_config().unwrap(); + let pool = helper + .query_pool() + .unwrap() + .assets + .into_iter() + .map(|asset| asset.amount.to_decimal256(6u8).unwrap()) + .collect_vec(); + + let ixs = [pool[0], pool[1] * config.pool_state.price_state.price_scale]; + let relative_diff = ixs[0].abs_diff(ixs[1]) / max(&ixs).unwrap(); + + assert!( + relative_diff < Decimal256::percent(3), + "Internal PCL value is off. Relative_diff: {}", + relative_diff + ); +} diff --git a/packages/astroport/Cargo.toml b/packages/astroport/Cargo.toml index 7d0cc9684..4061481e8 100644 --- a/packages/astroport/Cargo.toml +++ b/packages/astroport/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport" -version = "2.8.5" +version = "2.8.6" authors = ["Astroport"] edition = "2021" description = "Common Astroport types, queriers and other utils" diff --git a/packages/astroport/src/pair.rs b/packages/astroport/src/pair.rs index 7abd824fa..26769dd8f 100644 --- a/packages/astroport/src/pair.rs +++ b/packages/astroport/src/pair.rs @@ -1,10 +1,11 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{ + from_slice, Addr, Binary, Decimal, Decimal256, QuerierWrapper, StdResult, Uint128, Uint64, +}; +use cw20::Cw20ReceiveMsg; use crate::asset::{Asset, AssetInfo, PairInfo}; -use cosmwasm_std::{from_slice, Addr, Binary, Decimal, QuerierWrapper, StdResult, Uint128, Uint64}; -use cw20::Cw20ReceiveMsg; - /// The default swap slippage pub const DEFAULT_SLIPPAGE: &str = "0.005"; /// The maximum allowed swap slippage @@ -13,6 +14,10 @@ pub const MAX_ALLOWED_SLIPPAGE: &str = "0.5"; /// Decimal precision for TWAP results pub const TWAP_PRECISION: u8 = 6; +/// Min safe trading size (0.00001) to calculate a price. This value considers +/// amount in decimal form with respective token precision. +pub const MIN_TRADE_SIZE: Decimal256 = Decimal256::raw(10000000000000); + /// This structure describes the parameters used for creating a contract. #[cw_serde] pub struct InstantiateMsg {