diff --git a/Cargo.lock b/Cargo.lock index e3f1dd012..bd4681f2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2322,7 +2322,7 @@ dependencies = [ [[package]] name = "mars-oracle-osmosis" -version = "2.0.0" +version = "2.0.1" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -2339,6 +2339,7 @@ dependencies = [ "pyth-sdk-cw", "schemars", "serde", + "test-case", ] [[package]] diff --git a/contracts/oracle/osmosis/Cargo.toml b/contracts/oracle/osmosis/Cargo.toml index ccfce87af..eb5d98448 100644 --- a/contracts/oracle/osmosis/Cargo.toml +++ b/contracts/oracle/osmosis/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "mars-oracle-osmosis" description = "A smart contract that provides prices denominated in `uosmo` for assets used in the protocol" -version = { workspace = true } +version = "2.0.1" authors = { workspace = true } edition = { workspace = true } license = { workspace = true } @@ -40,3 +40,4 @@ cosmwasm-schema = { workspace = true } mars-owner = { workspace = true } mars-testing = { workspace = true } mars-utils = { workspace = true } +test-case = { workspace = true } diff --git a/contracts/oracle/osmosis/src/contract.rs b/contracts/oracle/osmosis/src/contract.rs index c26eb54a3..f403d5a45 100644 --- a/contracts/oracle/osmosis/src/contract.rs +++ b/contracts/oracle/osmosis/src/contract.rs @@ -49,6 +49,9 @@ pub mod entry { #[entry_point] pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> ContractResult { - migrations::v2_0_0::migrate(deps, msg) + match msg { + MigrateMsg::V1_1_0ToV2_0_0(updates) => migrations::v2_0_0::migrate(deps, updates), + MigrateMsg::V2_0_0ToV2_0_1 {} => migrations::v2_0_1::migrate(deps), + } } } diff --git a/contracts/oracle/osmosis/src/lib.rs b/contracts/oracle/osmosis/src/lib.rs index 30b7db33e..84e5bc3cd 100644 --- a/contracts/oracle/osmosis/src/lib.rs +++ b/contracts/oracle/osmosis/src/lib.rs @@ -5,6 +5,6 @@ pub mod msg; mod price_source; pub use price_source::{ - DowntimeDetector, GeometricTwap, OsmosisPriceSourceChecked, OsmosisPriceSourceUnchecked, - RedemptionRate, + DowntimeDetector, OsmosisPriceSourceChecked, OsmosisPriceSourceUnchecked, RedemptionRate, Twap, + TwapKind, }; diff --git a/contracts/oracle/osmosis/src/migrations/mod.rs b/contracts/oracle/osmosis/src/migrations/mod.rs index 7592b6f12..24a4db4ab 100644 --- a/contracts/oracle/osmosis/src/migrations/mod.rs +++ b/contracts/oracle/osmosis/src/migrations/mod.rs @@ -1 +1,2 @@ pub mod v2_0_0; +pub mod v2_0_1; diff --git a/contracts/oracle/osmosis/src/migrations/v2_0_0.rs b/contracts/oracle/osmosis/src/migrations/v2_0_0.rs index 9f3a0125f..07af471d2 100644 --- a/contracts/oracle/osmosis/src/migrations/v2_0_0.rs +++ b/contracts/oracle/osmosis/src/migrations/v2_0_0.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{Decimal, DepsMut, Order, Response, StdResult}; use cw2::{assert_contract_version, set_contract_version}; use mars_oracle_base::ContractError; -use mars_types::oracle::MigrateMsg; +use mars_types::oracle::V2Updates; use osmosis_std::types::osmosis::downtimedetector::v1beta1::Downtime; use crate::{ @@ -69,7 +69,7 @@ pub mod v1_state { pub type OsmosisPriceSourceChecked = OsmosisPriceSource; } -pub fn migrate(deps: DepsMut, msg: MigrateMsg) -> Result { +pub fn migrate(deps: DepsMut, msg: V2Updates) -> Result { // make sure we're migrating the correct contract and from the correct version assert_contract_version(deps.storage, &format!("crates.io:{CONTRACT_NAME}"), FROM_VERSION)?; diff --git a/contracts/oracle/osmosis/src/migrations/v2_0_1.rs b/contracts/oracle/osmosis/src/migrations/v2_0_1.rs new file mode 100644 index 000000000..98055f243 --- /dev/null +++ b/contracts/oracle/osmosis/src/migrations/v2_0_1.rs @@ -0,0 +1,19 @@ +use cosmwasm_std::{DepsMut, Response}; +use cw2::{assert_contract_version, set_contract_version}; +use mars_oracle_base::ContractError; + +use crate::contract::{CONTRACT_NAME, CONTRACT_VERSION}; + +const FROM_VERSION: &str = "2.0.0"; + +pub fn migrate(deps: DepsMut) -> Result { + // make sure we're migrating the correct contract and from the correct version + assert_contract_version(deps.storage, &format!("crates.io:{CONTRACT_NAME}"), FROM_VERSION)?; + + set_contract_version(deps.storage, format!("crates.io:{CONTRACT_NAME}"), CONTRACT_VERSION)?; + + Ok(Response::new() + .add_attribute("action", "migrate") + .add_attribute("from_version", FROM_VERSION) + .add_attribute("to_version", CONTRACT_VERSION)) +} diff --git a/contracts/oracle/osmosis/src/price_source.rs b/contracts/oracle/osmosis/src/price_source.rs index f738d2705..884c1992f 100644 --- a/contracts/oracle/osmosis/src/price_source.rs +++ b/contracts/oracle/osmosis/src/price_source.rs @@ -1,7 +1,9 @@ use std::{cmp::min, fmt}; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Decimal, Decimal256, Deps, Empty, Env, Isqrt, Uint128, Uint256}; +use cosmwasm_std::{ + Addr, Decimal, Decimal256, Deps, Empty, Env, Isqrt, QuerierWrapper, StdResult, Uint128, Uint256, +}; use cw_storage_plus::Map; use ica_oracle::msg::RedemptionRateResponse; use mars_oracle_base::{ @@ -174,8 +176,8 @@ pub enum OsmosisPriceSource { /// stAsset/USD = stAsset/Asset * Asset/USD transitive_denom: String, - /// Params to query geometric TWAP price - geometric_twap: GeometricTwap, + /// Params to query TWAP price + twap: Twap, /// Params to query redemption rate redemption_rate: RedemptionRate, @@ -183,7 +185,7 @@ pub enum OsmosisPriceSource { } #[cw_serde] -pub struct GeometricTwap { +pub struct Twap { /// Pool id for stAsset/Asset pool pub pool_id: u64, @@ -193,6 +195,52 @@ pub struct GeometricTwap { /// Detect when the chain is recovering from downtime pub downtime_detector: Option, + + /// Kind of TWAP + pub kind: TwapKind, +} + +#[cw_serde] +pub enum TwapKind { + ArithmeticTwap {}, + GeometricTwap {}, +} + +impl fmt::Display for TwapKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + TwapKind::ArithmeticTwap {} => write!(f, "arithmetic_twap"), + TwapKind::GeometricTwap {} => write!(f, "geometric_twap"), + } + } +} + +impl Twap { + fn query_price( + &self, + querier: &QuerierWrapper, + current_time: u64, + base_denom: &str, + quote_denom: &str, + ) -> StdResult { + let start_time = current_time - self.window_size; + match self.kind { + TwapKind::ArithmeticTwap {} => query_arithmetic_twap_price( + querier, + self.pool_id, + base_denom, + quote_denom, + start_time, + ), + TwapKind::GeometricTwap {} => query_geometric_twap_price( + querier, + self.pool_id, + base_denom, + quote_denom, + start_time, + ), + } + } } #[cw_serde] @@ -257,20 +305,21 @@ impl fmt::Display for OsmosisPriceSourceChecked { } OsmosisPriceSource::Lsd { transitive_denom, - geometric_twap, + twap, redemption_rate, } => { - let GeometricTwap { + let Twap { pool_id, window_size, downtime_detector, - } = geometric_twap; + kind, + } = twap; let dd_fmt = DowntimeDetector::fmt(downtime_detector); let RedemptionRate { contract_addr, max_staleness, } = redemption_rate; - format!("lsd:{transitive_denom}:{pool_id}:{window_size}:{dd_fmt}:{contract_addr}:{max_staleness}") + format!("lsd:{transitive_denom}:{pool_id}:{window_size}:{dd_fmt}:{kind}:{contract_addr}:{max_staleness}") } }; write!(f, "{label}") @@ -381,21 +430,18 @@ impl PriceSourceUnchecked for OsmosisPriceSour } OsmosisPriceSourceUnchecked::Lsd { transitive_denom, - geometric_twap, + twap, redemption_rate, } => { validate_native_denom(transitive_denom)?; - let pool = query_pool(&deps.querier, geometric_twap.pool_id)?; + let pool = query_pool(&deps.querier, twap.pool_id)?; helpers::assert_osmosis_pool_assets(&pool, denom, transitive_denom)?; - helpers::assert_osmosis_twap( - geometric_twap.window_size, - &geometric_twap.downtime_detector, - )?; + helpers::assert_osmosis_twap(twap.window_size, &twap.downtime_detector)?; Ok(OsmosisPriceSourceChecked::Lsd { transitive_denom: transitive_denom.to_string(), - geometric_twap: geometric_twap.clone(), + twap: twap.clone(), redemption_rate: RedemptionRate { contract_addr: deps.api.addr_validate(&redemption_rate.contract_addr)?, max_staleness: redemption_rate.max_staleness, @@ -510,18 +556,18 @@ impl PriceSourceChecked for OsmosisPriceSourceChecked { )?), OsmosisPriceSourceChecked::Lsd { transitive_denom, - geometric_twap, + twap, redemption_rate, } => { - Self::chain_recovered(deps, &geometric_twap.downtime_detector)?; + Self::chain_recovered(deps, &twap.downtime_detector)?; Self::query_lsd_price( deps, env, denom, transitive_denom, - geometric_twap.clone(), - redemption_rate.clone(), + twap, + redemption_rate, config, price_sources, kind, @@ -656,28 +702,22 @@ impl OsmosisPriceSourceChecked { /// /// stAsset/USD = stAsset/Asset * Asset/USD /// where: - /// stAsset/Asset = min(stAsset/Asset Geometric TWAP, stAsset/Asset Redemption Rate) + /// stAsset/Asset = min(stAsset/Asset TWAP, stAsset/Asset Redemption Rate) #[allow(clippy::too_many_arguments)] fn query_lsd_price( deps: &Deps, env: &Env, denom: &str, transitive_denom: &str, - geometric_twap: GeometricTwap, - redemption_rate: RedemptionRate, + twap: &Twap, + redemption_rate: &RedemptionRate, config: &Config, price_sources: &Map<&str, OsmosisPriceSourceChecked>, kind: ActionKind, ) -> ContractResult { let current_time = env.block.time.seconds(); - let start_time = current_time - geometric_twap.window_size; - let staked_price = query_geometric_twap_price( - &deps.querier, - geometric_twap.pool_id, - denom, - transitive_denom, - start_time, - )?; + let staked_price = + twap.query_price(&deps.querier, current_time, denom, transitive_denom)?; // query redemption rate let rr = query_redemption_rate( @@ -687,7 +727,7 @@ impl OsmosisPriceSourceChecked { )?; // Check if the redemption rate is not too old - assert_rr_not_too_old(current_time, &rr, &redemption_rate)?; + assert_rr_not_too_old(current_time, &rr, redemption_rate)?; // min from geometric TWAP and exchange rate let min_price = min(staked_price, rr.redemption_rate); diff --git a/contracts/oracle/osmosis/tests/tests/test_migration_v2.rs b/contracts/oracle/osmosis/tests/tests/test_migration_v2.rs index 7b3ce4b59..76fca6088 100644 --- a/contracts/oracle/osmosis/tests/tests/test_migration_v2.rs +++ b/contracts/oracle/osmosis/tests/tests/test_migration_v2.rs @@ -9,7 +9,7 @@ use mars_oracle_osmosis::{ DowntimeDetector, OsmosisPriceSourceChecked, }; use mars_testing::mock_dependencies; -use mars_types::oracle::MigrateMsg; +use mars_types::oracle::{MigrateMsg, V2Updates}; use osmosis_std::types::osmosis::downtimedetector::v1beta1::Downtime; use pyth_sdk_cw::PriceIdentifier; @@ -21,13 +21,21 @@ fn wrong_contract_name() { let err = migrate( deps.as_mut(), mock_env(), - MigrateMsg { + MigrateMsg::V1_1_0ToV2_0_0(V2Updates { max_confidence: Decimal::percent(5), max_deviation: Decimal::percent(5), - }, + }), ) .unwrap_err(); + assert_eq!( + err, + ContractError::Version(VersionError::WrongContract { + expected: "crates.io:mars-oracle-osmosis".to_string(), + found: "contract_xyz".to_string() + }) + ); + let err = migrate(deps.as_mut(), mock_env(), MigrateMsg::V2_0_0ToV2_0_1 {}).unwrap_err(); assert_eq!( err, ContractError::Version(VersionError::WrongContract { @@ -46,13 +54,12 @@ fn wrong_contract_version() { let err = migrate( deps.as_mut(), mock_env(), - MigrateMsg { + MigrateMsg::V1_1_0ToV2_0_0(V2Updates { max_confidence: Decimal::percent(5), max_deviation: Decimal::percent(5), - }, + }), ) .unwrap_err(); - assert_eq!( err, ContractError::Version(VersionError::WrongVersion { @@ -60,10 +67,19 @@ fn wrong_contract_version() { found: "4.1.0".to_string() }) ); + + let err = migrate(deps.as_mut(), mock_env(), MigrateMsg::V2_0_0ToV2_0_1 {}).unwrap_err(); + assert_eq!( + err, + ContractError::Version(VersionError::WrongVersion { + expected: "2.0.0".to_string(), + found: "4.1.0".to_string() + }) + ); } #[test] -fn successful_migration() { +fn successful_migration_to_v2_0_0() { let mut deps = mock_dependencies(&[]); cw2::set_contract_version(deps.as_mut().storage, "crates.io:mars-oracle-osmosis", "1.1.0") .unwrap(); @@ -126,10 +142,10 @@ fn successful_migration() { let res = migrate( deps.as_mut(), mock_env(), - MigrateMsg { + MigrateMsg::V1_1_0ToV2_0_0(V2Updates { max_confidence, max_deviation, - }, + }), ) .unwrap(); @@ -138,12 +154,12 @@ fn successful_migration() { assert!(res.data.is_none()); assert_eq!( res.attributes, - vec![attr("action", "migrate"), attr("from_version", "1.1.0"), attr("to_version", "2.0.0")] + vec![attr("action", "migrate"), attr("from_version", "1.1.0"), attr("to_version", "2.0.1")] ); let new_contract_version = ContractVersion { contract: "crates.io:mars-oracle-osmosis".to_string(), - version: "2.0.0".to_string(), + version: "2.0.1".to_string(), }; assert_eq!(cw2::get_contract_version(deps.as_ref().storage).unwrap(), new_contract_version); @@ -201,3 +217,26 @@ fn successful_migration() { } ); } + +#[test] +fn successful_migration_to_v2_0_1() { + let mut deps = mock_dependencies(&[]); + cw2::set_contract_version(deps.as_mut().storage, "crates.io:mars-oracle-osmosis", "2.0.0") + .unwrap(); + + let res = migrate(deps.as_mut(), mock_env(), MigrateMsg::V2_0_0ToV2_0_1 {}).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")] + ); + + let new_contract_version = ContractVersion { + contract: "crates.io:mars-oracle-osmosis".to_string(), + version: "2.0.1".to_string(), + }; + assert_eq!(cw2::get_contract_version(deps.as_ref().storage).unwrap(), new_contract_version); +} diff --git a/contracts/oracle/osmosis/tests/tests/test_price_source_fmt.rs b/contracts/oracle/osmosis/tests/tests/test_price_source_fmt.rs index db42bf206..fccb5fab2 100644 --- a/contracts/oracle/osmosis/tests/tests/test_price_source_fmt.rs +++ b/contracts/oracle/osmosis/tests/tests/test_price_source_fmt.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{Addr, Decimal}; use mars_oracle_osmosis::{ - DowntimeDetector, GeometricTwap, OsmosisPriceSourceChecked, RedemptionRate, + DowntimeDetector, OsmosisPriceSourceChecked, RedemptionRate, Twap, TwapKind, }; use osmosis_std::types::osmosis::downtimedetector::v1beta1::Downtime; use pyth_sdk_cw::PriceIdentifier; @@ -123,10 +123,11 @@ fn display_pyth_price_source() { fn display_lsd_price_source() { let ps = OsmosisPriceSourceChecked::Lsd { transitive_denom: "transitive".to_string(), - geometric_twap: GeometricTwap { + twap: Twap { pool_id: 456, window_size: 380, downtime_detector: None, + kind: TwapKind::ArithmeticTwap {}, }, redemption_rate: RedemptionRate { contract_addr: Addr::unchecked( @@ -135,17 +136,35 @@ fn display_lsd_price_source() { max_staleness: 1234, }, }; - assert_eq!(ps.to_string(), "lsd:transitive:456:380:None:osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc:1234"); + assert_eq!(ps.to_string(), "lsd:transitive:456:380:None:arithmetic_twap:osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc:1234"); let ps = OsmosisPriceSourceChecked::Lsd { transitive_denom: "transitive".to_string(), - geometric_twap: GeometricTwap { + twap: Twap { + pool_id: 456, + window_size: 380, + downtime_detector: None, + kind: TwapKind::GeometricTwap {}, + }, + redemption_rate: RedemptionRate { + contract_addr: Addr::unchecked( + "osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc", + ), + max_staleness: 1234, + }, + }; + assert_eq!(ps.to_string(), "lsd:transitive:456:380:None:geometric_twap:osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc:1234"); + + let ps = OsmosisPriceSourceChecked::Lsd { + transitive_denom: "transitive".to_string(), + twap: Twap { pool_id: 456, window_size: 380, downtime_detector: Some(DowntimeDetector { downtime: Downtime::Duration30m, recovery: 552, }), + kind: TwapKind::GeometricTwap {}, }, redemption_rate: RedemptionRate { contract_addr: Addr::unchecked( @@ -154,5 +173,5 @@ fn display_lsd_price_source() { max_staleness: 1234, }, }; - assert_eq!(ps.to_string(), "lsd:transitive:456:380:Some(Duration30m:552):osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc:1234"); + assert_eq!(ps.to_string(), "lsd:transitive:456:380:Some(Duration30m:552):geometric_twap:osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc:1234"); } diff --git a/contracts/oracle/osmosis/tests/tests/test_query_price.rs b/contracts/oracle/osmosis/tests/tests/test_query_price.rs index e57b53f61..203a8dac1 100644 --- a/contracts/oracle/osmosis/tests/tests/test_query_price.rs +++ b/contracts/oracle/osmosis/tests/tests/test_query_price.rs @@ -9,7 +9,7 @@ use helpers::prepare_query_balancer_pool_response; use ica_oracle::msg::RedemptionRateResponse; use mars_oracle_base::{pyth::scale_pyth_price, ContractError}; use mars_oracle_osmosis::{ - contract::entry, DowntimeDetector, GeometricTwap, OsmosisPriceSourceUnchecked, RedemptionRate, + contract::entry, DowntimeDetector, OsmosisPriceSourceUnchecked, RedemptionRate, Twap, TwapKind, }; use mars_testing::{mock_env_at_block_time, MarsMockQuerier}; use mars_types::oracle::{PriceResponse, QueryMsg}; @@ -448,14 +448,14 @@ fn querying_lsd_price() { ); let publish_time = 1677157333u64; - let (pyth_price, ustatom_uatom_price) = - setup_pyth_and_geometric_twap_for_lsd(&mut deps, publish_time); + let (pyth_price, _arithmetic_price, ustatom_uatom_geometric_price) = + setup_pyth_and_twap_for_lsd(&mut deps, publish_time); // setup redemption rate: stAtom/Atom deps.querier.set_redemption_rate( "ustatom", RedemptionRateResponse { - redemption_rate: ustatom_uatom_price + Decimal::one(), // geometric TWAP < redemption rate + redemption_rate: ustatom_uatom_geometric_price + Decimal::one(), // geometric TWAP < redemption rate update_time: publish_time, }, ); @@ -466,10 +466,11 @@ fn querying_lsd_price() { "ustatom", OsmosisPriceSourceUnchecked::Lsd { transitive_denom: "uatom".to_string(), - geometric_twap: GeometricTwap { + twap: Twap { pool_id: 803, window_size: 86400, downtime_detector: None, + kind: TwapKind::GeometricTwap {}, }, redemption_rate: RedemptionRate { contract_addr: "dummy_addr".to_string(), @@ -487,11 +488,11 @@ fn querying_lsd_price() { ) .unwrap(); let res: PriceResponse = from_binary(&res).unwrap(); - let expected_price = ustatom_uatom_price * pyth_price; + let expected_price = ustatom_uatom_geometric_price * pyth_price; assert_eq!(res.price, expected_price); // setup redemption rate: stAtom/Atom - let ustatom_uatom_redemption_rate = ustatom_uatom_price - Decimal::one(); // geometric TWAP > redemption rate + let ustatom_uatom_redemption_rate = ustatom_uatom_geometric_price - Decimal::one(); // geometric TWAP > redemption rate deps.querier.set_redemption_rate( "ustatom", RedemptionRateResponse { @@ -506,10 +507,11 @@ fn querying_lsd_price() { "ustatom", OsmosisPriceSourceUnchecked::Lsd { transitive_denom: "uatom".to_string(), - geometric_twap: GeometricTwap { + twap: Twap { pool_id: 803, window_size: 86400, downtime_detector: None, + kind: TwapKind::GeometricTwap {}, }, redemption_rate: RedemptionRate { contract_addr: "dummy_addr".to_string(), @@ -531,10 +533,10 @@ fn querying_lsd_price() { assert_eq!(res.price, expected_price); } -fn setup_pyth_and_geometric_twap_for_lsd( +fn setup_pyth_and_twap_for_lsd( deps: &mut OwnedDeps, publish_time: u64, -) -> (Decimal, Decimal) { +) -> (Decimal, Decimal, Decimal) { // setup pyth price: Atom/Usd let price_id = PriceIdentifier::from_hex( "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", @@ -575,17 +577,29 @@ fn setup_pyth_and_geometric_twap_for_lsd( }, ); + // setup arithmetic TWAP: stAtom/Atom + let ustatom_uatom_arithmetic_price = Decimal::from_ratio(1050u128, 1000u128); + deps.querier.set_arithmetic_twap_price( + 803, + "ustatom", + "uatom", + ArithmeticTwapToNowResponse { + arithmetic_twap: ustatom_uatom_arithmetic_price.to_string(), + }, + ); + // setup geometric TWAP: stAtom/Atom - let ustatom_uatom_price = Decimal::from_ratio(1054u128, 1000u128); + let ustatom_uatom_geometric_price = Decimal::from_ratio(1054u128, 1000u128); deps.querier.set_geometric_twap_price( 803, "ustatom", "uatom", GeometricTwapToNowResponse { - geometric_twap: ustatom_uatom_price.to_string(), + geometric_twap: ustatom_uatom_geometric_price.to_string(), }, ); - (pyth_price, ustatom_uatom_price) + + (pyth_price, ustatom_uatom_arithmetic_price, ustatom_uatom_geometric_price) } #[test] @@ -619,10 +633,11 @@ fn querying_lsd_price_if_no_transitive_denom_price_source() { "ustatom", OsmosisPriceSourceUnchecked::Lsd { transitive_denom: "uatom".to_string(), - geometric_twap: GeometricTwap { + twap: Twap { pool_id: 803, window_size: 86400, downtime_detector: None, + kind: TwapKind::GeometricTwap {}, }, redemption_rate: RedemptionRate { contract_addr: "dummy_addr".to_string(), @@ -664,14 +679,14 @@ fn querying_lsd_price_if_redemption_rate_too_old() { let max_staleness = 21600u64; let publish_time = 1677157333u64; - let (_pyth_price, ustatom_uatom_price) = - setup_pyth_and_geometric_twap_for_lsd(&mut deps, publish_time); + let (_pyth_price, _arithmetic_price, ustatom_uatom_geometric_price) = + setup_pyth_and_twap_for_lsd(&mut deps, publish_time); // setup redemption rate: stAtom/Atom deps.querier.set_redemption_rate( "ustatom", RedemptionRateResponse { - redemption_rate: ustatom_uatom_price + Decimal::one(), // geometric TWAP < redemption rate + redemption_rate: ustatom_uatom_geometric_price + Decimal::one(), // geometric TWAP < redemption rate update_time: publish_time - max_staleness - 1, }, ); @@ -682,10 +697,11 @@ fn querying_lsd_price_if_redemption_rate_too_old() { "ustatom", OsmosisPriceSourceUnchecked::Lsd { transitive_denom: "uatom".to_string(), - geometric_twap: GeometricTwap { + twap: Twap { pool_id: 803, window_size: 86400, downtime_detector: None, + kind: TwapKind::GeometricTwap {}, }, redemption_rate: RedemptionRate { contract_addr: "dummy_addr".to_string(), @@ -712,7 +728,7 @@ fn querying_lsd_price_if_redemption_rate_too_old() { } #[test] -fn querying_lsd_price_with_downtime_detector() { +fn querying_lsd_price_with_arithmetic_twap_and_downtime_detector() { let mut deps = helpers::setup_test_with_pools(); // price source used to convert USD to base_denom @@ -725,14 +741,96 @@ fn querying_lsd_price_with_downtime_detector() { ); let publish_time = 1677157333u64; - let (pyth_price, ustatom_uatom_price) = - setup_pyth_and_geometric_twap_for_lsd(&mut deps, publish_time); + let (pyth_price, ustatom_uatom_arithmetic_price, _geometric_price) = + setup_pyth_and_twap_for_lsd(&mut deps, publish_time); // setup redemption rate: stAtom/Atom deps.querier.set_redemption_rate( "ustatom", RedemptionRateResponse { - redemption_rate: ustatom_uatom_price + Decimal::one(), // geometric TWAP < redemption rate + redemption_rate: ustatom_uatom_arithmetic_price + Decimal::one(), // geometric TWAP < redemption rate + update_time: publish_time, + }, + ); + + let dd = DowntimeDetector { + downtime: Downtime::Duration10m, + recovery: 360, + }; + + // query price if geometric TWAP < redemption rate + helpers::set_price_source( + deps.as_mut(), + "ustatom", + OsmosisPriceSourceUnchecked::Lsd { + transitive_denom: "uatom".to_string(), + twap: Twap { + pool_id: 803, + window_size: 86400, + downtime_detector: Some(dd.clone()), + kind: TwapKind::ArithmeticTwap {}, + }, + redemption_rate: RedemptionRate { + contract_addr: "dummy_addr".to_string(), + max_staleness: 21600, + }, + }, + ); + + deps.querier.set_downtime_detector(dd.clone(), false); + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "ustatom".to_string(), + kind: None, + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::InvalidPrice { + reason: "chain is recovering from downtime".to_string() + } + ); + + deps.querier.set_downtime_detector(dd, true); + let res = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "ustatom".to_string(), + kind: None, + }, + ) + .unwrap(); + let res: PriceResponse = from_binary(&res).unwrap(); + let expected_price = ustatom_uatom_arithmetic_price * pyth_price; + assert_eq!(res.price, expected_price); +} + +#[test] +fn querying_lsd_price_with_geometric_twap_and_downtime_detector() { + let mut deps = helpers::setup_test_with_pools(); + + // price source used to convert USD to base_denom + helpers::set_price_source( + deps.as_mut(), + "usd", + OsmosisPriceSourceUnchecked::Fixed { + price: Decimal::from_str("1000000").unwrap(), + }, + ); + + let publish_time = 1677157333u64; + let (pyth_price, _arithmetic_price, ustatom_uatom_geometric_price) = + setup_pyth_and_twap_for_lsd(&mut deps, publish_time); + + // setup redemption rate: stAtom/Atom + deps.querier.set_redemption_rate( + "ustatom", + RedemptionRateResponse { + redemption_rate: ustatom_uatom_geometric_price + Decimal::one(), // geometric TWAP < redemption rate update_time: publish_time, }, ); @@ -748,10 +846,11 @@ fn querying_lsd_price_with_downtime_detector() { "ustatom", OsmosisPriceSourceUnchecked::Lsd { transitive_denom: "uatom".to_string(), - geometric_twap: GeometricTwap { + twap: Twap { pool_id: 803, window_size: 86400, downtime_detector: Some(dd.clone()), + kind: TwapKind::GeometricTwap {}, }, redemption_rate: RedemptionRate { contract_addr: "dummy_addr".to_string(), @@ -788,7 +887,7 @@ fn querying_lsd_price_with_downtime_detector() { ) .unwrap(); let res: PriceResponse = from_binary(&res).unwrap(); - let expected_price = ustatom_uatom_price * pyth_price; + let expected_price = ustatom_uatom_geometric_price * pyth_price; assert_eq!(res.price, expected_price); } 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 86e084aa1..22cf6b413 100644 --- a/contracts/oracle/osmosis/tests/tests/test_set_price_source.rs +++ b/contracts/oracle/osmosis/tests/tests/test_set_price_source.rs @@ -5,8 +5,8 @@ use mars_oracle_base::ContractError; use mars_oracle_osmosis::{ contract::entry::execute, msg::{ExecuteMsg, PriceSourceResponse}, - DowntimeDetector, GeometricTwap, OsmosisPriceSourceChecked, OsmosisPriceSourceUnchecked, - RedemptionRate, + DowntimeDetector, OsmosisPriceSourceChecked, OsmosisPriceSourceUnchecked, RedemptionRate, Twap, + TwapKind, }; use mars_owner::OwnerError::NotOwner; use mars_testing::mock_info; @@ -14,6 +14,7 @@ use mars_types::oracle::QueryMsg; use mars_utils::error::ValidationError; use osmosis_std::types::osmosis::downtimedetector::v1beta1::Downtime; use pyth_sdk_cw::PriceIdentifier; +use test_case::test_case; use super::helpers; @@ -793,10 +794,11 @@ fn setting_price_source_lsd_with_invalid_params() { denom: denom.to_string(), price_source: OsmosisPriceSourceUnchecked::Lsd { transitive_denom: transitive_denom.to_string(), - geometric_twap: GeometricTwap { + twap: Twap { pool_id, window_size, downtime_detector, + kind: TwapKind::GeometricTwap {}, }, redemption_rate: RedemptionRate { contract_addr: "dummy_addr".to_string(), @@ -897,78 +899,56 @@ fn setting_price_source_lsd_with_invalid_params() { ); } -#[test] -fn setting_price_source_lsd_successfully() { +#[test_case( + TwapKind::ArithmeticTwap {}, + Some(DowntimeDetector { + downtime: Downtime::Duration30m, + recovery: 360, + }); + "set LSD price source with arithmetic TWAP and downtime detector" +)] +#[test_case( + TwapKind::ArithmeticTwap {}, + None; + "set LSD price source with arithmetic TWAP and without downtime detector" +)] +#[test_case( + TwapKind::GeometricTwap {}, + Some(DowntimeDetector { + downtime: Downtime::Duration30m, + recovery: 360, + }); + "set LSD price source with geometric TWAP and downtime detector" +)] +#[test_case( + TwapKind::GeometricTwap {}, + None; + "set LSD price source with geometric TWAP and without downtime detector" +)] +fn asserting_lsd_price_source(twap_kind: TwapKind, downtime_detector: Option) { let mut deps = helpers::setup_test_with_pools(); // properly set twap price source - let res = execute( - deps.as_mut(), - mock_env(), - mock_info("owner"), - ExecuteMsg::SetPriceSource { - denom: "ustatom".to_string(), - price_source: OsmosisPriceSourceUnchecked::Lsd { - transitive_denom: "uatom".to_string(), - geometric_twap: GeometricTwap { - pool_id: 803, - window_size: 86400, - downtime_detector: None, - }, - redemption_rate: RedemptionRate { - contract_addr: "dummy_addr".to_string(), - max_staleness: 100, - }, - }, + let unchecked_lsd_ps = OsmosisPriceSourceUnchecked::Lsd { + transitive_denom: "uatom".to_string(), + twap: Twap { + pool_id: 803, + window_size: 86400, + downtime_detector, + kind: twap_kind, }, - ) - .unwrap(); - assert_eq!(res.messages.len(), 0); - - let res: PriceSourceResponse = helpers::query( - deps.as_ref(), - QueryMsg::PriceSource { - denom: "ustatom".to_string(), + redemption_rate: RedemptionRate { + contract_addr: "dummy_addr".to_string(), + max_staleness: 100, }, - ); - assert_eq!( - res.price_source, - OsmosisPriceSourceChecked::Lsd { - transitive_denom: "uatom".to_string(), - geometric_twap: GeometricTwap { - pool_id: 803, - window_size: 86400, - downtime_detector: None, - }, - redemption_rate: RedemptionRate { - contract_addr: Addr::unchecked("dummy_addr"), - max_staleness: 100 - } - } - ); - - // properly set twap price source with downtime detector + }; let res = execute( deps.as_mut(), mock_env(), mock_info("owner"), ExecuteMsg::SetPriceSource { denom: "ustatom".to_string(), - price_source: OsmosisPriceSourceUnchecked::Lsd { - transitive_denom: "uatom".to_string(), - geometric_twap: GeometricTwap { - pool_id: 803, - window_size: 86400, - downtime_detector: Some(DowntimeDetector { - downtime: Downtime::Duration30m, - recovery: 360u64, - }), - }, - redemption_rate: RedemptionRate { - contract_addr: "dummy_addr".to_string(), - max_staleness: 100, - }, - }, + price_source: unchecked_lsd_ps.clone(), }, ) .unwrap(); @@ -980,24 +960,28 @@ fn setting_price_source_lsd_successfully() { denom: "ustatom".to_string(), }, ); - assert_eq!( - res.price_source, + let checked_lsd_ps = unchecked_to_checked_lsd(unchecked_lsd_ps); + assert_eq!(res.price_source, checked_lsd_ps); +} + +fn unchecked_to_checked_lsd(ps: OsmosisPriceSourceUnchecked) -> OsmosisPriceSourceChecked { + if let OsmosisPriceSourceUnchecked::Lsd { + transitive_denom, + twap, + redemption_rate, + } = ps + { OsmosisPriceSourceChecked::Lsd { - transitive_denom: "uatom".to_string(), - geometric_twap: GeometricTwap { - pool_id: 803, - window_size: 86400, - downtime_detector: Some(DowntimeDetector { - downtime: Downtime::Duration30m, - recovery: 360u64, - }) - }, + transitive_denom, + twap, redemption_rate: RedemptionRate { - contract_addr: Addr::unchecked("dummy_addr"), - max_staleness: 100 - } + contract_addr: Addr::unchecked(redemption_rate.contract_addr), + max_staleness: redemption_rate.max_staleness, + }, } - ); + } else { + panic!("invalid price source type") + } } #[test] diff --git a/integration-tests/tests/test_oracles.rs b/integration-tests/tests/test_oracles.rs index 8a10e1b9e..ee05a4716 100644 --- a/integration-tests/tests/test_oracles.rs +++ b/integration-tests/tests/test_oracles.rs @@ -4,8 +4,8 @@ use cosmwasm_std::{coin, to_binary, Coin, Decimal, Empty, Isqrt, Uint128}; use helpers::osmosis::instantiate_stride_contract; use mars_oracle_base::ContractError; use mars_oracle_osmosis::{ - msg::PriceSourceResponse, DowntimeDetector, GeometricTwap, OsmosisPriceSourceChecked, - OsmosisPriceSourceUnchecked, RedemptionRate, + msg::PriceSourceResponse, DowntimeDetector, OsmosisPriceSourceChecked, + OsmosisPriceSourceUnchecked, RedemptionRate, Twap, TwapKind, }; use mars_types::{ address_provider::{ @@ -1039,10 +1039,11 @@ fn query_lsd_price() { denom: ibc_stuosmo.to_string(), price_source: OsmosisPriceSourceUnchecked::Lsd { transitive_denom: "uosmo".to_string(), - geometric_twap: GeometricTwap { + twap: Twap { pool_id, window_size: 10, downtime_detector: None, + kind: TwapKind::ArithmeticTwap {}, }, redemption_rate: RedemptionRate { contract_addr: stride_addr.clone(), diff --git a/packages/types/src/oracle/msg.rs b/packages/types/src/oracle/msg.rs index efdfa789b..d68510109 100644 --- a/packages/types/src/oracle/msg.rs +++ b/packages/types/src/oracle/msg.rs @@ -113,7 +113,13 @@ pub struct PriceResponse { } #[cw_serde] -pub struct MigrateMsg { +pub enum MigrateMsg { + V1_1_0ToV2_0_0(V2Updates), + V2_0_0ToV2_0_1 {}, +} + +#[cw_serde] +pub struct V2Updates { /// The maximum confidence deviation allowed for an oracle price. /// The confidence is measured as the percent of the confidence interval /// value provided by the oracle as compared to the weighted average value diff --git a/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json b/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json index b2b773bd2..7ee6ef9cf 100644 --- a/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json +++ b/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json @@ -1,6 +1,6 @@ { "contract_name": "mars-oracle-osmosis", - "contract_version": "2.0.0", + "contract_version": "2.0.1", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -203,39 +203,6 @@ "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" }, - "GeometricTwap": { - "type": "object", - "required": [ - "pool_id", - "window_size" - ], - "properties": { - "downtime_detector": { - "description": "Detect when the chain is recovering from downtime", - "anyOf": [ - { - "$ref": "#/definitions/DowntimeDetector" - }, - { - "type": "null" - } - ] - }, - "pool_id": { - "description": "Pool id for stAsset/Asset pool", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "window_size": { - "description": "Window size in seconds representing the entire window for which 'geometric' price is calculated. Value should be <= 172800 sec (48 hours).", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, "Identifier": { "type": "string" }, @@ -516,19 +483,11 @@ "lsd": { "type": "object", "required": [ - "geometric_twap", "redemption_rate", - "transitive_denom" + "transitive_denom", + "twap" ], "properties": { - "geometric_twap": { - "description": "Params to query geometric TWAP price", - "allOf": [ - { - "$ref": "#/definitions/GeometricTwap" - } - ] - }, "redemption_rate": { "description": "Params to query redemption rate", "allOf": [ @@ -540,6 +499,14 @@ "transitive_denom": { "description": "Transitive denom for which we query price in USD. It refers to 'Asset' in the equation: stAsset/USD = stAsset/Asset * Asset/USD", "type": "string" + }, + "twap": { + "description": "Params to query TWAP price", + "allOf": [ + { + "$ref": "#/definitions/Twap" + } + ] } }, "additionalProperties": false @@ -644,6 +611,78 @@ } }, "additionalProperties": false + }, + "Twap": { + "type": "object", + "required": [ + "kind", + "pool_id", + "window_size" + ], + "properties": { + "downtime_detector": { + "description": "Detect when the chain is recovering from downtime", + "anyOf": [ + { + "$ref": "#/definitions/DowntimeDetector" + }, + { + "type": "null" + } + ] + }, + "kind": { + "description": "Kind of TWAP", + "allOf": [ + { + "$ref": "#/definitions/TwapKind" + } + ] + }, + "pool_id": { + "description": "Pool id for stAsset/Asset pool", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "window_size": { + "description": "Window size in seconds representing the entire window for which 'geometric' price is calculated. Value should be <= 172800 sec (48 hours).", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "TwapKind": { + "oneOf": [ + { + "type": "object", + "required": [ + "arithmetic_twap" + ], + "properties": { + "arithmetic_twap": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "geometric_twap" + ], + "properties": { + "geometric_twap": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] } } }, diff --git a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts index 48e2533cb..1cb05610c 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts @@ -15,10 +15,11 @@ import { Decimal, Downtime, Identifier, + TwapKind, OwnerUpdate, DowntimeDetector, - GeometricTwap, RedemptionRateForString, + Twap, QueryMsg, ActionKind, ConfigResponse, diff --git a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.react-query.ts b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.react-query.ts index a1af98a4c..10b6cc763 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.react-query.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.react-query.ts @@ -16,10 +16,11 @@ import { Decimal, Downtime, Identifier, + TwapKind, OwnerUpdate, DowntimeDetector, - GeometricTwap, RedemptionRateForString, + Twap, QueryMsg, ActionKind, ConfigResponse, diff --git a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts index fa76e70a7..14650c339 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts @@ -86,9 +86,9 @@ export type OsmosisPriceSourceForString = } | { lsd: { - geometric_twap: GeometricTwap redemption_rate: RedemptionRateForString transitive_denom: string + twap: Twap } } export type Decimal = string @@ -119,6 +119,13 @@ export type Downtime = | 'Duration36h' | 'Duration48h' export type Identifier = string +export type TwapKind = + | { + arithmetic_twap: {} + } + | { + geometric_twap: {} + } export type OwnerUpdate = | { propose_new_owner: { @@ -138,15 +145,16 @@ export interface DowntimeDetector { downtime: Downtime recovery: number } -export interface GeometricTwap { - downtime_detector?: DowntimeDetector | null - pool_id: number - window_size: number -} export interface RedemptionRateForString { contract_addr: string max_staleness: number } +export interface Twap { + downtime_detector?: DowntimeDetector | null + kind: TwapKind + pool_id: number + window_size: number +} export type QueryMsg = | { config: {}