diff --git a/Cargo.lock b/Cargo.lock index 2505cba5f..f57b9752e 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", @@ -484,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", @@ -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" @@ -1103,13 +1160,13 @@ checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" [[package]] name = "mars-address-provider" -version = "1.0.1" +version = "1.1.0" dependencies = [ "bech32", "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", @@ -1118,7 +1175,7 @@ dependencies = [ [[package]] name = "mars-health" -version = "1.0.1" +version = "1.1.0" dependencies = [ "cosmwasm-std", "mars-red-bank-types", @@ -1128,12 +1185,12 @@ dependencies = [ [[package]] name = "mars-incentives" -version = "1.0.1" +version = "1.1.0" 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", @@ -1143,7 +1200,7 @@ dependencies = [ [[package]] name = "mars-integration-tests" -version = "1.0.1" +version = "1.1.0" dependencies = [ "anyhow", "cosmwasm-std", @@ -1157,18 +1214,18 @@ dependencies = [ "mars-rewards-collector-osmosis", "mars-testing", "mars-utils", - "osmosis-std 0.14.0", + "osmosis-std", "osmosis-test-tube", "serde", ] [[package]] name = "mars-oracle-base" -version = "1.0.1" +version = "1.1.0" 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", @@ -1179,19 +1236,20 @@ dependencies = [ [[package]] name = "mars-oracle-osmosis" -version = "1.0.1" +version = "1.1.0" 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", "mars-red-bank-types", "mars-testing", "mars-utils", - "osmosis-std 0.14.0", + "osmosis-std", + "pyth-sdk-cw", "schemars", "serde", "thiserror", @@ -1199,10 +1257,10 @@ dependencies = [ [[package]] name = "mars-osmosis" -version = "1.0.1" +version = "1.1.0" dependencies = [ "cosmwasm-std", - "osmosis-std 0.14.0", + "osmosis-std", "serde", ] @@ -1221,13 +1279,13 @@ dependencies = [ [[package]] name = "mars-red-bank" -version = "1.0.1" +version = "1.1.0" dependencies = [ "cosmwasm-schema", "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", @@ -1238,7 +1296,7 @@ dependencies = [ [[package]] name = "mars-red-bank-types" -version = "1.0.1" +version = "1.1.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1249,7 +1307,7 @@ dependencies = [ [[package]] name = "mars-rewards-collector-base" -version = "1.0.1" +version = "1.1.0" dependencies = [ "cosmwasm-std", "cw-storage-plus", @@ -1264,19 +1322,19 @@ dependencies = [ [[package]] name = "mars-rewards-collector-osmosis" -version = "1.0.1" +version = "1.1.0" 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", "mars-rewards-collector-base", "mars-testing", "mars-utils", - "osmosis-std 0.14.0", + "osmosis-std", "schemars", "serde", "thiserror", @@ -1284,7 +1342,7 @@ dependencies = [ [[package]] name = "mars-testing" -version = "1.0.1" +version = "1.1.0" dependencies = [ "anyhow", "cosmwasm-std", @@ -1296,8 +1354,9 @@ 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", "serde", "thiserror", @@ -1305,7 +1364,7 @@ dependencies = [ [[package]] name = "mars-utils" -version = "1.0.1" +version = "1.1.0" dependencies = [ "cosmwasm-std", "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" @@ -1413,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" @@ -1437,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", @@ -1445,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" @@ -1479,7 +1510,7 @@ dependencies = [ "bindgen", "cosmrs", "cosmwasm-std", - "osmosis-std 0.15.3", + "osmosis-std", "prost 0.11.9", "serde", "serde_json", @@ -1537,9 +1568,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 +1614,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 +1687,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 +1738,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 +1933,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 +1969,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 +2475,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..ffd421241 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ members = [ ] [workspace.package] -version = "1.0.1" +version = "1.1.0" authors = [ "Larry Engineer ", "Piotr Babel ", @@ -30,21 +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" -cw2 = { git = "https://github.com/mars-protocol/cw-plus", rev = "4014255" } -cw-multi-test = "0.16.1" +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.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"] } -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/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/base/src/contract.rs b/contracts/oracle/base/src/contract.rs index 04427c1cc..835e2780c 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::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 @@ -28,13 +29,16 @@ where pub config: Item<'a, Config>, /// 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 { @@ -42,14 +46,16 @@ where owner: Owner::new("owner"), config: Item::new("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 { @@ -77,7 +83,7 @@ where &self, deps: DepsMut, info: MessageInfo, - msg: ExecuteMsg

, + msg: ExecuteMsg, ) -> ContractResult { match msg { ExecuteMsg::UpdateOwner(update) => self.update_owner(deps, info, update), @@ -88,6 +94,9 @@ where ExecuteMsg::RemovePriceSource { denom, } => self.remove_price_source(deps, info.sender, denom), + ExecuteMsg::UpdateConfig { + base_denom, + } => self.update_config(deps, info.sender, base_denom), } } @@ -126,14 +135,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() @@ -157,6 +166,31 @@ where .add_attribute("denom", denom)) } + fn update_config( + &self, + deps: DepsMut, + sender_addr: Addr, + base_denom: 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 response = Response::new() + .add_attribute("action", "update_config") + .add_attribute("prev_base_denom", prev_base_denom) + .add_attribute("base_denom", config.base_denom); + + Ok(response) + } + fn query_config(&self, deps: Deps) -> StdResult { let owner_state = self.owner.query(deps.storage)?; let cfg = self.config.load(deps.storage)?; @@ -204,13 +238,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, }) } @@ -233,7 +261,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/error.rs b/contracts/oracle/base/src/error.rs index 6fcd449df..ab60864a8 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, + DecimalRangeExceeded, OverflowError, StdError, +}; use mars_owner::OwnerError; use mars_red_bank_types::error::MarsError; use mars_utils::error::ValidationError; @@ -27,6 +30,15 @@ pub enum ContractError { #[error("{0}")] Overflow(#[from] OverflowError), + #[error("{0}")] + CheckedMultiplyRatio(#[from] CheckedMultiplyRatioError), + + #[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 05841e8fd..c78ea8cc8 100644 --- a/contracts/oracle/base/src/traits.rs +++ b/contracts/oracle/base/src/traits.rs @@ -1,32 +1,35 @@ 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::Config; use schemars::JsonSchema; 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: /// /// - `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. /// @@ -39,7 +42,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/Cargo.toml b/contracts/oracle/osmosis/Cargo.toml index f876ce167..9cb816041 100644 --- a/contracts/oracle/osmosis/Cargo.toml +++ b/contracts/oracle/osmosis/Cargo.toml @@ -19,13 +19,16 @@ 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 } osmosis-std = { workspace = true } +pyth-sdk-cw = { workspace = true } schemars = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } 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..ab7627d9d 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) } @@ -47,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/lib.rs b/contracts/oracle/osmosis/src/lib.rs index d0f1c1fc4..31089de17 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_pyth_price, Downtime, DowntimeDetector, GeometricTwap, OsmosisPriceSourceChecked, + OsmosisPriceSourceUnchecked, RedemptionRate, +}; diff --git a/contracts/oracle/osmosis/src/migrations.rs b/contracts/oracle/osmosis/src/migrations.rs index f6b087e7a..67ecd6176 100644 --- a/contracts/oracle/osmosis/src/migrations.rs +++ b/contracts/oracle/osmosis/src/migrations.rs @@ -1,16 +1,36 @@ -/// 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}; - 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 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,28 +40,103 @@ 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!( 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.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()); } } } 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 e23662b65..dc9c52fb8 100644 --- a/contracts/oracle/osmosis/src/price_source.rs +++ b/contracts/oracle/osmosis/src/price_source.rs @@ -1,18 +1,20 @@ -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, }; +use mars_red_bank_types::oracle::Config; +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 /// @@ -80,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, @@ -149,9 +151,91 @@ pub enum OsmosisPriceSource { /// Detect when the chain is recovering from downtime 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 + /// 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 + /// 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: + /// - 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 + /// + /// osmo_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 + denom_decimals: u8, + }, + /// 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, +} + +#[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, } -impl fmt::Display for OsmosisPriceSource { +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 { @@ -188,81 +272,164 @@ 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 { + contract_addr, + price_feed_id, + max_staleness, + denom_decimals, + } => { + format!("pyth:{contract_addr}:{price_feed_id}:{max_staleness}:{denom_decimals}") + } + 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)?; + Ok(OsmosisPriceSourceChecked::StakedGeometricTwap { + transitive_denom: transitive_denom.to_string(), + pool_id: *pool_id, + window_size: *window_size, + downtime_detector: downtime_detector.clone(), + }) + } + OsmosisPriceSourceUnchecked::Pyth { + 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, + 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(*window_size, downtime_detector) + 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, + }, + }) } } } +} +impl PriceSourceChecked for OsmosisPriceSourceChecked { fn query_price( &self, deps: &Deps, env: &Env, denom: &str, - base_denom: &str, + config: &Config, price_sources: &Map<&str, Self>, ) -> 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 { + } => query_spot_price(&deps.querier, *pool_id, denom, &config.base_denom) + .map_err(Into::into), + OsmosisPriceSourceChecked::ArithmeticTwap { pool_id, window_size, downtime_detector, @@ -270,10 +437,16 @@ impl PriceSource for OsmosisPriceSource { 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) } - OsmosisPriceSource::GeometricTwap { + OsmosisPriceSourceChecked::GeometricTwap { pool_id, window_size, downtime_detector, @@ -281,19 +454,19 @@ impl PriceSource for OsmosisPriceSource { 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) } - OsmosisPriceSource::XykLiquidityToken { + OsmosisPriceSourceChecked::XykLiquidityToken { pool_id, - } => Self::query_xyk_liquidity_token_price( - deps, - env, - *pool_id, - base_denom, - price_sources, - ), - OsmosisPriceSource::StakedGeometricTwap { + } => Self::query_xyk_liquidity_token_price(deps, env, *pool_id, config, price_sources), + OsmosisPriceSourceChecked::StakedGeometricTwap { transitive_denom, pool_id, window_size, @@ -306,9 +479,42 @@ impl PriceSource for OsmosisPriceSource { env, denom, transitive_denom, - base_denom, *pool_id, *window_size, + config, + price_sources, + ) + } + OsmosisPriceSourceChecked::Pyth { + contract_addr, + price_feed_id, + max_staleness, + denom_decimals, + } => Ok(Self::query_pyth_price( + deps, + env, + contract_addr.to_owned(), + *price_feed_id, + *max_staleness, + *denom_decimals, + config, + price_sources, + )?), + 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, + geometric_twap.clone(), + redemption_rate.clone(), + config, price_sources, ) } @@ -316,7 +522,7 @@ impl PriceSource for OsmosisPriceSource { } } -impl OsmosisPriceSource { +impl OsmosisPriceSourceChecked { fn chain_recovered( deps: &Deps, downtime_detector: &Option, @@ -345,7 +551,7 @@ impl OsmosisPriceSource { 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 @@ -358,14 +564,14 @@ impl OsmosisPriceSource { 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, )?; @@ -387,15 +593,16 @@ 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, denom: &str, transitive_denom: &str, - base_denom: &str, pool_id: u64, window_size: u64, - price_sources: &Map<&str, OsmosisPriceSource>, + config: &Config, + price_sources: &Map<&str, OsmosisPriceSourceChecked>, ) -> ContractResult { let start_time = env.block.time.seconds() - window_size; let staked_price = query_geometric_twap_price( @@ -411,113 +618,225 @@ impl OsmosisPriceSource { deps, env, transitive_denom, - base_denom, + config, price_sources, )?; staked_price.checked_mul(transitive_price).map_err(Into::into) } -} -#[cfg(test)] -mod tests { - use super::*; + /// 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, + geometric_twap: GeometricTwap, + redemption_rate: RedemptionRate, + config: &Config, + price_sources: &Map<&str, OsmosisPriceSourceChecked>, + ) -> 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, + )?; - #[test] - fn display_downtime_detector() { - let dd = DowntimeDetector { - downtime: Downtime::Duration10m, - recovery: 550, - }; - assert_eq!(dd.to_string(), "Duration10m:550") - } + // 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 + ), + }); + } - #[test] - fn display_fixed_price_source() { - let ps = OsmosisPriceSource::Fixed { - price: Decimal::from_ratio(1u128, 2u128), - }; - assert_eq!(ps.to_string(), "fixed:0.5") - } + // min from geometric TWAP and exchange rate + let min_price = min(staked_price, rr.exchange_rate); - #[test] - fn display_spot_price_source() { - let ps = OsmosisPriceSource::Spot { - pool_id: 123, - }; - assert_eq!(ps.to_string(), "spot:123") + // use current price source + let transitive_price = price_sources.load(deps.storage, transitive_denom)?.query_price( + deps, + env, + transitive_denom, + config, + price_sources, + )?; + + min_price.checked_mul(transitive_price).map_err(Into::into) } - #[test] - fn display_arithmetic_twap_price_source() { - let ps = OsmosisPriceSource::ArithmeticTwap { - pool_id: 123, - window_size: 300, - downtime_detector: None, - }; - assert_eq!(ps.to_string(), "arithmetic_twap:123:300:None"); - - let ps = OsmosisPriceSource::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)"); + fn query_pyth_price( + deps: &Deps, + env: &Env, + 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)?; + 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 + ), + }); + } + + // 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_pyth_price( + current_price.price as u128, + current_price.expo, + denom_decimals, + usd_price, + )?; + + 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. +/// +/// 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/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 ATOM = price * 10^expo USD +/// 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 * usd_price_in_base_denom * 10^(-denom_decimals) +pub fn scale_pyth_price( + value: u128, + expo: i32, + denom_decimals: u8, + usd_price: Decimal, +) -> ContractResult { + let target_expo = Uint128::from(10u8).checked_pow(expo.unsigned_abs())?; + let pyth_price = if expo < 0 { + Decimal::checked_from_ratio(value, target_expo)? + } else { + let res = Uint128::from(value).checked_mul(target_expo)?; + 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)] +mod tests { + use std::str::FromStr; + + use super::*; #[test] - fn display_geometric_twap_price_source() { - let ps = OsmosisPriceSource::GeometricTwap { - pool_id: 123, - window_size: 300, - downtime_detector: None, - }; - assert_eq!(ps.to_string(), "geometric_twap:123:300:None"); - - let ps = OsmosisPriceSource::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)"); + fn scale_real_pyth_price() { + // ATOM + 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_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 display_staked_geometric_twap_price_source() { - let ps = OsmosisPriceSource::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 = OsmosisPriceSource::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)" - ); + 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 display_xyk_lp_price_source() { - let ps = OsmosisPriceSource::XykLiquidityToken { - pool_id: 224, - }; - assert_eq!(ps.to_string(), "xyk_liquidity_token:224") + 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 new file mode 100644 index 000000000..f37d3d13b --- /dev/null +++ b/contracts/oracle/osmosis/src/stride.rs @@ -0,0 +1,44 @@ +use cosmwasm_std::{to_binary, Addr, Decimal, QuerierWrapper, QueryRequest, StdResult, WasmQuery}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[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 33272dc0e..4d06d09df 100644 --- a/contracts/oracle/osmosis/tests/helpers.rs +++ b/contracts/oracle/osmosis/tests/helpers.rs @@ -8,19 +8,15 @@ 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}; 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(), @@ -132,7 +139,20 @@ fn prepare_pool_assets(coins: &[Coin], weights: &[u64]) -> Vec { .collect() } -pub fn set_price_source(deps: DepsMut, denom: &str, price_source: OsmosisPriceSource) { +pub fn set_pyth_price_source(deps: DepsMut, denom: &str, price_id: PriceIdentifier) { + set_price_source( + deps, + denom, + OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract".to_string(), + price_feed_id: price_id, + max_staleness: 30, + denom_decimals: 6, + }, + ) +} + +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_admin.rs b/contracts/oracle/osmosis/tests/test_admin.rs index 22cdbcabc..8ab96ae0c 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,7 +10,7 @@ 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()); @@ -72,3 +73,57 @@ fn instantiating_incorrect_denom() { })) ); } + +#[test] +fn update_config_if_unauthorized() { + let mut deps = helpers::setup_test(); + + let msg = ExecuteMsg::UpdateConfig { + base_denom: 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()), + }; + 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()), + }; + 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") + ] + ); + + 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()); +} 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 a0df0dcdb..e6eb05308 100644 --- a/contracts/oracle/osmosis/tests/test_query_price.rs +++ b/contracts/oracle/osmosis/tests/test_query_price.rs @@ -1,11 +1,22 @@ -use cosmwasm_std::{coin, Decimal, StdError}; +use std::str::FromStr; + +use cosmwasm_std::{ + coin, from_binary, + testing::{MockApi, MockStorage}, + Decimal, OwnedDeps, StdError, +}; use mars_oracle_base::ContractError; -use mars_oracle_osmosis::{Downtime, DowntimeDetector, OsmosisPriceSource}; +use mars_oracle_osmosis::{ + contract::entry, scale_pyth_price, stride::RedemptionRateResponse, Downtime, DowntimeDetector, + GeometricTwap, OsmosisPriceSourceUnchecked, RedemptionRate, +}; 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}; use crate::helpers::prepare_query_pool_response; @@ -13,12 +24,12 @@ 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(), "uosmo", - OsmosisPriceSource::Fixed { + OsmosisPriceSourceUnchecked::Fixed { price: Decimal::one(), }, ); @@ -34,12 +45,12 @@ 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(), "umars", - OsmosisPriceSource::Spot { + OsmosisPriceSourceUnchecked::Spot { pool_id: 89, }, ); @@ -48,7 +59,7 @@ fn querying_spot_price() { 89, "umars", "uosmo", - QuerySpotPriceResponse { + SpotPriceResponse { spot_price: Decimal::from_ratio(88888u128, 12345u128).to_string(), }, ); @@ -64,12 +75,12 @@ 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(), "umars", - OsmosisPriceSource::ArithmeticTwap { + OsmosisPriceSourceUnchecked::ArithmeticTwap { pool_id: 89, window_size: 86400, downtime_detector: None, @@ -96,7 +107,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, @@ -105,7 +116,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()), @@ -146,12 +157,12 @@ 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(), "umars", - OsmosisPriceSource::GeometricTwap { + OsmosisPriceSourceUnchecked::GeometricTwap { pool_id: 89, window_size: 86400, downtime_detector: None, @@ -178,7 +189,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, @@ -187,7 +198,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()), @@ -228,12 +239,12 @@ 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(), "uatom", - OsmosisPriceSource::GeometricTwap { + OsmosisPriceSourceUnchecked::GeometricTwap { pool_id: 1, window_size: 86400, downtime_detector: None, @@ -242,7 +253,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, @@ -281,12 +292,12 @@ 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(), "ustatom", - OsmosisPriceSource::StakedGeometricTwap { + OsmosisPriceSourceUnchecked::StakedGeometricTwap { transitive_denom: "uatom".to_string(), pool_id: 803, window_size: 86400, @@ -313,14 +324,14 @@ 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" )) ); } #[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, @@ -329,7 +340,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()), @@ -338,7 +349,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, @@ -391,9 +402,355 @@ 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(); + + // 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); + + // 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 { + contract_addr: "pyth_contract_addr".to_string(), + price_feed_id: price_id, + max_staleness: 1800u64, + denom_decimals: 6u8, + }, + ); + + let price = Price { + price: 1021000, + conf: 50000, + expo: -4, + publish_time: publish_time as i64, + }; + 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, + 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, + }; + + // 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(), + "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(); + let mut deps = helpers::setup_test_with_pools(); let assets = vec![coin(1, "uatom"), coin(1, "uosmo")]; deps.querier.set_query_pool_response( @@ -433,7 +790,7 @@ fn querying_xyk_lp_price() { helpers::set_price_source( deps.as_mut(), "uatom", - OsmosisPriceSource::Fixed { + OsmosisPriceSourceUnchecked::Fixed { price: uatom_price, }, ); @@ -444,7 +801,7 @@ fn querying_xyk_lp_price() { helpers::set_price_source( deps.as_mut(), "umars", - OsmosisPriceSource::Fixed { + OsmosisPriceSourceUnchecked::Fixed { price: umars_price, }, ); @@ -454,7 +811,7 @@ fn querying_xyk_lp_price() { helpers::set_price_source( deps.as_mut(), "uatom_umars_lp", - OsmosisPriceSource::XykLiquidityToken { + OsmosisPriceSourceUnchecked::XykLiquidityToken { pool_id: 10003, }, ); @@ -505,27 +862,265 @@ 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(); + // 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", + ) + .unwrap(); + + let max_staleness = 30u64; + helpers::set_price_source( + deps.as_mut(), + "uatom", + OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), + price_feed_id: price_id, + max_staleness, + denom_decimals: 6u8, + }, + ); + + 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() + } + ); +} + +#[test] +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", + ) + .unwrap(); + + let max_staleness = 30u64; + helpers::set_price_source( + deps.as_mut(), + "uatom", + OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), + price_feed_id: price_id, + max_staleness, + denom_decimals: 6u8, + }, + ); + + 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_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", + ) + .unwrap(); + + let max_staleness = 30u64; + helpers::set_price_source( + deps.as_mut(), + "uatom", + OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), + price_feed_id: price_id, + max_staleness, + denom_decimals: 6u8, + }, + ); + + 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", - 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, }, ); @@ -534,7 +1129,7 @@ fn querying_all_prices() { 1, "uatom", "uosmo", - QuerySpotPriceResponse { + SpotPriceResponse { spot_price: Decimal::from_ratio(77777u128, 12345u128).to_string(), }, ); @@ -542,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/oracle/osmosis/tests/test_remove_price_source.rs b/contracts/oracle/osmosis/tests/test_remove_price_source.rs index 5233ca3c3..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; @@ -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,26 +29,26 @@ 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(), "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 f053e2636..86da9976e 100644 --- a/contracts/oracle/osmosis/tests/test_set_price_source.rs +++ b/contracts/oracle/osmosis/tests/test_set_price_source.rs @@ -1,20 +1,22 @@ -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; 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(), @@ -22,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(), }, }, @@ -33,7 +35,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(), @@ -41,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(), }, }, @@ -57,7 +59,7 @@ fn setting_price_source_fixed() { ); assert_eq!( res.price_source, - OsmosisPriceSource::Fixed { + OsmosisPriceSourceChecked::Fixed { price: Decimal::one() } ); @@ -65,7 +67,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(), @@ -73,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(), }, }, @@ -91,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(), }, }, @@ -110,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(), }, }, @@ -125,7 +127,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( @@ -134,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, }, }, @@ -189,7 +191,7 @@ fn setting_price_source_spot() { ); assert_eq!( res.price_source, - OsmosisPriceSource::Spot { + OsmosisPriceSourceChecked::Spot { pool_id: 89, } ); @@ -197,7 +199,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, @@ -210,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, @@ -285,7 +287,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( @@ -294,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, @@ -312,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 @@ -326,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 { @@ -347,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 { @@ -360,7 +362,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, @@ -373,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, @@ -448,7 +450,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( @@ -457,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, @@ -475,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 @@ -489,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 { @@ -510,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 { @@ -523,7 +525,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, @@ -537,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, @@ -612,7 +614,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( @@ -621,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, @@ -640,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, @@ -655,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, @@ -677,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, @@ -689,9 +691,207 @@ 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(); + let mut deps = helpers::setup_test_with_pools(); let mut set_price_source_xyk_lp = |denom: &str, pool_id: u64| { execute( @@ -700,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, }, }, @@ -737,34 +937,78 @@ fn setting_price_source_xyk_lp() { ); assert_eq!( res.price_source, - OsmosisPriceSource::XykLiquidityToken { + OsmosisPriceSourceChecked::XykLiquidityToken { pool_id: 89, } ); } #[test] -fn querying_price_source() { +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: OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "new_pyth_contract_addr".to_string(), + price_feed_id: PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(), + max_staleness: 30, + denom_decimals: 8, + }, + }, + ) + .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, + OsmosisPriceSourceChecked::Pyth { + contract_addr: Addr::unchecked("new_pyth_contract_addr"), + price_feed_id: PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3" + ) + .unwrap(), + max_staleness: 30, + denom_decimals: 8 + }, + ); +} + +#[test] +fn querying_price_source() { + let mut deps = helpers::setup_test_with_pools(); + 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, }, ); @@ -778,7 +1022,7 @@ fn querying_price_source() { ); assert_eq!( res.price_source, - OsmosisPriceSource::Spot { + OsmosisPriceSourceChecked::Spot { pool_id: 89, } ); @@ -798,13 +1042,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 } } @@ -823,13 +1067,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/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/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_oracles.rs b/integration-tests/tests/test_oracles.rs index b98a251d0..10ff04208 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::{ @@ -70,7 +71,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, }, }, @@ -142,7 +143,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, }, }, @@ -154,7 +155,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, }, }, @@ -166,7 +167,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, }, }, @@ -228,7 +229,7 @@ fn query_spot_price() { &oracle_addr, &ExecuteMsg::SetPriceSource { denom: "uatom".to_string(), - price_source: OsmosisPriceSource::Spot { + price_source: OsmosisPriceSourceUnchecked::Spot { pool_id, }, }, @@ -247,7 +248,7 @@ fn query_spot_price() { .unwrap(); assert_eq!( price_source.price_source, - (OsmosisPriceSource::Spot { + (OsmosisPriceSourceChecked::Spot { pool_id }) ); @@ -288,7 +289,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, }, }, @@ -330,7 +331,7 @@ fn incorrect_pool_for_spot() { &oracle_addr, &ExecuteMsg::SetPriceSource { denom: "umars".to_string(), - price_source: OsmosisPriceSource::Spot { + price_source: OsmosisPriceSourceUnchecked::Spot { pool_id, }, }, @@ -375,7 +376,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, }, }, @@ -401,7 +402,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, }, }, @@ -449,7 +450,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, }, }, @@ -508,7 +509,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 { @@ -531,7 +532,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 { @@ -592,7 +593,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, @@ -615,7 +616,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 @@ -678,7 +679,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, @@ -701,7 +702,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 @@ -768,7 +769,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, }, }, @@ -786,7 +787,7 @@ fn compare_spot_and_twap_price() { .unwrap(); assert_eq!( price_source.price_source, - OsmosisPriceSource::Spot { + OsmosisPriceSourceChecked::Spot { pool_id, } ); @@ -804,7 +805,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, @@ -824,7 +825,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 @@ -844,7 +845,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, @@ -864,7 +865,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 @@ -910,7 +911,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, }, }, @@ -972,7 +973,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/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..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::{ @@ -10,7 +13,7 @@ use osmosis_std::{ osmosis::{ downtimedetector::v1beta1::DowntimedetectorQuerier, gamm::{ - v1beta1::{PoolAsset, PoolParams, QueryPoolRequest}, + v1beta1::{PoolAsset, PoolParams}, v2::GammQuerier, }, twap::v1beta1::TwapQuerier, @@ -51,8 +54,11 @@ 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 = QueryPoolRequest { + let req: QueryRequest = PoolRequest { pool_id, } .into(); @@ -65,6 +71,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/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..be5d14cf9 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, }, }, @@ -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 @@ -452,6 +453,8 @@ pub struct MockEnvBuilder { safety_fund_denom: String, fee_collector_denom: String, slippage_tolerance: Decimal, + + pyth_contract_addr: String, } impl MockEnvBuilder { @@ -464,11 +467,14 @@ 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(), 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 +518,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); diff --git a/packages/testing/src/lib.rs b/packages/testing/src/lib.rs index f8ec93050..22b4befc7 100644 --- a/packages/testing/src/lib.rs +++ b/packages/testing/src/lib.rs @@ -10,7 +10,9 @@ mod mock_address_provider; mod mocks; 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 79894e8e4..9df685ab2 100644 --- a/packages/testing/src/mars_mock_querier.rs +++ b/packages/testing/src/mars_mock_querier.rs @@ -4,21 +4,28 @@ 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::{ downtimedetector::v1beta1::RecoveredSinceDowntimeOfLengthResponse, - gamm::v2::QuerySpotPriceResponse, + poolmanager::v1beta1::SpotPriceResponse, 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, + redemption_rate_querier::RedemptionRateQuerier, }; pub struct MarsMockQuerier { @@ -26,7 +33,9 @@ pub struct MarsMockQuerier { oracle_querier: OracleQuerier, incentives_querier: IncentivesQuerier, osmosis_querier: OsmosisQuerier, + pyth_querier: PythQuerier, redbank_querier: RedBankQuerier, + redemption_rate_querier: RedemptionRateQuerier, } impl Querier for MarsMockQuerier { @@ -52,7 +61,9 @@ impl MarsMockQuerier { oracle_querier: OracleQuerier::default(), incentives_querier: IncentivesQuerier::default(), osmosis_querier: OsmosisQuerier::default(), + pyth_querier: PythQuerier::default(), redbank_querier: RedBankQuerier::default(), + redemption_rate_querier: Default::default(), } } @@ -85,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, @@ -134,6 +145,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); } @@ -156,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 { @@ -186,11 +214,21 @@ 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); } + // 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/osmosis_querier.rs b/packages/testing/src/osmosis_querier.rs index c23109335..539ba769a 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, @@ -38,7 +35,7 @@ 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 = + 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/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/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/packages/types/src/oracle.rs b/packages/types/src/oracle.rs index d5b9d02cc..d0be5836c 100644 --- a/packages/types/src/oracle.rs +++ b/packages/types/src/oracle.rs @@ -31,6 +31,10 @@ pub enum ExecuteMsg { }, /// Manages admin role state UpdateOwner(OwnerUpdate), + /// Update contract config (only callable by owner) + UpdateConfig { + base_denom: Option, + }, } #[cw_serde] diff --git a/schemas/mars-address-provider/mars-address-provider.json b/schemas/mars-address-provider/mars-address-provider.json index e3c3cea06..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.1", + "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 373dde4cb..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.1", + "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 01b6a5527..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.1", + "contract_version": "1.1.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -44,7 +44,7 @@ "type": "string" }, "price_source": { - "$ref": "#/definitions/OsmosisPriceSource" + "$ref": "#/definitions/OsmosisPriceSource_for_String" } }, "additionalProperties": false @@ -86,6 +86,28 @@ } }, "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" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": { @@ -147,7 +169,42 @@ } } }, - "OsmosisPriceSource": { + "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_for_String": { "oneOf": [ { "description": "Returns a fixed value;", @@ -344,6 +401,90 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "pyth" + ], + "properties": { + "pyth": { + "type": "object", + "required": [ + "contract_addr", + "denom_decimals", + "max_staleness", + "price_feed_id" + ], + "properties": { + "contract_addr": { + "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: - 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 + }, + "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 We can't verify what denoms consist of the price feed. Be very careful when adding it !!!", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + } + } + } + }, + "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 } ] }, @@ -422,6 +563,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/schemas/mars-red-bank/mars-red-bank.json b/schemas/mars-red-bank/mars-red-bank.json index 4c7b02ce0..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.1", + "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 d76cde937..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.1", + "contract_version": "1.1.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts index a457d942b..6f775dea6 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts @@ -10,11 +10,14 @@ import { Coin, StdFee } from '@cosmjs/amino' import { InstantiateMsg, ExecuteMsg, - OsmosisPriceSource, + OsmosisPriceSourceForString, Decimal, Downtime, + Identifier, OwnerUpdate, DowntimeDetector, + GeometricTwap, + RedemptionRateForString, QueryMsg, ConfigResponse, PriceResponse, @@ -113,7 +116,7 @@ export interface MarsOracleOsmosisInterface extends MarsOracleOsmosisReadOnlyInt priceSource, }: { denom: string - priceSource: OsmosisPriceSource + priceSource: OsmosisPriceSourceForString }, fee?: number | StdFee | 'auto', memo?: string, @@ -135,6 +138,16 @@ export interface MarsOracleOsmosisInterface extends MarsOracleOsmosisReadOnlyInt memo?: string, _funds?: Coin[], ) => Promise + updateConfig: ( + { + baseDenom, + }: { + baseDenom?: 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 ( @@ -160,7 +174,7 @@ export class MarsOracleOsmosisClient priceSource, }: { denom: string - priceSource: OsmosisPriceSource + priceSource: OsmosisPriceSourceForString }, fee: number | StdFee | 'auto' = 'auto', memo?: string, @@ -220,4 +234,27 @@ export class MarsOracleOsmosisClient _funds, ) } + updateConfig = async ( + { + baseDenom, + }: { + baseDenom?: 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, + }, + }, + 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..9af54f39a 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.react-query.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.react-query.ts @@ -11,11 +11,14 @@ import { StdFee, Coin } from '@cosmjs/amino' import { InstantiateMsg, ExecuteMsg, - OsmosisPriceSource, + OsmosisPriceSourceForString, Decimal, Downtime, + Identifier, OwnerUpdate, DowntimeDetector, + GeometricTwap, + RedemptionRateForString, QueryMsg, ConfigResponse, PriceResponse, @@ -164,6 +167,29 @@ export function useMarsOracleOsmosisConfigQuery({ { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, ) } +export interface MarsOracleOsmosisUpdateConfigMutation { + client: MarsOracleOsmosisClient + msg: { + baseDenom?: 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 @@ -211,7 +237,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 3203abe77..d5f9b5cba 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts @@ -13,7 +13,7 @@ export type ExecuteMsg = | { set_price_source: { denom: string - price_source: OsmosisPriceSource + price_source: OsmosisPriceSourceForString } } | { @@ -24,7 +24,12 @@ export type ExecuteMsg = | { update_owner: OwnerUpdate } -export type OsmosisPriceSource = + | { + update_config: { + base_denom?: string | null + } + } +export type OsmosisPriceSourceForString = | { fixed: { price: Decimal @@ -68,6 +73,23 @@ export type OsmosisPriceSource = [k: string]: unknown } } + | { + pyth: { + contract_addr: string + denom_decimals: number + max_staleness: number + price_feed_id: Identifier + [k: string]: unknown + } + } + | { + lsd: { + geometric_twap: GeometricTwap + redemption_rate: RedemptionRateForString + transitive_denom: string + [k: string]: unknown + } + } export type Decimal = string export type Downtime = | 'duration30s' @@ -95,6 +117,7 @@ export type Downtime = | 'duration24h' | 'duration36h' | 'duration48h' +export type Identifier = string export type OwnerUpdate = | { propose_new_owner: { @@ -115,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: {}