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" }, diff --git a/contracts/liquidity_hub/pool-manager/src/contract.rs b/contracts/liquidity_hub/pool-manager/src/contract.rs index b002893e..e27b12ec 100644 --- a/contracts/liquidity_hub/pool-manager/src/contract.rs +++ b/contracts/liquidity_hub/pool-manager/src/contract.rs @@ -1,13 +1,19 @@ 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, 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 +23,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 +52,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 +112,7 @@ pub fn execute( pair_identifier, ), ExecuteMsg::ProvideLiquidity { + max_spread, slippage_tolerance, receiver, pair_identifier, @@ -80,6 +123,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 d6e8a4b9..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; @@ -65,6 +66,15 @@ pub enum ContractError { #[error("{asset} is invalid")] InvalidAsset { asset: String }, + #[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 {}, @@ -80,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 {}, @@ -116,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/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 6f388568..d0f41319 100644 --- a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs @@ -1,7 +1,16 @@ use cosmwasm_std::{ - coins, wasm_execute, BankMsg, Coin, CosmosMsg, DepsMut, Env, MessageInfo, Response, + 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}, @@ -13,14 +22,14 @@ 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::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; -// todo allow providing liquidity with a single asset +pub const MAX_ASSETS_PER_POOL: usize = 4; #[allow(clippy::too_many_arguments)] pub fn provide_liquidity( @@ -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,170 +45,275 @@ 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 mut assets = info.funds.clone(); - let mut messages: Vec = vec![]; + let deposits = aggregate_coins(info.funds.clone())?; - //TODO verify that the assets sent in info match the ones from the pool!!! + ensure!(!deposits.is_empty(), ContractError::EmptyAssets); - 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)?; - } + // 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 {} + ); - // 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()) { - return Err(ContractError::InvalidZeroAmount {}); - } + 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() == 1usize; - let liquidity_token = pair.lp_denom.clone(); + if is_single_asset_provision { + ensure!( + !pool_assets.iter().any(|asset| asset.amount.is_zero()), + ContractError::EmptyPoolForSingleSideLiquidityProvision + ); - // Compute share and other logic based on the number of assets - let total_share = get_total_share(&deps.as_ref(), liquidity_token.clone())?; + 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_floor((2u64, 1u64))?, + }; + + 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 + .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") + ); - 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, - )); - } + 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, + }, + }, + )?; + + 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")])) + } else { + for asset in deposits.iter() { + let asset_denom = &asset.denom; - 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 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 - .multiply_ratio(total_share, pool_assets[0].amount), - pool_assets[1] - .amount - .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, - )?; - - share - } + 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)?; } - PairType::StableSwap { amp: _ } => { - // TODO: Handle stableswap - Uint128::one() + // 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 {}); } - }; - // mint LP token to sender - let receiver = receiver.unwrap_or_else(|| info.sender.to_string()); + 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 - // 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()), + 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()), + }, }, - }, - 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, - &info.sender, - &env.contract.address, - share, - )?); - } - - pair.assets = pool_assets.clone(); - PAIRS.save(deps.storage, &pair_identifier, &pair)?; + 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, + )?); + } - 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()), - ])) + pair.assets = pool_assets.clone(); + + 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()), + ])) + } } /// Withdraws the liquidity. The user burns the LP tokens in exchange for the tokens provided, including @@ -229,15 +344,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/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..183d3dcd 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, - SwapRouteResponse, SwapRoutesResponse, -}; -use white_whale_std::pool_network::{ - asset::PairType, - pair::{ReverseSimulationResponse, SimulationResponse}, - // router::SimulateSwapOperationsResponse, + AssetDecimalsResponse, Config, PairInfoResponse, ReverseSimulationResponse, SimulationResponse, + SwapRoute, SwapRouteCreatorResponse, SwapRouteResponse, SwapRoutesResponse, }; +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 90954161..bd75c667 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(), @@ -114,7 +115,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 +322,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::*; @@ -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(), @@ -750,7 +756,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"))); }, ); @@ -761,6 +766,7 @@ mod router { "uluna-uusd".to_string(), None, None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -896,6 +902,7 @@ mod router { "whale-uluna".to_string(), None, None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -919,6 +926,7 @@ mod router { "uluna-uusd".to_string(), None, None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -1120,6 +1128,7 @@ mod router { "whale-uluna".to_string(), None, None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -1143,6 +1152,7 @@ mod router { "uluna-uusd".to_string(), None, None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -1281,6 +1291,7 @@ mod router { "whale-uluna".to_string(), None, None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -1300,6 +1311,7 @@ mod router { "uluna-uusd".to_string(), None, None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -1347,7 +1359,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(), }) ); }); @@ -1443,6 +1455,7 @@ mod router { "whale-uluna".to_string(), None, None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -1462,6 +1475,7 @@ mod router { "uluna-uusd".to_string(), None, None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -1554,7 +1568,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(), }) ); }); @@ -1639,6 +1653,7 @@ mod router { "whale-uluna".to_string(), None, None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -1662,6 +1677,7 @@ mod router { "uluna-uusd".to_string(), None, None, + None, vec![ Coin { denom: "uluna".to_string(), @@ -1752,7 +1768,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); }, ); @@ -1848,6 +1864,7 @@ mod swapping { "whale-uluna".to_string(), None, None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -2069,6 +2086,7 @@ mod swapping { "whale-uluna".to_string(), None, None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -2094,7 +2112,6 @@ mod swapping { amount: Uint128::from(1000u128), }, |result| { - println!("{:?}", result); *simulated_return_amount.borrow_mut() = result.unwrap().return_amount; }, ); @@ -2155,7 +2172,6 @@ mod swapping { amount: Uint128::from(1000u128), }, |result| { - println!("{:?}", result); *simulated_offer_amount.borrow_mut() = result.unwrap().offer_amount; }, ); @@ -2183,7 +2199,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() { @@ -2272,6 +2287,7 @@ mod swapping { "whale-uluna".to_string(), None, None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -2322,7 +2338,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 +2361,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)); }, ); } @@ -2570,6 +2585,7 @@ mod locking_lp { "whale-uluna".to_string(), Some(86_400u64), None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -2619,7 +2635,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, @@ -2635,6 +2651,7 @@ mod locking_lp { "whale-uluna".to_string(), Some(200_000u64), None, + None, vec![ Coin { denom: "uwhale".to_string(), @@ -2669,7 +2686,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 +2694,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, @@ -2757,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(), @@ -2806,7 +2824,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, @@ -2822,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(), @@ -2857,7 +2876,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 +2885,603 @@ mod locking_lp { }); } } + +mod provide_liquidity { + 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; + + 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::percent(1), + }, + swap_fee: Fee { + share: Decimal::percent(1), + }, + burn_fee: Fee { + share: Decimal::zero(), + }, + extra_fees: vec![], + }; + + #[cfg(feature = "osmosis")] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::percent(1), + }, + swap_fee: Fee { + share: Decimal::percent(1), + }, + 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 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, + 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, + 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, + None, + vec![Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000_000u128), + }], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::EmptyPoolForSingleSideLiquidityProvision {} => {} + _ => panic!( + "Wrong error type, should return ContractError::EmptyPoolForSingleSideLiquidityProvision" + ), + } + }, + ); + + // let's provide liquidity with two assets + suite + .provide_liquidity( + creator.clone(), + "whale-uluna".to_string(), + None, + 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(); + + match err { + ContractError::AssetMismatch {} => {} + _ => panic!("Wrong error type, should return ContractError::AssetMismatch"), + } + }, + ) + .provide_liquidity( + creator.clone(), + "whale-uluna".to_string(), + 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), + }, + ], + |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, + 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| { + 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(); + }, + ) + .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(2_000_000u128)); + assert_eq!(luna.amount, Uint128::from(996_667u128)); + }); + + 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(996_667u128), + }, + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(2_000_000u128), + }, + ] + ); + }) + .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_497_835u128), + }, + 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_999_000u128), + }, + ] + ); + }); + + let bonding_manager = suite.bonding_manager_addr.clone(); + + 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(9_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_498_333u128), + }, + 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_999_999u128), + }, + ] + ); + }) + .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(3_333u128), + }, + Coin { + denom: "uusd".to_string(), + amount: Uint128::from(1_000u128), + }, + ] + ); + }); + } + + #[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(); + }, + ); + } +} diff --git a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs index 236167c4..39a5da6f 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs @@ -1,10 +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, - SimulateSwapOperationsResponse, SwapOperation, SwapRouteCreatorResponse, SwapRouteResponse, - SwapRoutesResponse, + 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}; @@ -17,7 +18,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; @@ -26,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) } @@ -319,12 +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, receiver: None, unlocking_duration, lock_position_identifier, @@ -767,4 +770,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()); + + 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 + } } 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,