From 4db3253ea0174b63f2d29387c5e473e7efbe2844 Mon Sep 17 00:00:00 2001 From: piobab Date: Fri, 20 Oct 2023 11:43:35 +0200 Subject: [PATCH] MP-3498. Configurable arithmetic and geometric twap for LSD (#347) * LSD price source with configured Arithmetic or Geometric TWAP. * Update schema. * Add migrate for oracle. * Simplify tests. * Update schema after changing migration. --- Cargo.lock | 3 +- contracts/oracle/osmosis/Cargo.toml | 3 +- contracts/oracle/osmosis/src/contract.rs | 5 +- contracts/oracle/osmosis/src/lib.rs | 4 +- .../oracle/osmosis/src/migrations/mod.rs | 1 + .../oracle/osmosis/src/migrations/v2_0_0.rs | 4 +- .../oracle/osmosis/src/migrations/v2_0_1.rs | 19 +++ contracts/oracle/osmosis/src/price_source.rs | 102 ++++++++---- .../osmosis/tests/tests/test_migration_v2.rs | 61 ++++++-- .../tests/tests/test_price_source_fmt.rs | 29 +++- .../osmosis/tests/tests/test_query_price.rs | 147 +++++++++++++++--- .../tests/tests/test_set_price_source.rs | 142 ++++++++--------- integration-tests/tests/test_oracles.rs | 7 +- packages/types/src/oracle/msg.rs | 8 +- .../mars-oracle-osmosis.json | 127 +++++++++------ .../MarsOracleOsmosis.client.ts | 3 +- .../MarsOracleOsmosis.react-query.ts | 3 +- .../MarsOracleOsmosis.types.ts | 20 ++- 18 files changed, 475 insertions(+), 213 deletions(-) create mode 100644 contracts/oracle/osmosis/src/migrations/v2_0_1.rs diff --git a/Cargo.lock b/Cargo.lock index e3f1dd01..bd4681f2 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 ccfce87a..eb5d9844 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 c26eb54a..f403d5a4 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 30b7db33..84e5bc3c 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 7592b6f1..24a4db4a 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 9f3a0125..07af471d 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 00000000..98055f24 --- /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 f738d270..884c1992 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 7b3ce4b5..76fca608 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 db42bf20..fccb5fab 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 e57b53f6..203a8dac 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 86e084aa..22cf6b41 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 8a10e1b9..ee05a471 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 efdfa789..d6851010 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 b2b773bd..7ee6ef9c 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 48e2533c..1cb05610 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 a1af98a4..10b6cc76 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 fa76e70a..14650c33 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: {}