From 15e1994a19b7d853adc2eb2e4d832bb22ce68173 Mon Sep 17 00:00:00 2001 From: piobab Date: Fri, 3 Mar 2023 15:58:35 +0100 Subject: [PATCH 01/11] Pyth (#183) * Pyth integration. * Add mocks. * Add validation. * Tests. * Add update_config. * Update schema. * Apply review comments. * Update clippy. --- Cargo.lock | 169 +++++-- Cargo.toml | 1 + contracts/oracle/base/src/contract.rs | 66 ++- contracts/oracle/base/src/error.rs | 11 +- contracts/oracle/base/src/traits.rs | 2 + contracts/oracle/osmosis/Cargo.toml | 1 + contracts/oracle/osmosis/src/helpers.rs | 20 + contracts/oracle/osmosis/src/price_source.rs | 166 +++++++ contracts/oracle/osmosis/tests/helpers.rs | 37 +- contracts/oracle/osmosis/tests/test_admin.rs | 71 ++- .../oracle/osmosis/tests/test_query_price.rs | 436 +++++++++++++++++- .../osmosis/tests/test_remove_price_source.rs | 4 +- .../osmosis/tests/test_set_price_source.rs | 112 ++++- .../oracle/osmosis/tests/test_update_owner.rs | 6 +- integration-tests/tests/test_oracles.rs | 14 + packages/testing/Cargo.toml | 1 + packages/testing/src/integration/mock_env.rs | 10 + packages/testing/src/lib.rs | 1 + packages/testing/src/mars_mock_querier.rs | 13 + packages/testing/src/pyth_querier.rs | 31 ++ packages/types/src/oracle.rs | 18 +- .../mars-oracle-osmosis.json | 95 +++- .../MarsOracleOsmosis.client.ts | 40 ++ .../MarsOracleOsmosis.react-query.ts | 25 + .../MarsOracleOsmosis.types.ts | 18 + 25 files changed, 1282 insertions(+), 86 deletions(-) create mode 100644 packages/testing/src/pyth_querier.rs diff --git a/Cargo.lock b/Cargo.lock index 2505cba5f..c0d4a6cfb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,9 +15,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" dependencies = [ "memchr", ] @@ -151,6 +151,51 @@ dependencies = [ "generic-array", ] +[[package]] +name = "borsh" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" +dependencies = [ + "borsh-derive", + "hashbrown 0.11.2", +] + +[[package]] +name = "borsh-derive" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" +dependencies = [ + "borsh-derive-internal", + "borsh-schema-derive-internal", + "proc-macro-crate", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bs58" version = "0.4.0" @@ -300,9 +345,9 @@ dependencies = [ [[package]] name = "cosmwasm-crypto" -version = "1.2.5" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75836a10cb9654c54e77ee56da94d592923092a10b369cdb0dbd56acefc16340" +checksum = "41c0e41be7e6c7d7ab3c61cdc32fcfaa14f948491a401cbc1c74bb33b6f4b851" dependencies = [ "digest 0.10.7", "ed25519-zebra", @@ -313,18 +358,18 @@ dependencies = [ [[package]] name = "cosmwasm-derive" -version = "1.2.5" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c9f7f0e51bfc7295f7b2664fe8513c966428642aa765dad8a74acdab5e0c773" +checksum = "3a7ee2798c92c00dd17bebb4210f81d5f647e5e92d847959b7977e0fd29a3500" dependencies = [ "syn 1.0.109", ] [[package]] name = "cosmwasm-schema" -version = "1.2.5" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f00b363610218eea83f24bbab09e1a7c3920b79f068334fdfcc62f6129ef9fc" +checksum = "407aca6f1671a08b60db8167f03bb7cb6b2378f0ddd9a030367b66ba33c2fd41" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -335,9 +380,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.2.5" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae38f909b2822d32b275c9e2db9728497aa33ffe67dd463bc67c6a3b7092785c" +checksum = "e6d1e00b8fd27ff923c10303023626358e23a6f9079f8ebec23a8b4b0bfcd4b3" dependencies = [ "proc-macro2", "quote", @@ -346,9 +391,9 @@ dependencies = [ [[package]] name = "cosmwasm-std" -version = "1.2.5" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a49b85345e811c8e80ec55d0d091e4fcb4f00f97ab058f9be5f614c444a730cb" +checksum = "92d5fdfd112b070055f068fad079d490117c8e905a588b92a5a7c9276d029930" dependencies = [ "base64", "cosmwasm-crypto", @@ -425,9 +470,9 @@ dependencies = [ [[package]] name = "cw-multi-test" -version = "0.16.4" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a18afd2e201221c6d72a57f0886ef2a22151bbc9e6db7af276fde8a91081042" +checksum = "127c7bb95853b8e828bdab97065c81cb5ddc20f7339180b61b2300565aaa99d1" dependencies = [ "anyhow", "cosmwasm-std", @@ -581,7 +626,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" dependencies = [ "curve25519-dalek", - "hashbrown", + "hashbrown 0.12.3", "hex", "rand_core 0.6.4", "serde", @@ -666,9 +711,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] @@ -780,9 +825,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "js-sys", @@ -827,6 +872,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -884,6 +938,9 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hmac" @@ -997,9 +1054,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1018,7 +1075,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", ] [[package]] @@ -1081,9 +1138,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.144" +version = "0.2.146" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" [[package]] name = "libloading" @@ -1192,6 +1249,7 @@ dependencies = [ "mars-testing", "mars-utils", "osmosis-std 0.14.0", + "pyth-sdk-cw", "schemars", "serde", "thiserror", @@ -1298,6 +1356,7 @@ dependencies = [ "mars-rewards-collector-osmosis", "osmosis-std 0.14.0", "prost 0.11.9", + "pyth-sdk-cw", "schemars", "serde", "thiserror", @@ -1391,9 +1450,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.2" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "opaque-debug" @@ -1537,9 +1596,9 @@ checksum = "c719dcf55f09a3a7e764c6649ab594c18a177e3599c467983cdf644bfc0a4088" [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pin-project" @@ -1583,11 +1642,20 @@ dependencies = [ "spki", ] +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + [[package]] name = "proc-macro2" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" +checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" dependencies = [ "unicode-ident", ] @@ -1647,6 +1715,31 @@ dependencies = [ "prost 0.11.9", ] +[[package]] +name = "pyth-sdk" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bf2540203ca3c7a5712fdb8b5897534b7f6a0b6e7b0923ff00466c5f9efcb3" +dependencies = [ + "borsh", + "borsh-derive", + "hex", + "schemars", + "serde", +] + +[[package]] +name = "pyth-sdk-cw" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c04e9f2961bce1ef13b09afcdb5aee7d4ddde83669e5f9d2824ba422cb00de48" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "pyth-sdk", + "thiserror", +] + [[package]] name = "quote" version = "1.0.28" @@ -1673,9 +1766,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.3" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ "aho-corasick", "memchr", @@ -1868,9 +1961,9 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.163" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" dependencies = [ "serde_derive", ] @@ -1904,9 +1997,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.163" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" dependencies = [ "proc-macro2", "quote", @@ -2410,9 +2503,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" dependencies = [ "form_urlencoded", "idna", diff --git a/Cargo.toml b/Cargo.toml index 03c62e688..574793705 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ mars-owner = { version = "1.2.0", features = ["emergency-owner"] } osmosis-std = "0.14.0" osmosis-test-tube = "15.1.0" prost = { version = "0.11.5", default-features = false, features = ["prost-derive"] } +pyth-sdk-cw = "1.0.0" schemars = "0.8.11" serde = { version = "1.0.152", default-features = false, features = ["derive"] } thiserror = "1.0.38" diff --git a/contracts/oracle/base/src/contract.rs b/contracts/oracle/base/src/contract.rs index 04427c1cc..d504f33eb 100644 --- a/contracts/oracle/base/src/contract.rs +++ b/contracts/oracle/base/src/contract.rs @@ -8,9 +8,9 @@ use cw_storage_plus::{Bound, Item, Map}; use mars_owner::{Owner, OwnerInit::SetInitialOwner, OwnerUpdate}; use mars_red_bank_types::oracle::{ Config, ConfigResponse, ExecuteMsg, InstantiateMsg, PriceResponse, PriceSourceResponse, - QueryMsg, + PythConfig, QueryMsg, }; -use mars_utils::helpers::validate_native_denom; +use mars_utils::helpers::{option_string_to_addr, validate_native_denom}; use crate::{error::ContractResult, PriceSource}; @@ -26,6 +26,8 @@ where pub owner: Owner<'a>, /// The contract's config pub config: Item<'a, Config>, + /// Pyth config + pub pyth_config: Item<'a, PythConfig>, /// The price source of each coin denom pub price_sources: Map<'a, &'a str, P>, /// Phantom data holds the custom query type @@ -41,6 +43,7 @@ where Self { owner: Owner::new("owner"), config: Item::new("config"), + pyth_config: Item::new("pyth_config"), price_sources: Map::new("price_sources"), custom_query: PhantomData, } @@ -70,6 +73,13 @@ where }, )?; + self.pyth_config.save( + deps.storage, + &PythConfig { + pyth_contract_addr: deps.api.addr_validate(&msg.pyth_contract_addr)?, + }, + )?; + Ok(Response::default()) } @@ -88,6 +98,10 @@ where ExecuteMsg::RemovePriceSource { denom, } => self.remove_price_source(deps, info.sender, denom), + ExecuteMsg::UpdateConfig { + base_denom, + pyth_contract_addr, + } => self.update_config(deps, info.sender, base_denom, pyth_contract_addr), } } @@ -157,13 +171,49 @@ where .add_attribute("denom", denom)) } + fn update_config( + &self, + deps: DepsMut, + sender_addr: Addr, + base_denom: Option, + pyth_contract_addr: Option, + ) -> ContractResult { + self.owner.assert_owner(deps.storage, &sender_addr)?; + + if let Some(bd) = &base_denom { + validate_native_denom(bd)?; + }; + + let mut config = self.config.load(deps.storage)?; + let prev_base_denom = config.base_denom.clone(); + config.base_denom = base_denom.unwrap_or(config.base_denom); + self.config.save(deps.storage, &config)?; + + let mut pyth_cfg = self.pyth_config.load(deps.storage)?; + let prev_pyth_contract_addr = pyth_cfg.pyth_contract_addr.clone(); + pyth_cfg.pyth_contract_addr = + option_string_to_addr(deps.api, pyth_contract_addr, pyth_cfg.pyth_contract_addr)?; + self.pyth_config.save(deps.storage, &pyth_cfg)?; + + let response = Response::new() + .add_attribute("action", "update_config") + .add_attribute("prev_base_denom", prev_base_denom) + .add_attribute("base_denom", config.base_denom) + .add_attribute("prev_pyth_contract_addr", prev_pyth_contract_addr) + .add_attribute("pyth_contract_addr", pyth_cfg.pyth_contract_addr); + + Ok(response) + } + fn query_config(&self, deps: Deps) -> StdResult { let owner_state = self.owner.query(deps.storage)?; let cfg = self.config.load(deps.storage)?; + let pyth_cfg = self.pyth_config.load(deps.storage)?; Ok(ConfigResponse { owner: owner_state.owner, proposed_new_owner: owner_state.proposed, base_denom: cfg.base_denom, + pyth_contract_addr: pyth_cfg.pyth_contract_addr.to_string(), }) } @@ -202,6 +252,7 @@ where fn query_price(&self, deps: Deps, env: Env, denom: String) -> ContractResult { let cfg = self.config.load(deps.storage)?; + let pyth_cfg = self.pyth_config.load(deps.storage)?; let price_source = self.price_sources.load(deps.storage, &denom)?; Ok(PriceResponse { price: price_source.query_price( @@ -210,6 +261,7 @@ where &denom, &cfg.base_denom, &self.price_sources, + &pyth_cfg, )?, denom, }) @@ -223,6 +275,7 @@ where limit: Option, ) -> ContractResult> { let cfg = self.config.load(deps.storage)?; + let pyth_cfg = self.pyth_config.load(deps.storage)?; let start = start_after.map(|denom| Bound::ExclusiveRaw(denom.into_bytes())); let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; @@ -233,7 +286,14 @@ where .map(|item| { let (k, v) = item?; Ok(PriceResponse { - price: v.query_price(&deps, &env, &k, &cfg.base_denom, &self.price_sources)?, + price: v.query_price( + &deps, + &env, + &k, + &cfg.base_denom, + &self.price_sources, + &pyth_cfg, + )?, denom: k, }) }) diff --git a/contracts/oracle/base/src/error.rs b/contracts/oracle/base/src/error.rs index 6fcd449df..7a1ee3898 100644 --- a/contracts/oracle/base/src/error.rs +++ b/contracts/oracle/base/src/error.rs @@ -1,4 +1,7 @@ -use cosmwasm_std::{ConversionOverflowError, OverflowError, StdError}; +use cosmwasm_std::{ + CheckedFromRatioError, CheckedMultiplyRatioError, ConversionOverflowError, OverflowError, + StdError, +}; use mars_owner::OwnerError; use mars_red_bank_types::error::MarsError; use mars_utils::error::ValidationError; @@ -27,6 +30,12 @@ pub enum ContractError { #[error("{0}")] Overflow(#[from] OverflowError), + #[error("{0}")] + CheckedMultiplyRatio(#[from] CheckedMultiplyRatioError), + + #[error("{0}")] + CheckedFromRatio(#[from] CheckedFromRatioError), + #[error("Invalid price source: {reason}")] InvalidPriceSource { reason: String, diff --git a/contracts/oracle/base/src/traits.rs b/contracts/oracle/base/src/traits.rs index 05841e8fd..3e52a427c 100644 --- a/contracts/oracle/base/src/traits.rs +++ b/contracts/oracle/base/src/traits.rs @@ -2,6 +2,7 @@ use std::fmt::{Debug, Display}; use cosmwasm_std::{CustomQuery, Decimal, Deps, Env, QuerierWrapper}; use cw_storage_plus::Map; +use mars_red_bank_types::oracle::PythConfig; use schemars::JsonSchema; use serde::{de::DeserializeOwned, Serialize}; @@ -41,5 +42,6 @@ where denom: &str, base_denom: &str, price_sources: &Map<&str, Self>, + pyth_config: &PythConfig, ) -> ContractResult; } diff --git a/contracts/oracle/osmosis/Cargo.toml b/contracts/oracle/osmosis/Cargo.toml index f876ce167..6950a9118 100644 --- a/contracts/oracle/osmosis/Cargo.toml +++ b/contracts/oracle/osmosis/Cargo.toml @@ -26,6 +26,7 @@ mars-oracle-base = { workspace = true } mars-osmosis = { workspace = true } mars-red-bank-types = { workspace = true } osmosis-std = { workspace = true } +pyth-sdk-cw = { workspace = true } schemars = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } diff --git a/contracts/oracle/osmosis/src/helpers.rs b/contracts/oracle/osmosis/src/helpers.rs index 342c9e2ce..f499ee2b3 100644 --- a/contracts/oracle/osmosis/src/helpers.rs +++ b/contracts/oracle/osmosis/src/helpers.rs @@ -1,3 +1,4 @@ +use cosmwasm_std::Decimal; use mars_oracle_base::{ContractError, ContractResult}; use mars_osmosis::helpers::{has_denom, Pool}; @@ -71,3 +72,22 @@ pub fn assert_osmosis_twap( Ok(()) } + +/// Assert Pyth configuration +pub fn assert_pyth(max_confidence: Decimal, max_deviation: Decimal) -> ContractResult<()> { + // TODO: update validation once confirmed with Risk team + if !max_confidence.le(&Decimal::one()) { + return Err(ContractError::InvalidPriceSource { + reason: "max_confidence must be in the range of <0;1>".to_string(), + }); + } + + // TODO: update validation once confirmed with Risk team + if !max_deviation.le(&Decimal::one()) { + return Err(ContractError::InvalidPriceSource { + reason: "max_deviation must be in the range of <0;1>".to_string(), + }); + } + + Ok(()) +} diff --git a/contracts/oracle/osmosis/src/price_source.rs b/contracts/oracle/osmosis/src/price_source.rs index e23662b65..57f80471e 100644 --- a/contracts/oracle/osmosis/src/price_source.rs +++ b/contracts/oracle/osmosis/src/price_source.rs @@ -9,6 +9,8 @@ use mars_osmosis::helpers::{ query_arithmetic_twap_price, query_geometric_twap_price, query_pool, query_spot_price, recovered_since_downtime_of_length, Pool, }; +use mars_red_bank_types::oracle::PythConfig; +use pyth_sdk_cw::{query_price_feed, PriceIdentifier}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -149,6 +151,24 @@ pub enum OsmosisPriceSource { /// Detect when the chain is recovering from downtime downtime_detector: Option, }, + Pyth { + /// Price feed id of an asset from the list: https://pyth.network/developers/price-feed-ids + price_feed_id: PriceIdentifier, + + /// The maximum number of seconds since the last price was by an oracle, before + /// rejecting the price as too stale + max_staleness: u64, + + /// 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 + /// of the price. + max_confidence: Decimal, + + /// The maximum deviation (percentage) between current and EMA price + max_deviation: Decimal, + }, } impl fmt::Display for OsmosisPriceSource { @@ -188,6 +208,14 @@ impl fmt::Display for OsmosisPriceSource { let dd_fmt = DowntimeDetector::fmt(downtime_detector); format!("staked_geometric_twap:{transitive_denom}:{pool_id}:{window_size}:{dd_fmt}") } + OsmosisPriceSource::Pyth { + price_feed_id, + max_staleness, + max_confidence, + max_deviation, + } => { + format!("pyth:{price_feed_id}:{max_staleness}:{max_confidence}:{max_deviation}") + } }; write!(f, "{label}") } @@ -244,6 +272,11 @@ impl PriceSource for OsmosisPriceSource { helpers::assert_osmosis_pool_assets(&pool, denom, transitive_denom)?; helpers::assert_osmosis_twap(*window_size, downtime_detector) } + OsmosisPriceSource::Pyth { + max_confidence, + max_deviation, + .. + } => helpers::assert_pyth(*max_confidence, *max_deviation), } } @@ -254,6 +287,7 @@ impl PriceSource for OsmosisPriceSource { denom: &str, base_denom: &str, price_sources: &Map<&str, Self>, + pyth_config: &PythConfig, ) -> ContractResult { match self { OsmosisPriceSource::Fixed { @@ -292,6 +326,7 @@ impl PriceSource for OsmosisPriceSource { *pool_id, base_denom, price_sources, + pyth_config, ), OsmosisPriceSource::StakedGeometricTwap { transitive_denom, @@ -310,8 +345,23 @@ impl PriceSource for OsmosisPriceSource { *pool_id, *window_size, price_sources, + pyth_config, ) } + OsmosisPriceSource::Pyth { + price_feed_id, + max_staleness, + max_confidence, + max_deviation, + } => Ok(Self::query_pyth_price( + deps, + env, + *price_feed_id, + *max_staleness, + *max_confidence, + *max_deviation, + pyth_config, + )?), } } } @@ -347,6 +397,7 @@ impl OsmosisPriceSource { pool_id: u64, base_denom: &str, price_sources: &Map<&str, Self>, + pyth_config: &PythConfig, ) -> ContractResult { // XYK pool asserted during price source creation let pool = query_pool(&deps.querier, pool_id)?; @@ -360,6 +411,7 @@ impl OsmosisPriceSource { &coin0.denom, base_denom, price_sources, + pyth_config, )?; let coin1_price = price_sources.load(deps.storage, &coin1.denom)?.query_price( deps, @@ -367,6 +419,7 @@ impl OsmosisPriceSource { &coin1.denom, base_denom, price_sources, + pyth_config, )?; let coin0_value = Uint256::from_uint128(coin0.amount) * Decimal256::from(coin0_price); @@ -387,6 +440,7 @@ impl OsmosisPriceSource { /// where: /// - stAsset/Asset price calculated using the geometric TWAP from the stAsset/Asset pool. /// - Asset/OSMO price comes from the Mars Oracle contract. + #[allow(clippy::too_many_arguments)] fn query_staked_asset_price( deps: &Deps, env: &Env, @@ -396,6 +450,7 @@ impl OsmosisPriceSource { pool_id: u64, window_size: u64, price_sources: &Map<&str, OsmosisPriceSource>, + pyth_config: &PythConfig, ) -> ContractResult { let start_time = env.block.time.seconds() - window_size; let staked_price = query_geometric_twap_price( @@ -413,10 +468,104 @@ impl OsmosisPriceSource { transitive_denom, base_denom, price_sources, + pyth_config, )?; staked_price.checked_mul(transitive_price).map_err(Into::into) } + + fn query_pyth_price( + deps: &Deps, + env: &Env, + price_feed_id: PriceIdentifier, + max_staleness: u64, + max_confidence: Decimal, + max_deviation: Decimal, + pyth_config: &PythConfig, + ) -> ContractResult { + let current_time = env.block.time.seconds(); + + let price_feed_response = + query_price_feed(&deps.querier, pyth_config.pyth_contract_addr.clone(), price_feed_id)?; + let price_feed = price_feed_response.price_feed; + + // Get the current price and confidence interval from the price feed + let current_price = price_feed.get_price_unchecked(); + + // Check if the current price is not too old + if (current_time - current_price.publish_time as u64) > max_staleness { + return Err(InvalidPrice { + reason: format!( + "current price publish time is too old/stale. published: {}, now: {}", + current_price.publish_time, current_time + ), + }); + } + + // Get an exponentially-weighted moving average price and confidence interval + let ema_price = price_feed.get_ema_price_unchecked(); + + // Check if the EMA price is not too old + if (current_time - ema_price.publish_time as u64) > max_staleness { + return Err(InvalidPrice { + reason: format!( + "EMA price publish time is too old/stale. published: {}, now: {}", + ema_price.publish_time, current_time + ), + }); + } + + // Check if the current and EMA price is > 0 + if current_price.price <= 0 || ema_price.price <= 0 { + return Err(InvalidPrice { + reason: "price can't be <= 0".to_string(), + }); + } + + let current_price_dec = scale_to_exponent(current_price.price as u128, current_price.expo)?; + let ema_price_dec = scale_to_exponent(ema_price.price as u128, ema_price.expo)?; + + // Check confidence deviation + let confidence = scale_to_exponent(current_price.conf as u128, current_price.expo)?; + let price_confidence = confidence.checked_div(ema_price_dec)?; + if price_confidence > max_confidence { + return Err(InvalidPrice { + reason: format!("price confidence deviation {price_confidence} exceeds max allowed {max_confidence}") + }); + } + + // Check price deviation + let delta = current_price_dec.abs_diff(ema_price_dec); + let price_deviation = delta.checked_div(ema_price_dec)?; + if price_deviation > max_deviation { + return Err(InvalidPrice { + reason: format!( + "price deviation {price_deviation} exceeds max allowed {max_deviation}" + ), + }); + } + + Ok(current_price_dec) + } +} + +/// Price feeds represent numbers in a fixed-point format. +/// The same exponent is used for both the price and confidence interval. +/// The integer representation of these values can be computed by multiplying by 10^exponent. +/// +/// As an example, imagine Pyth reported the following values for ATOM/USD: +/// expo: -8 +/// conf: 574566 +/// price: 1365133270 +/// The confidence interval is 574566 * 10^(-8) = $0.00574566, and the price is 1365133270 * 10^(-8) = $13.6513327. +fn scale_to_exponent(value: u128, expo: i32) -> ContractResult { + let target_expo = Uint128::from(10u8).checked_pow(expo.unsigned_abs())?; + if expo < 0 { + Ok(Decimal::checked_from_ratio(value, target_expo)?) + } else { + let res = Uint128::from(value).checked_mul(target_expo)?; + Ok(Decimal::from_ratio(res, 1u128)) + } } #[cfg(test)] @@ -520,4 +669,21 @@ mod tests { }; assert_eq!(ps.to_string(), "xyk_liquidity_token:224") } + + #[test] + fn display_pyth_price_source() { + let ps = OsmosisPriceSource::Pyth { + price_feed_id: PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(), + max_staleness: 60, + max_confidence: Decimal::from_ratio(5u128, 100u128), + max_deviation: Decimal::from_ratio(6u128, 100u128), + }; + assert_eq!( + ps.to_string(), + "pyth:0x61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3:60:0.05:0.06" + ) + } } diff --git a/contracts/oracle/osmosis/tests/helpers.rs b/contracts/oracle/osmosis/tests/helpers.rs index 33272dc0e..896bd5083 100644 --- a/contracts/oracle/osmosis/tests/helpers.rs +++ b/contracts/oracle/osmosis/tests/helpers.rs @@ -5,7 +5,7 @@ use std::marker::PhantomData; use cosmwasm_std::{ coin, from_binary, testing::{mock_env, MockApi, MockQuerier, MockStorage}, - Coin, Deps, DepsMut, OwnedDeps, + Coin, Decimal, Deps, DepsMut, OwnedDeps, }; use mars_oracle_base::ContractError; use mars_oracle_osmosis::{contract::entry, msg::ExecuteMsg, OsmosisPriceSource}; @@ -13,14 +13,10 @@ use mars_osmosis::helpers::{Pool, QueryPoolResponse}; use mars_red_bank_types::oracle::{InstantiateMsg, QueryMsg}; use mars_testing::{mock_info, MarsMockQuerier}; use osmosis_std::types::osmosis::gamm::v1beta1::PoolAsset; +use pyth_sdk_cw::PriceIdentifier; -pub fn setup_test() -> OwnedDeps { - let mut deps = OwnedDeps::<_, _, _> { - storage: MockStorage::default(), - api: MockApi::default(), - querier: MarsMockQuerier::new(MockQuerier::new(&[])), - custom_query_type: PhantomData, - }; +pub fn setup_test_with_pools() -> OwnedDeps { + let mut deps = setup_test(); // set a few osmosis pools let assets = vec![coin(42069, "uatom"), coin(69420, "uosmo")]; @@ -75,6 +71,17 @@ pub fn setup_test() -> OwnedDeps { ), ); + deps +} + +pub fn setup_test() -> OwnedDeps { + let mut deps = OwnedDeps::<_, _, _> { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MarsMockQuerier::new(MockQuerier::new(&[])), + custom_query_type: PhantomData, + }; + // instantiate the oracle contract entry::instantiate( deps.as_mut(), @@ -83,6 +90,7 @@ pub fn setup_test() -> OwnedDeps { InstantiateMsg { owner: "owner".to_string(), base_denom: "uosmo".to_string(), + pyth_contract_addr: "pyth_contract".to_string(), }, ) .unwrap(); @@ -132,6 +140,19 @@ fn prepare_pool_assets(coins: &[Coin], weights: &[u64]) -> Vec { .collect() } +pub fn set_pyth_price_source(deps: DepsMut, denom: &str, price_id: PriceIdentifier) { + set_price_source( + deps, + denom, + OsmosisPriceSource::Pyth { + price_feed_id: price_id, + max_staleness: 30, + max_confidence: Decimal::from_ratio(5u128, 100u128), + max_deviation: Decimal::from_ratio(6u128, 100u128), + }, + ) +} + pub fn set_price_source(deps: DepsMut, denom: &str, price_source: OsmosisPriceSource) { entry::execute( deps, diff --git a/contracts/oracle/osmosis/tests/test_admin.rs b/contracts/oracle/osmosis/tests/test_admin.rs index 22cdbcabc..964102c7b 100644 --- a/contracts/oracle/osmosis/tests/test_admin.rs +++ b/contracts/oracle/osmosis/tests/test_admin.rs @@ -1,6 +1,7 @@ -use cosmwasm_std::testing::mock_env; +use cosmwasm_std::{attr, testing::mock_env}; use mars_oracle_base::ContractError; -use mars_oracle_osmosis::contract::entry; +use mars_oracle_osmosis::{contract::entry, msg::ExecuteMsg}; +use mars_owner::OwnerError::NotOwner; use mars_red_bank_types::oracle::{ConfigResponse, InstantiateMsg, QueryMsg}; use mars_testing::{mock_dependencies, mock_info}; use mars_utils::error::ValidationError; @@ -9,12 +10,13 @@ mod helpers; #[test] fn instantiating() { - let deps = helpers::setup_test(); + let deps = helpers::setup_test_with_pools(); let cfg: ConfigResponse = helpers::query(deps.as_ref(), QueryMsg::Config {}); assert_eq!(cfg.owner.unwrap(), "owner".to_string()); assert_eq!(cfg.proposed_new_owner, None); assert_eq!(cfg.base_denom, "uosmo".to_string()); + assert_eq!(cfg.pyth_contract_addr, "pyth_contract".to_string()); } #[test] @@ -30,6 +32,7 @@ fn instantiating_incorrect_denom() { InstantiateMsg { owner: "owner".to_string(), base_denom: "!*jadfaefc".to_string(), + pyth_contract_addr: "pyth_contract_addr".to_string(), }, ); assert_eq!( @@ -46,6 +49,7 @@ fn instantiating_incorrect_denom() { InstantiateMsg { owner: "owner".to_string(), base_denom: "ahdbufenf&*!-".to_string(), + pyth_contract_addr: "pyth_contract_addr".to_string(), }, ); assert_eq!( @@ -63,6 +67,7 @@ fn instantiating_incorrect_denom() { InstantiateMsg { owner: "owner".to_string(), base_denom: "ab".to_string(), + pyth_contract_addr: "pyth_contract_addr".to_string(), }, ); assert_eq!( @@ -72,3 +77,63 @@ fn instantiating_incorrect_denom() { })) ); } + +#[test] +fn update_config_if_unauthorized() { + let mut deps = helpers::setup_test(); + + let msg = ExecuteMsg::UpdateConfig { + base_denom: None, + pyth_contract_addr: None, + }; + let info = mock_info("somebody"); + let res_err = entry::execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + assert_eq!(res_err, ContractError::Owner(NotOwner {})); +} + +#[test] +fn update_config_with_invalid_base_denom() { + let mut deps = helpers::setup_test(); + + let msg = ExecuteMsg::UpdateConfig { + base_denom: Some("*!fdskfna".to_string()), + pyth_contract_addr: None, + }; + let info = mock_info("owner"); + let res_err = entry::execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + assert_eq!( + res_err, + ContractError::Validation(ValidationError::InvalidDenom { + reason: "First character is not ASCII alphabetic".to_string() + }) + ); +} + +#[test] +fn update_config_with_new_params() { + let mut deps = helpers::setup_test(); + + let msg = ExecuteMsg::UpdateConfig { + base_denom: Some("uusdc".to_string()), + pyth_contract_addr: Some("new_pyth_contract_addr".to_string()), + }; + let info = mock_info("owner"); + let res = entry::execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + assert_eq!(res.messages.len(), 0); + assert_eq!( + res.attributes, + vec![ + attr("action", "update_config"), + attr("prev_base_denom", "uosmo"), + attr("base_denom", "uusdc"), + attr("prev_pyth_contract_addr", "pyth_contract"), + attr("pyth_contract_addr", "new_pyth_contract_addr") + ] + ); + + let cfg: ConfigResponse = helpers::query(deps.as_ref(), QueryMsg::Config {}); + assert_eq!(cfg.owner.unwrap(), "owner".to_string()); + assert_eq!(cfg.proposed_new_owner, None); + assert_eq!(cfg.base_denom, "uusdc".to_string()); + assert_eq!(cfg.pyth_contract_addr, "new_pyth_contract_addr".to_string()); +} diff --git a/contracts/oracle/osmosis/tests/test_query_price.rs b/contracts/oracle/osmosis/tests/test_query_price.rs index a0df0dcdb..13ea88e43 100644 --- a/contracts/oracle/osmosis/tests/test_query_price.rs +++ b/contracts/oracle/osmosis/tests/test_query_price.rs @@ -1,11 +1,13 @@ -use cosmwasm_std::{coin, Decimal, StdError}; +use cosmwasm_std::{coin, from_binary, Decimal, StdError}; use mars_oracle_base::ContractError; -use mars_oracle_osmosis::{Downtime, DowntimeDetector, OsmosisPriceSource}; +use mars_oracle_osmosis::{contract::entry, Downtime, DowntimeDetector, OsmosisPriceSource}; use mars_red_bank_types::oracle::{PriceResponse, QueryMsg}; +use mars_testing::mock_env_at_block_time; use osmosis_std::types::osmosis::{ gamm::v2::QuerySpotPriceResponse, twap::v1beta1::{ArithmeticTwapToNowResponse, GeometricTwapToNowResponse}, }; +use pyth_sdk_cw::{Price, PriceFeed, PriceFeedResponse, PriceIdentifier}; use crate::helpers::prepare_query_pool_response; @@ -13,7 +15,7 @@ mod helpers; #[test] fn querying_fixed_price() { - let mut deps = helpers::setup_test(); + let mut deps = helpers::setup_test_with_pools(); helpers::set_price_source( deps.as_mut(), @@ -34,7 +36,7 @@ fn querying_fixed_price() { #[test] fn querying_spot_price() { - let mut deps = helpers::setup_test(); + let mut deps = helpers::setup_test_with_pools(); helpers::set_price_source( deps.as_mut(), @@ -64,7 +66,7 @@ fn querying_spot_price() { #[test] fn querying_arithmetic_twap_price() { - let mut deps = helpers::setup_test(); + let mut deps = helpers::setup_test_with_pools(); helpers::set_price_source( deps.as_mut(), @@ -96,7 +98,7 @@ fn querying_arithmetic_twap_price() { #[test] fn querying_arithmetic_twap_price_with_downtime_detector() { - let mut deps = helpers::setup_test(); + let mut deps = helpers::setup_test_with_pools(); let dd = DowntimeDetector { downtime: Downtime::Duration10m, @@ -146,7 +148,7 @@ fn querying_arithmetic_twap_price_with_downtime_detector() { #[test] fn querying_geometric_twap_price() { - let mut deps = helpers::setup_test(); + let mut deps = helpers::setup_test_with_pools(); helpers::set_price_source( deps.as_mut(), @@ -178,7 +180,7 @@ fn querying_geometric_twap_price() { #[test] fn querying_geometric_twap_price_with_downtime_detector() { - let mut deps = helpers::setup_test(); + let mut deps = helpers::setup_test_with_pools(); let dd = DowntimeDetector { downtime: Downtime::Duration10m, @@ -228,7 +230,7 @@ fn querying_geometric_twap_price_with_downtime_detector() { #[test] fn querying_staked_geometric_twap_price() { - let mut deps = helpers::setup_test(); + let mut deps = helpers::setup_test_with_pools(); helpers::set_price_source( deps.as_mut(), @@ -281,7 +283,7 @@ fn querying_staked_geometric_twap_price() { #[test] fn querying_staked_geometric_twap_price_if_no_transitive_denom_price_source() { - let mut deps = helpers::setup_test(); + let mut deps = helpers::setup_test_with_pools(); helpers::set_price_source( deps.as_mut(), @@ -320,7 +322,7 @@ fn querying_staked_geometric_twap_price_if_no_transitive_denom_price_source() { #[test] fn querying_staked_geometric_twap_price_with_downtime_detector() { - let mut deps = helpers::setup_test(); + let mut deps = helpers::setup_test_with_pools(); let dd = DowntimeDetector { downtime: Downtime::Duration10m, @@ -393,7 +395,7 @@ fn querying_staked_geometric_twap_price_with_downtime_detector() { #[test] fn querying_xyk_lp_price() { - let mut deps = helpers::setup_test(); + let mut deps = helpers::setup_test_with_pools(); let assets = vec![coin(1, "uatom"), coin(1, "uosmo")]; deps.querier.set_query_pool_response( @@ -505,9 +507,417 @@ fn querying_xyk_lp_price() { } #[test] -fn querying_all_prices() { +fn querying_pyth_price_if_publish_price_too_old() { + let mut deps = helpers::setup_test(); + + let price_id = PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(); + + let max_staleness = 30u64; + helpers::set_price_source( + deps.as_mut(), + "uatom", + OsmosisPriceSource::Pyth { + price_feed_id: price_id, + max_staleness, + max_confidence: Decimal::from_ratio(5u128, 100u128), + max_deviation: Decimal::from_ratio(6u128, 100u128), + }, + ); + + let price_publish_time = 1677157333u64; + let ema_price_publish_time = price_publish_time + max_staleness; + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: 1371155677, + conf: 646723, + expo: -8, + publish_time: price_publish_time as i64, + }, + Price { + price: 1365133270, + conf: 574566, + expo: -8, + publish_time: ema_price_publish_time as i64, + }, + ), + }, + ); + + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(price_publish_time + max_staleness + 1u64), + QueryMsg::Price { + denom: "uatom".to_string(), + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::InvalidPrice { + reason: + "current price publish time is too old/stale. published: 1677157333, now: 1677157364" + .to_string() + } + ); + + let ema_price_publish_time = 1677157333u64; + let price_publish_time = ema_price_publish_time + max_staleness; + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: 1371155677, + conf: 646723, + expo: -8, + publish_time: price_publish_time as i64, + }, + Price { + price: 1365133270, + conf: 574566, + expo: -8, + publish_time: ema_price_publish_time as i64, + }, + ), + }, + ); + + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(ema_price_publish_time + max_staleness + 1u64), + QueryMsg::Price { + denom: "uatom".to_string(), + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::InvalidPrice { + reason: + "EMA price publish time is too old/stale. published: 1677157333, now: 1677157364" + .to_string() + } + ); +} + +#[test] +fn querying_pyth_price_if_signed() { + let mut deps = helpers::setup_test(); + + let price_id = PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(); + + let max_staleness = 30u64; + helpers::set_price_source( + deps.as_mut(), + "uatom", + OsmosisPriceSource::Pyth { + price_feed_id: price_id, + max_staleness, + max_confidence: Decimal::from_ratio(5u128, 100u128), + max_deviation: Decimal::from_ratio(6u128, 100u128), + }, + ); + + let publish_time = 1677157333u64; + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: -1371155677, + conf: 646723, + expo: -8, + publish_time: publish_time as i64, + }, + Price { + price: -1365133270, + conf: 574566, + expo: -8, + publish_time: publish_time as i64, + }, + ), + }, + ); + + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::InvalidPrice { + reason: "price can't be <= 0".to_string() + } + ); +} + +#[test] +fn querying_pyth_price_if_confidence_exceeded() { let mut deps = helpers::setup_test(); + let price_id = PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(); + + let max_staleness = 30u64; + helpers::set_price_source( + deps.as_mut(), + "uatom", + OsmosisPriceSource::Pyth { + price_feed_id: price_id, + max_staleness, + max_confidence: Decimal::from_ratio(5u128, 100u128), + max_deviation: Decimal::from_ratio(6u128, 100u128), + }, + ); + + let publish_time = 1677157333u64; + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: 1010000, + conf: 51000, + expo: -4, + publish_time: publish_time as i64, + }, + Price { + price: 1000000, + conf: 40000, + expo: -4, + publish_time: publish_time as i64, + }, + ), + }, + ); + + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::InvalidPrice { + reason: "price confidence deviation 0.051 exceeds max allowed 0.05".to_string() + } + ); +} + +#[test] +fn querying_pyth_price_if_deviation_exceeded() { + let mut deps = helpers::setup_test(); + + let price_id = PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(); + + let max_staleness = 30u64; + helpers::set_price_source( + deps.as_mut(), + "uatom", + OsmosisPriceSource::Pyth { + price_feed_id: price_id, + max_staleness, + max_confidence: Decimal::from_ratio(5u128, 100u128), + max_deviation: Decimal::from_ratio(6u128, 100u128), + }, + ); + + let publish_time = 1677157333u64; + + // price > ema_price + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: 1061000, + conf: 50000, + expo: -4, + publish_time: publish_time as i64, + }, + Price { + price: 1000000, + conf: 40000, + expo: -4, + publish_time: publish_time as i64, + }, + ), + }, + ); + + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::InvalidPrice { + reason: "price deviation 0.061 exceeds max allowed 0.06".to_string() + } + ); + + // ema_price > price + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: 939999, + conf: 50000, + expo: -4, + publish_time: publish_time as i64, + }, + Price { + price: 1000000, + conf: 40000, + expo: -4, + publish_time: publish_time as i64, + }, + ), + }, + ); + + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::InvalidPrice { + reason: "price deviation 0.060001 exceeds max allowed 0.06".to_string() + } + ); +} + +#[test] +fn querying_pyth_price_successfully() { + let mut deps = helpers::setup_test(); + + let price_id = PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(); + + let max_staleness = 30u64; + helpers::set_price_source( + deps.as_mut(), + "uatom", + OsmosisPriceSource::Pyth { + price_feed_id: price_id, + max_staleness, + max_confidence: Decimal::from_ratio(5u128, 100u128), + max_deviation: Decimal::from_ratio(6u128, 100u128), + }, + ); + + let publish_time = 1677157333u64; + + // exp < 0 + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: 1021000, + conf: 50000, + expo: -4, + publish_time: publish_time as i64, + }, + Price { + price: 1000000, + conf: 40000, + expo: -4, + publish_time: publish_time as i64, + }, + ), + }, + ); + + let res = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + }, + ) + .unwrap(); + let res: PriceResponse = from_binary(&res).unwrap(); + assert_eq!(res.price, Decimal::from_ratio(1021000u128, 10000u128)); + + // exp > 0 + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: 102, + conf: 5, + expo: 3, + publish_time: publish_time as i64, + }, + Price { + price: 100, + conf: 4, + expo: 3, + publish_time: publish_time as i64, + }, + ), + }, + ); + + let res = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + }, + ) + .unwrap(); + let res: PriceResponse = from_binary(&res).unwrap(); + assert_eq!(res.price, Decimal::from_ratio(102000u128, 1u128)); +} + +#[test] +fn querying_all_prices() { + let mut deps = helpers::setup_test_with_pools(); + helpers::set_price_source( deps.as_mut(), "uosmo", diff --git a/contracts/oracle/osmosis/tests/test_remove_price_source.rs b/contracts/oracle/osmosis/tests/test_remove_price_source.rs index 5233ca3c3..3030e7c6b 100644 --- a/contracts/oracle/osmosis/tests/test_remove_price_source.rs +++ b/contracts/oracle/osmosis/tests/test_remove_price_source.rs @@ -13,7 +13,7 @@ mod helpers; #[test] fn remove_price_source_by_non_owner() { - let mut deps = helpers::setup_test(); + let mut deps = helpers::setup_test_with_pools(); let err = execute( deps.as_mut(), @@ -29,7 +29,7 @@ fn remove_price_source_by_non_owner() { #[test] fn removing_price_source() { - let mut deps = helpers::setup_test(); + let mut deps = helpers::setup_test_with_pools(); helpers::set_price_source( deps.as_mut(), diff --git a/contracts/oracle/osmosis/tests/test_set_price_source.rs b/contracts/oracle/osmosis/tests/test_set_price_source.rs index f053e2636..f27510c4b 100644 --- a/contracts/oracle/osmosis/tests/test_set_price_source.rs +++ b/contracts/oracle/osmosis/tests/test_set_price_source.rs @@ -9,12 +9,13 @@ use mars_owner::OwnerError::NotOwner; use mars_red_bank_types::oracle::QueryMsg; use mars_testing::mock_info; use mars_utils::error::ValidationError; +use pyth_sdk_cw::PriceIdentifier; mod helpers; #[test] fn setting_price_source_by_non_owner() { - let mut deps = helpers::setup_test(); + let mut deps = helpers::setup_test_with_pools(); let err = execute( deps.as_mut(), @@ -33,7 +34,7 @@ fn setting_price_source_by_non_owner() { #[test] fn setting_price_source_fixed() { - let mut deps = helpers::setup_test(); + let mut deps = helpers::setup_test_with_pools(); let res = execute( deps.as_mut(), @@ -65,7 +66,7 @@ fn setting_price_source_fixed() { #[test] fn setting_price_source_incorrect_denom() { - let mut deps = helpers::setup_test(); + let mut deps = helpers::setup_test_with_pools(); let res = execute( deps.as_mut(), @@ -125,7 +126,7 @@ fn setting_price_source_incorrect_denom() { #[test] fn setting_price_source_spot() { - let mut deps = helpers::setup_test(); + let mut deps = helpers::setup_test_with_pools(); let mut set_price_source_spot = |denom: &str, pool_id: u64| { execute( @@ -197,7 +198,7 @@ fn setting_price_source_spot() { #[test] fn setting_price_source_arithmetic_twap_with_invalid_params() { - let mut deps = helpers::setup_test(); + let mut deps = helpers::setup_test_with_pools(); let mut set_price_source_twap = |denom: &str, @@ -285,7 +286,7 @@ fn setting_price_source_arithmetic_twap_with_invalid_params() { #[test] fn setting_price_source_arithmetic_twap_successfully() { - let mut deps = helpers::setup_test(); + let mut deps = helpers::setup_test_with_pools(); // properly set twap price source let res = execute( @@ -360,7 +361,7 @@ fn setting_price_source_arithmetic_twap_successfully() { #[test] fn setting_price_source_geometric_twap_with_invalid_params() { - let mut deps = helpers::setup_test(); + let mut deps = helpers::setup_test_with_pools(); let mut set_price_source_twap = |denom: &str, @@ -448,7 +449,7 @@ fn setting_price_source_geometric_twap_with_invalid_params() { #[test] fn setting_price_source_geometric_twap_successfully() { - let mut deps = helpers::setup_test(); + let mut deps = helpers::setup_test_with_pools(); // properly set twap price source let res = execute( @@ -523,7 +524,7 @@ fn setting_price_source_geometric_twap_successfully() { #[test] fn setting_price_source_staked_geometric_twap_with_invalid_params() { - let mut deps = helpers::setup_test(); + let mut deps = helpers::setup_test_with_pools(); let mut set_price_source_twap = |denom: &str, @@ -612,7 +613,7 @@ fn setting_price_source_staked_geometric_twap_with_invalid_params() { #[test] fn setting_price_source_staked_geometric_twap_successfully() { - let mut deps = helpers::setup_test(); + let mut deps = helpers::setup_test_with_pools(); // properly set twap price source let res = execute( @@ -691,7 +692,7 @@ fn setting_price_source_staked_geometric_twap_successfully() { #[test] fn setting_price_source_xyk_lp() { - let mut deps = helpers::setup_test(); + let mut deps = helpers::setup_test_with_pools(); let mut set_price_source_xyk_lp = |denom: &str, pool_id: u64| { execute( @@ -744,9 +745,96 @@ fn setting_price_source_xyk_lp() { } #[test] -fn querying_price_source() { +fn setting_price_source_pyth_with_invalid_params() { let mut deps = helpers::setup_test(); + let mut set_price_source_pyth = |max_confidence: Decimal, max_deviation: Decimal| { + execute( + deps.as_mut(), + mock_env(), + mock_info("owner"), + ExecuteMsg::SetPriceSource { + denom: "uatom".to_string(), + price_source: OsmosisPriceSource::Pyth { + price_feed_id: PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(), + max_staleness: 30, + max_confidence, + max_deviation, + }, + }, + ) + }; + + // attempting to set max_confidence > 100%; should fail + let err = set_price_source_pyth(Decimal::percent(101), Decimal::percent(5)).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "max_confidence must be in the range of <0;1>".to_string() + } + ); + + // attempting to set max_deviation > 100%; should fail + let err = set_price_source_pyth(Decimal::percent(5), Decimal::percent(101)).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "max_deviation must be in the range of <0;1>".to_string() + } + ); +} + +#[test] +fn setting_price_source_pyth_successfully() { + let mut deps = helpers::setup_test(); + + let res = execute( + deps.as_mut(), + mock_env(), + mock_info("owner"), + ExecuteMsg::SetPriceSource { + denom: "uatom".to_string(), + price_source: OsmosisPriceSource::Pyth { + price_feed_id: PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(), + max_staleness: 30, + max_confidence: Decimal::percent(5), + max_deviation: Decimal::percent(6), + }, + }, + ) + .unwrap(); + assert_eq!(res.messages.len(), 0); + + let res: PriceSourceResponse = helpers::query( + deps.as_ref(), + QueryMsg::PriceSource { + denom: "uatom".to_string(), + }, + ); + assert_eq!( + res.price_source, + OsmosisPriceSource::Pyth { + price_feed_id: PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3" + ) + .unwrap(), + max_staleness: 30, + max_confidence: Decimal::percent(5), + max_deviation: Decimal::percent(6), + }, + ); +} + +#[test] +fn querying_price_source() { + let mut deps = helpers::setup_test_with_pools(); + helpers::set_price_source( deps.as_mut(), "uosmo", diff --git a/contracts/oracle/osmosis/tests/test_update_owner.rs b/contracts/oracle/osmosis/tests/test_update_owner.rs index 162924520..0c0b0d57d 100644 --- a/contracts/oracle/osmosis/tests/test_update_owner.rs +++ b/contracts/oracle/osmosis/tests/test_update_owner.rs @@ -4,13 +4,13 @@ use mars_oracle_osmosis::contract::entry::execute; use mars_owner::{OwnerError::NotOwner, OwnerUpdate}; use mars_red_bank_types::oracle::{ConfigResponse, ExecuteMsg, QueryMsg}; -use crate::helpers::{query, setup_test}; +use crate::helpers::{query, setup_test_with_pools}; mod helpers; #[test] fn initialized_state() { - let deps = setup_test(); + let deps = setup_test_with_pools(); let config: ConfigResponse = query(deps.as_ref(), QueryMsg::Config {}); assert!(config.owner.is_some()); @@ -19,7 +19,7 @@ fn initialized_state() { #[test] fn update_owner() { - let mut deps = setup_test(); + let mut deps = setup_test_with_pools(); let original_config: ConfigResponse = query(deps.as_ref(), QueryMsg::Config {}); diff --git a/integration-tests/tests/test_oracles.rs b/integration-tests/tests/test_oracles.rs index b98a251d0..4705e7848 100644 --- a/integration-tests/tests/test_oracles.rs +++ b/integration-tests/tests/test_oracles.rs @@ -36,6 +36,8 @@ const OSMOSIS_ADDR_PROVIDER_CONTRACT_NAME: &str = "mars-address-provider"; const OSMOSIS_REWARDS_CONTRACT_NAME: &str = "mars-rewards-collector-osmosis"; const OSMOSIS_INCENTIVES_CONTRACT_NAME: &str = "mars-incentives"; +const OSMOSIS_PYTH_ADDR: &str = "osmo1svg55quy7jjee6dn0qx85qxxvx5cafkkw4tmqpcjr9dx99l0zrhs4usft5"; // correct bech32 addr to pass validation + #[test] fn querying_xyk_lp_price_if_no_price_for_tokens() { let app = OsmosisTestApp::new(); @@ -56,6 +58,7 @@ fn querying_xyk_lp_price_if_no_price_for_tokens() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), + pyth_contract_addr: OSMOSIS_PYTH_ADDR.to_string(), }, ); @@ -109,6 +112,7 @@ fn querying_xyk_lp_price_success() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), + pyth_contract_addr: OSMOSIS_PYTH_ADDR.to_string(), }, ); @@ -217,6 +221,7 @@ fn query_spot_price() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), + pyth_contract_addr: OSMOSIS_PYTH_ADDR.to_string(), }, ); @@ -281,6 +286,7 @@ fn set_spot_without_pools() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), + pyth_contract_addr: OSMOSIS_PYTH_ADDR.to_string(), }, ); @@ -318,6 +324,7 @@ fn incorrect_pool_for_spot() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), + pyth_contract_addr: OSMOSIS_PYTH_ADDR.to_string(), }, ); @@ -364,6 +371,7 @@ fn update_spot_with_different_pool() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), + pyth_contract_addr: OSMOSIS_PYTH_ADDR.to_string(), }, ); @@ -438,6 +446,7 @@ fn query_spot_price_after_lp_change() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), + pyth_contract_addr: OSMOSIS_PYTH_ADDR.to_string(), }, ); @@ -497,6 +506,7 @@ fn query_geometric_twap_price_with_downtime_detector() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), + pyth_contract_addr: OSMOSIS_PYTH_ADDR.to_string(), }, ); @@ -581,6 +591,7 @@ fn query_arithmetic_twap_price() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), + pyth_contract_addr: OSMOSIS_PYTH_ADDR.to_string(), }, ); @@ -667,6 +678,7 @@ fn query_geometric_twap_price() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), + pyth_contract_addr: OSMOSIS_PYTH_ADDR.to_string(), }, ); @@ -753,6 +765,7 @@ fn compare_spot_and_twap_price() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), + pyth_contract_addr: OSMOSIS_PYTH_ADDR.to_string(), }, ); @@ -1013,6 +1026,7 @@ fn setup_redbank(wasm: &Wasm, signer: &SigningAccount) -> (Strin &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), + pyth_contract_addr: OSMOSIS_PYTH_ADDR.to_string(), }, ); diff --git a/packages/testing/Cargo.toml b/packages/testing/Cargo.toml index 2c4940068..11c8c8751 100644 --- a/packages/testing/Cargo.toml +++ b/packages/testing/Cargo.toml @@ -30,6 +30,7 @@ mars-red-bank = { workspace = true } mars-red-bank-types = { workspace = true } mars-rewards-collector-osmosis = { workspace = true } prost = { workspace = true } +pyth-sdk-cw = { workspace = true } schemars = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } diff --git a/packages/testing/src/integration/mock_env.rs b/packages/testing/src/integration/mock_env.rs index 4a51d010a..6c4a06d3b 100644 --- a/packages/testing/src/integration/mock_env.rs +++ b/packages/testing/src/integration/mock_env.rs @@ -452,6 +452,8 @@ pub struct MockEnvBuilder { safety_fund_denom: String, fee_collector_denom: String, slippage_tolerance: Decimal, + + pyth_contract_addr: String, } impl MockEnvBuilder { @@ -469,6 +471,8 @@ impl MockEnvBuilder { safety_fund_denom: "uusdc".to_string(), fee_collector_denom: "uusdc".to_string(), slippage_tolerance: Decimal::percent(5), + pyth_contract_addr: "osmo1svg55quy7jjee6dn0qx85qxxvx5cafkkw4tmqpcjr9dx99l0zrhs4usft5" + .to_string(), // correct bech32 addr to pass validation } } @@ -512,6 +516,11 @@ impl MockEnvBuilder { self } + pub fn pyth_contract_addr(&mut self, pyth_contract_addr: Addr) -> &mut Self { + self.pyth_contract_addr = pyth_contract_addr.to_string(); + self + } + pub fn build(&mut self) -> MockEnv { let address_provider_addr = self.deploy_address_provider(); let incentives_addr = self.deploy_incentives(&address_provider_addr); @@ -604,6 +613,7 @@ impl MockEnvBuilder { &oracle::InstantiateMsg { owner: self.owner.to_string(), base_denom: self.base_denom.clone(), + pyth_contract_addr: self.pyth_contract_addr.clone(), }, &[], "oracle", diff --git a/packages/testing/src/lib.rs b/packages/testing/src/lib.rs index f8ec93050..4183321d2 100644 --- a/packages/testing/src/lib.rs +++ b/packages/testing/src/lib.rs @@ -10,6 +10,7 @@ mod mock_address_provider; mod mocks; mod oracle_querier; mod osmosis_querier; +mod pyth_querier; mod red_bank_querier; pub use helpers::*; diff --git a/packages/testing/src/mars_mock_querier.rs b/packages/testing/src/mars_mock_querier.rs index 79894e8e4..563b34a0e 100644 --- a/packages/testing/src/mars_mock_querier.rs +++ b/packages/testing/src/mars_mock_querier.rs @@ -12,12 +12,14 @@ use osmosis_std::types::osmosis::{ gamm::v2::QuerySpotPriceResponse, twap::v1beta1::{ArithmeticTwapToNowResponse, GeometricTwapToNowResponse}, }; +use pyth_sdk_cw::{PriceFeedResponse, PriceIdentifier}; use crate::{ incentives_querier::IncentivesQuerier, mock_address_provider, oracle_querier::OracleQuerier, osmosis_querier::{OsmosisQuerier, PriceKey}, + pyth_querier::PythQuerier, red_bank_querier::RedBankQuerier, }; @@ -26,6 +28,7 @@ pub struct MarsMockQuerier { oracle_querier: OracleQuerier, incentives_querier: IncentivesQuerier, osmosis_querier: OsmosisQuerier, + pyth_querier: PythQuerier, redbank_querier: RedBankQuerier, } @@ -52,6 +55,7 @@ impl MarsMockQuerier { oracle_querier: OracleQuerier::default(), incentives_querier: IncentivesQuerier::default(), osmosis_querier: OsmosisQuerier::default(), + pyth_querier: PythQuerier::default(), redbank_querier: RedBankQuerier::default(), } } @@ -134,6 +138,10 @@ impl MarsMockQuerier { ); } + pub fn set_pyth_price(&mut self, id: PriceIdentifier, price: PriceFeedResponse) { + self.pyth_querier.prices.insert(id, price); + } + pub fn set_redbank_market(&mut self, market: red_bank::Market) { self.redbank_querier.markets.insert(market.denom.clone(), market); } @@ -186,6 +194,11 @@ impl MarsMockQuerier { return self.incentives_querier.handle_query(&contract_addr, incentives_query); } + // Pyth Queries + if let Ok(pyth_query) = from_binary::(msg) { + return self.pyth_querier.handle_query(&contract_addr, pyth_query); + } + // RedBank Queries if let Ok(redbank_query) = from_binary::(msg) { return self.redbank_querier.handle_query(redbank_query); diff --git a/packages/testing/src/pyth_querier.rs b/packages/testing/src/pyth_querier.rs new file mode 100644 index 000000000..2e3cd277f --- /dev/null +++ b/packages/testing/src/pyth_querier.rs @@ -0,0 +1,31 @@ +use std::collections::HashMap; + +use cosmwasm_std::{to_binary, Addr, Binary, ContractResult, QuerierResult}; +use pyth_sdk_cw::{PriceFeedResponse, PriceIdentifier, QueryMsg}; + +#[derive(Default)] +pub struct PythQuerier { + pub prices: HashMap, +} + +impl PythQuerier { + pub fn handle_query(&self, _contract_addr: &Addr, query: QueryMsg) -> QuerierResult { + let res: ContractResult = match query { + QueryMsg::PriceFeed { + id, + } => { + let option_price = self.prices.get(&id); + + if let Some(price) = option_price { + to_binary(price).into() + } else { + Err(format!("[mock]: could not find Pyth price for {id}")).into() + } + } + + _ => Err("[mock]: Unsupported Pyth query").into(), + }; + + Ok(res).into() + } +} diff --git a/packages/types/src/oracle.rs b/packages/types/src/oracle.rs index d5b9d02cc..330f253e6 100644 --- a/packages/types/src/oracle.rs +++ b/packages/types/src/oracle.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::Decimal; +use cosmwasm_std::{Addr, Decimal}; use mars_owner::OwnerUpdate; #[cw_serde] @@ -8,6 +8,8 @@ pub struct InstantiateMsg { pub owner: String, /// The asset in which prices are denominated in pub base_denom: String, + /// Contract address of Pyth + pub pyth_contract_addr: String, } #[cw_serde] @@ -16,6 +18,12 @@ pub struct Config { pub base_denom: String, } +#[cw_serde] +pub struct PythConfig { + /// Contract address of Pyth + pub pyth_contract_addr: Addr, +} + #[cw_serde] pub enum ExecuteMsg { /// Specify the price source to be used for a coin @@ -31,6 +39,12 @@ pub enum ExecuteMsg { }, /// Manages admin role state UpdateOwner(OwnerUpdate), + + /// Update contract config (only callable by owner) + UpdateConfig { + base_denom: Option, + pyth_contract_addr: Option, + }, } #[cw_serde] @@ -81,6 +95,8 @@ pub struct ConfigResponse { pub proposed_new_owner: Option, /// The asset in which prices are denominated in pub base_denom: String, + /// Contract address of Pyth + pub pyth_contract_addr: String, } #[cw_serde] diff --git a/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json b/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json index 01b6a5527..26d47f341 100644 --- a/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json +++ b/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json @@ -8,7 +8,8 @@ "type": "object", "required": [ "base_denom", - "owner" + "owner", + "pyth_contract_addr" ], "properties": { "base_denom": { @@ -18,6 +19,10 @@ "owner": { "description": "The contract's owner, who can update config and price sources", "type": "string" + }, + "pyth_contract_addr": { + "description": "Contract address of Pyth", + "type": "string" } }, "additionalProperties": false @@ -86,6 +91,34 @@ } }, "additionalProperties": false + }, + { + "description": "Update contract config (only callable by owner)", + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "properties": { + "base_denom": { + "type": [ + "string", + "null" + ] + }, + "pyth_contract_addr": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": { @@ -147,6 +180,9 @@ } } }, + "Identifier": { + "type": "string" + }, "OsmosisPriceSource": { "oneOf": [ { @@ -344,6 +380,56 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "pyth" + ], + "properties": { + "pyth": { + "type": "object", + "required": [ + "max_confidence", + "max_deviation", + "max_staleness", + "price_feed_id" + ], + "properties": { + "max_confidence": { + "description": "The maximum confidence deviation allowed for an oracle price.\n\nThe confidence is measured as the percent of the confidence interval value provided by the oracle as compared to the weighted average value of the price.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "max_deviation": { + "description": "The maximum deviation (percentage) between current and EMA price", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "max_staleness": { + "description": "The maximum number of seconds since the last price was by an oracle, before rejecting the price as too stale", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "price_feed_id": { + "description": "Price feed id of an asset from the list: https://pyth.network/developers/price-feed-ids", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + } + } + } + }, + "additionalProperties": false } ] }, @@ -557,7 +643,8 @@ "title": "ConfigResponse", "type": "object", "required": [ - "base_denom" + "base_denom", + "pyth_contract_addr" ], "properties": { "base_denom": { @@ -577,6 +664,10 @@ "string", "null" ] + }, + "pyth_contract_addr": { + "description": "Contract address of Pyth", + "type": "string" } }, "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 a457d942b..bbbdea0a6 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts @@ -13,6 +13,7 @@ import { OsmosisPriceSource, Decimal, Downtime, + Identifier, OwnerUpdate, DowntimeDetector, QueryMsg, @@ -135,6 +136,18 @@ export interface MarsOracleOsmosisInterface extends MarsOracleOsmosisReadOnlyInt memo?: string, _funds?: Coin[], ) => Promise + updateConfig: ( + { + baseDenom, + pythContractAddr, + }: { + baseDenom?: string + pythContractAddr?: string + }, + fee?: number | StdFee | 'auto', + memo?: string, + funds?: Coin[], + ) => Promise } export class MarsOracleOsmosisClient extends MarsOracleOsmosisQueryClient @@ -152,6 +165,7 @@ export class MarsOracleOsmosisClient this.setPriceSource = this.setPriceSource.bind(this) this.removePriceSource = this.removePriceSource.bind(this) this.updateOwner = this.updateOwner.bind(this) + this.updateConfig = this.updateConfig.bind(this) } setPriceSource = async ( @@ -220,4 +234,30 @@ export class MarsOracleOsmosisClient _funds, ) } + updateConfig = async ( + { + baseDenom, + pythContractAddr, + }: { + baseDenom?: string + pythContractAddr?: string + }, + fee: number | StdFee | 'auto' = 'auto', + memo?: string, + funds?: Coin[], + ): Promise => { + return await this.client.execute( + this.sender, + this.contractAddress, + { + update_config: { + base_denom: baseDenom, + pyth_contract_addr: pythContractAddr, + }, + }, + fee, + memo, + funds, + ) + } } 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 32e75cf64..151fae53e 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.react-query.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.react-query.ts @@ -14,6 +14,7 @@ import { OsmosisPriceSource, Decimal, Downtime, + Identifier, OwnerUpdate, DowntimeDetector, QueryMsg, @@ -164,6 +165,30 @@ export function useMarsOracleOsmosisConfigQuery({ { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, ) } +export interface MarsOracleOsmosisUpdateConfigMutation { + client: MarsOracleOsmosisClient + msg: { + baseDenom?: string + pythContractAddr?: string + } + args?: { + fee?: number | StdFee | 'auto' + memo?: string + funds?: Coin[] + } +} +export function useMarsOracleOsmosisUpdateConfigMutation( + options?: Omit< + UseMutationOptions, + 'mutationFn' + >, +) { + return useMutation( + ({ client, msg, args: { fee, memo, funds } = {} }) => + client.updateConfig(msg, fee, memo, funds), + options, + ) +} export interface MarsOracleOsmosisUpdateOwnerMutation { client: MarsOracleOsmosisClient msg: OwnerUpdate diff --git a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts index 3203abe77..2984f772b 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts @@ -8,6 +8,7 @@ export interface InstantiateMsg { base_denom: string owner: string + pyth_contract_addr: string } export type ExecuteMsg = | { @@ -24,6 +25,12 @@ export type ExecuteMsg = | { update_owner: OwnerUpdate } + | { + update_config: { + base_denom?: string | null + pyth_contract_addr?: string | null + } + } export type OsmosisPriceSource = | { fixed: { @@ -68,6 +75,15 @@ export type OsmosisPriceSource = [k: string]: unknown } } + | { + pyth: { + max_confidence: Decimal + max_deviation: Decimal + max_staleness: number + price_feed_id: Identifier + [k: string]: unknown + } + } export type Decimal = string export type Downtime = | 'duration30s' @@ -95,6 +111,7 @@ export type Downtime = | 'duration24h' | 'duration36h' | 'duration48h' +export type Identifier = string export type OwnerUpdate = | { propose_new_owner: { @@ -145,6 +162,7 @@ export interface ConfigResponse { base_denom: string owner?: string | null proposed_new_owner?: string | null + pyth_contract_addr: string } export interface PriceResponse { denom: string From a1e5f08cab953709d1389fae21fa5c2241b2a954 Mon Sep 17 00:00:00 2001 From: Piotr Babel Date: Wed, 29 Mar 2023 00:27:57 +0200 Subject: [PATCH 02/11] MP-2475. Pyth phase 1 aligment. --- contracts/oracle/osmosis/src/helpers.rs | 20 -- contracts/oracle/osmosis/src/price_source.rs | 76 +------ contracts/oracle/osmosis/tests/helpers.rs | 4 +- .../oracle/osmosis/tests/test_query_price.rs | 203 ------------------ .../osmosis/tests/test_set_price_source.rs | 47 ---- .../mars-oracle-osmosis.json | 18 -- .../MarsOracleOsmosis.types.ts | 2 - 7 files changed, 9 insertions(+), 361 deletions(-) diff --git a/contracts/oracle/osmosis/src/helpers.rs b/contracts/oracle/osmosis/src/helpers.rs index f499ee2b3..342c9e2ce 100644 --- a/contracts/oracle/osmosis/src/helpers.rs +++ b/contracts/oracle/osmosis/src/helpers.rs @@ -1,4 +1,3 @@ -use cosmwasm_std::Decimal; use mars_oracle_base::{ContractError, ContractResult}; use mars_osmosis::helpers::{has_denom, Pool}; @@ -72,22 +71,3 @@ pub fn assert_osmosis_twap( Ok(()) } - -/// Assert Pyth configuration -pub fn assert_pyth(max_confidence: Decimal, max_deviation: Decimal) -> ContractResult<()> { - // TODO: update validation once confirmed with Risk team - if !max_confidence.le(&Decimal::one()) { - return Err(ContractError::InvalidPriceSource { - reason: "max_confidence must be in the range of <0;1>".to_string(), - }); - } - - // TODO: update validation once confirmed with Risk team - if !max_deviation.le(&Decimal::one()) { - return Err(ContractError::InvalidPriceSource { - reason: "max_deviation must be in the range of <0;1>".to_string(), - }); - } - - Ok(()) -} diff --git a/contracts/oracle/osmosis/src/price_source.rs b/contracts/oracle/osmosis/src/price_source.rs index 57f80471e..afc279d5e 100644 --- a/contracts/oracle/osmosis/src/price_source.rs +++ b/contracts/oracle/osmosis/src/price_source.rs @@ -158,16 +158,6 @@ pub enum OsmosisPriceSource { /// The maximum number of seconds since the last price was by an oracle, before /// rejecting the price as too stale max_staleness: u64, - - /// 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 - /// of the price. - max_confidence: Decimal, - - /// The maximum deviation (percentage) between current and EMA price - max_deviation: Decimal, }, } @@ -211,10 +201,8 @@ impl fmt::Display for OsmosisPriceSource { OsmosisPriceSource::Pyth { price_feed_id, max_staleness, - max_confidence, - max_deviation, } => { - format!("pyth:{price_feed_id}:{max_staleness}:{max_confidence}:{max_deviation}") + format!("pyth:{price_feed_id}:{max_staleness}") } }; write!(f, "{label}") @@ -273,10 +261,8 @@ impl PriceSource for OsmosisPriceSource { helpers::assert_osmosis_twap(*window_size, downtime_detector) } OsmosisPriceSource::Pyth { - max_confidence, - max_deviation, .. - } => helpers::assert_pyth(*max_confidence, *max_deviation), + } => Ok(()), } } @@ -351,17 +337,9 @@ impl PriceSource for OsmosisPriceSource { OsmosisPriceSource::Pyth { price_feed_id, max_staleness, - max_confidence, - max_deviation, - } => Ok(Self::query_pyth_price( - deps, - env, - *price_feed_id, - *max_staleness, - *max_confidence, - *max_deviation, - pyth_config, - )?), + } => { + Ok(Self::query_pyth_price(deps, env, *price_feed_id, *max_staleness, pyth_config)?) + } } } } @@ -479,8 +457,6 @@ impl OsmosisPriceSource { env: &Env, price_feed_id: PriceIdentifier, max_staleness: u64, - max_confidence: Decimal, - max_deviation: Decimal, pyth_config: &PythConfig, ) -> ContractResult { let current_time = env.block.time.seconds(); @@ -502,48 +478,14 @@ impl OsmosisPriceSource { }); } - // Get an exponentially-weighted moving average price and confidence interval - let ema_price = price_feed.get_ema_price_unchecked(); - - // Check if the EMA price is not too old - if (current_time - ema_price.publish_time as u64) > max_staleness { - return Err(InvalidPrice { - reason: format!( - "EMA price publish time is too old/stale. published: {}, now: {}", - ema_price.publish_time, current_time - ), - }); - } - - // Check if the current and EMA price is > 0 - if current_price.price <= 0 || ema_price.price <= 0 { + // Check if the current price is > 0 + if current_price.price <= 0 { return Err(InvalidPrice { reason: "price can't be <= 0".to_string(), }); } let current_price_dec = scale_to_exponent(current_price.price as u128, current_price.expo)?; - let ema_price_dec = scale_to_exponent(ema_price.price as u128, ema_price.expo)?; - - // Check confidence deviation - let confidence = scale_to_exponent(current_price.conf as u128, current_price.expo)?; - let price_confidence = confidence.checked_div(ema_price_dec)?; - if price_confidence > max_confidence { - return Err(InvalidPrice { - reason: format!("price confidence deviation {price_confidence} exceeds max allowed {max_confidence}") - }); - } - - // Check price deviation - let delta = current_price_dec.abs_diff(ema_price_dec); - let price_deviation = delta.checked_div(ema_price_dec)?; - if price_deviation > max_deviation { - return Err(InvalidPrice { - reason: format!( - "price deviation {price_deviation} exceeds max allowed {max_deviation}" - ), - }); - } Ok(current_price_dec) } @@ -678,12 +620,10 @@ mod tests { ) .unwrap(), max_staleness: 60, - max_confidence: Decimal::from_ratio(5u128, 100u128), - max_deviation: Decimal::from_ratio(6u128, 100u128), }; assert_eq!( ps.to_string(), - "pyth:0x61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3:60:0.05:0.06" + "pyth:0x61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3:60" ) } } diff --git a/contracts/oracle/osmosis/tests/helpers.rs b/contracts/oracle/osmosis/tests/helpers.rs index 896bd5083..4c4b6983f 100644 --- a/contracts/oracle/osmosis/tests/helpers.rs +++ b/contracts/oracle/osmosis/tests/helpers.rs @@ -5,7 +5,7 @@ use std::marker::PhantomData; use cosmwasm_std::{ coin, from_binary, testing::{mock_env, MockApi, MockQuerier, MockStorage}, - Coin, Decimal, Deps, DepsMut, OwnedDeps, + Coin, Deps, DepsMut, OwnedDeps, }; use mars_oracle_base::ContractError; use mars_oracle_osmosis::{contract::entry, msg::ExecuteMsg, OsmosisPriceSource}; @@ -147,8 +147,6 @@ pub fn set_pyth_price_source(deps: DepsMut, denom: &str, price_id: PriceIdentifi OsmosisPriceSource::Pyth { price_feed_id: price_id, max_staleness: 30, - max_confidence: Decimal::from_ratio(5u128, 100u128), - max_deviation: Decimal::from_ratio(6u128, 100u128), }, ) } diff --git a/contracts/oracle/osmosis/tests/test_query_price.rs b/contracts/oracle/osmosis/tests/test_query_price.rs index 13ea88e43..9226dad1e 100644 --- a/contracts/oracle/osmosis/tests/test_query_price.rs +++ b/contracts/oracle/osmosis/tests/test_query_price.rs @@ -522,8 +522,6 @@ fn querying_pyth_price_if_publish_price_too_old() { OsmosisPriceSource::Pyth { price_feed_id: price_id, max_staleness, - max_confidence: Decimal::from_ratio(5u128, 100u128), - max_deviation: Decimal::from_ratio(6u128, 100u128), }, ); @@ -566,46 +564,6 @@ fn querying_pyth_price_if_publish_price_too_old() { .to_string() } ); - - let ema_price_publish_time = 1677157333u64; - let price_publish_time = ema_price_publish_time + max_staleness; - deps.querier.set_pyth_price( - price_id, - PriceFeedResponse { - price_feed: PriceFeed::new( - price_id, - Price { - price: 1371155677, - conf: 646723, - expo: -8, - publish_time: price_publish_time as i64, - }, - Price { - price: 1365133270, - conf: 574566, - expo: -8, - publish_time: ema_price_publish_time as i64, - }, - ), - }, - ); - - let res_err = entry::query( - deps.as_ref(), - mock_env_at_block_time(ema_price_publish_time + max_staleness + 1u64), - QueryMsg::Price { - denom: "uatom".to_string(), - }, - ) - .unwrap_err(); - assert_eq!( - res_err, - ContractError::InvalidPrice { - reason: - "EMA price publish time is too old/stale. published: 1677157333, now: 1677157364" - .to_string() - } - ); } #[test] @@ -624,8 +582,6 @@ fn querying_pyth_price_if_signed() { OsmosisPriceSource::Pyth { price_feed_id: price_id, max_staleness, - max_confidence: Decimal::from_ratio(5u128, 100u128), - max_deviation: Decimal::from_ratio(6u128, 100u128), }, ); @@ -667,163 +623,6 @@ fn querying_pyth_price_if_signed() { ); } -#[test] -fn querying_pyth_price_if_confidence_exceeded() { - let mut deps = helpers::setup_test(); - - let price_id = PriceIdentifier::from_hex( - "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", - ) - .unwrap(); - - let max_staleness = 30u64; - helpers::set_price_source( - deps.as_mut(), - "uatom", - OsmosisPriceSource::Pyth { - price_feed_id: price_id, - max_staleness, - max_confidence: Decimal::from_ratio(5u128, 100u128), - max_deviation: Decimal::from_ratio(6u128, 100u128), - }, - ); - - let publish_time = 1677157333u64; - deps.querier.set_pyth_price( - price_id, - PriceFeedResponse { - price_feed: PriceFeed::new( - price_id, - Price { - price: 1010000, - conf: 51000, - expo: -4, - publish_time: publish_time as i64, - }, - Price { - price: 1000000, - conf: 40000, - expo: -4, - publish_time: publish_time as i64, - }, - ), - }, - ); - - let res_err = entry::query( - deps.as_ref(), - mock_env_at_block_time(publish_time), - QueryMsg::Price { - denom: "uatom".to_string(), - }, - ) - .unwrap_err(); - assert_eq!( - res_err, - ContractError::InvalidPrice { - reason: "price confidence deviation 0.051 exceeds max allowed 0.05".to_string() - } - ); -} - -#[test] -fn querying_pyth_price_if_deviation_exceeded() { - let mut deps = helpers::setup_test(); - - let price_id = PriceIdentifier::from_hex( - "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", - ) - .unwrap(); - - let max_staleness = 30u64; - helpers::set_price_source( - deps.as_mut(), - "uatom", - OsmosisPriceSource::Pyth { - price_feed_id: price_id, - max_staleness, - max_confidence: Decimal::from_ratio(5u128, 100u128), - max_deviation: Decimal::from_ratio(6u128, 100u128), - }, - ); - - let publish_time = 1677157333u64; - - // price > ema_price - deps.querier.set_pyth_price( - price_id, - PriceFeedResponse { - price_feed: PriceFeed::new( - price_id, - Price { - price: 1061000, - conf: 50000, - expo: -4, - publish_time: publish_time as i64, - }, - Price { - price: 1000000, - conf: 40000, - expo: -4, - publish_time: publish_time as i64, - }, - ), - }, - ); - - let res_err = entry::query( - deps.as_ref(), - mock_env_at_block_time(publish_time), - QueryMsg::Price { - denom: "uatom".to_string(), - }, - ) - .unwrap_err(); - assert_eq!( - res_err, - ContractError::InvalidPrice { - reason: "price deviation 0.061 exceeds max allowed 0.06".to_string() - } - ); - - // ema_price > price - deps.querier.set_pyth_price( - price_id, - PriceFeedResponse { - price_feed: PriceFeed::new( - price_id, - Price { - price: 939999, - conf: 50000, - expo: -4, - publish_time: publish_time as i64, - }, - Price { - price: 1000000, - conf: 40000, - expo: -4, - publish_time: publish_time as i64, - }, - ), - }, - ); - - let res_err = entry::query( - deps.as_ref(), - mock_env_at_block_time(publish_time), - QueryMsg::Price { - denom: "uatom".to_string(), - }, - ) - .unwrap_err(); - assert_eq!( - res_err, - ContractError::InvalidPrice { - reason: "price deviation 0.060001 exceeds max allowed 0.06".to_string() - } - ); -} - #[test] fn querying_pyth_price_successfully() { let mut deps = helpers::setup_test(); @@ -840,8 +639,6 @@ fn querying_pyth_price_successfully() { OsmosisPriceSource::Pyth { price_feed_id: price_id, max_staleness, - max_confidence: Decimal::from_ratio(5u128, 100u128), - max_deviation: Decimal::from_ratio(6u128, 100u128), }, ); diff --git a/contracts/oracle/osmosis/tests/test_set_price_source.rs b/contracts/oracle/osmosis/tests/test_set_price_source.rs index f27510c4b..5cbca7125 100644 --- a/contracts/oracle/osmosis/tests/test_set_price_source.rs +++ b/contracts/oracle/osmosis/tests/test_set_price_source.rs @@ -744,49 +744,6 @@ fn setting_price_source_xyk_lp() { ); } -#[test] -fn setting_price_source_pyth_with_invalid_params() { - let mut deps = helpers::setup_test(); - - let mut set_price_source_pyth = |max_confidence: Decimal, max_deviation: Decimal| { - execute( - deps.as_mut(), - mock_env(), - mock_info("owner"), - ExecuteMsg::SetPriceSource { - denom: "uatom".to_string(), - price_source: OsmosisPriceSource::Pyth { - price_feed_id: PriceIdentifier::from_hex( - "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", - ) - .unwrap(), - max_staleness: 30, - max_confidence, - max_deviation, - }, - }, - ) - }; - - // attempting to set max_confidence > 100%; should fail - let err = set_price_source_pyth(Decimal::percent(101), Decimal::percent(5)).unwrap_err(); - assert_eq!( - err, - ContractError::InvalidPriceSource { - reason: "max_confidence must be in the range of <0;1>".to_string() - } - ); - - // attempting to set max_deviation > 100%; should fail - let err = set_price_source_pyth(Decimal::percent(5), Decimal::percent(101)).unwrap_err(); - assert_eq!( - err, - ContractError::InvalidPriceSource { - reason: "max_deviation must be in the range of <0;1>".to_string() - } - ); -} - #[test] fn setting_price_source_pyth_successfully() { let mut deps = helpers::setup_test(); @@ -803,8 +760,6 @@ fn setting_price_source_pyth_successfully() { ) .unwrap(), max_staleness: 30, - max_confidence: Decimal::percent(5), - max_deviation: Decimal::percent(6), }, }, ) @@ -825,8 +780,6 @@ fn setting_price_source_pyth_successfully() { ) .unwrap(), max_staleness: 30, - max_confidence: Decimal::percent(5), - max_deviation: Decimal::percent(6), }, ); } diff --git a/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json b/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json index 26d47f341..51b65f8ba 100644 --- a/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json +++ b/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json @@ -390,28 +390,10 @@ "pyth": { "type": "object", "required": [ - "max_confidence", - "max_deviation", "max_staleness", "price_feed_id" ], "properties": { - "max_confidence": { - "description": "The maximum confidence deviation allowed for an oracle price.\n\nThe confidence is measured as the percent of the confidence interval value provided by the oracle as compared to the weighted average value of the price.", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, - "max_deviation": { - "description": "The maximum deviation (percentage) between current and EMA price", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, "max_staleness": { "description": "The maximum number of seconds since the last price was by an oracle, before rejecting the price as too stale", "type": "integer", diff --git a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts index 2984f772b..6ed5c3292 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts @@ -77,8 +77,6 @@ export type OsmosisPriceSource = } | { pyth: { - max_confidence: Decimal - max_deviation: Decimal max_staleness: number price_feed_id: Identifier [k: string]: unknown From 9ff2713a12131cc09399c9b9ff60a4bfe1fe78e5 Mon Sep 17 00:00:00 2001 From: piobab Date: Thu, 6 Apr 2023 10:14:08 +0200 Subject: [PATCH 03/11] Implementation for LSD v1 (#190) * Add LSD price source. * Split PriceSource to Unchecked and Checked. * LSD fmt. * Query LSD price. * Fix tests. * Add tests for LSD. * Update schema. * Apply comments from review. --- contracts/oracle/base/src/contract.rs | 26 +- contracts/oracle/base/src/traits.rs | 20 +- contracts/oracle/osmosis/examples/schema.rs | 4 +- contracts/oracle/osmosis/src/contract.rs | 7 +- contracts/oracle/osmosis/src/lib.rs | 6 +- contracts/oracle/osmosis/src/msg.rs | 6 +- contracts/oracle/osmosis/src/price_source.rs | 346 +++++++++++++--- contracts/oracle/osmosis/src/stride.rs | 46 +++ contracts/oracle/osmosis/tests/helpers.rs | 6 +- .../oracle/osmosis/tests/test_query_price.rs | 375 ++++++++++++++++-- .../osmosis/tests/test_remove_price_source.rs | 8 +- .../osmosis/tests/test_set_price_source.rs | 273 +++++++++++-- integration-tests/tests/test_oracles.rs | 53 +-- packages/testing/src/integration/mock_env.rs | 4 +- packages/testing/src/lib.rs | 1 + packages/testing/src/mars_mock_querier.rs | 27 +- .../testing/src/redemption_rate_querier.rs | 29 ++ .../mars-oracle-osmosis.json | 95 ++++- .../MarsOracleOsmosis.client.ts | 8 +- .../MarsOracleOsmosis.react-query.ts | 6 +- .../MarsOracleOsmosis.types.ts | 23 +- 21 files changed, 1181 insertions(+), 188 deletions(-) create mode 100644 contracts/oracle/osmosis/src/stride.rs create mode 100644 packages/testing/src/redemption_rate_querier.rs diff --git a/contracts/oracle/base/src/contract.rs b/contracts/oracle/base/src/contract.rs index d504f33eb..204510088 100644 --- a/contracts/oracle/base/src/contract.rs +++ b/contracts/oracle/base/src/contract.rs @@ -12,14 +12,15 @@ use mars_red_bank_types::oracle::{ }; use mars_utils::helpers::{option_string_to_addr, validate_native_denom}; -use crate::{error::ContractResult, PriceSource}; +use crate::{error::ContractResult, PriceSourceChecked, PriceSourceUnchecked}; const DEFAULT_LIMIT: u32 = 10; const MAX_LIMIT: u32 = 30; -pub struct OracleBase<'a, P, C> +pub struct OracleBase<'a, P, PU, C> where - P: PriceSource, + P: PriceSourceChecked, + PU: PriceSourceUnchecked, C: CustomQuery, { /// Contract's owner @@ -30,13 +31,16 @@ where pub pyth_config: Item<'a, PythConfig>, /// The price source of each coin denom pub price_sources: Map<'a, &'a str, P>, + /// Phantom data holds the unchecked price source type + pub unchecked_price_source: PhantomData, /// Phantom data holds the custom query type pub custom_query: PhantomData, } -impl<'a, P, C> Default for OracleBase<'a, P, C> +impl<'a, P, PU, C> Default for OracleBase<'a, P, PU, C> where - P: PriceSource, + P: PriceSourceChecked, + PU: PriceSourceUnchecked, C: CustomQuery, { fn default() -> Self { @@ -45,14 +49,16 @@ where config: Item::new("config"), pyth_config: Item::new("pyth_config"), price_sources: Map::new("price_sources"), + unchecked_price_source: PhantomData, custom_query: PhantomData, } } } -impl<'a, P, C> OracleBase<'a, P, C> +impl<'a, P, PU, C> OracleBase<'a, P, PU, C> where - P: PriceSource, + P: PriceSourceChecked, + PU: PriceSourceUnchecked, C: CustomQuery, { pub fn instantiate(&self, deps: DepsMut, msg: InstantiateMsg) -> ContractResult { @@ -87,7 +93,7 @@ where &self, deps: DepsMut, info: MessageInfo, - msg: ExecuteMsg

, + msg: ExecuteMsg, ) -> ContractResult { match msg { ExecuteMsg::UpdateOwner(update) => self.update_owner(deps, info, update), @@ -140,14 +146,14 @@ where deps: DepsMut, sender_addr: Addr, denom: String, - price_source: P, + price_source: PU, ) -> ContractResult { self.owner.assert_owner(deps.storage, &sender_addr)?; validate_native_denom(&denom)?; let cfg = self.config.load(deps.storage)?; - price_source.validate(&deps.querier, &denom, &cfg.base_denom)?; + let price_source = price_source.validate(deps.as_ref(), &denom, &cfg.base_denom)?; self.price_sources.save(deps.storage, &denom, &price_source)?; Ok(Response::new() diff --git a/contracts/oracle/base/src/traits.rs b/contracts/oracle/base/src/traits.rs index 3e52a427c..716a7083c 100644 --- a/contracts/oracle/base/src/traits.rs +++ b/contracts/oracle/base/src/traits.rs @@ -1,6 +1,6 @@ use std::fmt::{Debug, Display}; -use cosmwasm_std::{CustomQuery, Decimal, Deps, Env, QuerierWrapper}; +use cosmwasm_std::{CustomQuery, Decimal, Deps, Env}; use cw_storage_plus::Map; use mars_red_bank_types::oracle::PythConfig; use schemars::JsonSchema; @@ -8,19 +8,21 @@ use serde::{de::DeserializeOwned, Serialize}; use crate::ContractResult; -pub trait PriceSource: - Serialize + DeserializeOwned + Clone + Debug + Display + PartialEq + JsonSchema +pub trait PriceSourceUnchecked: + Serialize + DeserializeOwned + Clone + Debug + PartialEq + JsonSchema where + P: PriceSourceChecked, C: CustomQuery, { /// Validate whether the price source is valid for a given denom - fn validate( - &self, - querier: &QuerierWrapper, - denom: &str, - base_denom: &str, - ) -> ContractResult<()>; + fn validate(self, deps: Deps, denom: &str, base_denom: &str) -> ContractResult

; +} +pub trait PriceSourceChecked: + Serialize + DeserializeOwned + Clone + Debug + Display + PartialEq + JsonSchema +where + C: CustomQuery, +{ /// Query the price of an asset based on the given price source /// /// Notable arguments: diff --git a/contracts/oracle/osmosis/examples/schema.rs b/contracts/oracle/osmosis/examples/schema.rs index 678ce550e..0ba057079 100644 --- a/contracts/oracle/osmosis/examples/schema.rs +++ b/contracts/oracle/osmosis/examples/schema.rs @@ -1,11 +1,11 @@ use cosmwasm_schema::write_api; -use mars_oracle_osmosis::OsmosisPriceSource; +use mars_oracle_osmosis::OsmosisPriceSourceUnchecked; use mars_red_bank_types::oracle::{ExecuteMsg, InstantiateMsg, QueryMsg}; fn main() { write_api! { instantiate: InstantiateMsg, - execute: ExecuteMsg, + execute: ExecuteMsg, query: QueryMsg, } } diff --git a/contracts/oracle/osmosis/src/contract.rs b/contracts/oracle/osmosis/src/contract.rs index 22192ecb8..edf584b39 100644 --- a/contracts/oracle/osmosis/src/contract.rs +++ b/contracts/oracle/osmosis/src/contract.rs @@ -1,11 +1,12 @@ use cosmwasm_std::Empty; use mars_oracle_base::OracleBase; -use crate::OsmosisPriceSource; +use crate::price_source::{OsmosisPriceSourceChecked, OsmosisPriceSourceUnchecked}; /// The Osmosis oracle contract inherits logics from the base oracle contract, with the Osmosis query /// and price source plugins -pub type OsmosisOracle<'a> = OracleBase<'a, OsmosisPriceSource, Empty>; +pub type OsmosisOracle<'a> = + OracleBase<'a, OsmosisPriceSourceChecked, OsmosisPriceSourceUnchecked, Empty>; pub const CONTRACT_NAME: &str = "crates.io:mars-oracle-osmosis"; pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -35,7 +36,7 @@ pub mod entry { deps: DepsMut, _env: Env, info: MessageInfo, - msg: ExecuteMsg, + msg: ExecuteMsg, ) -> ContractResult { OsmosisOracle::default().execute(deps, info, msg) } diff --git a/contracts/oracle/osmosis/src/lib.rs b/contracts/oracle/osmosis/src/lib.rs index d0f1c1fc4..a81f5fd97 100644 --- a/contracts/oracle/osmosis/src/lib.rs +++ b/contracts/oracle/osmosis/src/lib.rs @@ -3,5 +3,9 @@ mod helpers; mod migrations; pub mod msg; mod price_source; +pub mod stride; -pub use price_source::{Downtime, DowntimeDetector, OsmosisPriceSource}; +pub use price_source::{ + scale_to_exponent, Downtime, DowntimeDetector, GeometricTwap, OsmosisPriceSourceChecked, + OsmosisPriceSourceUnchecked, RedemptionRate, +}; diff --git a/contracts/oracle/osmosis/src/msg.rs b/contracts/oracle/osmosis/src/msg.rs index 8067eb4e3..0abea7564 100644 --- a/contracts/oracle/osmosis/src/msg.rs +++ b/contracts/oracle/osmosis/src/msg.rs @@ -1,6 +1,6 @@ use mars_red_bank_types::oracle; -use crate::OsmosisPriceSource; +use crate::price_source::{OsmosisPriceSourceChecked, OsmosisPriceSourceUnchecked}; -pub type ExecuteMsg = oracle::ExecuteMsg; -pub type PriceSourceResponse = oracle::PriceSourceResponse; +pub type ExecuteMsg = oracle::ExecuteMsg; +pub type PriceSourceResponse = oracle::PriceSourceResponse; diff --git a/contracts/oracle/osmosis/src/price_source.rs b/contracts/oracle/osmosis/src/price_source.rs index afc279d5e..bc9492ef9 100644 --- a/contracts/oracle/osmosis/src/price_source.rs +++ b/contracts/oracle/osmosis/src/price_source.rs @@ -1,10 +1,10 @@ -use std::fmt; +use std::{cmp::min, fmt}; -use cosmwasm_std::{ - Decimal, Decimal256, Deps, Empty, Env, Isqrt, QuerierWrapper, Uint128, Uint256, -}; +use cosmwasm_std::{Addr, Decimal, Decimal256, Deps, Empty, Env, Isqrt, Uint128, Uint256}; use cw_storage_plus::Map; -use mars_oracle_base::{ContractError::InvalidPrice, ContractResult, PriceSource}; +use mars_oracle_base::{ + ContractError::InvalidPrice, ContractResult, PriceSourceChecked, PriceSourceUnchecked, +}; use mars_osmosis::helpers::{ query_arithmetic_twap_price, query_geometric_twap_price, query_pool, query_spot_price, recovered_since_downtime_of_length, Pool, @@ -14,7 +14,7 @@ use pyth_sdk_cw::{query_price_feed, PriceIdentifier}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::helpers; +use crate::{helpers, stride::query_redemption_rate}; /// Copied from https://github.com/osmosis-labs/osmosis-rust/blob/main/packages/osmosis-std/src/types/osmosis/downtimedetector/v1beta1.rs#L4 /// @@ -82,7 +82,7 @@ impl fmt::Display for DowntimeDetector { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub enum OsmosisPriceSource { +pub enum OsmosisPriceSource { /// Returns a fixed value; Fixed { price: Decimal, @@ -159,9 +159,63 @@ pub enum OsmosisPriceSource { /// rejecting the price as too stale max_staleness: u64, }, + /// Liquid Staking Derivatives (LSD) price quoted in USD based on data from Pyth, Osmosis and Stride. + /// + /// Equation to calculate the price: + /// stAsset/USD = stAsset/Asset * Asset/USD + /// where: + /// stAsset/Asset = min(stAsset/Asset Geometric TWAP, stAsset/Asset Redemption Rate) + /// + /// Example: + /// stATOM/USD = stATOM/ATOM * ATOM/USD + /// where: + /// - stATOM/ATOM = min(stAtom/Atom Geometric TWAP from Osmosis, stAtom/Atom Redemption Rate from Stride) + /// - ATOM/USD price comes from the Mars Oracle contract (should point to Pyth). + /// + /// NOTE: `pool_id` must point to stAsset/Asset Osmosis pool. + /// Asset/USD price source should be available in the Mars Oracle contract. + Lsd { + /// Transitive denom for which we query price in USD. It refers to 'Asset' in the equation: + /// stAsset/USD = stAsset/Asset * Asset/USD + transitive_denom: String, + + /// Params to query geometric TWAP price + geometric_twap: GeometricTwap, + + /// Params to query redemption rate + redemption_rate: RedemptionRate, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct GeometricTwap { + /// Pool id for stAsset/Asset pool + pub pool_id: u64, + + /// Window size in seconds representing the entire window for which 'geometric' price is calculated. + /// Value should be <= 172800 sec (48 hours). + pub window_size: u64, + + /// Detect when the chain is recovering from downtime + pub downtime_detector: Option, } -impl fmt::Display for OsmosisPriceSource { +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct RedemptionRate { + /// Contract addr + pub contract_addr: T, + + /// The maximum number of seconds since the last price was by an oracle, before + /// rejecting the price as too stale + pub max_staleness: u64, +} + +pub type OsmosisPriceSourceUnchecked = OsmosisPriceSource; +pub type OsmosisPriceSourceChecked = OsmosisPriceSource; + +impl fmt::Display for OsmosisPriceSourceChecked { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let label = match self { OsmosisPriceSource::Fixed { @@ -204,68 +258,135 @@ impl fmt::Display for OsmosisPriceSource { } => { format!("pyth:{price_feed_id}:{max_staleness}") } + OsmosisPriceSource::Lsd { + transitive_denom, + geometric_twap, + redemption_rate, + } => { + let GeometricTwap { + pool_id, + window_size, + downtime_detector, + } = geometric_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}") + } }; write!(f, "{label}") } } -impl PriceSource for OsmosisPriceSource { +impl PriceSourceUnchecked for OsmosisPriceSourceUnchecked { fn validate( - &self, - querier: &QuerierWrapper, + self, + deps: Deps, denom: &str, base_denom: &str, - ) -> ContractResult<()> { - match self { - OsmosisPriceSource::Fixed { - .. - } => Ok(()), - OsmosisPriceSource::Spot { + ) -> ContractResult { + match &self { + OsmosisPriceSourceUnchecked::Fixed { + price, + } => Ok(OsmosisPriceSourceChecked::Fixed { + price: *price, + }), + OsmosisPriceSourceUnchecked::Spot { pool_id, } => { - let pool = query_pool(querier, *pool_id)?; - helpers::assert_osmosis_pool_assets(&pool, denom, base_denom) + let pool = query_pool(&deps.querier, *pool_id)?; + helpers::assert_osmosis_pool_assets(&pool, denom, base_denom)?; + Ok(OsmosisPriceSourceChecked::Spot { + pool_id: *pool_id, + }) } - OsmosisPriceSource::ArithmeticTwap { + OsmosisPriceSourceUnchecked::ArithmeticTwap { pool_id, window_size, downtime_detector, } => { - let pool = query_pool(querier, *pool_id)?; + let pool = query_pool(&deps.querier, *pool_id)?; helpers::assert_osmosis_pool_assets(&pool, denom, base_denom)?; - helpers::assert_osmosis_twap(*window_size, downtime_detector) + helpers::assert_osmosis_twap(*window_size, downtime_detector)?; + Ok(OsmosisPriceSourceChecked::ArithmeticTwap { + pool_id: *pool_id, + window_size: *window_size, + downtime_detector: downtime_detector.clone(), + }) } - OsmosisPriceSource::GeometricTwap { + OsmosisPriceSourceUnchecked::GeometricTwap { pool_id, window_size, downtime_detector, } => { - let pool = query_pool(querier, *pool_id)?; + let pool = query_pool(&deps.querier, *pool_id)?; helpers::assert_osmosis_pool_assets(&pool, denom, base_denom)?; - helpers::assert_osmosis_twap(*window_size, downtime_detector) + helpers::assert_osmosis_twap(*window_size, downtime_detector)?; + Ok(OsmosisPriceSourceChecked::GeometricTwap { + pool_id: *pool_id, + window_size: *window_size, + downtime_detector: downtime_detector.clone(), + }) } - OsmosisPriceSource::XykLiquidityToken { + OsmosisPriceSourceUnchecked::XykLiquidityToken { pool_id, } => { - let pool = query_pool(querier, *pool_id)?; - helpers::assert_osmosis_xyk_pool(&pool) + let pool = query_pool(&deps.querier, *pool_id)?; + helpers::assert_osmosis_xyk_pool(&pool)?; + Ok(OsmosisPriceSourceChecked::XykLiquidityToken { + pool_id: *pool_id, + }) } - OsmosisPriceSource::StakedGeometricTwap { + OsmosisPriceSourceUnchecked::StakedGeometricTwap { transitive_denom, pool_id, window_size, downtime_detector, } => { - let pool = query_pool(querier, *pool_id)?; + let pool = query_pool(&deps.querier, *pool_id)?; helpers::assert_osmosis_pool_assets(&pool, denom, transitive_denom)?; - helpers::assert_osmosis_twap(*window_size, downtime_detector) + helpers::assert_osmosis_twap(*window_size, downtime_detector)?; + Ok(OsmosisPriceSourceChecked::StakedGeometricTwap { + transitive_denom: transitive_denom.to_string(), + pool_id: *pool_id, + window_size: *window_size, + downtime_detector: downtime_detector.clone(), + }) + } + OsmosisPriceSourceUnchecked::Pyth { + price_feed_id, + max_staleness, + } => Ok(OsmosisPriceSourceChecked::Pyth { + price_feed_id: *price_feed_id, + max_staleness: *max_staleness, + }), + OsmosisPriceSourceUnchecked::Lsd { + transitive_denom, + geometric_twap, + redemption_rate, + } => { + let pool = query_pool(&deps.querier, geometric_twap.pool_id)?; + helpers::assert_osmosis_pool_assets(&pool, denom, transitive_denom)?; + helpers::assert_osmosis_twap( + geometric_twap.window_size, + &geometric_twap.downtime_detector, + )?; + Ok(OsmosisPriceSourceChecked::Lsd { + transitive_denom: transitive_denom.to_string(), + geometric_twap: geometric_twap.clone(), + redemption_rate: RedemptionRate { + contract_addr: deps.api.addr_validate(&redemption_rate.contract_addr)?, + max_staleness: redemption_rate.max_staleness, + }, + }) } - OsmosisPriceSource::Pyth { - .. - } => Ok(()), } } +} +impl PriceSourceChecked for OsmosisPriceSourceChecked { fn query_price( &self, deps: &Deps, @@ -276,13 +397,13 @@ impl PriceSource for OsmosisPriceSource { pyth_config: &PythConfig, ) -> ContractResult { match self { - OsmosisPriceSource::Fixed { + OsmosisPriceSourceChecked::Fixed { price, } => Ok(*price), - OsmosisPriceSource::Spot { + OsmosisPriceSourceChecked::Spot { pool_id, } => query_spot_price(&deps.querier, *pool_id, denom, base_denom).map_err(Into::into), - OsmosisPriceSource::ArithmeticTwap { + OsmosisPriceSourceChecked::ArithmeticTwap { pool_id, window_size, downtime_detector, @@ -293,7 +414,7 @@ impl PriceSource for OsmosisPriceSource { query_arithmetic_twap_price(&deps.querier, *pool_id, denom, base_denom, start_time) .map_err(Into::into) } - OsmosisPriceSource::GeometricTwap { + OsmosisPriceSourceChecked::GeometricTwap { pool_id, window_size, downtime_detector, @@ -304,7 +425,7 @@ impl PriceSource for OsmosisPriceSource { query_geometric_twap_price(&deps.querier, *pool_id, denom, base_denom, start_time) .map_err(Into::into) } - OsmosisPriceSource::XykLiquidityToken { + OsmosisPriceSourceChecked::XykLiquidityToken { pool_id, } => Self::query_xyk_liquidity_token_price( deps, @@ -314,7 +435,7 @@ impl PriceSource for OsmosisPriceSource { price_sources, pyth_config, ), - OsmosisPriceSource::StakedGeometricTwap { + OsmosisPriceSourceChecked::StakedGeometricTwap { transitive_denom, pool_id, window_size, @@ -334,17 +455,36 @@ impl PriceSource for OsmosisPriceSource { pyth_config, ) } - OsmosisPriceSource::Pyth { + OsmosisPriceSourceChecked::Pyth { price_feed_id, max_staleness, } => { Ok(Self::query_pyth_price(deps, env, *price_feed_id, *max_staleness, pyth_config)?) } + OsmosisPriceSourceChecked::Lsd { + transitive_denom, + geometric_twap, + redemption_rate, + } => { + Self::chain_recovered(deps, &geometric_twap.downtime_detector)?; + + Self::query_lsd_price( + deps, + env, + denom, + transitive_denom, + base_denom, + geometric_twap.clone(), + redemption_rate.clone(), + price_sources, + pyth_config, + ) + } } } } -impl OsmosisPriceSource { +impl OsmosisPriceSourceChecked { fn chain_recovered( deps: &Deps, downtime_detector: &Option, @@ -427,7 +567,7 @@ impl OsmosisPriceSource { base_denom: &str, pool_id: u64, window_size: u64, - price_sources: &Map<&str, OsmosisPriceSource>, + price_sources: &Map<&str, OsmosisPriceSourceChecked>, pyth_config: &PythConfig, ) -> ContractResult { let start_time = env.block.time.seconds() - window_size; @@ -452,6 +592,66 @@ impl OsmosisPriceSource { staked_price.checked_mul(transitive_price).map_err(Into::into) } + /// Staked asset price quoted in USD. + /// + /// stAsset/USD = stAsset/Asset * Asset/USD + /// where: + /// stAsset/Asset = min(stAsset/Asset Geometric TWAP, stAsset/Asset Redemption Rate) + #[allow(clippy::too_many_arguments)] + fn query_lsd_price( + deps: &Deps, + env: &Env, + denom: &str, + transitive_denom: &str, + base_denom: &str, + geometric_twap: GeometricTwap, + redemption_rate: RedemptionRate, + price_sources: &Map<&str, OsmosisPriceSourceChecked>, + pyth_config: &PythConfig, + ) -> 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, + )?; + + // query redemption rate + let rr = query_redemption_rate( + &deps.querier, + redemption_rate.contract_addr.clone(), + denom.to_string(), + transitive_denom.to_string(), + )?; + // Check if the redemption rate is not too old + if (current_time - rr.last_updated) > redemption_rate.max_staleness { + return Err(InvalidPrice { + reason: format!( + "redemption rate update time is too old/stale. last updated: {}, now: {}", + rr.last_updated, current_time + ), + }); + } + + // min from geometric TWAP and exchange rate + let min_price = min(staked_price, rr.exchange_rate); + + // use current price source + let transitive_price = price_sources.load(deps.storage, transitive_denom)?.query_price( + deps, + env, + transitive_denom, + base_denom, + price_sources, + pyth_config, + )?; + + min_price.checked_mul(transitive_price).map_err(Into::into) + } + fn query_pyth_price( deps: &Deps, env: &Env, @@ -500,7 +700,7 @@ impl OsmosisPriceSource { /// conf: 574566 /// price: 1365133270 /// The confidence interval is 574566 * 10^(-8) = $0.00574566, and the price is 1365133270 * 10^(-8) = $13.6513327. -fn scale_to_exponent(value: u128, expo: i32) -> ContractResult { +pub fn scale_to_exponent(value: u128, expo: i32) -> ContractResult { let target_expo = Uint128::from(10u8).checked_pow(expo.unsigned_abs())?; if expo < 0 { Ok(Decimal::checked_from_ratio(value, target_expo)?) @@ -525,7 +725,7 @@ mod tests { #[test] fn display_fixed_price_source() { - let ps = OsmosisPriceSource::Fixed { + let ps = OsmosisPriceSourceChecked::Fixed { price: Decimal::from_ratio(1u128, 2u128), }; assert_eq!(ps.to_string(), "fixed:0.5") @@ -533,7 +733,7 @@ mod tests { #[test] fn display_spot_price_source() { - let ps = OsmosisPriceSource::Spot { + let ps = OsmosisPriceSourceChecked::Spot { pool_id: 123, }; assert_eq!(ps.to_string(), "spot:123") @@ -541,14 +741,14 @@ mod tests { #[test] fn display_arithmetic_twap_price_source() { - let ps = OsmosisPriceSource::ArithmeticTwap { + let ps = OsmosisPriceSourceChecked::ArithmeticTwap { pool_id: 123, window_size: 300, downtime_detector: None, }; assert_eq!(ps.to_string(), "arithmetic_twap:123:300:None"); - let ps = OsmosisPriceSource::ArithmeticTwap { + let ps = OsmosisPriceSourceChecked::ArithmeticTwap { pool_id: 123, window_size: 300, downtime_detector: Some(DowntimeDetector { @@ -561,14 +761,14 @@ mod tests { #[test] fn display_geometric_twap_price_source() { - let ps = OsmosisPriceSource::GeometricTwap { + let ps = OsmosisPriceSourceChecked::GeometricTwap { pool_id: 123, window_size: 300, downtime_detector: None, }; assert_eq!(ps.to_string(), "geometric_twap:123:300:None"); - let ps = OsmosisPriceSource::GeometricTwap { + let ps = OsmosisPriceSourceChecked::GeometricTwap { pool_id: 123, window_size: 300, downtime_detector: Some(DowntimeDetector { @@ -581,7 +781,7 @@ mod tests { #[test] fn display_staked_geometric_twap_price_source() { - let ps = OsmosisPriceSource::StakedGeometricTwap { + let ps = OsmosisPriceSourceChecked::StakedGeometricTwap { transitive_denom: "transitive".to_string(), pool_id: 123, window_size: 300, @@ -589,7 +789,7 @@ mod tests { }; assert_eq!(ps.to_string(), "staked_geometric_twap:transitive:123:300:None"); - let ps = OsmosisPriceSource::StakedGeometricTwap { + let ps = OsmosisPriceSourceChecked::StakedGeometricTwap { transitive_denom: "transitive".to_string(), pool_id: 123, window_size: 300, @@ -606,7 +806,7 @@ mod tests { #[test] fn display_xyk_lp_price_source() { - let ps = OsmosisPriceSource::XykLiquidityToken { + let ps = OsmosisPriceSourceChecked::XykLiquidityToken { pool_id: 224, }; assert_eq!(ps.to_string(), "xyk_liquidity_token:224") @@ -614,7 +814,7 @@ mod tests { #[test] fn display_pyth_price_source() { - let ps = OsmosisPriceSource::Pyth { + let ps = OsmosisPriceSourceChecked::Pyth { price_feed_id: PriceIdentifier::from_hex( "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", ) @@ -626,4 +826,42 @@ mod tests { "pyth:0x61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3:60" ) } + + #[test] + fn display_lsd_price_source() { + let ps = OsmosisPriceSourceChecked::Lsd { + transitive_denom: "transitive".to_string(), + geometric_twap: GeometricTwap { + pool_id: 456, + window_size: 380, + downtime_detector: None, + }, + redemption_rate: RedemptionRate { + contract_addr: Addr::unchecked( + "osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc", + ), + max_staleness: 1234, + }, + }; + assert_eq!(ps.to_string(), "lsd:transitive:456:380:None:osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc:1234"); + + let ps = OsmosisPriceSourceChecked::Lsd { + transitive_denom: "transitive".to_string(), + geometric_twap: GeometricTwap { + pool_id: 456, + window_size: 380, + downtime_detector: Some(DowntimeDetector { + downtime: Downtime::Duration30m, + recovery: 552, + }), + }, + redemption_rate: RedemptionRate { + contract_addr: Addr::unchecked( + "osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc", + ), + max_staleness: 1234, + }, + }; + assert_eq!(ps.to_string(), "lsd:transitive:456:380:Some(Duration30m:552):osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc:1234"); + } } diff --git a/contracts/oracle/osmosis/src/stride.rs b/contracts/oracle/osmosis/src/stride.rs new file mode 100644 index 000000000..a255c3ec2 --- /dev/null +++ b/contracts/oracle/osmosis/src/stride.rs @@ -0,0 +1,46 @@ +use cosmwasm_std::{to_binary, Addr, Decimal, QuerierWrapper, QueryRequest, StdResult, WasmQuery}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +// TODO: should be updated once Stride open source their contract + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, JsonSchema)] +pub struct Price { + pub denom: String, + pub base_denom: String, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, JsonSchema)] +pub struct RedemptionRateRequest { + pub price: Price, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, JsonSchema)] +pub struct RedemptionRateResponse { + pub exchange_rate: Decimal, + pub last_updated: u64, +} + +/// How much base_denom we get for 1 denom +/// +/// Example: +/// denom: stAtom, base_denom: Atom +/// exchange_rate: 1.0211 +/// 1 stAtom = 1.0211 Atom +pub fn query_redemption_rate( + querier: &QuerierWrapper, + contract_addr: Addr, + denom: String, + base_denom: String, +) -> StdResult { + let redemption_rate_response = querier.query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: contract_addr.into_string(), + msg: to_binary(&RedemptionRateRequest { + price: Price { + denom, + base_denom, + }, + })?, + }))?; + Ok(redemption_rate_response) +} diff --git a/contracts/oracle/osmosis/tests/helpers.rs b/contracts/oracle/osmosis/tests/helpers.rs index 4c4b6983f..240d4c605 100644 --- a/contracts/oracle/osmosis/tests/helpers.rs +++ b/contracts/oracle/osmosis/tests/helpers.rs @@ -8,7 +8,7 @@ use cosmwasm_std::{ Coin, Deps, DepsMut, OwnedDeps, }; use mars_oracle_base::ContractError; -use mars_oracle_osmosis::{contract::entry, msg::ExecuteMsg, OsmosisPriceSource}; +use mars_oracle_osmosis::{contract::entry, msg::ExecuteMsg, OsmosisPriceSourceUnchecked}; use mars_osmosis::helpers::{Pool, QueryPoolResponse}; use mars_red_bank_types::oracle::{InstantiateMsg, QueryMsg}; use mars_testing::{mock_info, MarsMockQuerier}; @@ -144,14 +144,14 @@ pub fn set_pyth_price_source(deps: DepsMut, denom: &str, price_id: PriceIdentifi set_price_source( deps, denom, - OsmosisPriceSource::Pyth { + OsmosisPriceSourceUnchecked::Pyth { price_feed_id: price_id, max_staleness: 30, }, ) } -pub fn set_price_source(deps: DepsMut, denom: &str, price_source: OsmosisPriceSource) { +pub fn set_price_source(deps: DepsMut, denom: &str, price_source: OsmosisPriceSourceUnchecked) { entry::execute( deps, mock_env(), diff --git a/contracts/oracle/osmosis/tests/test_query_price.rs b/contracts/oracle/osmosis/tests/test_query_price.rs index 9226dad1e..222840fdd 100644 --- a/contracts/oracle/osmosis/tests/test_query_price.rs +++ b/contracts/oracle/osmosis/tests/test_query_price.rs @@ -1,8 +1,15 @@ -use cosmwasm_std::{coin, from_binary, Decimal, StdError}; +use cosmwasm_std::{ + coin, from_binary, + testing::{MockApi, MockStorage}, + Decimal, OwnedDeps, StdError, +}; use mars_oracle_base::ContractError; -use mars_oracle_osmosis::{contract::entry, Downtime, DowntimeDetector, OsmosisPriceSource}; +use mars_oracle_osmosis::{ + contract::entry, scale_to_exponent, stride::RedemptionRateResponse, Downtime, DowntimeDetector, + GeometricTwap, OsmosisPriceSourceUnchecked, RedemptionRate, +}; use mars_red_bank_types::oracle::{PriceResponse, QueryMsg}; -use mars_testing::mock_env_at_block_time; +use mars_testing::{mock_env_at_block_time, MarsMockQuerier}; use osmosis_std::types::osmosis::{ gamm::v2::QuerySpotPriceResponse, twap::v1beta1::{ArithmeticTwapToNowResponse, GeometricTwapToNowResponse}, @@ -20,7 +27,7 @@ fn querying_fixed_price() { helpers::set_price_source( deps.as_mut(), "uosmo", - OsmosisPriceSource::Fixed { + OsmosisPriceSourceUnchecked::Fixed { price: Decimal::one(), }, ); @@ -41,7 +48,7 @@ fn querying_spot_price() { helpers::set_price_source( deps.as_mut(), "umars", - OsmosisPriceSource::Spot { + OsmosisPriceSourceUnchecked::Spot { pool_id: 89, }, ); @@ -71,7 +78,7 @@ fn querying_arithmetic_twap_price() { helpers::set_price_source( deps.as_mut(), "umars", - OsmosisPriceSource::ArithmeticTwap { + OsmosisPriceSourceUnchecked::ArithmeticTwap { pool_id: 89, window_size: 86400, downtime_detector: None, @@ -107,7 +114,7 @@ fn querying_arithmetic_twap_price_with_downtime_detector() { helpers::set_price_source( deps.as_mut(), "umars", - OsmosisPriceSource::ArithmeticTwap { + OsmosisPriceSourceUnchecked::ArithmeticTwap { pool_id: 89, window_size: 86400, downtime_detector: Some(dd.clone()), @@ -153,7 +160,7 @@ fn querying_geometric_twap_price() { helpers::set_price_source( deps.as_mut(), "umars", - OsmosisPriceSource::GeometricTwap { + OsmosisPriceSourceUnchecked::GeometricTwap { pool_id: 89, window_size: 86400, downtime_detector: None, @@ -189,7 +196,7 @@ fn querying_geometric_twap_price_with_downtime_detector() { helpers::set_price_source( deps.as_mut(), "umars", - OsmosisPriceSource::GeometricTwap { + OsmosisPriceSourceUnchecked::GeometricTwap { pool_id: 89, window_size: 86400, downtime_detector: Some(dd.clone()), @@ -235,7 +242,7 @@ fn querying_staked_geometric_twap_price() { helpers::set_price_source( deps.as_mut(), "uatom", - OsmosisPriceSource::GeometricTwap { + OsmosisPriceSourceUnchecked::GeometricTwap { pool_id: 1, window_size: 86400, downtime_detector: None, @@ -244,7 +251,7 @@ fn querying_staked_geometric_twap_price() { helpers::set_price_source( deps.as_mut(), "ustatom", - OsmosisPriceSource::StakedGeometricTwap { + OsmosisPriceSourceUnchecked::StakedGeometricTwap { transitive_denom: "uatom".to_string(), pool_id: 803, window_size: 86400, @@ -288,7 +295,7 @@ fn querying_staked_geometric_twap_price_if_no_transitive_denom_price_source() { helpers::set_price_source( deps.as_mut(), "ustatom", - OsmosisPriceSource::StakedGeometricTwap { + OsmosisPriceSourceUnchecked::StakedGeometricTwap { transitive_denom: "uatom".to_string(), pool_id: 803, window_size: 86400, @@ -315,7 +322,7 @@ fn querying_staked_geometric_twap_price_if_no_transitive_denom_price_source() { assert_eq!( res_err, ContractError::Std(StdError::not_found( - "mars_oracle_osmosis::price_source::OsmosisPriceSource" + "mars_oracle_osmosis::price_source::OsmosisPriceSource" )) ); } @@ -331,7 +338,7 @@ fn querying_staked_geometric_twap_price_with_downtime_detector() { helpers::set_price_source( deps.as_mut(), "uatom", - OsmosisPriceSource::GeometricTwap { + OsmosisPriceSourceUnchecked::GeometricTwap { pool_id: 1, window_size: 86400, downtime_detector: Some(dd.clone()), @@ -340,7 +347,7 @@ fn querying_staked_geometric_twap_price_with_downtime_detector() { helpers::set_price_source( deps.as_mut(), "ustatom", - OsmosisPriceSource::StakedGeometricTwap { + OsmosisPriceSourceUnchecked::StakedGeometricTwap { transitive_denom: "uatom".to_string(), pool_id: 803, window_size: 86400, @@ -393,6 +400,326 @@ fn querying_staked_geometric_twap_price_with_downtime_detector() { assert_eq!(res.price, expected_price); } +#[test] +fn querying_lsd_price() { + let mut deps = helpers::setup_test_with_pools(); + + let publish_time = 1677157333u64; + let (pyth_price, ustatom_uatom_price) = + setup_pyth_and_geometric_twap_for_lsd(&mut deps, publish_time); + + // setup redemption rate: stAtom/Atom + deps.querier.set_redemption_rate( + "ustatom", + "uatom", + RedemptionRateResponse { + exchange_rate: ustatom_uatom_price + Decimal::one(), // geometric TWAP < redemption rate + last_updated: publish_time, + }, + ); + + // query price if geometric TWAP < redemption rate + helpers::set_price_source( + deps.as_mut(), + "ustatom", + 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: 21600, + }, + }, + ); + let res = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "ustatom".to_string(), + }, + ) + .unwrap(); + let res: PriceResponse = from_binary(&res).unwrap(); + let expected_price = ustatom_uatom_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 + deps.querier.set_redemption_rate( + "ustatom", + "uatom", + RedemptionRateResponse { + exchange_rate: ustatom_uatom_redemption_rate, + last_updated: publish_time, + }, + ); + + // query price if geometric TWAP > redemption rate + helpers::set_price_source( + deps.as_mut(), + "ustatom", + 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: 21600, + }, + }, + ); + let res = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "ustatom".to_string(), + }, + ) + .unwrap(); + let res: PriceResponse = from_binary(&res).unwrap(); + let expected_price = ustatom_uatom_redemption_rate * pyth_price; + assert_eq!(res.price, expected_price); +} + +fn setup_pyth_and_geometric_twap_for_lsd( + deps: &mut OwnedDeps, + publish_time: u64, +) -> (Decimal, Decimal) { + // setup pyth price: Atom/Usd + let price_id = PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(); + + helpers::set_price_source( + deps.as_mut(), + "uatom", + OsmosisPriceSourceUnchecked::Pyth { + price_feed_id: price_id, + max_staleness: 1800u64, + }, + ); + + let price = Price { + price: 1021000, + conf: 50000, + expo: -4, + publish_time: publish_time as i64, + }; + let pyth_price = scale_to_exponent(price.price as u128, price.expo).unwrap(); + + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new(price_id, price, price), + }, + ); + + // setup geometric TWAP: stAtom/Atom + let ustatom_uatom_price = Decimal::from_ratio(1054u128, 1000u128); + deps.querier.set_geometric_twap_price( + 803, + "ustatom", + "uatom", + GeometricTwapToNowResponse { + geometric_twap: ustatom_uatom_price.to_string(), + }, + ); + (pyth_price, ustatom_uatom_price) +} + +#[test] +fn querying_lsd_price_if_no_transitive_denom_price_source() { + let mut deps = helpers::setup_test_with_pools(); + + // setup geometric TWAP: stAtom/Atom + let ustatom_uatom_price = Decimal::from_ratio(1054u128, 1000u128); + deps.querier.set_geometric_twap_price( + 803, + "ustatom", + "uatom", + GeometricTwapToNowResponse { + geometric_twap: ustatom_uatom_price.to_string(), + }, + ); + + // setup redemption rate: stAtom/Atom + let publish_time = 1677157333u64; + deps.querier.set_redemption_rate( + "ustatom", + "uatom", + RedemptionRateResponse { + exchange_rate: ustatom_uatom_price + Decimal::one(), // geometric TWAP < redemption rate + last_updated: publish_time, + }, + ); + + // query price if geometric TWAP < redemption rate + helpers::set_price_source( + deps.as_mut(), + "ustatom", + 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: 21600, + }, + }, + ); + + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "ustatom".to_string(), + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::Std(StdError::not_found( + "mars_oracle_osmosis::price_source::OsmosisPriceSource" + )) + ); +} + +#[test] +fn querying_lsd_price_if_redemption_rate_too_old() { + let mut deps = helpers::setup_test_with_pools(); + + 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); + + // setup redemption rate: stAtom/Atom + deps.querier.set_redemption_rate( + "ustatom", + "uatom", + RedemptionRateResponse { + exchange_rate: ustatom_uatom_price + Decimal::one(), // geometric TWAP < redemption rate + last_updated: publish_time - max_staleness - 1, + }, + ); + + // query price if geometric TWAP < redemption rate + helpers::set_price_source( + deps.as_mut(), + "ustatom", + 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, + }, + }, + ); + + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "ustatom".to_string(), + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::InvalidPrice { + reason: "redemption rate update time is too old/stale. last updated: 1677135732, now: 1677157333".to_string() + } + ); +} + +#[test] +fn querying_lsd_price_with_downtime_detector() { + let mut deps = helpers::setup_test_with_pools(); + + let publish_time = 1677157333u64; + let (pyth_price, ustatom_uatom_price) = + setup_pyth_and_geometric_twap_for_lsd(&mut deps, publish_time); + + // setup redemption rate: stAtom/Atom + deps.querier.set_redemption_rate( + "ustatom", + "uatom", + RedemptionRateResponse { + exchange_rate: ustatom_uatom_price + Decimal::one(), // geometric TWAP < redemption rate + last_updated: 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(), + geometric_twap: GeometricTwap { + pool_id: 803, + window_size: 86400, + downtime_detector: Some(dd.clone()), + }, + 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(), + }, + ) + .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(), + }, + ) + .unwrap(); + let res: PriceResponse = from_binary(&res).unwrap(); + let expected_price = ustatom_uatom_price * pyth_price; + assert_eq!(res.price, expected_price); +} + #[test] fn querying_xyk_lp_price() { let mut deps = helpers::setup_test_with_pools(); @@ -435,7 +762,7 @@ fn querying_xyk_lp_price() { helpers::set_price_source( deps.as_mut(), "uatom", - OsmosisPriceSource::Fixed { + OsmosisPriceSourceUnchecked::Fixed { price: uatom_price, }, ); @@ -446,7 +773,7 @@ fn querying_xyk_lp_price() { helpers::set_price_source( deps.as_mut(), "umars", - OsmosisPriceSource::Fixed { + OsmosisPriceSourceUnchecked::Fixed { price: umars_price, }, ); @@ -456,7 +783,7 @@ fn querying_xyk_lp_price() { helpers::set_price_source( deps.as_mut(), "uatom_umars_lp", - OsmosisPriceSource::XykLiquidityToken { + OsmosisPriceSourceUnchecked::XykLiquidityToken { pool_id: 10003, }, ); @@ -519,7 +846,7 @@ fn querying_pyth_price_if_publish_price_too_old() { helpers::set_price_source( deps.as_mut(), "uatom", - OsmosisPriceSource::Pyth { + OsmosisPriceSourceUnchecked::Pyth { price_feed_id: price_id, max_staleness, }, @@ -579,7 +906,7 @@ fn querying_pyth_price_if_signed() { helpers::set_price_source( deps.as_mut(), "uatom", - OsmosisPriceSource::Pyth { + OsmosisPriceSourceUnchecked::Pyth { price_feed_id: price_id, max_staleness, }, @@ -636,7 +963,7 @@ fn querying_pyth_price_successfully() { helpers::set_price_source( deps.as_mut(), "uatom", - OsmosisPriceSource::Pyth { + OsmosisPriceSourceUnchecked::Pyth { price_feed_id: price_id, max_staleness, }, @@ -718,21 +1045,21 @@ fn querying_all_prices() { helpers::set_price_source( deps.as_mut(), "uosmo", - OsmosisPriceSource::Fixed { + OsmosisPriceSourceUnchecked::Fixed { price: Decimal::one(), }, ); helpers::set_price_source( deps.as_mut(), "uatom", - OsmosisPriceSource::Spot { + OsmosisPriceSourceUnchecked::Spot { pool_id: 1, }, ); helpers::set_price_source( deps.as_mut(), "umars", - OsmosisPriceSource::Spot { + OsmosisPriceSourceUnchecked::Spot { pool_id: 89, }, ); diff --git a/contracts/oracle/osmosis/tests/test_remove_price_source.rs b/contracts/oracle/osmosis/tests/test_remove_price_source.rs index 3030e7c6b..b81e4ac10 100644 --- a/contracts/oracle/osmosis/tests/test_remove_price_source.rs +++ b/contracts/oracle/osmosis/tests/test_remove_price_source.rs @@ -3,7 +3,7 @@ use mars_oracle_base::ContractError; use mars_oracle_osmosis::{ contract::entry::execute, msg::{ExecuteMsg, PriceSourceResponse}, - OsmosisPriceSource, + OsmosisPriceSourceUnchecked, }; use mars_owner::OwnerError::NotOwner; use mars_red_bank_types::oracle::QueryMsg; @@ -34,21 +34,21 @@ fn removing_price_source() { helpers::set_price_source( deps.as_mut(), "uosmo", - OsmosisPriceSource::Fixed { + OsmosisPriceSourceUnchecked::Fixed { price: Decimal::one(), }, ); helpers::set_price_source( deps.as_mut(), "uatom", - OsmosisPriceSource::Spot { + OsmosisPriceSourceUnchecked::Spot { pool_id: 1, }, ); helpers::set_price_source( deps.as_mut(), "umars", - OsmosisPriceSource::Spot { + OsmosisPriceSourceUnchecked::Spot { pool_id: 89, }, ); diff --git a/contracts/oracle/osmosis/tests/test_set_price_source.rs b/contracts/oracle/osmosis/tests/test_set_price_source.rs index 5cbca7125..583604bbb 100644 --- a/contracts/oracle/osmosis/tests/test_set_price_source.rs +++ b/contracts/oracle/osmosis/tests/test_set_price_source.rs @@ -1,9 +1,10 @@ -use cosmwasm_std::{testing::mock_env, Decimal}; +use cosmwasm_std::{testing::mock_env, Addr, Decimal}; use mars_oracle_base::ContractError; use mars_oracle_osmosis::{ contract::entry::execute, msg::{ExecuteMsg, PriceSourceResponse}, - Downtime, DowntimeDetector, OsmosisPriceSource, + Downtime, DowntimeDetector, GeometricTwap, OsmosisPriceSourceChecked, + OsmosisPriceSourceUnchecked, RedemptionRate, }; use mars_owner::OwnerError::NotOwner; use mars_red_bank_types::oracle::QueryMsg; @@ -23,7 +24,7 @@ fn setting_price_source_by_non_owner() { mock_info("jake"), ExecuteMsg::SetPriceSource { denom: "uosmo".to_string(), - price_source: OsmosisPriceSource::Fixed { + price_source: OsmosisPriceSourceUnchecked::Fixed { price: Decimal::one(), }, }, @@ -42,7 +43,7 @@ fn setting_price_source_fixed() { mock_info("owner"), ExecuteMsg::SetPriceSource { denom: "uosmo".to_string(), - price_source: OsmosisPriceSource::Fixed { + price_source: OsmosisPriceSourceUnchecked::Fixed { price: Decimal::one(), }, }, @@ -58,7 +59,7 @@ fn setting_price_source_fixed() { ); assert_eq!( res.price_source, - OsmosisPriceSource::Fixed { + OsmosisPriceSourceChecked::Fixed { price: Decimal::one() } ); @@ -74,7 +75,7 @@ fn setting_price_source_incorrect_denom() { mock_info("owner"), ExecuteMsg::SetPriceSource { denom: "!*jadfaefc".to_string(), - price_source: OsmosisPriceSource::Fixed { + price_source: OsmosisPriceSourceUnchecked::Fixed { price: Decimal::one(), }, }, @@ -92,7 +93,7 @@ fn setting_price_source_incorrect_denom() { mock_info("owner"), ExecuteMsg::SetPriceSource { denom: "ahdbufenf&*!-".to_string(), - price_source: OsmosisPriceSource::Fixed { + price_source: OsmosisPriceSourceUnchecked::Fixed { price: Decimal::one(), }, }, @@ -111,7 +112,7 @@ fn setting_price_source_incorrect_denom() { mock_info("owner"), ExecuteMsg::SetPriceSource { denom: "ab".to_string(), - price_source: OsmosisPriceSource::Fixed { + price_source: OsmosisPriceSourceUnchecked::Fixed { price: Decimal::one(), }, }, @@ -135,7 +136,7 @@ fn setting_price_source_spot() { mock_info("owner"), ExecuteMsg::SetPriceSource { denom: denom.to_string(), - price_source: OsmosisPriceSource::Spot { + price_source: OsmosisPriceSourceUnchecked::Spot { pool_id, }, }, @@ -190,7 +191,7 @@ fn setting_price_source_spot() { ); assert_eq!( res.price_source, - OsmosisPriceSource::Spot { + OsmosisPriceSourceChecked::Spot { pool_id: 89, } ); @@ -211,7 +212,7 @@ fn setting_price_source_arithmetic_twap_with_invalid_params() { mock_info("owner"), ExecuteMsg::SetPriceSource { denom: denom.to_string(), - price_source: OsmosisPriceSource::ArithmeticTwap { + price_source: OsmosisPriceSourceUnchecked::ArithmeticTwap { pool_id, window_size, downtime_detector, @@ -295,7 +296,7 @@ fn setting_price_source_arithmetic_twap_successfully() { mock_info("owner"), ExecuteMsg::SetPriceSource { denom: "umars".to_string(), - price_source: OsmosisPriceSource::ArithmeticTwap { + price_source: OsmosisPriceSourceUnchecked::ArithmeticTwap { pool_id: 89, window_size: 86400, downtime_detector: None, @@ -313,7 +314,7 @@ fn setting_price_source_arithmetic_twap_successfully() { ); assert_eq!( res.price_source, - OsmosisPriceSource::ArithmeticTwap { + OsmosisPriceSourceChecked::ArithmeticTwap { pool_id: 89, window_size: 86400, downtime_detector: None @@ -327,7 +328,7 @@ fn setting_price_source_arithmetic_twap_successfully() { mock_info("owner"), ExecuteMsg::SetPriceSource { denom: "umars".to_string(), - price_source: OsmosisPriceSource::ArithmeticTwap { + price_source: OsmosisPriceSourceUnchecked::ArithmeticTwap { pool_id: 89, window_size: 86400, downtime_detector: Some(DowntimeDetector { @@ -348,7 +349,7 @@ fn setting_price_source_arithmetic_twap_successfully() { ); assert_eq!( res.price_source, - OsmosisPriceSource::ArithmeticTwap { + OsmosisPriceSourceChecked::ArithmeticTwap { pool_id: 89, window_size: 86400, downtime_detector: Some(DowntimeDetector { @@ -374,7 +375,7 @@ fn setting_price_source_geometric_twap_with_invalid_params() { mock_info("owner"), ExecuteMsg::SetPriceSource { denom: denom.to_string(), - price_source: OsmosisPriceSource::GeometricTwap { + price_source: OsmosisPriceSourceUnchecked::GeometricTwap { pool_id, window_size, downtime_detector, @@ -458,7 +459,7 @@ fn setting_price_source_geometric_twap_successfully() { mock_info("owner"), ExecuteMsg::SetPriceSource { denom: "umars".to_string(), - price_source: OsmosisPriceSource::GeometricTwap { + price_source: OsmosisPriceSourceUnchecked::GeometricTwap { pool_id: 89, window_size: 86400, downtime_detector: None, @@ -476,7 +477,7 @@ fn setting_price_source_geometric_twap_successfully() { ); assert_eq!( res.price_source, - OsmosisPriceSource::GeometricTwap { + OsmosisPriceSourceChecked::GeometricTwap { pool_id: 89, window_size: 86400, downtime_detector: None @@ -490,7 +491,7 @@ fn setting_price_source_geometric_twap_successfully() { mock_info("owner"), ExecuteMsg::SetPriceSource { denom: "umars".to_string(), - price_source: OsmosisPriceSource::GeometricTwap { + price_source: OsmosisPriceSourceUnchecked::GeometricTwap { pool_id: 89, window_size: 86400, downtime_detector: Some(DowntimeDetector { @@ -511,7 +512,7 @@ fn setting_price_source_geometric_twap_successfully() { ); assert_eq!( res.price_source, - OsmosisPriceSource::GeometricTwap { + OsmosisPriceSourceChecked::GeometricTwap { pool_id: 89, window_size: 86400, downtime_detector: Some(DowntimeDetector { @@ -538,7 +539,7 @@ fn setting_price_source_staked_geometric_twap_with_invalid_params() { mock_info("owner"), ExecuteMsg::SetPriceSource { denom: denom.to_string(), - price_source: OsmosisPriceSource::StakedGeometricTwap { + price_source: OsmosisPriceSourceUnchecked::StakedGeometricTwap { transitive_denom: transitive_denom.to_string(), pool_id, window_size, @@ -622,7 +623,7 @@ fn setting_price_source_staked_geometric_twap_successfully() { mock_info("owner"), ExecuteMsg::SetPriceSource { denom: "ustatom".to_string(), - price_source: OsmosisPriceSource::StakedGeometricTwap { + price_source: OsmosisPriceSourceUnchecked::StakedGeometricTwap { transitive_denom: "uatom".to_string(), pool_id: 803, window_size: 86400, @@ -641,7 +642,7 @@ fn setting_price_source_staked_geometric_twap_successfully() { ); assert_eq!( res.price_source, - OsmosisPriceSource::StakedGeometricTwap { + OsmosisPriceSourceChecked::StakedGeometricTwap { transitive_denom: "uatom".to_string(), pool_id: 803, window_size: 86400, @@ -656,7 +657,7 @@ fn setting_price_source_staked_geometric_twap_successfully() { mock_info("owner"), ExecuteMsg::SetPriceSource { denom: "ustatom".to_string(), - price_source: OsmosisPriceSource::StakedGeometricTwap { + price_source: OsmosisPriceSourceUnchecked::StakedGeometricTwap { transitive_denom: "uatom".to_string(), pool_id: 803, window_size: 86400, @@ -678,7 +679,7 @@ fn setting_price_source_staked_geometric_twap_successfully() { ); assert_eq!( res.price_source, - OsmosisPriceSource::StakedGeometricTwap { + OsmosisPriceSourceChecked::StakedGeometricTwap { transitive_denom: "uatom".to_string(), pool_id: 803, window_size: 86400, @@ -690,6 +691,204 @@ fn setting_price_source_staked_geometric_twap_successfully() { ); } +#[test] +fn setting_price_source_lsd_with_invalid_params() { + let mut deps = helpers::setup_test_with_pools(); + + let mut set_price_source_twap = + |denom: &str, + transitive_denom: &str, + pool_id: u64, + window_size: u64, + downtime_detector: Option| { + execute( + deps.as_mut(), + mock_env(), + mock_info("owner"), + ExecuteMsg::SetPriceSource { + denom: denom.to_string(), + price_source: OsmosisPriceSourceUnchecked::Lsd { + transitive_denom: transitive_denom.to_string(), + geometric_twap: GeometricTwap { + pool_id, + window_size, + downtime_detector, + }, + redemption_rate: RedemptionRate { + contract_addr: "dummy_addr".to_string(), + max_staleness: 100, + }, + }, + }, + ) + }; + + // attempting to use a pool that does not contain the denom of interest; should fail + let err = set_price_source_twap("ustatom", "umars", 803, 86400, None).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "pool 803 does not contain the base denom umars".to_string() + } + ); + let err = set_price_source_twap("umars", "uatom", 803, 86400, None).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "pool 803 does not contain umars".to_string() + } + ); + + // attempting to use a pool that contains more than two assets; should fail + let err = set_price_source_twap("ustatom", "uatom", 3333, 86400, None).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "expecting pool 3333 to contain exactly two coins; found 3".to_string() + } + ); + + // attempting to use not XYK pool + let err = set_price_source_twap("ustatom", "uatom", 4444, 86400, None).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "assets in pool 4444 do not have equal weights".to_string() + } + ); + + // attempting to set window_size bigger than 172800 sec (48h) + let err = set_price_source_twap("ustatom", "uatom", 803, 172801, None).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "expecting window size to be within 172800 sec".to_string() + } + ); + + // attempting to set downtime recovery to 0 + let err = set_price_source_twap( + "ustatom", + "uatom", + 803, + 86400, + Some(DowntimeDetector { + downtime: Downtime::Duration30s, + recovery: 0, + }), + ) + .unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "downtime recovery can't be 0".to_string() + } + ); +} + +#[test] +fn setting_price_source_lsd_successfully() { + 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, + }, + }, + }, + ) + .unwrap(); + assert_eq!(res.messages.len(), 0); + + let res: PriceSourceResponse = helpers::query( + deps.as_ref(), + QueryMsg::PriceSource { + denom: "ustatom".to_string(), + }, + ); + 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, + }, + }, + }, + ) + .unwrap(); + assert_eq!(res.messages.len(), 0); + + let res: PriceSourceResponse = helpers::query( + deps.as_ref(), + QueryMsg::PriceSource { + denom: "ustatom".to_string(), + }, + ); + assert_eq!( + res.price_source, + 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, + }) + }, + redemption_rate: RedemptionRate { + contract_addr: Addr::unchecked("dummy_addr"), + max_staleness: 100 + } + } + ); +} + #[test] fn setting_price_source_xyk_lp() { let mut deps = helpers::setup_test_with_pools(); @@ -701,7 +900,7 @@ fn setting_price_source_xyk_lp() { mock_info("owner"), ExecuteMsg::SetPriceSource { denom: denom.to_string(), - price_source: OsmosisPriceSource::XykLiquidityToken { + price_source: OsmosisPriceSourceUnchecked::XykLiquidityToken { pool_id, }, }, @@ -738,7 +937,7 @@ fn setting_price_source_xyk_lp() { ); assert_eq!( res.price_source, - OsmosisPriceSource::XykLiquidityToken { + OsmosisPriceSourceChecked::XykLiquidityToken { pool_id: 89, } ); @@ -754,7 +953,7 @@ fn setting_price_source_pyth_successfully() { mock_info("owner"), ExecuteMsg::SetPriceSource { denom: "uatom".to_string(), - price_source: OsmosisPriceSource::Pyth { + price_source: OsmosisPriceSourceUnchecked::Pyth { price_feed_id: PriceIdentifier::from_hex( "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", ) @@ -774,7 +973,7 @@ fn setting_price_source_pyth_successfully() { ); assert_eq!( res.price_source, - OsmosisPriceSource::Pyth { + OsmosisPriceSourceChecked::Pyth { price_feed_id: PriceIdentifier::from_hex( "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3" ) @@ -791,21 +990,21 @@ fn querying_price_source() { helpers::set_price_source( deps.as_mut(), "uosmo", - OsmosisPriceSource::Fixed { + OsmosisPriceSourceUnchecked::Fixed { price: Decimal::one(), }, ); helpers::set_price_source( deps.as_mut(), "uatom", - OsmosisPriceSource::Spot { + OsmosisPriceSourceUnchecked::Spot { pool_id: 1, }, ); helpers::set_price_source( deps.as_mut(), "umars", - OsmosisPriceSource::Spot { + OsmosisPriceSourceUnchecked::Spot { pool_id: 89, }, ); @@ -819,7 +1018,7 @@ fn querying_price_source() { ); assert_eq!( res.price_source, - OsmosisPriceSource::Spot { + OsmosisPriceSourceChecked::Spot { pool_id: 89, } ); @@ -839,13 +1038,13 @@ fn querying_price_source() { vec![ PriceSourceResponse { denom: "uatom".to_string(), - price_source: OsmosisPriceSource::Spot { + price_source: OsmosisPriceSourceChecked::Spot { pool_id: 1 } }, PriceSourceResponse { denom: "umars".to_string(), - price_source: OsmosisPriceSource::Spot { + price_source: OsmosisPriceSourceChecked::Spot { pool_id: 89 } } @@ -864,13 +1063,13 @@ fn querying_price_source() { vec![ PriceSourceResponse { denom: "umars".to_string(), - price_source: OsmosisPriceSource::Spot { + price_source: OsmosisPriceSourceChecked::Spot { pool_id: 89 } }, PriceSourceResponse { denom: "uosmo".to_string(), - price_source: OsmosisPriceSource::Fixed { + price_source: OsmosisPriceSourceChecked::Fixed { price: Decimal::one() } } diff --git a/integration-tests/tests/test_oracles.rs b/integration-tests/tests/test_oracles.rs index 4705e7848..863360aa8 100644 --- a/integration-tests/tests/test_oracles.rs +++ b/integration-tests/tests/test_oracles.rs @@ -3,7 +3,8 @@ use std::str::FromStr; use cosmwasm_std::{coin, Coin, Decimal, Isqrt, Uint128}; use mars_oracle_base::ContractError; use mars_oracle_osmosis::{ - msg::PriceSourceResponse, Downtime, DowntimeDetector, OsmosisPriceSource, + msg::PriceSourceResponse, Downtime, DowntimeDetector, OsmosisPriceSourceChecked, + OsmosisPriceSourceUnchecked, }; use mars_red_bank_types::{ address_provider::{ @@ -73,7 +74,7 @@ fn querying_xyk_lp_price_if_no_price_for_tokens() { &contract_addr, &ExecuteMsg::SetPriceSource { denom: "umars_uatom_lp".to_string(), - price_source: OsmosisPriceSource::XykLiquidityToken { + price_source: OsmosisPriceSourceUnchecked::XykLiquidityToken { pool_id: pool_mars_atom, }, }, @@ -146,7 +147,7 @@ fn querying_xyk_lp_price_success() { &contract_addr, &ExecuteMsg::SetPriceSource { denom: "umars_uatom_lp".to_string(), - price_source: OsmosisPriceSource::XykLiquidityToken { + price_source: OsmosisPriceSourceUnchecked::XykLiquidityToken { pool_id: pool_mars_atom, }, }, @@ -158,7 +159,7 @@ fn querying_xyk_lp_price_success() { &contract_addr, &ExecuteMsg::SetPriceSource { denom: "umars".to_string(), - price_source: OsmosisPriceSource::Spot { + price_source: OsmosisPriceSourceUnchecked::Spot { pool_id: pool_mars_osmo, }, }, @@ -170,7 +171,7 @@ fn querying_xyk_lp_price_success() { &contract_addr, &ExecuteMsg::SetPriceSource { denom: "uatom".to_string(), - price_source: OsmosisPriceSource::Spot { + price_source: OsmosisPriceSourceUnchecked::Spot { pool_id: pool_atom_osmo, }, }, @@ -233,7 +234,7 @@ fn query_spot_price() { &oracle_addr, &ExecuteMsg::SetPriceSource { denom: "uatom".to_string(), - price_source: OsmosisPriceSource::Spot { + price_source: OsmosisPriceSourceUnchecked::Spot { pool_id, }, }, @@ -252,7 +253,7 @@ fn query_spot_price() { .unwrap(); assert_eq!( price_source.price_source, - (OsmosisPriceSource::Spot { + (OsmosisPriceSourceChecked::Spot { pool_id }) ); @@ -294,7 +295,7 @@ fn set_spot_without_pools() { &oracle_addr, &ExecuteMsg::SetPriceSource { denom: "uatom".to_string(), - price_source: OsmosisPriceSource::Spot { + price_source: OsmosisPriceSourceUnchecked::Spot { pool_id: 1u64, }, }, @@ -337,7 +338,7 @@ fn incorrect_pool_for_spot() { &oracle_addr, &ExecuteMsg::SetPriceSource { denom: "umars".to_string(), - price_source: OsmosisPriceSource::Spot { + price_source: OsmosisPriceSourceUnchecked::Spot { pool_id, }, }, @@ -383,7 +384,7 @@ fn update_spot_with_different_pool() { &oracle_addr, &ExecuteMsg::SetPriceSource { denom: "uatom".to_string(), - price_source: OsmosisPriceSource::Spot { + price_source: OsmosisPriceSourceUnchecked::Spot { pool_id, }, }, @@ -409,7 +410,7 @@ fn update_spot_with_different_pool() { &oracle_addr, &ExecuteMsg::SetPriceSource { denom: "uatom".to_string(), - price_source: OsmosisPriceSource::Spot { + price_source: OsmosisPriceSourceUnchecked::Spot { pool_id, }, }, @@ -458,7 +459,7 @@ fn query_spot_price_after_lp_change() { &oracle_addr, &ExecuteMsg::SetPriceSource { denom: "uatom".to_string(), - price_source: OsmosisPriceSource::Spot { + price_source: OsmosisPriceSourceUnchecked::Spot { pool_id, }, }, @@ -518,7 +519,7 @@ fn query_geometric_twap_price_with_downtime_detector() { &oracle_addr, &ExecuteMsg::SetPriceSource { denom: "uatom".to_string(), - price_source: OsmosisPriceSource::GeometricTwap { + price_source: OsmosisPriceSourceUnchecked::GeometricTwap { pool_id, window_size: 10, // 10 seconds = 2 swaps when each swap increases block time by 5 seconds downtime_detector: Some(DowntimeDetector { @@ -541,7 +542,7 @@ fn query_geometric_twap_price_with_downtime_detector() { .unwrap(); assert_eq!( price_source.price_source, - (OsmosisPriceSource::GeometricTwap { + (OsmosisPriceSourceChecked::GeometricTwap { pool_id, window_size: 10, downtime_detector: Some(DowntimeDetector { @@ -603,7 +604,7 @@ fn query_arithmetic_twap_price() { &oracle_addr, &ExecuteMsg::SetPriceSource { denom: "uatom".to_string(), - price_source: OsmosisPriceSource::ArithmeticTwap { + price_source: OsmosisPriceSourceUnchecked::ArithmeticTwap { pool_id, window_size: 10, // 10 seconds = 2 swaps when each swap increases block time by 5 seconds downtime_detector: None, @@ -626,7 +627,7 @@ fn query_arithmetic_twap_price() { .unwrap(); assert_eq!( price_source.price_source, - (OsmosisPriceSource::ArithmeticTwap { + (OsmosisPriceSourceChecked::ArithmeticTwap { pool_id, window_size: 10, downtime_detector: None @@ -690,7 +691,7 @@ fn query_geometric_twap_price() { &oracle_addr, &ExecuteMsg::SetPriceSource { denom: "uatom".to_string(), - price_source: OsmosisPriceSource::GeometricTwap { + price_source: OsmosisPriceSourceUnchecked::GeometricTwap { pool_id, window_size: 10, // 10 seconds = 2 swaps when each swap increases block time by 5 seconds downtime_detector: None, @@ -713,7 +714,7 @@ fn query_geometric_twap_price() { .unwrap(); assert_eq!( price_source.price_source, - (OsmosisPriceSource::GeometricTwap { + (OsmosisPriceSourceChecked::GeometricTwap { pool_id, window_size: 10, downtime_detector: None @@ -781,7 +782,7 @@ fn compare_spot_and_twap_price() { &oracle_addr, &ExecuteMsg::SetPriceSource { denom: "uatom".to_string(), - price_source: OsmosisPriceSource::Spot { + price_source: OsmosisPriceSourceUnchecked::Spot { pool_id, }, }, @@ -799,7 +800,7 @@ fn compare_spot_and_twap_price() { .unwrap(); assert_eq!( price_source.price_source, - OsmosisPriceSource::Spot { + OsmosisPriceSourceChecked::Spot { pool_id, } ); @@ -817,7 +818,7 @@ fn compare_spot_and_twap_price() { &oracle_addr, &ExecuteMsg::SetPriceSource { denom: "uatom".to_string(), - price_source: OsmosisPriceSource::ArithmeticTwap { + price_source: OsmosisPriceSourceUnchecked::ArithmeticTwap { pool_id, window_size: 10, // 10 seconds = 2 swaps when each swap increases block time by 5 seconds downtime_detector: None, @@ -837,7 +838,7 @@ fn compare_spot_and_twap_price() { .unwrap(); assert_eq!( price_source.price_source, - OsmosisPriceSource::ArithmeticTwap { + OsmosisPriceSourceChecked::ArithmeticTwap { pool_id, window_size: 10, downtime_detector: None @@ -857,7 +858,7 @@ fn compare_spot_and_twap_price() { &oracle_addr, &ExecuteMsg::SetPriceSource { denom: "uatom".to_string(), - price_source: OsmosisPriceSource::GeometricTwap { + price_source: OsmosisPriceSourceUnchecked::GeometricTwap { pool_id, window_size: 10, // 10 seconds = 2 swaps when each swap increases block time by 5 seconds downtime_detector: None, @@ -877,7 +878,7 @@ fn compare_spot_and_twap_price() { .unwrap(); assert_eq!( price_source.price_source, - OsmosisPriceSource::GeometricTwap { + OsmosisPriceSourceChecked::GeometricTwap { pool_id, window_size: 10, downtime_detector: None @@ -923,7 +924,7 @@ fn redbank_should_fail_if_no_price() { &oracle_addr, &ExecuteMsg::SetPriceSource { denom: "uatom".to_string(), - price_source: OsmosisPriceSource::Spot { + price_source: OsmosisPriceSourceUnchecked::Spot { pool_id, }, }, @@ -985,7 +986,7 @@ fn redbank_quering_oracle_successfully() { &oracle_addr, &ExecuteMsg::SetPriceSource { denom: "uatom".to_string(), - price_source: OsmosisPriceSource::Spot { + price_source: OsmosisPriceSourceUnchecked::Spot { pool_id, }, }, diff --git a/packages/testing/src/integration/mock_env.rs b/packages/testing/src/integration/mock_env.rs index 6c4a06d3b..2203eecc1 100644 --- a/packages/testing/src/integration/mock_env.rs +++ b/packages/testing/src/integration/mock_env.rs @@ -5,7 +5,7 @@ use std::mem::take; use anyhow::Result as AnyResult; use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128}; use cw_multi_test::{App, AppResponse, BankSudo, BasicApp, Executor, SudoMsg}; -use mars_oracle_osmosis::OsmosisPriceSource; +use mars_oracle_osmosis::OsmosisPriceSourceUnchecked; use mars_red_bank_types::{ address_provider::{self, MarsAddressType}, incentives, oracle, @@ -186,7 +186,7 @@ impl Oracle { self.contract_addr.clone(), &oracle::ExecuteMsg::SetPriceSource { denom: denom.to_string(), - price_source: OsmosisPriceSource::Fixed { + price_source: OsmosisPriceSourceUnchecked::Fixed { price, }, }, diff --git a/packages/testing/src/lib.rs b/packages/testing/src/lib.rs index 4183321d2..22b4befc7 100644 --- a/packages/testing/src/lib.rs +++ b/packages/testing/src/lib.rs @@ -12,6 +12,7 @@ mod oracle_querier; mod osmosis_querier; mod pyth_querier; mod red_bank_querier; +mod redemption_rate_querier; pub use helpers::*; pub use mars_mock_querier::MarsMockQuerier; diff --git a/packages/testing/src/mars_mock_querier.rs b/packages/testing/src/mars_mock_querier.rs index 563b34a0e..13404366b 100644 --- a/packages/testing/src/mars_mock_querier.rs +++ b/packages/testing/src/mars_mock_querier.rs @@ -4,7 +4,11 @@ use cosmwasm_std::{ Addr, Coin, Decimal, Empty, Querier, QuerierResult, QueryRequest, StdResult, SystemError, SystemResult, Uint128, WasmQuery, }; -use mars_oracle_osmosis::DowntimeDetector; +use mars_oracle_osmosis::{ + stride, + stride::{Price, RedemptionRateResponse}, + DowntimeDetector, +}; use mars_osmosis::helpers::QueryPoolResponse; use mars_red_bank_types::{address_provider, incentives, oracle, red_bank}; use osmosis_std::types::osmosis::{ @@ -21,6 +25,7 @@ use crate::{ osmosis_querier::{OsmosisQuerier, PriceKey}, pyth_querier::PythQuerier, red_bank_querier::RedBankQuerier, + redemption_rate_querier::RedemptionRateQuerier, }; pub struct MarsMockQuerier { @@ -30,6 +35,7 @@ pub struct MarsMockQuerier { osmosis_querier: OsmosisQuerier, pyth_querier: PythQuerier, redbank_querier: RedBankQuerier, + redemption_rate_querier: RedemptionRateQuerier, } impl Querier for MarsMockQuerier { @@ -57,6 +63,7 @@ impl MarsMockQuerier { osmosis_querier: OsmosisQuerier::default(), pyth_querier: PythQuerier::default(), redbank_querier: RedBankQuerier::default(), + redemption_rate_querier: Default::default(), } } @@ -164,6 +171,19 @@ impl MarsMockQuerier { self.redbank_querier.users_positions.insert(user_address, position); } + pub fn set_redemption_rate( + &mut self, + denom: &str, + base_denom: &str, + redemption_rate: RedemptionRateResponse, + ) { + let price_key = Price { + denom: denom.to_string(), + base_denom: base_denom.to_string(), + }; + self.redemption_rate_querier.redemption_rates.insert(price_key, redemption_rate); + } + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { match &request { QueryRequest::Wasm(WasmQuery::Smart { @@ -204,6 +224,11 @@ impl MarsMockQuerier { return self.redbank_querier.handle_query(redbank_query); } + // Redemption Rate Queries + if let Ok(redemption_rate_req) = from_binary::(msg) { + return self.redemption_rate_querier.handle_query(redemption_rate_req); + } + panic!("[mock]: Unsupported wasm query: {msg:?}"); } diff --git a/packages/testing/src/redemption_rate_querier.rs b/packages/testing/src/redemption_rate_querier.rs new file mode 100644 index 000000000..1b44ec9ba --- /dev/null +++ b/packages/testing/src/redemption_rate_querier.rs @@ -0,0 +1,29 @@ +use std::collections::HashMap; + +use cosmwasm_std::{to_binary, Binary, ContractResult, QuerierResult}; +use mars_oracle_osmosis::stride::{Price, RedemptionRateRequest, RedemptionRateResponse}; + +#[derive(Default)] +pub struct RedemptionRateQuerier { + pub redemption_rates: HashMap, +} + +impl RedemptionRateQuerier { + pub fn handle_query(&self, req: RedemptionRateRequest) -> QuerierResult { + let res: ContractResult = { + let option_rr = self.redemption_rates.get(&req.price); + + if let Some(rr) = option_rr { + to_binary(rr).into() + } else { + Err(format!( + "[mock]: could not find redemption rate for denom {} and base_denom {}", + req.price.denom, req.price.base_denom + )) + .into() + } + }; + + Ok(res).into() + } +} diff --git a/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json b/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json index 51b65f8ba..55ffae578 100644 --- a/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json +++ b/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json @@ -49,7 +49,7 @@ "type": "string" }, "price_source": { - "$ref": "#/definitions/OsmosisPriceSource" + "$ref": "#/definitions/OsmosisPriceSource_for_String" } }, "additionalProperties": false @@ -180,10 +180,42 @@ } } }, + "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 + } + } + }, "Identifier": { "type": "string" }, - "OsmosisPriceSource": { + "OsmosisPriceSource_for_String": { "oneOf": [ { "description": "Returns a fixed value;", @@ -412,6 +444,46 @@ } }, "additionalProperties": false + }, + { + "description": "Liquid Staking Derivatives (LSD) price quoted in USD based on data from Pyth, Osmosis and Stride.\n\nEquation to calculate the price: stAsset/USD = stAsset/Asset * Asset/USD where: stAsset/Asset = min(stAsset/Asset Geometric TWAP, stAsset/Asset Redemption Rate)\n\nExample: stATOM/USD = stATOM/ATOM * ATOM/USD where: - stATOM/ATOM = min(stAtom/Atom Geometric TWAP from Osmosis, stAtom/Atom Redemption Rate from Stride) - ATOM/USD price comes from the Mars Oracle contract (should point to Pyth).\n\nNOTE: `pool_id` must point to stAsset/Asset Osmosis pool. Asset/USD price source should be available in the Mars Oracle contract.", + "type": "object", + "required": [ + "lsd" + ], + "properties": { + "lsd": { + "type": "object", + "required": [ + "geometric_twap", + "redemption_rate", + "transitive_denom" + ], + "properties": { + "geometric_twap": { + "description": "Params to query geometric TWAP price", + "allOf": [ + { + "$ref": "#/definitions/GeometricTwap" + } + ] + }, + "redemption_rate": { + "description": "Params to query redemption rate", + "allOf": [ + { + "$ref": "#/definitions/RedemptionRate_for_String" + } + ] + }, + "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" + } + } + } + }, + "additionalProperties": false } ] }, @@ -490,6 +562,25 @@ ] } ] + }, + "RedemptionRate_for_String": { + "type": "object", + "required": [ + "contract_addr", + "max_staleness" + ], + "properties": { + "contract_addr": { + "description": "Contract addr", + "type": "string" + }, + "max_staleness": { + "description": "The maximum number of seconds since the last price was by an oracle, before rejecting the price as too stale", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } } } }, diff --git a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts index bbbdea0a6..ef1b487e0 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts @@ -10,12 +10,14 @@ import { Coin, StdFee } from '@cosmjs/amino' import { InstantiateMsg, ExecuteMsg, - OsmosisPriceSource, + OsmosisPriceSourceForString, Decimal, Downtime, Identifier, OwnerUpdate, DowntimeDetector, + GeometricTwap, + RedemptionRateForString, QueryMsg, ConfigResponse, PriceResponse, @@ -114,7 +116,7 @@ export interface MarsOracleOsmosisInterface extends MarsOracleOsmosisReadOnlyInt priceSource, }: { denom: string - priceSource: OsmosisPriceSource + priceSource: OsmosisPriceSourceForString }, fee?: number | StdFee | 'auto', memo?: string, @@ -174,7 +176,7 @@ export class MarsOracleOsmosisClient priceSource, }: { denom: string - priceSource: OsmosisPriceSource + priceSource: OsmosisPriceSourceForString }, fee: number | StdFee | 'auto' = 'auto', memo?: string, 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 151fae53e..4abed44af 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.react-query.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.react-query.ts @@ -11,12 +11,14 @@ import { StdFee, Coin } from '@cosmjs/amino' import { InstantiateMsg, ExecuteMsg, - OsmosisPriceSource, + OsmosisPriceSourceForString, Decimal, Downtime, Identifier, OwnerUpdate, DowntimeDetector, + GeometricTwap, + RedemptionRateForString, QueryMsg, ConfigResponse, PriceResponse, @@ -236,7 +238,7 @@ export interface MarsOracleOsmosisSetPriceSourceMutation { client: MarsOracleOsmosisClient msg: { denom: string - priceSource: OsmosisPriceSource + priceSource: OsmosisPriceSourceForString } args?: { fee?: number | StdFee | 'auto' diff --git a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts index 6ed5c3292..3d4cf6fbd 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts @@ -14,7 +14,7 @@ export type ExecuteMsg = | { set_price_source: { denom: string - price_source: OsmosisPriceSource + price_source: OsmosisPriceSourceForString } } | { @@ -31,7 +31,7 @@ export type ExecuteMsg = pyth_contract_addr?: string | null } } -export type OsmosisPriceSource = +export type OsmosisPriceSourceForString = | { fixed: { price: Decimal @@ -82,6 +82,14 @@ export type OsmosisPriceSource = [k: string]: unknown } } + | { + lsd: { + geometric_twap: GeometricTwap + redemption_rate: RedemptionRateForString + transitive_denom: string + [k: string]: unknown + } + } export type Decimal = string export type Downtime = | 'duration30s' @@ -130,6 +138,17 @@ export interface DowntimeDetector { recovery: number [k: string]: unknown } +export interface GeometricTwap { + downtime_detector?: DowntimeDetector | null + pool_id: number + window_size: number + [k: string]: unknown +} +export interface RedemptionRateForString { + contract_addr: string + max_staleness: number + [k: string]: unknown +} export type QueryMsg = | { config: {} From a0d59d24cbfbce90255f79f151d9958e05291118 Mon Sep 17 00:00:00 2001 From: Piotr Babel Date: Thu, 4 May 2023 18:14:08 +0200 Subject: [PATCH 04/11] Extract pyth addr to Pyth price source config. --- contracts/oracle/base/src/contract.rs | 42 +++---------------- contracts/oracle/base/src/traits.rs | 2 - contracts/oracle/osmosis/src/price_source.rs | 39 +++++++++-------- contracts/oracle/osmosis/tests/helpers.rs | 2 +- contracts/oracle/osmosis/tests/test_admin.rs | 10 ----- .../oracle/osmosis/tests/test_query_price.rs | 4 ++ .../osmosis/tests/test_set_price_source.rs | 2 + integration-tests/tests/test_oracles.rs | 14 ------- packages/testing/src/integration/mock_env.rs | 1 - packages/types/src/oracle.rs | 14 +------ .../mars-oracle-osmosis.json | 25 ++++------- .../MarsOracleOsmosis.client.ts | 5 --- .../MarsOracleOsmosis.react-query.ts | 1 - .../MarsOracleOsmosis.types.ts | 4 +- 14 files changed, 40 insertions(+), 125 deletions(-) diff --git a/contracts/oracle/base/src/contract.rs b/contracts/oracle/base/src/contract.rs index 204510088..8fb78fb0a 100644 --- a/contracts/oracle/base/src/contract.rs +++ b/contracts/oracle/base/src/contract.rs @@ -8,9 +8,9 @@ use cw_storage_plus::{Bound, Item, Map}; use mars_owner::{Owner, OwnerInit::SetInitialOwner, OwnerUpdate}; use mars_red_bank_types::oracle::{ Config, ConfigResponse, ExecuteMsg, InstantiateMsg, PriceResponse, PriceSourceResponse, - PythConfig, QueryMsg, + QueryMsg, }; -use mars_utils::helpers::{option_string_to_addr, validate_native_denom}; +use mars_utils::helpers::validate_native_denom; use crate::{error::ContractResult, PriceSourceChecked, PriceSourceUnchecked}; @@ -27,8 +27,6 @@ where pub owner: Owner<'a>, /// The contract's config pub config: Item<'a, Config>, - /// Pyth config - pub pyth_config: Item<'a, PythConfig>, /// The price source of each coin denom pub price_sources: Map<'a, &'a str, P>, /// Phantom data holds the unchecked price source type @@ -47,7 +45,6 @@ where Self { owner: Owner::new("owner"), config: Item::new("config"), - pyth_config: Item::new("pyth_config"), price_sources: Map::new("price_sources"), unchecked_price_source: PhantomData, custom_query: PhantomData, @@ -79,13 +76,6 @@ where }, )?; - self.pyth_config.save( - deps.storage, - &PythConfig { - pyth_contract_addr: deps.api.addr_validate(&msg.pyth_contract_addr)?, - }, - )?; - Ok(Response::default()) } @@ -106,8 +96,7 @@ where } => self.remove_price_source(deps, info.sender, denom), ExecuteMsg::UpdateConfig { base_denom, - pyth_contract_addr, - } => self.update_config(deps, info.sender, base_denom, pyth_contract_addr), + } => self.update_config(deps, info.sender, base_denom), } } @@ -182,7 +171,6 @@ where deps: DepsMut, sender_addr: Addr, base_denom: Option, - pyth_contract_addr: Option, ) -> ContractResult { self.owner.assert_owner(deps.storage, &sender_addr)?; @@ -195,18 +183,10 @@ where config.base_denom = base_denom.unwrap_or(config.base_denom); self.config.save(deps.storage, &config)?; - let mut pyth_cfg = self.pyth_config.load(deps.storage)?; - let prev_pyth_contract_addr = pyth_cfg.pyth_contract_addr.clone(); - pyth_cfg.pyth_contract_addr = - option_string_to_addr(deps.api, pyth_contract_addr, pyth_cfg.pyth_contract_addr)?; - self.pyth_config.save(deps.storage, &pyth_cfg)?; - let response = Response::new() .add_attribute("action", "update_config") .add_attribute("prev_base_denom", prev_base_denom) - .add_attribute("base_denom", config.base_denom) - .add_attribute("prev_pyth_contract_addr", prev_pyth_contract_addr) - .add_attribute("pyth_contract_addr", pyth_cfg.pyth_contract_addr); + .add_attribute("base_denom", config.base_denom); Ok(response) } @@ -214,12 +194,10 @@ where fn query_config(&self, deps: Deps) -> StdResult { let owner_state = self.owner.query(deps.storage)?; let cfg = self.config.load(deps.storage)?; - let pyth_cfg = self.pyth_config.load(deps.storage)?; Ok(ConfigResponse { owner: owner_state.owner, proposed_new_owner: owner_state.proposed, base_denom: cfg.base_denom, - pyth_contract_addr: pyth_cfg.pyth_contract_addr.to_string(), }) } @@ -258,7 +236,6 @@ where fn query_price(&self, deps: Deps, env: Env, denom: String) -> ContractResult { let cfg = self.config.load(deps.storage)?; - let pyth_cfg = self.pyth_config.load(deps.storage)?; let price_source = self.price_sources.load(deps.storage, &denom)?; Ok(PriceResponse { price: price_source.query_price( @@ -267,7 +244,6 @@ where &denom, &cfg.base_denom, &self.price_sources, - &pyth_cfg, )?, denom, }) @@ -281,7 +257,6 @@ where limit: Option, ) -> ContractResult> { let cfg = self.config.load(deps.storage)?; - let pyth_cfg = self.pyth_config.load(deps.storage)?; let start = start_after.map(|denom| Bound::ExclusiveRaw(denom.into_bytes())); let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; @@ -292,14 +267,7 @@ where .map(|item| { let (k, v) = item?; Ok(PriceResponse { - price: v.query_price( - &deps, - &env, - &k, - &cfg.base_denom, - &self.price_sources, - &pyth_cfg, - )?, + price: v.query_price(&deps, &env, &k, &cfg.base_denom, &self.price_sources)?, denom: k, }) }) diff --git a/contracts/oracle/base/src/traits.rs b/contracts/oracle/base/src/traits.rs index 716a7083c..0f21d67f8 100644 --- a/contracts/oracle/base/src/traits.rs +++ b/contracts/oracle/base/src/traits.rs @@ -2,7 +2,6 @@ use std::fmt::{Debug, Display}; use cosmwasm_std::{CustomQuery, Decimal, Deps, Env}; use cw_storage_plus::Map; -use mars_red_bank_types::oracle::PythConfig; use schemars::JsonSchema; use serde::{de::DeserializeOwned, Serialize}; @@ -44,6 +43,5 @@ where denom: &str, base_denom: &str, price_sources: &Map<&str, Self>, - pyth_config: &PythConfig, ) -> ContractResult; } diff --git a/contracts/oracle/osmosis/src/price_source.rs b/contracts/oracle/osmosis/src/price_source.rs index bc9492ef9..3922fb210 100644 --- a/contracts/oracle/osmosis/src/price_source.rs +++ b/contracts/oracle/osmosis/src/price_source.rs @@ -9,7 +9,6 @@ use mars_osmosis::helpers::{ query_arithmetic_twap_price, query_geometric_twap_price, query_pool, query_spot_price, recovered_since_downtime_of_length, Pool, }; -use mars_red_bank_types::oracle::PythConfig; use pyth_sdk_cw::{query_price_feed, PriceIdentifier}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -152,6 +151,9 @@ pub enum OsmosisPriceSource { downtime_detector: Option, }, Pyth { + /// Contract address of Pyth + contract_addr: T, + /// Price feed id of an asset from the list: https://pyth.network/developers/price-feed-ids price_feed_id: PriceIdentifier, @@ -253,10 +255,11 @@ impl fmt::Display for OsmosisPriceSourceChecked { format!("staked_geometric_twap:{transitive_denom}:{pool_id}:{window_size}:{dd_fmt}") } OsmosisPriceSource::Pyth { + contract_addr, price_feed_id, max_staleness, } => { - format!("pyth:{price_feed_id}:{max_staleness}") + format!("pyth:{contract_addr}:{price_feed_id}:{max_staleness}") } OsmosisPriceSource::Lsd { transitive_denom, @@ -356,9 +359,11 @@ impl PriceSourceUnchecked for OsmosisPriceSour }) } OsmosisPriceSourceUnchecked::Pyth { + contract_addr, price_feed_id, max_staleness, } => Ok(OsmosisPriceSourceChecked::Pyth { + contract_addr: deps.api.addr_validate(contract_addr)?, price_feed_id: *price_feed_id, max_staleness: *max_staleness, }), @@ -394,7 +399,6 @@ impl PriceSourceChecked for OsmosisPriceSourceChecked { denom: &str, base_denom: &str, price_sources: &Map<&str, Self>, - pyth_config: &PythConfig, ) -> ContractResult { match self { OsmosisPriceSourceChecked::Fixed { @@ -433,7 +437,6 @@ impl PriceSourceChecked for OsmosisPriceSourceChecked { *pool_id, base_denom, price_sources, - pyth_config, ), OsmosisPriceSourceChecked::StakedGeometricTwap { transitive_denom, @@ -452,15 +455,19 @@ impl PriceSourceChecked for OsmosisPriceSourceChecked { *pool_id, *window_size, price_sources, - pyth_config, ) } OsmosisPriceSourceChecked::Pyth { + contract_addr, price_feed_id, max_staleness, - } => { - Ok(Self::query_pyth_price(deps, env, *price_feed_id, *max_staleness, pyth_config)?) - } + } => Ok(Self::query_pyth_price( + deps, + env, + contract_addr.to_owned(), + *price_feed_id, + *max_staleness, + )?), OsmosisPriceSourceChecked::Lsd { transitive_denom, geometric_twap, @@ -477,7 +484,6 @@ impl PriceSourceChecked for OsmosisPriceSourceChecked { geometric_twap.clone(), redemption_rate.clone(), price_sources, - pyth_config, ) } } @@ -515,7 +521,6 @@ impl OsmosisPriceSourceChecked { pool_id: u64, base_denom: &str, price_sources: &Map<&str, Self>, - pyth_config: &PythConfig, ) -> ContractResult { // XYK pool asserted during price source creation let pool = query_pool(&deps.querier, pool_id)?; @@ -529,7 +534,6 @@ impl OsmosisPriceSourceChecked { &coin0.denom, base_denom, price_sources, - pyth_config, )?; let coin1_price = price_sources.load(deps.storage, &coin1.denom)?.query_price( deps, @@ -537,7 +541,6 @@ impl OsmosisPriceSourceChecked { &coin1.denom, base_denom, price_sources, - pyth_config, )?; let coin0_value = Uint256::from_uint128(coin0.amount) * Decimal256::from(coin0_price); @@ -568,7 +571,6 @@ impl OsmosisPriceSourceChecked { pool_id: u64, window_size: u64, price_sources: &Map<&str, OsmosisPriceSourceChecked>, - pyth_config: &PythConfig, ) -> ContractResult { let start_time = env.block.time.seconds() - window_size; let staked_price = query_geometric_twap_price( @@ -586,7 +588,6 @@ impl OsmosisPriceSourceChecked { transitive_denom, base_denom, price_sources, - pyth_config, )?; staked_price.checked_mul(transitive_price).map_err(Into::into) @@ -607,7 +608,6 @@ impl OsmosisPriceSourceChecked { geometric_twap: GeometricTwap, redemption_rate: RedemptionRate, price_sources: &Map<&str, OsmosisPriceSourceChecked>, - pyth_config: &PythConfig, ) -> ContractResult { let current_time = env.block.time.seconds(); let start_time = current_time - geometric_twap.window_size; @@ -646,7 +646,6 @@ impl OsmosisPriceSourceChecked { transitive_denom, base_denom, price_sources, - pyth_config, )?; min_price.checked_mul(transitive_price).map_err(Into::into) @@ -655,14 +654,13 @@ impl OsmosisPriceSourceChecked { fn query_pyth_price( deps: &Deps, env: &Env, + contract_addr: Addr, price_feed_id: PriceIdentifier, max_staleness: u64, - pyth_config: &PythConfig, ) -> ContractResult { let current_time = env.block.time.seconds(); - let price_feed_response = - query_price_feed(&deps.querier, pyth_config.pyth_contract_addr.clone(), price_feed_id)?; + let price_feed_response = query_price_feed(&deps.querier, contract_addr, price_feed_id)?; let price_feed = price_feed_response.price_feed; // Get the current price and confidence interval from the price feed @@ -815,6 +813,7 @@ mod tests { #[test] fn display_pyth_price_source() { let ps = OsmosisPriceSourceChecked::Pyth { + contract_addr: Addr::unchecked("osmo12j43nf2f0qumnt2zrrmpvnsqgzndxefujlvr08"), price_feed_id: PriceIdentifier::from_hex( "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", ) @@ -823,7 +822,7 @@ mod tests { }; assert_eq!( ps.to_string(), - "pyth:0x61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3:60" + "pyth:osmo12j43nf2f0qumnt2zrrmpvnsqgzndxefujlvr08:0x61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3:60" ) } diff --git a/contracts/oracle/osmosis/tests/helpers.rs b/contracts/oracle/osmosis/tests/helpers.rs index 240d4c605..ce158e308 100644 --- a/contracts/oracle/osmosis/tests/helpers.rs +++ b/contracts/oracle/osmosis/tests/helpers.rs @@ -90,7 +90,6 @@ pub fn setup_test() -> OwnedDeps { InstantiateMsg { owner: "owner".to_string(), base_denom: "uosmo".to_string(), - pyth_contract_addr: "pyth_contract".to_string(), }, ) .unwrap(); @@ -145,6 +144,7 @@ pub fn set_pyth_price_source(deps: DepsMut, denom: &str, price_id: PriceIdentifi deps, denom, OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract".to_string(), price_feed_id: price_id, max_staleness: 30, }, diff --git a/contracts/oracle/osmosis/tests/test_admin.rs b/contracts/oracle/osmosis/tests/test_admin.rs index 964102c7b..405368226 100644 --- a/contracts/oracle/osmosis/tests/test_admin.rs +++ b/contracts/oracle/osmosis/tests/test_admin.rs @@ -16,7 +16,6 @@ fn instantiating() { assert_eq!(cfg.owner.unwrap(), "owner".to_string()); assert_eq!(cfg.proposed_new_owner, None); assert_eq!(cfg.base_denom, "uosmo".to_string()); - assert_eq!(cfg.pyth_contract_addr, "pyth_contract".to_string()); } #[test] @@ -32,7 +31,6 @@ fn instantiating_incorrect_denom() { InstantiateMsg { owner: "owner".to_string(), base_denom: "!*jadfaefc".to_string(), - pyth_contract_addr: "pyth_contract_addr".to_string(), }, ); assert_eq!( @@ -49,7 +47,6 @@ fn instantiating_incorrect_denom() { InstantiateMsg { owner: "owner".to_string(), base_denom: "ahdbufenf&*!-".to_string(), - pyth_contract_addr: "pyth_contract_addr".to_string(), }, ); assert_eq!( @@ -67,7 +64,6 @@ fn instantiating_incorrect_denom() { InstantiateMsg { owner: "owner".to_string(), base_denom: "ab".to_string(), - pyth_contract_addr: "pyth_contract_addr".to_string(), }, ); assert_eq!( @@ -84,7 +80,6 @@ fn update_config_if_unauthorized() { let msg = ExecuteMsg::UpdateConfig { base_denom: None, - pyth_contract_addr: None, }; let info = mock_info("somebody"); let res_err = entry::execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); @@ -97,7 +92,6 @@ fn update_config_with_invalid_base_denom() { let msg = ExecuteMsg::UpdateConfig { base_denom: Some("*!fdskfna".to_string()), - pyth_contract_addr: None, }; let info = mock_info("owner"); let res_err = entry::execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); @@ -115,7 +109,6 @@ fn update_config_with_new_params() { let msg = ExecuteMsg::UpdateConfig { base_denom: Some("uusdc".to_string()), - pyth_contract_addr: Some("new_pyth_contract_addr".to_string()), }; let info = mock_info("owner"); let res = entry::execute(deps.as_mut(), mock_env(), info, msg).unwrap(); @@ -126,8 +119,6 @@ fn update_config_with_new_params() { attr("action", "update_config"), attr("prev_base_denom", "uosmo"), attr("base_denom", "uusdc"), - attr("prev_pyth_contract_addr", "pyth_contract"), - attr("pyth_contract_addr", "new_pyth_contract_addr") ] ); @@ -135,5 +126,4 @@ fn update_config_with_new_params() { assert_eq!(cfg.owner.unwrap(), "owner".to_string()); assert_eq!(cfg.proposed_new_owner, None); assert_eq!(cfg.base_denom, "uusdc".to_string()); - assert_eq!(cfg.pyth_contract_addr, "new_pyth_contract_addr".to_string()); } diff --git a/contracts/oracle/osmosis/tests/test_query_price.rs b/contracts/oracle/osmosis/tests/test_query_price.rs index 222840fdd..ff18b95bf 100644 --- a/contracts/oracle/osmosis/tests/test_query_price.rs +++ b/contracts/oracle/osmosis/tests/test_query_price.rs @@ -502,6 +502,7 @@ fn setup_pyth_and_geometric_twap_for_lsd( deps.as_mut(), "uatom", OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), price_feed_id: price_id, max_staleness: 1800u64, }, @@ -847,6 +848,7 @@ fn querying_pyth_price_if_publish_price_too_old() { deps.as_mut(), "uatom", OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), price_feed_id: price_id, max_staleness, }, @@ -907,6 +909,7 @@ fn querying_pyth_price_if_signed() { deps.as_mut(), "uatom", OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), price_feed_id: price_id, max_staleness, }, @@ -964,6 +967,7 @@ fn querying_pyth_price_successfully() { deps.as_mut(), "uatom", OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), price_feed_id: price_id, max_staleness, }, diff --git a/contracts/oracle/osmosis/tests/test_set_price_source.rs b/contracts/oracle/osmosis/tests/test_set_price_source.rs index 583604bbb..2da1ee4ce 100644 --- a/contracts/oracle/osmosis/tests/test_set_price_source.rs +++ b/contracts/oracle/osmosis/tests/test_set_price_source.rs @@ -954,6 +954,7 @@ fn setting_price_source_pyth_successfully() { ExecuteMsg::SetPriceSource { denom: "uatom".to_string(), price_source: OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "new_pyth_contract_addr".to_string(), price_feed_id: PriceIdentifier::from_hex( "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", ) @@ -974,6 +975,7 @@ fn setting_price_source_pyth_successfully() { assert_eq!( res.price_source, OsmosisPriceSourceChecked::Pyth { + contract_addr: Addr::unchecked("new_pyth_contract_addr"), price_feed_id: PriceIdentifier::from_hex( "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3" ) diff --git a/integration-tests/tests/test_oracles.rs b/integration-tests/tests/test_oracles.rs index 863360aa8..10ff04208 100644 --- a/integration-tests/tests/test_oracles.rs +++ b/integration-tests/tests/test_oracles.rs @@ -37,8 +37,6 @@ const OSMOSIS_ADDR_PROVIDER_CONTRACT_NAME: &str = "mars-address-provider"; const OSMOSIS_REWARDS_CONTRACT_NAME: &str = "mars-rewards-collector-osmosis"; const OSMOSIS_INCENTIVES_CONTRACT_NAME: &str = "mars-incentives"; -const OSMOSIS_PYTH_ADDR: &str = "osmo1svg55quy7jjee6dn0qx85qxxvx5cafkkw4tmqpcjr9dx99l0zrhs4usft5"; // correct bech32 addr to pass validation - #[test] fn querying_xyk_lp_price_if_no_price_for_tokens() { let app = OsmosisTestApp::new(); @@ -59,7 +57,6 @@ fn querying_xyk_lp_price_if_no_price_for_tokens() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), - pyth_contract_addr: OSMOSIS_PYTH_ADDR.to_string(), }, ); @@ -113,7 +110,6 @@ fn querying_xyk_lp_price_success() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), - pyth_contract_addr: OSMOSIS_PYTH_ADDR.to_string(), }, ); @@ -222,7 +218,6 @@ fn query_spot_price() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), - pyth_contract_addr: OSMOSIS_PYTH_ADDR.to_string(), }, ); @@ -287,7 +282,6 @@ fn set_spot_without_pools() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), - pyth_contract_addr: OSMOSIS_PYTH_ADDR.to_string(), }, ); @@ -325,7 +319,6 @@ fn incorrect_pool_for_spot() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), - pyth_contract_addr: OSMOSIS_PYTH_ADDR.to_string(), }, ); @@ -372,7 +365,6 @@ fn update_spot_with_different_pool() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), - pyth_contract_addr: OSMOSIS_PYTH_ADDR.to_string(), }, ); @@ -447,7 +439,6 @@ fn query_spot_price_after_lp_change() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), - pyth_contract_addr: OSMOSIS_PYTH_ADDR.to_string(), }, ); @@ -507,7 +498,6 @@ fn query_geometric_twap_price_with_downtime_detector() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), - pyth_contract_addr: OSMOSIS_PYTH_ADDR.to_string(), }, ); @@ -592,7 +582,6 @@ fn query_arithmetic_twap_price() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), - pyth_contract_addr: OSMOSIS_PYTH_ADDR.to_string(), }, ); @@ -679,7 +668,6 @@ fn query_geometric_twap_price() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), - pyth_contract_addr: OSMOSIS_PYTH_ADDR.to_string(), }, ); @@ -766,7 +754,6 @@ fn compare_spot_and_twap_price() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), - pyth_contract_addr: OSMOSIS_PYTH_ADDR.to_string(), }, ); @@ -1027,7 +1014,6 @@ fn setup_redbank(wasm: &Wasm, signer: &SigningAccount) -> (Strin &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), - pyth_contract_addr: OSMOSIS_PYTH_ADDR.to_string(), }, ); diff --git a/packages/testing/src/integration/mock_env.rs b/packages/testing/src/integration/mock_env.rs index 2203eecc1..98f77ab69 100644 --- a/packages/testing/src/integration/mock_env.rs +++ b/packages/testing/src/integration/mock_env.rs @@ -613,7 +613,6 @@ impl MockEnvBuilder { &oracle::InstantiateMsg { owner: self.owner.to_string(), base_denom: self.base_denom.clone(), - pyth_contract_addr: self.pyth_contract_addr.clone(), }, &[], "oracle", diff --git a/packages/types/src/oracle.rs b/packages/types/src/oracle.rs index 330f253e6..d0be5836c 100644 --- a/packages/types/src/oracle.rs +++ b/packages/types/src/oracle.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Decimal}; +use cosmwasm_std::Decimal; use mars_owner::OwnerUpdate; #[cw_serde] @@ -8,8 +8,6 @@ pub struct InstantiateMsg { pub owner: String, /// The asset in which prices are denominated in pub base_denom: String, - /// Contract address of Pyth - pub pyth_contract_addr: String, } #[cw_serde] @@ -18,12 +16,6 @@ pub struct Config { pub base_denom: String, } -#[cw_serde] -pub struct PythConfig { - /// Contract address of Pyth - pub pyth_contract_addr: Addr, -} - #[cw_serde] pub enum ExecuteMsg { /// Specify the price source to be used for a coin @@ -39,11 +31,9 @@ pub enum ExecuteMsg { }, /// Manages admin role state UpdateOwner(OwnerUpdate), - /// Update contract config (only callable by owner) UpdateConfig { base_denom: Option, - pyth_contract_addr: Option, }, } @@ -95,8 +85,6 @@ pub struct ConfigResponse { pub proposed_new_owner: Option, /// The asset in which prices are denominated in pub base_denom: String, - /// Contract address of Pyth - pub pyth_contract_addr: String, } #[cw_serde] diff --git a/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json b/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json index 55ffae578..4ed1a18f8 100644 --- a/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json +++ b/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json @@ -8,8 +8,7 @@ "type": "object", "required": [ "base_denom", - "owner", - "pyth_contract_addr" + "owner" ], "properties": { "base_denom": { @@ -19,10 +18,6 @@ "owner": { "description": "The contract's owner, who can update config and price sources", "type": "string" - }, - "pyth_contract_addr": { - "description": "Contract address of Pyth", - "type": "string" } }, "additionalProperties": false @@ -107,12 +102,6 @@ "string", "null" ] - }, - "pyth_contract_addr": { - "type": [ - "string", - "null" - ] } }, "additionalProperties": false @@ -422,10 +411,15 @@ "pyth": { "type": "object", "required": [ + "contract_addr", "max_staleness", "price_feed_id" ], "properties": { + "contract_addr": { + "description": "Contract address of Pyth", + "type": "string" + }, "max_staleness": { "description": "The maximum number of seconds since the last price was by an oracle, before rejecting the price as too stale", "type": "integer", @@ -716,8 +710,7 @@ "title": "ConfigResponse", "type": "object", "required": [ - "base_denom", - "pyth_contract_addr" + "base_denom" ], "properties": { "base_denom": { @@ -737,10 +730,6 @@ "string", "null" ] - }, - "pyth_contract_addr": { - "description": "Contract address of Pyth", - "type": "string" } }, "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 ef1b487e0..d125fda51 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts @@ -141,10 +141,8 @@ export interface MarsOracleOsmosisInterface extends MarsOracleOsmosisReadOnlyInt updateConfig: ( { baseDenom, - pythContractAddr, }: { baseDenom?: string - pythContractAddr?: string }, fee?: number | StdFee | 'auto', memo?: string, @@ -239,10 +237,8 @@ export class MarsOracleOsmosisClient updateConfig = async ( { baseDenom, - pythContractAddr, }: { baseDenom?: string - pythContractAddr?: string }, fee: number | StdFee | 'auto' = 'auto', memo?: string, @@ -254,7 +250,6 @@ export class MarsOracleOsmosisClient { update_config: { base_denom: baseDenom, - pyth_contract_addr: pythContractAddr, }, }, fee, 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 4abed44af..9af54f39a 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.react-query.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.react-query.ts @@ -171,7 +171,6 @@ export interface MarsOracleOsmosisUpdateConfigMutation { client: MarsOracleOsmosisClient msg: { baseDenom?: string - pythContractAddr?: string } args?: { fee?: number | StdFee | 'auto' diff --git a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts index 3d4cf6fbd..1917f7a5f 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts @@ -8,7 +8,6 @@ export interface InstantiateMsg { base_denom: string owner: string - pyth_contract_addr: string } export type ExecuteMsg = | { @@ -28,7 +27,6 @@ export type ExecuteMsg = | { update_config: { base_denom?: string | null - pyth_contract_addr?: string | null } } export type OsmosisPriceSourceForString = @@ -77,6 +75,7 @@ export type OsmosisPriceSourceForString = } | { pyth: { + contract_addr: string max_staleness: number price_feed_id: Identifier [k: string]: unknown @@ -179,7 +178,6 @@ export interface ConfigResponse { base_denom: string owner?: string | null proposed_new_owner?: string | null - pyth_contract_addr: string } export interface PriceResponse { denom: string From ce78f5d4e2923ccb1da49a5f30d4fc6dd559420f Mon Sep 17 00:00:00 2001 From: piobab Date: Tue, 6 Jun 2023 23:57:44 +0200 Subject: [PATCH 05/11] Update decimals (#194) * Add decimals for Pyth prices. * Update schema. * Fix fmt. * Fix missing cw dep. * Add base_denom_decimals for price normalization in Pyth. * Add config arg. Change query_price signature. * Move fmt test cases. * Update comment. * Fix clippy. * Update comment. * Use wasm from optimizer for integration tests. * Fix pipeline. --- Cargo.lock | 14 +- Cargo.toml | 2 +- contracts/oracle/base/src/contract.rs | 22 +- contracts/oracle/base/src/traits.rs | 6 +- contracts/oracle/osmosis/src/lib.rs | 2 +- contracts/oracle/osmosis/src/price_source.rs | 273 +++++++----------- contracts/oracle/osmosis/tests/helpers.rs | 2 + contracts/oracle/osmosis/tests/test_admin.rs | 9 + .../osmosis/tests/test_price_source_fmt.rs | 157 ++++++++++ .../oracle/osmosis/tests/test_query_price.rs | 8 +- .../osmosis/tests/test_set_price_source.rs | 2 + integration-tests/tests/test_oracles.rs | 12 + packages/testing/src/integration/mock_env.rs | 3 + packages/types/src/oracle.rs | 7 + .../mars-oracle-osmosis.json | 31 +- .../MarsOracleOsmosis.client.ts | 5 + .../MarsOracleOsmosis.react-query.ts | 1 + .../MarsOracleOsmosis.types.ts | 4 + 18 files changed, 362 insertions(+), 198 deletions(-) create mode 100644 contracts/oracle/osmosis/tests/test_price_source_fmt.rs diff --git a/Cargo.lock b/Cargo.lock index c0d4a6cfb..1dcb1c037 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -529,7 +529,7 @@ dependencies = [ [[package]] name = "cw2" version = "1.0.1" -source = "git+https://github.com/mars-protocol/cw-plus?rev=4014255#4014255fc2d79486e332e02d1ab1421db86f4f0b" +source = "git+https://github.com/CosmWasm/cw-plus?rev=de1fb0b#de1fb0b9836e56e5640575d246274d882509d714" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1166,7 +1166,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus", - "cw2 1.0.1 (git+https://github.com/mars-protocol/cw-plus?rev=4014255)", + "cw2 1.0.1 (git+https://github.com/CosmWasm/cw-plus?rev=de1fb0b)", "mars-owner", "mars-red-bank-types", "serde", @@ -1190,7 +1190,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus", - "cw2 1.0.1 (git+https://github.com/mars-protocol/cw-plus?rev=4014255)", + "cw2 1.0.1 (git+https://github.com/CosmWasm/cw-plus?rev=de1fb0b)", "mars-owner", "mars-red-bank-types", "mars-testing", @@ -1225,7 +1225,7 @@ version = "1.0.1" dependencies = [ "cosmwasm-std", "cw-storage-plus", - "cw2 1.0.1 (git+https://github.com/mars-protocol/cw-plus?rev=4014255)", + "cw2 1.0.1 (git+https://github.com/CosmWasm/cw-plus?rev=de1fb0b)", "mars-owner", "mars-red-bank-types", "mars-utils", @@ -1241,7 +1241,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus", - "cw2 1.0.1 (git+https://github.com/mars-protocol/cw-plus?rev=4014255)", + "cw2 1.0.1 (git+https://github.com/CosmWasm/cw-plus?rev=de1fb0b)", "mars-oracle-base", "mars-osmosis", "mars-owner", @@ -1285,7 +1285,7 @@ dependencies = [ "cosmwasm-std", "cw-storage-plus", "cw-utils", - "cw2 1.0.1 (git+https://github.com/mars-protocol/cw-plus?rev=4014255)", + "cw2 1.0.1 (git+https://github.com/CosmWasm/cw-plus?rev=de1fb0b)", "mars-health", "mars-owner", "mars-red-bank-types", @@ -1327,7 +1327,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus", - "cw2 1.0.1 (git+https://github.com/mars-protocol/cw-plus?rev=4014255)", + "cw2 1.0.1 (git+https://github.com/CosmWasm/cw-plus?rev=de1fb0b)", "mars-osmosis", "mars-owner", "mars-red-bank-types", diff --git a/Cargo.toml b/Cargo.toml index 574793705..23988ae8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ anyhow = "1.0.68" bech32 = "0.9.1" cosmwasm-schema = "1.1.9" cosmwasm-std = "1.1.9" -cw2 = { git = "https://github.com/mars-protocol/cw-plus", rev = "4014255" } +cw2 = { git = "https://github.com/CosmWasm/cw-plus", rev = "de1fb0b" } cw-multi-test = "0.16.1" cw-storage-plus = "1.0.1" cw-utils = "1.0.1" diff --git a/contracts/oracle/base/src/contract.rs b/contracts/oracle/base/src/contract.rs index 8fb78fb0a..8d9e7624b 100644 --- a/contracts/oracle/base/src/contract.rs +++ b/contracts/oracle/base/src/contract.rs @@ -73,6 +73,7 @@ where deps.storage, &Config { base_denom: msg.base_denom, + base_denom_decimals: msg.base_denom_decimals, }, )?; @@ -96,7 +97,8 @@ where } => self.remove_price_source(deps, info.sender, denom), ExecuteMsg::UpdateConfig { base_denom, - } => self.update_config(deps, info.sender, base_denom), + base_denom_decimals, + } => self.update_config(deps, info.sender, base_denom, base_denom_decimals), } } @@ -171,6 +173,7 @@ where deps: DepsMut, sender_addr: Addr, base_denom: Option, + base_demom_decimals: Option, ) -> ContractResult { self.owner.assert_owner(deps.storage, &sender_addr)?; @@ -180,13 +183,17 @@ where let mut config = self.config.load(deps.storage)?; let prev_base_denom = config.base_denom.clone(); + let prev_base_denom_decimals = config.base_denom_decimals; config.base_denom = base_denom.unwrap_or(config.base_denom); + config.base_denom_decimals = base_demom_decimals.unwrap_or(config.base_denom_decimals); self.config.save(deps.storage, &config)?; let response = Response::new() .add_attribute("action", "update_config") .add_attribute("prev_base_denom", prev_base_denom) - .add_attribute("base_denom", config.base_denom); + .add_attribute("base_denom", config.base_denom) + .add_attribute("prev_base_denom_decimals", prev_base_denom_decimals.to_string()) + .add_attribute("base_denom_decimals", config.base_denom_decimals.to_string()); Ok(response) } @@ -198,6 +205,7 @@ where owner: owner_state.owner, proposed_new_owner: owner_state.proposed, base_denom: cfg.base_denom, + base_denom_decimals: cfg.base_denom_decimals, }) } @@ -238,13 +246,7 @@ where let cfg = self.config.load(deps.storage)?; let price_source = self.price_sources.load(deps.storage, &denom)?; Ok(PriceResponse { - price: price_source.query_price( - &deps, - &env, - &denom, - &cfg.base_denom, - &self.price_sources, - )?, + price: price_source.query_price(&deps, &env, &denom, &cfg, &self.price_sources)?, denom, }) } @@ -267,7 +269,7 @@ where .map(|item| { let (k, v) = item?; Ok(PriceResponse { - price: v.query_price(&deps, &env, &k, &cfg.base_denom, &self.price_sources)?, + price: v.query_price(&deps, &env, &k, &cfg, &self.price_sources)?, denom: k, }) }) diff --git a/contracts/oracle/base/src/traits.rs b/contracts/oracle/base/src/traits.rs index 0f21d67f8..75792221a 100644 --- a/contracts/oracle/base/src/traits.rs +++ b/contracts/oracle/base/src/traits.rs @@ -2,6 +2,7 @@ use std::fmt::{Debug, Display}; use cosmwasm_std::{CustomQuery, Decimal, Deps, Env}; use cw_storage_plus::Map; +use mars_red_bank_types::oracle::Config; use schemars::JsonSchema; use serde::{de::DeserializeOwned, Serialize}; @@ -28,9 +29,10 @@ where /// /// - `denom`: The coin whose price is to be queried. /// - /// - `base_denom`: The coin in which the price is to be denominated in. + /// - `config.base_denom`: The coin in which the price is to be denominated in. /// For example, if `denom` is uatom and `base_denom` is uosmo, the /// function should return how many uosmo is per one uatom. + /// - `config.base_denom_decimals`: The coin decimals used for price normalization. /// /// - `price_sources`: A map that stores the price source for each coin. /// This is necessary because for some coins, in order to calculate its @@ -41,7 +43,7 @@ where deps: &Deps, env: &Env, denom: &str, - base_denom: &str, + config: &Config, price_sources: &Map<&str, Self>, ) -> ContractResult; } diff --git a/contracts/oracle/osmosis/src/lib.rs b/contracts/oracle/osmosis/src/lib.rs index a81f5fd97..31089de17 100644 --- a/contracts/oracle/osmosis/src/lib.rs +++ b/contracts/oracle/osmosis/src/lib.rs @@ -6,6 +6,6 @@ mod price_source; pub mod stride; pub use price_source::{ - scale_to_exponent, Downtime, DowntimeDetector, GeometricTwap, OsmosisPriceSourceChecked, + scale_pyth_price, Downtime, DowntimeDetector, GeometricTwap, OsmosisPriceSourceChecked, OsmosisPriceSourceUnchecked, RedemptionRate, }; diff --git a/contracts/oracle/osmosis/src/price_source.rs b/contracts/oracle/osmosis/src/price_source.rs index 3922fb210..97eed6ca1 100644 --- a/contracts/oracle/osmosis/src/price_source.rs +++ b/contracts/oracle/osmosis/src/price_source.rs @@ -9,6 +9,7 @@ use mars_osmosis::helpers::{ query_arithmetic_twap_price, query_geometric_twap_price, query_pool, query_spot_price, recovered_since_downtime_of_length, Pool, }; +use mars_red_bank_types::oracle::Config; use pyth_sdk_cw::{query_price_feed, PriceIdentifier}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -160,6 +161,21 @@ pub enum OsmosisPriceSource { /// The maximum number of seconds since the last price was by an oracle, before /// rejecting the price as too stale max_staleness: u64, + + /// Assets are represented in their smallest unit and every asset can have different decimals (e.g. OSMO - 6 decimals, WETH - 18 decimals). + /// + /// Pyth prices are denominated in USD so basically it means how much 1 USDC, 1 ATOM, 1 OSMO is worth in USD (NOT 1 uusdc, 1 uatom, 1 uosmo). + /// We have to normalize it. We should get how much 1 utoken is worth in uusd. For example: + /// denom_decimals (OSMO) = 6 + /// base_denom_decimals (USD) = 6 + /// + /// 1 OSMO = 10^6 uosmo + /// 1 USD = 10^6 uusd + /// + /// osmo_price_in_usd = 0.59958994 + /// uosmo_price_in_uusd = osmo_price_in_usd / 10^denom_decimals * 10^base_denom_decimals = + /// uosmo_price_in_uusd = 0.59958994 * 10^(-6) * 10^6 = 0.59958994 + denom_decimals: u8, }, /// Liquid Staking Derivatives (LSD) price quoted in USD based on data from Pyth, Osmosis and Stride. /// @@ -258,8 +274,9 @@ impl fmt::Display for OsmosisPriceSourceChecked { contract_addr, price_feed_id, max_staleness, + denom_decimals, } => { - format!("pyth:{contract_addr}:{price_feed_id}:{max_staleness}") + format!("pyth:{contract_addr}:{price_feed_id}:{max_staleness}:{denom_decimals}") } OsmosisPriceSource::Lsd { transitive_denom, @@ -362,10 +379,12 @@ impl PriceSourceUnchecked for OsmosisPriceSour contract_addr, price_feed_id, max_staleness, + denom_decimals, } => Ok(OsmosisPriceSourceChecked::Pyth { contract_addr: deps.api.addr_validate(contract_addr)?, price_feed_id: *price_feed_id, max_staleness: *max_staleness, + denom_decimals: *denom_decimals, }), OsmosisPriceSourceUnchecked::Lsd { transitive_denom, @@ -397,7 +416,7 @@ impl PriceSourceChecked for OsmosisPriceSourceChecked { deps: &Deps, env: &Env, denom: &str, - base_denom: &str, + config: &Config, price_sources: &Map<&str, Self>, ) -> ContractResult { match self { @@ -406,7 +425,8 @@ impl PriceSourceChecked for OsmosisPriceSourceChecked { } => Ok(*price), OsmosisPriceSourceChecked::Spot { pool_id, - } => query_spot_price(&deps.querier, *pool_id, denom, base_denom).map_err(Into::into), + } => query_spot_price(&deps.querier, *pool_id, denom, &config.base_denom) + .map_err(Into::into), OsmosisPriceSourceChecked::ArithmeticTwap { pool_id, window_size, @@ -415,8 +435,14 @@ impl PriceSourceChecked for OsmosisPriceSourceChecked { Self::chain_recovered(deps, downtime_detector)?; let start_time = env.block.time.seconds() - window_size; - query_arithmetic_twap_price(&deps.querier, *pool_id, denom, base_denom, start_time) - .map_err(Into::into) + query_arithmetic_twap_price( + &deps.querier, + *pool_id, + denom, + &config.base_denom, + start_time, + ) + .map_err(Into::into) } OsmosisPriceSourceChecked::GeometricTwap { pool_id, @@ -426,18 +452,18 @@ impl PriceSourceChecked for OsmosisPriceSourceChecked { Self::chain_recovered(deps, downtime_detector)?; let start_time = env.block.time.seconds() - window_size; - query_geometric_twap_price(&deps.querier, *pool_id, denom, base_denom, start_time) - .map_err(Into::into) + query_geometric_twap_price( + &deps.querier, + *pool_id, + denom, + &config.base_denom, + start_time, + ) + .map_err(Into::into) } OsmosisPriceSourceChecked::XykLiquidityToken { pool_id, - } => Self::query_xyk_liquidity_token_price( - deps, - env, - *pool_id, - base_denom, - price_sources, - ), + } => Self::query_xyk_liquidity_token_price(deps, env, *pool_id, config, price_sources), OsmosisPriceSourceChecked::StakedGeometricTwap { transitive_denom, pool_id, @@ -451,9 +477,9 @@ impl PriceSourceChecked for OsmosisPriceSourceChecked { env, denom, transitive_denom, - base_denom, *pool_id, *window_size, + config, price_sources, ) } @@ -461,12 +487,15 @@ impl PriceSourceChecked for OsmosisPriceSourceChecked { contract_addr, price_feed_id, max_staleness, + denom_decimals, } => Ok(Self::query_pyth_price( deps, env, + config, contract_addr.to_owned(), *price_feed_id, *max_staleness, + *denom_decimals, )?), OsmosisPriceSourceChecked::Lsd { transitive_denom, @@ -480,9 +509,9 @@ impl PriceSourceChecked for OsmosisPriceSourceChecked { env, denom, transitive_denom, - base_denom, geometric_twap.clone(), redemption_rate.clone(), + config, price_sources, ) } @@ -519,7 +548,7 @@ impl OsmosisPriceSourceChecked { deps: &Deps, env: &Env, pool_id: u64, - base_denom: &str, + config: &Config, price_sources: &Map<&str, Self>, ) -> ContractResult { // XYK pool asserted during price source creation @@ -532,14 +561,14 @@ impl OsmosisPriceSourceChecked { deps, env, &coin0.denom, - base_denom, + config, price_sources, )?; let coin1_price = price_sources.load(deps.storage, &coin1.denom)?.query_price( deps, env, &coin1.denom, - base_denom, + config, price_sources, )?; @@ -567,9 +596,9 @@ impl OsmosisPriceSourceChecked { env: &Env, denom: &str, transitive_denom: &str, - base_denom: &str, pool_id: u64, window_size: u64, + config: &Config, price_sources: &Map<&str, OsmosisPriceSourceChecked>, ) -> ContractResult { let start_time = env.block.time.seconds() - window_size; @@ -586,7 +615,7 @@ impl OsmosisPriceSourceChecked { deps, env, transitive_denom, - base_denom, + config, price_sources, )?; @@ -604,9 +633,9 @@ impl OsmosisPriceSourceChecked { env: &Env, denom: &str, transitive_denom: &str, - base_denom: &str, geometric_twap: GeometricTwap, redemption_rate: RedemptionRate, + config: &Config, price_sources: &Map<&str, OsmosisPriceSourceChecked>, ) -> ContractResult { let current_time = env.block.time.seconds(); @@ -644,7 +673,7 @@ impl OsmosisPriceSourceChecked { deps, env, transitive_denom, - base_denom, + config, price_sources, )?; @@ -654,9 +683,11 @@ impl OsmosisPriceSourceChecked { fn query_pyth_price( deps: &Deps, env: &Env, + config: &Config, contract_addr: Addr, price_feed_id: PriceIdentifier, max_staleness: u64, + denom_decimals: u8, ) -> ContractResult { let current_time = env.block.time.seconds(); @@ -683,7 +714,12 @@ impl OsmosisPriceSourceChecked { }); } - let current_price_dec = scale_to_exponent(current_price.price as u128, current_price.expo)?; + let current_price_dec = scale_pyth_price( + current_price.price as u128, + current_price.expo, + denom_decimals, + config.base_denom_decimals, + )?; Ok(current_price_dec) } @@ -698,7 +734,35 @@ impl OsmosisPriceSourceChecked { /// conf: 574566 /// price: 1365133270 /// The confidence interval is 574566 * 10^(-8) = $0.00574566, and the price is 1365133270 * 10^(-8) = $13.6513327. -pub fn scale_to_exponent(value: u128, expo: i32) -> ContractResult { +/// +/// Moreover, we have to represent the price for utoken in uusd (instead of token/USD). +/// Pyth price should be normalized with token decimals. +/// +/// Let's try to convert ATOM/USD reported by Pyth to uatom/uusd: +/// denom_decimals (ATOM) = 6 +/// base_denom_decimals (USD) = 6 +/// +/// 1 ATOM = 10^6 uatom +/// 1 USD = 10^6 uusd +/// /// +/// 1 ATOM = price * 10^expo USD +/// 10^6 uatom = price * 10^expo * 10^6 uusd +/// uatom = price * 10^expo * 10^6 / 10^6 uusd +/// uatom = price * 10^expo * 10^6 * 10^(-6) uusd +/// uatom/uusd = 1365133270 * 10^(-8) * 10^6 * 10^(-6) +/// uatom/uusd = 1365133270 * 10^(-8) = 13.6513327 +/// +/// Generalized formula: +/// utoken/uusd = price * 10^expo * 10^base_denom_decimals * 10^(-denom_decimals) +/// +/// NOTE: if we don't introduce base_denom decimals we can overflow. +pub fn scale_pyth_price( + value: u128, + expo: i32, + denom_decimals: u8, + base_denom_decimals: u8, +) -> ContractResult { + let expo = expo - denom_decimals as i32 + base_denom_decimals as i32; let target_expo = Uint128::from(10u8).checked_pow(expo.unsigned_abs())?; if expo < 0 { Ok(Decimal::checked_from_ratio(value, target_expo)?) @@ -710,157 +774,18 @@ pub fn scale_to_exponent(value: u128, expo: i32) -> ContractResult { #[cfg(test)] mod tests { - use super::*; - - #[test] - fn display_downtime_detector() { - let dd = DowntimeDetector { - downtime: Downtime::Duration10m, - recovery: 550, - }; - assert_eq!(dd.to_string(), "Duration10m:550") - } - - #[test] - fn display_fixed_price_source() { - let ps = OsmosisPriceSourceChecked::Fixed { - price: Decimal::from_ratio(1u128, 2u128), - }; - assert_eq!(ps.to_string(), "fixed:0.5") - } - - #[test] - fn display_spot_price_source() { - let ps = OsmosisPriceSourceChecked::Spot { - pool_id: 123, - }; - assert_eq!(ps.to_string(), "spot:123") - } - - #[test] - fn display_arithmetic_twap_price_source() { - let ps = OsmosisPriceSourceChecked::ArithmeticTwap { - pool_id: 123, - window_size: 300, - downtime_detector: None, - }; - assert_eq!(ps.to_string(), "arithmetic_twap:123:300:None"); - - let ps = OsmosisPriceSourceChecked::ArithmeticTwap { - pool_id: 123, - window_size: 300, - downtime_detector: Some(DowntimeDetector { - downtime: Downtime::Duration30m, - recovery: 568, - }), - }; - assert_eq!(ps.to_string(), "arithmetic_twap:123:300:Some(Duration30m:568)"); - } - - #[test] - fn display_geometric_twap_price_source() { - let ps = OsmosisPriceSourceChecked::GeometricTwap { - pool_id: 123, - window_size: 300, - downtime_detector: None, - }; - assert_eq!(ps.to_string(), "geometric_twap:123:300:None"); - - let ps = OsmosisPriceSourceChecked::GeometricTwap { - pool_id: 123, - window_size: 300, - downtime_detector: Some(DowntimeDetector { - downtime: Downtime::Duration30m, - recovery: 568, - }), - }; - assert_eq!(ps.to_string(), "geometric_twap:123:300:Some(Duration30m:568)"); - } + use std::str::FromStr; - #[test] - fn display_staked_geometric_twap_price_source() { - let ps = OsmosisPriceSourceChecked::StakedGeometricTwap { - transitive_denom: "transitive".to_string(), - pool_id: 123, - window_size: 300, - downtime_detector: None, - }; - assert_eq!(ps.to_string(), "staked_geometric_twap:transitive:123:300:None"); - - let ps = OsmosisPriceSourceChecked::StakedGeometricTwap { - transitive_denom: "transitive".to_string(), - pool_id: 123, - window_size: 300, - downtime_detector: Some(DowntimeDetector { - downtime: Downtime::Duration30m, - recovery: 568, - }), - }; - assert_eq!( - ps.to_string(), - "staked_geometric_twap:transitive:123:300:Some(Duration30m:568)" - ); - } - - #[test] - fn display_xyk_lp_price_source() { - let ps = OsmosisPriceSourceChecked::XykLiquidityToken { - pool_id: 224, - }; - assert_eq!(ps.to_string(), "xyk_liquidity_token:224") - } - - #[test] - fn display_pyth_price_source() { - let ps = OsmosisPriceSourceChecked::Pyth { - contract_addr: Addr::unchecked("osmo12j43nf2f0qumnt2zrrmpvnsqgzndxefujlvr08"), - price_feed_id: PriceIdentifier::from_hex( - "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", - ) - .unwrap(), - max_staleness: 60, - }; - assert_eq!( - ps.to_string(), - "pyth:osmo12j43nf2f0qumnt2zrrmpvnsqgzndxefujlvr08:0x61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3:60" - ) - } + use super::*; #[test] - fn display_lsd_price_source() { - let ps = OsmosisPriceSourceChecked::Lsd { - transitive_denom: "transitive".to_string(), - geometric_twap: GeometricTwap { - pool_id: 456, - window_size: 380, - downtime_detector: None, - }, - redemption_rate: RedemptionRate { - contract_addr: Addr::unchecked( - "osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc", - ), - max_staleness: 1234, - }, - }; - assert_eq!(ps.to_string(), "lsd:transitive:456:380:None:osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc:1234"); - - let ps = OsmosisPriceSourceChecked::Lsd { - transitive_denom: "transitive".to_string(), - geometric_twap: GeometricTwap { - pool_id: 456, - window_size: 380, - downtime_detector: Some(DowntimeDetector { - downtime: Downtime::Duration30m, - recovery: 552, - }), - }, - redemption_rate: RedemptionRate { - contract_addr: Addr::unchecked( - "osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc", - ), - max_staleness: 1234, - }, - }; - assert_eq!(ps.to_string(), "lsd:transitive:456:380:Some(Duration30m:552):osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc:1234"); + fn scale_real_pyth_price() { + // ATOM + let uatom_price_in_usd = scale_pyth_price(1035200881u128, -8, 6u8, 6u8).unwrap(); + assert_eq!(uatom_price_in_usd, Decimal::from_str("10.35200881").unwrap()); + + // ETH + let ueth_price_in_usd = scale_pyth_price(181598000001u128, -8, 18u8, 6u8).unwrap(); + assert_eq!(ueth_price_in_usd, Decimal::from_str("0.00000000181598").unwrap()); } } diff --git a/contracts/oracle/osmosis/tests/helpers.rs b/contracts/oracle/osmosis/tests/helpers.rs index ce158e308..26c41f2d9 100644 --- a/contracts/oracle/osmosis/tests/helpers.rs +++ b/contracts/oracle/osmosis/tests/helpers.rs @@ -90,6 +90,7 @@ pub fn setup_test() -> OwnedDeps { InstantiateMsg { owner: "owner".to_string(), base_denom: "uosmo".to_string(), + base_denom_decimals: 6u8, }, ) .unwrap(); @@ -147,6 +148,7 @@ pub fn set_pyth_price_source(deps: DepsMut, denom: &str, price_id: PriceIdentifi contract_addr: "pyth_contract".to_string(), price_feed_id: price_id, max_staleness: 30, + denom_decimals: 6, }, ) } diff --git a/contracts/oracle/osmosis/tests/test_admin.rs b/contracts/oracle/osmosis/tests/test_admin.rs index 405368226..854845ee6 100644 --- a/contracts/oracle/osmosis/tests/test_admin.rs +++ b/contracts/oracle/osmosis/tests/test_admin.rs @@ -31,6 +31,7 @@ fn instantiating_incorrect_denom() { InstantiateMsg { owner: "owner".to_string(), base_denom: "!*jadfaefc".to_string(), + base_denom_decimals: 6u8, }, ); assert_eq!( @@ -47,6 +48,7 @@ fn instantiating_incorrect_denom() { InstantiateMsg { owner: "owner".to_string(), base_denom: "ahdbufenf&*!-".to_string(), + base_denom_decimals: 6u8, }, ); assert_eq!( @@ -64,6 +66,7 @@ fn instantiating_incorrect_denom() { InstantiateMsg { owner: "owner".to_string(), base_denom: "ab".to_string(), + base_denom_decimals: 6u8, }, ); assert_eq!( @@ -80,6 +83,7 @@ fn update_config_if_unauthorized() { let msg = ExecuteMsg::UpdateConfig { base_denom: None, + base_denom_decimals: None, }; let info = mock_info("somebody"); let res_err = entry::execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); @@ -92,6 +96,7 @@ fn update_config_with_invalid_base_denom() { let msg = ExecuteMsg::UpdateConfig { base_denom: Some("*!fdskfna".to_string()), + base_denom_decimals: None, }; let info = mock_info("owner"); let res_err = entry::execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); @@ -109,6 +114,7 @@ fn update_config_with_new_params() { let msg = ExecuteMsg::UpdateConfig { base_denom: Some("uusdc".to_string()), + base_denom_decimals: Some(10u8), }; let info = mock_info("owner"); let res = entry::execute(deps.as_mut(), mock_env(), info, msg).unwrap(); @@ -119,6 +125,8 @@ fn update_config_with_new_params() { attr("action", "update_config"), attr("prev_base_denom", "uosmo"), attr("base_denom", "uusdc"), + attr("prev_base_denom_decimals", "6"), + attr("base_denom_decimals", "10"), ] ); @@ -126,4 +134,5 @@ fn update_config_with_new_params() { assert_eq!(cfg.owner.unwrap(), "owner".to_string()); assert_eq!(cfg.proposed_new_owner, None); assert_eq!(cfg.base_denom, "uusdc".to_string()); + assert_eq!(cfg.base_denom_decimals, 10u8); } diff --git a/contracts/oracle/osmosis/tests/test_price_source_fmt.rs b/contracts/oracle/osmosis/tests/test_price_source_fmt.rs new file mode 100644 index 000000000..de4085329 --- /dev/null +++ b/contracts/oracle/osmosis/tests/test_price_source_fmt.rs @@ -0,0 +1,157 @@ +use cosmwasm_std::{Addr, Decimal}; +use mars_oracle_osmosis::{ + Downtime, DowntimeDetector, GeometricTwap, OsmosisPriceSourceChecked, RedemptionRate, +}; +use pyth_sdk_cw::PriceIdentifier; + +mod helpers; + +#[test] +fn display_downtime_detector() { + let dd = DowntimeDetector { + downtime: Downtime::Duration10m, + recovery: 550, + }; + assert_eq!(dd.to_string(), "Duration10m:550") +} + +#[test] +fn display_fixed_price_source() { + let ps = OsmosisPriceSourceChecked::Fixed { + price: Decimal::from_ratio(1u128, 2u128), + }; + assert_eq!(ps.to_string(), "fixed:0.5") +} + +#[test] +fn display_spot_price_source() { + let ps = OsmosisPriceSourceChecked::Spot { + pool_id: 123, + }; + assert_eq!(ps.to_string(), "spot:123") +} + +#[test] +fn display_arithmetic_twap_price_source() { + let ps = OsmosisPriceSourceChecked::ArithmeticTwap { + pool_id: 123, + window_size: 300, + downtime_detector: None, + }; + assert_eq!(ps.to_string(), "arithmetic_twap:123:300:None"); + + let ps = OsmosisPriceSourceChecked::ArithmeticTwap { + pool_id: 123, + window_size: 300, + downtime_detector: Some(DowntimeDetector { + downtime: Downtime::Duration30m, + recovery: 568, + }), + }; + assert_eq!(ps.to_string(), "arithmetic_twap:123:300:Some(Duration30m:568)"); +} + +#[test] +fn display_geometric_twap_price_source() { + let ps = OsmosisPriceSourceChecked::GeometricTwap { + pool_id: 123, + window_size: 300, + downtime_detector: None, + }; + assert_eq!(ps.to_string(), "geometric_twap:123:300:None"); + + let ps = OsmosisPriceSourceChecked::GeometricTwap { + pool_id: 123, + window_size: 300, + downtime_detector: Some(DowntimeDetector { + downtime: Downtime::Duration30m, + recovery: 568, + }), + }; + assert_eq!(ps.to_string(), "geometric_twap:123:300:Some(Duration30m:568)"); +} + +#[test] +fn display_staked_geometric_twap_price_source() { + let ps = OsmosisPriceSourceChecked::StakedGeometricTwap { + transitive_denom: "transitive".to_string(), + pool_id: 123, + window_size: 300, + downtime_detector: None, + }; + assert_eq!(ps.to_string(), "staked_geometric_twap:transitive:123:300:None"); + + let ps = OsmosisPriceSourceChecked::StakedGeometricTwap { + transitive_denom: "transitive".to_string(), + pool_id: 123, + window_size: 300, + downtime_detector: Some(DowntimeDetector { + downtime: Downtime::Duration30m, + recovery: 568, + }), + }; + assert_eq!(ps.to_string(), "staked_geometric_twap:transitive:123:300:Some(Duration30m:568)"); +} + +#[test] +fn display_xyk_lp_price_source() { + let ps = OsmosisPriceSourceChecked::XykLiquidityToken { + pool_id: 224, + }; + assert_eq!(ps.to_string(), "xyk_liquidity_token:224") +} + +#[test] +fn display_pyth_price_source() { + let ps = OsmosisPriceSourceChecked::Pyth { + contract_addr: Addr::unchecked("osmo12j43nf2f0qumnt2zrrmpvnsqgzndxefujlvr08"), + price_feed_id: PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(), + max_staleness: 60, + denom_decimals: 18, + }; + assert_eq!( + ps.to_string(), + "pyth:osmo12j43nf2f0qumnt2zrrmpvnsqgzndxefujlvr08:0x61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3:60:18" + ) +} + +#[test] +fn display_lsd_price_source() { + let ps = OsmosisPriceSourceChecked::Lsd { + transitive_denom: "transitive".to_string(), + geometric_twap: GeometricTwap { + pool_id: 456, + window_size: 380, + downtime_detector: None, + }, + redemption_rate: RedemptionRate { + contract_addr: Addr::unchecked( + "osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc", + ), + max_staleness: 1234, + }, + }; + assert_eq!(ps.to_string(), "lsd:transitive:456:380:None:osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc:1234"); + + let ps = OsmosisPriceSourceChecked::Lsd { + transitive_denom: "transitive".to_string(), + geometric_twap: GeometricTwap { + pool_id: 456, + window_size: 380, + downtime_detector: Some(DowntimeDetector { + downtime: Downtime::Duration30m, + recovery: 552, + }), + }, + redemption_rate: RedemptionRate { + contract_addr: Addr::unchecked( + "osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc", + ), + max_staleness: 1234, + }, + }; + assert_eq!(ps.to_string(), "lsd:transitive:456:380:Some(Duration30m:552):osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc:1234"); +} diff --git a/contracts/oracle/osmosis/tests/test_query_price.rs b/contracts/oracle/osmosis/tests/test_query_price.rs index ff18b95bf..57c05e979 100644 --- a/contracts/oracle/osmosis/tests/test_query_price.rs +++ b/contracts/oracle/osmosis/tests/test_query_price.rs @@ -5,7 +5,7 @@ use cosmwasm_std::{ }; use mars_oracle_base::ContractError; use mars_oracle_osmosis::{ - contract::entry, scale_to_exponent, stride::RedemptionRateResponse, Downtime, DowntimeDetector, + contract::entry, scale_pyth_price, stride::RedemptionRateResponse, Downtime, DowntimeDetector, GeometricTwap, OsmosisPriceSourceUnchecked, RedemptionRate, }; use mars_red_bank_types::oracle::{PriceResponse, QueryMsg}; @@ -505,6 +505,7 @@ fn setup_pyth_and_geometric_twap_for_lsd( contract_addr: "pyth_contract_addr".to_string(), price_feed_id: price_id, max_staleness: 1800u64, + denom_decimals: 6u8, }, ); @@ -514,7 +515,7 @@ fn setup_pyth_and_geometric_twap_for_lsd( expo: -4, publish_time: publish_time as i64, }; - let pyth_price = scale_to_exponent(price.price as u128, price.expo).unwrap(); + let pyth_price = scale_pyth_price(price.price as u128, price.expo, 6u8, 6u8).unwrap(); deps.querier.set_pyth_price( price_id, @@ -851,6 +852,7 @@ fn querying_pyth_price_if_publish_price_too_old() { contract_addr: "pyth_contract_addr".to_string(), price_feed_id: price_id, max_staleness, + denom_decimals: 6u8, }, ); @@ -912,6 +914,7 @@ fn querying_pyth_price_if_signed() { contract_addr: "pyth_contract_addr".to_string(), price_feed_id: price_id, max_staleness, + denom_decimals: 6u8, }, ); @@ -970,6 +973,7 @@ fn querying_pyth_price_successfully() { contract_addr: "pyth_contract_addr".to_string(), price_feed_id: price_id, max_staleness, + denom_decimals: 6u8, }, ); diff --git a/contracts/oracle/osmosis/tests/test_set_price_source.rs b/contracts/oracle/osmosis/tests/test_set_price_source.rs index 2da1ee4ce..86da9976e 100644 --- a/contracts/oracle/osmosis/tests/test_set_price_source.rs +++ b/contracts/oracle/osmosis/tests/test_set_price_source.rs @@ -960,6 +960,7 @@ fn setting_price_source_pyth_successfully() { ) .unwrap(), max_staleness: 30, + denom_decimals: 8, }, }, ) @@ -981,6 +982,7 @@ fn setting_price_source_pyth_successfully() { ) .unwrap(), max_staleness: 30, + denom_decimals: 8 }, ); } diff --git a/integration-tests/tests/test_oracles.rs b/integration-tests/tests/test_oracles.rs index 10ff04208..849d243e3 100644 --- a/integration-tests/tests/test_oracles.rs +++ b/integration-tests/tests/test_oracles.rs @@ -57,6 +57,7 @@ fn querying_xyk_lp_price_if_no_price_for_tokens() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), + base_denom_decimals: 6u8, }, ); @@ -110,6 +111,7 @@ fn querying_xyk_lp_price_success() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), + base_denom_decimals: 6u8, }, ); @@ -218,6 +220,7 @@ fn query_spot_price() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), + base_denom_decimals: 6u8, }, ); @@ -282,6 +285,7 @@ fn set_spot_without_pools() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), + base_denom_decimals: 6u8, }, ); @@ -319,6 +323,7 @@ fn incorrect_pool_for_spot() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), + base_denom_decimals: 6u8, }, ); @@ -365,6 +370,7 @@ fn update_spot_with_different_pool() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), + base_denom_decimals: 6u8, }, ); @@ -439,6 +445,7 @@ fn query_spot_price_after_lp_change() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), + base_denom_decimals: 6u8, }, ); @@ -498,6 +505,7 @@ fn query_geometric_twap_price_with_downtime_detector() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), + base_denom_decimals: 6u8, }, ); @@ -582,6 +590,7 @@ fn query_arithmetic_twap_price() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), + base_denom_decimals: 6u8, }, ); @@ -668,6 +677,7 @@ fn query_geometric_twap_price() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), + base_denom_decimals: 6u8, }, ); @@ -754,6 +764,7 @@ fn compare_spot_and_twap_price() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), + base_denom_decimals: 6u8, }, ); @@ -1014,6 +1025,7 @@ fn setup_redbank(wasm: &Wasm, signer: &SigningAccount) -> (Strin &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), + base_denom_decimals: 6u8, }, ); diff --git a/packages/testing/src/integration/mock_env.rs b/packages/testing/src/integration/mock_env.rs index 98f77ab69..af7242d98 100644 --- a/packages/testing/src/integration/mock_env.rs +++ b/packages/testing/src/integration/mock_env.rs @@ -445,6 +445,7 @@ pub struct MockEnvBuilder { chain_prefix: String, mars_denom: String, base_denom: String, + base_denom_decimals: u8, close_factor: Decimal, // rewards-collector params @@ -466,6 +467,7 @@ impl MockEnvBuilder { chain_prefix: "".to_string(), // empty prefix for multitest because deployed contracts have addresses such as contract1, contract2 etc which are invalid in address-provider mars_denom: "umars".to_string(), base_denom: "uosmo".to_string(), + base_denom_decimals: 6u8, close_factor: Decimal::percent(80), safety_tax_rate: Decimal::percent(50), safety_fund_denom: "uusdc".to_string(), @@ -613,6 +615,7 @@ impl MockEnvBuilder { &oracle::InstantiateMsg { owner: self.owner.to_string(), base_denom: self.base_denom.clone(), + base_denom_decimals: self.base_denom_decimals, }, &[], "oracle", diff --git a/packages/types/src/oracle.rs b/packages/types/src/oracle.rs index d0be5836c..e6baea67f 100644 --- a/packages/types/src/oracle.rs +++ b/packages/types/src/oracle.rs @@ -8,12 +8,16 @@ pub struct InstantiateMsg { pub owner: String, /// The asset in which prices are denominated in pub base_denom: String, + /// The asset decimals used for price normalization + pub base_denom_decimals: u8, } #[cw_serde] pub struct Config { /// The asset in which prices are denominated in pub base_denom: String, + /// The asset decimals used for price normalization + pub base_denom_decimals: u8, } #[cw_serde] @@ -34,6 +38,7 @@ pub enum ExecuteMsg { /// Update contract config (only callable by owner) UpdateConfig { base_denom: Option, + base_denom_decimals: Option, }, } @@ -85,6 +90,8 @@ pub struct ConfigResponse { pub proposed_new_owner: Option, /// The asset in which prices are denominated in pub base_denom: String, + /// The asset decimals used for price normalization + pub base_denom_decimals: u8, } #[cw_serde] diff --git a/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json b/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json index 4ed1a18f8..6faa112a5 100644 --- a/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json +++ b/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json @@ -8,6 +8,7 @@ "type": "object", "required": [ "base_denom", + "base_denom_decimals", "owner" ], "properties": { @@ -15,6 +16,12 @@ "description": "The asset in which prices are denominated in", "type": "string" }, + "base_denom_decimals": { + "description": "The asset decimals used for price normalization", + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, "owner": { "description": "The contract's owner, who can update config and price sources", "type": "string" @@ -102,6 +109,14 @@ "string", "null" ] + }, + "base_denom_decimals": { + "type": [ + "integer", + "null" + ], + "format": "uint8", + "minimum": 0.0 } }, "additionalProperties": false @@ -412,6 +427,7 @@ "type": "object", "required": [ "contract_addr", + "denom_decimals", "max_staleness", "price_feed_id" ], @@ -420,6 +436,12 @@ "description": "Contract address of Pyth", "type": "string" }, + "denom_decimals": { + "description": "Assets are represented in their smallest unit and every asset can have different decimals (e.g. OSMO - 6 decimals, WETH - 18 decimals).\n\nPyth prices are denominated in USD so basically it means how much 1 USDC, 1 ATOM, 1 OSMO is worth in USD (NOT 1 uusdc, 1 uatom, 1 uosmo). We have to normalize it. We should get how much 1 utoken is worth in uusd. For example: denom_decimals (OSMO) = 6 base_denom_decimals (USD) = 6\n\n1 OSMO = 10^6 uosmo 1 USD = 10^6 uusd\n\nosmo_price_in_usd = 0.59958994 uosmo_price_in_uusd = osmo_price_in_usd / 10^denom_decimals * 10^base_denom_decimals = uosmo_price_in_uusd = 0.59958994 * 10^(-6) * 10^6 = 0.59958994", + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, "max_staleness": { "description": "The maximum number of seconds since the last price was by an oracle, before rejecting the price as too stale", "type": "integer", @@ -710,13 +732,20 @@ "title": "ConfigResponse", "type": "object", "required": [ - "base_denom" + "base_denom", + "base_denom_decimals" ], "properties": { "base_denom": { "description": "The asset in which prices are denominated in", "type": "string" }, + "base_denom_decimals": { + "description": "The asset decimals used for price normalization", + "type": "integer", + "format": "uint8", + "minimum": 0.0 + }, "owner": { "description": "The contract's owner", "type": [ diff --git a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts index d125fda51..fc2417d84 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts @@ -141,8 +141,10 @@ export interface MarsOracleOsmosisInterface extends MarsOracleOsmosisReadOnlyInt updateConfig: ( { baseDenom, + baseDenomDecimals, }: { baseDenom?: string + baseDenomDecimals?: number }, fee?: number | StdFee | 'auto', memo?: string, @@ -237,8 +239,10 @@ export class MarsOracleOsmosisClient updateConfig = async ( { baseDenom, + baseDenomDecimals, }: { baseDenom?: string + baseDenomDecimals?: number }, fee: number | StdFee | 'auto' = 'auto', memo?: string, @@ -250,6 +254,7 @@ export class MarsOracleOsmosisClient { update_config: { base_denom: baseDenom, + base_denom_decimals: baseDenomDecimals, }, }, fee, 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 9af54f39a..52e34d7da 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.react-query.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.react-query.ts @@ -171,6 +171,7 @@ export interface MarsOracleOsmosisUpdateConfigMutation { client: MarsOracleOsmosisClient msg: { baseDenom?: string + baseDenomDecimals?: number } args?: { fee?: number | StdFee | 'auto' diff --git a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts index 1917f7a5f..42782bcb5 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts @@ -7,6 +7,7 @@ export interface InstantiateMsg { base_denom: string + base_denom_decimals: number owner: string } export type ExecuteMsg = @@ -27,6 +28,7 @@ export type ExecuteMsg = | { update_config: { base_denom?: string | null + base_denom_decimals?: number | null } } export type OsmosisPriceSourceForString = @@ -76,6 +78,7 @@ export type OsmosisPriceSourceForString = | { pyth: { contract_addr: string + denom_decimals: number max_staleness: number price_feed_id: Identifier [k: string]: unknown @@ -176,6 +179,7 @@ export type QueryMsg = } export interface ConfigResponse { base_denom: string + base_denom_decimals: number owner?: string | null proposed_new_owner?: string | null } From 63e92d603160a0b0c52a022dda11a7e19c7062a3 Mon Sep 17 00:00:00 2001 From: piobab Date: Fri, 9 Jun 2023 12:34:19 +0200 Subject: [PATCH 06/11] Improve pyth base dec (#202) * Use price source for usd -> base_denom convertion. * Fix test and update comments. * Fix fmt. --- contracts/oracle/base/src/contract.rs | 12 +- contracts/oracle/base/src/error.rs | 7 +- contracts/oracle/base/src/traits.rs | 1 - contracts/oracle/osmosis/src/price_source.rs | 113 +++++++++++++----- contracts/oracle/osmosis/src/stride.rs | 2 - contracts/oracle/osmosis/tests/helpers.rs | 1 - contracts/oracle/osmosis/tests/test_admin.rs | 11 +- .../oracle/osmosis/tests/test_query_price.rs | 55 ++++++++- integration-tests/tests/test_oracles.rs | 12 -- packages/testing/src/integration/mock_env.rs | 1 - packages/types/src/oracle.rs | 7 -- .../mars-oracle-osmosis.json | 28 +---- .../MarsOracleOsmosis.client.ts | 5 - .../MarsOracleOsmosis.react-query.ts | 1 - .../MarsOracleOsmosis.types.ts | 3 - 15 files changed, 147 insertions(+), 112 deletions(-) diff --git a/contracts/oracle/base/src/contract.rs b/contracts/oracle/base/src/contract.rs index 8d9e7624b..835e2780c 100644 --- a/contracts/oracle/base/src/contract.rs +++ b/contracts/oracle/base/src/contract.rs @@ -73,7 +73,6 @@ where deps.storage, &Config { base_denom: msg.base_denom, - base_denom_decimals: msg.base_denom_decimals, }, )?; @@ -97,8 +96,7 @@ where } => self.remove_price_source(deps, info.sender, denom), ExecuteMsg::UpdateConfig { base_denom, - base_denom_decimals, - } => self.update_config(deps, info.sender, base_denom, base_denom_decimals), + } => self.update_config(deps, info.sender, base_denom), } } @@ -173,7 +171,6 @@ where deps: DepsMut, sender_addr: Addr, base_denom: Option, - base_demom_decimals: Option, ) -> ContractResult { self.owner.assert_owner(deps.storage, &sender_addr)?; @@ -183,17 +180,13 @@ where let mut config = self.config.load(deps.storage)?; let prev_base_denom = config.base_denom.clone(); - let prev_base_denom_decimals = config.base_denom_decimals; config.base_denom = base_denom.unwrap_or(config.base_denom); - config.base_denom_decimals = base_demom_decimals.unwrap_or(config.base_denom_decimals); self.config.save(deps.storage, &config)?; let response = Response::new() .add_attribute("action", "update_config") .add_attribute("prev_base_denom", prev_base_denom) - .add_attribute("base_denom", config.base_denom) - .add_attribute("prev_base_denom_decimals", prev_base_denom_decimals.to_string()) - .add_attribute("base_denom_decimals", config.base_denom_decimals.to_string()); + .add_attribute("base_denom", config.base_denom); Ok(response) } @@ -205,7 +198,6 @@ where owner: owner_state.owner, proposed_new_owner: owner_state.proposed, base_denom: cfg.base_denom, - base_denom_decimals: cfg.base_denom_decimals, }) } diff --git a/contracts/oracle/base/src/error.rs b/contracts/oracle/base/src/error.rs index 7a1ee3898..ab60864a8 100644 --- a/contracts/oracle/base/src/error.rs +++ b/contracts/oracle/base/src/error.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ - CheckedFromRatioError, CheckedMultiplyRatioError, ConversionOverflowError, OverflowError, - StdError, + CheckedFromRatioError, CheckedMultiplyRatioError, ConversionOverflowError, + DecimalRangeExceeded, OverflowError, StdError, }; use mars_owner::OwnerError; use mars_red_bank_types::error::MarsError; @@ -36,6 +36,9 @@ pub enum ContractError { #[error("{0}")] CheckedFromRatio(#[from] CheckedFromRatioError), + #[error("{0}")] + DecimalRangeExceeded(#[from] DecimalRangeExceeded), + #[error("Invalid price source: {reason}")] InvalidPriceSource { reason: String, diff --git a/contracts/oracle/base/src/traits.rs b/contracts/oracle/base/src/traits.rs index 75792221a..c78ea8cc8 100644 --- a/contracts/oracle/base/src/traits.rs +++ b/contracts/oracle/base/src/traits.rs @@ -32,7 +32,6 @@ where /// - `config.base_denom`: The coin in which the price is to be denominated in. /// For example, if `denom` is uatom and `base_denom` is uosmo, the /// function should return how many uosmo is per one uatom. - /// - `config.base_denom_decimals`: The coin decimals used for price normalization. /// /// - `price_sources`: A map that stores the price source for each coin. /// This is necessary because for some coins, in order to calculate its diff --git a/contracts/oracle/osmosis/src/price_source.rs b/contracts/oracle/osmosis/src/price_source.rs index 97eed6ca1..dc9c52fb8 100644 --- a/contracts/oracle/osmosis/src/price_source.rs +++ b/contracts/oracle/osmosis/src/price_source.rs @@ -156,6 +156,8 @@ pub enum OsmosisPriceSource { contract_addr: T, /// Price feed id of an asset from the list: https://pyth.network/developers/price-feed-ids + /// We can't verify what denoms consist of the price feed. + /// Be very careful when adding it !!! price_feed_id: PriceIdentifier, /// The maximum number of seconds since the last price was by an oracle, before @@ -166,15 +168,15 @@ pub enum OsmosisPriceSource { /// /// Pyth prices are denominated in USD so basically it means how much 1 USDC, 1 ATOM, 1 OSMO is worth in USD (NOT 1 uusdc, 1 uatom, 1 uosmo). /// We have to normalize it. We should get how much 1 utoken is worth in uusd. For example: - /// denom_decimals (OSMO) = 6 - /// base_denom_decimals (USD) = 6 + /// - base_denom = uusd + /// - price source set for usd (e.g. FIXED price source where 1 usd = 1000000 uusd) + /// - denom_decimals (ATOM) = 6 /// /// 1 OSMO = 10^6 uosmo - /// 1 USD = 10^6 uusd /// /// osmo_price_in_usd = 0.59958994 - /// uosmo_price_in_uusd = osmo_price_in_usd / 10^denom_decimals * 10^base_denom_decimals = - /// uosmo_price_in_uusd = 0.59958994 * 10^(-6) * 10^6 = 0.59958994 + /// uosmo_price_in_uusd = osmo_price_in_usd * usd_price_in_base_denom / 10^denom_decimals = + /// uosmo_price_in_uusd = 0.59958994 * 1000000 * 10^(-6) = 0.59958994 denom_decimals: u8, }, /// Liquid Staking Derivatives (LSD) price quoted in USD based on data from Pyth, Osmosis and Stride. @@ -491,11 +493,12 @@ impl PriceSourceChecked for OsmosisPriceSourceChecked { } => Ok(Self::query_pyth_price( deps, env, - config, contract_addr.to_owned(), *price_feed_id, *max_staleness, *denom_decimals, + config, + price_sources, )?), OsmosisPriceSourceChecked::Lsd { transitive_denom, @@ -683,12 +686,22 @@ impl OsmosisPriceSourceChecked { fn query_pyth_price( deps: &Deps, env: &Env, - config: &Config, contract_addr: Addr, price_feed_id: PriceIdentifier, max_staleness: u64, denom_decimals: u8, + config: &Config, + price_sources: &Map<&str, OsmosisPriceSourceChecked>, ) -> ContractResult { + // Use current price source for USD to check how much 1 USD is worth in base_denom + let usd_price = price_sources.load(deps.storage, "usd")?.query_price( + deps, + env, + "usd", + config, + price_sources, + )?; + let current_time = env.block.time.seconds(); let price_feed_response = query_price_feed(&deps.querier, contract_addr, price_feed_id)?; @@ -718,7 +731,7 @@ impl OsmosisPriceSourceChecked { current_price.price as u128, current_price.expo, denom_decimals, - config.base_denom_decimals, + usd_price, )?; Ok(current_price_dec) @@ -735,41 +748,59 @@ impl OsmosisPriceSourceChecked { /// price: 1365133270 /// The confidence interval is 574566 * 10^(-8) = $0.00574566, and the price is 1365133270 * 10^(-8) = $13.6513327. /// -/// Moreover, we have to represent the price for utoken in uusd (instead of token/USD). +/// Moreover, we have to represent the price for utoken in base_denom. /// Pyth price should be normalized with token decimals. /// -/// Let's try to convert ATOM/USD reported by Pyth to uatom/uusd: -/// denom_decimals (ATOM) = 6 -/// base_denom_decimals (USD) = 6 +/// Let's try to convert ATOM/USD reported by Pyth to uatom/base_denom: +/// - base_denom = uusd +/// - price source set for usd (e.g. FIXED price source where 1 usd = 1000000 uusd) +/// - denom_decimals (ATOM) = 6 /// /// 1 ATOM = 10^6 uatom -/// 1 USD = 10^6 uusd -/// /// +/// /// 1 ATOM = price * 10^expo USD -/// 10^6 uatom = price * 10^expo * 10^6 uusd -/// uatom = price * 10^expo * 10^6 / 10^6 uusd -/// uatom = price * 10^expo * 10^6 * 10^(-6) uusd -/// uatom/uusd = 1365133270 * 10^(-8) * 10^6 * 10^(-6) +/// 10^6 uatom = price * 10^expo * 1000000 uusd +/// uatom = price * 10^expo * 1000000 / 10^6 uusd +/// uatom = price * 10^expo * 1000000 * 10^(-6) uusd +/// uatom/uusd = 1365133270 * 10^(-8) * 1000000 * 10^(-6) /// uatom/uusd = 1365133270 * 10^(-8) = 13.6513327 /// /// Generalized formula: -/// utoken/uusd = price * 10^expo * 10^base_denom_decimals * 10^(-denom_decimals) -/// -/// NOTE: if we don't introduce base_denom decimals we can overflow. +/// utoken/uusd = price * 10^expo * usd_price_in_base_denom * 10^(-denom_decimals) pub fn scale_pyth_price( value: u128, expo: i32, denom_decimals: u8, - base_denom_decimals: u8, + usd_price: Decimal, ) -> ContractResult { - let expo = expo - denom_decimals as i32 + base_denom_decimals as i32; let target_expo = Uint128::from(10u8).checked_pow(expo.unsigned_abs())?; - if expo < 0 { - Ok(Decimal::checked_from_ratio(value, target_expo)?) + let pyth_price = if expo < 0 { + Decimal::checked_from_ratio(value, target_expo)? } else { let res = Uint128::from(value).checked_mul(target_expo)?; - Ok(Decimal::from_ratio(res, 1u128)) - } + Decimal::from_ratio(res, 1u128) + }; + + let denom_scaled = Decimal::from_atomics(1u128, denom_decimals as u32)?; + + // Multiplication order matters !!! It can overflow doing different ways. + // usd_price is represented in smallest unit so it can be quite big number and can be used to reduce number of decimals. + // + // Let's assume that: + // - usd_price = 1000000 = 10^6 + // - expo = -8 + // - denom_decimals = 18 + // + // If we multiply usd_price by denom_scaled firstly we will decrease number of decimals used in next multiplication by pyth_price: + // 10^6 * 10^(-18) = 10^(-12) + // 12 decimals used. + // + // BUT if we multiply pyth_price by denom_scaled: + // 10^(-8) * 10^(-18) = 10^(-26) + // 26 decimals used (overflow) !!! + let price = usd_price.checked_mul(denom_scaled)?.checked_mul(pyth_price)?; + + Ok(price) } #[cfg(test)] @@ -781,11 +812,31 @@ mod tests { #[test] fn scale_real_pyth_price() { // ATOM - let uatom_price_in_usd = scale_pyth_price(1035200881u128, -8, 6u8, 6u8).unwrap(); - assert_eq!(uatom_price_in_usd, Decimal::from_str("10.35200881").unwrap()); + let uatom_price_in_uusd = + scale_pyth_price(1035200881u128, -8, 6u8, Decimal::from_str("1000000").unwrap()) + .unwrap(); + assert_eq!(uatom_price_in_uusd, Decimal::from_str("10.35200881").unwrap()); // ETH - let ueth_price_in_usd = scale_pyth_price(181598000001u128, -8, 18u8, 6u8).unwrap(); - assert_eq!(ueth_price_in_usd, Decimal::from_str("0.00000000181598").unwrap()); + let ueth_price_in_uusd = + scale_pyth_price(181598000001u128, -8, 18u8, Decimal::from_str("1000000").unwrap()) + .unwrap(); + assert_eq!(ueth_price_in_uusd, Decimal::from_str("0.00000000181598").unwrap()); + } + + #[test] + fn scale_pyth_price_if_expo_above_zero() { + let ueth_price_in_uusd = + scale_pyth_price(181598000001u128, 8, 18u8, Decimal::from_str("1000000").unwrap()) + .unwrap(); + assert_eq!(ueth_price_in_uusd, Decimal::from_atomics(181598000001u128, 4u32).unwrap()); + } + + #[test] + fn scale_big_eth_pyth_price() { + let ueth_price_in_uusd = + scale_pyth_price(100000098000001u128, -8, 18u8, Decimal::from_str("1000000").unwrap()) + .unwrap(); + assert_eq!(ueth_price_in_uusd, Decimal::from_atomics(100000098000001u128, 20u32).unwrap()); } } diff --git a/contracts/oracle/osmosis/src/stride.rs b/contracts/oracle/osmosis/src/stride.rs index a255c3ec2..f37d3d13b 100644 --- a/contracts/oracle/osmosis/src/stride.rs +++ b/contracts/oracle/osmosis/src/stride.rs @@ -2,8 +2,6 @@ use cosmwasm_std::{to_binary, Addr, Decimal, QuerierWrapper, QueryRequest, StdRe use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -// TODO: should be updated once Stride open source their contract - #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, JsonSchema)] pub struct Price { pub denom: String, diff --git a/contracts/oracle/osmosis/tests/helpers.rs b/contracts/oracle/osmosis/tests/helpers.rs index 26c41f2d9..4d06d09df 100644 --- a/contracts/oracle/osmosis/tests/helpers.rs +++ b/contracts/oracle/osmosis/tests/helpers.rs @@ -90,7 +90,6 @@ pub fn setup_test() -> OwnedDeps { InstantiateMsg { owner: "owner".to_string(), base_denom: "uosmo".to_string(), - base_denom_decimals: 6u8, }, ) .unwrap(); diff --git a/contracts/oracle/osmosis/tests/test_admin.rs b/contracts/oracle/osmosis/tests/test_admin.rs index 854845ee6..8ab96ae0c 100644 --- a/contracts/oracle/osmosis/tests/test_admin.rs +++ b/contracts/oracle/osmosis/tests/test_admin.rs @@ -31,7 +31,6 @@ fn instantiating_incorrect_denom() { InstantiateMsg { owner: "owner".to_string(), base_denom: "!*jadfaefc".to_string(), - base_denom_decimals: 6u8, }, ); assert_eq!( @@ -48,7 +47,6 @@ fn instantiating_incorrect_denom() { InstantiateMsg { owner: "owner".to_string(), base_denom: "ahdbufenf&*!-".to_string(), - base_denom_decimals: 6u8, }, ); assert_eq!( @@ -66,7 +64,6 @@ fn instantiating_incorrect_denom() { InstantiateMsg { owner: "owner".to_string(), base_denom: "ab".to_string(), - base_denom_decimals: 6u8, }, ); assert_eq!( @@ -83,7 +80,6 @@ fn update_config_if_unauthorized() { let msg = ExecuteMsg::UpdateConfig { base_denom: None, - base_denom_decimals: None, }; let info = mock_info("somebody"); let res_err = entry::execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); @@ -96,7 +92,6 @@ fn update_config_with_invalid_base_denom() { let msg = ExecuteMsg::UpdateConfig { base_denom: Some("*!fdskfna".to_string()), - base_denom_decimals: None, }; let info = mock_info("owner"); let res_err = entry::execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); @@ -114,7 +109,6 @@ fn update_config_with_new_params() { let msg = ExecuteMsg::UpdateConfig { base_denom: Some("uusdc".to_string()), - base_denom_decimals: Some(10u8), }; let info = mock_info("owner"); let res = entry::execute(deps.as_mut(), mock_env(), info, msg).unwrap(); @@ -124,9 +118,7 @@ fn update_config_with_new_params() { vec![ attr("action", "update_config"), attr("prev_base_denom", "uosmo"), - attr("base_denom", "uusdc"), - attr("prev_base_denom_decimals", "6"), - attr("base_denom_decimals", "10"), + attr("base_denom", "uusdc") ] ); @@ -134,5 +126,4 @@ fn update_config_with_new_params() { assert_eq!(cfg.owner.unwrap(), "owner".to_string()); assert_eq!(cfg.proposed_new_owner, None); assert_eq!(cfg.base_denom, "uusdc".to_string()); - assert_eq!(cfg.base_denom_decimals, 10u8); } diff --git a/contracts/oracle/osmosis/tests/test_query_price.rs b/contracts/oracle/osmosis/tests/test_query_price.rs index 57c05e979..5d8c60a00 100644 --- a/contracts/oracle/osmosis/tests/test_query_price.rs +++ b/contracts/oracle/osmosis/tests/test_query_price.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use cosmwasm_std::{ coin, from_binary, testing::{MockApi, MockStorage}, @@ -404,6 +406,15 @@ fn querying_staked_geometric_twap_price_with_downtime_detector() { fn querying_lsd_price() { 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, ustatom_uatom_price) = setup_pyth_and_geometric_twap_for_lsd(&mut deps, publish_time); @@ -515,7 +526,13 @@ fn setup_pyth_and_geometric_twap_for_lsd( expo: -4, publish_time: publish_time as i64, }; - let pyth_price = scale_pyth_price(price.price as u128, price.expo, 6u8, 6u8).unwrap(); + let pyth_price = scale_pyth_price( + price.price as u128, + price.expo, + 6u8, + Decimal::from_str("1000000").unwrap(), + ) + .unwrap(); deps.querier.set_pyth_price( price_id, @@ -674,6 +691,15 @@ fn querying_lsd_price_with_downtime_detector() { recovery: 360, }; + // 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(), + }, + ); + // query price if geometric TWAP < redemption rate helpers::set_price_source( deps.as_mut(), @@ -839,6 +865,15 @@ fn querying_xyk_lp_price() { fn querying_pyth_price_if_publish_price_too_old() { let mut deps = helpers::setup_test(); + // 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 price_id = PriceIdentifier::from_hex( "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", ) @@ -901,6 +936,15 @@ fn querying_pyth_price_if_publish_price_too_old() { fn querying_pyth_price_if_signed() { let mut deps = helpers::setup_test(); + // 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 price_id = PriceIdentifier::from_hex( "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", ) @@ -960,6 +1004,15 @@ fn querying_pyth_price_if_signed() { fn querying_pyth_price_successfully() { let mut deps = helpers::setup_test(); + // 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 price_id = PriceIdentifier::from_hex( "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", ) diff --git a/integration-tests/tests/test_oracles.rs b/integration-tests/tests/test_oracles.rs index 849d243e3..10ff04208 100644 --- a/integration-tests/tests/test_oracles.rs +++ b/integration-tests/tests/test_oracles.rs @@ -57,7 +57,6 @@ fn querying_xyk_lp_price_if_no_price_for_tokens() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), - base_denom_decimals: 6u8, }, ); @@ -111,7 +110,6 @@ fn querying_xyk_lp_price_success() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), - base_denom_decimals: 6u8, }, ); @@ -220,7 +218,6 @@ fn query_spot_price() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), - base_denom_decimals: 6u8, }, ); @@ -285,7 +282,6 @@ fn set_spot_without_pools() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), - base_denom_decimals: 6u8, }, ); @@ -323,7 +319,6 @@ fn incorrect_pool_for_spot() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), - base_denom_decimals: 6u8, }, ); @@ -370,7 +365,6 @@ fn update_spot_with_different_pool() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), - base_denom_decimals: 6u8, }, ); @@ -445,7 +439,6 @@ fn query_spot_price_after_lp_change() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), - base_denom_decimals: 6u8, }, ); @@ -505,7 +498,6 @@ fn query_geometric_twap_price_with_downtime_detector() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), - base_denom_decimals: 6u8, }, ); @@ -590,7 +582,6 @@ fn query_arithmetic_twap_price() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), - base_denom_decimals: 6u8, }, ); @@ -677,7 +668,6 @@ fn query_geometric_twap_price() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), - base_denom_decimals: 6u8, }, ); @@ -764,7 +754,6 @@ fn compare_spot_and_twap_price() { &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), - base_denom_decimals: 6u8, }, ); @@ -1025,7 +1014,6 @@ fn setup_redbank(wasm: &Wasm, signer: &SigningAccount) -> (Strin &InstantiateMsg { owner: signer.address(), base_denom: "uosmo".to_string(), - base_denom_decimals: 6u8, }, ); diff --git a/packages/testing/src/integration/mock_env.rs b/packages/testing/src/integration/mock_env.rs index af7242d98..be5d14cf9 100644 --- a/packages/testing/src/integration/mock_env.rs +++ b/packages/testing/src/integration/mock_env.rs @@ -615,7 +615,6 @@ impl MockEnvBuilder { &oracle::InstantiateMsg { owner: self.owner.to_string(), base_denom: self.base_denom.clone(), - base_denom_decimals: self.base_denom_decimals, }, &[], "oracle", diff --git a/packages/types/src/oracle.rs b/packages/types/src/oracle.rs index e6baea67f..d0be5836c 100644 --- a/packages/types/src/oracle.rs +++ b/packages/types/src/oracle.rs @@ -8,16 +8,12 @@ pub struct InstantiateMsg { pub owner: String, /// The asset in which prices are denominated in pub base_denom: String, - /// The asset decimals used for price normalization - pub base_denom_decimals: u8, } #[cw_serde] pub struct Config { /// The asset in which prices are denominated in pub base_denom: String, - /// The asset decimals used for price normalization - pub base_denom_decimals: u8, } #[cw_serde] @@ -38,7 +34,6 @@ pub enum ExecuteMsg { /// Update contract config (only callable by owner) UpdateConfig { base_denom: Option, - base_denom_decimals: Option, }, } @@ -90,8 +85,6 @@ pub struct ConfigResponse { pub proposed_new_owner: Option, /// The asset in which prices are denominated in pub base_denom: String, - /// The asset decimals used for price normalization - pub base_denom_decimals: u8, } #[cw_serde] diff --git a/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json b/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json index 6faa112a5..a27fc05b4 100644 --- a/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json +++ b/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json @@ -8,7 +8,6 @@ "type": "object", "required": [ "base_denom", - "base_denom_decimals", "owner" ], "properties": { @@ -16,12 +15,6 @@ "description": "The asset in which prices are denominated in", "type": "string" }, - "base_denom_decimals": { - "description": "The asset decimals used for price normalization", - "type": "integer", - "format": "uint8", - "minimum": 0.0 - }, "owner": { "description": "The contract's owner, who can update config and price sources", "type": "string" @@ -109,14 +102,6 @@ "string", "null" ] - }, - "base_denom_decimals": { - "type": [ - "integer", - "null" - ], - "format": "uint8", - "minimum": 0.0 } }, "additionalProperties": false @@ -437,7 +422,7 @@ "type": "string" }, "denom_decimals": { - "description": "Assets are represented in their smallest unit and every asset can have different decimals (e.g. OSMO - 6 decimals, WETH - 18 decimals).\n\nPyth prices are denominated in USD so basically it means how much 1 USDC, 1 ATOM, 1 OSMO is worth in USD (NOT 1 uusdc, 1 uatom, 1 uosmo). We have to normalize it. We should get how much 1 utoken is worth in uusd. For example: denom_decimals (OSMO) = 6 base_denom_decimals (USD) = 6\n\n1 OSMO = 10^6 uosmo 1 USD = 10^6 uusd\n\nosmo_price_in_usd = 0.59958994 uosmo_price_in_uusd = osmo_price_in_usd / 10^denom_decimals * 10^base_denom_decimals = uosmo_price_in_uusd = 0.59958994 * 10^(-6) * 10^6 = 0.59958994", + "description": "Assets are represented in their smallest unit and every asset can have different decimals (e.g. OSMO - 6 decimals, WETH - 18 decimals).\n\nPyth prices are denominated in USD so basically it means how much 1 USDC, 1 ATOM, 1 OSMO is worth in USD (NOT 1 uusdc, 1 uatom, 1 uosmo). We have to normalize it. We should get how much 1 utoken is worth in uusd. For example: - base_denom = uusd - price source set for usd (e.g. FIXED price source where 1 usd = 1000000 uusd) - denom_decimals (ATOM) = 6\n\n1 OSMO = 10^6 uosmo\n\nosmo_price_in_usd = 0.59958994 uosmo_price_in_uusd = osmo_price_in_usd * usd_price_in_base_denom / 10^denom_decimals = uosmo_price_in_uusd = 0.59958994 * 1000000 * 10^(-6) = 0.59958994", "type": "integer", "format": "uint8", "minimum": 0.0 @@ -449,7 +434,7 @@ "minimum": 0.0 }, "price_feed_id": { - "description": "Price feed id of an asset from the list: https://pyth.network/developers/price-feed-ids", + "description": "Price feed id of an asset from the list: https://pyth.network/developers/price-feed-ids We can't verify what denoms consist of the price feed. Be very careful when adding it !!!", "allOf": [ { "$ref": "#/definitions/Identifier" @@ -732,20 +717,13 @@ "title": "ConfigResponse", "type": "object", "required": [ - "base_denom", - "base_denom_decimals" + "base_denom" ], "properties": { "base_denom": { "description": "The asset in which prices are denominated in", "type": "string" }, - "base_denom_decimals": { - "description": "The asset decimals used for price normalization", - "type": "integer", - "format": "uint8", - "minimum": 0.0 - }, "owner": { "description": "The contract's owner", "type": [ diff --git a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts index fc2417d84..d125fda51 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts @@ -141,10 +141,8 @@ export interface MarsOracleOsmosisInterface extends MarsOracleOsmosisReadOnlyInt updateConfig: ( { baseDenom, - baseDenomDecimals, }: { baseDenom?: string - baseDenomDecimals?: number }, fee?: number | StdFee | 'auto', memo?: string, @@ -239,10 +237,8 @@ export class MarsOracleOsmosisClient updateConfig = async ( { baseDenom, - baseDenomDecimals, }: { baseDenom?: string - baseDenomDecimals?: number }, fee: number | StdFee | 'auto' = 'auto', memo?: string, @@ -254,7 +250,6 @@ export class MarsOracleOsmosisClient { update_config: { base_denom: baseDenom, - base_denom_decimals: baseDenomDecimals, }, }, fee, 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 52e34d7da..9af54f39a 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.react-query.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.react-query.ts @@ -171,7 +171,6 @@ export interface MarsOracleOsmosisUpdateConfigMutation { client: MarsOracleOsmosisClient msg: { baseDenom?: string - baseDenomDecimals?: number } args?: { fee?: number | StdFee | 'auto' diff --git a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts index 42782bcb5..d5f9b5cba 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts @@ -7,7 +7,6 @@ export interface InstantiateMsg { base_denom: string - base_denom_decimals: number owner: string } export type ExecuteMsg = @@ -28,7 +27,6 @@ export type ExecuteMsg = | { update_config: { base_denom?: string | null - base_denom_decimals?: number | null } } export type OsmosisPriceSourceForString = @@ -179,7 +177,6 @@ export type QueryMsg = } export interface ConfigResponse { base_denom: string - base_denom_decimals: number owner?: string | null proposed_new_owner?: string | null } From b9adc8f2e169b61e752870b1d20ab105b2bbb6c9 Mon Sep 17 00:00:00 2001 From: piobab Date: Fri, 9 Jun 2023 13:48:05 +0200 Subject: [PATCH 07/11] Bump deps (#204) * Bump deps. Fix build. * Migrate Spot / Pool queries to poolmanager. * Use GammQueries for Spot. New PoolManager query is not yet whitelisted. --- Cargo.lock | 42 ++++--------------- Cargo.toml | 18 ++++---- .../oracle/osmosis/tests/test_query_price.rs | 8 ++-- .../rewards-collector/osmosis/src/route.rs | 5 ++- .../osmosis/tests/test_swap.rs | 2 +- integration-tests/tests/helpers.rs | 5 ++- .../tests/test_rewards_collector.rs | 2 +- packages/chains/osmosis/src/helpers.rs | 8 +++- packages/testing/src/mars_mock_querier.rs | 4 +- packages/testing/src/osmosis_querier.rs | 17 ++++---- .../MarsOracleOsmosis.client.ts | 6 +-- 11 files changed, 47 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1dcb1c037..44c7338b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1214,7 +1214,7 @@ dependencies = [ "mars-rewards-collector-osmosis", "mars-testing", "mars-utils", - "osmosis-std 0.14.0", + "osmosis-std", "osmosis-test-tube", "serde", ] @@ -1248,7 +1248,7 @@ dependencies = [ "mars-red-bank-types", "mars-testing", "mars-utils", - "osmosis-std 0.14.0", + "osmosis-std", "pyth-sdk-cw", "schemars", "serde", @@ -1260,7 +1260,7 @@ name = "mars-osmosis" version = "1.0.1" dependencies = [ "cosmwasm-std", - "osmosis-std 0.14.0", + "osmosis-std", "serde", ] @@ -1334,7 +1334,7 @@ dependencies = [ "mars-rewards-collector-base", "mars-testing", "mars-utils", - "osmosis-std 0.14.0", + "osmosis-std", "schemars", "serde", "thiserror", @@ -1354,7 +1354,7 @@ dependencies = [ "mars-red-bank", "mars-red-bank-types", "mars-rewards-collector-osmosis", - "osmosis-std 0.14.0", + "osmosis-std", "prost 0.11.9", "pyth-sdk-cw", "schemars", @@ -1472,22 +1472,6 @@ version = "6.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" -[[package]] -name = "osmosis-std" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fc0a9075efd64ed5a8be3bf134cbf1080570d68384f2ad58ffaac6c00d063fd" -dependencies = [ - "chrono", - "cosmwasm-std", - "osmosis-std-derive 0.13.2", - "prost 0.11.9", - "prost-types", - "schemars", - "serde", - "serde-cw-value", -] - [[package]] name = "osmosis-std" version = "0.15.3" @@ -1496,7 +1480,7 @@ checksum = "87725a7480b98887167edf878daa52201a13322ad88e34355a7f2ddc663e047e" dependencies = [ "chrono", "cosmwasm-std", - "osmosis-std-derive 0.15.3", + "osmosis-std-derive", "prost 0.11.9", "prost-types", "schemars", @@ -1504,18 +1488,6 @@ dependencies = [ "serde-cw-value", ] -[[package]] -name = "osmosis-std-derive" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a455e262a6fdfd3914f3a4e11e6bc0ce491901cb9d507d7856d7ef6e129e90c6" -dependencies = [ - "itertools", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "osmosis-std-derive" version = "0.15.3" @@ -1538,7 +1510,7 @@ dependencies = [ "bindgen", "cosmrs", "cosmwasm-std", - "osmosis-std 0.15.3", + "osmosis-std", "prost 0.11.9", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 23988ae8d..7e0a484cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,22 +30,22 @@ documentation = "https://docs.marsprotocol.io/" keywords = ["mars", "cosmos", "cosmwasm"] [workspace.dependencies] -anyhow = "1.0.68" +anyhow = "1.0.71" bech32 = "0.9.1" -cosmwasm-schema = "1.1.9" -cosmwasm-std = "1.1.9" +cosmwasm-schema = "1.2.6" +cosmwasm-std = "1.2.6" cw2 = { git = "https://github.com/CosmWasm/cw-plus", rev = "de1fb0b" } -cw-multi-test = "0.16.1" +cw-multi-test = "0.16.5" cw-storage-plus = "1.0.1" cw-utils = "1.0.1" mars-owner = { version = "1.2.0", features = ["emergency-owner"] } -osmosis-std = "0.14.0" +osmosis-std = "0.15.3" osmosis-test-tube = "15.1.0" prost = { version = "0.11.5", default-features = false, features = ["prost-derive"] } -pyth-sdk-cw = "1.0.0" -schemars = "0.8.11" -serde = { version = "1.0.152", default-features = false, features = ["derive"] } -thiserror = "1.0.38" +pyth-sdk-cw = "1.2.0" +schemars = "0.8.12" +serde = { version = "1.0.163", default-features = false, features = ["derive"] } +thiserror = "1.0.40" # packages mars-health = { version = "1.0.0", path = "./packages/health" } diff --git a/contracts/oracle/osmosis/tests/test_query_price.rs b/contracts/oracle/osmosis/tests/test_query_price.rs index 5d8c60a00..e6eb05308 100644 --- a/contracts/oracle/osmosis/tests/test_query_price.rs +++ b/contracts/oracle/osmosis/tests/test_query_price.rs @@ -13,7 +13,7 @@ use mars_oracle_osmosis::{ use mars_red_bank_types::oracle::{PriceResponse, QueryMsg}; use mars_testing::{mock_env_at_block_time, MarsMockQuerier}; use osmosis_std::types::osmosis::{ - gamm::v2::QuerySpotPriceResponse, + poolmanager::v1beta1::SpotPriceResponse, twap::v1beta1::{ArithmeticTwapToNowResponse, GeometricTwapToNowResponse}, }; use pyth_sdk_cw::{Price, PriceFeed, PriceFeedResponse, PriceIdentifier}; @@ -59,7 +59,7 @@ fn querying_spot_price() { 89, "umars", "uosmo", - QuerySpotPriceResponse { + SpotPriceResponse { spot_price: Decimal::from_ratio(88888u128, 12345u128).to_string(), }, ); @@ -1129,7 +1129,7 @@ fn querying_all_prices() { 1, "uatom", "uosmo", - QuerySpotPriceResponse { + SpotPriceResponse { spot_price: Decimal::from_ratio(77777u128, 12345u128).to_string(), }, ); @@ -1137,7 +1137,7 @@ fn querying_all_prices() { 89, "umars", "uosmo", - QuerySpotPriceResponse { + SpotPriceResponse { spot_price: Decimal::from_ratio(88888u128, 12345u128).to_string(), }, ); diff --git a/contracts/rewards-collector/osmosis/src/route.rs b/contracts/rewards-collector/osmosis/src/route.rs index de8085807..34262e4f9 100644 --- a/contracts/rewards-collector/osmosis/src/route.rs +++ b/contracts/rewards-collector/osmosis/src/route.rs @@ -5,7 +5,10 @@ use mars_osmosis::helpers::{has_denom, query_arithmetic_twap_price, query_pool}; use mars_rewards_collector_base::{ContractError, ContractResult, Route}; use osmosis_std::types::{ cosmos::base::v1beta1::Coin, - osmosis::gamm::v1beta1::{MsgSwapExactAmountIn, SwapAmountInRoute as OsmosisSwapAmountInRoute}, + osmosis::{ + gamm::v1beta1::MsgSwapExactAmountIn, + poolmanager::v1beta1::SwapAmountInRoute as OsmosisSwapAmountInRoute, + }, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; diff --git a/contracts/rewards-collector/osmosis/tests/test_swap.rs b/contracts/rewards-collector/osmosis/tests/test_swap.rs index de7c9d404..60465aedf 100644 --- a/contracts/rewards-collector/osmosis/tests/test_swap.rs +++ b/contracts/rewards-collector/osmosis/tests/test_swap.rs @@ -8,7 +8,7 @@ use mars_testing::mock_info; use osmosis_std::types::{ cosmos::base::v1beta1::Coin, osmosis::{ - gamm::v1beta1::{MsgSwapExactAmountIn, SwapAmountInRoute}, + gamm::v1beta1::MsgSwapExactAmountIn, poolmanager::v1beta1::SwapAmountInRoute, twap::v1beta1::ArithmeticTwapToNowResponse, }, }; diff --git a/integration-tests/tests/helpers.rs b/integration-tests/tests/helpers.rs index a3833e48e..1fd286a00 100644 --- a/integration-tests/tests/helpers.rs +++ b/integration-tests/tests/helpers.rs @@ -7,8 +7,9 @@ use mars_red_bank::error::ContractError; use mars_red_bank_types::red_bank::{ InitOrUpdateAssetParams, InterestRateModel, UserHealthStatus, UserPositionResponse, }; -use osmosis_std::types::osmosis::gamm::v1beta1::{ - MsgSwapExactAmountIn, MsgSwapExactAmountInResponse, SwapAmountInRoute, +use osmosis_std::types::osmosis::{ + gamm::v1beta1::{MsgSwapExactAmountIn, MsgSwapExactAmountInResponse}, + poolmanager::v1beta1::SwapAmountInRoute, }; use osmosis_test_tube::{Account, ExecuteResponse, OsmosisTestApp, Runner, SigningAccount}; diff --git a/integration-tests/tests/test_rewards_collector.rs b/integration-tests/tests/test_rewards_collector.rs index 6b56ddef9..ae56315ab 100644 --- a/integration-tests/tests/test_rewards_collector.rs +++ b/integration-tests/tests/test_rewards_collector.rs @@ -240,7 +240,7 @@ fn distribute_rewards_if_ibc_channel_invalid() { &[ coin(1_000_000_000_000, "uusdc"), coin(1_000_000_000_000, "umars"), - coin(1_000_000_000_000, "uosmo"), + coin(1_000_000_000_000, "uosmo"), // for gas ], 2, ) diff --git a/packages/chains/osmosis/src/helpers.rs b/packages/chains/osmosis/src/helpers.rs index 8bedda778..27599107b 100644 --- a/packages/chains/osmosis/src/helpers.rs +++ b/packages/chains/osmosis/src/helpers.rs @@ -10,9 +10,10 @@ use osmosis_std::{ osmosis::{ downtimedetector::v1beta1::DowntimedetectorQuerier, gamm::{ - v1beta1::{PoolAsset, PoolParams, QueryPoolRequest}, + v1beta1::{PoolAsset, PoolParams}, v2::GammQuerier, }, + poolmanager::v1beta1::PoolRequest, twap::v1beta1::TwapQuerier, }, }, @@ -52,7 +53,7 @@ pub struct QueryPoolResponse { /// Query an Osmosis pool's coin depths and the supply of of liquidity token pub fn query_pool(querier: &QuerierWrapper, pool_id: u64) -> StdResult { - let req: QueryRequest = QueryPoolRequest { + let req: QueryRequest = PoolRequest { pool_id, } .into(); @@ -65,6 +66,9 @@ pub fn has_denom(denom: &str, pool_assets: &[PoolAsset]) -> bool { } /// Query the spot price of a coin, denominated in OSMO +/// +/// FIXME: migrate to Spot queries from PoolManager once whitelisted in https://github.com/osmosis-labs/osmosis/blob/main/wasmbinding/stargate_whitelist.go#L127 +#[allow(deprecated)] pub fn query_spot_price( querier: &QuerierWrapper, pool_id: u64, diff --git a/packages/testing/src/mars_mock_querier.rs b/packages/testing/src/mars_mock_querier.rs index 13404366b..9df685ab2 100644 --- a/packages/testing/src/mars_mock_querier.rs +++ b/packages/testing/src/mars_mock_querier.rs @@ -13,7 +13,7 @@ use mars_osmosis::helpers::QueryPoolResponse; use mars_red_bank_types::{address_provider, incentives, oracle, red_bank}; use osmosis_std::types::osmosis::{ downtimedetector::v1beta1::RecoveredSinceDowntimeOfLengthResponse, - gamm::v2::QuerySpotPriceResponse, + poolmanager::v1beta1::SpotPriceResponse, twap::v1beta1::{ArithmeticTwapToNowResponse, GeometricTwapToNowResponse}, }; use pyth_sdk_cw::{PriceFeedResponse, PriceIdentifier}; @@ -96,7 +96,7 @@ impl MarsMockQuerier { id: u64, base_asset_denom: &str, quote_asset_denom: &str, - spot_price: QuerySpotPriceResponse, + spot_price: SpotPriceResponse, ) { let price_key = PriceKey { pool_id: id, diff --git a/packages/testing/src/osmosis_querier.rs b/packages/testing/src/osmosis_querier.rs index c23109335..c3e347b77 100644 --- a/packages/testing/src/osmosis_querier.rs +++ b/packages/testing/src/osmosis_querier.rs @@ -6,10 +6,7 @@ use osmosis_std::types::osmosis::{ downtimedetector::v1beta1::{ RecoveredSinceDowntimeOfLengthRequest, RecoveredSinceDowntimeOfLengthResponse, }, - gamm::{ - v1beta1::QueryPoolRequest, - v2::{QuerySpotPriceRequest, QuerySpotPriceResponse}, - }, + poolmanager::v1beta1::{PoolRequest, SpotPriceRequest, SpotPriceResponse}, twap::v1beta1::{ ArithmeticTwapToNowRequest, ArithmeticTwapToNowResponse, GeometricTwapToNowRequest, GeometricTwapToNowResponse, @@ -28,7 +25,7 @@ pub struct PriceKey { pub struct OsmosisQuerier { pub pools: HashMap, - pub spot_prices: HashMap, + pub spot_prices: HashMap, pub arithmetic_twap_prices: HashMap, pub geometric_twap_prices: HashMap, @@ -37,8 +34,8 @@ pub struct OsmosisQuerier { impl OsmosisQuerier { pub fn handle_stargate_query(&self, path: &str, data: &Binary) -> Result { - if path == "/osmosis.gamm.v1beta1.Query/Pool" { - let parse_osmosis_query: Result = + if path == "/osmosis.poolmanager.v1beta1.Query/Pool" { + let parse_osmosis_query: Result = Message::decode(data.as_slice()); if let Ok(osmosis_query) = parse_osmosis_query { return Ok(self.handle_query_pool_request(osmosis_query)); @@ -46,7 +43,7 @@ impl OsmosisQuerier { } if path == "/osmosis.gamm.v2.Query/SpotPrice" { - let parse_osmosis_query: Result = + let parse_osmosis_query: Result = Message::decode(data.as_slice()); if let Ok(osmosis_query) = parse_osmosis_query { return Ok(self.handle_query_spot_request(osmosis_query)); @@ -80,7 +77,7 @@ impl OsmosisQuerier { Err(()) } - fn handle_query_pool_request(&self, request: QueryPoolRequest) -> QuerierResult { + fn handle_query_pool_request(&self, request: PoolRequest) -> QuerierResult { let pool_id = request.pool_id; let res: ContractResult = match self.pools.get(&pool_id) { Some(query_response) => to_binary(&query_response).into(), @@ -93,7 +90,7 @@ impl OsmosisQuerier { Ok(res).into() } - fn handle_query_spot_request(&self, request: QuerySpotPriceRequest) -> QuerierResult { + fn handle_query_spot_request(&self, request: SpotPriceRequest) -> QuerierResult { let price_key = PriceKey { pool_id: request.pool_id, denom_in: request.base_asset_denom, diff --git a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts index d125fda51..6f775dea6 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts @@ -146,7 +146,7 @@ export interface MarsOracleOsmosisInterface extends MarsOracleOsmosisReadOnlyInt }, fee?: number | StdFee | 'auto', memo?: string, - funds?: Coin[], + _funds?: Coin[], ) => Promise } export class MarsOracleOsmosisClient @@ -242,7 +242,7 @@ export class MarsOracleOsmosisClient }, fee: number | StdFee | 'auto' = 'auto', memo?: string, - funds?: Coin[], + _funds?: Coin[], ): Promise => { return await this.client.execute( this.sender, @@ -254,7 +254,7 @@ export class MarsOracleOsmosisClient }, fee, memo, - funds, + _funds, ) } } From 5b3393e458d54b400b7718fec64cff2bd3c197b7 Mon Sep 17 00:00:00 2001 From: Piotr Babel Date: Fri, 9 Jun 2023 14:39:53 +0200 Subject: [PATCH 08/11] Bump contract ver and rust-optimizer. --- Cargo.lock | 26 +++++++++---------- Cargo.toml | 2 +- Makefile.toml | 4 +-- contracts/oracle/osmosis/src/migrations.rs | 6 ++--- .../mars-address-provider.json | 2 +- schemas/mars-incentives/mars-incentives.json | 2 +- .../mars-oracle-osmosis.json | 2 +- schemas/mars-red-bank/mars-red-bank.json | 2 +- .../mars-rewards-collector-osmosis.json | 2 +- 9 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 44c7338b4..77dd9d137 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,7 +1160,7 @@ checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" [[package]] name = "mars-address-provider" -version = "1.0.1" +version = "1.0.2" dependencies = [ "bech32", "cosmwasm-schema", @@ -1175,7 +1175,7 @@ dependencies = [ [[package]] name = "mars-health" -version = "1.0.1" +version = "1.0.2" dependencies = [ "cosmwasm-std", "mars-red-bank-types", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "mars-incentives" -version = "1.0.1" +version = "1.0.2" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1200,7 +1200,7 @@ dependencies = [ [[package]] name = "mars-integration-tests" -version = "1.0.1" +version = "1.0.2" dependencies = [ "anyhow", "cosmwasm-std", @@ -1221,7 +1221,7 @@ dependencies = [ [[package]] name = "mars-oracle-base" -version = "1.0.1" +version = "1.0.2" dependencies = [ "cosmwasm-std", "cw-storage-plus", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "mars-oracle-osmosis" -version = "1.0.1" +version = "1.0.2" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1257,7 +1257,7 @@ dependencies = [ [[package]] name = "mars-osmosis" -version = "1.0.1" +version = "1.0.2" dependencies = [ "cosmwasm-std", "osmosis-std", @@ -1279,7 +1279,7 @@ dependencies = [ [[package]] name = "mars-red-bank" -version = "1.0.1" +version = "1.0.2" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1296,7 +1296,7 @@ dependencies = [ [[package]] name = "mars-red-bank-types" -version = "1.0.1" +version = "1.0.2" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1307,7 +1307,7 @@ dependencies = [ [[package]] name = "mars-rewards-collector-base" -version = "1.0.1" +version = "1.0.2" dependencies = [ "cosmwasm-std", "cw-storage-plus", @@ -1322,7 +1322,7 @@ dependencies = [ [[package]] name = "mars-rewards-collector-osmosis" -version = "1.0.1" +version = "1.0.2" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1342,7 +1342,7 @@ dependencies = [ [[package]] name = "mars-testing" -version = "1.0.1" +version = "1.0.2" dependencies = [ "anyhow", "cosmwasm-std", @@ -1364,7 +1364,7 @@ dependencies = [ [[package]] name = "mars-utils" -version = "1.0.1" +version = "1.0.2" dependencies = [ "cosmwasm-std", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 7e0a484cc..4be54065a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ members = [ ] [workspace.package] -version = "1.0.1" +version = "1.0.2" authors = [ "Larry Engineer ", "Piotr Babel ", diff --git a/Makefile.toml b/Makefile.toml index 16a21b35e..6d3057612 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -18,9 +18,9 @@ args = ["build", "--release", "--target", "wasm32-unknown-unknown", "--locked"] [tasks.rust-optimizer] script = """ if [[ $(arch) == "arm64" ]]; then - image="cosmwasm/workspace-optimizer-arm64:0.12.11" + image="cosmwasm/workspace-optimizer-arm64:0.12.13" else - image="cosmwasm/workspace-optimizer:0.12.11" + image="cosmwasm/workspace-optimizer:0.12.13" fi docker run --rm -v "$(pwd)":/code \ --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ diff --git a/contracts/oracle/osmosis/src/migrations.rs b/contracts/oracle/osmosis/src/migrations.rs index f6b087e7a..94dfe74d5 100644 --- a/contracts/oracle/osmosis/src/migrations.rs +++ b/contracts/oracle/osmosis/src/migrations.rs @@ -5,7 +5,7 @@ pub mod v1_0_0 { use crate::contract::{CONTRACT_NAME, CONTRACT_VERSION}; - const FROM_VERSION: &str = "1.0.0"; + const FROM_VERSION: &str = "1.0.1"; pub fn migrate(deps: DepsMut) -> ContractResult { // make sure we're migrating the correct contract and from the correct version @@ -38,8 +38,8 @@ pub mod v1_0_0 { res.attributes, vec![ attr("action", "migrate"), - attr("from_version", "1.0.0"), - attr("to_version", "1.0.1") + attr("from_version", "1.0.1"), + attr("to_version", "1.0.2") ] ); } diff --git a/schemas/mars-address-provider/mars-address-provider.json b/schemas/mars-address-provider/mars-address-provider.json index e3c3cea06..1485617bb 100644 --- a/schemas/mars-address-provider/mars-address-provider.json +++ b/schemas/mars-address-provider/mars-address-provider.json @@ -1,6 +1,6 @@ { "contract_name": "mars-address-provider", - "contract_version": "1.0.1", + "contract_version": "1.0.2", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-incentives/mars-incentives.json b/schemas/mars-incentives/mars-incentives.json index 373dde4cb..3cc610ca7 100644 --- a/schemas/mars-incentives/mars-incentives.json +++ b/schemas/mars-incentives/mars-incentives.json @@ -1,6 +1,6 @@ { "contract_name": "mars-incentives", - "contract_version": "1.0.1", + "contract_version": "1.0.2", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json b/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json index a27fc05b4..84df63a8d 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": "1.0.1", + "contract_version": "1.0.2", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-red-bank/mars-red-bank.json b/schemas/mars-red-bank/mars-red-bank.json index 4c7b02ce0..46c363b5e 100644 --- a/schemas/mars-red-bank/mars-red-bank.json +++ b/schemas/mars-red-bank/mars-red-bank.json @@ -1,6 +1,6 @@ { "contract_name": "mars-red-bank", - "contract_version": "1.0.1", + "contract_version": "1.0.2", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-rewards-collector-osmosis/mars-rewards-collector-osmosis.json b/schemas/mars-rewards-collector-osmosis/mars-rewards-collector-osmosis.json index d76cde937..f966ee9c0 100644 --- a/schemas/mars-rewards-collector-osmosis/mars-rewards-collector-osmosis.json +++ b/schemas/mars-rewards-collector-osmosis/mars-rewards-collector-osmosis.json @@ -1,6 +1,6 @@ { "contract_name": "mars-rewards-collector-osmosis", - "contract_version": "1.0.1", + "contract_version": "1.0.2", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", From 23f64579a0dfcfc4f5d50f1999666d5b1b45b869 Mon Sep 17 00:00:00 2001 From: Piotr Babel Date: Fri, 9 Jun 2023 15:03:57 +0200 Subject: [PATCH 09/11] Use old Pool query. --- packages/chains/osmosis/src/helpers.rs | 7 ++++++- packages/testing/src/osmosis_querier.rs | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/chains/osmosis/src/helpers.rs b/packages/chains/osmosis/src/helpers.rs index 27599107b..1503569fd 100644 --- a/packages/chains/osmosis/src/helpers.rs +++ b/packages/chains/osmosis/src/helpers.rs @@ -3,6 +3,9 @@ use std::str::FromStr; use cosmwasm_std::{ coin, Decimal, Empty, QuerierWrapper, QueryRequest, StdError, StdResult, Uint128, }; +/// FIXME: migrate to Spot queries from PoolManager once whitelisted in https://github.com/osmosis-labs/osmosis/blob/main/wasmbinding/stargate_whitelist.go#L127 +#[allow(deprecated)] +use osmosis_std::types::osmosis::gamm::v1beta1::QueryPoolRequest as PoolRequest; use osmosis_std::{ shim::{Duration, Timestamp}, types::{ @@ -13,7 +16,6 @@ use osmosis_std::{ v1beta1::{PoolAsset, PoolParams}, v2::GammQuerier, }, - poolmanager::v1beta1::PoolRequest, twap::v1beta1::TwapQuerier, }, }, @@ -52,6 +54,9 @@ pub struct QueryPoolResponse { } /// Query an Osmosis pool's coin depths and the supply of of liquidity token +/// +/// FIXME: migrate to Spot queries from PoolManager once whitelisted in https://github.com/osmosis-labs/osmosis/blob/main/wasmbinding/stargate_whitelist.go#L127 +#[allow(deprecated)] pub fn query_pool(querier: &QuerierWrapper, pool_id: u64) -> StdResult { let req: QueryRequest = PoolRequest { pool_id, diff --git a/packages/testing/src/osmosis_querier.rs b/packages/testing/src/osmosis_querier.rs index c3e347b77..539ba769a 100644 --- a/packages/testing/src/osmosis_querier.rs +++ b/packages/testing/src/osmosis_querier.rs @@ -34,7 +34,7 @@ pub struct OsmosisQuerier { impl OsmosisQuerier { pub fn handle_stargate_query(&self, path: &str, data: &Binary) -> Result { - if path == "/osmosis.poolmanager.v1beta1.Query/Pool" { + if path == "/osmosis.gamm.v1beta1.Query/Pool" { let parse_osmosis_query: Result = Message::decode(data.as_slice()); if let Ok(osmosis_query) = parse_osmosis_query { From 9526219b7c2ce7cb5326f1cf714d4118e62110c8 Mon Sep 17 00:00:00 2001 From: Piotr Babel Date: Mon, 12 Jun 2023 10:23:40 +0200 Subject: [PATCH 10/11] Set version to 1.1.0. --- Cargo.lock | 26 +++++++++---------- Cargo.toml | 2 +- contracts/oracle/osmosis/src/migrations.rs | 2 +- .../mars-address-provider.json | 2 +- schemas/mars-incentives/mars-incentives.json | 2 +- .../mars-oracle-osmosis.json | 2 +- schemas/mars-red-bank/mars-red-bank.json | 2 +- .../mars-rewards-collector-osmosis.json | 2 +- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 77dd9d137..f57b9752e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,7 +1160,7 @@ checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" [[package]] name = "mars-address-provider" -version = "1.0.2" +version = "1.1.0" dependencies = [ "bech32", "cosmwasm-schema", @@ -1175,7 +1175,7 @@ dependencies = [ [[package]] name = "mars-health" -version = "1.0.2" +version = "1.1.0" dependencies = [ "cosmwasm-std", "mars-red-bank-types", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "mars-incentives" -version = "1.0.2" +version = "1.1.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1200,7 +1200,7 @@ dependencies = [ [[package]] name = "mars-integration-tests" -version = "1.0.2" +version = "1.1.0" dependencies = [ "anyhow", "cosmwasm-std", @@ -1221,7 +1221,7 @@ dependencies = [ [[package]] name = "mars-oracle-base" -version = "1.0.2" +version = "1.1.0" dependencies = [ "cosmwasm-std", "cw-storage-plus", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "mars-oracle-osmosis" -version = "1.0.2" +version = "1.1.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1257,7 +1257,7 @@ dependencies = [ [[package]] name = "mars-osmosis" -version = "1.0.2" +version = "1.1.0" dependencies = [ "cosmwasm-std", "osmosis-std", @@ -1279,7 +1279,7 @@ dependencies = [ [[package]] name = "mars-red-bank" -version = "1.0.2" +version = "1.1.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1296,7 +1296,7 @@ dependencies = [ [[package]] name = "mars-red-bank-types" -version = "1.0.2" +version = "1.1.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1307,7 +1307,7 @@ dependencies = [ [[package]] name = "mars-rewards-collector-base" -version = "1.0.2" +version = "1.1.0" dependencies = [ "cosmwasm-std", "cw-storage-plus", @@ -1322,7 +1322,7 @@ dependencies = [ [[package]] name = "mars-rewards-collector-osmosis" -version = "1.0.2" +version = "1.1.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1342,7 +1342,7 @@ dependencies = [ [[package]] name = "mars-testing" -version = "1.0.2" +version = "1.1.0" dependencies = [ "anyhow", "cosmwasm-std", @@ -1364,7 +1364,7 @@ dependencies = [ [[package]] name = "mars-utils" -version = "1.0.2" +version = "1.1.0" dependencies = [ "cosmwasm-std", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 4be54065a..ffd421241 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ members = [ ] [workspace.package] -version = "1.0.2" +version = "1.1.0" authors = [ "Larry Engineer ", "Piotr Babel ", diff --git a/contracts/oracle/osmosis/src/migrations.rs b/contracts/oracle/osmosis/src/migrations.rs index 94dfe74d5..93a88f7b2 100644 --- a/contracts/oracle/osmosis/src/migrations.rs +++ b/contracts/oracle/osmosis/src/migrations.rs @@ -39,7 +39,7 @@ pub mod v1_0_0 { vec![ attr("action", "migrate"), attr("from_version", "1.0.1"), - attr("to_version", "1.0.2") + attr("to_version", "1.1.0") ] ); } diff --git a/schemas/mars-address-provider/mars-address-provider.json b/schemas/mars-address-provider/mars-address-provider.json index 1485617bb..dcdf2e3f4 100644 --- a/schemas/mars-address-provider/mars-address-provider.json +++ b/schemas/mars-address-provider/mars-address-provider.json @@ -1,6 +1,6 @@ { "contract_name": "mars-address-provider", - "contract_version": "1.0.2", + "contract_version": "1.1.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-incentives/mars-incentives.json b/schemas/mars-incentives/mars-incentives.json index 3cc610ca7..a47a92113 100644 --- a/schemas/mars-incentives/mars-incentives.json +++ b/schemas/mars-incentives/mars-incentives.json @@ -1,6 +1,6 @@ { "contract_name": "mars-incentives", - "contract_version": "1.0.2", + "contract_version": "1.1.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json b/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json index 84df63a8d..d0e0532be 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": "1.0.2", + "contract_version": "1.1.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-red-bank/mars-red-bank.json b/schemas/mars-red-bank/mars-red-bank.json index 46c363b5e..d961e6207 100644 --- a/schemas/mars-red-bank/mars-red-bank.json +++ b/schemas/mars-red-bank/mars-red-bank.json @@ -1,6 +1,6 @@ { "contract_name": "mars-red-bank", - "contract_version": "1.0.2", + "contract_version": "1.1.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-rewards-collector-osmosis/mars-rewards-collector-osmosis.json b/schemas/mars-rewards-collector-osmosis/mars-rewards-collector-osmosis.json index f966ee9c0..bf881cde9 100644 --- a/schemas/mars-rewards-collector-osmosis/mars-rewards-collector-osmosis.json +++ b/schemas/mars-rewards-collector-osmosis/mars-rewards-collector-osmosis.json @@ -1,6 +1,6 @@ { "contract_name": "mars-rewards-collector-osmosis", - "contract_version": "1.0.2", + "contract_version": "1.1.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", From aab034ea186c37d2df5219e1bb3b85bfb44cbd64 Mon Sep 17 00:00:00 2001 From: piobab Date: Mon, 12 Jun 2023 15:27:07 +0200 Subject: [PATCH 11/11] Migrate owner state. (#205) --- contracts/oracle/osmosis/Cargo.toml | 2 + contracts/oracle/osmosis/src/contract.rs | 2 +- contracts/oracle/osmosis/src/migrations.rs | 103 ++++++++++++++++++++- 3 files changed, 102 insertions(+), 5 deletions(-) diff --git a/contracts/oracle/osmosis/Cargo.toml b/contracts/oracle/osmosis/Cargo.toml index 6950a9118..9cb816041 100644 --- a/contracts/oracle/osmosis/Cargo.toml +++ b/contracts/oracle/osmosis/Cargo.toml @@ -19,9 +19,11 @@ doctest = false backtraces = ["cosmwasm-std/backtraces"] [dependencies] +cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw2 = { workspace = true } cw-storage-plus = { workspace = true } +mars-owner = { workspace = true } mars-oracle-base = { workspace = true } mars-osmosis = { workspace = true } mars-red-bank-types = { workspace = true } diff --git a/contracts/oracle/osmosis/src/contract.rs b/contracts/oracle/osmosis/src/contract.rs index edf584b39..ab7627d9d 100644 --- a/contracts/oracle/osmosis/src/contract.rs +++ b/contracts/oracle/osmosis/src/contract.rs @@ -48,6 +48,6 @@ pub mod entry { #[entry_point] pub fn migrate(deps: DepsMut, _env: Env, _msg: Empty) -> ContractResult { - migrations::v1_0_0::migrate(deps) + migrations::v1_0_1::migrate(deps) } } diff --git a/contracts/oracle/osmosis/src/migrations.rs b/contracts/oracle/osmosis/src/migrations.rs index 93a88f7b2..67ecd6176 100644 --- a/contracts/oracle/osmosis/src/migrations.rs +++ b/contracts/oracle/osmosis/src/migrations.rs @@ -1,7 +1,8 @@ -/// Migration logic for Oracle contract with version: 1.0.0 -pub mod v1_0_0 { +/// Migration logic for Oracle contract with version: 1.0.1 +pub mod v1_0_1 { use cosmwasm_std::{DepsMut, Response}; use mars_oracle_base::ContractResult; + use mars_owner::{Owner, OwnerInit}; use crate::contract::{CONTRACT_NAME, CONTRACT_VERSION}; @@ -11,6 +12,25 @@ pub mod v1_0_0 { // make sure we're migrating the correct contract and from the correct version cw2::assert_contract_version(deps.as_ref().storage, CONTRACT_NAME, FROM_VERSION)?; + // map old owner struct to new one + let old_owner = old_state::OWNER.load(deps.storage)?; + let owner = match old_owner { + old_state::OwnerState::B(state) => state.owner.to_string(), + old_state::OwnerState::C(state) => state.owner.to_string(), + }; + + // clear old owner state + old_state::OWNER.remove(deps.storage); + + // initalize owner with new struct + Owner::new("owner").initialize( + deps.storage, + deps.api, + OwnerInit::SetInitialOwner { + owner, + }, + )?; + // update contract version cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; @@ -20,18 +40,60 @@ pub mod v1_0_0 { .add_attribute("to_version", CONTRACT_VERSION)) } + pub mod old_state { + use cosmwasm_schema::cw_serde; + use cosmwasm_std::Addr; + use cw_storage_plus::Item; + + pub const OWNER: Item = Item::new("owner"); + + /// Old OwnerState variants: + /// A(OwnerUninitialized) + /// B(OwnerSetNoneProposed) + /// C(OwnerSetWithProposed) + /// D(OwnerRoleAbolished) + /// + /// Oracle contract can be in B or C state. Emergency owner is not supported for this contract. + /// We can only read `owner` value and omit `proposed` if exist. + #[cw_serde] + pub enum OwnerState { + B(OwnerSetNoneProposed), + C(OwnerSetWithProposed), + } + + #[cw_serde] + pub struct OwnerSetNoneProposed { + pub owner: Addr, + } + + #[cw_serde] + pub struct OwnerSetWithProposed { + pub owner: Addr, + } + } + #[cfg(test)] mod tests { - use cosmwasm_std::{attr, testing::mock_dependencies}; + use cosmwasm_std::{attr, testing::mock_dependencies, Addr}; use super::*; + use crate::migrations::v1_0_1::old_state::{OwnerSetNoneProposed, OwnerSetWithProposed}; #[test] - fn proper_migration() { + fn migration_owner_from_state_b() { let mut deps = mock_dependencies(); cw2::set_contract_version(deps.as_mut().storage, CONTRACT_NAME, FROM_VERSION).unwrap(); + old_state::OWNER + .save( + deps.as_mut().storage, + &old_state::OwnerState::B(OwnerSetNoneProposed { + owner: Addr::unchecked("xyz"), + }), + ) + .unwrap(); + let res = migrate(deps.as_mut()).unwrap(); assert_eq!(res.messages, vec![]); assert_eq!( @@ -42,6 +104,39 @@ pub mod v1_0_0 { attr("to_version", "1.1.0") ] ); + + let new_owner = Owner::new("owner").query(&deps.storage).unwrap(); + assert_eq!(new_owner.owner.unwrap(), "xyz".to_string()); + } + + #[test] + fn migration_owner_from_state_c() { + let mut deps = mock_dependencies(); + + cw2::set_contract_version(deps.as_mut().storage, CONTRACT_NAME, FROM_VERSION).unwrap(); + + old_state::OWNER + .save( + deps.as_mut().storage, + &old_state::OwnerState::C(OwnerSetWithProposed { + owner: Addr::unchecked("xyz"), + }), + ) + .unwrap(); + + let res = migrate(deps.as_mut()).unwrap(); + assert_eq!(res.messages, vec![]); + assert_eq!( + res.attributes, + vec![ + attr("action", "migrate"), + attr("from_version", "1.0.1"), + attr("to_version", "1.1.0") + ] + ); + + let new_owner = Owner::new("owner").query(&deps.storage).unwrap(); + assert_eq!(new_owner.owner.unwrap(), "xyz".to_string()); } } }