diff --git a/Cargo.lock b/Cargo.lock index 608d9de87..fe81c36cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -968,9 +968,12 @@ dependencies = [ "cw20-base", "epoch-manager", "incentive-manager", + "proptest", + "rand", "semver", "serde", "sha2 0.10.8", + "stable-swap-sim", "test-case", "thiserror", "whale-lair", @@ -1423,6 +1426,13 @@ dependencies = [ "pyo3", ] +[[package]] +name = "stable-swap-sim1" +version = "0.1.0" +dependencies = [ + "pyo3", +] + [[package]] name = "stableswap-3pool" version = "1.2.3" @@ -1440,7 +1450,7 @@ dependencies = [ "schemars", "semver", "serde", - "stable-swap-sim", + "stable-swap-sim1", "thiserror", "white-whale-std", ] diff --git a/contracts/liquidity_hub/incentive-manager/Cargo.toml b/contracts/liquidity_hub/incentive-manager/Cargo.toml index 4b8b16409..9acc3d4b8 100644 --- a/contracts/liquidity_hub/incentive-manager/Cargo.toml +++ b/contracts/liquidity_hub/incentive-manager/Cargo.toml @@ -44,4 +44,4 @@ epoch-manager.workspace = true whale-lair.workspace = true anyhow.workspace = true bonding-manager.workspace = true -pool-manager.workspace = true \ No newline at end of file +pool-manager.workspace = true diff --git a/contracts/liquidity_hub/pool-manager/Cargo.toml b/contracts/liquidity_hub/pool-manager/Cargo.toml index bce69c63b..038dc68a3 100644 --- a/contracts/liquidity_hub/pool-manager/Cargo.toml +++ b/contracts/liquidity_hub/pool-manager/Cargo.toml @@ -58,3 +58,6 @@ incentive-manager.workspace = true epoch-manager.workspace = true white-whale-testing.workspace = true bonding-manager.workspace = true +proptest = "1.0.0" +rand = "0.8.4" +stable-swap-sim = { path = "./sim", version = "^0.1" } diff --git a/contracts/liquidity_hub/pool-manager/proptest-regressions/helpers.txt b/contracts/liquidity_hub/pool-manager/proptest-regressions/helpers.txt new file mode 100644 index 000000000..5ef4eb073 --- /dev/null +++ b/contracts/liquidity_hub/pool-manager/proptest-regressions/helpers.txt @@ -0,0 +1,9 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 55857276de2241e3d09d36aba47854e0017db66f6c5a61e306b38ad0d3b8aeeb # shrinks to amp_factor = 1, initial_user_token_a_amount = 10000000, initial_user_token_b_amount = 10000000 +cc 33456e9a9f11bed69ac5171155ce7a64f73f912fcbfede19046989302d1b2da9 # shrinks to amp_factor = 10, deposit_amount_a = 0, deposit_amount_b = 0, deposit_amount_c = 0, swap_token_a_amount = 0, swap_token_b_amount = 0, swap_token_c_amount = 1, pool_token_supply = 0 +cc 75c3b0922c450b034b92dc8c2ea87edff47c90bbede702d84c9fd9c672e2f31f # shrinks to amp_factor = 141, deposit_amount_a = 308442737939502983046195411808336, deposit_amount_b = 0, deposit_amount_c = 0, swap_token_a_amount = 870112623450347049437652954298478, swap_token_b_amount = 501497230776538877048085549853566, swap_token_c_amount = 24063806364666791266594852039507, pool_token_supply = 2 diff --git a/contracts/liquidity_hub/pool-manager/sim/Cargo.toml b/contracts/liquidity_hub/pool-manager/sim/Cargo.toml new file mode 100644 index 000000000..e01fbc983 --- /dev/null +++ b/contracts/liquidity_hub/pool-manager/sim/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "stable-swap-sim" +version = "0.1.0" +description = "Simulations of the StableSwap invariant compared to Curve's reference implementation." +authors = ["Paul Stelzig paul@irulast.com>"] +edition = "2021" + +[lib] +name = "sim" + +[dependencies] +pyo3 = { version = "0.17.3", features = ["auto-initialize"] } diff --git a/contracts/liquidity_hub/pool-manager/sim/simulation.py b/contracts/liquidity_hub/pool-manager/sim/simulation.py new file mode 100644 index 000000000..12606d3bb --- /dev/null +++ b/contracts/liquidity_hub/pool-manager/sim/simulation.py @@ -0,0 +1,172 @@ +# Source from: https://github.com/curvefi/curve-contract/blob/master/tests/simulation.py + +class Curve: + + """ + Python model of Curve pool math. + """ + + def __init__(self, A, D, n, p=None, tokens=None): + """ + A: Amplification coefficient + D: Total deposit size + n: number of currencies + p: target prices + """ + self.A = A # actually A * n ** (n - 1) because it's an invariant + self.n = n + self.fee = 10 ** 7 + if p: + self.p = p + else: + self.p = [10 ** 18] * n + if isinstance(D, list): + self.x = D + else: + self.x = [D // n * 10 ** 18 // _p for _p in self.p] + self.tokens = tokens + + def xp(self): + return [x * p // 10 ** 18 for x, p in zip(self.x, self.p)] + + def D(self): + """ + D invariant calculation in non-overflowing integer operations + iteratively + + A * sum(x_i) * n**n + D = A * D * n**n + D**(n+1) / (n**n * prod(x_i)) + + Converging solution: + D[j+1] = (A * n**n * sum(x_i) - D[j]**(n+1) / (n**n prod(x_i))) / (A * n**n - 1) + """ + Dprev = 0 + xp = self.xp() + S = sum(xp) + D = S + Ann = self.A * self.n + while abs(D - Dprev) > 1: + D_P = D + for x in xp: + D_P = D_P * D // (self.n * x) + Dprev = D + D = (Ann * S + D_P * self.n) * D // ((Ann - 1) * D + (self.n + 1) * D_P) + + return D + + def y(self, i, j, x): + """ + Calculate x[j] if one makes x[i] = x + + Done by solving quadratic equation iteratively. + x_1**2 + x1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) + x_1**2 + b*x_1 = c + + x_1 = (x_1**2 + c) / (2*x_1 + b) + """ + D = self.D() + xx = self.xp() + xx[i] = x # x is quantity of underlying asset brought to 1e18 precision + xx = [xx[k] for k in range(self.n) if k != j] + Ann = self.A * self.n + c = D + for y in xx: + c = c * D // (y * self.n) + c = c * D // (self.n * Ann) + b = sum(xx) + D // Ann - D + y_prev = 0 + y = D + while abs(y - y_prev) > 1: + y_prev = y + y = (y ** 2 + c) // (2 * y + b) + return y # the result is in underlying units too + + def y_D(self, i, _D): + """ + Calculate x[j] if one makes x[i] = x + + Done by solving quadratic equation iteratively. + x_1**2 + x1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) + x_1**2 + b*x_1 = c + + x_1 = (x_1**2 + c) / (2*x_1 + b) + """ + xx = self.xp() + xx = [xx[k] for k in range(self.n) if k != i] + S = sum(xx) + Ann = self.A * self.n + c = _D + for y in xx: + c = c * _D // (y * self.n) + c = c * _D // (self.n * Ann) + b = S + _D // Ann + y_prev = 0 + y = _D + while abs(y - y_prev) > 1: + y_prev = y + y = (y ** 2 + c) // (2 * y + b - _D) + return y # the result is in underlying units too + + def dy(self, i, j, dx): + # dx and dy are in underlying units + xp = self.xp() + return xp[j] - self.y(i, j, xp[i] + dx) + + def exchange(self, i, j, dx): + xp = self.xp() + x = xp[i] + dx + y = self.y(i, j, x) + dy = xp[j] - y + fee = dy * self.fee // 10 ** 10 + assert dy > 0 + self.x[i] = x * 10 ** 18 // self.p[i] + self.x[j] = (y + fee) * 10 ** 18 // self.p[j] + return dy - fee + + def remove_liquidity_imbalance(self, amounts): + _fee = self.fee * self.n // (4 * (self.n - 1)) + + old_balances = self.x + new_balances = self.x[:] + D0 = self.D() + for i in range(self.n): + new_balances[i] -= amounts[i] + self.x = new_balances + D1 = self.D() + self.x = old_balances + fees = [0] * self.n + for i in range(self.n): + ideal_balance = D1 * old_balances[i] // D0 + difference = abs(ideal_balance - new_balances[i]) + fees[i] = _fee * difference // 10 ** 10 + new_balances[i] -= fees[i] + self.x = new_balances + D2 = self.D() + self.x = old_balances + + token_amount = (D0 - D2) * self.tokens // D0 + + return token_amount + + def calc_withdraw_one_coin(self, token_amount, i): + xp = self.xp() + xp_reduced = list(xp) + + D0 = self.D() + D1 = D0 - token_amount * D0 // self.tokens + new_y = self.y_D(i, D1) + + fee = self.fee * self.n // (4 * (self.n - 1)) + for j in range(self.n): + dx_expected = 0 + if j == i: + dx_expected = xp[j] * D1 // D0 - new_y + else: + dx_expected = xp[j] - xp[j] * D1 // D0 + xp_reduced[j] -= fee * dx_expected // 10 ** 10 + + self.x = [x * 10 ** 18 // p for x, p in zip(xp_reduced, self.p)] + dy = xp_reduced[i] - self.y_D(i, D1) - 1 # Withdraw less to account for rounding errors + self.x = [x * 10 ** 18 // p for x, p in zip(xp, self.p)] + dy_0 = xp[i] - new_y + + return dy, dy_0 - dy diff --git a/contracts/liquidity_hub/pool-manager/sim/src/lib.rs b/contracts/liquidity_hub/pool-manager/sim/src/lib.rs new file mode 100644 index 000000000..a9aef2dcd --- /dev/null +++ b/contracts/liquidity_hub/pool-manager/sim/src/lib.rs @@ -0,0 +1,209 @@ +//! Simulations of the StableSwap invariant compared to Curve's reference implementation. +#![allow(deprecated)] +#![allow(deprecated)] + +use pyo3::prelude::*; +use pyo3::types::PyTuple; +use std::fs::File; +use std::io::prelude::*; + +pub const MODEL_FEE_NUMERATOR: u64 = 10000000; +pub const MODEL_FEE_DENOMINATOR: u64 = 10000000000; + +const DEFAULT_POOL_TOKENS: u128 = 0; +const DEFAULT_TARGET_PRICE: u128 = 1000000000000000000; +const FILE_NAME: &str = "simulation.py"; +const FILE_PATH: &str = "sim/simulation.py"; +const MODULE_NAME: &str = "simulation"; + +pub struct Model { + pub py_src: String, + pub amp_factor: u64, + pub balances: Vec, + pub n_coins: u8, + pub target_prices: Vec, + pub pool_tokens: u128, +} + +impl Model { + /// Constructs a new [`Model`]. + pub fn new(amp_factor: u64, balances: Vec, n_coins: u8) -> Model { + let src_file = File::open(FILE_PATH); + let mut src_file = match src_file { + Ok(file) => file, + Err(error) => { + panic!("{error:?}\n Please run `curl -L + https://raw.githubusercontent.com/curvefi/curve-contract/master/tests/simulation.py > sim/simulation.py`") + } + }; + let mut src_content = String::new(); + let _ = src_file.read_to_string(&mut src_content); + + assert_eq!(balances.len(), n_coins.into()); + + Self { + py_src: src_content, + amp_factor, + balances, + n_coins, + target_prices: vec![DEFAULT_TARGET_PRICE; n_coins.into()], + pool_tokens: DEFAULT_POOL_TOKENS, + } + } + + pub fn new_with_pool_tokens( + amp_factor: u64, + balances: Vec, + n_coins: u8, + pool_token_amount: u128, + ) -> Model { + let src_file = File::open(FILE_PATH); + let mut src_file = match src_file { + Ok(file) => file, + Err(error) => { + panic!("{error:?}\n Please run `curl -L + https://raw.githubusercontent.com/curvefi/curve-contract/master/tests/simulation.py > sim/simulation.py`") + } + }; + let mut src_content = String::new(); + let _ = src_file.read_to_string(&mut src_content); + + Self { + py_src: src_content, + amp_factor, + balances, + n_coins, + target_prices: vec![DEFAULT_TARGET_PRICE, DEFAULT_TARGET_PRICE], + pool_tokens: pool_token_amount, + } + } + + pub fn sim_d(&self) -> u128 { + let gil = Python::acquire_gil(); + return self + .call0(gil.python(), "D") + .unwrap() + .extract(gil.python()) + .unwrap(); + } + + pub fn sim_dy(&self, i: u128, j: u128, dx: u128) -> u128 { + let gil = Python::acquire_gil(); + return self + .call1(gil.python(), "dy", (i, j, dx)) + .unwrap() + .extract(gil.python()) + .unwrap(); + } + + pub fn sim_exchange(&self, i: u128, j: u128, dx: u128) -> u128 { + let gil = Python::acquire_gil(); + return self + .call1(gil.python(), "exchange", (i, j, dx)) + .unwrap() + .extract(gil.python()) + .unwrap(); + } + + pub fn sim_xp(&self) -> Vec { + let gil = Python::acquire_gil(); + return self + .call0(gil.python(), "xp") + .unwrap() + .extract(gil.python()) + .unwrap(); + } + + pub fn sim_y(&self, i: u128, j: u128, x: u128) -> u128 { + let gil = Python::acquire_gil(); + return self + .call1(gil.python(), "y", (i, j, x)) + .unwrap() + .extract(gil.python()) + .unwrap(); + } + + pub fn sim_y_d(&self, i: u128, d: u128) -> u128 { + let gil = Python::acquire_gil(); + return self + .call1(gil.python(), "y_D", (i, d)) + .unwrap() + .extract(gil.python()) + .unwrap(); + } + + pub fn sim_remove_liquidity_imbalance(&self, amounts: Vec) -> u128 { + let gil = Python::acquire_gil(); + return self + .call1( + gil.python(), + "remove_liquidity_imbalance", + PyTuple::new(gil.python(), amounts.to_vec()), + ) + .unwrap() + .extract(gil.python()) + .unwrap(); + } + + pub fn sim_calc_withdraw_one_coin(&self, token_amount: u128, i: u128) -> (u128, u128) { + let gil = Python::acquire_gil(); + return self + .call1(gil.python(), "calc_withdraw_one_coin", (token_amount, i)) + .unwrap() + .extract(gil.python()) + .unwrap(); + } + + fn call0(&self, py: Python, method_name: &str) -> Result { + let sim = PyModule::from_code(py, &self.py_src, FILE_NAME, MODULE_NAME).unwrap(); + let model = sim + .getattr("Curve")? + .call1(( + self.amp_factor, + self.balances.to_vec(), + self.n_coins, + self.target_prices.to_vec(), + self.pool_tokens, + )) + .unwrap() + .to_object(py); + let py_ret = model.as_ref(py).call_method0(method_name); + self.extract_py_ret(py, py_ret) + } + + fn call1( + &self, + py: Python, + method_name: &str, + args: impl IntoPy>, + ) -> Result { + let sim = PyModule::from_code(py, &self.py_src, FILE_NAME, MODULE_NAME).unwrap(); + let model = sim + .getattr("Curve")? + .call1(( + self.amp_factor, + self.balances.to_vec(), + self.n_coins, + self.target_prices.to_vec(), + self.pool_tokens, + )) + .unwrap() + .to_object(py); + let py_ret = model.as_ref(py).call_method1(method_name, args); + self.extract_py_ret(py, py_ret) + } + + fn extract_py_ret(&self, py: Python, ret: PyResult<&PyAny>) -> Result { + match ret { + Ok(v) => v.extract(), + Err(e) => { + e.print_and_set_sys_last_vars(py); + panic!("Python exeuction failed.") + } + } + } + + pub fn print_src(&self) { + println!("{}", self.py_src); + } +} diff --git a/contracts/liquidity_hub/pool-manager/src/error.rs b/contracts/liquidity_hub/pool-manager/src/error.rs index ca76e0b79..b9e9da968 100644 --- a/contracts/liquidity_hub/pool-manager/src/error.rs +++ b/contracts/liquidity_hub/pool-manager/src/error.rs @@ -159,6 +159,9 @@ pub enum ContractError { #[error("Invalid swap route: {0}")] InvalidSwapRoute(SwapRoute), + + #[error("Invalid pool assets length, expected {expected} got {actual}")] + InvalidPoolAssetsLength { expected: usize, actual: usize }, } impl From for ContractError { diff --git a/contracts/liquidity_hub/pool-manager/src/helpers.rs b/contracts/liquidity_hub/pool-manager/src/helpers.rs index 3e7870f6e..0c0495687 100644 --- a/contracts/liquidity_hub/pool-manager/src/helpers.rs +++ b/contracts/liquidity_hub/pool-manager/src/helpers.rs @@ -2,8 +2,8 @@ use std::ops::Mul; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - ensure, Addr, Coin, Decimal, Decimal256, Deps, DepsMut, Env, StdError, StdResult, Storage, - Uint128, Uint256, + coin, ensure, Addr, Coin, Decimal, Decimal256, Deps, DepsMut, Env, StdError, StdResult, + Storage, Uint128, Uint256, }; use white_whale_std::fee::PoolFee; @@ -16,17 +16,25 @@ use crate::math::Decimal256Helper; /// The amount of iterations to perform when calculating the Newton-Raphson approximation. const NEWTON_ITERATIONS: u64 = 32; -// todo isn't this for the 3pool? shouldn't it be 3 -// the number of assets in the pool -const N_COINS: Uint256 = Uint256::from_u128(2); +/// Encodes all results of swapping from a source token to a destination token. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SwapResult { + /// New amount of source token + pub new_source_amount: Uint128, + /// New amount of destination token + pub new_destination_amount: Uint128, + /// Amount of destination token swapped + pub amount_swapped: Uint128, +} fn calculate_stableswap_d( + n_coins: Uint256, offer_pool: Decimal256, ask_pool: Decimal256, amp: &u64, precision: u8, ) -> Result { - let n_coins = Decimal256::from_ratio(N_COINS, Uint256::from_u128(1)); + let n_coins_decimal = Decimal256::from_ratio(n_coins, Uint256::one()); let sum_pools = offer_pool.checked_add(ask_pool)?; if sum_pools.is_zero() { @@ -35,7 +43,7 @@ fn calculate_stableswap_d( } // ann = amp * n_coins - let ann = Decimal256::from_ratio(Uint256::from_u128((*amp).into()).checked_mul(N_COINS)?, 1u8); + let ann = Decimal256::from_ratio(Uint256::from_u128((*amp).into()).checked_mul(n_coins)?, 1u8); // perform Newton-Raphson method let mut current_d = sum_pools; @@ -45,7 +53,7 @@ fn calculate_stableswap_d( let new_d = [offer_pool, ask_pool] .into_iter() .try_fold::<_, _, Result<_, ContractError>>(current_d, |acc, pool| { - let mul_pools = pool.checked_mul(n_coins)?; + let mul_pools = pool.checked_mul(n_coins_decimal)?; acc.checked_multiply_ratio(current_d, mul_pools) })?; @@ -53,12 +61,16 @@ fn calculate_stableswap_d( // current_d = ((ann * sum_pools + new_d * n_coins) * current_d) / ((ann - 1) * current_d + (n_coins + 1) * new_d) current_d = (ann .checked_mul(sum_pools)? - .checked_add(new_d.checked_mul(n_coins)?)? + .checked_add(new_d.checked_mul(n_coins_decimal)?)? .checked_mul(current_d)?) .checked_div( (ann.checked_sub(Decimal256::one())? .checked_mul(current_d)? - .checked_add(n_coins.checked_add(Decimal256::one())?.checked_mul(new_d)?))?, + .checked_add( + n_coins_decimal + .checked_add(Decimal256::one())? + .checked_mul(new_d)?, + ))?, )?; if current_d >= old_d { @@ -92,6 +104,7 @@ pub enum StableSwapDirection { /// Calculates the new pool amount given the current pools and swap size. pub fn calculate_stableswap_y( + n_coins: Uint256, offer_pool: Decimal256, ask_pool: Decimal256, offer_amount: Decimal256, @@ -99,9 +112,9 @@ pub fn calculate_stableswap_y( ask_precision: u8, direction: StableSwapDirection, ) -> Result { - let ann = Uint256::from_u128((*amp).into()).checked_mul(N_COINS)?; + let ann = Uint256::from_u128((*amp).into()).checked_mul(n_coins)?; - let d = calculate_stableswap_d(offer_pool, ask_pool, amp, ask_precision)? + let d = calculate_stableswap_d(n_coins, offer_pool, ask_pool, amp, ask_precision)? .to_uint256_with_precision(u32::from(ask_precision))?; let pool_sum = match direction { @@ -111,8 +124,8 @@ pub fn calculate_stableswap_y( .to_uint256_with_precision(u32::from(ask_precision))?; let c = d - .checked_multiply_ratio(d, pool_sum.checked_mul(N_COINS)?)? - .checked_multiply_ratio(d, ann.checked_mul(N_COINS)?)?; + .checked_multiply_ratio(d, pool_sum.checked_mul(n_coins)?)? + .checked_multiply_ratio(d, ann.checked_mul(n_coins)?)?; let b = pool_sum.checked_add(d.checked_div(ann)?)?; @@ -138,8 +151,11 @@ pub fn calculate_stableswap_y( Err(ContractError::ConvergeError) } +#[allow(clippy::too_many_arguments)] /// computes a swap +#[allow(clippy::too_many_arguments)] pub fn compute_swap( + n_coins: Uint256, offer_pool: Uint128, ask_pool: Uint128, offer_amount: Uint128, @@ -177,6 +193,7 @@ pub fn compute_swap( let offer_amount = Decimal256::decimal_with_precision(offer_amount, offer_precision)?; let new_pool = calculate_stableswap_y( + n_coins, offer_pool, ask_pool, offer_amount, @@ -472,10 +489,12 @@ pub struct OfferAmountComputation { pub osmosis_fee_amount: Uint128, } +// TODO: make this work with n_coins being dynamic + pub fn assert_slippage_tolerance( slippage_tolerance: &Option, - deposits: &[Uint128; 2], - pools: &[Coin; 2], + deposits: &[Coin], + pools: &[Coin], pool_type: PoolType, amount: Uint128, pool_token_supply: Uint128, @@ -487,14 +506,19 @@ pub fn assert_slippage_tolerance( } let one_minus_slippage_tolerance = Decimal256::one() - slippage_tolerance; - let deposits: [Uint256; 2] = [deposits[0].into(), deposits[1].into()]; - let pools: [Uint256; 2] = [pools[0].amount.into(), pools[1].amount.into()]; + let deposits: Vec = deposits.iter().map(|coin| coin.amount.into()).collect(); + let pools: Vec = pools.iter().map(|coin| coin.amount.into()).collect(); // Ensure each prices are not dropped as much as slippage tolerance rate match pool_type { PoolType::StableSwap { .. } => { - let pools_total = pools[0].checked_add(pools[1])?; - let deposits_total = deposits[0].checked_add(deposits[1])?; + // TODO: shouldn't be necessary to handle unwraps properly as they come from Uint128, but doublecheck! + let pools_total: Uint256 = pools + .into_iter() + .fold(Uint256::zero(), |acc, x| acc.checked_add(x).unwrap()); + let deposits_total: Uint256 = deposits + .into_iter() + .fold(Uint256::zero(), |acc, x| acc.checked_add(x).unwrap()); let pool_ratio = Decimal256::from_ratio(pools_total, pool_token_supply); let deposit_ratio = Decimal256::from_ratio(deposits_total, amount); @@ -507,6 +531,12 @@ pub fn assert_slippage_tolerance( } } PoolType::ConstantProduct => { + if deposits.len() != 2 || pools.len() != 2 { + return Err(ContractError::InvalidPoolAssetsLength { + expected: 2, + actual: deposits.len(), + }); + } if Decimal256::from_ratio(deposits[0], deposits[1]) * one_minus_slippage_tolerance > Decimal256::from_ratio(pools[0], pools[1]) || Decimal256::from_ratio(deposits[1], deposits[0]) @@ -662,3 +692,633 @@ pub fn get_asset_indexes_in_pool( ask_decimal, )) } + +// TODO: handle unwraps properly +#[allow(clippy::unwrap_used)] +pub fn compute_d(amp_factor: &u64, deposits: &[Coin]) -> Option { + let n_coins = Uint128::from(deposits.len() as u128); + + // sum(x_i), a.k.a S + let sum_x = deposits + .iter() + .fold(Uint128::zero(), |acc, x| acc.checked_add(x.amount).unwrap()); + + if sum_x == Uint128::zero() { + Some(Uint256::zero()) + } else { + // do as below but for a generic number of assets + let amount_times_coins: Vec = deposits + .iter() + .map(|coin| coin.amount.checked_mul(n_coins).unwrap()) + .collect(); + + // Newton's method to approximate D + let mut d_prev: Uint256; + let mut d: Uint256 = sum_x.into(); + for _ in 0..256 { + let mut d_prod = d; + for amount in amount_times_coins.clone().into_iter() { + d_prod = d_prod + .checked_mul(d) + .unwrap() + .checked_div(amount.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() <= Uint256::one() { + break; + } + } else if d_prev.checked_sub(d).unwrap() <= Uint256::one() { + break; + } + } + + Some(d) + } +} + +// TODO: handle unwraps properly +#[allow(clippy::unwrap_used)] +fn compute_next_d( + amp_factor: &u64, + d_init: Uint256, + d_prod: Uint256, + sum_x: Uint128, + n_coins: Uint128, +) -> Option { + let ann = amp_factor.checked_mul(n_coins.u128() as u64)?; + let leverage = Uint256::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()) +} + +/// Computes the amount of pool tokens to mint after a deposit. +#[allow(clippy::unwrap_used, clippy::too_many_arguments)] +pub fn compute_mint_amount_for_deposit( + amp_factor: &u64, + deposits: &[Coin], + swaps: &[Coin], + pool_token_supply: Uint128, +) -> Option { + // Initial invariant + let d_0 = compute_d(amp_factor, deposits)?; + + let new_balances: Vec = swaps + .iter() + .enumerate() + .map(|(i, pool_asset)| { + let deposit_amount = deposits[i].amount; + let new_amount = pool_asset.amount.checked_add(deposit_amount).unwrap(); + Coin { + denom: pool_asset.denom.clone(), + amount: new_amount, + } + }) + .collect(); + + // Invariant after change + let d_1 = compute_d(amp_factor, &new_balances)?; + if d_1 <= d_0 { + None + } else { + let amount = Uint256::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()) + } +} + +/// Compute the swap amount `y` in proportion to `x`. +/// +/// Solve for `y`: +/// +/// ```text +/// y**2 + y * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) +/// y**2 + b*y = c +/// ``` +#[allow(clippy::many_single_char_names, clippy::unwrap_used)] +pub fn compute_y_raw( + n_coins: u8, + amp_factor: &u64, + swap_in: Uint128, + //swap_out: Uint128, + no_swap: Uint128, + d: Uint256, +) -> Option { + let ann = amp_factor.checked_mul(n_coins.into())?; // A * n ** n + + // sum' = prod' = x + // c = D ** (n + 1) / (n ** (2 * n) * prod' * A) + let mut c = d; + + c = c + .checked_mul(d) + .unwrap() + .checked_div(swap_in.checked_mul(n_coins.into()).unwrap().into()) + .unwrap(); + + c = c + .checked_mul(d) + .unwrap() + .checked_div(no_swap.checked_mul(n_coins.into()).unwrap().into()) + .unwrap(); + c = c + .checked_mul(d) + .unwrap() + .checked_div(ann.checked_mul(n_coins.into()).unwrap().into()) + .unwrap(); + // b = sum(swap_in, no_swap) + D // Ann - D + // not subtracting D here because that could result in a negative. + let b = d + .checked_div(ann.into()) + .unwrap() + .checked_add(swap_in.into()) + .unwrap() + .checked_add(no_swap.into()) + .unwrap(); + + // Solve for y by approximating: y**2 + b*y = c + let mut y_prev: Uint256; + let mut y = d; + for _ in 0..1000 { + y_prev = y; + // y = (y * y + c) / (2 * y + b - d); + let y_numerator = y.checked_mul(y).unwrap().checked_add(c).unwrap(); + let y_denominator = y + .checked_mul(Uint256::from(2u8)) + .unwrap() + .checked_add(b) + .unwrap() + .checked_sub(d) + .unwrap(); + y = y_numerator.checked_div(y_denominator).unwrap(); + if y > y_prev { + if y.checked_sub(y_prev).unwrap() <= Uint256::one() { + break; + } + } else if y_prev.checked_sub(y).unwrap() <= Uint256::one() { + break; + } + } + Some(y) +} + +/// Computes the swap amount `y` in proportion to `x`. +#[allow(clippy::unwrap_used)] +pub fn compute_y( + n_coins: u8, + amp_factor: &u64, + x: Uint128, + no_swap: Uint128, + d: Uint256, +) -> Option { + let amount = compute_y_raw(n_coins, amp_factor, x, no_swap, d)?; + Some(Uint128::try_from(amount).unwrap()) +} + +/// Compute SwapResult after an exchange +#[allow(clippy::unwrap_used)] +pub fn swap_to( + n_coins: u8, + amp_factor: &u64, + source_amount: Uint128, + swap_source_amount: Uint128, + swap_destination_amount: Uint128, + unswaped_amount: Uint128, +) -> Option { + let deposits = vec![ + coin(swap_source_amount.u128(), "denom1"), + coin(swap_destination_amount.u128(), "denom2"), + coin(unswaped_amount.u128(), "denom3"), + ]; + let y = compute_y( + n_coins, + amp_factor, + swap_source_amount.checked_add(source_amount).unwrap(), + unswaped_amount, + compute_d(amp_factor, &deposits).unwrap(), + )?; + // https://github.com/curvefi/curve-contract/blob/b0bbf77f8f93c9c5f4e415bce9cd71f0cdee960e/contracts/pool-templates/base/SwapTemplateBase.vy#L466 + let dy = swap_destination_amount + .checked_sub(y) + .unwrap() + .checked_sub(Uint128::one()) + .unwrap(); + + let amount_swapped = dy; + let new_destination_amount = swap_destination_amount.checked_sub(amount_swapped).unwrap(); + let new_source_amount = swap_source_amount.checked_add(source_amount).unwrap(); + + Some(SwapResult { + new_source_amount, + new_destination_amount, + amount_swapped, + }) +} + +#[cfg(test)] +#[allow( + clippy::unwrap_used, + clippy::arithmetic_side_effects, + clippy::too_many_arguments +)] +mod tests { + use cosmwasm_std::coin; + use proptest::prelude::*; + use rand::Rng; + use sim::Model; + + use super::*; + + /// Minimum amplification coefficient. + pub const MIN_AMP: u64 = 1; + + /// Maximum amplification coefficient. + pub const MAX_AMP: u64 = 1_000_000; + + /// Maximum number of tokens to swap at once. + pub const MAX_TOKENS_IN: Uint128 = Uint128::new(2u128 << 110); + + /// Number of coins in a swap. Hardcoded to 3 to reuse previous tests + pub const N_COINS: u8 = 3; + + fn check_d(model: &Model, amount_a: u128, amount_b: u128, amount_c: u128) -> Uint256 { + let deposits = vec![ + coin(amount_a, "denom1"), + coin(amount_b, "denom2"), + coin(amount_c, "denom4"), + ]; + + let d = compute_d(&model.amp_factor, &deposits).unwrap(); + d + } + + fn check_y(model: &Model, swap_in: u128, no_swap: u128, d: Uint256) { + let y = compute_y_raw( + N_COINS, + &model.amp_factor, + Uint128::new(swap_in), + Uint128::new(no_swap), + d, + ) + .unwrap(); + assert_eq!( + Uint128::try_from(y).unwrap().u128(), + model.sim_y(0, 1, swap_in) + ) + } + + #[test] + fn test_curve_math_specific() { + // Specific cases + let model_no_balance = Model::new(1, vec![0, 0, 0], N_COINS); + check_d(&model_no_balance, 0, 0, 0); + + let amount_a = 1046129065254161082u128; + let amount_b = 1250710035549196829u128; + let amount_c = 1111111111111111111u128; + let model = Model::new(1188, vec![amount_a, amount_b, amount_c], N_COINS); + let d = check_d(&model, amount_a, amount_b, amount_c); + let amount_x = 2045250484898639148u128; + check_y(&model, amount_x, amount_c, d); + + let amount_a = 862538457714585493u128; + let amount_b = 492548187909826733u128; + let amount_c = 777777777777777777u128; + let model = Model::new(9, vec![amount_a, amount_b, amount_c], N_COINS); + let d = check_d(&model, amount_a, amount_b, amount_c); + let amount_x = 815577754938955939u128; + + check_y(&model, amount_x, amount_c, d); + } + + #[test] + fn test_compute_mint_amount_for_deposit() { + let deposits = vec![ + coin(MAX_TOKENS_IN.u128(), "denom1"), + coin(MAX_TOKENS_IN.u128(), "denom2"), + coin(MAX_TOKENS_IN.u128(), "denom4"), + ]; + + let pool_assets = vec![ + coin(MAX_TOKENS_IN.u128(), "denom1"), + coin(MAX_TOKENS_IN.u128(), "denom2"), + coin(MAX_TOKENS_IN.u128(), "denom4"), + ]; + + let pool_token_supply = MAX_TOKENS_IN; + + let actual_mint_amount = + compute_mint_amount_for_deposit(&MIN_AMP, &deposits, &pool_assets, pool_token_supply) + .unwrap(); + let expected_mint_amount = MAX_TOKENS_IN; + + assert_eq!(actual_mint_amount, expected_mint_amount); + } + + #[ignore] + #[test] + fn test_curve_math_with_random_inputs() { + for _ in 0..100 { + let mut rng = rand::thread_rng(); + + let amp_factor: u64 = rng.gen_range(MIN_AMP..=MAX_AMP); + let amount_a = rng.gen_range(1..=MAX_TOKENS_IN.u128()); + let amount_b = rng.gen_range(1..=MAX_TOKENS_IN.u128()); + let amount_c = rng.gen_range(1..=MAX_TOKENS_IN.u128()); + println!("testing curve_math_with_random_inputs:"); + println!( + "amp_factor: {}, amount_a: {}, amount_b: {}, amount_c: {}", + amp_factor, amount_a, amount_b, amount_c, + ); + + let model = Model::new(amp_factor, vec![amount_a, amount_b, amount_c], N_COINS); + let d = check_d(&model, amount_a, amount_b, amount_c); + let amount_x = rng.gen_range(0..=amount_a); + + println!("amount_x: {}", amount_x); + check_y(&model, amount_x, amount_c, d); + } + } + + #[derive(Debug)] + struct SwapTest { + pub amp_factor: u64, + pub swap_reserve_balance_a: Uint128, + pub swap_reserve_balance_b: Uint128, + pub swap_reserve_balance_c: Uint128, + pub user_token_balance_a: Uint128, + pub user_token_balance_b: Uint128, + } + + impl SwapTest { + pub fn swap_a_to_b(&mut self, swap_amount: Uint128) { + self.do_swap(true, swap_amount) + } + + pub fn swap_b_to_a(&mut self, swap_amount: Uint128) { + self.do_swap(false, swap_amount) + } + + fn do_swap(&mut self, swap_a_to_b: bool, source_amount: Uint128) { + let (swap_source_amount, swap_dest_amount) = match swap_a_to_b { + true => (self.swap_reserve_balance_a, self.swap_reserve_balance_b), + false => (self.swap_reserve_balance_b, self.swap_reserve_balance_a), + }; + + let SwapResult { + new_source_amount, + new_destination_amount, + amount_swapped, + .. + } = swap_to( + N_COINS, + &self.amp_factor, + source_amount, + swap_source_amount, + swap_dest_amount, + self.swap_reserve_balance_c, + ) + .unwrap(); + + match swap_a_to_b { + true => { + self.swap_reserve_balance_a = new_source_amount; + self.swap_reserve_balance_b = new_destination_amount; + self.user_token_balance_a -= source_amount; + self.user_token_balance_b += amount_swapped; + } + false => { + self.swap_reserve_balance_a = new_destination_amount; + self.swap_reserve_balance_b = new_source_amount; + self.user_token_balance_a += amount_swapped; + self.user_token_balance_b -= source_amount; + } + } + } + } + + proptest! { + #[test] + fn test_swaps_does_not_result_in_more_tokens( + amp_factor in MIN_AMP..=MAX_AMP, + initial_user_token_a_amount in 10_000_000..MAX_TOKENS_IN.u128() >> 16, + initial_user_token_b_amount in 10_000_000..MAX_TOKENS_IN.u128() >> 16, + ) { + + let mut t = SwapTest { amp_factor, swap_reserve_balance_a: MAX_TOKENS_IN, swap_reserve_balance_b: MAX_TOKENS_IN, + swap_reserve_balance_c: MAX_TOKENS_IN, + user_token_balance_a: Uint128::new(initial_user_token_a_amount), + user_token_balance_b:Uint128::new(initial_user_token_b_amount), + }; + + const ITERATIONS: u64 = 100; + const SHRINK_MULTIPLIER: u64= 10; + + for i in 0..ITERATIONS { + let before_balance_a = t.user_token_balance_a; + let before_balance_b = t.user_token_balance_b; + let swap_amount = before_balance_a / Uint128::from((i + 1) * SHRINK_MULTIPLIER); + t.swap_a_to_b(swap_amount); + let after_balance = t.user_token_balance_a + t.user_token_balance_b; + + assert!(before_balance_a + before_balance_b >= after_balance, "before_a: {}, before_b: {}, after_a: {}, after_b: {}, amp_factor: {:?}", before_balance_a, before_balance_b, t.user_token_balance_a, t.user_token_balance_b, amp_factor); + } + + for i in 0..ITERATIONS { + let before_balance_a = t.user_token_balance_a; + let before_balance_b = t.user_token_balance_b; + let swap_amount = before_balance_a / Uint128::from((i + 1) * SHRINK_MULTIPLIER); + t.swap_a_to_b(swap_amount); + let after_balance = t.user_token_balance_a + t.user_token_balance_b; + + assert!(before_balance_a + before_balance_b >= after_balance, "before_a: {}, before_b: {}, after_a: {}, after_b: {}, amp_factor: {:?}", before_balance_a, before_balance_b, t.user_token_balance_a, t.user_token_balance_b, amp_factor); + } + } + } + + #[test] + fn test_swaps_does_not_result_in_more_tokens_specific_one() { + const AMP_FACTOR: u64 = 324449; + const INITIAL_SWAP_RESERVE_AMOUNT: Uint128 = Uint128::new(100_000_000_000u128); + const INITIAL_USER_TOKEN_AMOUNT: Uint128 = Uint128::new(10_000_000_000u128); + + let mut t = SwapTest { + amp_factor: AMP_FACTOR, + swap_reserve_balance_a: INITIAL_SWAP_RESERVE_AMOUNT, + swap_reserve_balance_b: INITIAL_SWAP_RESERVE_AMOUNT, + swap_reserve_balance_c: INITIAL_SWAP_RESERVE_AMOUNT, + user_token_balance_a: INITIAL_USER_TOKEN_AMOUNT, + user_token_balance_b: INITIAL_USER_TOKEN_AMOUNT, + }; + + t.swap_a_to_b(Uint128::new(2097152u128)); + t.swap_a_to_b(Uint128::new(8053063680u128)); + t.swap_a_to_b(Uint128::new(48u128)); + assert!( + t.user_token_balance_a + t.user_token_balance_b + <= INITIAL_USER_TOKEN_AMOUNT * Uint128::from(2u8) + ); + } + + #[test] + fn test_swaps_does_not_result_in_more_tokens_specific_two() { + const AMP_FACTOR: u64 = 186512; + const INITIAL_SWAP_RESERVE_AMOUNT: Uint128 = Uint128::new(100_000_000_000u128); + const INITIAL_USER_TOKEN_AMOUNT: Uint128 = Uint128::new(1_000_000_000u128); + + let mut t = SwapTest { + amp_factor: AMP_FACTOR, + swap_reserve_balance_a: INITIAL_SWAP_RESERVE_AMOUNT, + swap_reserve_balance_b: INITIAL_SWAP_RESERVE_AMOUNT, + swap_reserve_balance_c: INITIAL_SWAP_RESERVE_AMOUNT, + user_token_balance_a: INITIAL_USER_TOKEN_AMOUNT, + user_token_balance_b: INITIAL_USER_TOKEN_AMOUNT, + }; + + t.swap_b_to_a(Uint128::new(33579101u128)); + t.swap_a_to_b(Uint128::new(2097152u128)); + assert!( + t.user_token_balance_a + t.user_token_balance_b + <= INITIAL_USER_TOKEN_AMOUNT * Uint128::from(2u8) + ); + } + + #[test] + fn test_swaps_does_not_result_in_more_tokens_specific_three() { + const AMP_FACTOR: u64 = 1220; + const INITIAL_SWAP_RESERVE_AMOUNT: Uint128 = Uint128::new(100_000_000_000u128); + const INITIAL_USER_TOKEN_AMOUNT: Uint128 = Uint128::new(1_000_000_000u128); + + let mut t = SwapTest { + amp_factor: AMP_FACTOR, + swap_reserve_balance_a: INITIAL_SWAP_RESERVE_AMOUNT, + swap_reserve_balance_b: INITIAL_SWAP_RESERVE_AMOUNT, + swap_reserve_balance_c: INITIAL_SWAP_RESERVE_AMOUNT, + user_token_balance_a: INITIAL_USER_TOKEN_AMOUNT, + user_token_balance_b: INITIAL_USER_TOKEN_AMOUNT, + }; + + t.swap_b_to_a(Uint128::from(65535u128)); + t.swap_b_to_a(Uint128::from(6133503u128)); + t.swap_a_to_b(Uint128::from(65535u128)); + assert!( + t.user_token_balance_a + t.user_token_balance_b + <= INITIAL_USER_TOKEN_AMOUNT * Uint128::from(2u8) + ); + } + + proptest! { + #[test] + fn test_virtual_price_does_not_decrease_from_deposit( + amp_factor in MIN_AMP..=MAX_AMP, + deposit_amount_a in 0..MAX_TOKENS_IN.u128() >> 2, + deposit_amount_b in 0..MAX_TOKENS_IN.u128() >> 2, + deposit_amount_c in 0..MAX_TOKENS_IN.u128() >> 2, + swap_token_a_amount in 0..MAX_TOKENS_IN.u128(), + swap_token_b_amount in 0..MAX_TOKENS_IN.u128(), + swap_token_c_amount in 0..MAX_TOKENS_IN.u128(), + pool_token_supply in 0..MAX_TOKENS_IN.u128(), + ) { + let swaps = vec![ + coin(swap_token_a_amount, "denom1"), + coin(swap_token_b_amount, "denom2"), + coin(swap_token_c_amount, "denom3"), + ]; + + let d0 = compute_d(&_factor, &swaps).unwrap(); + + let deposits = vec![ + coin(deposit_amount_a, "denom1"), + coin(deposit_amount_b, "denom2"), + coin(deposit_amount_c, "denom3"), + ]; + + let mint_amount = compute_mint_amount_for_deposit( + &_factor, + &swaps, + &deposits, + Uint128::new(pool_token_supply), + ); + prop_assume!(mint_amount.is_some()); + + let new_swap_token_a_amount = swap_token_a_amount + deposit_amount_a; + let new_swap_token_b_amount = swap_token_b_amount + deposit_amount_b; + let new_swap_token_c_amount = swap_token_c_amount + deposit_amount_c; + let new_pool_token_supply = pool_token_supply + mint_amount.unwrap().u128(); + + let new_swaps = vec![ + coin(new_swap_token_a_amount, "denom1"), + coin(new_swap_token_b_amount, "denom2"), + coin(new_swap_token_c_amount, "denom3"), + ]; + + let d1 = compute_d(&_factor, &new_swaps).unwrap(); + + assert!(d0 < d1); + assert!(d0 / Uint256::from( pool_token_supply) <= d1 / Uint256::from( new_pool_token_supply)); + } + } + + proptest! { + #[test] + fn test_virtual_price_does_not_decrease_from_swap( + amp_factor in MIN_AMP..=MAX_AMP, + source_token_amount in 0..MAX_TOKENS_IN.u128(), + swap_source_amount in 0..MAX_TOKENS_IN.u128(), + swap_destination_amount in 0..MAX_TOKENS_IN.u128(), + unswapped_amount in 0..MAX_TOKENS_IN.u128(), + ) { + let source_token_amount = source_token_amount; + let swap_source_amount = swap_source_amount; + let swap_destination_amount = swap_destination_amount; + let unswapped_amount = unswapped_amount; + + let deposits = vec![ + coin(swap_source_amount, "denom1"), + coin(swap_destination_amount, "denom2"), + coin(unswapped_amount, "denom3"), + ]; + + let d0 = compute_d(&_factor, &deposits).unwrap(); + + let swap_result = swap_to(N_COINS, &_factor, source_token_amount.into(), swap_source_amount.into(), swap_destination_amount.into(), unswapped_amount.into()); + prop_assume!(swap_result.is_some()); + + let swap_result = swap_result.unwrap(); + + let swaps = vec![ + coin(swap_result.new_source_amount.u128(), "denom1"), + coin(swap_result.new_destination_amount.u128(), "denom2"), + coin(unswapped_amount, "denom3"), + ]; + + let d1 = compute_d(&_factor, &swaps).unwrap(); + + assert!(d0 <= d1); // Pool token supply not changed on swaps + } + } +} diff --git a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs index 8d163a13b..6dea5bfcf 100644 --- a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ coin, coins, ensure, to_json_binary, wasm_execute, BankMsg, Coin, CosmosMsg, DepsMut, Env, - MessageInfo, Response, StdError, SubMsg, + MessageInfo, Response, SubMsg, }; use cosmwasm_std::{Decimal, OverflowError, Uint128}; @@ -23,7 +23,7 @@ use crate::{ // After writing create_pool I see this can get quite verbose so attempting to // break it down into smaller modules which house some things like swap, liquidity etc use crate::contract::SINGLE_SIDE_LIQUIDITY_PROVISION_REPLY_ID; -use crate::helpers::aggregate_outgoing_fees; +use crate::helpers::{aggregate_outgoing_fees, compute_d, compute_mint_amount_for_deposit}; use crate::queries::query_simulation; use crate::state::{ LiquidityProvisionData, SingleSideLiquidityProvisionBuffer, @@ -221,10 +221,7 @@ pub fn provide_liquidity( .integer_sqrt() .as_u128(), ) - .checked_sub(MINIMUM_LIQUIDITY_AMOUNT) - .map_err(|_| { - ContractError::InvalidInitialLiquidityAmount(MINIMUM_LIQUIDITY_AMOUNT) - })?; + .saturating_sub(MINIMUM_LIQUIDITY_AMOUNT); // share should be above zero after subtracting the MINIMUM_LIQUIDITY_AMOUNT if share.is_zero() { @@ -251,22 +248,11 @@ pub fn provide_liquidity( .multiply_ratio(total_share, pool_assets[1].amount), ); - let deposits_as: [Uint128; 2] = deposits - .iter() - .map(|coin| coin.amount) - .collect::>() - .try_into() - .map_err(|_| StdError::generic_err("Error converting vector to array"))?; - let pools_as: [Coin; 2] = pool_assets - .to_vec() - .try_into() - .map_err(|_| StdError::generic_err("Error converting vector to array"))?; - // assert slippage tolerance helpers::assert_slippage_tolerance( &slippage_tolerance, - &deposits_as, - &pools_as, + &deposits, + &pool_assets, pool.pool_type.clone(), amount, total_share, @@ -275,10 +261,47 @@ pub fn provide_liquidity( amount } } - PoolType::StableSwap { amp: _ } => { - // TODO: Handle stableswap + PoolType::StableSwap { amp: amp_factor } => { + 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::try_from(compute_d(amp_factor, &deposits).unwrap())? + .saturating_sub(MINIMUM_LIQUIDITY_AMOUNT); - Uint128::one() + // share should be above zero after subtracting the min_lp_token_amount + if share.is_zero() { + return Err(ContractError::InvalidInitialLiquidityAmount( + MINIMUM_LIQUIDITY_AMOUNT, + )); + } + + // mint the lp tokens to the contract + messages.push(white_whale_std::lp_common::mint_lp_token_msg( + liquidity_token.clone(), + &env.contract.address, + &env.contract.address, + MINIMUM_LIQUIDITY_AMOUNT, + )?); + + share + } else { + let amount = compute_mint_amount_for_deposit( + amp_factor, + &deposits, + &pool_assets, + total_share, + ) + .unwrap(); + helpers::assert_slippage_tolerance( + &slippage_tolerance, + &deposits, + &pool_assets, + pool.pool_type.clone(), + amount, + total_share, + )?; + amount + } } }; diff --git a/contracts/liquidity_hub/pool-manager/src/queries.rs b/contracts/liquidity_hub/pool-manager/src/queries.rs index 330226ad9..a02e4f3a0 100644 --- a/contracts/liquidity_hub/pool-manager/src/queries.rs +++ b/contracts/liquidity_hub/pool-manager/src/queries.rs @@ -1,6 +1,8 @@ use std::cmp::Ordering; -use cosmwasm_std::{coin, ensure, Coin, Decimal256, Deps, Fraction, Order, StdResult, Uint128}; +use cosmwasm_std::{ + coin, ensure, Coin, Decimal256, Deps, Fraction, Order, StdResult, Uint128, Uint256, +}; use white_whale_std::pool_manager::{ AssetDecimalsResponse, Config, PoolInfoResponse, PoolType, ReverseSimulationResponse, @@ -55,6 +57,7 @@ pub fn query_simulation( get_asset_indexes_in_pool(&pool_info, offer_asset.denom, ask_asset_denom)?; let swap_computation = helpers::compute_swap( + Uint256::from(pool_info.assets.len() as u128), offer_asset_in_pool.amount, ask_asset_in_pool.amount, offer_asset.amount, @@ -161,6 +164,7 @@ pub fn query_reverse_simulation( let max_precision = offer_decimal.max(ask_decimal); let new_offer_pool_amount = calculate_stableswap_y( + Uint256::from(pool_info.assets.len() as u128), offer_pool, ask_pool, before_fees, diff --git a/contracts/liquidity_hub/pool-manager/src/state.rs b/contracts/liquidity_hub/pool-manager/src/state.rs index 7422f803d..529fd04ca 100644 --- a/contracts/liquidity_hub/pool-manager/src/state.rs +++ b/contracts/liquidity_hub/pool-manager/src/state.rs @@ -95,3 +95,12 @@ pub const SWAP_ROUTES: Map<(&str, &str), SwapOperations> = Map::new("swap_routes pub const CONFIG: Item = Item::new("config"); pub const POOL_COUNTER: Item = Item::new("pool_count"); + +pub const STABLE_SWAP_PARAMS: Item = Item::new("stable_swap_params"); +#[cw_serde] +pub struct StableSwapParams { + pub initial_amp: u64, + pub future_amp: u64, + pub initial_amp_block: u64, + pub future_amp_block: u64, +} diff --git a/contracts/liquidity_hub/pool-manager/src/swap/perform_swap.rs b/contracts/liquidity_hub/pool-manager/src/swap/perform_swap.rs index 01ba30198..21153e32d 100644 --- a/contracts/liquidity_hub/pool-manager/src/swap/perform_swap.rs +++ b/contracts/liquidity_hub/pool-manager/src/swap/perform_swap.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Coin, Decimal, DepsMut, Uint128}; +use cosmwasm_std::{Coin, Decimal, DepsMut, Uint128, Uint256}; use white_whale_std::pool_manager::PoolInfo; use white_whale_std::pool_network::swap::assert_max_spread; @@ -58,6 +58,7 @@ pub fn perform_swap( // compute the swap let swap_computation = helpers::compute_swap( + Uint256::from(pool_info.assets.len() as u128), offer_asset_in_pool.amount, ask_asset_in_pool.amount, offer_asset.amount, diff --git a/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs b/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs index e33e39a25..1ada98dda 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs @@ -223,7 +223,7 @@ mod pool_creation_failures { }, extra_fees: vec![], }; - // Create a poo + // Create a pool suite .instantiate_default() .add_one_day() @@ -240,10 +240,11 @@ mod pool_creation_failures { vec![coin(90, "uusd")], |result| { let err = result.unwrap_err().downcast::().unwrap(); - match err { ContractError::InvalidPoolCreationFee { .. } => {} - _ => panic!("Wrong error type, should return ContractError::Unauthorized"), + _ => panic!( + "Wrong error type, should return ContractError::InvalidPoolCreationFee" + ), } }, ); @@ -2078,7 +2079,7 @@ mod swapping { } #[test] - fn basic_swapping_test_stable_swap() { + fn basic_swapping_test_stable_swap_two_assets() { let mut suite = TestingSuite::default_with_balances(vec![ coin(1_000_000_001u128, "uwhale".to_string()), coin(1_000_000_000u128, "uluna".to_string()), @@ -2123,7 +2124,7 @@ mod swapping { extra_fees: vec![], }; - // Create a pool + // Create a stableswap pool with amp = 100 suite .instantiate_default() .add_one_day() @@ -2961,7 +2962,9 @@ mod locking_lp { } mod provide_liquidity { - use cosmwasm_std::{coin, Coin, Decimal, StdError, Uint128}; + use std::cell::RefCell; + + use cosmwasm_std::{assert_approx_eq, coin, Coin, Decimal, StdError, Uint128}; use white_whale_std::fee::{Fee, PoolFee}; use white_whale_std::pool_manager::PoolType; @@ -3577,6 +3580,506 @@ mod provide_liquidity { }, ); } + + #[test] + fn provide_liquidity_stable_swap() { + let mut suite = TestingSuite::default_with_balances(vec![ + coin(1_000_000_001u128, "uwhale".to_string()), + coin(1_000_000_000u128, "uluna".to_string()), + coin(1_000_000_001u128, "uusd".to_string()), + ]); + let creator = suite.creator(); + let _other = suite.senders[1].clone(); + let _unauthorized = suite.senders[2].clone(); + // Asset infos with uwhale and uluna + + let asset_infos = vec![ + "uwhale".to_string(), + "uluna".to_string(), + "uusd".to_string(), + ]; + + // Protocol fee is 0.01% and swap fee is 0.02% and burn fee is 0% + #[cfg(not(feature = "osmosis"))] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::from_ratio(1u128, 1000u128), + }, + swap_fee: Fee { + share: Decimal::from_ratio(1u128, 10_000_u128), + }, + burn_fee: Fee { + share: Decimal::zero(), + }, + extra_fees: vec![], + }; + + #[cfg(feature = "osmosis")] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::from_ratio(1u128, 1000u128), + }, + swap_fee: Fee { + share: Decimal::from_ratio(1u128, 100_00u128), + }, + burn_fee: Fee { + share: Decimal::zero(), + }, + osmosis_fee: Fee { + share: Decimal::from_ratio(1u128, 1000u128), + }, + extra_fees: vec![], + }; + + // Create a pool + suite.instantiate_default().create_pool( + creator.clone(), + asset_infos, + vec![6u8, 6u8, 6u8], + pool_fees, + PoolType::StableSwap { amp: 100 }, + Some("whale-uluna-uusd".to_string()), + vec![coin(1000, "uusd")], + |result| { + result.unwrap(); + }, + ); + + // Lets try to add liquidity + suite.provide_liquidity( + creator.clone(), + "whale-uluna-uusd".to_string(), + None, + None, + None, + None, + vec![ + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(1_000_000u128), + }, + Coin { + denom: "uusd".to_string(), + amount: Uint128::from(1_000_000u128), + }, + ], + |result| { + // Ensure we got 999000 in the response which is 1mil less the initial liquidity amount + for event in result.unwrap().events { + println!("{:?}", event); + } + }, + ); + let simulated_return_amount = RefCell::new(Uint128::zero()); + suite.query_simulation( + "whale-uluna-uusd".to_string(), + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000u128), + }, + "uluna".to_string(), + |result| { + *simulated_return_amount.borrow_mut() = result.unwrap().return_amount; + }, + ); + + // Now lets try a swap + suite.swap( + creator.clone(), + "uluna".to_string(), + None, + None, + None, + "whale-uluna-uusd".to_string(), + vec![coin(1_000u128, "uwhale".to_string())], + |result| { + // Find the key with 'offer_amount' and the key with 'return_amount' + // Ensure that the offer amount is 1000 and the return amount is greater than 0 + let mut return_amount = String::new(); + let mut offer_amount = String::new(); + + for event in result.unwrap().events { + if event.ty == "wasm" { + for attribute in event.attributes { + match attribute.key.as_str() { + "return_amount" => return_amount = attribute.value, + "offer_amount" => offer_amount = attribute.value, + _ => {} + } + } + } + } + // Because the Pool was created and 1_000_000 of each token has been provided as liquidity + // Assuming no fees we should expect a small swap of 1000 to result in not too much slippage + // Expect 1000 give or take 0.002 difference + // Once fees are added and being deducted properly only the "0.002" should be changed. + assert_approx_eq!( + offer_amount.parse::().unwrap(), + return_amount.parse::().unwrap(), + "0.002" + ); + assert_approx_eq!( + simulated_return_amount.borrow().u128(), + return_amount.parse::().unwrap(), + "0.002" + ); + }, + ); + + let simulated_offer_amount = RefCell::new(Uint128::zero()); + // Now lets try a reverse simulation by swapping uluna to uwhale + suite.query_reverse_simulation( + "whale-uluna-uusd".to_string(), + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1000u128), + }, + "uluna".to_string(), + |result| { + *simulated_offer_amount.borrow_mut() = result.unwrap().offer_amount; + }, + ); + + // Another swap but this time the other way around + suite.swap( + creator.clone(), + "uwhale".to_string(), + None, + None, + None, + "whale-uluna-uusd".to_string(), + vec![coin( + simulated_offer_amount.borrow().u128(), + "uluna".to_string(), + )], + |result| { + // Find the key with 'offer_amount' and the key with 'return_amount' + // Ensure that the offer amount is 1000 and the return amount is greater than 0 + let mut return_amount = String::new(); + let mut offer_amount = String::new(); + + for event in result.unwrap().events { + if event.ty == "wasm" { + for attribute in event.attributes { + match attribute.key.as_str() { + "return_amount" => return_amount = attribute.value, + "offer_amount" => offer_amount = attribute.value, + _ => {} + } + } + } + } + assert_approx_eq!( + simulated_offer_amount.borrow().u128(), + offer_amount.parse::().unwrap(), + "0.002" + ); + + assert_approx_eq!(1000u128, return_amount.parse::().unwrap(), "0.003"); + }, + ); + + // And now uwhale to uusd + suite.query_reverse_simulation( + "whale-uluna-uusd".to_string(), + Coin { + denom: "uusd".to_string(), + amount: Uint128::from(1000u128), + }, + "uwhale".to_string(), + |result| { + *simulated_return_amount.borrow_mut() = result.unwrap().offer_amount; + }, + ); + // Another swap but this time uwhale to uusd + suite.swap( + creator.clone(), + "uusd".to_string(), + None, + None, + None, + "whale-uluna-uusd".to_string(), + vec![coin( + simulated_return_amount.borrow().u128(), + "uwhale".to_string(), + )], + |result| { + // Find the key with 'offer_amount' and the key with 'return_amount' + // Ensure that the offer amount is 1000 and the return amount is greater than 0 + let mut return_amount = String::new(); + let mut offer_amount = String::new(); + + for event in result.unwrap().events { + if event.ty == "wasm" { + for attribute in event.attributes { + match attribute.key.as_str() { + "return_amount" => return_amount = attribute.value, + "offer_amount" => offer_amount = attribute.value, + _ => {} + } + } + } + } + assert_approx_eq!( + simulated_return_amount.borrow().u128(), + return_amount.parse::().unwrap(), + "0.002" + ); + assert_approx_eq!(1000u128, offer_amount.parse::().unwrap(), "0.003"); + }, + ); + + // And now uusd to uluna + suite.query_reverse_simulation( + "whale-uluna-uusd".to_string(), + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(1000u128), + }, + "uusd".to_string(), + |result| { + *simulated_offer_amount.borrow_mut() = result.unwrap().offer_amount; + }, + ); + // Another swap but this time uusd to uluna + suite.swap( + creator.clone(), + "uluna".to_string(), + None, + None, + None, + "whale-uluna-uusd".to_string(), + vec![coin( + simulated_offer_amount.borrow().u128(), + "uusd".to_string(), + )], + |result| { + let mut return_amount = String::new(); + let mut offer_amount = String::new(); + + for event in result.unwrap().events { + if event.ty == "wasm" { + for attribute in event.attributes { + match attribute.key.as_str() { + "return_amount" => return_amount = attribute.value, + "offer_amount" => offer_amount = attribute.value, + _ => {} + } + } + } + } + assert_approx_eq!( + simulated_offer_amount.borrow().u128(), + offer_amount.parse::().unwrap(), + "0.002" + ); + + assert_approx_eq!(1000u128, return_amount.parse::().unwrap(), "0.003"); + }, + ); + } + + // This test is to ensure that the edge case of providing liquidity with 3 assets + #[test] + fn provide_liquidity_stable_swap_edge_case() { + let mut suite = TestingSuite::default_with_balances(vec![ + coin(1_000_000_001u128, "uwhale".to_string()), + coin(1_000_000_000u128, "uluna".to_string()), + coin(1_000_000_001u128, "uusd".to_string()), + ]); + let creator = suite.creator(); + let _other = suite.senders[1].clone(); + let _unauthorized = suite.senders[2].clone(); + // Asset infos with uwhale and uluna + + let asset_infos = vec![ + "uwhale".to_string(), + "uluna".to_string(), + "uusd".to_string(), + ]; + + // Protocol fee is 0.01% and swap fee is 0.02% and burn fee is 0% + #[cfg(not(feature = "osmosis"))] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::from_ratio(1u128, 1000u128), + }, + swap_fee: Fee { + share: Decimal::from_ratio(1u128, 10_000_u128), + }, + burn_fee: Fee { + share: Decimal::zero(), + }, + extra_fees: vec![], + }; + + #[cfg(feature = "osmosis")] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::from_ratio(1u128, 1000u128), + }, + swap_fee: Fee { + share: Decimal::from_ratio(1u128, 100_00u128), + }, + burn_fee: Fee { + share: Decimal::zero(), + }, + osmosis_fee: Fee { + share: Decimal::from_ratio(1u128, 1000u128), + }, + extra_fees: vec![], + }; + + // Create a pool with 3 assets + suite.instantiate_default().create_pool( + creator.clone(), + asset_infos, + vec![6u8, 6u8, 6u8], + pool_fees, + PoolType::StableSwap { amp: 100 }, + Some("whale-uluna-uusd".to_string()), + vec![coin(1000, "uusd")], + |result| { + result.unwrap(); + }, + ); + + // Adding liquidity with less than the minimum liquidity amount should fail + suite.provide_liquidity( + creator.clone(), + "whale-uluna-uusd".to_string(), + None, + None, + None, + None, + vec![ + Coin { + denom: "uwhale".to_string(), + amount: MINIMUM_LIQUIDITY_AMOUNT + .checked_div(Uint128::new(3u128)) + .unwrap(), + }, + Coin { + denom: "uluna".to_string(), + amount: MINIMUM_LIQUIDITY_AMOUNT + .checked_div(Uint128::new(3u128)) + .unwrap(), + }, + Coin { + denom: "uusd".to_string(), + amount: MINIMUM_LIQUIDITY_AMOUNT + .checked_div(Uint128::new(3u128)) + .unwrap(), + }, + ], + |result| { + assert_eq!( + result.unwrap_err().downcast_ref::(), + Some(&ContractError::InvalidInitialLiquidityAmount( + MINIMUM_LIQUIDITY_AMOUNT + )) + ); + }, + ); + + // Lets try to add liquidity with the correct amount (1_000_000 of each asset) + suite.provide_liquidity( + creator.clone(), + "whale-uluna-uusd".to_string(), + None, + None, + None, + None, + vec![ + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(1_000_000u128), + }, + Coin { + denom: "uusd".to_string(), + amount: Uint128::from(1_000_000u128), + }, + ], + |result| { + // Ensure we got 999000 in the response which is 1mil less the initial liquidity amount + for event in result.unwrap().events { + for attribute in event.attributes { + if attribute.key == "share" { + assert_approx_eq!( + attribute.value.parse::().unwrap(), + 1_000_000u128 * 3, + "0.002" + ); + } + } + } + }, + ); + + let simulated_return_amount = RefCell::new(Uint128::zero()); + suite.query_simulation( + "whale-uluna-uusd".to_string(), + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000u128), + }, + "uluna".to_string(), + |result| { + *simulated_return_amount.borrow_mut() = result.unwrap().return_amount; + }, + ); + + // Now lets try a swap + suite.swap( + creator.clone(), + "uluna".to_string(), + None, + None, + None, + "whale-uluna-uusd".to_string(), + vec![coin(1_000u128, "uwhale".to_string())], + |result| { + // Find the key with 'offer_amount' and the key with 'return_amount' + // Ensure that the offer amount is 1000 and the return amount is greater than 0 + let mut return_amount = String::new(); + let mut offer_amount = String::new(); + + for event in result.unwrap().events { + if event.ty == "wasm" { + for attribute in event.attributes { + match attribute.key.as_str() { + "return_amount" => return_amount = attribute.value, + "offer_amount" => offer_amount = attribute.value, + _ => {} + } + } + } + } + // Because the Pool was created and 1_000_000 of each token has been provided as liquidity + // Assuming no fees we should expect a small swap of 1000 to result in not too much slippage + // Expect 1000 give or take 0.002 difference + // Once fees are added and being deducted properly only the "0.002" should be changed. + assert_approx_eq!( + offer_amount.parse::().unwrap(), + return_amount.parse::().unwrap(), + "0.002" + ); + assert_approx_eq!( + simulated_return_amount.borrow().u128(), + return_amount.parse::().unwrap(), + "0.002" + ); + }, + ); + } } mod multiple_pools { diff --git a/contracts/liquidity_hub/pool-network/stableswap_3pool/Cargo.toml b/contracts/liquidity_hub/pool-network/stableswap_3pool/Cargo.toml index ff1b9f752..261cde330 100644 --- a/contracts/liquidity_hub/pool-network/stableswap_3pool/Cargo.toml +++ b/contracts/liquidity_hub/pool-network/stableswap_3pool/Cargo.toml @@ -47,4 +47,4 @@ cosmwasm-schema.workspace = true [dev-dependencies] proptest = "1.0.0" rand = "0.8.4" -stable-swap-sim = { path = "./sim", version = "^0.1" } +stable-swap-sim1 = { path = "./sim", version = "^0.1" } diff --git a/contracts/liquidity_hub/pool-network/stableswap_3pool/sim/Cargo.toml b/contracts/liquidity_hub/pool-network/stableswap_3pool/sim/Cargo.toml index e01fbc983..25ba31e2d 100644 --- a/contracts/liquidity_hub/pool-network/stableswap_3pool/sim/Cargo.toml +++ b/contracts/liquidity_hub/pool-network/stableswap_3pool/sim/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "stable-swap-sim" +name = "stable-swap-sim1" version = "0.1.0" description = "Simulations of the StableSwap invariant compared to Curve's reference implementation." authors = ["Paul Stelzig paul@irulast.com>"] edition = "2021" [lib] -name = "sim" +name = "sim1" [dependencies] pyo3 = { version = "0.17.3", features = ["auto-initialize"] } diff --git a/contracts/liquidity_hub/pool-network/stableswap_3pool/src/stableswap_math/curve.rs b/contracts/liquidity_hub/pool-network/stableswap_3pool/src/stableswap_math/curve.rs index 3aec253e8..f4b8308bd 100644 --- a/contracts/liquidity_hub/pool-network/stableswap_3pool/src/stableswap_math/curve.rs +++ b/contracts/liquidity_hub/pool-network/stableswap_3pool/src/stableswap_math/curve.rs @@ -167,6 +167,16 @@ impl StableSwap { let amount_b_times_coins = amount_b.checked_mul(N_COINS.into()).unwrap(); let amount_c_times_coins = amount_c.checked_mul(N_COINS.into()).unwrap(); + if amount_a_times_coins == Uint128::zero() + || amount_b_times_coins == Uint128::zero() + || amount_c_times_coins == Uint128::zero() + { + println!( + "amount_a_times_coins: {}, amount_b_times_coins: {}, amount_c_times_coins: {}", + amount_a_times_coins, amount_b_times_coins, amount_c_times_coins + ); + } + // Newton's method to approximate D let mut d_prev: Uint256; let mut d: Uint256 = sum_x.into(); @@ -217,6 +227,7 @@ impl StableSwap { ) -> Option { // Initial invariant let d_0 = self.compute_d(swap_amount_a, swap_amount_b, swap_amount_c)?; + let new_balances = [ swap_amount_a.checked_add(deposit_amount_a).unwrap(), swap_amount_b.checked_add(deposit_amount_b).unwrap(), @@ -383,7 +394,7 @@ mod tests { use super::*; use proptest::prelude::*; use rand::Rng; - use sim::Model; + use sim1::Model; use std::cmp; /// Timestamp at 0 @@ -776,6 +787,7 @@ mod tests { let start_ramp_ts = cmp::max(0, current_ts - MIN_RAMP_DURATION); let stop_ramp_ts = cmp::min(u64::MAX, current_ts + MIN_RAMP_DURATION); let invariant = StableSwap::new(amp_factor, amp_factor, current_ts, start_ramp_ts, stop_ramp_ts); + let d0 = invariant.compute_d(Uint128::new(swap_token_a_amount), Uint128::new(swap_token_b_amount), Uint128::new(swap_token_c_amount)).unwrap(); let mint_amount = invariant.compute_mint_amount_for_deposit( @@ -793,6 +805,7 @@ mod tests { let new_swap_token_b_amount = swap_token_b_amount + deposit_amount_b; let new_swap_token_c_amount = swap_token_c_amount + deposit_amount_c; let new_pool_token_supply = pool_token_supply + mint_amount.unwrap().u128(); + let d1 = invariant.compute_d(Uint128::new(new_swap_token_a_amount), Uint128::new(new_swap_token_b_amount), Uint128::new(new_swap_token_c_amount)).unwrap(); assert!(d0 < d1); diff --git a/xtask/src/main.rs b/xtask/src/main.rs index ab04e749d..dc4229c1b 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -97,6 +97,7 @@ pub mod tasks { member.name != "fee-distributor-mock" && member.name != "stableswap-3pool" && member.name != "stable-swap-sim" + && member.name != "stable-swap-sim1" }); for contract in contracts {