From b7600432d21e5c130f0ba54aea181cb51d87d1cf Mon Sep 17 00:00:00 2001 From: nahem Date: Mon, 29 Apr 2024 13:09:14 +0200 Subject: [PATCH] feat(smart-contracts): add reverse simulate swap operations with tests --- .../pool-manager/src/contract.rs | 14 +- .../liquidity_hub/pool-manager/src/helpers.rs | 38 +++- .../src/tests/integration_tests.rs | 190 ++++++++++++++++++ .../pool-manager/src/tests/suite.rs | 43 +++- packages/white-whale-std/src/pool_manager.rs | 22 +- 5 files changed, 284 insertions(+), 23 deletions(-) diff --git a/contracts/liquidity_hub/pool-manager/src/contract.rs b/contracts/liquidity_hub/pool-manager/src/contract.rs index d41d35b4..66d60308 100644 --- a/contracts/liquidity_hub/pool-manager/src/contract.rs +++ b/contracts/liquidity_hub/pool-manager/src/contract.rs @@ -1,5 +1,5 @@ use crate::error::ContractError; -use crate::helpers::simulate_swap_operations; +use crate::helpers::{reverse_simulate_swap_operations, simulate_swap_operations}; use crate::queries::{get_swap_route, get_swap_route_creator, get_swap_routes}; use crate::router::commands::{add_swap_routes, remove_swap_routes}; use crate::state::{Config, MANAGER_CONFIG, PAIRS, PAIR_COUNTER}; @@ -228,12 +228,12 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result Ok(to_binary(&queries::reverse_simulate_swap_operations( - // deps, env, ask_amount, operations, - // )?)?), + QueryMsg::ReverseSimulateSwapOperations { + ask_amount, + operations, + } => Ok(to_json_binary(&reverse_simulate_swap_operations( + deps, ask_amount, operations, + )?)?), QueryMsg::SwapRoute { offer_asset_denom, ask_asset_denom, diff --git a/contracts/liquidity_hub/pool-manager/src/helpers.rs b/contracts/liquidity_hub/pool-manager/src/helpers.rs index c1ae5a73..472194b5 100644 --- a/contracts/liquidity_hub/pool-manager/src/helpers.rs +++ b/contracts/liquidity_hub/pool-manager/src/helpers.rs @@ -620,11 +620,39 @@ pub fn simulate_swap_operations( token_out_denom: _, pool_identifier, } => { - let res = query_simulation( - deps, - coin(offer_amount.u128(), token_in_denom), - pool_identifier, - )?; + let res = + query_simulation(deps, coin(amount.u128(), token_in_denom), pool_identifier)?; + amount = res.return_amount; + } + } + } + + Ok(SimulateSwapOperationsResponse { amount }) +} + +/// This function iterates over the swap operations in the reverse order, +/// simulates each swap to get the final amount after all the swaps. +pub fn reverse_simulate_swap_operations( + deps: Deps, + ask_amount: Uint128, + operations: Vec, +) -> Result { + let operations_len = operations.len(); + if operations_len == 0 { + return Err(ContractError::NoSwapOperationsProvided {}); + } + + let mut amount = ask_amount; + + for operation in operations.into_iter().rev() { + match operation { + SwapOperation::WhaleSwap { + token_in_denom: _, + token_out_denom, + pool_identifier, + } => { + let res = + query_simulation(deps, coin(amount.u128(), token_out_denom), pool_identifier)?; amount = res.return_amount; } } diff --git a/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs b/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs index 7fc2e991..fc432a8d 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/integration_tests.rs @@ -1558,6 +1558,196 @@ mod router { ); }); } + + #[test] + fn query_swap_operations() { + let mut suite = TestingSuite::default_with_balances(vec![ + coin(1_000_000_000u128, "uwhale".to_string()), + coin(1_000_000_000u128, "uluna".to_string()), + coin(1_000_000_000u128, "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!["uwhale".to_string(), "uluna".to_string()]; + let second_pair = vec!["uluna".to_string(), "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% + }, + extra_fees: vec![], + }; + #[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), + }, + extra_fees: vec![], + }; + + // Create a pair + suite + .instantiate_default() + .create_pair( + creator.clone(), + first_pair, + vec![6u8, 6u8], + pool_fees.clone(), + white_whale_std::pool_network::asset::PairType::ConstantProduct, + Some("whale-uluna".to_string()), + vec![coin(1_000, "uusd")], + |result| { + result.unwrap(); + }, + ) + .create_pair( + creator.clone(), + second_pair, + vec![6u8, 6u8], + pool_fees, + white_whale_std::pool_network::asset::PairType::ConstantProduct, + Some("uluna-uusd".to_string()), + vec![coin(1_000, "uusd")], + |result| { + result.unwrap(); + }, + ); + + // Lets try to add liquidity + suite.provide_liquidity( + creator.clone(), + "whale-uluna".to_string(), + None, + None, + vec![ + Coin { + denom: "uwhale".to_string(), + amount: Uint128::from(1_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(1_000_000u128), + }, + ], + |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(), + None, + None, + vec![ + Coin { + denom: "uluna".to_string(), + amount: Uint128::from(1_000_000u128), + }, + Coin { + denom: "uusd".to_string(), + amount: Uint128::from(1_000_000u128), + }, + ], + |result| { + // ensure we got 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_denom: "uwhale".to_string(), + token_out_denom: "uluna".to_string(), + pool_identifier: "whale-uluna".to_string(), + }, + white_whale_std::pool_manager::SwapOperation::WhaleSwap { + token_in_denom: "uluna".to_string(), + token_out_denom: "uusd".to_string(), + pool_identifier: "uluna-uusd".to_string(), + }, + ]; + + // simulating (reverse) swap operations should return the correct same amount as the pools are balanced + // going from whale -> uusd should return 974 uusd + // going from uusd -> whale should return 974 whale + suite.query_simulate_swap_operations( + Uint128::new(1_000), + swap_operations.clone(), + |result| { + let result = result.unwrap(); + assert_eq!(result.amount.u128(), 974); + }, + ); + suite.query_reverse_simulate_swap_operations( + Uint128::new(1_000), + swap_operations.clone(), + |result| { + let result = result.unwrap(); + assert_eq!(result.amount.u128(), 974); + }, + ); + + // execute the swap operations to unbalance the pools + // sold 10_000 whale for some uusd, so the price of whale should go down + suite.execute_swap_operations( + creator.clone(), + swap_operations.clone(), + None, + None, + None, + vec![coin(10_000u128, "uwhale".to_string())], + |result| { + result.unwrap(); + }, + ); + + // now to get 1_000 uusd we should swap more whale than before + suite.query_reverse_simulate_swap_operations( + Uint128::new(1_000), + swap_operations.clone(), + |result| { + let result = result.unwrap(); + assert_eq!(result.amount.u128(), 1_006); + }, + ); + + // and if simulate swap operations with 1_000 more whale we should get even less uusd than before + suite.query_simulate_swap_operations( + Uint128::new(1_000), + swap_operations.clone(), + |result| { + let result = result.unwrap(); + assert_eq!(result.amount.u128(), 935); + }, + ); + } } mod swapping { diff --git a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs index a506d092..02661ae1 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs @@ -1,7 +1,6 @@ use cosmwasm_std::testing::MockStorage; use white_whale_std::pool_manager::{ - Config, FeatureToggle, PairInfoResponse, SwapOperation, SwapRouteCreatorResponse, - SwapRouteResponse, SwapRoutesResponse, + Config, FeatureToggle, PairInfoResponse, ReverseSimulateSwapOperationsResponse, SimulateSwapOperationsResponse, SwapOperation, SwapRouteCreatorResponse, SwapRouteResponse, SwapRoutesResponse }; use white_whale_std::pool_manager::{InstantiateMsg, PairInfo}; @@ -17,7 +16,7 @@ use white_whale_std::fee::PoolFee; use white_whale_std::incentive_manager::PositionsResponse; use white_whale_std::lp_common::LP_SYMBOL; use white_whale_std::pool_network::asset::{AssetInfo, PairType}; -use white_whale_std::pool_network::pair::{ReverseSimulationResponse, SimulationResponse}; +use white_whale_std::pool_manager::{ReverseSimulationResponse, SimulationResponse}; use white_whale_testing::multi_test::stargate_mock::StargateMock; fn contract_pool_manager() -> Box> { @@ -609,6 +608,44 @@ impl TestingSuite { self } + pub(crate) fn query_simulate_swap_operations( + &mut self, + offer_amount: Uint128, + operations: Vec, + result: impl Fn(StdResult), + ) -> &mut Self { + let pair_info_response: StdResult = self.app.wrap().query_wasm_smart( + &self.pool_manager_addr, + &white_whale_std::pool_manager::QueryMsg::SimulateSwapOperations { + offer_amount, + operations, + }, + ); + + result(pair_info_response); + + self + } + + pub(crate) fn query_reverse_simulate_swap_operations( + &mut self, + ask_amount: Uint128, + operations: Vec, + result: impl Fn(StdResult), + ) -> &mut Self { + let pair_info_response: StdResult = self.app.wrap().query_wasm_smart( + &self.pool_manager_addr, + &white_whale_std::pool_manager::QueryMsg::ReverseSimulateSwapOperations { + ask_amount, + operations, + }, + ); + + result(pair_info_response); + + self + } + pub(crate) fn query_amount_of_lp_token( &mut self, identifier: String, diff --git a/packages/white-whale-std/src/pool_manager.rs b/packages/white-whale-std/src/pool_manager.rs index cea8ae97..9fc28f10 100644 --- a/packages/white-whale-std/src/pool_manager.rs +++ b/packages/white-whale-std/src/pool_manager.rs @@ -248,19 +248,20 @@ pub enum QueryMsg { #[returns(SwapRoutesResponse)] SwapRoutes {}, - // /// Simulates swap operations. + /// Simulates swap operations. #[returns(SimulateSwapOperationsResponse)] SimulateSwapOperations { offer_amount: Uint128, operations: Vec, }, - // /// Simulates a reverse swap operations, i.e. given the ask asset, how much of the offer asset - // /// is needed to perform the swap. - // #[returns(SimulateSwapOperationsResponse)] - // ReverseSimulateSwapOperations { - // ask_amount: Uint128, - // operations: Vec, - // }, + /// Simulates a reverse swap operations, i.e. given the ask asset, how much of the offer asset + /// is needed to perform the swap. + #[returns(ReverseSimulateSwapOperationsResponse)] + ReverseSimulateSwapOperations { + ask_amount: Uint128, + operations: Vec, + }, + #[returns(PairInfoResponse)] Pair { pair_identifier: String }, /// Retrieves the creator of the swap routes that can then remove them. @@ -335,6 +336,11 @@ pub struct SimulateSwapOperationsResponse { pub amount: Uint128, } +#[cw_serde] +pub struct ReverseSimulateSwapOperationsResponse { + pub amount: Uint128, +} + #[cw_serde] pub struct SwapRouteCreatorResponse { pub creator: String,