From ec833adaf27db9ef7cdddbe19ec03cec508e39e9 Mon Sep 17 00:00:00 2001 From: kaimen-sano Date: Thu, 15 Feb 2024 18:09:17 +1300 Subject: [PATCH 01/15] feat(pool-manager): add router logic This does not represent a finished product. There are many things to refine first, including * adding more integration tests * moving swap operations from the creator to a sub-user to make it easier to validate that funds end up at the right place at the end * handle fee distribution correctly * rework logic to not have to parse response attributes, but rather receive information from the `swap` function --- .../pool-manager/src/contract.rs | 36 +- .../liquidity_hub/pool-manager/src/error.rs | 3 + .../pool-manager/src/router/commands.rs | 368 ++++++++---------- .../src/tests/integration_tests.rs | 74 ++-- .../pool-manager/src/tests/suite.rs | 85 ++-- .../terraswap_router/rustfmt.toml | 2 +- packages/white-whale-std/src/pool_manager.rs | 25 +- 7 files changed, 286 insertions(+), 307 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/src/contract.rs b/contracts/liquidity_hub/pool-manager/src/contract.rs index 5951026d..16b8261b 100644 --- a/contracts/liquidity_hub/pool-manager/src/contract.rs +++ b/contracts/liquidity_hub/pool-manager/src/contract.rs @@ -1,7 +1,7 @@ use crate::error::ContractError; use crate::queries::{get_swap_route, get_swap_routes}; use crate::state::{Config, MANAGER_CONFIG, PAIRS, PAIR_COUNTER}; -use crate::{liquidity, manager, queries, swap}; +use crate::{liquidity, manager, queries, router, swap}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ @@ -157,23 +157,23 @@ pub fn execute( // ) // }, - // ExecuteMsg::ExecuteSwapOperations { - // operations, - // minimum_receive, - // to, - // max_spread, - // } => { - // let api = deps.api; - // router::commands::execute_swap_operations( - // deps, - // env, - // info.sender, - // operations, - // minimum_receive, - // optional_addr_validate(api, to)?, - // max_spread, - // ) - // } + ExecuteMsg::ExecuteSwapOperations { + operations, + minimum_receive, + to, + max_spread, + } => { + let api = deps.api; + router::commands::execute_swap_operations( + deps, + env, + info, + operations, + minimum_receive, + optional_addr_validate(api, to)?, + max_spread, + ) + } // ExecuteMsg::ExecuteSwapOperation { // operation, // to, diff --git a/contracts/liquidity_hub/pool-manager/src/error.rs b/contracts/liquidity_hub/pool-manager/src/error.rs index 8127dbf0..2b9200a2 100644 --- a/contracts/liquidity_hub/pool-manager/src/error.rs +++ b/contracts/liquidity_hub/pool-manager/src/error.rs @@ -140,6 +140,9 @@ pub enum ContractError { amount: cosmwasm_std::Uint128, expected: cosmwasm_std::Uint128, }, + + #[error("Funds for {denom} were missing when performing swap")] + MissingNativeSwapFunds { denom: String }, } impl From for ContractError { diff --git a/contracts/liquidity_hub/pool-manager/src/router/commands.rs b/contracts/liquidity_hub/pool-manager/src/router/commands.rs index c8add0ae..9b2235bd 100644 --- a/contracts/liquidity_hub/pool-manager/src/router/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/router/commands.rs @@ -1,207 +1,161 @@ -// use std::collections::HashMap; - -// use cosmwasm_std::{DepsMut, Env, Addr, Uint128, Decimal, Response, StdError, Deps, CosmosMsg, WasmMsg, to_binary, StdResult, Coin, MessageInfo}; -// use cw20::Cw20ExecuteMsg; -// use white_whale_std::{pool_network::{asset::{AssetInfo, Asset}}, pool_manager::{SwapOperation, ExecuteMsg, NPairInfo}}; - -// use crate::{ContractError, state::{MANAGER_CONFIG, get_pair_by_identifier, Config}}; - -// fn assert_operations(operations: &[SwapOperation]) -> Result<(), ContractError> { -// let mut ask_asset_map: HashMap = HashMap::new(); -// for operation in operations.iter() { -// let (offer_asset, ask_asset, _pool_identifier) = match operation { -// SwapOperation::WhaleSwap { -// token_in_info: offer_asset_info, -// token_out_info: ask_asset_info, -// pool_identifier, -// } => (offer_asset_info.clone(), ask_asset_info.clone(), pool_identifier.clone()), -// }; - -// ask_asset_map.remove(&offer_asset.to_string()); -// ask_asset_map.insert(ask_asset.to_string(), true); -// } - -// if ask_asset_map.keys().len() != 1 { -// return Err(ContractError::MultipleOutputToken {}); -// } - -// Ok(()) -// } - -// pub fn execute_swap_operations( -// deps: DepsMut, -// env: Env, -// sender: Addr, -// operations: Vec, -// minimum_receive: Option, -// to: Option, -// max_spread: Option, -// ) -> Result { -// let operations_len = operations.len(); -// if operations_len == 0 { -// return Err(StdError::generic_err("Must provide swap operations to execute").into()); -// } - -// // Assert the operations are properly set -// assert_operations(&operations)?; - -// let to = if let Some(to) = to { to } else { sender }; -// let target_asset_info = operations -// .last() -// .ok_or_else(|| ContractError::Std(StdError::generic_err("Couldn't get swap operation")))? -// .get_target_asset_info(); - -// let mut operation_index = 0; -// let mut messages: Vec = operations -// .into_iter() -// .map(|op| { -// operation_index += 1; -// Ok(CosmosMsg::Wasm(WasmMsg::Execute { -// contract_addr: env.contract.address.to_string(), -// funds: vec![], -// msg: to_binary(&ExecuteMsg::ExecuteSwapOperation { -// operation: op, -// to: if operation_index == operations_len { -// Some(to.to_string()) -// } else { -// None -// }, -// max_spread, -// })?, -// })) -// }) -// .collect::>>()?; - -// // Execute minimum amount assertion -// if let Some(minimum_receive) = minimum_receive { -// let receiver_balance = -// target_asset_info.query_balance(&deps.querier, deps.api, to.clone())?; - -// messages.push(CosmosMsg::Wasm(WasmMsg::Execute { -// contract_addr: env.contract.address.to_string(), -// funds: vec![], -// msg: to_binary(&ExecuteMsg::AssertMinimumReceive { -// asset_info: target_asset_info, -// prev_balance: receiver_balance, -// minimum_receive, -// receiver: to.to_string(), -// })?, -// })) -// } - -// Ok(Response::new().add_messages(messages)) -// } - -// /// Execute swap operation -// /// swap all offer asset to ask asset -// pub fn execute_swap_operation( -// deps: DepsMut, -// env: Env, -// info: MessageInfo, -// operation: SwapOperation, -// to: Option, -// max_spread: Option, -// ) -> Result { -// if env.contract.address != info.sender { -// return Err(ContractError::Unauthorized {}); -// } - -// let messages: Vec = match operation { -// SwapOperation::WhaleSwap { -// token_in_info, -// token_out_info, -// pool_identifier, -// } => { -// let _config: Config = MANAGER_CONFIG.load(deps.as_ref().storage)?; -// let pair_info: NPairInfo = get_pair_by_identifier( -// &deps.as_ref(), -// pool_identifier.clone(), -// )?; -// let mut offer_asset: Asset = Asset { -// info: token_in_info.clone(), -// amount: Uint128::zero(), -// }; -// // Return the offer_asset from pair_info.assets that matches token_in_info -// for asset in pair_info.assets { -// if asset.info.equal(&token_in_info) { -// offer_asset = asset; -// } -// } - -// vec![asset_into_swap_msg( -// deps.as_ref(), -// env.contract.address, -// offer_asset, -// token_out_info, -// pool_identifier, -// max_spread, -// to, -// )?] -// } -// }; - -// Ok(Response::new().add_messages(messages)) -// } - -// pub fn asset_into_swap_msg( -// _deps: Deps, -// pair_contract: Addr, -// offer_asset: Asset, -// ask_asset: AssetInfo, -// pair_identifier: String, -// max_spread: Option, -// to: Option, -// ) -> Result { -// match offer_asset.info.clone() { -// AssetInfo::NativeToken { denom } => Ok(CosmosMsg::Wasm(WasmMsg::Execute { -// contract_addr: pair_contract.to_string(), -// funds: vec![Coin { -// denom, -// amount: offer_asset.amount, -// }], -// msg: to_binary(&white_whale_std::pool_manager::ExecuteMsg::Swap { -// offer_asset, -// belief_price: None, -// max_spread, -// to, -// ask_asset, -// pair_identifier, -// })?, -// })), -// AssetInfo::Token { contract_addr } => Ok(CosmosMsg::Wasm(WasmMsg::Execute { -// contract_addr, -// funds: vec![], -// msg: to_binary(&Cw20ExecuteMsg::Send { -// contract: pair_contract.to_string(), -// amount: offer_asset.amount, -// msg: to_binary(&white_whale_std::pool_manager::Cw20HookMsg::Swap { -// belief_price: None, -// max_spread, -// to, -// ask_asset, -// pair_identifier, -// })?, -// })?, -// })), -// } -// } - -// pub fn assert_minimum_receive( -// deps: Deps, -// asset_info: AssetInfo, -// prev_balance: Uint128, -// minimum_receive: Uint128, -// receiver: Addr, -// ) -> Result { -// let receiver_balance = asset_info.query_balance(&deps.querier, deps.api, receiver)?; -// let swap_amount = receiver_balance.checked_sub(prev_balance)?; - -// if swap_amount < minimum_receive { -// return Err(ContractError::MinimumReceiveAssertion { -// minimum_receive, -// swap_amount, -// }); -// } - -// Ok(Response::default()) -// } +use std::{collections::HashMap, str::FromStr}; + +use cosmwasm_std::{ + coin, Addr, BankMsg, Decimal, DepsMut, Env, MessageInfo, Response, StdError, Uint128, +}; +use white_whale_std::{ + pool_manager::{ExecuteMsg, SwapOperation}, + pool_network::asset::{Asset, AssetInfo}, +}; + +use crate::ContractError; + +/// Checks that an arbitrary amount of [`SwapOperation`]s will not result in +/// multiple output tokens. +fn assert_operations(operations: &[SwapOperation]) -> Result<(), ContractError> { + let mut ask_asset_map: HashMap = HashMap::new(); + for operation in operations.iter() { + let (offer_asset, ask_asset, _pool_identifier) = match operation { + SwapOperation::WhaleSwap { + token_in_info: offer_asset_info, + token_out_info: ask_asset_info, + pool_identifier, + } => ( + offer_asset_info.clone(), + ask_asset_info.clone(), + pool_identifier.clone(), + ), + }; + + ask_asset_map.remove(&offer_asset.to_string()); + ask_asset_map.insert(ask_asset.to_string(), true); + } + + if ask_asset_map.keys().len() != 1 { + return Err(ContractError::MultipleOutputToken {}); + } + + Ok(()) +} + +pub fn execute_swap_operations( + mut deps: DepsMut, + env: Env, + info: MessageInfo, + operations: Vec, + minimum_receive: Option, + to: Option, + max_spread: Option, +) -> Result { + // ensure that there was at least one operation + // and retrieve the output token info + let target_asset_info = operations + .last() + .ok_or(ContractError::NoSwapOperationsProvided {})? + .get_target_asset_info(); + let target_denom = match &target_asset_info { + AssetInfo::NativeToken { denom } => denom, + _ => { + return Err(ContractError::InvalidAsset { + asset: target_asset_info.to_string(), + }) + } + }; + + let offer_asset_info = operations + .first() + .ok_or(ContractError::NoSwapOperationsProvided {})? + .get_input_asset_info(); + let offer_denom = match &offer_asset_info { + AssetInfo::NativeToken { denom } => denom, + _ => { + return Err(ContractError::InvalidAsset { + asset: offer_asset_info.to_string(), + }) + } + }; + + assert_operations(&operations)?; + + // we return the output to the sender if no alternative recipient was specified. + let to = to.unwrap_or(info.sender); + + // perform each swap operation + // we start off with the initial funds + let mut previous_swap_output = Asset { + amount: info + .funds + .iter() + .find(|token| &token.denom == offer_denom) + .ok_or(ContractError::MissingNativeSwapFunds { + denom: offer_denom.to_owned(), + })? + .amount, + info: offer_asset_info.to_owned(), + }; + + for operation in operations { + match operation { + SwapOperation::WhaleSwap { + token_in_info, + token_out_info, + pool_identifier, + } => match &token_in_info { + AssetInfo::NativeToken { denom } => { + let swap_response = crate::contract::execute( + deps.branch(), + env.clone(), + MessageInfo { + sender: env.contract.address.clone(), + funds: vec![coin(previous_swap_output.amount.u128(), denom)], + }, + ExecuteMsg::Swap { + offer_asset: previous_swap_output.clone(), + ask_asset: token_out_info.clone(), + belief_price: None, + max_spread, + to: None, + pair_identifier: pool_identifier, + }, + )?; + + // we have to find the swap amount + let swap_amount = swap_response + .attributes + .iter() + .find(|attr| attr.key == "return_amount") + .map(|amt| Uint128::from_str(&amt.value)) + .ok_or(StdError::generic_err( + "Failed to retrieve returned swap amount", + ))??; + + // update the previous swap output + previous_swap_output = Asset { + amount: swap_amount, + info: token_out_info, + } + } + AssetInfo::Token { .. } => { + return Err(StdError::generic_err("cw20 token swaps are disabled"))? + } + }, + } + } + + // Execute minimum amount assertion + let receiver_balance = previous_swap_output.amount; + if let Some(minimum_receive) = minimum_receive { + if receiver_balance < minimum_receive { + return Err(ContractError::MinimumReceiveAssertion { + minimum_receive, + swap_amount: receiver_balance, + }); + } + } + + // send output to recipient + Ok(Response::new().add_message(BankMsg::Send { + to_address: to.to_string(), + amount: vec![coin(receiver_balance.u128(), target_denom)], + })) +} 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 cc00b5bd..71e0f929 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs @@ -258,7 +258,7 @@ fn deposit_and_withdraw_cw20() { creator.clone(), suite.cw20_tokens[0].clone(), Uint128::from(1000000u128), - suite.vault_manager_addr.clone(), + suite.pool_manager_addr.clone(), ); // Lets try to add liquidity suite.provide_liquidity( @@ -469,6 +469,8 @@ mod pair_creation_failures { } mod router { + use cosmwasm_std::Event; + use super::*; #[test] fn basic_swap_operations_test() { @@ -574,10 +576,9 @@ mod router { }, ], |result| { - // Ensure we got 999000 in the response which is 1mil less the initial liquidity amount - for event in result.unwrap().events { - println!("{:?}", event); - } + // ensure we got 999,000 in the response (1m - initial liquidity amount) + let result = result.unwrap(); + assert!(result.has_event(&Event::new("wasm").add_attribute("share", "999000"))); }, ); @@ -610,18 +611,16 @@ mod router { }, ], |result| { - // Ensure we got 999000 in the response which is 1mil less the initial liquidity amount - for event in result.unwrap().events { - println!("{:?}", event); - } + // ensure we got 999,000 in the response (1m - initial liquidity amount) + let result = result.unwrap(); + assert!(result.has_event(&Event::new("wasm").add_attribute("share", "999000"))); }, ); - // Prepare teh swap operations, we want to go from WHALE -> UUSD - // We will use the uluna-uusd pair as the intermediary - // use this type white_whale_std::pool_manager::SwapOperation + // Prepare the swap operations, we want to go from WHALE -> UUSD + // We will use the uluna-uusd pair as the intermediary pool - let _swap_operations = vec![ + let swap_operations = vec![ white_whale_std::pool_manager::SwapOperation::WhaleSwap { token_in_info: AssetInfo::NativeToken { denom: "uwhale".to_string(), @@ -642,27 +641,34 @@ mod router { }, ]; - // suite.execute_swap_operations( - // creator.clone(), swap_operations, None, None, None,vec![coin(1000u128, "uwhale".to_string())], |result| { - // // Find the key with 'offer_amount' and the key with 'return_amount' - // // Ensure that the offer amount is 1000 and the return amount is greater than 0 - // let mut return_amount = String::new(); - // let mut offer_amount = String::new(); - - // for event in result.unwrap().events { - // println!("{:?}", event); - // if event.ty == "wasm" { - // for attribute in event.attributes { - // match attribute.key.as_str() { - // "return_amount" => return_amount = attribute.value, - // "offer_amount" => offer_amount = attribute.value, - // _ => {} - // } - // } - // } - // } - // // assert_ne!(true,true); - // }); + // before swap uusd balance = 1_000_000_001 + // - 2*1_000 pair creation fee + // - 1_000_000 liquidity provision + // - 1 for native token creation (for decimal precisions) + // = 998_998_000 + let pre_swap_amount = 998_998_000; + suite.query_balance(creator.to_string(), "uusd".to_string(), |amt| { + assert_eq!(amt.unwrap().amount.u128(), pre_swap_amount); + }); + + suite.execute_swap_operations( + creator.clone(), + swap_operations, + None, + None, + None, + vec![coin(1000u128, "uwhale".to_string())], + |result| { + result.unwrap(); + }, + ); + + // ensure that the whale got swapped to an appropriate amount of uusd + // we swap 1000 whale for 998 uusd + let post_swap_amount = pre_swap_amount + 998; + suite.query_balance(creator.to_string(), "uusd".to_string(), |amt| { + assert_eq!(amt.unwrap().amount.u128(), post_swap_amount); + }); } } diff --git a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs index 0800308c..91dda94a 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs @@ -58,7 +58,7 @@ pub struct TestingSuite { app: App, pub senders: [Addr; 3], pub whale_lair_addr: Addr, - pub vault_manager_addr: Addr, + pub pool_manager_addr: Addr, pub cw20_tokens: Vec, } @@ -130,7 +130,7 @@ impl TestingSuite { app, senders: [sender_1, sender_2, sender_3], whale_lair_addr: Addr::unchecked(""), - vault_manager_addr: Addr::unchecked(""), + pool_manager_addr: Addr::unchecked(""), cw20_tokens: vec![], } } @@ -156,14 +156,14 @@ impl TestingSuite { }, }; - let vault_manager_id = self.app.store_code(contract_pool_manager()); + let pool_manager_id = self.app.store_code(contract_pool_manager()); let creator = self.creator().clone(); - self.vault_manager_addr = self + self.pool_manager_addr = self .app .instantiate_contract( - vault_manager_id, + pool_manager_id, creator.clone(), &msg, &[], @@ -309,7 +309,7 @@ impl TestingSuite { self.app .execute_contract( sender, - self.vault_manager_addr.clone(), + self.pool_manager_addr.clone(), &msg, &[Coin { denom: native_token_denom.to_string(), @@ -335,7 +335,7 @@ impl TestingSuite { result( self.app - .execute_contract(sender, self.vault_manager_addr.clone(), &msg, &[]), + .execute_contract(sender, self.pool_manager_addr.clone(), &msg, &[]), ); self @@ -359,7 +359,7 @@ impl TestingSuite { result( self.app - .execute_contract(sender, self.vault_manager_addr.clone(), &msg, &funds), + .execute_contract(sender, self.pool_manager_addr.clone(), &msg, &funds), ); self @@ -389,32 +389,37 @@ impl TestingSuite { result( self.app - .execute_contract(sender, self.vault_manager_addr.clone(), &msg, &funds), + .execute_contract(sender, self.pool_manager_addr.clone(), &msg, &funds), ); self } - // #[track_caller] - // pub(crate) fn execute_swap_operations( - // &mut self, - // sender: Addr, - // operations: Vec, - // minimum_receive: Option, - // to: Option, - // max_spread: Option, - // funds: Vec, - // result: impl Fn(Result), - // ) -> &mut Self { - // let msg = white_whale_std::pool_manager::ExecuteMsg::ExecuteSwapOperations { operations, minimum_receive, to, max_spread }; - - // result( - // self.app - // .execute_contract(sender, self.vault_manager_addr.clone(), &msg, &funds), - // ); - - // self - // } + #[track_caller] + pub(crate) fn execute_swap_operations( + &mut self, + sender: Addr, + operations: Vec, + minimum_receive: Option, + to: Option, + max_spread: Option, + funds: Vec, + result: impl Fn(Result), + ) -> &mut Self { + let msg = white_whale_std::pool_manager::ExecuteMsg::ExecuteSwapOperations { + operations, + minimum_receive, + to, + max_spread, + }; + + result( + self.app + .execute_contract(sender, self.pool_manager_addr.clone(), &msg, &funds), + ); + + self + } #[track_caller] pub(crate) fn create_pair( @@ -438,7 +443,7 @@ impl TestingSuite { result(self.app.execute_contract( sender, - self.vault_manager_addr.clone(), + self.pool_manager_addr.clone(), &msg, &pair_creation_fee_funds, )); @@ -461,7 +466,7 @@ impl TestingSuite { result( self.app - .execute_contract(sender, self.vault_manager_addr.clone(), &msg, &[]), + .execute_contract(sender, self.pool_manager_addr.clone(), &msg, &[]), ); self @@ -481,7 +486,7 @@ impl TestingSuite { // Send the cw20 amount with a message let msg = cw20::Cw20ExecuteMsg::Send { - contract: self.vault_manager_addr.to_string(), + contract: self.pool_manager_addr.to_string(), amount: amount, msg: to_json_binary(&Cw20HookMsg::WithdrawLiquidity { pair_identifier: pair_identifier, @@ -506,7 +511,7 @@ impl TestingSuite { ) -> &mut Self { let ownership_response: StdResult> = self.app.wrap().query_wasm_smart( - &self.vault_manager_addr, + &self.pool_manager_addr, &white_whale_std::pool_manager::QueryMsg::Ownership {}, ); @@ -529,12 +534,12 @@ impl TestingSuite { } pub(crate) fn query_pair_info( - &mut self, + &self, pair_identifier: String, result: impl Fn(StdResult), - ) -> &mut Self { + ) -> &Self { let pair_info_response: StdResult = self.app.wrap().query_wasm_smart( - &self.vault_manager_addr, + &self.pool_manager_addr, &white_whale_std::pool_manager::QueryMsg::Pair { pair_identifier: pair_identifier, }, @@ -553,7 +558,7 @@ impl TestingSuite { result: impl Fn(StdResult), ) -> &mut Self { let pair_info_response: StdResult = self.app.wrap().query_wasm_smart( - &self.vault_manager_addr, + &self.pool_manager_addr, &white_whale_std::pool_manager::QueryMsg::Simulation { offer_asset, ask_asset: Asset { @@ -578,7 +583,7 @@ impl TestingSuite { ) -> &mut Self { let pair_info_response: StdResult = self.app.wrap().query_wasm_smart( - &self.vault_manager_addr, + &self.pool_manager_addr, &white_whale_std::pool_manager::QueryMsg::ReverseSimulation { offer_asset: Asset { amount: Uint128::zero(), @@ -605,7 +610,7 @@ impl TestingSuite { .app .wrap() .query_wasm_smart( - &self.vault_manager_addr, + &self.pool_manager_addr, &white_whale_std::pool_manager::QueryMsg::Pair { pair_identifier: identifier, }, @@ -645,7 +650,7 @@ impl TestingSuite { .app .wrap() .query_wasm_smart( - &self.vault_manager_addr, + &self.pool_manager_addr, &white_whale_std::pool_manager::QueryMsg::Pair { pair_identifier: identifier, }, diff --git a/contracts/liquidity_hub/pool-network/terraswap_router/rustfmt.toml b/contracts/liquidity_hub/pool-network/terraswap_router/rustfmt.toml index 6918e223..0132d878 100644 --- a/contracts/liquidity_hub/pool-network/terraswap_router/rustfmt.toml +++ b/contracts/liquidity_hub/pool-network/terraswap_router/rustfmt.toml @@ -1,5 +1,5 @@ # stable -newline_style = "unix" +newline_style = "Unix" hard_tabs = false tab_spaces = 4 diff --git a/packages/white-whale-std/src/pool_manager.rs b/packages/white-whale-std/src/pool_manager.rs index 5cb77d84..10068f99 100644 --- a/packages/white-whale-std/src/pool_manager.rs +++ b/packages/white-whale-std/src/pool_manager.rs @@ -34,6 +34,13 @@ pub enum SwapOperation { } impl SwapOperation { + /// Retrieves the `token_in_info` [`AssetInfo`] used for this swap operation. + pub fn get_input_asset_info(&self) -> &AssetInfo { + match self { + SwapOperation::WhaleSwap { token_in_info, .. } => token_in_info, + } + } + pub fn get_target_asset_info(&self) -> AssetInfo { match self { SwapOperation::WhaleSwap { token_out_info, .. } => token_out_info.clone(), @@ -166,13 +173,17 @@ pub enum ExecuteMsg { decimals: u8, }, - // /// Execute multiple [SwapOperation]s, i.e. multi-hop swaps. - // ExecuteSwapOperations { - // operations: Vec, - // minimum_receive: Option, - // to: Option, - // max_spread: Option, - // }, + /// Execute multiple [`SwapOperations`] to allow for multi-hop swaps. + ExecuteSwapOperations { + operations: Vec, + /// The minimum amount of the output (i.e., final swap operation token) required for the message to succeed. + minimum_receive: Option, + /// The (optional) recipient of the output tokens. + /// + /// If left unspecified, tokens will be sent to the sender of the message. + to: Option, + max_spread: Option, + }, // /// Swap the offer to ask token. This message can only be called internally by the router contract. // ExecuteSwapOperation { // operation: SwapOperation, From eb86f41d82251fbbb7301f8248d27dafe99421c5 Mon Sep 17 00:00:00 2001 From: kaimen-sano Date: Tue, 27 Feb 2024 02:51:26 +1300 Subject: [PATCH 02/15] refactor(pool-manager): move internal swap logic to helper func Moves the asset -> asset swap logic to a helper function, `perform_swap`, which can then be used by each piece of relevant code (inside the router and the swap contract executors). This reduces code duplication and makes a cleaner, more unified interface. --- .../pool-manager/src/contract.rs | 12 +- .../pool-manager/src/router/commands.rs | 41 +---- .../pool-manager/src/swap/commands.rs | 167 ++++++------------ .../pool-manager/src/swap/mod.rs | 1 + .../pool-manager/src/swap/perform_swap.rs | 137 ++++++++++++++ 5 files changed, 206 insertions(+), 152 deletions(-) create mode 100644 contracts/liquidity_hub/pool-manager/src/swap/perform_swap.rs diff --git a/contracts/liquidity_hub/pool-manager/src/contract.rs b/contracts/liquidity_hub/pool-manager/src/contract.rs index 16b8261b..081b1444 100644 --- a/contracts/liquidity_hub/pool-manager/src/contract.rs +++ b/contracts/liquidity_hub/pool-manager/src/contract.rs @@ -94,21 +94,12 @@ pub fn execute( to, pair_identifier, } => { - // check if the swap feature is enabled - let feature_toggle: FeatureToggle = MANAGER_CONFIG.load(deps.storage)?.feature_toggle; - if !feature_toggle.swaps_enabled { - return Err(ContractError::OperationDisabled("swap".to_string())); - } - - if !offer_asset.is_native_token() { - return Err(ContractError::Unauthorized {}); - } - let to_addr = if let Some(to_addr) = to { Some(deps.api.addr_validate(&to_addr)?) } else { None }; + swap::commands::swap( deps, env, @@ -166,7 +157,6 @@ pub fn execute( let api = deps.api; router::commands::execute_swap_operations( deps, - env, info, operations, minimum_receive, diff --git a/contracts/liquidity_hub/pool-manager/src/router/commands.rs b/contracts/liquidity_hub/pool-manager/src/router/commands.rs index 9b2235bd..e6908610 100644 --- a/contracts/liquidity_hub/pool-manager/src/router/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/router/commands.rs @@ -8,7 +8,7 @@ use white_whale_std::{ pool_network::asset::{Asset, AssetInfo}, }; -use crate::ContractError; +use crate::{swap::perform_swap::perform_swap, ContractError}; /// Checks that an arbitrary amount of [`SwapOperation`]s will not result in /// multiple output tokens. @@ -40,7 +40,6 @@ fn assert_operations(operations: &[SwapOperation]) -> Result<(), ContractError> pub fn execute_swap_operations( mut deps: DepsMut, - env: Env, info: MessageInfo, operations: Vec, minimum_receive: Option, @@ -98,42 +97,20 @@ pub fn execute_swap_operations( match operation { SwapOperation::WhaleSwap { token_in_info, - token_out_info, pool_identifier, + .. } => match &token_in_info { - AssetInfo::NativeToken { denom } => { - let swap_response = crate::contract::execute( + AssetInfo::NativeToken { .. } => { + let swap_result = perform_swap( deps.branch(), - env.clone(), - MessageInfo { - sender: env.contract.address.clone(), - funds: vec![coin(previous_swap_output.amount.u128(), denom)], - }, - ExecuteMsg::Swap { - offer_asset: previous_swap_output.clone(), - ask_asset: token_out_info.clone(), - belief_price: None, - max_spread, - to: None, - pair_identifier: pool_identifier, - }, + previous_swap_output, + pool_identifier, + None, + max_spread, )?; - // we have to find the swap amount - let swap_amount = swap_response - .attributes - .iter() - .find(|attr| attr.key == "return_amount") - .map(|amt| Uint128::from_str(&amt.value)) - .ok_or(StdError::generic_err( - "Failed to retrieve returned swap amount", - ))??; - // update the previous swap output - previous_swap_output = Asset { - amount: swap_amount, - info: token_out_info, - } + previous_swap_output = swap_result.return_asset; } AssetInfo::Token { .. } => { return Err(StdError::generic_err("cw20 token swaps are disabled"))? diff --git a/contracts/liquidity_hub/pool-manager/src/swap/commands.rs b/contracts/liquidity_hub/pool-manager/src/swap/commands.rs index f4912f35..b323e035 100644 --- a/contracts/liquidity_hub/pool-manager/src/swap/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/swap/commands.rs @@ -1,12 +1,7 @@ use cosmwasm_std::{Addr, CosmosMsg, DepsMut, Env, MessageInfo, Response}; use white_whale_std::pool_network::asset::{Asset, AssetInfo}; -use crate::helpers; -use crate::state::{get_decimals, get_pair_by_identifier}; -use crate::{ - state::{MANAGER_CONFIG, PAIRS}, - ContractError, -}; +use crate::{state::MANAGER_CONFIG, ContractError}; #[cfg(any(feature = "token_factory", feature = "osmosis_token_factory"))] use cosmwasm_std::coins; #[cfg(any(feature = "token_factory", feature = "osmosis_token_factory"))] @@ -24,6 +19,9 @@ pub const MAX_ASSETS_PER_POOL: usize = 4; pub const LP_SYMBOL: &str = "uLP"; use cosmwasm_std::Decimal; + +use super::perform_swap::perform_swap; + #[allow(clippy::too_many_arguments)] pub fn swap( deps: DepsMut, @@ -37,117 +35,65 @@ pub fn swap( to: Option, pair_identifier: String, ) -> Result { + // ensure only native tokens are being swapped + // in the future, we are removing cw20 tokens + if !offer_asset.is_native_token() { + return Err(ContractError::Unauthorized {}); + } + let config = MANAGER_CONFIG.load(deps.storage)?; // check if the deposit feature is enabled if !config.feature_toggle.deposits_enabled { return Err(ContractError::OperationDisabled("swap".to_string())); } - - offer_asset.assert_sent_native_token_balance(&info)?; - - let mut pair_info = get_pair_by_identifier(&deps.as_ref(), pair_identifier.clone())?; - let pools = pair_info.assets.clone(); - // determine what's the offer and ask pool based on the offer_asset - let offer_pool: Asset; - let ask_pool: Asset; - let offer_decimal: u8; - let ask_decimal: u8; - let decimals = get_decimals(&pair_info); - // We now have the pools and pair info; we can now calculate the swap - // Verify the pool - if offer_asset.info.equal(&pools[0].info) { - offer_pool = pools[0].clone(); - ask_pool = pools[1].clone(); - offer_decimal = decimals[0]; - ask_decimal = decimals[1]; - } else if offer_asset.info.equal(&pools[1].info) { - offer_pool = pools[1].clone(); - ask_pool = pools[0].clone(); - - offer_decimal = decimals[1]; - ask_decimal = decimals[0]; - } else { - return Err(ContractError::AssetMismatch {}); + // check if the swap feature is enabled + if !config.feature_toggle.swaps_enabled { + return Err(ContractError::OperationDisabled("swap".to_string())); } - let mut messages: Vec = vec![]; - - let receiver = to.unwrap_or_else(|| sender.clone()); - - let offer_amount = offer_asset.amount; - let pool_fees = pair_info.pool_fees.clone(); - - let swap_computation = helpers::compute_swap( - offer_pool.amount, - ask_pool.amount, - offer_amount, - pool_fees, - &pair_info.pair_type, - offer_decimal, - ask_decimal, - )?; - - let return_asset = Asset { - info: ask_pool.info.clone(), - amount: swap_computation.return_amount, - }; + offer_asset.assert_sent_native_token_balance(&info)?; - // Assert spread and other operations - // check max spread limit if exist - helpers::assert_max_spread( + // perform the swap + let swap_result = perform_swap( + deps, + offer_asset.clone(), + pair_identifier, belief_price, max_spread, - offer_asset.clone(), - return_asset.clone(), - swap_computation.spread_amount, - offer_decimal, - ask_decimal, )?; - if !swap_computation.return_amount.is_zero() { - messages.push(return_asset.into_msg(receiver.clone())?); - } + // add messages + let mut messages: Vec = vec![]; + let receiver = to.unwrap_or_else(|| sender.clone()); - // State changes to the pairs balances - // Deduct the return amount from the pool and add the offer amount to the pool - if offer_asset.info.equal(&pools[0].info) { - pair_info.assets[0].amount += offer_amount; - pair_info.assets[1].amount -= swap_computation.return_amount; - PAIRS.save(deps.storage, pair_identifier, &pair_info)?; - } else { - pair_info.assets[1].amount += offer_amount; - pair_info.assets[0].amount -= swap_computation.return_amount; - PAIRS.save(deps.storage, pair_identifier, &pair_info)?; + // first we add the swap result + if !swap_result.return_asset.amount.is_zero() { + messages.push( + swap_result + .return_asset + .clone() + .into_msg(receiver.clone())?, + ); } - - // TODO: Might be handy to make the below fees into a helper method too which returns Msgs - // burn ask_asset from the pool - if !swap_computation.burn_fee_amount.is_zero() { - let burn_asset = Asset { - info: ask_pool.info.clone(), - amount: swap_computation.burn_fee_amount, - }; - messages.push(burn_asset.into_burn_msg()?); + // then we add the fees + if !swap_result.burn_fee_asset.amount.is_zero() { + messages.push(swap_result.burn_fee_asset.clone().into_burn_msg()?); } - - // Prepare a message to send the protocol fee and the swap fee to the protocol fee collector - if !swap_computation.protocol_fee_amount.is_zero() { - let protocol_fee_asset = Asset { - info: ask_pool.info.clone(), - amount: swap_computation.protocol_fee_amount, - }; - - messages.push(protocol_fee_asset.into_msg(config.fee_collector_addr.clone())?); + if !swap_result.protocol_fee_asset.amount.is_zero() { + messages.push( + swap_result + .protocol_fee_asset + .clone() + .into_msg(config.fee_collector_addr.clone())?, + ); } - - // Prepare a message to send the swap fee to the swap fee collector - if !swap_computation.swap_fee_amount.is_zero() { - let swap_fee_asset = Asset { - info: ask_pool.info.clone(), - amount: swap_computation.swap_fee_amount, - }; - - messages.push(swap_fee_asset.into_msg(config.fee_collector_addr)?); + if !swap_result.swap_fee_asset.amount.is_zero() { + messages.push( + swap_result + .swap_fee_asset + .clone() + .into_msg(config.fee_collector_addr)?, + ); } // 1. send collateral token from the contract to a user @@ -157,22 +103,25 @@ pub fn swap( ("sender", sender.as_str()), ("receiver", receiver.as_str()), ("offer_asset", &offer_asset.info.to_string()), - ("ask_asset", &ask_pool.info.to_string()), - ("offer_amount", &offer_amount.to_string()), - ("return_amount", &swap_computation.return_amount.to_string()), - ("spread_amount", &swap_computation.spread_amount.to_string()), + ("ask_asset", &swap_result.return_asset.info.to_string()), + ("offer_amount", &offer_asset.amount.to_string()), + ( + "return_amount", + &swap_result.return_asset.amount.to_string(), + ), + ("spread_amount", &swap_result.spread_amount.to_string()), ( "swap_fee_amount", - &swap_computation.swap_fee_amount.to_string(), + &swap_result.swap_fee_asset.amount.to_string(), ), ( "protocol_fee_amount", - &swap_computation.protocol_fee_amount.to_string(), + &swap_result.protocol_fee_asset.amount.to_string(), ), ( "burn_fee_amount", - &swap_computation.burn_fee_amount.to_string(), + &swap_result.burn_fee_asset.amount.to_string(), ), - ("swap_type", pair_info.pair_type.get_label()), + ("swap_type", swap_result.pair_info.pair_type.get_label()), ])) } diff --git a/contracts/liquidity_hub/pool-manager/src/swap/mod.rs b/contracts/liquidity_hub/pool-manager/src/swap/mod.rs index 82b6da3c..a9cbdae0 100644 --- a/contracts/liquidity_hub/pool-manager/src/swap/mod.rs +++ b/contracts/liquidity_hub/pool-manager/src/swap/mod.rs @@ -1 +1,2 @@ pub mod commands; +pub(crate) mod perform_swap; diff --git a/contracts/liquidity_hub/pool-manager/src/swap/perform_swap.rs b/contracts/liquidity_hub/pool-manager/src/swap/perform_swap.rs new file mode 100644 index 00000000..cfdb4df2 --- /dev/null +++ b/contracts/liquidity_hub/pool-manager/src/swap/perform_swap.rs @@ -0,0 +1,137 @@ +use cosmwasm_std::{Decimal, DepsMut, Uint128}; +use white_whale_std::{pool_manager::NPairInfo, pool_network::asset::Asset}; + +use crate::{ + helpers, + state::{get_pair_by_identifier, PAIRS}, + ContractError, +}; + +pub struct SwapResult { + /// The asset that should be returned to the user from the swap. + pub return_asset: Asset, + /// The burn fee of `return_asset` associated with this swap transaction. + pub burn_fee_asset: Asset, + /// The protocol fee of `return_asset` associated with this swap transaction. + pub protocol_fee_asset: Asset, + /// The swap fee of `return_asset` associated with this swap transaction. + pub swap_fee_asset: Asset, + + /// The pair that was traded. + pub pair_info: NPairInfo, + /// The amount of spread that occurred during the swap from the original exchange rate. + pub spread_amount: Uint128, +} + +/// Attempts to perform a swap from `offer_asset` to the relevant opposing +/// asset in the pair identified by `pair_identifier`. +/// +/// Assumes that `offer_asset` is a **native token**. +/// +/// The resulting [`SwapResult`] has actions that should be taken, as the swap has been performed. +/// In other words, the caller of the `perform_swap` function _should_ make use +/// of each field in [`SwapResult`] (besides fields like `spread_amount`). +#[must_use] +pub fn perform_swap( + deps: DepsMut, + offer_asset: Asset, + pair_identifier: String, + belief_price: Option, + max_spread: Option, +) -> Result { + let mut pair_info = get_pair_by_identifier(&deps.as_ref(), pair_identifier.clone())?; + let pools = &pair_info.assets; + + // compute the offer and ask pool + let offer_pool: Asset; + let ask_pool: Asset; + let offer_decimal: u8; + let ask_decimal: u8; + let decimals = &pair_info.asset_decimals; + + // calculate the swap + // first, set relevant variables + if offer_asset.info.equal(&pools[0].info) { + offer_pool = pools[0].clone(); + ask_pool = pools[1].clone(); + offer_decimal = decimals[0]; + ask_decimal = decimals[1]; + } else if offer_asset.info.equal(&pools[1].info) { + offer_pool = pools[1].clone(); + ask_pool = pools[0].clone(); + + offer_decimal = decimals[1]; + ask_decimal = decimals[0]; + } else { + return Err(ContractError::AssetMismatch {}); + } + + let offer_amount = offer_asset.amount; + let pool_fees = pair_info.pool_fees.clone(); + + let swap_computation = helpers::compute_swap( + offer_pool.amount, + ask_pool.amount, + offer_amount, + pool_fees, + &pair_info.pair_type, + offer_decimal, + ask_decimal, + )?; + + let return_asset = Asset { + info: ask_pool.info.clone(), + amount: swap_computation.return_amount, + }; + + // Assert spread and other operations + // check max spread limit if exist + helpers::assert_max_spread( + belief_price, + max_spread, + offer_asset.clone(), + return_asset.clone(), + swap_computation.spread_amount, + offer_decimal, + ask_decimal, + )?; + + // State changes to the pairs balances + // Deduct the return amount from the pool and add the offer amount to the pool + if offer_asset.info.equal(&pools[0].info) { + pair_info.assets[0].amount += offer_amount; + pair_info.assets[1].amount -= swap_computation.return_amount; + PAIRS.save(deps.storage, pair_identifier, &pair_info)?; + } else { + pair_info.assets[1].amount += offer_amount; + pair_info.assets[0].amount -= swap_computation.return_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 = Asset { + info: ask_pool.info.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 = Asset { + info: ask_pool.info.clone(), + amount: swap_computation.protocol_fee_amount, + }; + // Prepare a message to send the swap fee to the swap fee collector + let swap_fee_asset = Asset { + info: ask_pool.info.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, + }) +} From ed2d97c261fb5eb12e1f3af32764390219feb567 Mon Sep 17 00:00:00 2001 From: kaimen-sano Date: Tue, 5 Mar 2024 00:19:32 +1300 Subject: [PATCH 03/15] docs(pool_manager): add router msg rustdocs --- packages/white-whale-std/src/pool_manager.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/white-whale-std/src/pool_manager.rs b/packages/white-whale-std/src/pool_manager.rs index 10068f99..0a0272e2 100644 --- a/packages/white-whale-std/src/pool_manager.rs +++ b/packages/white-whale-std/src/pool_manager.rs @@ -175,6 +175,11 @@ pub enum ExecuteMsg { /// Execute multiple [`SwapOperations`] to allow for multi-hop swaps. ExecuteSwapOperations { + /// The operations that should be performed in sequence. + /// + /// The amount in each swap will be the output from the previous swap. + /// + /// The first swap will use whatever funds are sent in the [`MessageInfo`]. operations: Vec, /// The minimum amount of the output (i.e., final swap operation token) required for the message to succeed. minimum_receive: Option, @@ -182,6 +187,9 @@ pub enum ExecuteMsg { /// /// If left unspecified, tokens will be sent to the sender of the message. to: Option, + /// The (optional) maximum spread to incur when performing any swap. + /// + /// If left unspecified, there is no limit to what spread the transaction can incur. max_spread: Option, }, // /// Swap the offer to ask token. This message can only be called internally by the router contract. From c64256fee6259bd24907e2c3b003a64d54aa0584 Mon Sep 17 00:00:00 2001 From: kaimen-sano Date: Tue, 5 Mar 2024 00:39:32 +1300 Subject: [PATCH 04/15] fix(pool-manager): toggles for swap and withdraw enabled * Fixes incorrect usage of checking for `deposits_enabled` when withdrawing liquidity. We should use `withdraws_enabled`. * Fixes incorrect usage of checking for `deposits_enabled` when performing a swap. We should only check for `swaps_enabled`. --- .../liquidity_hub/pool-manager/src/liquidity/commands.rs | 2 +- contracts/liquidity_hub/pool-manager/src/swap/commands.rs | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs index 6f0aa2b3..23cb407d 100644 --- a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs @@ -234,7 +234,7 @@ pub fn withdraw_liquidity( ) -> Result { let config = MANAGER_CONFIG.load(deps.storage)?; // check if the deposit feature is enabled - if !config.feature_toggle.deposits_enabled { + if !config.feature_toggle.withdrawals_enabled { return Err(ContractError::OperationDisabled( "provide_liquidity".to_string(), )); diff --git a/contracts/liquidity_hub/pool-manager/src/swap/commands.rs b/contracts/liquidity_hub/pool-manager/src/swap/commands.rs index b323e035..0305c5a8 100644 --- a/contracts/liquidity_hub/pool-manager/src/swap/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/swap/commands.rs @@ -42,10 +42,6 @@ pub fn swap( } let config = MANAGER_CONFIG.load(deps.storage)?; - // check if the deposit feature is enabled - if !config.feature_toggle.deposits_enabled { - return Err(ContractError::OperationDisabled("swap".to_string())); - } // check if the swap feature is enabled if !config.feature_toggle.swaps_enabled { return Err(ContractError::OperationDisabled("swap".to_string())); From f6201bd097cdfcd90f8d7cc66e2bf98eb764b2c7 Mon Sep 17 00:00:00 2001 From: kaimen-sano Date: Tue, 5 Mar 2024 00:41:57 +1300 Subject: [PATCH 05/15] docs(pool-manager): withdraw liqudity feature check comments --- contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs index 23cb407d..046c2ff4 100644 --- a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs @@ -233,7 +233,7 @@ pub fn withdraw_liquidity( pair_identifier: String, ) -> Result { let config = MANAGER_CONFIG.load(deps.storage)?; - // check if the deposit feature is enabled + // check if the withdraw feature is enabled if !config.feature_toggle.withdrawals_enabled { return Err(ContractError::OperationDisabled( "provide_liquidity".to_string(), From 2997d175522ff8fb083a18ea5bfcab2f74325d2c Mon Sep 17 00:00:00 2001 From: kaimen-sano Date: Tue, 5 Mar 2024 00:44:02 +1300 Subject: [PATCH 06/15] chore(pool-manager): fix return error msg for withdraw disabled --- contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs index 046c2ff4..e9055d36 100644 --- a/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/liquidity/commands.rs @@ -236,7 +236,7 @@ pub fn withdraw_liquidity( // check if the withdraw feature is enabled if !config.feature_toggle.withdrawals_enabled { return Err(ContractError::OperationDisabled( - "provide_liquidity".to_string(), + "withdraw_liquidity".to_string(), )); } // Get the pair by the pair_identifier From b651a1ad0cfe5abfe84ea92ba23c6e96c23d4ea7 Mon Sep 17 00:00:00 2001 From: kaimen-sano Date: Tue, 5 Mar 2024 00:50:12 +1300 Subject: [PATCH 07/15] feat(pool-manager): check swaps are enabled in multiswap --- .../liquidity_hub/pool-manager/src/router/commands.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/contracts/liquidity_hub/pool-manager/src/router/commands.rs b/contracts/liquidity_hub/pool-manager/src/router/commands.rs index e6908610..52a9de29 100644 --- a/contracts/liquidity_hub/pool-manager/src/router/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/router/commands.rs @@ -8,7 +8,7 @@ use white_whale_std::{ pool_network::asset::{Asset, AssetInfo}, }; -use crate::{swap::perform_swap::perform_swap, ContractError}; +use crate::{state::MANAGER_CONFIG, swap::perform_swap::perform_swap, ContractError}; /// Checks that an arbitrary amount of [`SwapOperation`]s will not result in /// multiple output tokens. @@ -46,6 +46,12 @@ pub fn execute_swap_operations( to: Option, max_spread: Option, ) -> 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 that there was at least one operation // and retrieve the output token info let target_asset_info = operations From 6fd1f61be08a306281fdd0c53f16b89753f92469 Mon Sep 17 00:00:00 2001 From: kaimen-sano Date: Tue, 5 Mar 2024 02:18:25 +1300 Subject: [PATCH 08/15] feat(pool-manager): add burn fee messages for multihop swap --- .../pool-manager/src/router/commands.rs | 66 ++++++++++++++----- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/src/router/commands.rs b/contracts/liquidity_hub/pool-manager/src/router/commands.rs index 52a9de29..e05c15aa 100644 --- a/contracts/liquidity_hub/pool-manager/src/router/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/router/commands.rs @@ -1,10 +1,10 @@ -use std::{collections::HashMap, str::FromStr}; +use std::collections::HashMap; use cosmwasm_std::{ - coin, Addr, BankMsg, Decimal, DepsMut, Env, MessageInfo, Response, StdError, Uint128, + coin, Addr, BankMsg, Decimal, DepsMut, MessageInfo, Response, StdError, Uint128, }; use white_whale_std::{ - pool_manager::{ExecuteMsg, SwapOperation}, + pool_manager::SwapOperation, pool_network::asset::{Asset, AssetInfo}, }; @@ -79,15 +79,7 @@ pub fn execute_swap_operations( }) } }; - - assert_operations(&operations)?; - - // we return the output to the sender if no alternative recipient was specified. - let to = to.unwrap_or(info.sender); - - // perform each swap operation - // we start off with the initial funds - let mut previous_swap_output = Asset { + let offer_asset = Asset { amount: info .funds .iter() @@ -99,6 +91,18 @@ pub fn execute_swap_operations( info: offer_asset_info.to_owned(), }; + assert_operations(&operations)?; + + // we return the output to the sender if no alternative recipient was specified. + let to = to.unwrap_or(info.sender.clone()); + + // perform each swap operation + // we start off with the initial funds + let mut previous_swap_output = offer_asset.clone(); + + // stores messages for sending fees after the swaps + let mut fee_messages = vec![]; + for operation in operations { match operation { SwapOperation::WhaleSwap { @@ -117,6 +121,25 @@ pub fn execute_swap_operations( // update the previous swap output previous_swap_output = swap_result.return_asset; + + // add the fee messages + if !swap_result.burn_fee_asset.amount.is_zero() { + fee_messages.push(swap_result.burn_fee_asset.into_burn_msg()?); + } + if !swap_result.protocol_fee_asset.amount.is_zero() { + fee_messages.push( + swap_result + .protocol_fee_asset + .into_msg(config.fee_collector_addr.clone())?, + ); + } + if !swap_result.swap_fee_asset.amount.is_zero() { + fee_messages.push( + swap_result + .swap_fee_asset + .into_msg(config.fee_collector_addr.clone())?, + ); + } } AssetInfo::Token { .. } => { return Err(StdError::generic_err("cw20 token swaps are disabled"))? @@ -137,8 +160,19 @@ pub fn execute_swap_operations( } // send output to recipient - Ok(Response::new().add_message(BankMsg::Send { - to_address: to.to_string(), - amount: vec![coin(receiver_balance.u128(), target_denom)], - })) + Ok(Response::new() + .add_message(BankMsg::Send { + to_address: to.to_string(), + amount: vec![coin(receiver_balance.u128(), target_denom)], + }) + .add_messages(fee_messages) + .add_attributes(vec![ + ("action", "execute_swap_operations"), + ("sender", &info.sender.as_str()), + ("receiver", to.as_str()), + ("offer_info", &offer_asset.info.to_string()), + ("offer_amount", &offer_asset.amount.to_string()), + ("return_info", &target_asset_info.to_string()), + ("return_amount", &receiver_balance.to_string()), + ])) } From b76d3dc6e92e97ba1333e012f057bfabc57188b8 Mon Sep 17 00:00:00 2001 From: kaimen-sano Date: Tue, 19 Mar 2024 11:42:06 +1300 Subject: [PATCH 09/15] fix(pool-manager): reject non-consecutive router swaps We do not want users to provide swap operations in a manner where the output of one swap is not the input of the next swap. This change ensures that the passed operations satisfy such a state inside the `assert_operations` step. --- .../liquidity_hub/pool-manager/src/error.rs | 8 +++ .../pool-manager/src/router/commands.rs | 60 +++++++++++++++---- 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/src/error.rs b/contracts/liquidity_hub/pool-manager/src/error.rs index 2b9200a2..b61991fc 100644 --- a/contracts/liquidity_hub/pool-manager/src/error.rs +++ b/contracts/liquidity_hub/pool-manager/src/error.rs @@ -7,6 +7,7 @@ use cw_ownable::OwnershipError; use cw_utils::PaymentError; use semver::Version; use thiserror::Error; +use white_whale_std::pool_network::asset::AssetInfo; #[derive(Error, Debug, PartialEq)] pub enum ContractError { @@ -135,6 +136,13 @@ pub enum ContractError { #[error("Must provide swap operations to execute")] NoSwapOperationsProvided {}, + + #[error("Attempt to perform non-consecutive swap operation from previous output of {previous_output} to next input of {next_input}")] + NonConsecutiveSwapOperations { + previous_output: AssetInfo, + next_input: AssetInfo, + }, + #[error("Invalid pair creation fee, expected {expected} got {amount}")] InvalidPairCreationFee { amount: cosmwasm_std::Uint128, diff --git a/contracts/liquidity_hub/pool-manager/src/router/commands.rs b/contracts/liquidity_hub/pool-manager/src/router/commands.rs index e05c15aa..f398939f 100644 --- a/contracts/liquidity_hub/pool-manager/src/router/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/router/commands.rs @@ -12,29 +12,46 @@ use crate::{state::MANAGER_CONFIG, swap::perform_swap::perform_swap, ContractErr /// Checks that an arbitrary amount of [`SwapOperation`]s will not result in /// multiple output tokens. -fn assert_operations(operations: &[SwapOperation]) -> Result<(), ContractError> { +/// +/// Also checks that the output of each swap acts as the input of the next swap. +fn assert_operations(operations: Vec) -> Result<(), ContractError> { + // first check that there is only one output let mut ask_asset_map: HashMap = HashMap::new(); for operation in operations.iter() { - let (offer_asset, ask_asset, _pool_identifier) = match operation { + let (offer_asset_info, ask_asset_info, ..) = match operation { SwapOperation::WhaleSwap { token_in_info: offer_asset_info, token_out_info: ask_asset_info, - pool_identifier, - } => ( - offer_asset_info.clone(), - ask_asset_info.clone(), - pool_identifier.clone(), - ), + .. + } => (offer_asset_info, ask_asset_info), }; - ask_asset_map.remove(&offer_asset.to_string()); - ask_asset_map.insert(ask_asset.to_string(), true); + ask_asset_map.remove(&offer_asset_info.to_string()); + ask_asset_map.insert(ask_asset_info.to_string(), true); } if ask_asset_map.keys().len() != 1 { return Err(ContractError::MultipleOutputToken {}); } + // check that the output of each swap is the input of the next swap + let mut previous_output_info = operations + .first() + .ok_or(ContractError::NoSwapOperationsProvided {})? + .get_input_asset_info() + .clone(); + + for operation in operations { + if operation.get_input_asset_info() != &previous_output_info { + return Err(ContractError::NonConsecutiveSwapOperations { + previous_output: previous_output_info.to_owned(), + next_input: operation.get_input_asset_info().clone(), + }); + } + + previous_output_info = operation.get_target_asset_info(); + } + Ok(()) } @@ -91,7 +108,7 @@ pub fn execute_swap_operations( info: offer_asset_info.to_owned(), }; - assert_operations(&operations)?; + assert_operations(operations.clone())?; // we return the output to the sender if no alternative recipient was specified. let to = to.unwrap_or(info.sender.clone()); @@ -102,6 +119,8 @@ pub fn execute_swap_operations( // stores messages for sending fees after the swaps let mut fee_messages = vec![]; + // stores swap attributes to add to tx info + let mut swap_attributes = vec![]; for operation in operations { match operation { @@ -111,13 +130,27 @@ pub fn execute_swap_operations( .. } => match &token_in_info { AssetInfo::NativeToken { .. } => { + // inside assert_operations() we have already checked that + // the output of each swap is the input of the next swap. + let swap_result = perform_swap( deps.branch(), - previous_swap_output, + previous_swap_output.clone(), pool_identifier, None, max_spread, )?; + swap_attributes.push(( + "swap", + format!( + "in={}, out={}, burn_fee={}, protocol_fee={}, swap_fee={}", + previous_swap_output, + swap_result.return_asset, + swap_result.burn_fee_asset, + swap_result.protocol_fee_asset, + swap_result.swap_fee_asset + ), + )); // update the previous swap output previous_swap_output = swap_result.return_asset; @@ -174,5 +207,6 @@ pub fn execute_swap_operations( ("offer_amount", &offer_asset.amount.to_string()), ("return_info", &target_asset_info.to_string()), ("return_amount", &receiver_balance.to_string()), - ])) + ]) + .add_attributes(swap_attributes)) } From 6f17a1a24bee02ce74a2b29e2284b172cb9f6ec0 Mon Sep 17 00:00:00 2001 From: kaimen-sano Date: Tue, 19 Mar 2024 11:56:54 +1300 Subject: [PATCH 10/15] refactor(pool-manager): remove redundant assert_operations check The previous commit changes the check inside assert_operations to ensure that the output of each swap acts as the input of the next swap. Naturally, this ensures that each swap does not result in multiple outputs. We therefore simplify the code by removing this check. This has an added benefit of removing our `HashMap` usage. --- .../liquidity_hub/pool-manager/src/error.rs | 3 -- .../pool-manager/src/router/commands.rs | 28 ++----------------- 2 files changed, 2 insertions(+), 29 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/src/error.rs b/contracts/liquidity_hub/pool-manager/src/error.rs index b61991fc..163c799b 100644 --- a/contracts/liquidity_hub/pool-manager/src/error.rs +++ b/contracts/liquidity_hub/pool-manager/src/error.rs @@ -37,9 +37,6 @@ pub enum ContractError { #[error("The provided assets are both the same")] SameAsset {}, - #[error("Invalid operations; multiple output token")] - MultipleOutputToken {}, - #[error("Attempt to migrate to version {new_version}, but contract is on a higher version {current_version}")] MigrateInvalidVersion { new_version: Version, diff --git a/contracts/liquidity_hub/pool-manager/src/router/commands.rs b/contracts/liquidity_hub/pool-manager/src/router/commands.rs index f398939f..fe13784f 100644 --- a/contracts/liquidity_hub/pool-manager/src/router/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/router/commands.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - use cosmwasm_std::{ coin, Addr, BankMsg, Decimal, DepsMut, MessageInfo, Response, StdError, Uint128, }; @@ -10,30 +8,8 @@ use white_whale_std::{ use crate::{state::MANAGER_CONFIG, swap::perform_swap::perform_swap, ContractError}; -/// Checks that an arbitrary amount of [`SwapOperation`]s will not result in -/// multiple output tokens. -/// -/// Also checks that the output of each swap acts as the input of the next swap. +/// Checks that the output of each [`SwapOperation`] acts as the input of the next swap. fn assert_operations(operations: Vec) -> Result<(), ContractError> { - // first check that there is only one output - let mut ask_asset_map: HashMap = HashMap::new(); - for operation in operations.iter() { - let (offer_asset_info, ask_asset_info, ..) = match operation { - SwapOperation::WhaleSwap { - token_in_info: offer_asset_info, - token_out_info: ask_asset_info, - .. - } => (offer_asset_info, ask_asset_info), - }; - - ask_asset_map.remove(&offer_asset_info.to_string()); - ask_asset_map.insert(ask_asset_info.to_string(), true); - } - - if ask_asset_map.keys().len() != 1 { - return Err(ContractError::MultipleOutputToken {}); - } - // check that the output of each swap is the input of the next swap let mut previous_output_info = operations .first() @@ -44,7 +20,7 @@ fn assert_operations(operations: Vec) -> Result<(), ContractError for operation in operations { if operation.get_input_asset_info() != &previous_output_info { return Err(ContractError::NonConsecutiveSwapOperations { - previous_output: previous_output_info.to_owned(), + previous_output: previous_output_info, next_input: operation.get_input_asset_info().clone(), }); } From 91d90103958ab667c3d49a8d5db65e86938a60b8 Mon Sep 17 00:00:00 2001 From: kaimen-sano Date: Tue, 19 Mar 2024 13:32:08 +1300 Subject: [PATCH 11/15] test(pool-manager): add router tests --- .../src/tests/integration_tests.rs | 926 +++++++++++++++++- .../pool-manager/src/tests/suite.rs | 2 +- 2 files changed, 912 insertions(+), 16 deletions(-) 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 dea11439..9ec8120c 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs @@ -1,11 +1,5 @@ use crate::ContractError; -use white_whale_std::pool_manager::NPairInfo; -use white_whale_std::pool_manager::{ExecuteMsg, InstantiateMsg, QueryMsg}; -// use crate::tests::suite::SuiteBuilder; -use cosmwasm_std::testing::MOCK_CONTRACT_ADDR; use cosmwasm_std::{coin, Addr, Coin, Decimal, Uint128}; -use cw20::BalanceResponse; -use cw_multi_test::{App, Contract, ContractWrapper, Executor}; use white_whale_std::fee::Fee; use white_whale_std::pool_network::asset::{Asset, AssetInfo, MINIMUM_LIQUIDITY_AMOUNT}; use white_whale_std::pool_network::pair::PoolFee; @@ -569,6 +563,633 @@ mod router { }, ]; + #[cfg(not(feature = "osmosis"))] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::bps(50), // 0.5% + }, + swap_fee: Fee { + share: Decimal::bps(50), // 0.5% + }, + burn_fee: Fee { + share: Decimal::bps(50), // 0.5% + }, + }; + #[cfg(feature = "osmosis")] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::bps(50), + }, + swap_fee: Fee { + share: Decimal::bps(50), + }, + burn_fee: Fee { + share: Decimal::bps(50), + }, + osmosis_fee: Fee { + share: Decimal::bps(50), + }, + }; + + // Create a pair + suite + .instantiate_with_cw20_lp_token() + .add_native_token_decimals(creator.clone(), "uwhale".to_string(), 6) + .add_native_token_decimals(creator.clone(), "uluna".to_string(), 6) + .add_native_token_decimals(creator.clone(), "uusd".to_string(), 6) + .create_pair( + creator.clone(), + first_pair, + pool_fees.clone(), + white_whale_std::pool_network::asset::PairType::ConstantProduct, + false, + Some("whale-uluna".to_string()), + vec![coin(1000, "uusd")], + |result| { + result.unwrap(); + }, + ) + .create_pair( + creator.clone(), + second_pair, + pool_fees, + white_whale_std::pool_network::asset::PairType::ConstantProduct, + false, + Some("uluna-uusd".to_string()), + vec![coin(1000, "uusd")], + |result| { + result.unwrap(); + }, + ); + + // Lets try to add liquidity + suite.provide_liquidity( + creator.clone(), + "whale-uluna".to_string(), + vec![ + Asset { + info: AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + amount: Uint128::from(1000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + amount: Uint128::from(1000000u128), + }, + ], + vec![ + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1000000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(1000000u128), + }, + ], + |result| { + // ensure we got 999,000 in the response (1m - initial liquidity amount) + let result = result.unwrap(); + assert!(result.has_event(&Event::new("wasm").add_attribute("share", "999000"))); + }, + ); + + // Lets try to add liquidity + suite.provide_liquidity( + creator.clone(), + "uluna-uusd".to_string(), + vec![ + Asset { + info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + amount: Uint128::from(1000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Uint128::from(1000000u128), + }, + ], + vec![ + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(1000000u128), + }, + Coin { + denom: "uusd".to_string(), + amount: Uint128::from(1000000u128), + }, + ], + |result| { + // ensure we got 999,000 in the response (1m - initial liquidity amount) + let result = result.unwrap(); + assert!(result.has_event(&Event::new("wasm").add_attribute("share", "999000"))); + }, + ); + + // Prepare the swap operations, we want to go from WHALE -> UUSD + // We will use the uluna-uusd pair as the intermediary pool + + let swap_operations = vec![ + white_whale_std::pool_manager::SwapOperation::WhaleSwap { + token_in_info: AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + token_out_info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + pool_identifier: "whale-uluna".to_string(), + }, + white_whale_std::pool_manager::SwapOperation::WhaleSwap { + token_in_info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + token_out_info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + pool_identifier: "uluna-uusd".to_string(), + }, + ]; + + // before swap uusd balance = 1_000_000_001 + // - 2*1_000 pair creation fee + // - 1_000_000 liquidity provision + // - 1 for native token creation (for decimal precisions) + // = 998_998_000 + let pre_swap_amount = 998_998_000; + suite.query_balance(creator.to_string(), "uusd".to_string(), |amt| { + assert_eq!(amt.unwrap().amount.u128(), pre_swap_amount); + }); + + suite.execute_swap_operations( + creator.clone(), + swap_operations, + None, + None, + None, + vec![coin(1000u128, "uwhale".to_string())], + |result| { + result.unwrap(); + }, + ); + + // ensure that the whale got swapped to an appropriate amount of uusd + // we swap 1000 whale for 974 uusd + // with a fee of 4*6 = 24 uusd + let post_swap_amount = pre_swap_amount + 974; + suite.query_balance(creator.to_string(), "uusd".to_string(), |amt| { + assert_eq!(amt.unwrap().amount.u128(), post_swap_amount); + }); + + // ensure that fees got sent to the appropriate place + suite.query_balance( + suite.whale_lair_addr.to_string(), + "uusd".to_string(), + |amt| { + assert_eq!(amt.unwrap().amount.u128(), 2000 + 4 * 2); + }, + ); + suite.query_balance( + suite.whale_lair_addr.to_string(), + "uwhale".to_string(), + |amt| { + assert_eq!(amt.unwrap().amount.u128(), 0); + }, + ); + suite.query_balance( + suite.whale_lair_addr.to_string(), + "uluna".to_string(), + |amt| { + assert_eq!(amt.unwrap().amount.u128(), 4 * 2); + }, + ); + } + + #[test] + fn rejects_empty_swaps() { + let mut suite = TestingSuite::default_with_balances(vec![ + coin(1_000_000_001u128, "uwhale".to_string()), + coin(1_000_000_000u128, "uluna".to_string()), + coin(1_000_000_001u128, "uusd".to_string()), + ]); + let creator = suite.creator(); + let _other = suite.senders[1].clone(); + let _unauthorized = suite.senders[2].clone(); + // Asset infos with uwhale and uluna + + let first_pair = vec![ + AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ]; + + let second_pair = vec![ + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + ]; + + // Default Pool fees white_whale_std::pool_network::pair::PoolFee + // Protocol fee is 0.01% and swap fee is 0.02% and burn fee is 0% + #[cfg(not(feature = "osmosis"))] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::from_ratio(1u128, 100_000u128), + }, + swap_fee: Fee { + share: Decimal::from_ratio(1u128, 100_000u128), + }, + burn_fee: Fee { + share: Decimal::zero(), + }, + }; + + #[cfg(feature = "osmosis")] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::from_ratio(1u128, 100_000u128), + }, + swap_fee: Fee { + share: Decimal::from_ratio(1u128, 100_000u128), + }, + burn_fee: Fee { + share: Decimal::zero(), + }, + osmosis_fee: Fee { + share: Decimal::from_ratio(1u128, 100_000u128), + }, + }; + + // Create a pair + suite + .instantiate_with_cw20_lp_token() + .add_native_token_decimals(creator.clone(), "uwhale".to_string(), 6) + .add_native_token_decimals(creator.clone(), "uluna".to_string(), 6) + .add_native_token_decimals(creator.clone(), "uusd".to_string(), 6) + .create_pair( + creator.clone(), + first_pair, + pool_fees.clone(), + white_whale_std::pool_network::asset::PairType::ConstantProduct, + false, + Some("whale-uluna".to_string()), + vec![coin(1000, "uusd")], + |result| { + result.unwrap(); + }, + ) + .create_pair( + creator.clone(), + second_pair, + pool_fees, + white_whale_std::pool_network::asset::PairType::ConstantProduct, + false, + Some("uluna-uusd".to_string()), + vec![coin(1000, "uusd")], + |result| { + result.unwrap(); + }, + ); + + // Lets try to add liquidity + suite.provide_liquidity( + creator.clone(), + "whale-uluna".to_string(), + vec![ + Asset { + info: AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + amount: Uint128::from(1000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + amount: Uint128::from(1000000u128), + }, + ], + vec![ + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1000000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(1000000u128), + }, + ], + |result| { + // ensure we got 999,000 in the response (1m - initial liquidity amount) + let result = result.unwrap(); + assert!(result.has_event(&Event::new("wasm").add_attribute("share", "999000"))); + }, + ); + + // Lets try to add liquidity + suite.provide_liquidity( + creator.clone(), + "uluna-uusd".to_string(), + vec![ + Asset { + info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + amount: Uint128::from(1000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Uint128::from(1000000u128), + }, + ], + vec![ + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(1000000u128), + }, + Coin { + denom: "uusd".to_string(), + amount: Uint128::from(1000000u128), + }, + ], + |result| { + // ensure we got 999,000 in the response (1m - initial liquidity amount) + let result = result.unwrap(); + assert!(result.has_event(&Event::new("wasm").add_attribute("share", "999000"))); + }, + ); + + // attempt to perform a 0 swap operations + let swap_operations = vec![]; + + suite.execute_swap_operations( + creator.clone(), + swap_operations, + None, + None, + None, + vec![coin(1000u128, "uwhale".to_string())], + |result| { + assert_eq!( + result.unwrap_err().downcast_ref::(), + Some(&ContractError::NoSwapOperationsProvided {}) + ) + }, + ); + } + + #[test] + fn rejects_non_consecutive_swaps() { + let mut suite = TestingSuite::default_with_balances(vec![ + coin(1_000_000_001u128, "uwhale".to_string()), + coin(1_000_000_000u128, "uluna".to_string()), + coin(1_000_000_001u128, "uusd".to_string()), + ]); + let creator = suite.creator(); + let other = suite.senders[1].clone(); + let _unauthorized = suite.senders[2].clone(); + // Asset infos with uwhale and uluna + + let first_pair = vec![ + AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ]; + + let second_pair = vec![ + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + ]; + + // Default Pool fees white_whale_std::pool_network::pair::PoolFee + // Protocol fee is 0.01% and swap fee is 0.02% and burn fee is 0% + #[cfg(not(feature = "osmosis"))] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::from_ratio(1u128, 100_000u128), + }, + swap_fee: Fee { + share: Decimal::from_ratio(1u128, 100_000u128), + }, + burn_fee: Fee { + share: Decimal::zero(), + }, + }; + + #[cfg(feature = "osmosis")] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::from_ratio(1u128, 100_000u128), + }, + swap_fee: Fee { + share: Decimal::from_ratio(1u128, 100_000u128), + }, + burn_fee: Fee { + share: Decimal::zero(), + }, + osmosis_fee: Fee { + share: Decimal::from_ratio(1u128, 100_000u128), + }, + }; + + // Create a pair + suite + .instantiate_with_cw20_lp_token() + .add_native_token_decimals(creator.clone(), "uwhale".to_string(), 6) + .add_native_token_decimals(creator.clone(), "uluna".to_string(), 6) + .add_native_token_decimals(creator.clone(), "uusd".to_string(), 6) + .create_pair( + creator.clone(), + first_pair, + pool_fees.clone(), + white_whale_std::pool_network::asset::PairType::ConstantProduct, + false, + Some("whale-uluna".to_string()), + vec![coin(1000, "uusd")], + |result| { + result.unwrap(); + }, + ) + .create_pair( + creator.clone(), + second_pair, + pool_fees, + white_whale_std::pool_network::asset::PairType::ConstantProduct, + false, + Some("uluna-uusd".to_string()), + vec![coin(1000, "uusd")], + |result| { + result.unwrap(); + }, + ); + + // Lets try to add liquidity + suite.provide_liquidity( + creator.clone(), + "whale-uluna".to_string(), + vec![ + Asset { + info: AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + amount: Uint128::from(1000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + amount: Uint128::from(1000000u128), + }, + ], + vec![ + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1000000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(1000000u128), + }, + ], + |result| { + // ensure we got 999,000 in the response (1m - initial liquidity amount) + let result = result.unwrap(); + assert!(result.has_event(&Event::new("wasm").add_attribute("share", "999000"))); + }, + ); + + // Lets try to add liquidity + suite.provide_liquidity( + creator.clone(), + "uluna-uusd".to_string(), + vec![ + Asset { + info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + amount: Uint128::from(1000000u128), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Uint128::from(1000000u128), + }, + ], + vec![ + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(1000000u128), + }, + Coin { + denom: "uusd".to_string(), + amount: Uint128::from(1000000u128), + }, + ], + |result| { + // ensure we got 999,000 in the response (1m - initial liquidity amount) + let result = result.unwrap(); + assert!(result.has_event(&Event::new("wasm").add_attribute("share", "999000"))); + }, + ); + + // Prepare the swap operations, we want to go from WHALE -> UUSD + // We will use the uluna-uusd pair as the intermediary pool + + let swap_operations = vec![ + white_whale_std::pool_manager::SwapOperation::WhaleSwap { + token_in_info: AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + token_out_info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + pool_identifier: "whale-uluna".to_string(), + }, + white_whale_std::pool_manager::SwapOperation::WhaleSwap { + token_in_info: AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + token_out_info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + pool_identifier: "whale-uluna".to_string(), + }, + ]; + + suite.execute_swap_operations( + other.clone(), + swap_operations, + None, + None, + None, + vec![coin(1000u128, "uwhale".to_string())], + |result| { + assert_eq!( + result.unwrap_err().downcast_ref::(), + Some(&ContractError::NonConsecutiveSwapOperations { + previous_output: AssetInfo::NativeToken { + denom: "uluna".to_string() + }, + next_input: AssetInfo::NativeToken { + denom: "uwhale".to_string() + } + }) + ); + }, + ); + } + + #[test] + fn sends_to_correct_receiver() { + let mut suite = TestingSuite::default_with_balances(vec![ + coin(1_000_000_001u128, "uwhale".to_string()), + coin(1_000_000_000u128, "uluna".to_string()), + coin(1_000_000_001u128, "uusd".to_string()), + ]); + let creator = suite.creator(); + let other = suite.senders[1].clone(); + let unauthorized = suite.senders[2].clone(); + // Asset infos with uwhale and uluna + + let first_pair = vec![ + AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ]; + + let second_pair = vec![ + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + ]; + // Default Pool fees white_whale_std::pool_network::pair::PoolFee // Protocol fee is 0.01% and swap fee is 0.02% and burn fee is 0% #[cfg(not(feature = "osmosis"))] @@ -600,6 +1221,281 @@ mod router { }, }; + // Create a pair + suite + .instantiate_with_cw20_lp_token() + .add_native_token_decimals(creator.clone(), "uwhale".to_string(), 6) + .add_native_token_decimals(creator.clone(), "uluna".to_string(), 6) + .add_native_token_decimals(creator.clone(), "uusd".to_string(), 6) + .create_pair( + creator.clone(), + first_pair, + pool_fees.clone(), + white_whale_std::pool_network::asset::PairType::ConstantProduct, + false, + Some("whale-uluna".to_string()), + vec![coin(1000, "uusd")], + |result| { + result.unwrap(); + }, + ) + .create_pair( + creator.clone(), + second_pair, + pool_fees, + white_whale_std::pool_network::asset::PairType::ConstantProduct, + false, + Some("uluna-uusd".to_string()), + vec![coin(1000, "uusd")], + |result| { + result.unwrap(); + }, + ); + + // Lets try to add liquidity + let liquidity_amount = 1_000_000u128; + suite.provide_liquidity( + creator.clone(), + "whale-uluna".to_string(), + vec![ + Asset { + info: AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + amount: Uint128::from(liquidity_amount), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + amount: Uint128::from(liquidity_amount), + }, + ], + vec![ + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(liquidity_amount), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(liquidity_amount), + }, + ], + |result| { + // ensure we got 999,000 in the response (1m - initial liquidity amount) + let result = result.unwrap(); + assert!(result.has_event(&Event::new("wasm").add_attribute("share", "999000"))); + }, + ); + + // Lets try to add liquidity + suite.provide_liquidity( + creator.clone(), + "uluna-uusd".to_string(), + vec![ + Asset { + info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + amount: Uint128::from(liquidity_amount), + }, + Asset { + info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + amount: Uint128::from(liquidity_amount), + }, + ], + vec![ + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(liquidity_amount), + }, + Coin { + denom: "uusd".to_string(), + amount: Uint128::from(liquidity_amount), + }, + ], + |result| { + // ensure we got 999,000 in the response (1m - initial liquidity amount) + let result = result.unwrap(); + assert!(result.has_event(&Event::new("wasm").add_attribute("share", "999000"))); + }, + ); + + // Prepare the swap operations, we want to go from WHALE -> UUSD + // We will use the uluna-uusd pair as the intermediary pool + + let swap_operations = vec![ + white_whale_std::pool_manager::SwapOperation::WhaleSwap { + token_in_info: AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + token_out_info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + pool_identifier: "whale-uluna".to_string(), + }, + white_whale_std::pool_manager::SwapOperation::WhaleSwap { + token_in_info: AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + token_out_info: AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + pool_identifier: "uluna-uusd".to_string(), + }, + ]; + + // before swap uusd balance = 1_000_000_001 + // before swap uwhale balance = 1_000_000_001 + // before swap uluna balance = 1_000_000_001 + let pre_swap_amount = 1_000_000_001; + suite.query_balance(other.to_string(), "uusd".to_string(), |amt| { + assert_eq!(amt.unwrap().amount.u128(), pre_swap_amount); + }); + suite.query_balance(other.to_string(), "uwhale".to_string(), |amt| { + assert_eq!(amt.unwrap().amount.u128(), pre_swap_amount); + }); + suite.query_balance(other.to_string(), "uluna".to_string(), |amt| { + assert_eq!(amt.unwrap().amount.u128(), pre_swap_amount - 1); + }); + // also check the same for unauthorized receiver + suite.query_balance(other.to_string(), "uusd".to_string(), |amt| { + assert_eq!(amt.unwrap().amount.u128(), pre_swap_amount); + }); + suite.query_balance(other.to_string(), "uwhale".to_string(), |amt| { + assert_eq!(amt.unwrap().amount.u128(), pre_swap_amount); + }); + suite.query_balance(other.to_string(), "uluna".to_string(), |amt| { + assert_eq!(amt.unwrap().amount.u128(), pre_swap_amount - 1); + }); + // also check for contract + // when we add tokens to the contract, we must send a fee of 1_u128 so the contract + // can register the native token + suite.query_balance( + suite.pool_manager_addr.to_string(), + "uusd".to_string(), + |amt| { + assert_eq!(amt.unwrap().amount.u128(), liquidity_amount + 1); + }, + ); + suite.query_balance( + suite.pool_manager_addr.to_string(), + "uwhale".to_string(), + |amt| { + assert_eq!(amt.unwrap().amount.u128(), liquidity_amount + 1); + }, + ); + suite.query_balance( + suite.pool_manager_addr.to_string(), + "uluna".to_string(), + |amt| { + assert_eq!(amt.unwrap().amount.u128(), 2 * liquidity_amount + 1); + }, + ); + + // perform swaps + suite.execute_swap_operations( + other.clone(), + swap_operations, + None, + Some(unauthorized.to_string()), + None, + vec![coin(1000u128, "uwhale".to_string())], + |result| { + result.unwrap(); + }, + ); + + // ensure that the whale got swapped to an appropriate amount of uusd + // we swap 1000 whale for 998 uusd + let post_swap_amount = pre_swap_amount + 998; + suite.query_balance(unauthorized.to_string(), "uusd".to_string(), |amt| { + assert_eq!(amt.unwrap().amount.u128(), post_swap_amount); + }); + // check that the balances of the contract are ok + suite.query_balance( + suite.pool_manager_addr.to_string(), + "uusd".to_string(), + |amt| { + assert_eq!(amt.unwrap().amount.u128(), liquidity_amount - 998 + 1); + }, + ); + suite.query_balance( + suite.pool_manager_addr.to_string(), + "uwhale".to_string(), + |amt| { + assert_eq!(amt.unwrap().amount.u128(), liquidity_amount + 1000 + 1); + }, + ); + suite.query_balance( + suite.pool_manager_addr.to_string(), + "uluna".to_string(), + |amt| { + assert_eq!(amt.unwrap().amount.u128(), 2 * liquidity_amount + 1); + }, + ); + } + + #[test] + fn checks_minimum_receive() { + let mut suite = TestingSuite::default_with_balances(vec![ + coin(1_000_000_001u128, "uwhale".to_string()), + coin(1_000_000_000u128, "uluna".to_string()), + coin(1_000_000_001u128, "uusd".to_string()), + ]); + let creator = suite.creator(); + let _other = suite.senders[1].clone(); + let _unauthorized = suite.senders[2].clone(); + // Asset infos with uwhale and uluna + + let first_pair = vec![ + AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ]; + + let second_pair = vec![ + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + ]; + + #[cfg(not(feature = "osmosis"))] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::bps(50), // 0.5% + }, + swap_fee: Fee { + share: Decimal::bps(50), // 0.5% + }, + burn_fee: Fee { + share: Decimal::bps(50), // 0.5% + }, + }; + #[cfg(feature = "osmosis")] + let pool_fees = PoolFee { + protocol_fee: Fee { + share: Decimal::bps(50), + }, + swap_fee: Fee { + share: Decimal::bps(50), + }, + burn_fee: Fee { + share: Decimal::bps(50), + }, + osmosis_fee: Fee { + share: Decimal::bps(50), + }, + }; + // Create a pair suite .instantiate_with_cw20_lp_token() @@ -735,24 +1631,24 @@ mod router { assert_eq!(amt.unwrap().amount.u128(), pre_swap_amount); }); + // require an output of 975 uusd suite.execute_swap_operations( creator.clone(), swap_operations, - None, + Some(Uint128::new(975)), None, None, vec![coin(1000u128, "uwhale".to_string())], |result| { - result.unwrap(); + assert_eq!( + result.unwrap_err().downcast_ref::(), + Some(&ContractError::MinimumReceiveAssertion { + minimum_receive: Uint128::new(975), + swap_amount: Uint128::new(974) + }) + ) }, ); - - // ensure that the whale got swapped to an appropriate amount of uusd - // we swap 1000 whale for 998 uusd - let post_swap_amount = pre_swap_amount + 998; - suite.query_balance(creator.to_string(), "uusd".to_string(), |amt| { - assert_eq!(amt.unwrap().amount.u128(), post_swap_amount); - }); } } diff --git a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs index 91dda94a..e37b6a2d 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs @@ -523,7 +523,7 @@ impl TestingSuite { pub(crate) fn query_balance( &mut self, addr: String, - denom: String, + denom: impl Into, result: impl Fn(StdResult), ) -> &mut Self { let balance_resp: StdResult = self.app.wrap().query_balance(&addr, denom); From 0763e84bbf5b1a5fb67db3054f3cae1b1e8141b7 Mon Sep 17 00:00:00 2001 From: kaimen-sano Date: Tue, 19 Mar 2024 13:37:04 +1300 Subject: [PATCH 12/15] chore: generate schemas --- .../pool-manager/schema/pool-manager.json | 55 +++++++++++++++++++ .../pool-manager/schema/raw/execute.json | 55 +++++++++++++++++++ .../vault_router/schema/raw/execute.json | 15 ----- .../vault_router/schema/vault_router.json | 15 ----- .../white-whale-std/src/tokenfactory/burn.rs | 7 ++- .../src/tokenfactory/common.rs | 1 + 6 files changed, 117 insertions(+), 31 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/schema/pool-manager.json b/contracts/liquidity_hub/pool-manager/schema/pool-manager.json index 4381eaef..fcddc80e 100644 --- a/contracts/liquidity_hub/pool-manager/schema/pool-manager.json +++ b/contracts/liquidity_hub/pool-manager/schema/pool-manager.json @@ -308,6 +308,61 @@ }, "additionalProperties": false }, + { + "description": "Execute multiple [`SwapOperations`] to allow for multi-hop swaps.", + "type": "object", + "required": [ + "execute_swap_operations" + ], + "properties": { + "execute_swap_operations": { + "type": "object", + "required": [ + "operations" + ], + "properties": { + "max_spread": { + "description": "The (optional) maximum spread to incur when performing any swap.\n\nIf left unspecified, there is no limit to what spread the transaction can incur.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "minimum_receive": { + "description": "The minimum amount of the output (i.e., final swap operation token) required for the message to succeed.", + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "operations": { + "description": "The operations that should be performed in sequence.\n\nThe amount in each swap will be the output from the previous swap.\n\nThe first swap will use whatever funds are sent in the [`MessageInfo`].", + "type": "array", + "items": { + "$ref": "#/definitions/SwapOperation" + } + }, + "to": { + "description": "The (optional) recipient of the output tokens.\n\nIf left unspecified, tokens will be sent to the sender of the message.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Adds swap routes to the router.", "type": "object", diff --git a/contracts/liquidity_hub/pool-manager/schema/raw/execute.json b/contracts/liquidity_hub/pool-manager/schema/raw/execute.json index e9deee7e..e097996d 100644 --- a/contracts/liquidity_hub/pool-manager/schema/raw/execute.json +++ b/contracts/liquidity_hub/pool-manager/schema/raw/execute.json @@ -201,6 +201,61 @@ }, "additionalProperties": false }, + { + "description": "Execute multiple [`SwapOperations`] to allow for multi-hop swaps.", + "type": "object", + "required": [ + "execute_swap_operations" + ], + "properties": { + "execute_swap_operations": { + "type": "object", + "required": [ + "operations" + ], + "properties": { + "max_spread": { + "description": "The (optional) maximum spread to incur when performing any swap.\n\nIf left unspecified, there is no limit to what spread the transaction can incur.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "minimum_receive": { + "description": "The minimum amount of the output (i.e., final swap operation token) required for the message to succeed.", + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "operations": { + "description": "The operations that should be performed in sequence.\n\nThe amount in each swap will be the output from the previous swap.\n\nThe first swap will use whatever funds are sent in the [`MessageInfo`].", + "type": "array", + "items": { + "$ref": "#/definitions/SwapOperation" + } + }, + "to": { + "description": "The (optional) recipient of the output tokens.\n\nIf left unspecified, tokens will be sent to the sender of the message.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Adds swap routes to the router.", "type": "object", diff --git a/contracts/liquidity_hub/vault-network/vault_router/schema/raw/execute.json b/contracts/liquidity_hub/vault-network/vault_router/schema/raw/execute.json index 2c63c8b4..9a393121 100644 --- a/contracts/liquidity_hub/vault-network/vault_router/schema/raw/execute.json +++ b/contracts/liquidity_hub/vault-network/vault_router/schema/raw/execute.json @@ -614,21 +614,6 @@ "additionalProperties": false } ] - }, - "WeightedVoteOption": { - "type": "object", - "required": [ - "option", - "weight" - ], - "properties": { - "option": { - "$ref": "#/definitions/VoteOption" - }, - "weight": { - "$ref": "#/definitions/Decimal" - } - } } } } diff --git a/contracts/liquidity_hub/vault-network/vault_router/schema/vault_router.json b/contracts/liquidity_hub/vault-network/vault_router/schema/vault_router.json index 551573df..0c59ceb1 100644 --- a/contracts/liquidity_hub/vault-network/vault_router/schema/vault_router.json +++ b/contracts/liquidity_hub/vault-network/vault_router/schema/vault_router.json @@ -639,21 +639,6 @@ "additionalProperties": false } ] - }, - "WeightedVoteOption": { - "type": "object", - "required": [ - "option", - "weight" - ], - "properties": { - "option": { - "$ref": "#/definitions/VoteOption" - }, - "weight": { - "$ref": "#/definitions/Decimal" - } - } } } }, diff --git a/packages/white-whale-std/src/tokenfactory/burn.rs b/packages/white-whale-std/src/tokenfactory/burn.rs index 5a77c659..88caad0f 100644 --- a/packages/white-whale-std/src/tokenfactory/burn.rs +++ b/packages/white-whale-std/src/tokenfactory/burn.rs @@ -1,5 +1,10 @@ -#[allow(unused_imports)] +#[cfg(any( + feature = "token_factory", + feature = "osmosis_token_factory", + feature = "injective" +))] use crate::tokenfactory::common::{create_msg, MsgTypes}; +#[allow(unused_imports)] #[cfg(any( feature = "token_factory", feature = "osmosis_token_factory", diff --git a/packages/white-whale-std/src/tokenfactory/common.rs b/packages/white-whale-std/src/tokenfactory/common.rs index 25cc35b2..141aac54 100644 --- a/packages/white-whale-std/src/tokenfactory/common.rs +++ b/packages/white-whale-std/src/tokenfactory/common.rs @@ -57,6 +57,7 @@ pub(crate) trait EncodeMessage { fn encode(sender: String, data: Self) -> Vec; } #[allow(dead_code)] +#[cfg(feature = "token_factory")] pub(crate) fn create_msg( sender: Addr, message_data: M, From 984c2fdd91b33c7aa883f91c3d27a125f20a5cab Mon Sep 17 00:00:00 2001 From: kaimen-sano Date: Tue, 19 Mar 2024 13:39:12 +1300 Subject: [PATCH 13/15] chore: run clippy --- .../liquidity_hub/pool-manager/src/swap/perform_swap.rs | 1 - packages/white-whale-std/src/tokenfactory/common.rs | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) 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 cfdb4df2..5eb43323 100644 --- a/contracts/liquidity_hub/pool-manager/src/swap/perform_swap.rs +++ b/contracts/liquidity_hub/pool-manager/src/swap/perform_swap.rs @@ -31,7 +31,6 @@ pub struct SwapResult { /// The resulting [`SwapResult`] has actions that should be taken, as the swap has been performed. /// In other words, the caller of the `perform_swap` function _should_ make use /// of each field in [`SwapResult`] (besides fields like `spread_amount`). -#[must_use] pub fn perform_swap( deps: DepsMut, offer_asset: Asset, diff --git a/packages/white-whale-std/src/tokenfactory/common.rs b/packages/white-whale-std/src/tokenfactory/common.rs index 141aac54..0922955c 100644 --- a/packages/white-whale-std/src/tokenfactory/common.rs +++ b/packages/white-whale-std/src/tokenfactory/common.rs @@ -1,5 +1,9 @@ +#[cfg(feature = "token_factory")] use cosmwasm_schema::cw_serde; +#[cfg(feature = "token_factory")] use cosmwasm_std::{Addr, CosmosMsg}; + +#[cfg(feature = "token_factory")] #[cw_serde] enum Protocol { Injective, @@ -7,6 +11,7 @@ enum Protocol { Osmosis, } +#[cfg(feature = "token_factory")] impl Protocol { #![allow(dead_code)] #[allow(unreachable_code)] From d6eb7c330b99c4d0f685682f78683e28a283459c Mon Sep 17 00:00:00 2001 From: kaimen-sano Date: Tue, 19 Mar 2024 13:47:31 +1300 Subject: [PATCH 14/15] chore: generate schemas --- .../vault_router/schema/raw/execute.json | 172 ++---------------- .../vault_router/schema/vault_router.json | 172 ++---------------- 2 files changed, 30 insertions(+), 314 deletions(-) diff --git a/contracts/liquidity_hub/vault-network/vault_router/schema/raw/execute.json b/contracts/liquidity_hub/vault-network/vault_router/schema/raw/execute.json index f2ccdbe7..aeb43dad 100644 --- a/contracts/liquidity_hub/vault-network/vault_router/schema/raw/execute.json +++ b/contracts/liquidity_hub/vault-network/vault_router/schema/raw/execute.json @@ -364,30 +364,6 @@ }, "additionalProperties": false }, - { - "type": "object", - "required": [ - "staking" - ], - "properties": { - "staking": { - "$ref": "#/definitions/StakingMsg" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "distribution" - ], - "properties": { - "distribution": { - "$ref": "#/definitions/DistributionMsg" - } - }, - "additionalProperties": false - }, { "description": "A Stargate message encoded the same way as a protobuf [Any](https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/any.proto). This is the same structure as messages in `TxBody` from [ADR-020](https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-020-protobuf-transaction-encoding.md)", "type": "object", @@ -455,55 +431,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "DistributionMsg": { - "description": "The message types of the distribution module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto", - "oneOf": [ - { - "description": "This is translated to a [MsgSetWithdrawAddress](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L29-L37). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "set_withdraw_address" - ], - "properties": { - "set_withdraw_address": { - "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "description": "The `withdraw_address`", - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This is translated to a [[MsgWithdrawDelegatorReward](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L42-L50). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "withdraw_delegator_reward" - ], - "properties": { - "withdraw_delegator_reward": { - "type": "object", - "required": [ - "validator" - ], - "properties": { - "validator": { - "description": "The `validator_address`", - "type": "string" - } - } - } - }, - "additionalProperties": false - } - ] - }, "Empty": { "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", "type": "object" @@ -728,90 +655,6 @@ } } }, - "StakingMsg": { - "description": "The message types of the staking module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto", - "oneOf": [ - { - "description": "This is translated to a [MsgDelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L81-L90). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "delegate" - ], - "properties": { - "delegate": { - "type": "object", - "required": [ - "amount", - "validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "validator": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This is translated to a [MsgUndelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L112-L121). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "undelegate" - ], - "properties": { - "undelegate": { - "type": "object", - "required": [ - "amount", - "validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "validator": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This is translated to a [MsgBeginRedelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L95-L105). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "redelegate" - ], - "properties": { - "redelegate": { - "type": "object", - "required": [ - "amount", - "dst_validator", - "src_validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "dst_validator": { - "type": "string" - }, - "src_validator": { - "type": "string" - } - } - } - }, - "additionalProperties": false - } - ] - }, "Timestamp": { "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", "allOf": [ @@ -1065,6 +908,21 @@ "additionalProperties": false } ] + }, + "WeightedVoteOption": { + "type": "object", + "required": [ + "option", + "weight" + ], + "properties": { + "option": { + "$ref": "#/definitions/VoteOption" + }, + "weight": { + "$ref": "#/definitions/Decimal" + } + } } } } diff --git a/contracts/liquidity_hub/vault-network/vault_router/schema/vault_router.json b/contracts/liquidity_hub/vault-network/vault_router/schema/vault_router.json index 0bd0165a..dca64b12 100644 --- a/contracts/liquidity_hub/vault-network/vault_router/schema/vault_router.json +++ b/contracts/liquidity_hub/vault-network/vault_router/schema/vault_router.json @@ -389,30 +389,6 @@ }, "additionalProperties": false }, - { - "type": "object", - "required": [ - "staking" - ], - "properties": { - "staking": { - "$ref": "#/definitions/StakingMsg" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "distribution" - ], - "properties": { - "distribution": { - "$ref": "#/definitions/DistributionMsg" - } - }, - "additionalProperties": false - }, { "description": "A Stargate message encoded the same way as a protobuf [Any](https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/any.proto). This is the same structure as messages in `TxBody` from [ADR-020](https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-020-protobuf-transaction-encoding.md)", "type": "object", @@ -480,55 +456,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "DistributionMsg": { - "description": "The message types of the distribution module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto", - "oneOf": [ - { - "description": "This is translated to a [MsgSetWithdrawAddress](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L29-L37). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "set_withdraw_address" - ], - "properties": { - "set_withdraw_address": { - "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "description": "The `withdraw_address`", - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This is translated to a [[MsgWithdrawDelegatorReward](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L42-L50). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "withdraw_delegator_reward" - ], - "properties": { - "withdraw_delegator_reward": { - "type": "object", - "required": [ - "validator" - ], - "properties": { - "validator": { - "description": "The `validator_address`", - "type": "string" - } - } - } - }, - "additionalProperties": false - } - ] - }, "Empty": { "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", "type": "object" @@ -753,90 +680,6 @@ } } }, - "StakingMsg": { - "description": "The message types of the staking module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto", - "oneOf": [ - { - "description": "This is translated to a [MsgDelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L81-L90). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "delegate" - ], - "properties": { - "delegate": { - "type": "object", - "required": [ - "amount", - "validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "validator": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This is translated to a [MsgUndelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L112-L121). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "undelegate" - ], - "properties": { - "undelegate": { - "type": "object", - "required": [ - "amount", - "validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "validator": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This is translated to a [MsgBeginRedelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L95-L105). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "redelegate" - ], - "properties": { - "redelegate": { - "type": "object", - "required": [ - "amount", - "dst_validator", - "src_validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "dst_validator": { - "type": "string" - }, - "src_validator": { - "type": "string" - } - } - } - }, - "additionalProperties": false - } - ] - }, "Timestamp": { "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", "allOf": [ @@ -1090,6 +933,21 @@ "additionalProperties": false } ] + }, + "WeightedVoteOption": { + "type": "object", + "required": [ + "option", + "weight" + ], + "properties": { + "option": { + "$ref": "#/definitions/VoteOption" + }, + "weight": { + "$ref": "#/definitions/Decimal" + } + } } } }, From e59999f8c5f59d33dbad4bc21985bd3f0a140e89 Mon Sep 17 00:00:00 2001 From: Kerber0x Date: Mon, 25 Mar 2024 12:08:35 +0000 Subject: [PATCH 15/15] chore: fix clippy --- .../liquidity_hub/pool-manager/src/router/commands.rs | 2 +- .../pool-manager/src/swap/perform_swap.rs | 2 +- contracts/liquidity_hub/vault-manager/Cargo.toml | 6 +++--- packages/white-whale-std/Cargo.toml | 10 +++++----- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/src/router/commands.rs b/contracts/liquidity_hub/pool-manager/src/router/commands.rs index fe13784f..46d122d5 100644 --- a/contracts/liquidity_hub/pool-manager/src/router/commands.rs +++ b/contracts/liquidity_hub/pool-manager/src/router/commands.rs @@ -177,7 +177,7 @@ pub fn execute_swap_operations( .add_messages(fee_messages) .add_attributes(vec![ ("action", "execute_swap_operations"), - ("sender", &info.sender.as_str()), + ("sender", info.sender.as_str()), ("receiver", to.as_str()), ("offer_info", &offer_asset.info.to_string()), ("offer_amount", &offer_asset.amount.to_string()), 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 5eb43323..146bd6a5 100644 --- a/contracts/liquidity_hub/pool-manager/src/swap/perform_swap.rs +++ b/contracts/liquidity_hub/pool-manager/src/swap/perform_swap.rs @@ -120,7 +120,7 @@ pub fn perform_swap( }; // Prepare a message to send the swap fee to the swap fee collector let swap_fee_asset = Asset { - info: ask_pool.info.clone(), + info: ask_pool.info, amount: swap_computation.swap_fee_amount, }; diff --git a/contracts/liquidity_hub/vault-manager/Cargo.toml b/contracts/liquidity_hub/vault-manager/Cargo.toml index 9a894201..3d088e4b 100644 --- a/contracts/liquidity_hub/vault-manager/Cargo.toml +++ b/contracts/liquidity_hub/vault-manager/Cargo.toml @@ -11,9 +11,9 @@ documentation.workspace = true publish.workspace = true exclude = [ - # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. - "contract.wasm", - "hash.txt", + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", ] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/packages/white-whale-std/Cargo.toml b/packages/white-whale-std/Cargo.toml index 0785407e..60c6d994 100644 --- a/packages/white-whale-std/Cargo.toml +++ b/packages/white-whale-std/Cargo.toml @@ -3,10 +3,10 @@ name = "white-whale-std" version = "1.1.3" edition.workspace = true authors = [ - "Kerber0x ", - "0xFable <0xfabledev@gmail.com>", - "kaimen-sano ", - "White Whale ", + "Kerber0x ", + "0xFable <0xfabledev@gmail.com>", + "kaimen-sano ", + "White Whale ", ] description = "Common White Whale types and utils" license.workspace = true @@ -20,7 +20,7 @@ injective = ["token_factory"] osmosis = ["osmosis_token_factory"] token_factory = [] osmosis_token_factory = [ - "token_factory", + "token_factory", ] # this is for the osmosis token factory proto definitions, which defer from the standard token factory :) backtraces = ["cosmwasm-std/backtraces"]