diff --git a/contracts/liquidity_hub/pool-manager/src/contract.rs b/contracts/liquidity_hub/pool-manager/src/contract.rs index 66d60308..0e7ad73d 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_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, PAIRS, 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 +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 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 472194b5..b40423b7 100644 --- a/contracts/liquidity_hub/pool-manager/src/helpers.rs +++ b/contracts/liquidity_hub/pool-manager/src/helpers.rs @@ -3,12 +3,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; @@ -172,6 +174,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"))] { @@ -660,3 +665,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 dc45103f..96026258 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, SwapRoute, SwapRouteCreatorResponse, SwapRouteResponse, - SwapRoutesResponse, -}; -use white_whale_std::pool_network::{ - asset::PairType, - pair::{ReverseSimulationResponse, SimulationResponse}, - // router::SimulateSwapOperationsResponse, + AssetDecimalsResponse, Config, ReverseSimulationResponse, SimulationResponse, SwapRoute, + SwapRouteCreatorResponse, SwapRouteResponse, SwapRoutesResponse, }; +use white_whale_std::pool_network::asset::PairType; use crate::state::MANAGER_CONFIG; 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 c928aa7c..2002fb4a 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 crate::{ @@ -7,6 +8,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, @@ -16,7 +18,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. @@ -100,36 +104,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 4f50473c..0944cda8 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"))); }, ); @@ -1735,7 +1734,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); }, ); @@ -1926,7 +1925,6 @@ mod swapping { amount: Uint128::from(1000u128), }, |result| { - println!("{:?}", result); *simulated_offer_amount.borrow_mut() = result.unwrap().offer_amount; }, ); @@ -1954,7 +1952,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() { @@ -2068,7 +2065,6 @@ mod swapping { amount: Uint128::from(1000u128), }, |result| { - println!("{:?}", result); *simulated_return_amount.borrow_mut() = result.unwrap().return_amount; }, ); @@ -2129,7 +2125,6 @@ mod swapping { amount: Uint128::from(1000u128), }, |result| { - println!("{:?}", result); *simulated_offer_amount.borrow_mut() = result.unwrap().offer_amount; }, ); @@ -2157,7 +2152,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() { @@ -2296,7 +2290,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() { @@ -2320,12 +2313,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)); }, ); } @@ -2869,10 +2862,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(), @@ -2912,7 +2905,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 @@ -2963,30 +2955,9 @@ mod provide_liquidity { 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 {} => {} + ContractError::EmptyPoolForSingleSideLiquidityProvision {} => {} _ => panic!( - "Wrong error type, should return ContractError::InvalidZeroAmount" + "Wrong error type, should return ContractError::EmptyPoolForSingleSideLiquidityProvision" ), } }, @@ -2994,31 +2965,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(), @@ -3037,7 +2983,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"), @@ -3102,7 +3047,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) })); @@ -3138,11 +3082,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!( @@ -3189,7 +3154,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(), @@ -3207,6 +3172,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(); @@ -3254,7 +3221,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(), @@ -3270,6 +3237,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 d1fa6b84..f4e8fdc0 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, @@ -789,7 +795,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 9fc28f10..b19098bb 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,