From b7600432d21e5c130f0ba54aea181cb51d87d1cf Mon Sep 17 00:00:00 2001 From: nahem Date: Mon, 29 Apr 2024 13:09:14 +0200 Subject: [PATCH 1/2] 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, From deebeee9b8a1484cc1b92a03cf2584022f359a51 Mon Sep 17 00:00:00 2001 From: nahem Date: Mon, 29 Apr 2024 13:30:03 +0200 Subject: [PATCH 2/2] chore: cargo fmt + cargo clippy + just schemas --- .../pool-manager/schema/pool-manager.json | 50 +++++++++++++++++++ .../pool-manager/schema/raw/query.json | 30 +++++++++++ ...e_to_reverse_simulate_swap_operations.json | 20 ++++++++ .../pool-manager/src/tests/suite.rs | 36 +++++++------ 4 files changed, 120 insertions(+), 16 deletions(-) create mode 100644 contracts/liquidity_hub/pool-manager/schema/raw/response_to_reverse_simulate_swap_operations.json diff --git a/contracts/liquidity_hub/pool-manager/schema/pool-manager.json b/contracts/liquidity_hub/pool-manager/schema/pool-manager.json index 2c93551f..5686e9f6 100644 --- a/contracts/liquidity_hub/pool-manager/schema/pool-manager.json +++ b/contracts/liquidity_hub/pool-manager/schema/pool-manager.json @@ -836,6 +836,7 @@ "additionalProperties": false }, { + "description": "Simulates swap operations.", "type": "object", "required": [ "simulate_swap_operations" @@ -863,6 +864,35 @@ }, "additionalProperties": false }, + { + "description": "Simulates a reverse swap operations, i.e. given the ask asset, how much of the offer asset is needed to perform the swap.", + "type": "object", + "required": [ + "reverse_simulate_swap_operations" + ], + "properties": { + "reverse_simulate_swap_operations": { + "type": "object", + "required": [ + "ask_amount", + "operations" + ], + "properties": { + "ask_amount": { + "$ref": "#/definitions/Uint128" + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/definitions/SwapOperation" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -1378,6 +1408,26 @@ } } }, + "reverse_simulate_swap_operations": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ReverseSimulateSwapOperationsResponse", + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false, + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, "reverse_simulation": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ReverseSimulationResponse", diff --git a/contracts/liquidity_hub/pool-manager/schema/raw/query.json b/contracts/liquidity_hub/pool-manager/schema/raw/query.json index 3e230a04..a74ad26d 100644 --- a/contracts/liquidity_hub/pool-manager/schema/raw/query.json +++ b/contracts/liquidity_hub/pool-manager/schema/raw/query.json @@ -139,6 +139,7 @@ "additionalProperties": false }, { + "description": "Simulates swap operations.", "type": "object", "required": [ "simulate_swap_operations" @@ -166,6 +167,35 @@ }, "additionalProperties": false }, + { + "description": "Simulates a reverse swap operations, i.e. given the ask asset, how much of the offer asset is needed to perform the swap.", + "type": "object", + "required": [ + "reverse_simulate_swap_operations" + ], + "properties": { + "reverse_simulate_swap_operations": { + "type": "object", + "required": [ + "ask_amount", + "operations" + ], + "properties": { + "ask_amount": { + "$ref": "#/definitions/Uint128" + }, + "operations": { + "type": "array", + "items": { + "$ref": "#/definitions/SwapOperation" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ diff --git a/contracts/liquidity_hub/pool-manager/schema/raw/response_to_reverse_simulate_swap_operations.json b/contracts/liquidity_hub/pool-manager/schema/raw/response_to_reverse_simulate_swap_operations.json new file mode 100644 index 00000000..6e6216b7 --- /dev/null +++ b/contracts/liquidity_hub/pool-manager/schema/raw/response_to_reverse_simulate_swap_operations.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ReverseSimulateSwapOperationsResponse", + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false, + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs index 02661ae1..4c1f177a 100644 --- a/contracts/liquidity_hub/pool-manager/src/tests/suite.rs +++ b/contracts/liquidity_hub/pool-manager/src/tests/suite.rs @@ -1,6 +1,8 @@ use cosmwasm_std::testing::MockStorage; use white_whale_std::pool_manager::{ - Config, FeatureToggle, PairInfoResponse, ReverseSimulateSwapOperationsResponse, SimulateSwapOperationsResponse, SwapOperation, SwapRouteCreatorResponse, SwapRouteResponse, SwapRoutesResponse + Config, FeatureToggle, PairInfoResponse, ReverseSimulateSwapOperationsResponse, + SimulateSwapOperationsResponse, SwapOperation, SwapRouteCreatorResponse, SwapRouteResponse, + SwapRoutesResponse, }; use white_whale_std::pool_manager::{InstantiateMsg, PairInfo}; @@ -15,8 +17,8 @@ use white_whale_std::epoch_manager::epoch_manager::{Epoch, EpochConfig}; use white_whale_std::fee::PoolFee; use white_whale_std::incentive_manager::PositionsResponse; use white_whale_std::lp_common::LP_SYMBOL; -use white_whale_std::pool_network::asset::{AssetInfo, PairType}; use white_whale_std::pool_manager::{ReverseSimulationResponse, SimulationResponse}; +use white_whale_std::pool_network::asset::{AssetInfo, PairType}; use white_whale_testing::multi_test::stargate_mock::StargateMock; fn contract_pool_manager() -> Box> { @@ -614,13 +616,14 @@ impl TestingSuite { 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, - }, - ); + 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); @@ -633,13 +636,14 @@ impl TestingSuite { 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, - }, - ); + 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);