From e81b62b772d92225d16da6fe8aadd48b915461a8 Mon Sep 17 00:00:00 2001
From: Timofey <5527315+epanchee@users.noreply.github.com>
Date: Wed, 27 Sep 2023 20:41:29 +0400
Subject: [PATCH 1/2] fix(PCL): backporting bugfix from
 66d00d735a654afcc6d793f27997d2e93cea5834

---
 Cargo.lock                                    |   2 +-
 contracts/pair_concentrated/src/contract.rs   |  31 +--
 contracts/pair_concentrated/tests/helper.rs   |  24 ++-
 .../tests/pair_concentrated_integration.rs    | 181 +++++++++++++++++-
 packages/astroport/Cargo.toml                 |   2 +-
 packages/astroport/src/pair.rs                |  11 +-
 6 files changed, 226 insertions(+), 25 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 11c31d012..ec13df1c0 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",
diff --git a/contracts/pair_concentrated/src/contract.rs b/contracts/pair_concentrated/src/contract.rs
index 849545531..4dadda0fc 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,
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<Decimal>,
+    ) -> AnyResult<AppResponse> {
+        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<Decimal>,
+        belief_price: Option<Decimal>,
     ) -> AnyResult<AppResponse> {
         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<PoolResponse> {
+        self.app
+            .wrap()
+            .query_wasm_smart(&self.pair_addr, &QueryMsg::Pool {})
+    }
+
     pub fn query_lp_price(&self) -> StdResult<Decimal256> {
         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 {

From 75a9ee76aca3904c0867ec41d7331e25be84705f Mon Sep 17 00:00:00 2001
From: Timofey <5527315+epanchee@users.noreply.github.com>
Date: Thu, 28 Sep 2023 12:54:10 +0400
Subject: [PATCH 2/2] bump PCL pair version

---
 Cargo.lock                                  | 2 +-
 contracts/pair_concentrated/Cargo.toml      | 2 +-
 contracts/pair_concentrated/src/contract.rs | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index ec13df1c0..c52ddc13b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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 4dadda0fc..9551b36b5 100644
--- a/contracts/pair_concentrated/src/contract.rs
+++ b/contracts/pair_concentrated/src/contract.rs
@@ -907,7 +907,7 @@ pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response, C
         "astroport-pair-concentrated" => 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 {}),