From eddae9b693b00f32c0ba59e3180c9f76b8e05793 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Fri, 26 Apr 2024 17:52:17 +0100 Subject: [PATCH 1/7] feat: single-side liquidity provision --- .../liquidity_hub/pool-manager/src/error.rs | 3 + .../pool-manager/src/liquidity/commands.rs | 109 +++-- .../src/tests/integration_tests.rs | 451 +++++++++++++++++- .../pool-manager/src/tests/suite.rs | 28 +- 4 files changed, 538 insertions(+), 53 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/src/error.rs b/contracts/liquidity_hub/pool-manager/src/error.rs index d6e8a4b9..feb1c23d 100644 --- a/contracts/liquidity_hub/pool-manager/src/error.rs +++ b/contracts/liquidity_hub/pool-manager/src/error.rs @@ -65,6 +65,9 @@ pub enum ContractError { #[error("{asset} is invalid")] InvalidAsset { asset: String }, + #[error("Trying to provide liquidity without any assets")] + EmptyAssets, + #[error("Pair does not exist")] UnExistingPair {}, diff --git a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs index 6f388568..4bf14d00 100644 --- a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs @@ -1,5 +1,6 @@ use cosmwasm_std::{ - coins, wasm_execute, BankMsg, Coin, CosmosMsg, DepsMut, Env, MessageInfo, Response, + coins, ensure, wasm_execute, BankMsg, Coin, CosmosMsg, DepsMut, Env, MessageInfo, Response, + StdError, }; use white_whale_std::pool_network::asset::PairType; @@ -14,14 +15,13 @@ use crate::{ // After writing create_pair 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 cosmwasm_std::{Decimal, OverflowError, Uint128}; +use white_whale_std::coin::aggregate_coins; use white_whale_std::pool_network::{ asset::{get_total_share, MINIMUM_LIQUIDITY_AMOUNT}, U256, }; pub const MAX_ASSETS_PER_POOL: usize = 4; -// todo allow providing liquidity with a single asset - #[allow(clippy::too_many_arguments)] pub fn provide_liquidity( deps: DepsMut, @@ -45,18 +45,40 @@ pub fn provide_liquidity( let mut pair = get_pair_by_identifier(&deps.as_ref(), &pair_identifier)?; let mut pool_assets = pair.assets.clone(); - let mut assets = info.funds.clone(); + let deposits = aggregate_coins(info.funds.clone())?; let mut messages: Vec = vec![]; - //TODO verify that the assets sent in info match the ones from the pool!!! - - for (i, pool) in assets.iter_mut().enumerate() { - // Increment the pool asset amount by the amount sent - pool_assets[i].amount = pool_assets[i].amount.checked_add(pool.amount)?; + ensure!(!deposits.is_empty(), ContractError::EmptyAssets); + + // verify that the assets sent match the ones from the pool + ensure!( + deposits.iter().all(|asset| pool_assets + .iter() + .any(|pool_asset| pool_asset.denom == asset.denom)), + ContractError::AssetMismatch {} + ); + + // check if the user is providing liquidity with a single asset + let is_single_asset_provision = deposits.len() == 1; + + for asset in deposits.iter() { + let asset_denom = &asset.denom; + if let Some(pool_asset_index) = pool_assets + .iter() + .position(|pool_asset| pool_asset.denom == *asset_denom) + { + // Increment the pool asset amount by the amount sent + pool_assets[pool_asset_index].amount = pool_assets[pool_asset_index] + .amount + .checked_add(asset.amount)?; + } else { + return Err(ContractError::AssetMismatch {}); + } } - // After totting up the pool assets we need to check if any of them are zero - if pool_assets.iter().any(|deposit| deposit.amount.is_zero()) { + // After totting up the pool assets we need to check if any of them are zero. + // The very first deposit cannot be done with a single asset + if pool_assets.iter().any(|deposit| deposit.amount.is_zero()) && is_single_asset_provision { return Err(ContractError::InvalidZeroAmount {}); } @@ -70,7 +92,6 @@ pub fn provide_liquidity( if total_share == Uint128::zero() { // Make sure at least MINIMUM_LIQUIDITY_AMOUNT is deposited to mitigate the risk of the first // depositor preventing small liquidity providers from joining the pool - let share = Uint128::new( (U256::from(pool_assets[0].amount.u128()) .checked_mul(U256::from(pool_assets[1].amount.u128())) @@ -82,6 +103,7 @@ pub fn provide_liquidity( .map_err(|_| { ContractError::InvalidInitialLiquidityAmount(MINIMUM_LIQUIDITY_AMOUNT) })?; + // share should be above zero after subtracting the MINIMUM_LIQUIDITY_AMOUNT if share.is_zero() { return Err(ContractError::InvalidInitialLiquidityAmount( @@ -98,19 +120,19 @@ pub fn provide_liquidity( share } else { - let share = { - let numerator = U256::from(pool_assets[0].amount.u128()) - .checked_mul(U256::from(total_share.u128())) - .ok_or::(ContractError::LiquidityShareComputation {})?; - - let denominator = U256::from(pool_assets[0].amount.u128()); - - let result = numerator - .checked_div(denominator) - .ok_or::(ContractError::LiquidityShareComputation {})?; - - Uint128::from(result.as_u128()) - }; + // let share = { + // let numerator = U256::from(pool_assets[0].amount.u128()) + // .checked_mul(U256::from(total_share.u128())) + // .ok_or::(ContractError::LiquidityShareComputation {})?; + // + // let denominator = U256::from(pool_assets[0].amount.u128()); + // + // let result = numerator + // .checked_div(denominator) + // .ok_or::(ContractError::LiquidityShareComputation {})?; + // + // Uint128::from(result.as_u128()) + // }; let amount = std::cmp::min( pool_assets[0] @@ -121,20 +143,31 @@ pub fn provide_liquidity( .multiply_ratio(total_share, pool_assets[1].amount), ); - let deps_as = [pool_assets[0].amount, pool_assets[1].amount]; - let pools_as = [pool_assets[0].clone(), pool_assets[1].clone()]; - - // assert slippage tolerance - helpers::assert_slippage_tolerance( - &slippage_tolerance, - &deps_as, - &pools_as, - pair.pair_type.clone(), - amount, - total_share, - )?; + //todo i think we need to skip slippage_tolerance assertion in this case + if !is_single_asset_provision { + 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, + pair.pair_type.clone(), + amount, + total_share, + )?; + } - share + amount } } PairType::StableSwap { amp: _ } => { 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 90954161..81ddb679 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs @@ -114,7 +114,7 @@ fn deposit_and_withdraw_sanity_check() { // creator should have 999_000 LP shares (1M - MINIMUM_LIQUIDITY_AMOUNT) .query_all_balances(creator.to_string(), |result| { let balances = result.unwrap(); - println!("{:?}", balances); + assert!(balances.iter().any(|coin| { coin.denom == lp_denom && coin.amount == Uint128::from(999_000u128) })); @@ -321,7 +321,7 @@ mod pair_creation_failures { mod router { use cosmwasm_std::{Event, StdError}; - use std::error::Error; + use white_whale_std::pool_manager::{SwapRoute, SwapRouteCreatorResponse}; use super::*; @@ -1347,7 +1347,7 @@ mod router { result.unwrap_err().downcast_ref::(), Some(&ContractError::SwapRouteAlreadyExists { offer_asset: "uwhale".to_string(), - ask_asset: "uusd".to_string() + ask_asset: "uusd".to_string(), }) ); }); @@ -1554,7 +1554,7 @@ mod router { result.unwrap_err().downcast_ref::(), Some(&ContractError::NoSwapRouteForAssets { offer_asset: "uwhale".to_string(), - ask_asset: "uusd".to_string() + ask_asset: "uusd".to_string(), }) ); }); @@ -2619,7 +2619,7 @@ mod locking_lp { assert_eq!(positions.len(), 1); assert_eq!(positions[0], Position { identifier: "1".to_string(), - lp_asset: Coin{ denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(999_000u128) }, + lp_asset: Coin { denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(999_000u128) }, unlocking_duration: 86_400, open: true, expiring_at: None, @@ -2669,7 +2669,7 @@ mod locking_lp { assert_eq!(positions.len(), 2); assert_eq!(positions[0], Position { identifier: "1".to_string(), - lp_asset: Coin{ denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(999_000u128) }, + lp_asset: Coin { denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(999_000u128) }, unlocking_duration: 86_400, open: true, expiring_at: None, @@ -2677,7 +2677,7 @@ mod locking_lp { }); assert_eq!(positions[1], Position { identifier: "2".to_string(), - lp_asset: Coin{ denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(1_000_000u128) }, + lp_asset: Coin { denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(1_000_000u128) }, unlocking_duration: 200_000, open: true, expiring_at: None, @@ -2806,7 +2806,7 @@ mod locking_lp { assert_eq!(positions.len(), 1); assert_eq!(positions[0], Position { identifier: "incentive_identifier".to_string(), - lp_asset: Coin{ denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(999_000u128) }, + lp_asset: Coin { denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(999_000u128) }, unlocking_duration: 86_400, open: true, expiring_at: None, @@ -2857,7 +2857,7 @@ mod locking_lp { assert_eq!(positions.len(), 1); assert_eq!(positions[0], Position { identifier: "incentive_identifier".to_string(), - lp_asset: Coin{ denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(1_999_000u128) }, + lp_asset: Coin { denom: "factory/migaloo1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqqhavvl/uwhale-uluna.pool.whale-uluna.uLP".to_string(), amount: Uint128::from(1_999_000u128) }, unlocking_duration: 86_400, open: true, expiring_at: None, @@ -2866,3 +2866,436 @@ mod locking_lp { }); } } + +mod provide_liquidity { + use cosmwasm_std::{coin, Coin, Decimal, Uint128}; + + use white_whale_std::fee::{Fee, PoolFee}; + use white_whale_std::pool_network::asset::MINIMUM_LIQUIDITY_AMOUNT; + + use crate::tests::suite::TestingSuite; + use crate::ContractError; + + #[test] + fn provide_liquidity_with_single_asset() { + let mut suite = TestingSuite::default_with_balances(vec![ + coin(10_000_000u128, "uwhale".to_string()), + coin(10_000_000u128, "uluna".to_string()), + coin(10_000_000u128, "uosmo".to_string()), + coin(10_000u128, "uusd".to_string()), + ]); + let creator = suite.creator(); + let other = suite.senders[1].clone(); + let _unauthorized = suite.senders[2].clone(); + + // Asset denoms with uwhale and uluna + let asset_denoms = vec!["uwhale".to_string(), "uluna".to_string()]; + + // Default Pool fees white_whale_std::pool_network::pair::PoolFee + #[cfg(not(feature = "osmosis"))] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::zero(), + }, + swap_fee: Fee { + share: Decimal::zero(), + }, + burn_fee: Fee { + share: Decimal::zero(), + }, + extra_fees: vec![], + }; + + #[cfg(feature = "osmosis")] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::zero(), + }, + swap_fee: Fee { + share: Decimal::zero(), + }, + burn_fee: Fee { + share: Decimal::zero(), + }, + osmosis_fee: Fee { + share: Decimal::zero(), + }, + extra_fees: vec![], + }; + + // Create a pair + suite.instantiate_default().create_pair( + creator.clone(), + asset_denoms, + vec![6u8, 6u8], + pool_fees, + white_whale_std::pool_network::asset::PairType::ConstantProduct, + Some("whale-uluna".to_string()), + vec![coin(1000, "uusd")], + |result| { + result.unwrap(); + }, + ); + + let contract_addr = suite.pool_manager_addr.clone(); + let incentive_manager_addr = suite.incentive_manager_addr.clone(); + let lp_denom = suite.get_lp_denom("whale-uluna".to_string()); + + // Lets try to add liquidity + suite + .provide_liquidity( + creator.clone(), + "whale-uluna".to_string(), + None, + None, + vec![], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::EmptyAssets => {} + _ => panic!("Wrong error type, should return ContractError::EmptyAssets"), + } + }, + ) + .provide_liquidity( + creator.clone(), + "whale-uluna".to_string(), + None, + None, + vec![Coin { + denom: "uosmo".to_string(), + amount: Uint128::from(1_000_000u128), + }], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::AssetMismatch { .. } => {} + _ => panic!("Wrong error type, should return ContractError::AssetMismatch"), + } + }, + ) + .provide_liquidity( + creator.clone(), + "whale-uluna".to_string(), + None, + None, + vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000_000u128), + }], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::InvalidZeroAmount {} => {} + _ => panic!( + "Wrong error type, should return ContractError::InvalidZeroAmount" + ), + } + }, + ) + .provide_liquidity( + creator.clone(), + "whale-uluna".to_string(), + None, + None, + vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000_000u128), + }], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + // it was trying to provide single-side liquidity with no funds in the pool + match err { + ContractError::InvalidZeroAmount {} => {} + _ => panic!( + "Wrong error type, should return ContractError::InvalidZeroAmount" + ), + } + }, + ); + + // let's provide liquidity with two assets + suite + .provide_liquidity( + creator.clone(), + "whale-uluna".to_string(), + None, + None, + vec![ + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000_000u128), + }, + Coin { + denom: "uosmo".to_string(), + amount: Uint128::from(1_000_000u128), + }, + ], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + // it was trying to provide single-side liquidity with no funds in the pool + match err { + ContractError::AssetMismatch {} => {} + _ => panic!("Wrong error type, should return ContractError::AssetMismatch"), + } + }, + ) + .provide_liquidity( + creator.clone(), + "whale-uluna".to_string(), + None, + None, + vec![ + Coin { + denom: "uosmo".to_string(), + amount: Uint128::from(1_000_000u128), + }, + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000_000u128), + }, + ], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + // it was trying to provide single-side liquidity with no funds in the pool + match err { + ContractError::AssetMismatch {} => {} + _ => panic!("Wrong error type, should return ContractError::AssetMismatch"), + } + }, + ) + .provide_liquidity( + creator.clone(), + "whale-uluna".to_string(), + 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), + }, + ], + |result| { + result.unwrap(); + }, + ) + .query_all_balances(creator.to_string(), |result| { + let balances = result.unwrap(); + + assert!(balances.iter().any(|coin| { + coin.denom == lp_denom && coin.amount == Uint128::from(999_000u128) + })); + }) + // contract should have 1_000 LP shares (MINIMUM_LIQUIDITY_AMOUNT) + .query_all_balances(contract_addr.to_string(), |result| { + let balances = result.unwrap(); + // check that balances has 999_000 factory/migaloo1wug8sewp6cedgkmrmvhl3lf3tulagm9hnvy8p0rppz9yjw0g4wtqvk723g/uwhale-uluna.pool.whale-uluna.uLP + assert!(balances.iter().any(|coin| { + coin.denom == lp_denom.clone() && coin.amount == MINIMUM_LIQUIDITY_AMOUNT + })); + }); + + // now let's provide liquidity with a single asset + suite + .provide_liquidity( + other.clone(), + "whale-uluna".to_string(), + None, + None, + vec![ + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000_000u128), + }, + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000_000u128), + }, + ], + |result| { + result.unwrap(); + }, + ) + .query_all_balances(other.to_string(), |result| { + let balances = result.unwrap(); + + assert!(balances.iter().any(|coin| { + coin.denom == lp_denom && coin.amount == Uint128::from(1_000_000u128) + })); + }) + .query_all_balances(contract_addr.to_string(), |result| { + let balances = result.unwrap(); + // check that balances has 999_000 factory/migaloo1wug8sewp6cedgkmrmvhl3lf3tulagm9hnvy8p0rppz9yjw0g4wtqvk723g/uwhale-uluna.pool.whale-uluna.uLP + assert!(balances.iter().any(|coin| { + coin.denom == lp_denom.clone() && coin.amount == MINIMUM_LIQUIDITY_AMOUNT + })); + }); + + suite + .query_lp_supply("whale-uluna".to_string(), |res| { + // total amount of LP tokens issued should be 2_000_000 = 999_000 to the first LP, + // 1_000 to the contract, and 1_000_000 to the second, single-side LP + assert_eq!(res.unwrap().amount, Uint128::from(2_000_000u128)); + }) + .query_pair_info("whale-uluna".to_string(), |res| { + let response = res.unwrap(); + + let whale = response + .pair_info + .assets + .iter() + .find(|coin| coin.denom == "uwhale".to_string()) + .unwrap(); + let luna = response + .pair_info + .assets + .iter() + .find(|coin| coin.denom == "uluna".to_string()) + .unwrap(); + + assert_eq!(whale.amount, Uint128::from(3_000_000u128)); + assert_eq!(luna.amount, Uint128::from(1_000_000u128)); + }); + + // let's withdraw both LPs + suite + .query_all_balances(creator.clone().to_string(), |result| { + let balances = result.unwrap(); + assert_eq!( + balances, + vec![ + Coin { + denom: lp_denom.clone(), + amount: Uint128::from(999_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(9_000_000u128), + }, + Coin { + denom: "uosmo".to_string(), + amount: Uint128::from(10_000_000u128), + }, + Coin { + denom: "uusd".to_string(), + amount: Uint128::from(9_000u128), + }, + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(9_000_000u128), + }, + ] + ); + }) + .withdraw_liquidity( + creator.clone(), + "whale-uluna".to_string(), + vec![Coin { + denom: lp_denom.clone(), + amount: Uint128::from(999_000u128), + }], + |result| { + result.unwrap(); + }, + ) + .query_all_balances(creator.clone().to_string(), |result| { + let balances = result.unwrap(); + assert_eq!( + balances, + vec![ + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(9_499_500u128), + }, + Coin { + denom: "uosmo".to_string(), + amount: Uint128::from(10_000_000u128), + }, + Coin { + denom: "uusd".to_string(), + amount: Uint128::from(9_000u128), + }, + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(10_498_500u128), + }, + ] + ); + }); + + suite + .query_all_balances(other.clone().to_string(), |result| { + let balances = result.unwrap(); + assert_eq!( + balances, + vec![ + Coin { + denom: lp_denom.clone(), + amount: Uint128::from(1_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(10_000_000u128), + }, + Coin { + denom: "uosmo".to_string(), + amount: Uint128::from(10_000_000u128), + }, + Coin { + denom: "uusd".to_string(), + amount: Uint128::from(10_000u128), + }, + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(8_000_000u128), + }, + ] + ); + }) + .withdraw_liquidity( + other.clone(), + "whale-uluna".to_string(), + vec![Coin { + denom: lp_denom.clone(), + amount: Uint128::from(1_000_000u128), + }], + |result| { + result.unwrap(); + }, + ) + .query_all_balances(other.clone().to_string(), |result| { + let balances = result.unwrap(); + assert_eq!( + balances, + vec![ + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(10_499_999u128), + }, + Coin { + denom: "uosmo".to_string(), + amount: Uint128::from(10_000_000u128), + }, + Coin { + denom: "uusd".to_string(), + amount: Uint128::from(10_000u128), + }, + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(9_499_999u128), + }, + ] + ); + }); + } +} diff --git a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs index 236167c4..232a0b93 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs @@ -1,9 +1,6 @@ use cosmwasm_std::testing::MockStorage; -use white_whale_std::pool_manager::{ - Config, FeatureToggle, PairInfoResponse, ReverseSimulateSwapOperationsResponse, - SimulateSwapOperationsResponse, SwapOperation, SwapRouteCreatorResponse, SwapRouteResponse, - SwapRoutesResponse, -}; +use std::cell::RefCell; +use white_whale_std::pool_manager::{Config, FeatureToggle, PairInfoResponse, ReverseSimulateSwapOperationsResponse, ReverseSimulationResponse, SimulateSwapOperationsResponse, SimulationResponse, SwapOperation, SwapRouteCreatorResponse, SwapRouteResponse, SwapRoutesResponse}; use white_whale_std::pool_manager::{InstantiateMsg, PairInfo}; use cosmwasm_std::{coin, Addr, Coin, Decimal, Empty, StdResult, Timestamp, Uint128, Uint64}; @@ -17,7 +14,6 @@ use white_whale_std::epoch_manager::epoch_manager::{Epoch, EpochConfig}; use white_whale_std::fee::PoolFee; use white_whale_std::incentive_manager::PositionsResponse; use white_whale_std::lp_common::LP_SYMBOL; -use white_whale_std::pool_manager::{ReverseSimulationResponse, SimulationResponse}; use white_whale_std::pool_network::asset::{AssetInfo, PairType}; use white_whale_testing::multi_test::stargate_mock::StargateMock; @@ -767,4 +763,24 @@ impl TestingSuite { self } + + #[track_caller] + pub(crate) fn query_lp_supply( + &mut self, + identifier: String, + result: impl Fn(StdResult), + ) -> &mut Self { + let lp_denom = RefCell::new("".to_string()); + + let pair = self.query_pair_info(identifier.clone(), |res| { + let response = res.unwrap(); + *lp_denom.borrow_mut() = response.pair_info.lp_denom.clone(); + }); + + let supply_response = self.app.wrap().query_supply(lp_denom.into_inner()); + + result(supply_response); + + self + } } From f9820080b19174980202cf93b5a63bed1e609c45 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Wed, 1 May 2024 10:33:20 +0100 Subject: [PATCH 2/7] chore: make swap before providing liqudity with a single asset --- .../pool-manager/src/contract.rs | 46 ++++- .../liquidity_hub/pool-manager/src/error.rs | 6 + .../liquidity_hub/pool-manager/src/helpers.rs | 57 +++++- .../pool-manager/src/liquidity/commands.rs | 188 ++++++++++++------ .../pool-manager/src/manager/commands.rs | 2 +- .../liquidity_hub/pool-manager/src/queries.rs | 9 +- .../liquidity_hub/pool-manager/src/state.rs | 32 ++- .../pool-manager/src/swap/commands.rs | 49 +++-- .../pool-manager/src/swap/perform_swap.rs | 54 +++-- .../src/tests/integration_tests.rs | 115 +++++------ .../pool-manager/src/tests/suite.rs | 12 +- .../pool-manager/src/tests/unit_tests/swap.rs | 1 - packages/white-whale-std/src/pool_manager.rs | 2 + 13 files changed, 407 insertions(+), 166 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/src/contract.rs b/contracts/liquidity_hub/pool-manager/src/contract.rs index b002893e..d7d2ddca 100644 --- a/contracts/liquidity_hub/pool-manager/src/contract.rs +++ b/contracts/liquidity_hub/pool-manager/src/contract.rs @@ -1,13 +1,17 @@ use crate::error::ContractError; -use crate::helpers::{reverse_simulate_swap_operations, simulate_swap_operations}; +use crate::helpers::{reverse_simulate_swap_operations, simulate_swap_operations, validate_asset_balance}; use crate::queries::{get_pair, get_swap_route, get_swap_route_creator, get_swap_routes}; use crate::router::commands::{add_swap_routes, remove_swap_routes}; -use crate::state::{Config, MANAGER_CONFIG, PAIR_COUNTER}; +use crate::state::{ + Config, SingleSideLiquidityProvisionBuffer, MANAGER_CONFIG, PAIRS, PAIR_COUNTER, + TMP_SINGLE_SIDE_LIQUIDITY_PROVISION, +}; use crate::{liquidity, manager, queries, router, swap}; #[cfg(not(feature = "library"))] use cosmwasm_std::{ entry_point, to_json_binary, Addr, Api, Binary, Deps, DepsMut, Env, MessageInfo, Response, }; +use cosmwasm_std::{wasm_execute, Reply, StdError}; use cw2::set_contract_version; use semver::Version; use white_whale_std::pool_manager::{ @@ -17,6 +21,7 @@ use white_whale_std::pool_manager::{ // version info for migration info const CONTRACT_NAME: &str = "crates.io:ww-pool-manager"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const SINGLE_SIDE_LIQUIDITY_PROVISION_REPLY_ID: u64 = 1; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -45,6 +50,41 @@ pub fn instantiate( Ok(Response::default()) } +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result { + match msg.id { + SINGLE_SIDE_LIQUIDITY_PROVISION_REPLY_ID => { + let SingleSideLiquidityProvisionBuffer { + receiver, + expected_offer_asset_balance_in_contract, + expected_ask_asset_balance_in_contract, + offer_asset_half, + expected_ask_asset, + liquidity_provision_data, + } = TMP_SINGLE_SIDE_LIQUIDITY_PROVISION.load(deps.storage)?; + + validate_asset_balance(&deps, &env, &expected_offer_asset_balance_in_contract)?; + validate_asset_balance(&deps, &env, &expected_ask_asset_balance_in_contract)?; + + TMP_SINGLE_SIDE_LIQUIDITY_PROVISION.remove(deps.storage); + + Ok(Response::default().add_message(wasm_execute( + env.contract.address.into_string(), + &ExecuteMsg::ProvideLiquidity { + slippage_tolerance: liquidity_provision_data.slippage_tolerance, + max_spread: liquidity_provision_data.max_spread, + receiver: Some(receiver), + pair_identifier: liquidity_provision_data.pair_identifier, + unlocking_duration: liquidity_provision_data.unlocking_duration, + lock_position_identifier: liquidity_provision_data.lock_position_identifier, + }, + vec![offer_asset_half, expected_ask_asset], + )?)) + } + _ => Err(StdError::generic_err("reply id not found").into()), + } +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( deps: DepsMut, @@ -70,6 +110,7 @@ pub fn execute( pair_identifier, ), ExecuteMsg::ProvideLiquidity { + max_spread, slippage_tolerance, receiver, pair_identifier, @@ -80,6 +121,7 @@ pub fn execute( env, info, slippage_tolerance, + max_spread, receiver, pair_identifier, unlocking_duration, diff --git a/contracts/liquidity_hub/pool-manager/src/error.rs b/contracts/liquidity_hub/pool-manager/src/error.rs index feb1c23d..a2580ebf 100644 --- a/contracts/liquidity_hub/pool-manager/src/error.rs +++ b/contracts/liquidity_hub/pool-manager/src/error.rs @@ -68,6 +68,12 @@ pub enum ContractError { #[error("Trying to provide liquidity without any assets")] EmptyAssets, + #[error("Invalid single side liquidity provision swap, expected {expected} got {actual}")] + InvalidSingleSideLiquidityProvisionSwap { expected: Uint128, actual: Uint128 }, + + #[error("Cannot provide single-side liquidity when the pool is empty")] + EmptyPoolForSingleSideLiquidityProvision, + #[error("Pair does not exist")] UnExistingPair {}, diff --git a/contracts/liquidity_hub/pool-manager/src/helpers.rs b/contracts/liquidity_hub/pool-manager/src/helpers.rs index 83c0921d..00fc01e1 100644 --- a/contracts/liquidity_hub/pool-manager/src/helpers.rs +++ b/contracts/liquidity_hub/pool-manager/src/helpers.rs @@ -2,12 +2,14 @@ use std::ops::Mul; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - coin, Addr, Coin, Decimal, Decimal256, Deps, 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; -use white_whale_std::pool_manager::{SimulateSwapOperationsResponse, SwapOperation}; +use white_whale_std::pool_manager::{ + SimulateSwapOperationsResponse, SimulationResponse, SwapOperation, +}; use white_whale_std::pool_network::asset::{Asset, AssetInfo, PairType}; use crate::error::ContractError; @@ -171,6 +173,9 @@ pub fn compute_swap( let protocol_fee_amount: Uint256 = pool_fees.protocol_fee.compute(return_amount); let burn_fee_amount: Uint256 = pool_fees.burn_fee.compute(return_amount); + //todo compute the extra fees + //let extra_fees_amount: Uint256 = pool_fees.extra_fees.compute(return_amount); + // swap and protocol fee will be absorbed by the pool. Burn fee amount will be burned on a subsequent msg. #[cfg(not(feature = "osmosis"))] { @@ -590,3 +595,49 @@ pub fn reverse_simulate_swap_operations( Ok(SimulateSwapOperationsResponse { amount }) } + +/// Validates the amounts after a single side liquidity provision swap are correct. +pub fn validate_asset_balance( + deps: &DepsMut, + env: &Env, + expected_balance: &Coin, +) -> Result<(), ContractError> { + let new_asset_balance = deps + .querier + .query_balance(&env.contract.address, expected_balance.denom.to_owned())?; + + ensure!( + expected_balance == &new_asset_balance, + ContractError::InvalidSingleSideLiquidityProvisionSwap { + expected: expected_balance.amount, + actual: new_asset_balance.amount + } + ); + + Ok(()) +} + +/// Aggregates the fees from a simulation response that go out of the contract, i.e. protocol fee, burn fee +/// and osmosis fee, if applicable. Doesn't know about the denom, just the amount. +pub fn aggregate_outgoing_fees( + simulation_response: &SimulationResponse, +) -> Result { + let fees = { + #[cfg(not(feature = "osmosis"))] + { + simulation_response + .protocol_fee_amount + .checked_add(simulation_response.burn_fee_amount)? + } + + #[cfg(feature = "osmosis")] + { + simulation_response + .protocol_fee_amount + .checked_add(simulation_response.burn_fee_amount)? + .checked_add(simulation_response.osmosis_fee_amount)? + } + }; + + Ok(fees) +} diff --git a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs index 4bf14d00..f0e0baff 100644 --- a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs @@ -1,8 +1,16 @@ use cosmwasm_std::{ - coins, ensure, wasm_execute, BankMsg, Coin, CosmosMsg, DepsMut, Env, MessageInfo, Response, - StdError, + coin, coins, ensure, wasm_execute, BankMsg, Coin, CosmosMsg, DepsMut, Env, MessageInfo, + Response, StdError, SubMsg, }; +use cosmwasm_std::{Decimal, OverflowError, Uint128}; + +use white_whale_std::coin::aggregate_coins; +use white_whale_std::pool_manager::ExecuteMsg; use white_whale_std::pool_network::asset::PairType; +use white_whale_std::pool_network::{ + asset::{get_total_share, MINIMUM_LIQUIDITY_AMOUNT}, + U256, +}; use crate::{ helpers::{self}, @@ -14,12 +22,13 @@ use crate::{ }; // After writing create_pair 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 cosmwasm_std::{Decimal, OverflowError, Uint128}; -use white_whale_std::coin::aggregate_coins; -use white_whale_std::pool_network::{ - asset::{get_total_share, MINIMUM_LIQUIDITY_AMOUNT}, - U256, +use crate::contract::SINGLE_SIDE_LIQUIDITY_PROVISION_REPLY_ID; +use crate::helpers::aggregate_outgoing_fees; +use crate::queries::query_simulation; +use crate::state::{ + LiquidityProvisionData, SingleSideLiquidityProvisionBuffer, TMP_SINGLE_SIDE_LIQUIDITY_PROVISION, }; + pub const MAX_ASSETS_PER_POOL: usize = 4; #[allow(clippy::too_many_arguments)] @@ -28,6 +37,7 @@ pub fn provide_liquidity( env: Env, info: MessageInfo, slippage_tolerance: Option, + max_spread: Option, receiver: Option, pair_identifier: String, unlocking_duration: Option, @@ -35,18 +45,16 @@ pub fn provide_liquidity( ) -> Result { let config = MANAGER_CONFIG.load(deps.storage)?; // check if the deposit feature is enabled - if !config.feature_toggle.deposits_enabled { - return Err(ContractError::OperationDisabled( - "provide_liquidity".to_string(), - )); - } + ensure!( + config.feature_toggle.deposits_enabled, + ContractError::OperationDisabled("provide_liquidity".to_string()) + ); // Get the pair by the pair_identifier let mut pair = get_pair_by_identifier(&deps.as_ref(), &pair_identifier)?; let mut pool_assets = pair.assets.clone(); let deposits = aggregate_coins(info.funds.clone())?; - let mut messages: Vec = vec![]; ensure!(!deposits.is_empty(), ContractError::EmptyAssets); @@ -58,8 +66,93 @@ pub fn provide_liquidity( ContractError::AssetMismatch {} ); + let receiver = receiver.unwrap_or_else(|| info.sender.to_string()); // check if the user is providing liquidity with a single asset - let is_single_asset_provision = deposits.len() == 1; + let is_single_asset_provision = deposits.len() == 1usize; + + if is_single_asset_provision { + //todo maybe put all the code within this block in a function?? + ensure!( + !pool_assets.iter().any(|asset| asset.amount.is_zero()), + ContractError::EmptyPoolForSingleSideLiquidityProvision + ); + + let deposit = deposits[0].clone(); + + // swap half of the deposit asset for the other asset in the pool + let swap_half = Coin { + denom: deposit.denom.clone(), + amount: deposit.amount.checked_div(Uint128::new(2))?, + }; + + let swap_simulation_response = + query_simulation(deps.as_ref(), swap_half.clone(), pair_identifier.clone())?; + + let ask_denom = pool_assets + .iter() + .find(|pool_asset| pool_asset.denom != deposit.denom) + .ok_or(ContractError::AssetMismatch {})? + .denom + .clone(); + + // let's compute the expected offer asset balance in the contract after the swap and liquidity + // provision takes place. This should be the same value as of now. Even though half of it + // will be swapped, eventually all of it will be sent to the contract in the second step of + // the single side liquidity provision + let expected_offer_asset_balance_in_contract = deps + .querier + .query_balance(&env.contract.address, deposit.denom)?; + + // let's compute the expected ask asset balance in the contract after the swap and liquidity + // provision takes place. It should be the current balance minus the fees that will be sent + // off the contract. + let mut expected_ask_asset_balance_in_contract = deps + .querier + .query_balance(&env.contract.address, ask_denom.clone())?; + + expected_ask_asset_balance_in_contract.amount = expected_ask_asset_balance_in_contract + .amount + .checked_sub(aggregate_outgoing_fees(&swap_simulation_response)?)?; + + TMP_SINGLE_SIDE_LIQUIDITY_PROVISION.save( + deps.storage, + &SingleSideLiquidityProvisionBuffer { + receiver, + expected_offer_asset_balance_in_contract, + expected_ask_asset_balance_in_contract, + offer_asset_half: swap_half.clone(), + expected_ask_asset: coin( + swap_simulation_response.return_amount.u128(), + ask_denom.clone(), + ), + liquidity_provision_data: LiquidityProvisionData { + max_spread, + slippage_tolerance, + pair_identifier: pair_identifier.clone(), + unlocking_duration, + lock_position_identifier, + }, + }, + )?; + + return Ok(Response::default() + .add_submessage(SubMsg::reply_on_success( + wasm_execute( + env.contract.address.into_string(), + &ExecuteMsg::Swap { + offer_asset: swap_half.clone(), + ask_asset_denom: ask_denom, + belief_price: None, + max_spread, + to: None, + pair_identifier, + }, + vec![swap_half], + )?, + SINGLE_SIDE_LIQUIDITY_PROVISION_REPLY_ID, + )) + .add_attributes(vec![("action", "single_side_liquidity_provision")])); + } for asset in deposits.iter() { let asset_denom = &asset.denom; @@ -78,10 +171,12 @@ pub fn provide_liquidity( // After totting up the pool assets we need to check if any of them are zero. // The very first deposit cannot be done with a single asset - if pool_assets.iter().any(|deposit| deposit.amount.is_zero()) && is_single_asset_provision { + if pool_assets.iter().any(|deposit| deposit.amount.is_zero()) { return Err(ContractError::InvalidZeroAmount {}); } + let mut messages: Vec = vec![]; + let liquidity_token = pair.lp_denom.clone(); // Compute share and other logic based on the number of assets @@ -120,20 +215,6 @@ pub fn provide_liquidity( share } else { - // let share = { - // let numerator = U256::from(pool_assets[0].amount.u128()) - // .checked_mul(U256::from(total_share.u128())) - // .ok_or::(ContractError::LiquidityShareComputation {})?; - // - // let denominator = U256::from(pool_assets[0].amount.u128()); - // - // let result = numerator - // .checked_div(denominator) - // .ok_or::(ContractError::LiquidityShareComputation {})?; - // - // Uint128::from(result.as_u128()) - // }; - let amount = std::cmp::min( pool_assets[0] .amount @@ -143,29 +224,26 @@ pub fn provide_liquidity( .multiply_ratio(total_share, pool_assets[1].amount), ); - //todo i think we need to skip slippage_tolerance assertion in this case - if !is_single_asset_provision { - 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, - pair.pair_type.clone(), - amount, - total_share, - )?; - } + 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, + pair.pair_type.clone(), + amount, + total_share, + )?; amount } @@ -177,9 +255,6 @@ pub fn provide_liquidity( } }; - // mint LP token to sender - let receiver = receiver.unwrap_or_else(|| info.sender.to_string()); - // if the unlocking duration is set, lock the LP tokens in the incentive manager if let Some(unlocking_duration) = unlocking_duration { // mint the lp tokens to the contract @@ -209,13 +284,14 @@ pub fn provide_liquidity( // if not, just mint the LP tokens to the receiver messages.push(white_whale_std::lp_common::mint_lp_token_msg( liquidity_token, - &info.sender, + &deps.api.addr_validate(&receiver)?, &env.contract.address, share, )?); } pair.assets = pool_assets.clone(); + PAIRS.save(deps.storage, &pair_identifier, &pair)?; Ok(Response::new().add_messages(messages).add_attributes(vec![ diff --git a/contracts/liquidity_hub/pool-manager/src/manager/commands.rs b/contracts/liquidity_hub/pool-manager/src/manager/commands.rs index 16a12f57..68d9f31d 100644 --- a/contracts/liquidity_hub/pool-manager/src/manager/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/manager/commands.rs @@ -88,7 +88,7 @@ pub fn create_pair( // Load config for pool creation fee let config: Config = MANAGER_CONFIG.load(deps.storage)?; - // Check if fee was provided and is sufficient + // Check if fee was provided and is sufficientd if !config.pool_creation_fee.amount.is_zero() { // verify fee payment let amount = cw_utils::must_pay(&info, &config.pool_creation_fee.denom)?; diff --git a/contracts/liquidity_hub/pool-manager/src/queries.rs b/contracts/liquidity_hub/pool-manager/src/queries.rs index 39f5c3b4..98d4e242 100644 --- a/contracts/liquidity_hub/pool-manager/src/queries.rs +++ b/contracts/liquidity_hub/pool-manager/src/queries.rs @@ -3,14 +3,10 @@ use std::cmp::Ordering; use cosmwasm_std::{Coin, Decimal256, Deps, Env, Fraction, Order, StdResult, Uint128}; use white_whale_std::pool_manager::{ - AssetDecimalsResponse, Config, PairInfoResponse, SwapRoute, SwapRouteCreatorResponse, + AssetDecimalsResponse, Config, ReverseSimulationResponse, SimulationResponse, PairInfoResponse, SwapRoute, SwapRouteCreatorResponse, SwapRouteResponse, SwapRoutesResponse, }; -use white_whale_std::pool_network::{ - asset::PairType, - pair::{ReverseSimulationResponse, SimulationResponse}, - // router::SimulateSwapOperationsResponse, -}; +use white_whale_std::pool_network::asset::PairType; use crate::state::{MANAGER_CONFIG, PAIRS}; use crate::{ @@ -53,6 +49,7 @@ pub fn query_simulation( ) -> Result { let pair_info = get_pair_by_identifier(&deps, &pair_identifier)?; let pools = pair_info.assets.clone(); + // determine what's the offer and ask pool based on the offer_asset let offer_pool: Coin; let ask_pool: Coin; diff --git a/contracts/liquidity_hub/pool-manager/src/state.rs b/contracts/liquidity_hub/pool-manager/src/state.rs index d766a1d2..32a2ebca 100644 --- a/contracts/liquidity_hub/pool-manager/src/state.rs +++ b/contracts/liquidity_hub/pool-manager/src/state.rs @@ -1,10 +1,38 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::Deps; +use cosmwasm_std::{Coin, Decimal, Deps}; use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, UniqueIndex}; + +pub use white_whale_std::pool_manager::Config; use white_whale_std::pool_manager::{PairInfo, SwapOperation}; use crate::ContractError; +/// Holds information about the single side liquidity provision temporarily until the swap/liquidity +/// provision is completed +#[cw_serde] +pub struct SingleSideLiquidityProvisionBuffer { + pub receiver: String, + pub expected_offer_asset_balance_in_contract: Coin, + pub expected_ask_asset_balance_in_contract: Coin, + pub offer_asset_half: Coin, + pub expected_ask_asset: Coin, + pub liquidity_provision_data: LiquidityProvisionData, +} + +/// Holds information about the intended liquidity provision when a user provides liquidity with a +/// single asset. +#[cw_serde] +pub struct LiquidityProvisionData { + pub max_spread: Option, + pub slippage_tolerance: Option, + pub pair_identifier: String, + pub unlocking_duration: Option, + pub lock_position_identifier: Option, +} + +pub const TMP_SINGLE_SIDE_LIQUIDITY_PROVISION: Item = + Item::new("tmp_single_side_liquidity_provision"); + pub const PAIRS: IndexedMap<&str, PairInfo, PairIndexes> = IndexedMap::new( "pairs", PairIndexes { @@ -42,8 +70,8 @@ pub struct SwapOperations { pub creator: String, pub swap_operations: Vec, } + pub const SWAP_ROUTES: Map<(&str, &str), SwapOperations> = Map::new("swap_routes"); -pub use white_whale_std::pool_manager::Config; pub const MANAGER_CONFIG: Item = Item::new("manager_config"); pub const PAIR_COUNTER: Item = Item::new("vault_count"); diff --git a/contracts/liquidity_hub/pool-manager/src/swap/commands.rs b/contracts/liquidity_hub/pool-manager/src/swap/commands.rs index 7cfbe6bb..d5a60f32 100644 --- a/contracts/liquidity_hub/pool-manager/src/swap/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/swap/commands.rs @@ -1,10 +1,12 @@ use crate::{state::MANAGER_CONFIG, ContractError}; use cosmwasm_std::{ - to_json_binary, Addr, BankMsg, Coin, CosmosMsg, DepsMut, Env, MessageInfo, Response, WasmMsg, + ensure, to_json_binary, Addr, BankMsg, Coin, CosmosMsg, DepsMut, Env, MessageInfo, Response, + WasmMsg, }; pub const MAX_ASSETS_PER_POOL: usize = 4; +use crate::state::get_pair_by_identifier; use cosmwasm_std::Decimal; use white_whale_std::whale_lair; @@ -17,7 +19,7 @@ pub fn swap( info: MessageInfo, sender: Addr, offer_asset: Coin, - _ask_asset_denom: String, + ask_asset_denom: String, belief_price: Option, max_spread: Option, to: Option, @@ -25,15 +27,28 @@ pub fn swap( ) -> Result { let config = MANAGER_CONFIG.load(deps.storage)?; // check if the swap feature is enabled - if !config.feature_toggle.swaps_enabled { - return Err(ContractError::OperationDisabled("swap".to_string())); - } + ensure!( + config.feature_toggle.swaps_enabled, + ContractError::OperationDisabled("swap".to_string()) + ); + // todo remove this, not needed. You can just swap whatever it is sent in info.funds, just worth + // veritying the asset is the same as the one in the pool if cw_utils::one_coin(&info)? != offer_asset { return Err(ContractError::AssetMismatch {}); } - //todo get the pool by pair_identifier and verify the ask_asset_denom matches the one in the pool + // verify that the assets sent match the ones from the pool + let pair = get_pair_by_identifier(&deps.as_ref(), &pair_identifier)?; + ensure!( + vec![ask_asset_denom, offer_asset.denom.clone()] + .iter() + .all(|asset| pair + .assets + .iter() + .any(|pool_asset| pool_asset.denom == *asset)), + ContractError::AssetMismatch {} + ); // perform the swap let swap_result = perform_swap( @@ -72,16 +87,15 @@ pub fn swap( })); } - // todo remove, this stays within the pool - if !swap_result.swap_fee_asset.amount.is_zero() { - messages.push(CosmosMsg::Bank(BankMsg::Send { - to_address: config.bonding_manager_addr.to_string(), - amount: vec![swap_result.swap_fee_asset.clone()], - })); - } + //todo remove, this stays within the pool. Verify this with a test with multiple (duplicated) + // pools, see how the swap fees behave + // if !swap_result.swap_fee_asset.amount.is_zero() { + // messages.push(CosmosMsg::Bank(BankMsg::Send { + // to_address: config.bonding_manager_addr.to_string(), + // amount: vec![swap_result.swap_fee_asset.clone()], + // })); + // } - // 1. send collateral token from the contract to a user - // 2. stores the protocol fees Ok(Response::new().add_messages(messages).add_attributes(vec![ ("action", "swap"), ("sender", sender.as_str()), @@ -106,6 +120,11 @@ pub fn swap( "burn_fee_amount", &swap_result.burn_fee_asset.amount.to_string(), ), + #[cfg(feature = "osmosis")] + ( + "osmosis_fee_amount", + &swap_result.osmosis_fee_amount.to_string(), + ), ("swap_type", swap_result.pair_info.pair_type.get_label()), ])) } 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 335b219f..7b098c8d 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,5 @@ use cosmwasm_std::{Coin, Decimal, DepsMut, Uint128}; + use white_whale_std::pool_manager::PairInfo; use white_whale_std::pool_network::swap::assert_max_spread; @@ -8,6 +9,7 @@ use crate::{ ContractError, }; +#[derive(Debug)] pub struct SwapResult { /// The asset that should be returned to the user from the swap. pub return_asset: Coin, @@ -17,7 +19,9 @@ pub struct SwapResult { pub protocol_fee_asset: Coin, /// The swap fee of `return_asset` associated with this swap transaction. pub swap_fee_asset: Coin, - + /// The osmosis fee of `return_asset` associated with this swap transaction. + #[cfg(feature = "osmosis")] + pub osmosis_fee_asset: Coin, /// The pair that was traded. pub pair_info: PairInfo, /// The amount of spread that occurred during the swap from the original exchange rate. @@ -99,36 +103,60 @@ pub fn perform_swap( if offer_asset.denom == pools[0].denom { pair_info.assets[0].amount += offer_amount; pair_info.assets[1].amount -= swap_computation.return_amount; + pair_info.assets[1].amount -= swap_computation.protocol_fee_amount; + pair_info.assets[1].amount -= swap_computation.burn_fee_amount; } else { pair_info.assets[1].amount += offer_amount; pair_info.assets[0].amount -= swap_computation.return_amount; + pair_info.assets[0].amount -= swap_computation.protocol_fee_amount; + pair_info.assets[0].amount -= swap_computation.burn_fee_amount; } PAIRS.save(deps.storage, &pair_identifier, &pair_info)?; // TODO: Might be handy to make the below fees into a helper method - // burn ask_asset from the pool let burn_fee_asset = Coin { denom: ask_pool.denom.clone(), amount: swap_computation.burn_fee_amount, }; - // Prepare a message to send the protocol fee and the swap fee to the protocol fee collector let protocol_fee_asset = Coin { denom: ask_pool.denom.clone(), amount: swap_computation.protocol_fee_amount, }; - // Prepare a message to send the swap fee to the swap fee collector + + #[allow(clippy::redundant_clone)] let swap_fee_asset = Coin { - denom: ask_pool.denom, + denom: ask_pool.denom.clone(), amount: swap_computation.swap_fee_amount, }; - Ok(SwapResult { - return_asset, - swap_fee_asset, - burn_fee_asset, - protocol_fee_asset, - pair_info, - spread_amount: swap_computation.spread_amount, - }) + #[cfg(not(feature = "osmosis"))] + { + Ok(SwapResult { + return_asset, + swap_fee_asset, + burn_fee_asset, + protocol_fee_asset, + pair_info, + spread_amount: swap_computation.spread_amount, + }) + } + + #[cfg(feature = "osmosis")] + { + let osmosis_fee_asset = Coin { + denom: ask_pool.denom, + amount: swap_computation.swap_fee_amount, + }; + + Ok(SwapResult { + return_asset, + swap_fee_asset, + burn_fee_asset, + protocol_fee_asset, + osmosis_fee_asset, + pair_info, + spread_amount: swap_computation.spread_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 81ddb679..ccf9d10c 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs @@ -750,7 +750,6 @@ mod router { |result| { // ensure we got 999,000 in the response (1m - initial liquidity amount) let result = result.unwrap(); - println!("{:?}", result); assert!(result.has_event(&Event::new("wasm").add_attribute("share", "999000"))); }, ); @@ -1752,7 +1751,7 @@ mod router { swap_operations.clone(), |result| { let result = result.unwrap(); - assert_eq!(result.amount.u128(), 1_006); + assert_eq!(result.amount.u128(), 1_007); }, ); @@ -2094,7 +2093,6 @@ mod swapping { amount: Uint128::from(1000u128), }, |result| { - println!("{:?}", result); *simulated_return_amount.borrow_mut() = result.unwrap().return_amount; }, ); @@ -2155,7 +2153,6 @@ mod swapping { amount: Uint128::from(1000u128), }, |result| { - println!("{:?}", result); *simulated_offer_amount.borrow_mut() = result.unwrap().offer_amount; }, ); @@ -2183,7 +2180,6 @@ mod swapping { let mut offer_amount = String::new(); for event in result.unwrap().events { - println!("{:?}", event); if event.ty == "wasm" { for attribute in event.attributes { match attribute.key.as_str() { @@ -2322,7 +2318,6 @@ mod swapping { let mut offer_amount = String::new(); for event in result.unwrap().events { - println!("{:?}", event); if event.ty == "wasm" { for attribute in event.attributes { match attribute.key.as_str() { @@ -2346,12 +2341,12 @@ mod swapping { ); // Verify fee collection by querying the address of the whale lair and checking its balance - // Should be 297 uLUNA + // Should be 99 uLUNA suite.query_balance( suite.bonding_manager_addr.to_string(), "uluna".to_string(), |result| { - assert_eq!(result.unwrap().amount, Uint128::from(297u128)); + assert_eq!(result.unwrap().amount, Uint128::from(99u128)); }, ); } @@ -2895,10 +2890,10 @@ mod provide_liquidity { #[cfg(not(feature = "osmosis"))] let pool_fees = PoolFee { protocol_fee: Fee { - share: Decimal::zero(), + share: Decimal::percent(1), }, swap_fee: Fee { - share: Decimal::zero(), + share: Decimal::percent(1), }, burn_fee: Fee { share: Decimal::zero(), @@ -2938,7 +2933,6 @@ mod provide_liquidity { ); let contract_addr = suite.pool_manager_addr.clone(); - let incentive_manager_addr = suite.incentive_manager_addr.clone(); let lp_denom = suite.get_lp_denom("whale-uluna".to_string()); // Lets try to add liquidity @@ -2989,30 +2983,9 @@ mod provide_liquidity { let err = result.unwrap_err().downcast::().unwrap(); match err { - ContractError::InvalidZeroAmount {} => {} + ContractError::EmptyPoolForSingleSideLiquidityProvision {} => {} _ => panic!( - "Wrong error type, should return ContractError::InvalidZeroAmount" - ), - } - }, - ) - .provide_liquidity( - creator.clone(), - "whale-uluna".to_string(), - None, - None, - vec![Coin { - denom: "uwhale".to_string(), - amount: Uint128::from(1_000_000u128), - }], - |result| { - let err = result.unwrap_err().downcast::().unwrap(); - - // it was trying to provide single-side liquidity with no funds in the pool - match err { - ContractError::InvalidZeroAmount {} => {} - _ => panic!( - "Wrong error type, should return ContractError::InvalidZeroAmount" + "Wrong error type, should return ContractError::EmptyPoolForSingleSideLiquidityProvision" ), } }, @@ -3020,31 +2993,6 @@ mod provide_liquidity { // let's provide liquidity with two assets suite - .provide_liquidity( - creator.clone(), - "whale-uluna".to_string(), - None, - None, - vec![ - Coin { - denom: "uwhale".to_string(), - amount: Uint128::from(1_000_000u128), - }, - Coin { - denom: "uosmo".to_string(), - amount: Uint128::from(1_000_000u128), - }, - ], - |result| { - let err = result.unwrap_err().downcast::().unwrap(); - - // it was trying to provide single-side liquidity with no funds in the pool - match err { - ContractError::AssetMismatch {} => {} - _ => panic!("Wrong error type, should return ContractError::AssetMismatch"), - } - }, - ) .provide_liquidity( creator.clone(), "whale-uluna".to_string(), @@ -3063,7 +3011,6 @@ mod provide_liquidity { |result| { let err = result.unwrap_err().downcast::().unwrap(); - // it was trying to provide single-side liquidity with no funds in the pool match err { ContractError::AssetMismatch {} => {} _ => panic!("Wrong error type, should return ContractError::AssetMismatch"), @@ -3128,7 +3075,6 @@ mod provide_liquidity { ) .query_all_balances(other.to_string(), |result| { let balances = result.unwrap(); - assert!(balances.iter().any(|coin| { coin.denom == lp_denom && coin.amount == Uint128::from(1_000_000u128) })); @@ -3164,11 +3110,32 @@ mod provide_liquidity { .unwrap(); assert_eq!(whale.amount, Uint128::from(3_000_000u128)); - assert_eq!(luna.amount, Uint128::from(1_000_000u128)); + assert_eq!(luna.amount, Uint128::from(995_000u128)); }); + let pool_manager = suite.pool_manager_addr.clone(); // let's withdraw both LPs suite + .query_all_balances(pool_manager.clone().to_string(), |result| { + let balances = result.unwrap(); + assert_eq!( + balances, + vec![ + Coin { + denom: lp_denom.clone(), + amount: Uint128::from(1_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(995_000u128), + }, + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(3_000_000u128), + }, + ] + ); + }) .query_all_balances(creator.clone().to_string(), |result| { let balances = result.unwrap(); assert_eq!( @@ -3215,7 +3182,7 @@ mod provide_liquidity { vec![ Coin { denom: "uluna".to_string(), - amount: Uint128::from(9_499_500u128), + amount: Uint128::from(9_497_002u128), }, Coin { denom: "uosmo".to_string(), @@ -3233,6 +3200,8 @@ mod provide_liquidity { ); }); + let bonding_manager = suite.bonding_manager_addr.clone(); + suite .query_all_balances(other.clone().to_string(), |result| { let balances = result.unwrap(); @@ -3280,7 +3249,7 @@ mod provide_liquidity { vec![ Coin { denom: "uluna".to_string(), - amount: Uint128::from(10_499_999u128), + amount: Uint128::from(10_497_500u128), }, Coin { denom: "uosmo".to_string(), @@ -3296,6 +3265,24 @@ mod provide_liquidity { }, ] ); + }) + .query_all_balances(bonding_manager.to_string(), |result| { + let balances = result.unwrap(); + // check that the bonding manager got the luna fees for the single-side lp + // plus the pool creation fee + assert_eq!( + balances, + vec![ + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(5_000u128), + }, + Coin { + denom: "uusd".to_string(), + amount: Uint128::from(1_000u128), + }, + ] + ); }); } } diff --git a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs index 232a0b93..aa53abe8 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs @@ -1,6 +1,10 @@ use cosmwasm_std::testing::MockStorage; use std::cell::RefCell; -use white_whale_std::pool_manager::{Config, FeatureToggle, PairInfoResponse, ReverseSimulateSwapOperationsResponse, ReverseSimulationResponse, SimulateSwapOperationsResponse, SimulationResponse, SwapOperation, SwapRouteCreatorResponse, SwapRouteResponse, SwapRoutesResponse}; +use white_whale_std::pool_manager::{ + Config, FeatureToggle, PairInfoResponse, ReverseSimulateSwapOperationsResponse, + ReverseSimulationResponse, SimulateSwapOperationsResponse, SimulationResponse, SwapOperation, + SwapRouteCreatorResponse, SwapRouteResponse, SwapRoutesResponse, +}; use white_whale_std::pool_manager::{InstantiateMsg, PairInfo}; use cosmwasm_std::{coin, Addr, Coin, Decimal, Empty, StdResult, Timestamp, Uint128, Uint64}; @@ -22,7 +26,8 @@ fn contract_pool_manager() -> Box> { crate::contract::execute, crate::contract::instantiate, crate::contract::query, - ); + ) + .with_reply(crate::contract::reply); Box::new(contract) } @@ -321,6 +326,7 @@ impl TestingSuite { let msg = white_whale_std::pool_manager::ExecuteMsg::ProvideLiquidity { pair_identifier, slippage_tolerance: None, + max_spread: None, receiver: None, unlocking_duration, lock_position_identifier, @@ -772,7 +778,7 @@ impl TestingSuite { ) -> &mut Self { let lp_denom = RefCell::new("".to_string()); - let pair = self.query_pair_info(identifier.clone(), |res| { + self.query_pair_info(identifier.clone(), |res| { let response = res.unwrap(); *lp_denom.borrow_mut() = response.pair_info.lp_denom.clone(); }); diff --git a/contracts/liquidity_hub/pool-manager/src/tests/unit_tests/swap.rs b/contracts/liquidity_hub/pool-manager/src/tests/unit_tests/swap.rs index 1d2a5060..478516e5 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/unit_tests/swap.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/unit_tests/swap.rs @@ -139,7 +139,6 @@ fn try_native_to_token() { }], ); let res = execute(deps.as_mut(), env, info, msg).unwrap(); - println!("{:?}", res); assert_eq!(res.messages.len(), 1); let msg_transfer = res.messages.get(0).expect("no message"); diff --git a/packages/white-whale-std/src/pool_manager.rs b/packages/white-whale-std/src/pool_manager.rs index 3c45b03e..bfa8dc09 100644 --- a/packages/white-whale-std/src/pool_manager.rs +++ b/packages/white-whale-std/src/pool_manager.rs @@ -139,6 +139,7 @@ pub enum ExecuteMsg { /// Provides liquidity to the pool ProvideLiquidity { slippage_tolerance: Option, + max_spread: Option, receiver: Option, pair_identifier: String, /// The amount of time in seconds to unlock tokens if taking part on the incentives. If not passed, @@ -149,6 +150,7 @@ pub enum ExecuteMsg { }, /// Swap an offer asset to the other Swap { + //todo remove offer_asset, take it from info.funds offer_asset: Coin, ask_asset_denom: String, belief_price: Option, From 932aee4c64a34bb29973249cce88317e870b07ed Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Wed, 1 May 2024 10:44:49 +0100 Subject: [PATCH 3/7] refactor: tweak code --- .../pool-manager/src/liquidity/commands.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs index f0e0baff..78391395 100644 --- a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs @@ -156,17 +156,16 @@ pub fn provide_liquidity( for asset in deposits.iter() { let asset_denom = &asset.denom; - if let Some(pool_asset_index) = pool_assets + + let pool_asset_index = pool_assets .iter() - .position(|pool_asset| pool_asset.denom == *asset_denom) - { - // Increment the pool asset amount by the amount sent - pool_assets[pool_asset_index].amount = pool_assets[pool_asset_index] - .amount - .checked_add(asset.amount)?; - } else { - return Err(ContractError::AssetMismatch {}); - } + .position(|pool_asset| &pool_asset.denom == asset_denom) + .ok_or(ContractError::AssetMismatch {})?; + + // Increment the pool asset amount by the amount sent + pool_assets[pool_asset_index].amount = pool_assets[pool_asset_index] + .amount + .checked_add(asset.amount)?; } // After totting up the pool assets we need to check if any of them are zero. From 26d63dd69dd48c621c401733170ea12b77ab260e Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Wed, 1 May 2024 10:47:56 +0100 Subject: [PATCH 4/7] chore: regenerate schemas --- .../pool-manager/schema/pool-manager.json | 10 ++++++++++ .../liquidity_hub/pool-manager/schema/raw/execute.json | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/contracts/liquidity_hub/pool-manager/schema/pool-manager.json b/contracts/liquidity_hub/pool-manager/schema/pool-manager.json index 9351efff..38209617 100644 --- a/contracts/liquidity_hub/pool-manager/schema/pool-manager.json +++ b/contracts/liquidity_hub/pool-manager/schema/pool-manager.json @@ -116,6 +116,16 @@ "null" ] }, + "max_spread": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, "pair_identifier": { "type": "string" }, diff --git a/contracts/liquidity_hub/pool-manager/schema/raw/execute.json b/contracts/liquidity_hub/pool-manager/schema/raw/execute.json index 44e27ae1..e595c48e 100644 --- a/contracts/liquidity_hub/pool-manager/schema/raw/execute.json +++ b/contracts/liquidity_hub/pool-manager/schema/raw/execute.json @@ -69,6 +69,16 @@ "null" ] }, + "max_spread": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, "pair_identifier": { "type": "string" }, From a694b5dd0b11f5008fbe02cdb963aac1f923a3a7 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Wed, 1 May 2024 11:32:40 +0100 Subject: [PATCH 5/7] test: fix max spread assetion in single-side lp test --- .../pool-manager/src/contract.rs | 6 +- .../liquidity_hub/pool-manager/src/queries.rs | 4 +- .../src/tests/integration_tests.rs | 77 ++++++++++++++++--- .../pool-manager/src/tests/suite.rs | 5 +- 4 files changed, 75 insertions(+), 17 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/src/contract.rs b/contracts/liquidity_hub/pool-manager/src/contract.rs index d7d2ddca..e27b12ec 100644 --- a/contracts/liquidity_hub/pool-manager/src/contract.rs +++ b/contracts/liquidity_hub/pool-manager/src/contract.rs @@ -1,9 +1,11 @@ use crate::error::ContractError; -use crate::helpers::{reverse_simulate_swap_operations, simulate_swap_operations, validate_asset_balance}; +use crate::helpers::{ + reverse_simulate_swap_operations, simulate_swap_operations, validate_asset_balance, +}; use crate::queries::{get_pair, get_swap_route, get_swap_route_creator, get_swap_routes}; use crate::router::commands::{add_swap_routes, remove_swap_routes}; use crate::state::{ - Config, SingleSideLiquidityProvisionBuffer, MANAGER_CONFIG, PAIRS, PAIR_COUNTER, + Config, SingleSideLiquidityProvisionBuffer, MANAGER_CONFIG, PAIR_COUNTER, TMP_SINGLE_SIDE_LIQUIDITY_PROVISION, }; use crate::{liquidity, manager, queries, router, swap}; diff --git a/contracts/liquidity_hub/pool-manager/src/queries.rs b/contracts/liquidity_hub/pool-manager/src/queries.rs index 98d4e242..183d3dcd 100644 --- a/contracts/liquidity_hub/pool-manager/src/queries.rs +++ b/contracts/liquidity_hub/pool-manager/src/queries.rs @@ -3,8 +3,8 @@ use std::cmp::Ordering; use cosmwasm_std::{Coin, Decimal256, Deps, Env, Fraction, Order, StdResult, Uint128}; use white_whale_std::pool_manager::{ - AssetDecimalsResponse, Config, ReverseSimulationResponse, SimulationResponse, PairInfoResponse, SwapRoute, SwapRouteCreatorResponse, - SwapRouteResponse, SwapRoutesResponse, + AssetDecimalsResponse, Config, PairInfoResponse, ReverseSimulationResponse, SimulationResponse, + SwapRoute, SwapRouteCreatorResponse, SwapRouteResponse, SwapRoutesResponse, }; use white_whale_std::pool_network::asset::PairType; 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 ccf9d10c..f2c642b6 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs @@ -89,6 +89,7 @@ fn deposit_and_withdraw_sanity_check() { "whale-uluna".to_string(), None, None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -405,6 +406,7 @@ mod router { "whale-uluna".to_string(), None, None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -428,6 +430,7 @@ mod router { "uluna-uusd".to_string(), None, None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -596,6 +599,7 @@ mod router { "whale-uluna".to_string(), None, None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -619,6 +623,7 @@ mod router { "uluna-uusd".to_string(), None, None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -737,6 +742,7 @@ mod router { "whale-uluna".to_string(), None, None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -760,6 +766,7 @@ mod router { "uluna-uusd".to_string(), None, None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -895,6 +902,7 @@ mod router { "whale-uluna".to_string(), None, None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -918,6 +926,7 @@ mod router { "uluna-uusd".to_string(), None, None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -1119,6 +1128,7 @@ mod router { "whale-uluna".to_string(), None, None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -1142,6 +1152,7 @@ mod router { "uluna-uusd".to_string(), None, None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -1280,6 +1291,7 @@ mod router { "whale-uluna".to_string(), None, None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -1299,6 +1311,7 @@ mod router { "uluna-uusd".to_string(), None, None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -1442,6 +1455,7 @@ mod router { "whale-uluna".to_string(), None, None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -1461,6 +1475,7 @@ mod router { "uluna-uusd".to_string(), None, None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -1638,6 +1653,7 @@ mod router { "whale-uluna".to_string(), None, None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -1661,6 +1677,7 @@ mod router { "uluna-uusd".to_string(), None, None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -1847,6 +1864,7 @@ mod swapping { "whale-uluna".to_string(), None, None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -2068,6 +2086,7 @@ mod swapping { "whale-uluna".to_string(), None, None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -2268,6 +2287,7 @@ mod swapping { "whale-uluna".to_string(), None, None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -2565,6 +2585,7 @@ mod locking_lp { "whale-uluna".to_string(), Some(86_400u64), None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -2630,6 +2651,7 @@ mod locking_lp { "whale-uluna".to_string(), Some(200_000u64), None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -2752,6 +2774,7 @@ mod locking_lp { "whale-uluna".to_string(), Some(86_400u64), Some("incentive_identifier".to_string()), + None, vec![ Coin { denom: "uwhale".to_string(), @@ -2817,6 +2840,7 @@ mod locking_lp { "whale-uluna".to_string(), Some(200_000u64), Some("incentive_identifier".to_string()), + None, vec![ Coin { denom: "uwhale".to_string(), @@ -2863,7 +2887,7 @@ mod locking_lp { } mod provide_liquidity { - use cosmwasm_std::{coin, Coin, Decimal, Uint128}; + use cosmwasm_std::{coin, Coin, Decimal, StdError, Uint128}; use white_whale_std::fee::{Fee, PoolFee}; use white_whale_std::pool_network::asset::MINIMUM_LIQUIDITY_AMOUNT; @@ -2942,6 +2966,7 @@ mod provide_liquidity { "whale-uluna".to_string(), None, None, + None, vec![], |result| { let err = result.unwrap_err().downcast::().unwrap(); @@ -2957,6 +2982,7 @@ mod provide_liquidity { "whale-uluna".to_string(), None, None, + None, vec![Coin { denom: "uosmo".to_string(), amount: Uint128::from(1_000_000u128), @@ -2975,6 +3001,7 @@ mod provide_liquidity { "whale-uluna".to_string(), None, None, + None, vec![Coin { denom: "uwhale".to_string(), amount: Uint128::from(1_000_000u128), @@ -2998,6 +3025,7 @@ mod provide_liquidity { "whale-uluna".to_string(), None, None, + None, vec![ Coin { denom: "uosmo".to_string(), @@ -3022,6 +3050,7 @@ mod provide_liquidity { "whale-uluna".to_string(), None, None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -3059,6 +3088,7 @@ mod provide_liquidity { "whale-uluna".to_string(), None, None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -3069,6 +3099,31 @@ mod provide_liquidity { amount: Uint128::from(1_000_000u128), }, ], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + assert_eq!( + err, + ContractError::Std(StdError::generic_err("Spread limit exceeded")) + ); + }, + ) + .provide_liquidity( + other.clone(), + "whale-uluna".to_string(), + None, + None, + Some(Decimal::percent(50)), + vec![ + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(500_000u128), + }, + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(500_000u128), + }, + ], |result| { result.unwrap(); }, @@ -3109,8 +3164,8 @@ mod provide_liquidity { .find(|coin| coin.denom == "uluna".to_string()) .unwrap(); - assert_eq!(whale.amount, Uint128::from(3_000_000u128)); - assert_eq!(luna.amount, Uint128::from(995_000u128)); + assert_eq!(whale.amount, Uint128::from(2_000_000u128)); + assert_eq!(luna.amount, Uint128::from(996_667u128)); }); let pool_manager = suite.pool_manager_addr.clone(); @@ -3127,11 +3182,11 @@ mod provide_liquidity { }, Coin { denom: "uluna".to_string(), - amount: Uint128::from(995_000u128), + amount: Uint128::from(996_667u128), }, Coin { denom: "uwhale".to_string(), - amount: Uint128::from(3_000_000u128), + amount: Uint128::from(2_000_000u128), }, ] ); @@ -3182,7 +3237,7 @@ mod provide_liquidity { vec![ Coin { denom: "uluna".to_string(), - amount: Uint128::from(9_497_002u128), + amount: Uint128::from(9_497_835u128), }, Coin { denom: "uosmo".to_string(), @@ -3194,7 +3249,7 @@ mod provide_liquidity { }, Coin { denom: "uwhale".to_string(), - amount: Uint128::from(10_498_500u128), + amount: Uint128::from(9_999_000u128), }, ] ); @@ -3226,7 +3281,7 @@ mod provide_liquidity { }, Coin { denom: "uwhale".to_string(), - amount: Uint128::from(8_000_000u128), + amount: Uint128::from(9_000_000u128), }, ] ); @@ -3249,7 +3304,7 @@ mod provide_liquidity { vec![ Coin { denom: "uluna".to_string(), - amount: Uint128::from(10_497_500u128), + amount: Uint128::from(10_498_333u128), }, Coin { denom: "uosmo".to_string(), @@ -3261,7 +3316,7 @@ mod provide_liquidity { }, Coin { denom: "uwhale".to_string(), - amount: Uint128::from(9_499_999u128), + amount: Uint128::from(9_999_999u128), }, ] ); @@ -3275,7 +3330,7 @@ mod provide_liquidity { vec![ Coin { denom: "uluna".to_string(), - amount: Uint128::from(5_000u128), + amount: Uint128::from(3_333u128), }, Coin { denom: "uusd".to_string(), diff --git a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs index aa53abe8..39a5da6f 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs @@ -1,11 +1,11 @@ use cosmwasm_std::testing::MockStorage; use std::cell::RefCell; +use white_whale_std::pool_manager::InstantiateMsg; use white_whale_std::pool_manager::{ Config, FeatureToggle, PairInfoResponse, ReverseSimulateSwapOperationsResponse, ReverseSimulationResponse, SimulateSwapOperationsResponse, SimulationResponse, SwapOperation, SwapRouteCreatorResponse, SwapRouteResponse, SwapRoutesResponse, }; -use white_whale_std::pool_manager::{InstantiateMsg, PairInfo}; use cosmwasm_std::{coin, Addr, Coin, Decimal, Empty, StdResult, Timestamp, Uint128, Uint64}; use cw_multi_test::addons::{MockAddressGenerator, MockApiBech32}; @@ -320,13 +320,14 @@ impl TestingSuite { pair_identifier: String, unlocking_duration: Option, lock_position_identifier: Option, + max_spread: Option, funds: Vec, result: impl Fn(Result), ) -> &mut Self { let msg = white_whale_std::pool_manager::ExecuteMsg::ProvideLiquidity { pair_identifier, slippage_tolerance: None, - max_spread: None, + max_spread, receiver: None, unlocking_duration, lock_position_identifier, From 533674d81e3daf675ae175c9b495deae39dbb067 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Wed, 1 May 2024 13:32:34 +0100 Subject: [PATCH 6/7] chore: add sanity check when providing liquidity with a single asset --- .../liquidity_hub/pool-manager/src/error.rs | 11 +- .../pool-manager/src/liquidity/commands.rs | 18 ++- .../src/tests/integration_tests.rs | 148 +++++++++++++++++- 3 files changed, 169 insertions(+), 8 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/src/error.rs b/contracts/liquidity_hub/pool-manager/src/error.rs index a2580ebf..1e146fec 100644 --- a/contracts/liquidity_hub/pool-manager/src/error.rs +++ b/contracts/liquidity_hub/pool-manager/src/error.rs @@ -1,7 +1,8 @@ use crate::liquidity::commands::MAX_ASSETS_PER_POOL; use cosmwasm_std::{ - CheckedFromRatioError, CheckedMultiplyRatioError, ConversionOverflowError, DivideByZeroError, - Instantiate2AddressError, OverflowError, StdError, Uint128, + CheckedFromRatioError, CheckedMultiplyFractionError, CheckedMultiplyRatioError, + ConversionOverflowError, DivideByZeroError, Instantiate2AddressError, OverflowError, StdError, + Uint128, }; use cw_ownable::OwnershipError; use cw_utils::PaymentError; @@ -89,6 +90,9 @@ pub enum ContractError { #[error("Failed to compute the LP share with the given deposit")] LiquidityShareComputation {}, + #[error("The amount of LP shares to withdraw is invalid")] + InvalidLpShare, + #[error("Spread limit exceeded")] MaxSpreadAssertion {}, @@ -125,6 +129,9 @@ pub enum ContractError { #[error(transparent)] CheckedMultiplyRatioError(#[from] CheckedMultiplyRatioError), + #[error(transparent)] + CheckedMultiplyFractionError(#[from] CheckedMultiplyFractionError), + #[error(transparent)] CheckedFromRatioError(#[from] CheckedFromRatioError), diff --git a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs index 78391395..3cb74ea3 100644 --- a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs @@ -82,7 +82,7 @@ pub fn provide_liquidity( // swap half of the deposit asset for the other asset in the pool let swap_half = Coin { denom: deposit.denom.clone(), - amount: deposit.amount.checked_div(Uint128::new(2))?, + amount: deposit.amount.checked_div_floor((2u64, 1u64))?, }; let swap_simulation_response = @@ -112,7 +112,15 @@ pub fn provide_liquidity( expected_ask_asset_balance_in_contract.amount = expected_ask_asset_balance_in_contract .amount - .checked_sub(aggregate_outgoing_fees(&swap_simulation_response)?)?; + .saturating_sub(aggregate_outgoing_fees(&swap_simulation_response)?); + + // sanity check. Theoretically, with the given conditions of min LP, pool fees and max spread assertion, + // the expected ask asset balance in the contract will always be greater than zero after + // subtracting the fees. + ensure!( + !expected_ask_asset_balance_in_contract.amount.is_zero(), + StdError::generic_err("Spread limit exceeded") + ); TMP_SINGLE_SIDE_LIQUIDITY_PROVISION.save( deps.storage, @@ -337,15 +345,17 @@ pub fn withdraw_liquidity( // Get the ratio of the amount to withdraw to the total share let share_ratio: Decimal = Decimal::from_ratio(amount, total_share); + // sanity check, the share_ratio cannot possibly be greater than 1 + ensure!(share_ratio <= Decimal::one(), ContractError::InvalidLpShare); + // Use the ratio to calculate the amount of each pool asset to refund let refund_assets: Vec = pair .assets .iter() .map(|pool_asset| { - let refund_amount = pool_asset.amount; Ok(Coin { denom: pool_asset.denom.clone(), - amount: refund_amount * share_ratio, + amount: pool_asset.amount * share_ratio, }) }) .collect::, OverflowError>>()?; 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 f2c642b6..bd75c667 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs @@ -2928,10 +2928,10 @@ mod provide_liquidity { #[cfg(feature = "osmosis")] let pool_fees = PoolFee { protocol_fee: Fee { - share: Decimal::zero(), + share: Decimal::percent(1), }, swap_fee: Fee { - share: Decimal::zero(), + share: Decimal::percent(1), }, burn_fee: Fee { share: Decimal::zero(), @@ -3340,4 +3340,148 @@ mod provide_liquidity { ); }); } + + #[test] + fn provide_liquidity_with_single_asset_edge_case() { + let mut suite = TestingSuite::default_with_balances(vec![ + coin(1_000_000u128, "uwhale".to_string()), + coin(1_000_000u128, "uluna".to_string()), + coin(1_000_000u128, "uosmo".to_string()), + coin(10_000u128, "uusd".to_string()), + ]); + let creator = suite.creator(); + let other = suite.senders[1].clone(); + let _unauthorized = suite.senders[2].clone(); + + // Asset denoms with uwhale and uluna + let asset_denoms = vec!["uwhale".to_string(), "uluna".to_string()]; + + // Default Pool fees white_whale_std::pool_network::pair::PoolFee + #[cfg(not(feature = "osmosis"))] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::percent(15), + }, + swap_fee: Fee { + share: Decimal::percent(5), + }, + burn_fee: Fee { + share: Decimal::zero(), + }, + extra_fees: vec![], + }; + + #[cfg(feature = "osmosis")] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::percent(15), + }, + swap_fee: Fee { + share: Decimal::percent(5), + }, + burn_fee: Fee { + share: Decimal::zero(), + }, + osmosis_fee: Fee { + share: Decimal::percent(10), + }, + extra_fees: vec![], + }; + + // Create a pair + suite.instantiate_default().create_pair( + creator.clone(), + asset_denoms, + vec![6u8, 6u8], + pool_fees, + white_whale_std::pool_network::asset::PairType::ConstantProduct, + Some("whale-uluna".to_string()), + vec![coin(1000, "uusd")], + |result| { + result.unwrap(); + }, + ); + + let contract_addr = suite.pool_manager_addr.clone(); + + // let's provide liquidity with two assets + suite + .provide_liquidity( + creator.clone(), + "whale-uluna".to_string(), + None, + None, + None, + vec![ + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_100u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(1_100u128), + }, + ], + |result| { + result.unwrap(); + }, + ) + .query_all_balances(contract_addr.to_string(), |result| { + let balances = result.unwrap(); + println!("contract_addr {:?}", balances); + }); + + // now let's provide liquidity with a single asset + suite + .provide_liquidity( + other.clone(), + "whale-uluna".to_string(), + None, + None, + Some(Decimal::percent(50)), + vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_760u128), + }], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + assert_eq!( + err, + ContractError::Std(StdError::generic_err("Spread limit exceeded")) + ); + }, + ) + .provide_liquidity( + other.clone(), + "whale-uluna".to_string(), + None, + None, + Some(Decimal::percent(50)), + vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(10_000u128), + }], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + assert_eq!( + err, + ContractError::Std(StdError::generic_err("Spread limit exceeded")) + ); + }, + ) + .provide_liquidity( + other.clone(), + "whale-uluna".to_string(), + None, + None, + Some(Decimal::percent(50)), + vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000u128), + }], + |result| { + result.unwrap(); + }, + ); + } } From 5f5b6a8bc7ba4902c5b18b4e6a08fba7d9349017 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Wed, 1 May 2024 13:43:46 +0100 Subject: [PATCH 7/7] refactor: tweak code around single asset lp --- .../pool-manager/src/liquidity/commands.rs | 289 +++++++++--------- 1 file changed, 144 insertions(+), 145 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs index 3cb74ea3..d0f41319 100644 --- a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs @@ -71,7 +71,6 @@ pub fn provide_liquidity( let is_single_asset_provision = deposits.len() == 1usize; if is_single_asset_provision { - //todo maybe put all the code within this block in a function?? ensure!( !pool_assets.iter().any(|asset| asset.amount.is_zero()), ContractError::EmptyPoolForSingleSideLiquidityProvision @@ -143,7 +142,7 @@ pub fn provide_liquidity( }, )?; - return Ok(Response::default() + Ok(Response::default() .add_submessage(SubMsg::reply_on_success( wasm_execute( env.contract.address.into_string(), @@ -159,162 +158,162 @@ pub fn provide_liquidity( )?, SINGLE_SIDE_LIQUIDITY_PROVISION_REPLY_ID, )) - .add_attributes(vec![("action", "single_side_liquidity_provision")])); - } - - for asset in deposits.iter() { - let asset_denom = &asset.denom; - - let pool_asset_index = pool_assets - .iter() - .position(|pool_asset| &pool_asset.denom == asset_denom) - .ok_or(ContractError::AssetMismatch {})?; - - // Increment the pool asset amount by the amount sent - pool_assets[pool_asset_index].amount = pool_assets[pool_asset_index] - .amount - .checked_add(asset.amount)?; - } - - // After totting up the pool assets we need to check if any of them are zero. - // The very first deposit cannot be done with a single asset - if pool_assets.iter().any(|deposit| deposit.amount.is_zero()) { - return Err(ContractError::InvalidZeroAmount {}); - } + .add_attributes(vec![("action", "single_side_liquidity_provision")])) + } else { + for asset in deposits.iter() { + let asset_denom = &asset.denom; - let mut messages: Vec = vec![]; + let pool_asset_index = pool_assets + .iter() + .position(|pool_asset| &pool_asset.denom == asset_denom) + .ok_or(ContractError::AssetMismatch {})?; - let liquidity_token = pair.lp_denom.clone(); + // Increment the pool asset amount by the amount sent + pool_assets[pool_asset_index].amount = pool_assets[pool_asset_index] + .amount + .checked_add(asset.amount)?; + } - // Compute share and other logic based on the number of assets - let total_share = get_total_share(&deps.as_ref(), liquidity_token.clone())?; + // After totting up the pool assets we need to check if any of them are zero. + // The very first deposit cannot be done with a single asset + if pool_assets.iter().any(|deposit| deposit.amount.is_zero()) { + return Err(ContractError::InvalidZeroAmount {}); + } - let share = match &pair.pair_type { - PairType::ConstantProduct => { - if total_share == Uint128::zero() { - // Make sure at least MINIMUM_LIQUIDITY_AMOUNT is deposited to mitigate the risk of the first - // depositor preventing small liquidity providers from joining the pool - let share = Uint128::new( - (U256::from(pool_assets[0].amount.u128()) - .checked_mul(U256::from(pool_assets[1].amount.u128())) - .ok_or::(ContractError::LiquidityShareComputation {}))? - .integer_sqrt() - .as_u128(), - ) - .checked_sub(MINIMUM_LIQUIDITY_AMOUNT) - .map_err(|_| { - ContractError::InvalidInitialLiquidityAmount(MINIMUM_LIQUIDITY_AMOUNT) - })?; - - // share should be above zero after subtracting the MINIMUM_LIQUIDITY_AMOUNT - if share.is_zero() { - return Err(ContractError::InvalidInitialLiquidityAmount( + let mut messages: Vec = vec![]; + + let liquidity_token = pair.lp_denom.clone(); + + // Compute share and other logic based on the number of assets + let total_share = get_total_share(&deps.as_ref(), liquidity_token.clone())?; + + let share = match &pair.pair_type { + PairType::ConstantProduct => { + if total_share == Uint128::zero() { + // Make sure at least MINIMUM_LIQUIDITY_AMOUNT is deposited to mitigate the risk of the first + // depositor preventing small liquidity providers from joining the pool + let share = Uint128::new( + (U256::from(pool_assets[0].amount.u128()) + .checked_mul(U256::from(pool_assets[1].amount.u128())) + .ok_or::(ContractError::LiquidityShareComputation {}))? + .integer_sqrt() + .as_u128(), + ) + .checked_sub(MINIMUM_LIQUIDITY_AMOUNT) + .map_err(|_| { + ContractError::InvalidInitialLiquidityAmount(MINIMUM_LIQUIDITY_AMOUNT) + })?; + + // share should be above zero after subtracting the MINIMUM_LIQUIDITY_AMOUNT + if share.is_zero() { + return Err(ContractError::InvalidInitialLiquidityAmount( + MINIMUM_LIQUIDITY_AMOUNT, + )); + } + + 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 = std::cmp::min( + pool_assets[0] + .amount + .multiply_ratio(total_share, pool_assets[0].amount), + pool_assets[1] + .amount + .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, + pair.pair_type.clone(), + amount, + total_share, + )?; + + amount } + } + PairType::StableSwap { amp: _ } => { + // TODO: Handle stableswap - 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 = std::cmp::min( - pool_assets[0] - .amount - .multiply_ratio(total_share, pool_assets[0].amount), - pool_assets[1] - .amount - .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, - pair.pair_type.clone(), - amount, - total_share, - )?; - - amount + Uint128::one() } - } - PairType::StableSwap { amp: _ } => { - // TODO: Handle stableswap + }; - Uint128::one() - } - }; - - // if the unlocking duration is set, lock the LP tokens in the incentive manager - if let Some(unlocking_duration) = unlocking_duration { - // 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, - share, - )?); - - // lock the lp tokens in the incentive manager on behalf of the receiver - messages.push( - wasm_execute( - config.incentive_manager_addr, - &white_whale_std::incentive_manager::ExecuteMsg::ManagePosition { - action: white_whale_std::incentive_manager::PositionAction::Fill { - identifier: lock_position_identifier, - unlocking_duration, - receiver: Some(receiver.clone()), + // if the unlocking duration is set, lock the LP tokens in the incentive manager + if let Some(unlocking_duration) = unlocking_duration { + // 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, + share, + )?); + + // lock the lp tokens in the incentive manager on behalf of the receiver + messages.push( + wasm_execute( + config.incentive_manager_addr, + &white_whale_std::incentive_manager::ExecuteMsg::ManagePosition { + action: white_whale_std::incentive_manager::PositionAction::Fill { + identifier: lock_position_identifier, + unlocking_duration, + receiver: Some(receiver.clone()), + }, }, - }, - coins(share.u128(), liquidity_token), - )? - .into(), - ); - } else { - // if not, just mint the LP tokens to the receiver - messages.push(white_whale_std::lp_common::mint_lp_token_msg( - liquidity_token, - &deps.api.addr_validate(&receiver)?, - &env.contract.address, - share, - )?); - } + coins(share.u128(), liquidity_token), + )? + .into(), + ); + } else { + // if not, just mint the LP tokens to the receiver + messages.push(white_whale_std::lp_common::mint_lp_token_msg( + liquidity_token, + &deps.api.addr_validate(&receiver)?, + &env.contract.address, + share, + )?); + } - pair.assets = pool_assets.clone(); + pair.assets = pool_assets.clone(); - PAIRS.save(deps.storage, &pair_identifier, &pair)?; + PAIRS.save(deps.storage, &pair_identifier, &pair)?; - Ok(Response::new().add_messages(messages).add_attributes(vec![ - ("action", "provide_liquidity"), - ("sender", info.sender.as_str()), - ("receiver", receiver.as_str()), - ( - "assets", - &pool_assets - .iter() - .map(|asset| asset.to_string()) - .collect::>() - .join(", "), - ), - ("share", &share.to_string()), - ])) + Ok(Response::new().add_messages(messages).add_attributes(vec![ + ("action", "provide_liquidity"), + ("sender", info.sender.as_str()), + ("receiver", receiver.as_str()), + ( + "assets", + &pool_assets + .iter() + .map(|asset| asset.to_string()) + .collect::>() + .join(", "), + ), + ("share", &share.to_string()), + ])) + } } /// Withdraws the liquidity. The user burns the LP tokens in exchange for the tokens provided, including