diff --git a/.cargo/audit.toml b/.cargo/audit.toml index a2e9d4635..d7d5f6f73 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -3,4 +3,4 @@ [advisories] # Ignore the following advisory IDs. # Reported vulnerabilities relate to test-tube which is only used for testing. -ignore = ["RUSTSEC-2024-0003", "RUSTSEC-2024-0006"] \ No newline at end of file +ignore = ["RUSTSEC-2024-0003", "RUSTSEC-2024-0006", "RUSTSEC-2024-0019"] \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 42e8d64c0..17f8b6a95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2354,6 +2354,7 @@ dependencies = [ name = "mars-osmosis" version = "2.0.0" dependencies = [ + "cosmwasm-schema", "cosmwasm-std", "osmosis-std 0.22.0", "prost 0.12.3", @@ -2602,7 +2603,7 @@ dependencies = [ [[package]] name = "mars-swapper-osmosis" -version = "2.0.1" +version = "2.0.2" dependencies = [ "anyhow", "cosmwasm-schema", @@ -2616,6 +2617,7 @@ dependencies = [ "mars-testing", "mars-types", "osmosis-std 0.22.0", + "serde", ] [[package]] diff --git a/contracts/oracle/osmosis/src/helpers.rs b/contracts/oracle/osmosis/src/helpers.rs index a9e8889cf..64920b27d 100644 --- a/contracts/oracle/osmosis/src/helpers.rs +++ b/contracts/oracle/osmosis/src/helpers.rs @@ -26,6 +26,7 @@ pub fn assert_osmosis_pool_assets( } Pool::StableSwap(_) => {} Pool::ConcentratedLiquidity(_) => {} + Pool::CosmWasm(_) => {} }; Ok(()) @@ -47,6 +48,11 @@ pub fn assert_osmosis_xyk_lp_pool(pool: &Pool) -> ContractResult<()> { reason: format!("ConcentratedLiquidity pool not supported. Pool id {}", cl_pool.id), }); } + Pool::CosmWasm(cw_pool) => { + return Err(ContractError::InvalidPriceSource { + reason: format!("CosmWasm pool not supported. Pool id {}", cw_pool.id), + }); + } }; Ok(()) diff --git a/contracts/oracle/osmosis/src/price_source.rs b/contracts/oracle/osmosis/src/price_source.rs index 884c1992f..8d32c68b9 100644 --- a/contracts/oracle/osmosis/src/price_source.rs +++ b/contracts/oracle/osmosis/src/price_source.rs @@ -624,6 +624,11 @@ impl OsmosisPriceSourceChecked { ), }) } + Pool::CosmWasm(pool) => { + return Err(ContractError::InvalidPrice { + reason: format!("CosmWasm pool not supported. Pool id {}", pool.id), + }) + } }; let coin0 = Pool::unwrap_coin(&pool.pool_assets[0].token)?; diff --git a/contracts/oracle/osmosis/tests/tests/helpers/mod.rs b/contracts/oracle/osmosis/tests/tests/helpers/mod.rs index f74d38137..3d5e65c6d 100644 --- a/contracts/oracle/osmosis/tests/tests/helpers/mod.rs +++ b/contracts/oracle/osmosis/tests/tests/helpers/mod.rs @@ -5,14 +5,18 @@ use std::{marker::PhantomData, str::FromStr}; use cosmwasm_std::{ coin, from_json, testing::{mock_env, MockApi, MockQuerier, MockStorage}, - Coin, Decimal, Deps, DepsMut, OwnedDeps, + to_json_vec, Coin, Decimal, Deps, DepsMut, OwnedDeps, }; use mars_oracle_base::ContractError; use mars_oracle_osmosis::{contract::entry, msg::ExecuteMsg, OsmosisPriceSourceUnchecked}; use mars_osmosis::{BalancerPool, ConcentratedLiquidityPool, StableSwapPool}; use mars_testing::{mock_info, MarsMockQuerier}; use mars_types::oracle::{InstantiateMsg, QueryMsg}; -use osmosis_std::types::osmosis::{gamm::v1beta1::PoolAsset, poolmanager::v1beta1::PoolResponse}; +use osmosis_std::types::osmosis::{ + cosmwasmpool::v1beta1::{CosmWasmPool, InstantiateMsg as CosmwasmPoolInstantiateMsg}, + gamm::v1beta1::PoolAsset, + poolmanager::v1beta1::PoolResponse, +}; use pyth_sdk_cw::PriceIdentifier; pub fn setup_test_with_pools() -> OwnedDeps { @@ -100,6 +104,12 @@ pub fn setup_test_with_pools() -> OwnedDeps PoolResponse { + let msg = CosmwasmPoolInstantiateMsg { + pool_asset_denoms: vec![token0.to_string(), token1.to_string()], + }; + let pool = CosmWasmPool { + contract_address: "osmo10c8y69yylnlwrhu32ralf08ekladhfknfqrjsy9yqc9ml8mlxpqq2sttzk" + .to_string(), + pool_id, + code_id: 148, + instantiate_msg: to_json_vec(&msg).unwrap(), + }; + PoolResponse { + pool: Some(pool.to_any()), + } +} + pub fn set_pyth_price_source(deps: DepsMut, denom: &str, price_id: PriceIdentifier) { set_price_source( deps, diff --git a/contracts/oracle/osmosis/tests/tests/test_set_price_source.rs b/contracts/oracle/osmosis/tests/tests/test_set_price_source.rs index 22cf6b413..a6c4a4158 100644 --- a/contracts/oracle/osmosis/tests/tests/test_set_price_source.rs +++ b/contracts/oracle/osmosis/tests/tests/test_set_price_source.rs @@ -1038,6 +1038,15 @@ fn setting_price_source_xyk_lp() { } ); + // attempting to use CosmWasm pool + let err = set_price_source_xyk_lp("uausdc_unusdc_lp", 8888).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "CosmWasm pool not supported. Pool id 8888".to_string() + } + ); + // properly set xyk lp price source let res = set_price_source_xyk_lp("uosmo_umars_lp", 89).unwrap(); assert_eq!(res.messages.len(), 0); diff --git a/contracts/swapper/osmosis/Cargo.toml b/contracts/swapper/osmosis/Cargo.toml index 66217813d..c418c1aa8 100644 --- a/contracts/swapper/osmosis/Cargo.toml +++ b/contracts/swapper/osmosis/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mars-swapper-osmosis" -version = "2.0.1" +version = "2.0.2" authors = { workspace = true } license = { workspace = true } edition = { workspace = true } @@ -34,3 +34,4 @@ osmosis-std = { workspace = true } anyhow = { workspace = true } cw-it = { workspace = true, features = ["osmosis-test-tube"] } mars-testing = { workspace = true } +serde = { workspace = true } diff --git a/contracts/swapper/osmosis/src/contract.rs b/contracts/swapper/osmosis/src/contract.rs index 93c1734de..3722602c7 100644 --- a/contracts/swapper/osmosis/src/contract.rs +++ b/contracts/swapper/osmosis/src/contract.rs @@ -41,6 +41,6 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> ContractResult { pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { match msg { MigrateMsg::V1_0_0ToV2_0_0 {} => migrations::v2_0_0::migrate(deps), - MigrateMsg::V2_0_0ToV2_0_1 {} => migrations::v2_0_1::migrate(deps), + MigrateMsg::V2_0_1ToV2_0_2 {} => migrations::v2_0_2::migrate(deps), } } diff --git a/contracts/swapper/osmosis/src/migrations/mod.rs b/contracts/swapper/osmosis/src/migrations/mod.rs index 24a4db4ab..55e91380a 100644 --- a/contracts/swapper/osmosis/src/migrations/mod.rs +++ b/contracts/swapper/osmosis/src/migrations/mod.rs @@ -1,2 +1,2 @@ pub mod v2_0_0; -pub mod v2_0_1; +pub mod v2_0_2; diff --git a/contracts/swapper/osmosis/src/migrations/v2_0_1.rs b/contracts/swapper/osmosis/src/migrations/v2_0_2.rs similarity index 95% rename from contracts/swapper/osmosis/src/migrations/v2_0_1.rs rename to contracts/swapper/osmosis/src/migrations/v2_0_2.rs index 641f6b1eb..c9f573469 100644 --- a/contracts/swapper/osmosis/src/migrations/v2_0_1.rs +++ b/contracts/swapper/osmosis/src/migrations/v2_0_2.rs @@ -4,7 +4,7 @@ use mars_swapper_base::ContractError; use crate::contract::{CONTRACT_NAME, CONTRACT_VERSION}; -const FROM_VERSION: &str = "2.0.0"; +const FROM_VERSION: &str = "2.0.1"; pub fn migrate(deps: DepsMut) -> Result { // make sure we're migrating the correct contract and from the correct version diff --git a/contracts/swapper/osmosis/src/route.rs b/contracts/swapper/osmosis/src/route.rs index 1f0df365e..586b69036 100644 --- a/contracts/swapper/osmosis/src/route.rs +++ b/contracts/swapper/osmosis/src/route.rs @@ -1,8 +1,8 @@ use std::fmt; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{BlockInfo, CosmosMsg, Decimal, Empty, Env, Fraction, QuerierWrapper, Uint128}; -use mars_osmosis::helpers::{query_arithmetic_twap_price, query_pool, CommonPoolData}; +use cosmwasm_std::{coin, BlockInfo, CosmosMsg, Decimal, Empty, Env, QuerierWrapper, Uint128}; +use mars_osmosis::helpers::{query_arithmetic_twap_price, query_pool, CommonPoolData, Pool}; use mars_swapper_base::{ContractError, ContractResult, Route}; use mars_types::swapper::{EstimateExactInSwapResponse, SwapperRoute}; use osmosis_std::types::osmosis::gamm::v1beta1::MsgSwapExactAmountIn; @@ -198,21 +198,25 @@ fn query_out_amount( ) -> ContractResult { let start_time = block.time.seconds() - TWAP_WINDOW_SIZE_SECONDS; - let mut price = Decimal::one(); - let mut denom_in = coin_in.denom.clone(); + let mut coin_in = coin_in.clone(); for step in steps { - let step_price = query_arithmetic_twap_price( - querier, - step.pool_id, - &denom_in, - &step.token_out_denom, - start_time, - )?; - price = price.checked_mul(step_price)?; - denom_in = step.token_out_denom.clone(); + let pool = query_pool(querier, step.pool_id)?; + let out_amount = if let Pool::CosmWasm(cw_pool) = pool { + // TWAP not supported. + // This is transmuter (https://github.com/osmosis-labs/transmuter) pool. + cw_pool.query_out_amount(querier, step.pool_id, &coin_in, &step.token_out_denom)? + } else { + let price = query_arithmetic_twap_price( + querier, + step.pool_id, + &coin_in.denom, + &step.token_out_denom, + start_time, + )?; + coin_in.amount.checked_mul_floor(price)? + }; + coin_in = coin(out_amount.u128(), &step.token_out_denom); } - let out_amount = - coin_in.amount.checked_multiply_ratio(price.numerator(), price.denominator())?; - Ok(out_amount) + Ok(coin_in.amount) } diff --git a/contracts/swapper/osmosis/tests/tests/test_estimate.rs b/contracts/swapper/osmosis/tests/tests/test_estimate.rs index 203fb0fc5..90bfe4330 100644 --- a/contracts/swapper/osmosis/tests/tests/test_estimate.rs +++ b/contracts/swapper/osmosis/tests/tests/test_estimate.rs @@ -1,11 +1,26 @@ -use cosmwasm_std::{coin, Uint128}; +use std::{marker::PhantomData, str::FromStr}; + +use cosmwasm_std::{ + coin, from_json, + testing::{mock_env, MockApi, MockQuerier, MockStorage}, + to_json_vec, Decimal, OwnedDeps, Uint128, +}; use cw_it::osmosis_test_tube::{Gamm, Module, OsmosisTestApp, RunnerResult, Wasm}; +use mars_osmosis::ConcentratedLiquidityPool; use mars_swapper_osmosis::{ config::OsmosisConfig, + contract::{instantiate, query}, route::{OsmosisRoute, SwapAmountInRoute}, }; +use mars_testing::{mock_info, MarsMockQuerier}; use mars_types::swapper::{ - EstimateExactInSwapResponse, ExecuteMsg, OsmoRoute, OsmoSwap, QueryMsg, SwapperRoute, + EstimateExactInSwapResponse, ExecuteMsg, InstantiateMsg, OsmoRoute, OsmoSwap, QueryMsg, + SwapperRoute, +}; +use osmosis_std::types::osmosis::{ + cosmwasmpool::v1beta1::{CosmWasmPool, InstantiateMsg as CosmwasmPoolInstantiateMsg}, + poolmanager::v1beta1::PoolResponse, + twap::v1beta1::ArithmeticTwapToNowResponse, }; use super::helpers::{ @@ -293,3 +308,106 @@ fn estimate_swap_multi_step() { .unwrap(); assert_eq!(res.amount, expected_output); } + +#[test] +fn estimate_swap_multi_step_with_cosmwasm_pool() { + let mut deps = OwnedDeps::<_, _, _> { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MarsMockQuerier::new(MockQuerier::new(&[])), + custom_query_type: PhantomData, + }; + + // instantiate the swapper contract + instantiate( + deps.as_mut(), + mock_env(), + mock_info("owner"), + InstantiateMsg { + owner: "owner".to_string(), + }, + ) + .unwrap(); + + let atom = "uatom".to_string(); + let noble_usdc = "unusdc".to_string(); + let axl_usdc = "uausdc".to_string(); + + // prepare ConcentratedLiquidity pool + let cl_pool_id = 1251; + let pool = ConcentratedLiquidityPool { + address: "osmo126pr9qp44aft4juw7x4ev4s2qdtnwe38jzwunec9pxt5cpzaaphqyagqpu".to_string(), + incentives_address: "osmo1h2mhtj3wmsdt3uacev9pgpg38hkcxhsmyyn9ums0ya6eddrsafjsxs9j03" + .to_string(), + spread_rewards_address: "osmo16j5sssw32xuk8a0kjj8n54g25ye6kr339nz5axf8lzyeajk0k22stsm36c" + .to_string(), + id: cl_pool_id, + current_tick_liquidity: "3820025893854099618.699762490947860933".to_string(), + token0: atom.clone(), + token1: noble_usdc.clone(), + current_sqrt_price: "656651.537483144215151633465586753226461989".to_string(), + current_tick: 102311912, + tick_spacing: 100, + exponent_at_price_one: -6, + spread_factor: "0.002000000000000000".to_string(), + last_liquidity_update: None, + }; + let cl_pool = PoolResponse { + pool: Some(pool.to_any()), + }; + + // prepare CosmWasm (transmuter) pool + let cw_pool_id = 1212; + let msg = CosmwasmPoolInstantiateMsg { + pool_asset_denoms: vec![axl_usdc.clone(), noble_usdc.clone()], + }; + let pool = CosmWasmPool { + contract_address: "osmo10c8y69yylnlwrhu32ralf08ekladhfknfqrjsy9yqc9ml8mlxpqq2sttzk" + .to_string(), + pool_id: cw_pool_id, + code_id: 148, + instantiate_msg: to_json_vec(&msg).unwrap(), + }; + let cw_pool = PoolResponse { + pool: Some(pool.to_any()), + }; + + deps.querier.set_query_pool_response(cl_pool_id, cl_pool); + deps.querier.set_query_pool_response(cw_pool_id, cw_pool); + + // set arithmetic twap price for the ConcentratedLiquidity pool + deps.querier.set_arithmetic_twap_price( + cl_pool_id, + &atom, + &noble_usdc, + ArithmeticTwapToNowResponse { + arithmetic_twap: Decimal::from_str("11.5").unwrap().to_string(), + }, + ); + + // check the estimate swap output + let res = query( + deps.as_ref(), + mock_env(), + QueryMsg::EstimateExactInSwap { + coin_in: coin(1250, &atom), + denom_out: axl_usdc.clone(), + route: Some(SwapperRoute::Osmo(OsmoRoute { + swaps: vec![ + OsmoSwap { + pool_id: cl_pool_id, + to: noble_usdc, + }, + OsmoSwap { + pool_id: cw_pool_id, + to: axl_usdc, + }, + ], + })), + }, + ) + .unwrap(); + let res: EstimateExactInSwapResponse = from_json(res).unwrap(); + // 1250 * 11.5 * 1 = 14375 + assert_eq!(res.amount, Uint128::from(14375u128)); +} diff --git a/contracts/swapper/osmosis/tests/tests/test_migration_v2.rs b/contracts/swapper/osmosis/tests/tests/test_migration_v2.rs index d73a914f6..45279614d 100644 --- a/contracts/swapper/osmosis/tests/tests/test_migration_v2.rs +++ b/contracts/swapper/osmosis/tests/tests/test_migration_v2.rs @@ -64,12 +64,12 @@ fn successful_migration() { assert!(res.data.is_none()); assert_eq!( res.attributes, - vec![attr("action", "migrate"), attr("from_version", "1.0.0"), attr("to_version", "2.0.1")] // to_version should be 2.0.0 but because of global current version in Cargo.toml is different + vec![attr("action", "migrate"), attr("from_version", "1.0.0"), attr("to_version", "2.0.2")] // to_version should be 2.0.0 but because of global current version in Cargo.toml is different ); let new_contract_version = ContractVersion { contract: "crates.io:mars-swapper-osmosis".to_string(), - version: "2.0.1".to_string(), // should be 2.0.0 but global current version in Cargo.toml is different + version: "2.0.2".to_string(), // should be 2.0.0 but global current version in Cargo.toml is different }; assert_eq!(cw2::get_contract_version(deps.as_ref().storage).unwrap(), new_contract_version); @@ -85,22 +85,22 @@ fn successful_migration() { #[test] fn successful_migration_to_v2_0_2() { let mut deps = mock_dependencies(&[]); - cw2::set_contract_version(deps.as_mut().storage, "crates.io:mars-swapper-osmosis", "2.0.0") + cw2::set_contract_version(deps.as_mut().storage, "crates.io:mars-swapper-osmosis", "2.0.1") .unwrap(); - let res = migrate(deps.as_mut(), mock_env(), MigrateMsg::V2_0_0ToV2_0_1 {}).unwrap(); + let res = migrate(deps.as_mut(), mock_env(), MigrateMsg::V2_0_1ToV2_0_2 {}).unwrap(); assert_eq!(res.messages, vec![]); assert_eq!(res.events, vec![] as Vec); assert!(res.data.is_none()); assert_eq!( res.attributes, - vec![attr("action", "migrate"), attr("from_version", "2.0.0"), attr("to_version", "2.0.1")] + vec![attr("action", "migrate"), attr("from_version", "2.0.1"), attr("to_version", "2.0.2")] ); let new_contract_version = ContractVersion { contract: "crates.io:mars-swapper-osmosis".to_string(), - version: "2.0.1".to_string(), + version: "2.0.2".to_string(), }; assert_eq!(cw2::get_contract_version(deps.as_ref().storage).unwrap(), new_contract_version); } diff --git a/packages/chains/osmosis/Cargo.toml b/packages/chains/osmosis/Cargo.toml index 7f44f9934..6360acc9e 100755 --- a/packages/chains/osmosis/Cargo.toml +++ b/packages/chains/osmosis/Cargo.toml @@ -18,7 +18,8 @@ doctest = false backtraces = ["cosmwasm-std/backtraces"] [dependencies] -cosmwasm-std = { workspace = true } -osmosis-std = { workspace = true } -serde = { workspace = true } -prost = { workspace = true } +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +osmosis-std = { workspace = true } +serde = { workspace = true } +prost = { workspace = true } diff --git a/packages/chains/osmosis/src/helpers.rs b/packages/chains/osmosis/src/helpers.rs index 875d38a13..5deffc0d0 100644 --- a/packages/chains/osmosis/src/helpers.rs +++ b/packages/chains/osmosis/src/helpers.rs @@ -1,7 +1,9 @@ use std::str::FromStr; +use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - coin, Decimal, Empty, QuerierWrapper, QueryRequest, StdError, StdResult, Uint128, + coin, from_json, to_json_binary, Decimal, Empty, QuerierWrapper, QueryRequest, StdError, + StdResult, Uint128, WasmQuery, }; use osmosis_std::{ shim::{Duration, Timestamp}, @@ -9,6 +11,10 @@ use osmosis_std::{ cosmos::base::v1beta1::Coin, osmosis::{ concentratedliquidity::v1beta1::Pool as ConcentratedLiquidityPool, + cosmwasmpool::v1beta1::{ + CalcOutAmtGivenIn, CalcOutAmtGivenInRequest, CalcOutAmtGivenInResponse, + CosmWasmPool as OsmoCosmWasmPool, CosmwasmpoolQuerier, InstantiateMsg, + }, downtimedetector::v1beta1::DowntimedetectorQuerier, gamm::{ poolmodels::stableswap::v1beta1::Pool as StableSwapPool, @@ -21,6 +27,43 @@ use osmosis_std::{ }; use prost::Message; +#[derive(Debug, PartialEq)] +pub struct CosmWasmPool { + pub id: u64, + pub pool_asset_configs: Vec, +} + +impl CosmWasmPool { + pub fn query_out_amount( + &self, + querier: &QuerierWrapper, + pool_id: u64, + coin_in: &cosmwasm_std::Coin, + denom_out: &str, + ) -> StdResult { + let contract_addr = query_cosmwasm_pool_contract_addr(querier, pool_id)?; + let out_amount = + query_cosmwasm_pool_out_amount(querier, &contract_addr, coin_in, denom_out)?; + Ok(out_amount) + } +} + +/// Fields taken from Instantiate msg https://github.com/osmosis-labs/transmuter/blob/47bbb023463578937a7086ad80071196126349d9/contracts/transmuter/src/contract.rs#L74 +#[cw_serde] +struct TransmuterV3InstantiateMsg { + pub pool_asset_configs: Vec, + pub alloyed_asset_subdenom: String, + pub alloyed_asset_normalization_factor: Uint128, + pub admin: Option, + pub moderator: Option, +} + +#[cw_serde] +pub struct AssetConfig { + pub denom: String, + pub normalization_factor: Uint128, +} + // Get denoms from different type of the pool pub trait CommonPoolData { fn get_pool_id(&self) -> u64; @@ -32,6 +75,7 @@ pub enum Pool { Balancer(BalancerPool), StableSwap(StableSwapPool), ConcentratedLiquidity(ConcentratedLiquidityPool), + CosmWasm(CosmWasmPool), } impl CommonPoolData for Pool { @@ -40,6 +84,7 @@ impl CommonPoolData for Pool { Pool::Balancer(pool) => pool.id, Pool::StableSwap(pool) => pool.id, Pool::ConcentratedLiquidity(pool) => pool.id, + Pool::CosmWasm(pool) => pool.id, } } @@ -57,6 +102,9 @@ impl CommonPoolData for Pool { Pool::ConcentratedLiquidity(pool) => { vec![pool.token0.clone(), pool.token1.clone()] } + Pool::CosmWasm(pool) => { + pool.pool_asset_configs.iter().map(|ac| ac.denom.clone()).collect() + } } } } @@ -77,9 +125,60 @@ impl TryFrom for Pool { return Ok(Pool::ConcentratedLiquidity(pool)); } + if let Ok(pool) = OsmoCosmWasmPool::decode(value.value.as_slice()) { + // Try to parse the instantiate message of the cosmwasm pool: + // V1: + // ```json + // { + // "pool_asset_denoms": [ + // "ibc/40F1B2458AEDA66431F9D44F48413240B8D28C072463E2BF53655728683583E3", + // "ibc/6F34E1BD664C36CE49ACC28E60D62559A5F96C4F9A6CCE4FC5A67B2852E24CFE" + // ] + // } + // + // V2: + // ```json + // { + // "pool_asset_denoms": [ + // "uosmo", + // "factory/osmo14eq94mckd6kp0pwnxx33ycpk762z7rum29epr3/teko02" + // ], + // "admin": "osmo14eq94mckd6kp0pwnxx33ycpk762z7rum29epr3", + // "alloyed_asset_subdenom": "teko" + // } + // + // Both of them have the same field `pool_asset_denoms` and it's the only field we need to use. + if let Ok(msg) = from_json::(&pool.instantiate_msg) { + return Ok(Pool::CosmWasm(CosmWasmPool { + id: pool.pool_id, + pool_asset_configs: msg + .pool_asset_denoms + .iter() + .map(|denom| AssetConfig { + denom: denom.clone(), + normalization_factor: Uint128::one(), // 1:1 conversion of one asset to another + }) + .collect(), + })); + } + + // try to parse the instantiate message V3 + if let Ok(msg) = from_json::(&pool.instantiate_msg) { + return Ok(Pool::CosmWasm(CosmWasmPool { + id: pool.pool_id, + pool_asset_configs: msg.pool_asset_configs, + })); + } + + return Err(StdError::parse_err( + "Pool", + "Failed to parse CosmWasm pool instantiate message.", + )); + } + Err(StdError::parse_err( "Pool", - "Unsupported pool: must be either `Balancer`, `StableSwap` or `ConcentratedLiquidity`.", + "Unsupported pool: must be either `Balancer`, `StableSwap`, `ConcentratedLiquidity` or CosmWasm transmuter.", )) } } @@ -186,12 +285,55 @@ pub fn recovered_since_downtime_of_length( Ok(downtime_detector_res.succesfully_recovered) } +/// Query contract address for cosmwasm pool id. It is used to query smart contract (e.g. `calc_out_amt_given_in`). +pub fn query_cosmwasm_pool_contract_addr( + querier: &QuerierWrapper, + pool_id: u64, +) -> StdResult { + let res = CosmwasmpoolQuerier::new(querier).contract_info_by_pool_id(pool_id)?; + Ok(res.contract_address) +} + +/// Execute `calc_out_amt_given_in` query on CosmWasm pool contract +pub fn query_cosmwasm_pool_out_amount( + querier: &QuerierWrapper, + contract_addr: &str, + token_in: &cosmwasm_std::Coin, + token_out_denom: &str, +) -> StdResult { + let res: CalcOutAmtGivenInResponse = querier.query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: contract_addr.to_string(), + msg: to_json_binary(&CalcOutAmtGivenInRequest { + calc_out_amt_given_in: Some(CalcOutAmtGivenIn { + token_in: Some(Coin { + denom: token_in.denom.to_string(), + amount: token_in.amount.to_string(), + }), + token_out_denom: token_out_denom.to_string(), + swap_fee: "0".to_string(), // 0 is required by the contract + }), + })?, + }))?; + // token_out should be available so `expect` just in case + let amount_str = + res.token_out.expect("token_out not found for CalcOutAmtGivenInRequest response").amount; + Uint128::from_str(&amount_str) +} + #[cfg(test)] mod tests { + use cosmwasm_std::to_json_vec; use osmosis_std::types::osmosis::gamm::v1beta1::PoolAsset; use super::*; + #[cw_serde] + struct TransmuterV1InstantiateMsg { + pub pool_asset_denoms: Vec, + pub alloyed_asset_subdenom: String, + pub admin: Option, + } + #[test] fn unwrapping_coin() { let pool = BalancerPool { @@ -322,4 +464,115 @@ mod tests { pool.get_pool_denoms() ); } + + #[test] + fn common_data_for_cosmwasm_pool_v1() { + let msg = InstantiateMsg { + pool_asset_denoms: vec![ + "ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858".to_string(), + "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4".to_string(), + ], + }; + let cosmwasm_pool = OsmoCosmWasmPool { + contract_address: "pool_address".to_string(), + pool_id: 1212, + code_id: 148, + instantiate_msg: to_json_vec(&msg).unwrap(), + }; + + let any_pool = cosmwasm_pool.to_any(); + let pool: Pool = any_pool.try_into().unwrap(); + + assert_eq!(cosmwasm_pool.pool_id, pool.get_pool_id()); + assert_eq!( + vec![ + "ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858".to_string(), + "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4".to_string() + ], + pool.get_pool_denoms() + ); + } + + #[test] + fn common_data_for_cosmwasm_pool_v2() { + // check if extra fields are ignored during deserialization + let msg = TransmuterV1InstantiateMsg { + pool_asset_denoms: vec![ + "uosmo".to_string(), + "factory/osmo14eq94mckd6kp0pwnxx33ycpk762z7rum29epr3/teko02".to_string(), + ], + alloyed_asset_subdenom: "teko".to_string(), + admin: Some("osmo14eq94mckd6kp0pwnxx33ycpk762z7rum29epr3".to_string()), + }; + let cosmwasm_pool = OsmoCosmWasmPool { + contract_address: "pool_address".to_string(), + pool_id: 1212, + code_id: 148, + instantiate_msg: to_json_vec(&msg).unwrap(), + }; + + let any_pool = cosmwasm_pool.to_any(); + let pool: Pool = any_pool.try_into().unwrap(); + + assert_eq!(cosmwasm_pool.pool_id, pool.get_pool_id()); + assert_eq!( + vec![ + "uosmo".to_string(), + "factory/osmo14eq94mckd6kp0pwnxx33ycpk762z7rum29epr3/teko02".to_string(), + ], + pool.get_pool_denoms() + ); + } + + #[test] + fn common_data_for_cosmwasm_pool_v3() { + let msg = InstantiateMsg { + pool_asset_denoms: vec![ + "ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858".to_string(), + "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4".to_string(), + ], + }; + let cosmwasm_pool = OsmoCosmWasmPool { + contract_address: "pool_address".to_string(), + pool_id: 1212, + code_id: 148, + instantiate_msg: to_json_vec(&msg).unwrap(), + }; + + let any_pool = cosmwasm_pool.to_any(); + let pool: Pool = any_pool.try_into().unwrap(); + + assert_eq!(cosmwasm_pool.pool_id, pool.get_pool_id()); + assert_eq!( + vec![ + "ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858".to_string(), + "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4".to_string() + ], + pool.get_pool_denoms() + ); + } + + #[test] + fn cosmwasm_pool_error_handled() { + let msg = InstantiateMsg { + pool_asset_denoms: vec![ + "ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858".to_string(), + "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4".to_string(), + ], + }; + let cosmwasm_pool = OsmoCosmWasmPool { + contract_address: "pool_address".to_string(), + pool_id: 1212, + code_id: 148, + instantiate_msg: msg.encode_to_vec(), + }; + + let any_pool = cosmwasm_pool.to_any(); + let pool: Result = any_pool.try_into(); + + assert_eq!( + pool.unwrap_err(), + StdError::parse_err("Pool", "Failed to parse CosmWasm pool instantiate message.",) + ); + } } diff --git a/packages/testing/src/cosmwasm_pool_querier.rs b/packages/testing/src/cosmwasm_pool_querier.rs new file mode 100644 index 000000000..a6d079eb9 --- /dev/null +++ b/packages/testing/src/cosmwasm_pool_querier.rs @@ -0,0 +1,26 @@ +use cosmwasm_std::{to_json_binary, Binary, ContractResult, QuerierResult}; +use osmosis_std::types::{ + cosmos::base::v1beta1::Coin, + osmosis::cosmwasmpool::v1beta1::{CalcOutAmtGivenInRequest, CalcOutAmtGivenInResponse}, +}; + +#[derive(Default)] +pub struct CosmWasmPoolQuerier {} + +impl CosmWasmPoolQuerier { + pub fn handle_query(&self, query: CalcOutAmtGivenInRequest) -> QuerierResult { + let res: ContractResult = { + let token_in = query.calc_out_amt_given_in.clone().unwrap().token_in.unwrap(); + let denom_out = query.calc_out_amt_given_in.unwrap().token_out_denom; + + to_json_binary(&CalcOutAmtGivenInResponse { + token_out: Some(Coin { + denom: denom_out, + amount: token_in.amount, + }), + }) + .into() + }; + Ok(res).into() + } +} diff --git a/packages/testing/src/lib.rs b/packages/testing/src/lib.rs index eceaed296..18fc1b2de 100644 --- a/packages/testing/src/lib.rs +++ b/packages/testing/src/lib.rs @@ -4,6 +4,7 @@ extern crate core; #[cfg(feature = "astroport")] pub mod astroport_swapper; +mod cosmwasm_pool_querier; /// cosmwasm_std::testing overrides and custom test helpers mod helpers; mod incentives_querier; diff --git a/packages/testing/src/mars_mock_querier.rs b/packages/testing/src/mars_mock_querier.rs index c4ff82e18..0226ccbf8 100644 --- a/packages/testing/src/mars_mock_querier.rs +++ b/packages/testing/src/mars_mock_querier.rs @@ -8,6 +8,7 @@ use ica_oracle::msg::RedemptionRateResponse; use mars_oracle_osmosis::DowntimeDetector; use mars_types::{address_provider, incentives, oracle, params::AssetParams, red_bank}; use osmosis_std::types::osmosis::{ + cosmwasmpool::v1beta1::CalcOutAmtGivenInRequest, downtimedetector::v1beta1::RecoveredSinceDowntimeOfLengthResponse, poolmanager::v1beta1::{PoolResponse, SpotPriceResponse}, twap::v1beta1::{ArithmeticTwapToNowResponse, GeometricTwapToNowResponse}, @@ -15,6 +16,7 @@ use osmosis_std::types::osmosis::{ use pyth_sdk_cw::{PriceFeedResponse, PriceIdentifier}; use crate::{ + cosmwasm_pool_querier::CosmWasmPoolQuerier, incentives_querier::IncentivesQuerier, mock_address_provider, oracle_querier::OracleQuerier, @@ -34,6 +36,7 @@ pub struct MarsMockQuerier { redbank_querier: RedBankQuerier, redemption_rate_querier: RedemptionRateQuerier, params_querier: ParamsQuerier, + cosmwasm_pool_queries: CosmWasmPoolQuerier, } impl Querier for MarsMockQuerier { @@ -63,6 +66,7 @@ impl MarsMockQuerier { redbank_querier: RedBankQuerier::default(), redemption_rate_querier: Default::default(), params_querier: ParamsQuerier::default(), + cosmwasm_pool_queries: CosmWasmPoolQuerier::default(), } } @@ -259,6 +263,11 @@ impl MarsMockQuerier { return self.params_querier.handle_query(params_query); } + // CosmWasm pool Queries + if let Ok(cw_pool_query) = from_json::(msg) { + return self.cosmwasm_pool_queries.handle_query(cw_pool_query); + } + panic!("[mock]: Unsupported wasm query: {msg:?}"); } diff --git a/packages/testing/src/osmosis_querier.rs b/packages/testing/src/osmosis_querier.rs index 8d9c35d4d..9995bfaff 100644 --- a/packages/testing/src/osmosis_querier.rs +++ b/packages/testing/src/osmosis_querier.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use cosmwasm_std::{to_json_binary, Binary, ContractResult, QuerierResult, SystemError}; use osmosis_std::types::osmosis::{ + cosmwasmpool::v1beta1::{ContractInfoByPoolIdRequest, ContractInfoByPoolIdResponse}, downtimedetector::v1beta1::{ RecoveredSinceDowntimeOfLengthRequest, RecoveredSinceDowntimeOfLengthResponse, }, @@ -73,6 +74,14 @@ impl OsmosisQuerier { } } + if path == "/osmosis.cosmwasmpool.v1beta1.Query/ContractInfoByPoolId" { + let parse_osmosis_query: Result = + Message::decode(data.as_slice()); + if let Ok(osmosis_query) = parse_osmosis_query { + return Ok(self.handle_cosmwasm_pool_contract_info_request(osmosis_query.pool_id)); + } + } + Err(()) } @@ -170,4 +179,13 @@ impl OsmosisQuerier { }; Ok(res).into() } + + fn handle_cosmwasm_pool_contract_info_request(&self, pool_id: u64) -> QuerierResult { + let res: ContractResult = to_json_binary(&ContractInfoByPoolIdResponse { + contract_address: format!("pool_id_{}", pool_id), + code_id: pool_id, + }) + .into(); + Ok(res).into() + } } diff --git a/packages/types/src/swapper.rs b/packages/types/src/swapper.rs index ce7eb95ad..9c80a6416 100644 --- a/packages/types/src/swapper.rs +++ b/packages/types/src/swapper.rs @@ -122,5 +122,5 @@ pub struct EstimateExactInSwapResponse { #[cw_serde] pub enum MigrateMsg { V1_0_0ToV2_0_0 {}, - V2_0_0ToV2_0_1 {}, + V2_0_1ToV2_0_2 {}, } diff --git a/schemas/mars-swapper-osmosis/mars-swapper-osmosis.json b/schemas/mars-swapper-osmosis/mars-swapper-osmosis.json index c7a66b7be..c18b36771 100644 --- a/schemas/mars-swapper-osmosis/mars-swapper-osmosis.json +++ b/schemas/mars-swapper-osmosis/mars-swapper-osmosis.json @@ -1,6 +1,6 @@ { "contract_name": "mars-swapper-osmosis", - "contract_version": "2.0.1", + "contract_version": "2.0.2", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#",