diff --git a/Cargo.lock b/Cargo.lock index 274f61f..cea24fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,7 +83,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faa5be5b72abea167f87c868379ba3c2be356bfca9e6f474fd055fa0f7eeb4f2" dependencies = [ - "anchor-syn 0.28.0", + "anchor-syn", "anyhow", "proc-macro2", "quote", @@ -91,25 +91,13 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "anchor-attribute-access-control" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5f619f1d04f53621925ba8a2e633ba5a6081f2ae14758cbb67f38fd823e0a3e" -dependencies = [ - "anchor-syn 0.29.0", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "anchor-attribute-account" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f468970344c7c9f9d03b4da854fd7c54f21305059f53789d0045c1dd803f0018" dependencies = [ - "anchor-syn 0.28.0", + "anchor-syn", "anyhow", "bs58 0.5.0", "proc-macro2", @@ -118,150 +106,68 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "anchor-attribute-account" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f2a3e1df4685f18d12a943a9f2a7456305401af21a07c9fe076ef9ecd6e400" -dependencies = [ - "anchor-syn 0.29.0", - "bs58 0.5.0", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "anchor-attribute-constant" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59948e7f9ef8144c2aefb3f32a40c5fce2798baeec765ba038389e82301017ef" dependencies = [ - "anchor-syn 0.28.0", + "anchor-syn", "proc-macro2", "syn 1.0.109", ] -[[package]] -name = "anchor-attribute-constant" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9423945cb55627f0b30903288e78baf6f62c6c8ab28fb344b6b25f1ffee3dca7" -dependencies = [ - "anchor-syn 0.29.0", - "quote", - "syn 1.0.109", -] - [[package]] name = "anchor-attribute-error" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc753c9d1c7981cb8948cf7e162fb0f64558999c0413058e2d43df1df5448086" dependencies = [ - "anchor-syn 0.28.0", + "anchor-syn", "proc-macro2", "quote", "syn 1.0.109", ] -[[package]] -name = "anchor-attribute-error" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ed12720033cc3c3bf3cfa293349c2275cd5ab99936e33dd4bf283aaad3e241" -dependencies = [ - "anchor-syn 0.29.0", - "quote", - "syn 1.0.109", -] - [[package]] name = "anchor-attribute-event" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38b4e172ba1b52078f53fdc9f11e3dc0668ad27997838a0aad2d148afac8c97" dependencies = [ - "anchor-syn 0.28.0", + "anchor-syn", "anyhow", "proc-macro2", "quote", "syn 1.0.109", ] -[[package]] -name = "anchor-attribute-event" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eef4dc0371eba2d8c8b54794b0b0eb786a234a559b77593d6f80825b6d2c77a2" -dependencies = [ - "anchor-syn 0.29.0", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "anchor-attribute-program" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4eebd21543606ab61e2d83d9da37d24d3886a49f390f9c43a1964735e8c0f0d5" dependencies = [ - "anchor-syn 0.28.0", + "anchor-syn", "anyhow", "proc-macro2", "quote", "syn 1.0.109", ] -[[package]] -name = "anchor-attribute-program" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b18c4f191331e078d4a6a080954d1576241c29c56638783322a18d308ab27e4f" -dependencies = [ - "anchor-syn 0.29.0", - "quote", - "syn 1.0.109", -] - [[package]] name = "anchor-derive-accounts" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec4720d899b3686396cced9508f23dab420f1308344456ec78ef76f98fda42af" dependencies = [ - "anchor-syn 0.28.0", + "anchor-syn", "anyhow", "proc-macro2", "quote", "syn 1.0.109", ] -[[package]] -name = "anchor-derive-accounts" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de10d6e9620d3bcea56c56151cad83c5992f50d5960b3a9bebc4a50390ddc3c" -dependencies = [ - "anchor-syn 0.29.0", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "anchor-derive-serde" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4e2e5be518ec6053d90a2a7f26843dbee607583c779e6c8395951b9739bdfbe" -dependencies = [ - "anchor-syn 0.29.0", - "borsh-derive-internal 0.10.3", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "anchor-derive-space" version = "0.28.0" @@ -273,57 +179,20 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "anchor-derive-space" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ecc31d19fa54840e74b7a979d44bcea49d70459de846088a1d71e87ba53c419" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "anchor-lang" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d2d4b20100f1310a774aba3471ef268e5c4ba4d5c28c0bbe663c2658acbc414" dependencies = [ - "anchor-attribute-access-control 0.28.0", - "anchor-attribute-account 0.28.0", - "anchor-attribute-constant 0.28.0", - "anchor-attribute-error 0.28.0", - "anchor-attribute-event 0.28.0", - "anchor-attribute-program 0.28.0", - "anchor-derive-accounts 0.28.0", - "anchor-derive-space 0.28.0", - "arrayref", - "base64 0.13.1", - "bincode", - "borsh 0.10.3", - "bytemuck", - "getrandom 0.2.10", - "solana-program", - "thiserror", -] - -[[package]] -name = "anchor-lang" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35da4785497388af0553586d55ebdc08054a8b1724720ef2749d313494f2b8ad" -dependencies = [ - "anchor-attribute-access-control 0.29.0", - "anchor-attribute-account 0.29.0", - "anchor-attribute-constant 0.29.0", - "anchor-attribute-error 0.29.0", - "anchor-attribute-event 0.29.0", - "anchor-attribute-program 0.29.0", - "anchor-derive-accounts 0.29.0", - "anchor-derive-serde", - "anchor-derive-space 0.29.0", - "anchor-syn 0.29.0", + "anchor-attribute-access-control", + "anchor-attribute-account", + "anchor-attribute-constant", + "anchor-attribute-error", + "anchor-attribute-event", + "anchor-attribute-program", + "anchor-derive-accounts", + "anchor-derive-space", "arrayref", "base64 0.13.1", "bincode", @@ -340,7 +209,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78f860599da1c2354e7234c768783049eb42e2f54509ecfc942d2e0076a2da7b" dependencies = [ - "anchor-lang 0.28.0", + "anchor-lang", "mpl-token-metadata 1.13.2", "serum_dex", "solana-program", @@ -367,24 +236,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "anchor-syn" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9101b84702fed2ea57bd22992f75065da5648017135b844283a2f6d74f27825" -dependencies = [ - "anyhow", - "bs58 0.5.0", - "heck 0.3.3", - "proc-macro2", - "quote", - "serde", - "serde_json", - "sha2 0.10.8", - "syn 1.0.109", - "thiserror", -] - [[package]] name = "anyhow" version = "1.0.75" @@ -432,7 +283,7 @@ dependencies = [ "derivative", "digest 0.10.7", "itertools 0.10.5", - "num-bigint", + "num-bigint 0.4.4", "num-traits", "paste", "rustc_version", @@ -455,7 +306,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" dependencies = [ - "num-bigint", + "num-bigint 0.4.4", "num-traits", "proc-macro2", "quote", @@ -484,7 +335,7 @@ dependencies = [ "ark-serialize-derive", "ark-std", "digest 0.10.7", - "num-bigint", + "num-bigint 0.4.4", ] [[package]] @@ -659,7 +510,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive 0.10.3", - "hashbrown 0.12.3", + "hashbrown 0.13.2", ] [[package]] @@ -802,7 +653,7 @@ checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.60", ] [[package]] @@ -989,7 +840,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.38", + "syn 2.0.60", ] [[package]] @@ -1000,7 +851,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core", "quote", - "syn 2.0.38", + "syn 2.0.60", ] [[package]] @@ -1135,6 +986,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "fast-math" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2465292146cdfc2011350fe3b1c616ac83cf0faeedb33463ba1c332ed8948d66" +dependencies = [ + "ieee754", +] + [[package]] name = "feature-probe" version = "0.1.1" @@ -1308,6 +1168,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "ieee754" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9007da9cacbd3e6343da136e98b0d2df013f553d35bdec8b518f07bea768e19c" + [[package]] name = "im" version = "15.1.0" @@ -1386,7 +1252,7 @@ dependencies = [ name = "jup-perp-itf" version = "0.1.0" dependencies = [ - "anchor-lang 0.28.0", + "anchor-lang", "anchor-spl", "solana-program", ] @@ -1410,7 +1276,7 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" name = "lb-clmm-itf" version = "0.1.0" dependencies = [ - "anchor-lang 0.28.0", + "anchor-lang", "bytemuck", "decimal-wad", ] @@ -1420,7 +1286,7 @@ name = "lb_clmm" version = "0.5.0" source = "git+ssh://git@github.com/hubbleprotocol/lb-clmm.git#0eabb9945419a1f0f51837823a3505864c7636d3" dependencies = [ - "anchor-lang 0.28.0", + "anchor-lang", "anchor-spl", "bytemuck", "mpl-token-metadata 3.2.3", @@ -1620,17 +1486,42 @@ dependencies = [ "spl-token-2022 0.6.1", ] +[[package]] +name = "num" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" +dependencies = [ + "num-bigint 0.2.6", + "num-complex 0.2.4", + "num-integer", + "num-iter", + "num-rational 0.2.4", + "num-traits", +] + [[package]] name = "num" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" dependencies = [ - "num-bigint", - "num-complex", + "num-bigint 0.4.4", + "num-complex 0.4.4", "num-integer", "num-iter", - "num-rational", + "num-rational 0.4.1", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg", + "num-integer", "num-traits", ] @@ -1645,6 +1536,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg", + "num-traits", +] + [[package]] name = "num-complex" version = "0.4.4" @@ -1673,7 +1574,7 @@ checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.60", ] [[package]] @@ -1697,6 +1598,18 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg", + "num-bigint 0.2.6", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.1" @@ -1704,7 +1617,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ "autocfg", - "num-bigint", + "num-bigint 0.4.4", "num-integer", "num-traits", ] @@ -1766,7 +1679,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.60", ] [[package]] @@ -1778,7 +1691,7 @@ dependencies = [ "proc-macro-crate 2.0.0", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.60", ] [[package]] @@ -1894,9 +1807,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] @@ -1952,6 +1865,37 @@ dependencies = [ "thiserror", ] +[[package]] +name = "pyth-solana-receiver-sdk" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "937c8595148fb2a9a90439daf6a371a5b3c9fcd9b636f26d36ae31d6846d4339" +dependencies = [ + "anchor-lang", + "hex", + "pythnet-sdk", + "solana-program", +] + +[[package]] +name = "pythnet-sdk" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f246dc1ea2966f811d894590014f9163942b33255fc7560fe629726a721aad1" +dependencies = [ + "bincode", + "borsh 0.10.3", + "bytemuck", + "byteorder", + "fast-math", + "hex", + "rustc_version", + "serde", + "sha3 0.10.8", + "slow_primes", + "thiserror", +] + [[package]] name = "qstring" version = "0.7.2" @@ -1972,9 +1916,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -2070,7 +2014,7 @@ name = "raydium-amm-v3" version = "0.1.0" source = "git+https://github.com/raydium-io/raydium-clmm#cea4d49ef5acc643cb04fb4db96af6d73a5f0a1d" dependencies = [ - "anchor-lang 0.28.0", + "anchor-lang", "anchor-spl", "arrayref", "bytemuck", @@ -2306,7 +2250,7 @@ dependencies = [ name = "scope" version = "0.1.0" dependencies = [ - "anchor-lang 0.28.0", + "anchor-lang", "anchor-spl", "arrayref", "bytemuck", @@ -2317,11 +2261,13 @@ dependencies = [ "lb-clmm-itf", "num_enum 0.7.1", "pyth-sdk-solana", + "pyth-solana-receiver-sdk", "raydium-amm-v3", "rust_decimal", "serde", "sha2 0.10.8", "solana-program", + "static_assertions", "strum", "switchboard-program", "whirlpool", @@ -2332,7 +2278,7 @@ dependencies = [ name = "scope-types" version = "1.0.0" dependencies = [ - "anchor-lang 0.28.0", + "anchor-lang", "bytemuck", "cfg-if", "num_enum 0.7.1", @@ -2342,9 +2288,9 @@ dependencies = [ [[package]] name = "scope-types" version = "1.0.0" -source = "git+https://github.com/hubbleprotocol/scope#6ce6ff1e7d0832a425835b4ac23f4c0a1e7fbcd7" +source = "git+https://github.com/Kamino-Finance/scope#d5801f596ce18a167afde2f8e68f814ec78d5165" dependencies = [ - "anchor-lang 0.28.0", + "anchor-lang", "bytemuck", "cfg-if", "num_enum 0.5.11", @@ -2371,9 +2317,9 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.190" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" +checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" dependencies = [ "serde_derive", ] @@ -2389,13 +2335,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.190" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" +checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.60", ] [[package]] @@ -2411,9 +2357,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ "itoa", "ryu", @@ -2439,7 +2385,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.60", ] [[package]] @@ -2568,11 +2514,20 @@ dependencies = [ "typenum", ] +[[package]] +name = "slow_primes" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58267dd2fbaa6dceecba9e3e106d2d90a2b02497c0e8b01b8759beccf5113938" +dependencies = [ + "num 0.2.1", +] + [[package]] name = "smallvec" -version = "1.11.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "solana-frozen-abi" @@ -2616,7 +2571,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.38", + "syn 2.0.60", ] [[package]] @@ -2662,7 +2617,7 @@ dependencies = [ "libsecp256k1", "log", "memoffset", - "num-bigint", + "num-bigint 0.4.4", "num-derive 0.3.3", "num-traits", "parking_lot", @@ -2748,7 +2703,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.38", + "syn 2.0.60", ] [[package]] @@ -2837,7 +2792,7 @@ checksum = "fadbefec4f3c678215ca72bd71862697bb06b41fd77c0088902dd3203354387b" dependencies = [ "quote", "spl-discriminator-syn", - "syn 2.0.38", + "syn 2.0.60", ] [[package]] @@ -2849,7 +2804,7 @@ dependencies = [ "proc-macro2", "quote", "sha2 0.10.8", - "syn 2.0.38", + "syn 2.0.60", "thiserror", ] @@ -2906,7 +2861,7 @@ dependencies = [ "proc-macro2", "quote", "sha2 0.10.8", - "syn 2.0.38", + "syn 2.0.60", ] [[package]] @@ -3090,7 +3045,7 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" name = "switchboard-itf" version = "0.1.0" dependencies = [ - "anchor-lang 0.29.0", + "anchor-lang", "bytemuck", "rust_decimal", ] @@ -3153,9 +3108,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.38" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", @@ -3194,7 +3149,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.60", ] [[package]] @@ -3393,7 +3348,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.60", "wasm-bindgen-shared", ] @@ -3415,7 +3370,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.60", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3441,7 +3396,7 @@ name = "whirlpool" version = "0.2.0" source = "git+https://github.com/hubbleprotocol/whirlpools?branch=anchor/0.28.0#c77e1e4c87dde54bfdaaaae3b806c9b660cefdc3" dependencies = [ - "anchor-lang 0.28.0", + "anchor-lang", "anchor-spl", "borsh 0.9.3", "bytemuck", @@ -3571,21 +3526,21 @@ dependencies = [ [[package]] name = "yvaults" version = "0.1.0" -source = "git+ssh://git@github.com/hubbleprotocol/yvaults.git#1096f49c5c9230fcf91d07ce9c5bc52411e88c1b" +source = "git+ssh://git@github.com/hubbleprotocol/yvaults.git?branch=scope-public-compat#096fe93b90007118980085ba3b6707f252ef1136" dependencies = [ - "anchor-lang 0.28.0", + "anchor-lang", "anchor-spl", "bytemuck", "decimal-wad", "lb_clmm", "mpl-token-metadata 1.13.2", - "num", + "num 0.4.1", "num-derive 0.4.1", "num-traits", "num_enum 0.7.1", "raydium-amm-v3", "rust_decimal", - "scope-types 1.0.0 (git+https://github.com/hubbleprotocol/scope)", + "scope-types 1.0.0 (git+https://github.com/Kamino-Finance/scope)", "sha2 0.10.8", "solana-security-txt", "spl-token 3.5.0", @@ -3616,7 +3571,7 @@ checksum = "020f3dfe25dfc38dfea49ce62d5d45ecdd7f0d8a724fa63eb36b6eba4ec76806" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.60", ] [[package]] @@ -3636,5 +3591,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.60", ] diff --git a/configs/mainnet/hubble.json b/configs/mainnet/hubble.json index a666da4..3076253 100644 --- a/configs/mainnet/hubble.json +++ b/configs/mainnet/hubble.json @@ -99,8 +99,8 @@ }, "24": { "label": "MNDE/USD", - "oracle_mapping": "4pDjBPCEHamdZwUab5hcqn4VYUmKx84CCCVT6NYXJGTv", - "oracle_type": "SwitchboardV2", + "oracle_mapping": "4dusJxxxiYrMTLGYS6cCAyu3gPn2xXLBjS7orMToZHi1", + "oracle_type": "Pyth", "twap_enabled": true, "max_age": 200 }, @@ -355,9 +355,9 @@ }, "83": { "label": "TWAP MNDE/USD", - "oracle_type": "ScopeTwap", - "twap_source": 24, - "max_age": 220 + "oracle_type": "PythEMA", + "oracle_mapping": "4dusJxxxiYrMTLGYS6cCAyu3gPn2xXLBjS7orMToZHi1", + "max_age": 200 }, "84": { "label": "HNT/USD", @@ -434,19 +434,6 @@ "twap_source": 94, "max_age": 220 }, - "96": { - "label": "COCO/USD", - "oracle_mapping": "iYVRYxRKVMUEt5EtUUaPPQyBHzc8cPLndFbQdzqVjzL", - "oracle_type": "SwitchboardV2", - "twap_enabled": true, - "max_age": 200 - }, - "97": { - "label": "TWAP COCO/USD", - "oracle_type": "ScopeTwap", - "twap_source": 96, - "max_age": 220 - }, "100": { "label": "CHAI/USD", "oracle_mapping": "FKjV7Udz5jgqxnf2ZdNZA9MmADM3P4NYjtGDn3wNpAuK", @@ -592,13 +579,6 @@ "oracle_mapping": "8szGkuLTAux9XMgZ2vtY39jVSowEcpBfFfD8hXSEqdGC", "oracle_type": "MsolStake" }, - "123": { - "label": "xSTEP/USD", - "oracle_mapping": "G6dMsEkMdgjX6Lsd1hMtgq79JivHpSsraDr9MD4XNiAt", - "oracle_type": "SwitchboardV2", - "max_age": 200, - "twap_enabled": true - }, "124": { "label": "Fetched JLP/USD", "oracle_mapping": "5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq", @@ -666,12 +646,6 @@ "oracle_type": "PythEMA", "max_age": 200 }, - "135": { - "label": "TWAP xSTEP", - "oracle_type": "ScopeTwap", - "twap_source": 123, - "max_age": 250 - }, "136": { "label": "Computed JLP/USD", "oracle_mapping": "5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq", @@ -685,42 +659,6 @@ "max_age": 200, "twap_source": 136 }, - "140": { - "label": "HBB per 5XL..PFM kHBB-USDH", - "oracle_mapping": "5XLxZ2wbJeD9ub2ii2zVWvPjnjSHXGatQ399PAA2yPFM", - "oracle_type": "KTokenToTokenB", - "max_age": 650 - }, - "141": { - "label": "HBB per 2G8x..EmFe kHBB-USDH", - "oracle_mapping": "2G8xgnQjAJnLQBX2nVh42TWqPrMmVm3q5aPJAkArEmFe", - "oracle_type": "KTokenToTokenB", - "max_age": 650 - }, - "142": { - "label": "Orca SOL/USDC", - "oracle_mapping": "7qbRF6YsyGuLUVs6Y1q64bdVrfe4ZcUUz1JRdoVNUJnm", - "oracle_type": "OrcaWhirlpoolAtoB", - "max_age": 100 - }, - "143": { - "label": "Orca JITOSOL/USDC", - "oracle_mapping": "GgAqL1Lfs3zYZK6Gea9Bey5jmPbK8c4dmBbe5gPeYQEz", - "oracle_type": "OrcaWhirlpoolBtoA", - "max_age": 100 - }, - "144": { - "label": "Raydium SOL/USDC", - "oracle_mapping": "2QdhepnKRTLjjSqPL1PtKNwqrUkoLee5Gqs8bvZhRdMv", - "oracle_type": "RaydiumAmmV3AtoB", - "max_age": 100 - }, - "145": { - "label": "Raydium MSOL/SOL", - "oracle_mapping": "8EzbUfvcRT1Q6RL462ekGkgqbxsPmwC5FMLQZhSPMjJ3", - "oracle_type": "RaydiumAmmV3BtoA", - "max_age": 100 - }, "146": { "label": "JTO/USD", "oracle_mapping": "D8UUgr8a3aR3yUeHLu7v8FWK7E8Y5sSU7qrYBXUJXBQ5", @@ -733,19 +671,6 @@ "oracle_mapping": "D8UUgr8a3aR3yUeHLu7v8FWK7E8Y5sSU7qrYBXUJXBQ5", "max_age": 120 }, - "148": { - "label": "Orca SHDW/jitoSOL", - "oracle_mapping": "HJ9xK7sT9ujrAxBbsd2BMaS6c6SF9KqUsLUtu3h7frJe", - "oracle_type": "OrcaWhirlpoolAtoB", - "twap_enabled": true, - "max_age": 100 - }, - "149": { - "label": "TWAP Orca SHDW/jitoSOL", - "oracle_type": "ScopeTwap", - "twap_source": 148, - "max_age": 120 - }, "150": { "label": "SHDW per GhN...GaY kSHDW-JITOSOL", "oracle_mapping": "GhNJyhF7YpVH4FE8iMXo71rUU1uZkJgaTTPLu1mkkGaY", @@ -1243,13 +1168,6 @@ "twap_enabled": true, "max_age": 100 }, - "227": { - "label": "Orca MASH/SOL", - "oracle_mapping": "3ucQuo7HgbzsKSPK4LYzkFrkxtqArMwqMUr2wBzbYNem", - "oracle_type": "OrcaWhirlpoolBtoA", - "twap_enabled": true, - "max_age": 100 - }, "228": { "label": "Orca RKT/SOL", "oracle_mapping": "DEzY7sLa3aCmXbqZ71j1mSXirGP8HNEir5YGx1R8Sgnq", @@ -1309,12 +1227,6 @@ "twap_source": 226, "max_age": 120 }, - "237": { - "label": "TWAP Orca MASH/SOL", - "oracle_type": "ScopeTwap", - "twap_source": 227, - "max_age": 120 - }, "238": { "label": "TWAP Orca RKT/SOL", "oracle_type": "ScopeTwap", @@ -2045,12 +1957,6 @@ "twap_source": 349, "max_age": 120 }, - "351": { - "label": "kPAWN-SOL_Orca/USD", - "oracle_mapping": "GHMbbitxf1FeaQz76wXmj8jhHDJDXn3pSjNVt1E9Mb8H", - "oracle_type": "KToken", - "max_age": 250 - }, "352": { "label": "Switchboard USDC/USD", "oracle_type": "SwitchboardV2", @@ -2120,13 +2026,246 @@ "label": "Scope JLP/USD", "oracle_mapping": "5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq", "oracle_type": "JupiterLpScope", - "max_age": 255, + "max_age": 200, "twap_enabled": true }, "363": { "label": "TWAP Scope JLP/USD", "oracle_type": "ScopeTwap", "twap_source": 362, - "max_age": 260 + "max_age": 200 + }, + "364": { + "label": "Orca SHARK/SOL", + "oracle_mapping": "A9iU4aWr8E1sT2JTrQe3akPDPBDNpeQNfCGcTp7zqyH5", + "oracle_type": "OrcaWhirlpoolAtoB", + "max_age": 300, + "twap_enabled": true + }, + "365": { + "label": "TWAP Orca SHARK/SOL", + "oracle_type": "ScopeTwap", + "twap_source": 364, + "max_age": 360 + }, + "366": { + "label": "Orca PRCL/JITOSOL", + "oracle_mapping": "BWGP8CqKgg1rV6kYNFnbGbSYXxnYah4BQg1Sw1Nqy7hm", + "oracle_type": "OrcaWhirlpoolAtoB", + "max_age": 300, + "twap_enabled": true + }, + "367": { + "label": "TWAP Orca PRCL/JITOSOL", + "oracle_type": "ScopeTwap", + "twap_source": 366, + "max_age": 360 + }, + "368": { + "label": "Orca ZEUS/SOL", + "oracle_mapping": "Dsxt7wUigQU5puH9nHjmfwnUenMycAgsFtrkfggLh8P1", + "oracle_type": "OrcaWhirlpoolBtoA", + "max_age": 300, + "twap_enabled": true + }, + "369": { + "label": "TWAP Orca ZEUS/SOL", + "oracle_type": "ScopeTwap", + "twap_source": 368, + "max_age": 360 + }, + "370": { + "label": "Orca MEW/SOL", + "oracle_mapping": "6b3pGVYwAemYXuCQCH2qQee3zyp3nzeqgpoyxUXSCQbc", + "oracle_type": "OrcaWhirlpoolAtoB", + "max_age": 300, + "twap_enabled": true + }, + "371": { + "label": "TWAP Orca MEW/SOL", + "oracle_type": "ScopeTwap", + "twap_source": 370, + "max_age": 360 + }, + "372": { + "label": "Orca PUPS/SOL", + "oracle_mapping": "7mCZ6guS9V3qaHhBYyXqKgaR6MPxKsPBPW6qse3SWsuX", + "oracle_type": "OrcaWhirlpoolAtoB", + "max_age": 300, + "twap_enabled": true + }, + "373": { + "label": "TWAP Orca PUPS/SOL", + "oracle_type": "ScopeTwap", + "twap_source": 372, + "max_age": 360 + }, + "374": { + "label": "Orca BODEN/SOL", + "oracle_mapping": "G2Rm8fZ8ZgB8rgWB4Hbvf2FgmsypgdSyXujxHUphzqPT", + "oracle_type": "OrcaWhirlpoolBtoA", + "max_age": 300, + "twap_enabled": true + }, + "375": { + "label": "TWAP Orca BODEN/SOL", + "oracle_type": "ScopeTwap", + "twap_source": 374, + "max_age": 360 + }, + "376": { + "label": "Orca KMNO/SOL", + "oracle_mapping": "7t6yo3ii96aEqm11iHsZpG6M4aiX397DYP9BFeizfa7a", + "oracle_type": "OrcaWhirlpoolAtoB", + "max_age": 300, + "twap_enabled": true + }, + "377": { + "label": "TWAP Orca KMNO/SOL", + "oracle_type": "ScopeTwap", + "twap_source": 376, + "max_age": 360 + }, + "378": { + "label": "Orca JupSOL/SOL", + "oracle_mapping": "DtYKbQELgMZ3ihFUrCcCs9gy4djcUuhwgR7UpxVpP2Tg", + "oracle_type": "OrcaWhirlpoolBtoA", + "max_age": 300, + "twap_enabled": true + }, + "379": { + "label": "TWAP Orca JupSOL/SOL", + "oracle_type": "ScopeTwap", + "twap_source": 378, + "max_age": 360 + }, + "380": { + "label": "Raydium BORG/bSOL", + "oracle_mapping": "C8tx8FLnohFcoH4vP2cFBcsSv1paUQXV1uRUVM6VYKxw", + "oracle_type": "RaydiumAmmV3BtoA", + "max_age": 300, + "twap_enabled": true + }, + "381": { + "label": "TWAP Raydium BORG/bSOL", + "oracle_type": "ScopeTwap", + "twap_source": 380, + "max_age": 360 + }, + "382": { + "label": "HSOL/SOL", + "oracle_type": "SplStake", + "oracle_mapping": "3wK2g8ZdzAH8FJ7PKr2RcvGh7V9VYson5hrVsJM5Lmws", + "max_age": 120 + }, + "383": { + "label": "JupSOL/SOL", + "oracle_type": "SplStake", + "oracle_mapping": "8VpRhuxa7sUUepdY3kQiTmX9rS5vx4WgaXiAnXq4KCtr", + "max_age": 120 + }, + "384": { + "label": "CompassSOL/SOL", + "oracle_type": "SplStake", + "oracle_mapping": "AwDeTcW6BovNYR34Df1TPm4bFwswa4CJY4YPye2LXtPS", + "max_age": 120 + }, + "385": { + "label": "PicoSOL/SOL", + "oracle_type": "SplStake", + "oracle_mapping": "8Dv3hNYcEWEaa4qVx9BTN1Wfvtha1z8cWDUXb7KVACVe", + "max_age": 120 + }, + "387": { + "label": "BONKSOL/SOL", + "oracle_type": "SplStake", + "oracle_mapping": "ArAQfbzsdotoKB5jJcZa3ajQrrPcWr2YQoDAEAiFxJAC", + "max_age": 120 + }, + "388": { + "label": "vSOL/SOL", + "oracle_type": "SplStake", + "oracle_mapping": "Fu9BYC6tWBo1KMKaP3CFoKfRhqv9akmy3DuYwnCyWiyC", + "max_age": 120 + }, + "389": { + "label": "Orca GME/SOL", + "oracle_mapping": "2qKjGUBdgLcGVt1JbjLfXtphPQNkq4ujd6PyrTBWkeJ5", + "oracle_type": "OrcaWhirlpoolBtoA", + "twap_enabled": true, + "max_age": 120 + }, + "390": { + "label": "TWAP Orca GME/SOL", + "oracle_type": "ScopeTwap", + "twap_source": 389, + "max_age": 120 + }, + "391": { + "label": "Orca DRIFT/JITOSOL", + "oracle_mapping": "Mze7Csr8pew6HqnjZFTfsfsJtrvdwHErnvcLZjJdMa1", + "oracle_type": "OrcaWhirlpoolAtoB", + "twap_enabled": true, + "max_age": 120 + }, + "392": { + "label": "TWAP Orca DRIFT/JITOSOL", + "oracle_type": "ScopeTwap", + "twap_source": 391, + "max_age": 120 + }, + "393": { + "label": "JucySOL/SOL", + "oracle_type": "SplStake", + "oracle_mapping": "AZGSr2fUyKkPLMhAW6WUEKEsQiRMAFKf8Fjnt4MFFaGv", + "max_age": 120 + }, + "394": { + "label": "Orca SHDW/SOL", + "oracle_mapping": "6jwmmjnx3mDbA6QauSZ7DY8Z1B8wZncxXM1tJd2unpuS", + "oracle_type": "OrcaWhirlpoolAtoB", + "twap_enabled": true, + "max_age": 100 + }, + "395": { + "label": "TWAP Orca SHDW/SOL", + "oracle_type": "ScopeTwap", + "twap_source": 394, + "max_age": 120 + }, + "396": { + "label": "Pyth Pull BTC/USD", + "oracle_mapping": "4cSM2e6rvbGQUFiJbqytoVMi5GgghSMr8LwVrT9VPSPo", + "oracle_type": "PythPullBased", + "ref_price": 2, + "max_age": 175 + }, + "397": { + "label": "Pyth Pull USDC/USD", + "oracle_mapping": "Dpw1EAVrSB1ibxiDQyTAW6Zip3J4Btk2x4SgApQCeFbX", + "oracle_type": "PythPullBased", + "ref_price": 20, + "max_age": 175 + }, + "398": { + "label": "Pyth Pull USDT/USD", + "oracle_mapping": "HT2PLQBcG5EiCcNSaMHAjSgd9F98ecpATbk4Sk5oYuM", + "oracle_type": "PythPullBased", + "ref_price": 22, + "max_age": 175 + }, + "399": { + "label": "Pyth Pull SOL/USD", + "oracle_mapping": "7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE", + "oracle_type": "PythPullBased", + "ref_price": 0, + "max_age": 175 + }, + "400": { + "label": "Pyth Pull ETH/USD", + "oracle_mapping": "42amVS4KgzR9rA28tkVYqVXjq9Qa8dcZQMbH5EYFX6XC", + "oracle_type": "PythPullBased", + "ref_price": 1, + "max_age": 175 } } \ No newline at end of file diff --git a/programs/scope-types/Cargo.toml b/programs/scope-types/Cargo.toml index 5f44987..ef627d2 100644 --- a/programs/scope-types/Cargo.toml +++ b/programs/scope-types/Cargo.toml @@ -18,6 +18,7 @@ crate-type = ["cdylib", "lib"] devnet = [] localnet = [] mainnet = [] +staging = [] [dependencies] anchor-lang = ">= 0.28.0" diff --git a/programs/scope-types/build.rs b/programs/scope-types/build.rs index 2552a40..684fdf4 100644 --- a/programs/scope-types/build.rs +++ b/programs/scope-types/build.rs @@ -15,6 +15,7 @@ fn main() { println!("cargo:rerun-if-env-changed=CLUSTER"); // Set feature according to current cluster match cluster.as_str() { + "staging" => println!("cargo:rustc-cfg=feature=\"staging\""), "localnet" => println!("cargo:rustc-cfg=feature=\"localnet\""), "devnet" => { println!("cargo:rustc-cfg=feature=\"devnet\""); diff --git a/programs/scope-types/src/lib.rs b/programs/scope-types/src/lib.rs index 80b5f2c..14c3004 100644 --- a/programs/scope-types/src/lib.rs +++ b/programs/scope-types/src/lib.rs @@ -110,12 +110,12 @@ pub struct OracleMappings { } impl OracleMappings { - pub fn is_twap_enabled(&self, token: usize) -> bool { - self.twap_enabled[token] > 0 + pub fn is_twap_enabled(&self, entry_id: usize) -> bool { + self.twap_enabled[entry_id] > 0 } - pub fn get_twap_source(&self, token: usize) -> usize { - usize::from(self.twap_source[token]) + pub fn get_twap_source(&self, entry_id: usize) -> usize { + usize::from(self.twap_source[entry_id]) } } diff --git a/programs/scope-types/src/program_id.rs b/programs/scope-types/src/program_id.rs index 4f7fc69..412a7ed 100644 --- a/programs/scope-types/src/program_id.rs +++ b/programs/scope-types/src/program_id.rs @@ -13,15 +13,30 @@ compile_error!("'localnet' and 'devnet' features are mutually exclusive"); #[cfg(all(feature = "mainnet", feature = "skip_price_validation"))] compile_error!("'mainnet' and 'skip_price_validation' features are mutually exclusive"); +#[cfg(all(feature = "mainnet", feature = "staging"))] +compile_error!("'mainnet' and 'staging' features are mutually exclusive"); + +#[cfg(all(feature = "localnet", feature = "staging"))] +compile_error!("'localnet' and 'staging' features are mutually exclusive"); + +#[cfg(all(feature = "devnet", feature = "staging"))] +compile_error!("'devnet' and 'staging' features are mutually exclusive"); + +#[cfg(all(feature = "skip_price_validation", feature = "staging"))] +compile_error!("'skip_price_validation' and 'staging' features are mutually exclusive"); + cfg_if::cfg_if! { if #[cfg(feature = "mainnet")] { pub const PROGRAM_ID:Pubkey = pubkey!("HFn8GnPADiny6XqUoWE8uRPPxb29ikn4yTuPa9MF2fWJ"); } + else if #[cfg(feature = "staging")] { + pub const PROGRAM_ID:Pubkey = pubkey!("scpStzYvKzE7DHwsGMP5XLhcMTuLr3feoiC9mJ3yHr5"); + } else if #[cfg(feature = "localnet")] { pub const PROGRAM_ID:Pubkey = pubkey!("2fU6YqiA2aj9Ct1tDagA8Tng7otgxHM5KwgnsUWsMFxM"); } else if #[cfg(feature = "devnet")] { pub const PROGRAM_ID:Pubkey = pubkey!("3Vw8Ngkh1MVJTPHthmUbmU2XKtFEkjYvJzMqrv2rh9yX"); } else { - compile_error!("At least one of 'mainnet', 'localnet' or 'devnet' feature need to be set"); + compile_error!("At least one of 'mainnet', 'staging', 'localnet' or 'devnet' feature need to be set"); } } diff --git a/programs/scope/Cargo.toml b/programs/scope/Cargo.toml index f1bcecf..b51b404 100644 --- a/programs/scope/Cargo.toml +++ b/programs/scope/Cargo.toml @@ -25,12 +25,13 @@ yvaults = ["dep:yvaults"] devnet = ["skip_price_validation"] localnet = [] mainnet = [] +staging = [] serde = ["dep:serde"] [dependencies] anchor-lang = "0.28.0" anchor-spl = "0.28.0" -solana-program = "~1.16.18" +solana-program = ">1.16.18" bytemuck = { version = "1.4.0", features = ["min_const_generics", "derive"] } num_enum = "0.7.0" cfg-if = "1.0.0" @@ -44,7 +45,7 @@ arrayref = "0.3.6" decimal-wad = "0.1.7" rust_decimal = "1.18.0" # Comment out the line below if you do not have access to the yvaults repo -yvaults = { git = "ssh://git@github.com/hubbleprotocol/yvaults.git", features = [ +yvaults = { git = "ssh://git@github.com/hubbleprotocol/yvaults.git", branch = "scope-public-compat", features = [ "no-entrypoint", "cpi", "mainnet", @@ -63,4 +64,6 @@ raydium-amm-v3 = { git = "https://github.com/raydium-io/raydium-clmm", features jup-perp-itf = { path = "../jup-perp-itf", features = ["cpi"] } lb-clmm-itf = { path = "../lb-clmm-itf", features = ["no-entrypoint"] } intbits = "0.2.0" +pyth-solana-receiver-sdk = "0.1.0" +static_assertions = "1.1.0" diff --git a/programs/scope/build.rs b/programs/scope/build.rs index 2552a40..684fdf4 100644 --- a/programs/scope/build.rs +++ b/programs/scope/build.rs @@ -15,6 +15,7 @@ fn main() { println!("cargo:rerun-if-env-changed=CLUSTER"); // Set feature according to current cluster match cluster.as_str() { + "staging" => println!("cargo:rustc-cfg=feature=\"staging\""), "localnet" => println!("cargo:rustc-cfg=feature=\"localnet\""), "devnet" => { println!("cargo:rustc-cfg=feature=\"devnet\""); diff --git a/programs/scope/src/errors.rs b/programs/scope/src/errors.rs index c4b70fd..15d8151 100644 --- a/programs/scope/src/errors.rs +++ b/programs/scope/src/errors.rs @@ -84,6 +84,15 @@ pub enum ScopeError { #[msg("The stake pool fee is higher than the maximum allowed")] StakeFeeTooHigh, + + #[msg("Cannot get a valid price for the tokens composing the Ktoken")] + KTokenUnderlyingPriceNotValid, + + #[msg("Error while computing the Ktoken pool holdings")] + KTokenHoldingsCalculationError, + + #[msg("Cannot resize the account we only allow it to grow in size")] + CannotResizeAccount, } impl From> for ScopeError diff --git a/programs/scope/src/handlers/handler_extend_mapping.rs b/programs/scope/src/handlers/handler_extend_mapping.rs new file mode 100644 index 0000000..7504f13 --- /dev/null +++ b/programs/scope/src/handlers/handler_extend_mapping.rs @@ -0,0 +1,62 @@ +use crate::ScopeError; +use anchor_lang::prelude::*; + +use crate::utils::consts::ORACLE_MAPPING_EXTENDED_SIZE; +use crate::utils::pdas::seeds; + +#[derive(Accounts)] +#[instruction(feed_name: String)] +pub struct ExtendMapping<'info> { + /// CHECK: At creation admin can be anyone, this ix can't override an existing feed + #[account(mut)] + pub admin: Signer<'info>, + + // Set space to max size here + // The ability to create multiple feeds is mostly useful for tests + #[account(seeds = [seeds::CONFIG, feed_name.as_bytes()], bump, has_one = admin, has_one = oracle_mappings)] + pub configuration: AccountLoader<'info, crate::Configuration>, + + /// CHECK: checked above + #[account(mut, owner = crate::ID)] + pub oracle_mappings: AccountInfo<'info>, + + pub system_program: Program<'info, System>, +} + +pub fn process(ctx: Context, _: String) -> Result<()> { + resize_account_and_make_rent_exempt( + &mut ctx.accounts.oracle_mappings, + &ctx.accounts.admin, + &ctx.accounts.system_program, + ORACLE_MAPPING_EXTENDED_SIZE + 8, + ) +} + +fn resize_account_and_make_rent_exempt<'a>( + account: &mut AccountInfo<'a>, + payer: &Signer<'a>, + system_program: &Program<'a, System>, + new_size: usize, +) -> Result<()> { + if new_size <= account.data_len() { + // It can be resized even when new_size <= old_size, but we want to exclude + // this use case in order to catch human errors. + return Err(error!(ScopeError::CannotResizeAccount)); + } + // Make account rent exempt for the new size by transfering enough lamports. + let rent = Rent::get()?; + let new_minimum_balance = rent.minimum_balance(new_size); + + let lamports_diff = new_minimum_balance.saturating_sub(account.lamports()); + solana_program::program::invoke( + &solana_program::system_instruction::transfer(payer.key, account.key, lamports_diff), + &[ + payer.to_account_info().clone(), + account.clone(), + system_program.to_account_info(), + ], + )?; + + // No need to zero init as we always grow memory. + account.realloc(new_size, false).map_err(Into::into) +} diff --git a/programs/scope/src/handlers/handler_initialize.rs b/programs/scope/src/handlers/handler_initialize.rs index aca6e15..a2c7b08 100644 --- a/programs/scope/src/handlers/handler_initialize.rs +++ b/programs/scope/src/handlers/handler_initialize.rs @@ -27,38 +27,37 @@ pub struct Initialize<'info> { // Account is pre-reserved/paid outside the program #[account(zero)] - pub oracle_mappings: AccountLoader<'info, crate::OracleMappings>, + pub oracle_mappings: AccountLoader<'info, crate::OracleMappingsOld>, } pub fn process(ctx: Context, _: String) -> Result<()> { - // Initialize oracle mapping account let _ = ctx.accounts.oracle_mappings.load_init()?; + let _ = ctx.accounts.token_metadatas.load_init()?; + let mut oracle_prices = ctx.accounts.oracle_prices.load_init()?; + let mut oracle_twaps = ctx.accounts.oracle_twaps.load_init()?; + let mut configuration: std::cell::RefMut<'_, crate::Configuration> = + ctx.accounts.configuration.load_init()?; - // Initialize oracle price account + let admin = ctx.accounts.admin.key(); let oracle_pbk = ctx.accounts.oracle_mappings.key(); let twaps_pbk = ctx.accounts.oracle_twaps.key(); + let prices_pbk = ctx.accounts.oracle_prices.key(); + let metadata_pbk = ctx.accounts.token_metadatas.key(); - let mut oracle_prices = ctx.accounts.oracle_prices.load_init()?; + // Initialize oracle mapping account oracle_prices.oracle_mappings = oracle_pbk; // Initialize configuration account - let prices_pbk = ctx.accounts.oracle_prices.key(); - let admin = ctx.accounts.admin.key(); - let mut configuration: std::cell::RefMut<'_, crate::Configuration> = - ctx.accounts.configuration.load_init()?; configuration.admin = admin; configuration.oracle_mappings = oracle_pbk; configuration.oracle_prices = prices_pbk; configuration.oracle_twaps = twaps_pbk; + configuration.tokens_metadata = metadata_pbk; configuration.admin_cached = Pubkey::default(); // Initialize oracle twap account - let mut oracle_twaps = ctx.accounts.oracle_twaps.load_init()?; oracle_twaps.oracle_prices = prices_pbk; oracle_twaps.oracle_mappings = oracle_pbk; - let _ = ctx.accounts.token_metadatas.load_init()?; - configuration.tokens_metadata = ctx.accounts.token_metadatas.key(); - Ok(()) } diff --git a/programs/scope/src/handlers/handler_refresh_prices.rs b/programs/scope/src/handlers/handler_refresh_prices.rs index 6449b5d..92519ab 100644 --- a/programs/scope/src/handlers/handler_refresh_prices.rs +++ b/programs/scope/src/handlers/handler_refresh_prices.rs @@ -9,6 +9,8 @@ use solana_program::{ }, }; +use crate::utils::zero_copy_deserialize; +use crate::OracleMappingsCore; use crate::{ oracles::{get_price, OracleType}, ScopeError, @@ -20,8 +22,9 @@ const COMPUTE_BUDGET_ID: Pubkey = pubkey!("ComputeBudget111111111111111111111111 pub struct RefreshList<'info> { #[account(mut, has_one = oracle_mappings)] pub oracle_prices: AccountLoader<'info, crate::OraclePrices>, - #[account()] - pub oracle_mappings: AccountLoader<'info, crate::OracleMappings>, + /// CHECK: Checked above + #[account(owner = crate::ID)] + pub oracle_mappings: AccountInfo<'info>, #[account(mut, has_one = oracle_prices, has_one = oracle_mappings)] pub oracle_twaps: AccountLoader<'info, crate::OracleTwaps>, /// CHECK: Sysvar fixed address @@ -36,7 +39,8 @@ pub fn refresh_price_list<'info>( ) -> Result<()> { check_execution_ctx(&ctx.accounts.instruction_sysvar_account_info)?; - let oracle_mappings = &ctx.accounts.oracle_mappings.load()?; + let oracle_mappings = + &zero_copy_deserialize::(&ctx.accounts.oracle_mappings)?; let mut oracle_twaps = ctx.accounts.oracle_twaps.load_mut()?; // No token to refresh @@ -102,9 +106,9 @@ pub fn refresh_price_list<'info>( } else { match price_res { Ok(price) => price, - Err(e) => { + Err(_) => { msg!( - "Price skipped as validation failed (token {token_idx}, type {price_type:?}): {e}", + "Price skipped as validation failed (token {token_idx}, type {price_type:?})", ); continue; } diff --git a/programs/scope/src/handlers/handler_reset_twap.rs b/programs/scope/src/handlers/handler_reset_twap.rs index 6eba02e..f58f748 100644 --- a/programs/scope/src/handlers/handler_reset_twap.rs +++ b/programs/scope/src/handlers/handler_reset_twap.rs @@ -1,7 +1,7 @@ use anchor_lang::prelude::*; use solana_program::sysvar::instructions::ID as SYSVAR_INSTRUCTIONS_ID; -use crate::oracles::check_context; +use crate::{oracles::check_context, utils::pdas::seeds}; #[derive(Accounts)] #[instruction(token:u64, feed_name: String)] @@ -10,11 +10,11 @@ pub struct ResetTwap<'info> { #[account()] pub oracle_prices: AccountLoader<'info, crate::OraclePrices>, - #[account(seeds = [b"conf", feed_name.as_bytes()], bump, - has_one = admin, - has_one = oracle_prices, - has_one = oracle_twaps, - )] + #[account(seeds = [seeds::CONFIG, feed_name.as_bytes()], bump, + has_one = admin, + has_one = oracle_prices, + has_one = oracle_twaps, + )] pub configuration: AccountLoader<'info, crate::Configuration>, #[account(mut, has_one = oracle_prices)] pub oracle_twaps: AccountLoader<'info, crate::OracleTwaps>, diff --git a/programs/scope/src/handlers/handler_update_mapping.rs b/programs/scope/src/handlers/handler_update_mapping.rs index 635e409..ea49a61 100644 --- a/programs/scope/src/handlers/handler_update_mapping.rs +++ b/programs/scope/src/handlers/handler_update_mapping.rs @@ -1,44 +1,59 @@ -use anchor_lang::prelude::*; - +use crate::utils::pdas::seeds; +use crate::utils::zero_copy_deserialize_mut; use crate::{ oracles::{check_context, validate_oracle_account, OracleType}, - OracleMappings, ScopeError, + OracleMappingsCore, ScopeError, }; +use anchor_lang::prelude::*; #[derive(Accounts)] -#[instruction(token:u64, price_type: u8, twap_enabled: bool, twap_source: u16, feed_name: String)] +#[instruction(token:u16, price_type: u8, twap_enabled: bool, twap_source: u16, ref_price_index: u16, feed_name: String)] pub struct UpdateOracleMapping<'info> { pub admin: Signer<'info>, - #[account(seeds = [b"conf", feed_name.as_bytes()], bump, has_one = admin, has_one = oracle_mappings)] + #[account(seeds = [seeds::CONFIG, feed_name.as_bytes()], bump, has_one = admin, has_one = oracle_mappings)] pub configuration: AccountLoader<'info, crate::Configuration>, - #[account(mut)] - pub oracle_mappings: AccountLoader<'info, OracleMappings>, + + /// CHECK: checked above + on deserialize + #[account(mut, owner = crate::ID)] + pub oracle_mappings: AccountInfo<'info>, /// CHECK: We trust the admin to provide a trustable account here. Some basic sanity checks are done based on type pub price_info: Option>, } +pub fn reset_price_ref_mapping(ctx: Context) -> Result<()> { + let mut oracle_mappings = + zero_copy_deserialize_mut::(&ctx.accounts.oracle_mappings)?; + for i in 0..oracle_mappings.price_info_accounts.len() { + oracle_mappings.ref_price[i] = u16::MAX; + } + Ok(()) +} + pub fn process( ctx: Context, - token: usize, + entry_id: usize, price_type: u8, twap_enabled: bool, twap_source: u16, + ref_price_index: u16, _: String, ) -> Result<()> { check_context(&ctx)?; msg!( - "UpdateOracleMapping, token: {}, price_type: {}, twap_enabled: {}, twap_source: {}", - token, + "UpdateOracleMapping, token: {}, price_type: {}, twap_enabled: {}, twap_source: {}, ref_price_index: {}", + entry_id, price_type, twap_enabled, - twap_source + twap_source, + ref_price_index ); - let mut oracle_mappings = ctx.accounts.oracle_mappings.load_mut()?; + let mut oracle_mappings = + zero_copy_deserialize_mut::(&ctx.accounts.oracle_mappings)?; let ref_price_pubkey = oracle_mappings .price_info_accounts - .get_mut(token) + .get_mut(entry_id) .ok_or(ScopeError::BadTokenNb)?; let price_type: OracleType = price_type .try_into() @@ -61,9 +76,10 @@ pub fn process( } } - oracle_mappings.price_types[token] = price_type.into(); - oracle_mappings.twap_enabled[token] = u8::from(twap_enabled); - oracle_mappings.twap_source[token] = twap_source; + oracle_mappings.price_types[entry_id] = price_type.into(); + oracle_mappings.twap_enabled[entry_id] = u8::from(twap_enabled); + oracle_mappings.twap_source[entry_id] = twap_source; + oracle_mappings.ref_price[entry_id] = ref_price_index; Ok(()) } diff --git a/programs/scope/src/handlers/handler_update_token_metadata.rs b/programs/scope/src/handlers/handler_update_token_metadata.rs index 29ca362..2dd3c49 100644 --- a/programs/scope/src/handlers/handler_update_token_metadata.rs +++ b/programs/scope/src/handlers/handler_update_token_metadata.rs @@ -1,12 +1,12 @@ +use crate::utils::pdas::seeds; use crate::ScopeError; use anchor_lang::prelude::*; use num_enum::TryFromPrimitive; - #[derive(TryFromPrimitive, PartialEq, Eq, Clone, Copy, Debug)] #[repr(u64)] pub enum UpdateTokenMetadataMode { Name = 0, - MaxPriceAgeSeconds = 1, + MaxPriceAgeSlots = 1, } impl UpdateTokenMetadataMode { @@ -17,7 +17,7 @@ impl UpdateTokenMetadataMode { pub fn to_u16(self) -> u16 { match self { UpdateTokenMetadataMode::Name => 0, - UpdateTokenMetadataMode::MaxPriceAgeSeconds => 1, + UpdateTokenMetadataMode::MaxPriceAgeSlots => 1, } } } @@ -26,7 +26,7 @@ impl UpdateTokenMetadataMode { #[instruction(index: u64, mode: u64, feed_name: String, value: Vec)] pub struct UpdateTokensMetadata<'info> { pub admin: Signer<'info>, - #[account(seeds = [b"conf", feed_name.as_bytes()], bump, has_one = admin, has_one = tokens_metadata)] + #[account(seeds = [seeds::CONFIG, feed_name.as_bytes()], bump, has_one = admin, has_one = tokens_metadata)] pub configuration: AccountLoader<'info, crate::Configuration>, #[account(mut)] @@ -51,10 +51,10 @@ pub fn process( .try_into() .map_err(|_| ScopeError::InvalidTokenUpdateMode)?; match mode { - UpdateTokenMetadataMode::MaxPriceAgeSeconds => { + UpdateTokenMetadataMode::MaxPriceAgeSlots => { let value = u64::from_le_bytes(value[..8].try_into().unwrap()); msg!("Setting token max age for index {:?} to {}", index, value); - token_metadata.max_age_price_seconds = value; + token_metadata.max_age_price_slots = value; } UpdateTokenMetadataMode::Name => { assert!( diff --git a/programs/scope/src/handlers/mod.rs b/programs/scope/src/handlers/mod.rs index e68bf19..6b60207 100644 --- a/programs/scope/src/handlers/mod.rs +++ b/programs/scope/src/handlers/mod.rs @@ -1,6 +1,7 @@ pub mod handler_approve_admin_cached; pub mod handler_close_mint_map; pub mod handler_create_mint_map; +pub mod handler_extend_mapping; pub mod handler_initialize; pub mod handler_refresh_prices; pub mod handler_reset_twap; @@ -11,6 +12,7 @@ pub mod handler_update_token_metadata; pub use handler_approve_admin_cached::*; pub use handler_close_mint_map::*; pub use handler_create_mint_map::*; +pub use handler_extend_mapping::*; pub use handler_initialize::*; pub use handler_refresh_prices::*; pub use handler_reset_twap::*; diff --git a/programs/scope/src/lib.rs b/programs/scope/src/lib.rs index fbec100..e34050c 100644 --- a/programs/scope/src/lib.rs +++ b/programs/scope/src/lib.rs @@ -50,10 +50,11 @@ pub mod scope { pub fn update_mapping( ctx: Context, - token: u64, + token: u16, price_type: u8, twap_enabled: bool, twap_source: u16, + ref_price_index: u16, feed_name: String, ) -> Result<()> { let token: usize = token @@ -65,15 +66,36 @@ pub mod scope { price_type, twap_enabled, twap_source, + ref_price_index, feed_name, ) } + pub fn update_mapping_reset_price_ref( + ctx: Context, + token: u16, + price_type: u8, + twap_enabled: bool, + twap_source: u16, + ref_price_index: u16, + feed_name: String, + ) -> Result<()> { + let _ = ( + token, + price_type, + twap_enabled, + twap_source, + ref_price_index, + feed_name, + ); + handler_update_mapping::reset_price_ref_mapping(ctx) + } + pub fn reset_twap(ctx: Context, token: u64, feed_name: String) -> Result<()> { - let token: usize = token + let entry_id: usize = token .try_into() .map_err(|_| ScopeError::OutOfRangeIntegralConversion)?; - handler_reset_twap::process(ctx, token, feed_name) + handler_reset_twap::process(ctx, entry_id, feed_name) } pub fn update_token_metadata( @@ -120,4 +142,8 @@ pub mod scope { pub fn close_mint_map(ctx: Context) -> Result<()> { handler_close_mint_map::process(ctx) } + + pub fn extend_mapping(ctx: Context, feed_name: String) -> Result<()> { + handler_extend_mapping::process(ctx, feed_name) + } } diff --git a/programs/scope/src/oracles/ctokens.rs b/programs/scope/src/oracles/ctokens.rs index d9e2d6e..9077e13 100644 --- a/programs/scope/src/oracles/ctokens.rs +++ b/programs/scope/src/oracles/ctokens.rs @@ -10,7 +10,13 @@ const DECIMALS: u32 = 15u32; // Gives the price of 1 cToken in the collateral token pub fn get_price(solend_reserve_account: &AccountInfo, clock: &Clock) -> Result { - let mut reserve = Reserve::unpack(&solend_reserve_account.data.borrow())?; + let mut reserve = Reserve::unpack(&solend_reserve_account.data.borrow()).map_err(|e| { + msg!( + "Error unpacking CToken account {}", + solend_reserve_account.key() + ); + e + })?; // Manual refresh of the reserve to ensure the most accurate price let (last_updated_slot, unix_timestamp) = if reserve.accrue_interest(clock.slot).is_ok() { @@ -30,7 +36,13 @@ pub fn get_price(solend_reserve_account: &AccountInfo, clock: &Clock) -> Result< ) }; - let value = scaled_rate(&reserve)?; + let value = scaled_rate(&reserve).map_err(|e| { + msg!( + "Error getting scaled rate for CToken account {}: {e:?}", + solend_reserve_account.key() + ); + e + })?; let price = Price { value, diff --git a/programs/scope/src/oracles/jupiter_lp.rs b/programs/scope/src/oracles/jupiter_lp.rs index 257a792..b113b9f 100644 --- a/programs/scope/src/oracles/jupiter_lp.rs +++ b/programs/scope/src/oracles/jupiter_lp.rs @@ -9,8 +9,10 @@ use solana_program::program_pack::Pack; use crate::scope_chain::get_price_from_chain; use crate::utils::account_deserialize; use crate::utils::math::ten_pow; +use crate::utils::price_impl::check_ref_price_difference; use crate::{ - DatedPrice, MintToScopeChain, MintsToScopeChains, OraclePrices, Price, Result, ScopeError, + DatedPrice, MintToScopeChain, MintsToScopeChains, OracleMappingsCore, OraclePrices, Price, + Result, ScopeError, }; pub use jup_perp_itf as perpetuals; @@ -108,13 +110,19 @@ where ); // 2. Check accounts - check_accounts(jup_pool_pk, &jup_pool, mint_acc, &custodies_accs)?; + check_accounts(jup_pool_pk, &jup_pool, mint_acc, &custodies_accs).map_err(|e| { + msg!("Error while checking accounts: {:?}", e); + e + })?; // Check of oracles will be done in the next step while deserializing custodies // (avoid double iteration or keeping custodies in memory) // 3. Get mint supply - let lp_token_supply = get_lp_token_supply(mint_acc)?; + let lp_token_supply = get_lp_token_supply(mint_acc).map_err(|e| { + msg!("Error while getting mint supply: {:?}", e); + e + })?; // 4. Compute AUM and prices @@ -142,6 +150,13 @@ where custodies_and_prices_iter, aum_and_age_getter, ) + .map_err(|e| { + msg!( + "Error while computing price from custodies and prices: {:?}", + e + ); + e + }) } /// Get the price of 1 JLP token in USD using a scope mapping @@ -158,6 +173,7 @@ pub fn get_price_recomputed_scope<'a, 'b>( clock: &Clock, oracle_prices_pk: &Pubkey, oracle_prices: &OraclePrices, + oracle_mappings: &OracleMappingsCore, extra_accounts: &mut impl Iterator>, ) -> Result where @@ -194,7 +210,10 @@ where require_gte!(mint_to_price_map.mapping.len(), num_custodies); // 2. Check accounts - check_accounts(jup_pool_pk, &jup_pool, mint_acc, &custodies_accs)?; + check_accounts(jup_pool_pk, &jup_pool, mint_acc, &custodies_accs).map_err(|e| { + msg!("Error while checking accounts: {:?}", e); + e + })?; require_keys_eq!( *oracle_prices_pk, @@ -218,7 +237,10 @@ where // 3. Get mint supply - let lp_token_supply = get_lp_token_supply(mint_acc)?; + let lp_token_supply = get_lp_token_supply(mint_acc).map_err(|e| { + msg!("Error while getting mint supply: {:?}", e); + e + })?; // 4. Compute AUM and prices @@ -246,12 +268,27 @@ where compute_custody_aum(&custody, &dated_price) }; - compute_price_from_custodies_and_prices( + let price = compute_price_from_custodies_and_prices( lp_token_supply, clock, custodies_and_prices_iter, aum_and_age_getter, ) + .map_err(|e| { + msg!( + "Error while computing price from custodies and prices: {:?}", + e + ); + e + })?; + + if oracle_mappings.ref_price[entry_id] != u16::MAX { + let ref_price = + oracle_prices.prices[usize::from(oracle_mappings.ref_price[entry_id])].price; + check_ref_price_difference(price.price, ref_price)?; + } + + Ok(price) } fn compute_price_from_custodies_and_prices( diff --git a/programs/scope/src/oracles/ktokens.rs b/programs/scope/src/oracles/ktokens.rs index f5edcd3..c19fd18 100644 --- a/programs/scope/src/oracles/ktokens.rs +++ b/programs/scope/src/oracles/ktokens.rs @@ -24,6 +24,7 @@ use yvaults::{ }, }; +use crate::ScopeResult; use crate::{ utils::{account_deserialize, zero_copy_deserialize}, DatedPrice, Price, ScopeError, @@ -47,7 +48,7 @@ pub fn get_price<'a, 'b>( k_account: &AccountInfo, clock: &Clock, extra_accounts: &mut impl Iterator>, -) -> Result +) -> ScopeResult where 'a: 'b, { @@ -87,7 +88,7 @@ where name, expected ); - err!(ScopeError::UnexpectedAccount) + Err(ScopeError::UnexpectedAccount) } else { Ok(()) } @@ -134,7 +135,8 @@ where &strategy_account_ref, None, clock.slot, - )?; + ) + .map_err(|_| ScopeError::KTokenUnderlyingPriceNotValid)?; let holdings = holdings(&strategy_account_ref, clmm.as_ref(), &token_prices)?; @@ -149,7 +151,11 @@ where &scope_prices_ref, &collateral_infos_ref, &strategy_account_ref, - )?; + ) + .map_err(|e| { + msg!("Error getting component price last update: {:?}", e); + e + })?; Ok(DatedPrice { price, @@ -159,17 +165,19 @@ where }) } -fn get_clmm<'a, 'info>( +pub(super) fn get_clmm<'a, 'info>( pool: &'a AccountInfo<'info>, position: &'a AccountInfo<'info>, strategy: &WhirlpoolStrategy, -) -> Result> { +) -> ScopeResult> { let dex = DEX::try_from(strategy.strategy_dex).unwrap(); let clmm: Box = match dex { DEX::Orca => { - let pool = account_deserialize::(pool)?; + let pool = account_deserialize::(pool) + .map_err(|_| ScopeError::UnableToDeserializeAccount)?; let position = if strategy.position != Pubkey::default() { - let position = account_deserialize::(position)?; + let position = account_deserialize::(position) + .map_err(|_| ScopeError::UnableToDeserializeAccount)?; Some(position) } else { None @@ -207,7 +215,7 @@ fn get_component_px_last_update( scope_prices: &ScopePrices, collateral_infos: &CollateralInfos, strategy: &WhirlpoolStrategy, -) -> Result<(u64, u64)> { +) -> ScopeResult<(u64, u64)> { let token_a = yvaults::state::CollateralToken::try_from(strategy.token_a_collateral_id) .map_err(|_| ScopeError::ConversionFailure)?; let token_b = yvaults::state::CollateralToken::try_from(strategy.token_b_collateral_id) @@ -251,22 +259,30 @@ pub fn holdings( strategy: &WhirlpoolStrategy, clmm: &dyn Clmm, prices: &TokenPrices, -) -> Result { +) -> ScopeResult { // https://github.com/0xparashar/UniV3NFTOracle/blob/master/contracts/UniV3NFTOracle.sol#L27 // We are using the sqrt price derived from price_a and price_b // instead of the whirlpool price which could be manipulated/stale let pool_sqrt_price = price_utils::sqrt_price_from_scope_prices( - &prices.get( - CollateralToken::try_from(strategy.token_a_collateral_id) - .map_err(|_| ScopeError::ConversionFailure)?, - )?, - &prices.get( - CollateralToken::try_from(strategy.token_b_collateral_id) - .map_err(|_| ScopeError::ConversionFailure)?, - )?, + &prices + .get( + CollateralToken::try_from(strategy.token_a_collateral_id) + .map_err(|_| ScopeError::ConversionFailure)?, + ) + .map_err(|_| ScopeError::KTokenUnderlyingPriceNotValid)?, + &prices + .get( + CollateralToken::try_from(strategy.token_b_collateral_id) + .map_err(|_| ScopeError::ConversionFailure)?, + ) + .map_err(|_| ScopeError::KTokenUnderlyingPriceNotValid)?, strategy.token_a_mint_decimals, strategy.token_b_mint_decimals, - )?; + ) + .map_err(|e| { + msg!("Error calculating sqrt price: {:?}", e); + ScopeError::ConversionFailure + })?; if cfg!(feature = "debug") { let w = price_utils::calc_price_from_sqrt_price( @@ -283,7 +299,10 @@ pub fn holdings( msg!("o: {} w: {} d: {}%", w, o, diff * 100.0); } - holdings_no_rewards(strategy, clmm, prices, pool_sqrt_price) + holdings_no_rewards(strategy, clmm, prices, pool_sqrt_price).map_err(|e| { + msg!("Error calculating holdings: {:?}", e); + ScopeError::KTokenHoldingsCalculationError + }) } pub fn holdings_no_rewards( diff --git a/programs/scope/src/oracles/ktokens_token_x.rs b/programs/scope/src/oracles/ktokens_token_x.rs index b8cda26..42b0746 100644 --- a/programs/scope/src/oracles/ktokens_token_x.rs +++ b/programs/scope/src/oracles/ktokens_token_x.rs @@ -3,20 +3,16 @@ use std::ops::Deref; use anchor_lang::{prelude::*, Result}; use yvaults::{ self as kamino, - clmm::{orca_clmm::OrcaClmm, Clmm}, + clmm::Clmm, operations::vault_operations::common, - raydium_amm_v3::states::{PersonalPositionState as RaydiumPosition, PoolState as RaydiumPool}, - raydium_clmm::RaydiumClmm, state::CollateralToken, state::{CollateralInfos, GlobalConfig, WhirlpoolStrategy}, - utils::types::DEX, utils::{enums::LiquidityCalculationMode, price::TokenPrices}, - whirlpool::state::{Position as OrcaPosition, Whirlpool as OrcaWhirlpool}, }; +use crate::ScopeResult; use crate::{ utils::{ - account_deserialize, math::{price_of_lamports_to_price_of_tokens, u64_div_to_price}, zero_copy_deserialize, }, @@ -43,7 +39,7 @@ pub fn get_token_x_per_share<'a, 'b>( clock: &Clock, extra_accounts: &mut impl Iterator>, token: TokenTypes, -) -> Result +) -> ScopeResult where 'a: 'b, { @@ -83,7 +79,7 @@ where name, expected ); - err!(ScopeError::UnexpectedAccount) + Err(ScopeError::UnexpectedAccount) } else { Ok(()) } @@ -118,7 +114,7 @@ where let scope_prices_ref = zero_copy_deserialize::(scope_prices_account_info)?; - let clmm = get_clmm( + let clmm = super::ktokens::get_clmm( pool_account_info, position_account_info, &strategy_account_ref, @@ -130,10 +126,16 @@ where &strategy_account_ref, None, clock.slot, - )?; + ) + .map_err(|_| ScopeError::KTokenUnderlyingPriceNotValid)?; let num_token_x = - holdings_of_token_x(&strategy_account_ref, clmm.as_ref(), &token_prices, token)?; + holdings_of_token_x(&strategy_account_ref, clmm.as_ref(), &token_prices, token).map_err( + |e| { + msg!("Error while computing the Ktoken pool holdings: {:?}", e); + ScopeError::KTokenHoldingsCalculationError + }, + )?; let num_shares = strategy_account_ref.shares_issued; // Get the least-recently updated component price from both scope chains @@ -168,48 +170,6 @@ where }) } -fn get_clmm<'a, 'info>( - pool: &'a AccountInfo<'info>, - position: &'a AccountInfo<'info>, - strategy: &WhirlpoolStrategy, -) -> Result> { - let dex = DEX::try_from(strategy.strategy_dex).unwrap(); - let clmm: Box = match dex { - DEX::Orca => { - let pool = account_deserialize::(pool)?; - let position = if strategy.position != Pubkey::default() { - let position = account_deserialize::(position)?; - Some(position) - } else { - None - }; - Box::new(OrcaClmm { - pool, - position, - lower_tick_array: None, - upper_tick_array: None, - }) - } - DEX::Raydium => { - let pool = zero_copy_deserialize::(pool)?; - let position = if strategy.position != Pubkey::default() { - let position = account_deserialize::(position)?; - Some(position) - } else { - None - }; - Box::new(RaydiumClmm { - pool, - position, - protocol_position: None, - lower_tick_array: None, - upper_tick_array: None, - }) - } - }; - Ok(clmm) -} - /// Returns amount of token x in the strategy /// Use a sqrt price derived from price_a and price_b, not from the pool as it cannot be considered reliable pub fn holdings_of_token_x( diff --git a/programs/scope/src/oracles/meteora_dlmm.rs b/programs/scope/src/oracles/meteora_dlmm.rs index c485415..dbcdcce 100644 --- a/programs/scope/src/oracles/meteora_dlmm.rs +++ b/programs/scope/src/oracles/meteora_dlmm.rs @@ -59,7 +59,7 @@ where lb_clmm::get_x64_price_from_id(lb_pair_state.active_id, lb_pair_state.bin_step) .ok_or_else(|| { msg!("Math overflow when calculating dlmm price"); - error!(ScopeError::MathOverflow) + ScopeError::MathOverflow })?; let q64x64_price = if a_to_b { U192::from(q64x64_price) @@ -68,7 +68,10 @@ where (U192::one() << 128) / q64x64_price }; - let lamport_price = math::q64x64_price_to_price(q64x64_price)?; + let lamport_price = math::q64x64_price_to_price(q64x64_price).map_err(|e| { + msg!("Error while computing the price of the tokens in the pool: {e:?}",); + e + })?; let (src_token_decimals, dst_token_decimals) = if a_to_b { (mint_a_decimals, mint_b_decimals) } else { diff --git a/programs/scope/src/oracles/mod.rs b/programs/scope/src/oracles/mod.rs index b119f9a..ca560eb 100644 --- a/programs/scope/src/oracles/mod.rs +++ b/programs/scope/src/oracles/mod.rs @@ -10,6 +10,7 @@ pub mod msol_stake; pub mod orca_whirlpool; pub mod pyth; pub mod pyth_ema; +pub mod pyth_pull_based; pub mod raydium_ammv3; pub mod spl_stake; pub mod switchboard_v2; @@ -22,8 +23,9 @@ use num_enum::{IntoPrimitive, TryFromPrimitive}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use crate::{DatedPrice, OracleMappings, OraclePrices, OracleTwaps, ScopeError}; +use crate::{DatedPrice, OracleMappingsCore, OraclePrices, OracleTwaps, ScopeError}; +#[cfg(feature = "yvaults")] use self::ktokens_token_x::TokenTypes; pub fn check_context(ctx: &Context) -> Result<()> { @@ -93,6 +95,8 @@ pub enum OracleType { MeteoraDlmmBtoA = 19, /// Jupiter's perpetual LP tokens computed from scope prices JupiterLpScope = 20, + /// Pyth Pull based oracles + PythPullBased = 21, } impl OracleType { @@ -103,7 +107,8 @@ impl OracleType { /// Get the number of compute unit needed to refresh the price of a token pub fn get_update_cu_budget(&self) -> u32 { match self { - OracleType::Pyth => 20_000, + OracleType::PythPullBased => 20_000, + OracleType::Pyth => 35_000, OracleType::SwitchboardV2 => 30_000, OracleType::CToken => 130_000, OracleType::SplStake => 20_000, @@ -112,11 +117,11 @@ impl OracleType { OracleType::KTokenToTokenA | OracleType::KTokenToTokenB => 100_000, OracleType::MsolStake => 20_000, OracleType::JupiterLpFetch => 40_000, - OracleType::ScopeTwap => 15_000, + OracleType::ScopeTwap => 30_000, OracleType::OrcaWhirlpoolAtoB | OracleType::OrcaWhirlpoolBtoA | OracleType::RaydiumAmmV3AtoB - | OracleType::RaydiumAmmV3BtoA => 20_000, + | OracleType::RaydiumAmmV3BtoA => 25_000, OracleType::MeteoraDlmmAtoB | OracleType::MeteoraDlmmBtoA => 30_000, OracleType::JupiterLpCompute | OracleType::JupiterLpScope => 120_000, OracleType::DeprecatedPlaceholder1 | OracleType::DeprecatedPlaceholder2 => { @@ -138,7 +143,7 @@ pub fn get_price<'a, 'b>( extra_accounts: &mut impl Iterator>, clock: &Clock, oracle_twaps: &OracleTwaps, - oracle_mappings: &OracleMappings, + oracle_mappings: &OracleMappingsCore, oracle_prices: &AccountLoader, index: usize, ) -> crate::Result @@ -147,7 +152,14 @@ where { match price_type { OracleType::Pyth => pyth::get_price(base_account, clock), - OracleType::SwitchboardV2 => switchboard_v2::get_price(base_account), + OracleType::PythPullBased => pyth_pull_based::get_price( + index, + base_account, + clock, + oracle_prices.load()?.deref(), + oracle_mappings, + ), + OracleType::SwitchboardV2 => switchboard_v2::get_price(base_account).map_err(Into::into), OracleType::CToken => ctokens::get_price(base_account, clock), OracleType::SplStake => spl_stake::get_price(base_account, clock), #[cfg(not(feature = "yvaults"))] @@ -156,21 +168,34 @@ where } OracleType::PythEMA => pyth_ema::get_price(base_account, clock), #[cfg(feature = "yvaults")] - OracleType::KToken => ktokens::get_price(base_account, clock, extra_accounts), + OracleType::KToken => { + ktokens::get_price(base_account, clock, extra_accounts).map_err(|e| { + msg!("Error getting KToken price: {:?}", e); + e.into() + }) + } #[cfg(feature = "yvaults")] OracleType::KTokenToTokenA => ktokens_token_x::get_token_x_per_share( base_account, clock, extra_accounts, TokenTypes::TokenA, - ), + ) + .map_err(|e| { + msg!("Error getting KToken share ratio: {:?}", e); + e.into() + }), #[cfg(feature = "yvaults")] OracleType::KTokenToTokenB => ktokens_token_x::get_token_x_per_share( base_account, clock, extra_accounts, TokenTypes::TokenB, - ), + ) + .map_err(|e| { + msg!("Error getting KToken share ratio: {:?}", e); + e.into() + }), #[cfg(not(feature = "yvaults"))] OracleType::KTokenToTokenA => { panic!("yvaults feature is not enabled, KToken oracle type is not available") @@ -179,11 +204,18 @@ where OracleType::KTokenToTokenB => { panic!("yvaults feature is not enabled, KToken oracle type is not available") } - OracleType::MsolStake => msol_stake::get_price(base_account, clock), + OracleType::MsolStake => msol_stake::get_price(base_account, clock).map_err(Into::into), OracleType::JupiterLpFetch => { - jupiter_lp::get_price_no_recompute(base_account, clock, extra_accounts) + jupiter_lp::get_price_no_recompute(base_account, clock, extra_accounts).map_err(|e| { + msg!("Error getting Jupiter LP price: {:?}", e); + e + }) } - OracleType::ScopeTwap => twap::get_price(oracle_mappings, oracle_twaps, index, clock), + OracleType::ScopeTwap => twap::get_price(oracle_mappings, oracle_twaps, index, clock) + .map_err(|e| { + msg!("Error getting Scope TWAP price: {:?}", e); + e.into() + }), OracleType::OrcaWhirlpoolAtoB => { orca_whirlpool::get_price(true, base_account, clock, extra_accounts) } @@ -207,6 +239,7 @@ where clock, &oracle_prices.key(), oracle_prices.load()?.deref(), + oracle_mappings, extra_accounts, ), OracleType::DeprecatedPlaceholder1 | OracleType::DeprecatedPlaceholder2 => { @@ -225,6 +258,7 @@ pub fn validate_oracle_account( ) -> crate::Result<()> { match price_type { OracleType::Pyth => pyth::validate_pyth_price_info(price_account), + OracleType::PythPullBased => pyth_pull_based::validate_price_update_v2_info(price_account), OracleType::SwitchboardV2 => Ok(()), // TODO at least check account ownership? OracleType::CToken => Ok(()), // TODO how shall we validate ctoken account? OracleType::SplStake => Ok(()), // TODO, should validate ownership of the account diff --git a/programs/scope/src/oracles/msol_stake.rs b/programs/scope/src/oracles/msol_stake.rs index f882d34..ce61b71 100644 --- a/programs/scope/src/oracles/msol_stake.rs +++ b/programs/scope/src/oracles/msol_stake.rs @@ -1,7 +1,7 @@ use anchor_lang::prelude::*; use solana_program::borsh0_10::try_from_slice_unchecked; -use crate::{DatedPrice, Price, Result, ScopeError}; +use crate::{DatedPrice, Price, ScopeError, ScopeResult}; use self::msol_stake_pool::State; @@ -11,14 +11,17 @@ const DECIMALS: u32 = 15u32; pub fn get_price( msol_pool_account_info: &AccountInfo, current_clock: &Clock, -) -> Result { +) -> ScopeResult { let stake_pool = try_from_slice_unchecked::(&msol_pool_account_info.data.borrow()[8..]) .map_err(|_| { - msg!("Provided pubkey is not a MSOL Stake account"); + msg!("Provided pubkey is not a valid MSOL Stake account"); ScopeError::UnexpectedAccount })?; - let value = scaled_rate(&stake_pool)?; + let value = scaled_rate(&stake_pool).map_err(|e| { + msg!("Error while calculating the scaled rate: {:?}", e); + e + })?; let price = Price { value, @@ -34,7 +37,7 @@ pub fn get_price( Ok(dated_price) } -fn scaled_rate(stake_pool: &State) -> Result { +fn scaled_rate(stake_pool: &State) -> ScopeResult { const FACTOR: u64 = 10u64.pow(DECIMALS); stake_pool.calc_lamports_from_msol_amount(FACTOR) } @@ -46,16 +49,16 @@ mod msol_stake_pool { /// as value = shares * share_price where share_price=total_value/total_shares /// or shares = amount_value / share_price where share_price=total_value/total_shares /// => shares = amount_value * 1/share_price where 1/share_price=total_shares/total_value - pub fn proportional(amount: u64, numerator: u64, denominator: u64) -> Result { + pub fn proportional(amount: u64, numerator: u64, denominator: u64) -> ScopeResult { if denominator == 0 { return Ok(amount); } u64::try_from((amount as u128) * (numerator as u128) / (denominator as u128)) - .map_err(|_| ScopeError::MathOverflow.into()) + .map_err(|_| ScopeError::MathOverflow) } #[inline] //alias for proportional - pub fn value_from_shares(shares: u64, total_value: u64, total_shares: u64) -> Result { + pub fn value_from_shares(shares: u64, total_value: u64, total_shares: u64) -> ScopeResult { proportional(shares, total_value, total_shares) } @@ -190,31 +193,31 @@ mod msol_stake_pool { } impl State { - pub fn total_cooling_down(&self) -> Result { + pub fn total_cooling_down(&self) -> ScopeResult { self.stake_system .delayed_unstake_cooling_down .checked_add(self.emergency_cooling_down) - .ok_or_else(|| ScopeError::MathOverflow.into()) + .ok_or(ScopeError::MathOverflow) } /// total_active_balance + total_cooling_down + available_reserve_balance - pub fn total_lamports_under_control(&self) -> Result { + pub fn total_lamports_under_control(&self) -> ScopeResult { self.validator_system .total_active_balance .checked_add(self.total_cooling_down()?) .ok_or(ScopeError::MathOverflow)? .checked_add(self.available_reserve_balance) // reserve_pda.lamports() - self.rent_exempt_for_token_acc - .ok_or_else(|| ScopeError::MathOverflow.into()) + .ok_or(ScopeError::MathOverflow) } - pub fn total_virtual_staked_lamports(&self) -> Result { + pub fn total_virtual_staked_lamports(&self) -> ScopeResult { // if we get slashed it may be negative but we must use 0 instead Ok(self .total_lamports_under_control()? .saturating_sub(self.circulating_ticket_balance)) //tickets created -> cooling down lamports or lamports already in reserve and not claimed yet } - pub fn calc_lamports_from_msol_amount(&self, msol_amount: u64) -> Result { + pub fn calc_lamports_from_msol_amount(&self, msol_amount: u64) -> ScopeResult { value_from_shares( msol_amount, self.total_virtual_staked_lamports()?, diff --git a/programs/scope/src/oracles/orca_whirlpool.rs b/programs/scope/src/oracles/orca_whirlpool.rs index 159cf73..d8b74a2 100644 --- a/programs/scope/src/oracles/orca_whirlpool.rs +++ b/programs/scope/src/oracles/orca_whirlpool.rs @@ -58,7 +58,11 @@ where pool_data.sqrt_price, mint_a_decimals, mint_b_decimals, - )?; + ) + .map_err(|e| { + msg!("Error while computing the price of the tokens in the pool: {e:?}",); + e + })?; // Return price Ok(DatedPrice { diff --git a/programs/scope/src/oracles/pyth.rs b/programs/scope/src/oracles/pyth.rs index 3c08b33..d1fb61a 100644 --- a/programs/scope/src/oracles/pyth.rs +++ b/programs/scope/src/oracles/pyth.rs @@ -16,10 +16,10 @@ use anchor_lang::solana_program::clock::DEFAULT_MS_PER_SLOT; use pyth_client::PriceType; use pyth_sdk_solana::state as pyth_client; -use crate::{DatedPrice, Price, Result, ScopeError}; +use crate::{DatedPrice, Price, ScopeError}; /// validate price confidence - confidence/price ratio should be less than 2% -const ORACLE_CONFIDENCE_FACTOR: u64 = 50; // 100% / 2% +pub const ORACLE_CONFIDENCE_FACTOR: u64 = 50; // 100% / 2% /// Only update with prices not older than 10 minutes, users can still check actual price age const STALENESS_SLOT_THRESHOLD: u64 = (10 * 60 * 1000) / DEFAULT_MS_PER_SLOT; // 10 minutes @@ -27,8 +27,10 @@ const STALENESS_SLOT_THRESHOLD: u64 = (10 * 60 * 1000) / DEFAULT_MS_PER_SLOT; // pub fn get_price(price_info: &AccountInfo, clock: &Clock) -> Result { let data = price_info.try_borrow_data()?; let price_account: &pyth_client::SolanaPriceAccount = - pyth_client::load_price_account(data.as_ref()) - .map_err(|_| error!(ScopeError::PriceNotValid))?; + pyth_client::load_price_account(data.as_ref()).map_err(|e| { + msg!("Error loading pyth account {}: {:?}", price_info.key, e); + ScopeError::PriceNotValid + })?; let oldest_accepted_slot = clock.slot.saturating_sub(STALENESS_SLOT_THRESHOLD); @@ -60,21 +62,25 @@ pub fn get_price(price_info: &AccountInfo, clock: &Clock) -> Result price_account.prev_timestamp, ) } else { - msg!("No valid price in pyth account {}", price_info.key); - return err!(ScopeError::PriceNotValid); + msg!( + "Price in pyth account {} is older than 10 minutes", + price_info.key + ); + return Err(ScopeError::PriceNotValid.into()); }; if pyth_price.expo > 0 { msg!( - "Pyth price account provided has a negative price exponent: {}", + "Pyth price account {} provided has a negative price exponent: {}", + price_info.key, pyth_price.expo ); - return err!(ScopeError::PriceNotValid); + return Err(ScopeError::PriceNotValid.into()); } let price = validate_valid_price(&pyth_price, ORACLE_CONFIDENCE_FACTOR).map_err(|e| { msg!( - "Confidence interval check failed on pyth account {}", + "Price validity check failed on pyth account {}", price_info.key ); e @@ -94,19 +100,21 @@ pub fn get_price(price_info: &AccountInfo, clock: &Clock) -> Result pub fn validate_valid_price( pyth_price: &pyth_client::Price, oracle_confidence_factor: u64, -) -> Result { +) -> std::result::Result { if cfg!(feature = "skip_price_validation") { return Ok(u64::try_from(pyth_price.price).unwrap()); } let price = u64::try_from(pyth_price.price).unwrap(); if price == 0 { - return err!(ScopeError::PriceNotValid); + msg!("Pyth price is 0"); + return Err(ScopeError::PriceNotValid); } let conf: u64 = pyth_price.conf; let conf_50x: u64 = conf.checked_mul(oracle_confidence_factor).unwrap(); if conf_50x > price { - return err!(ScopeError::PriceNotValid); + msg!("Pyth price has a confidence interval too large: {}", conf); + return Err(ScopeError::PriceNotValid); }; Ok(price) } diff --git a/programs/scope/src/oracles/pyth_ema.rs b/programs/scope/src/oracles/pyth_ema.rs index 36556be..0756f2a 100644 --- a/programs/scope/src/oracles/pyth_ema.rs +++ b/programs/scope/src/oracles/pyth_ema.rs @@ -25,8 +25,10 @@ const STALENESS_THRESHOLD: u64 = 10 * 60; // 10 minutes pub fn get_price(price_info: &AccountInfo, clock: &Clock) -> Result { let data = price_info.try_borrow_data()?; let price_account: &pyth_client::SolanaPriceAccount = - pyth_client::load_price_account(data.as_ref()) - .map_err(|_| error!(ScopeError::PriceNotValid))?; + pyth_client::load_price_account(data.as_ref()).map_err(|_| { + msg!("Loading pyth price account failed {}", price_info.key); + ScopeError::PriceNotValid + })?; let pyth_raw = price_account.to_price_feed(price_info.key); @@ -36,23 +38,22 @@ pub fn get_price(price_info: &AccountInfo, clock: &Clock) -> Result } else if let Some(pyth_ema_price) = pyth_raw.get_ema_price_no_older_than(clock.unix_timestamp, STALENESS_THRESHOLD) { - if price_account.agg.status != pyth_client::PriceStatus::Trading { - msg!("No valid EMA price in pyth account {}", price_info.key); - return err!(ScopeError::PriceNotValid); - } - // Or use the current valid price if available pyth_ema_price } else { - msg!("No valid EMA price in pyth account {}", price_info.key); - return err!(ScopeError::PriceNotValid); + msg!( + "No recent (10 minutes) EMA price in pyth account {}", + price_info.key + ); + return Err(ScopeError::PriceNotValid.into()); }; if pyth_ema_price.expo > 0 { msg!( - "Pyth price account provided has a negative EMA price exponent: {}", - pyth_ema_price.expo + "Pyth price account {} provided has a negative EMA price exponent: {}", + price_info.key, + pyth_ema_price.expo, ); - return err!(ScopeError::PriceNotValid); + return Err(ScopeError::PriceNotValid.into()); } let ema_price = diff --git a/programs/scope/src/oracles/pyth_pull_based.rs b/programs/scope/src/oracles/pyth_pull_based.rs new file mode 100644 index 0000000..bb8996c --- /dev/null +++ b/programs/scope/src/oracles/pyth_pull_based.rs @@ -0,0 +1,94 @@ +use anchor_lang::prelude::*; +use anchor_lang::solana_program::clock; +use pyth_solana_receiver_sdk::price_update::{self, PriceUpdateV2, VerificationLevel}; + +use crate::{ + utils::{account_deserialize, price_impl::check_ref_price_difference}, + DatedPrice, OracleMappingsCore as OracleMappings, OraclePrices, Price, ScopeError, +}; +pub const MAXIMUM_AGE: u64 = 10 * 60; // Ten minutes +use pyth_sdk_solana::state as pyth_client; + +use super::pyth::{validate_valid_price, ORACLE_CONFIDENCE_FACTOR}; + +pub fn get_price( + entry_id: usize, + price_info: &AccountInfo, + clock: &Clock, + oracle_prices: &OraclePrices, + oracle_mappings: &OracleMappings, +) -> Result { + let price_account: PriceUpdateV2 = account_deserialize(price_info)?; + + let price = price_account.get_price_no_older_than_with_custom_verification_level( + clock, + i64::MAX.try_into().unwrap(), // MAXIMUM_AGE, // this should be filtered by the caller + &price_account.price_message.feed_id, + VerificationLevel::Full, // All our prices and the sponsored feeds are full verified + )?; + + let price_update::Price { + price, + conf, + exponent, + publish_time, + } = price; + + if exponent > 0 { + msg!( + "Pyth price account provided has a negative price exponent: {}", + exponent + ); + return err!(ScopeError::PriceNotValid); + } + + // Validate confidence, rebuild the struct to match the pyth_client::Price struct + // and reuse the logic + let old_pyth_price = pyth_client::Price { + conf, + expo: exponent, + price, + publish_time, + }; + let price_value = + validate_valid_price(&old_pyth_price, ORACLE_CONFIDENCE_FACTOR).map_err(|e| { + msg!( + "Confidence interval check failed on pyth account {}", + price_info.key + ); + e + })?; + + let final_price = Price { + value: price_value, + exp: exponent.abs().try_into().unwrap(), + }; + + if oracle_mappings.ref_price[entry_id] != u16::MAX { + let ref_price = + oracle_prices.prices[usize::from(oracle_mappings.ref_price[entry_id])].price; + check_ref_price_difference(final_price, ref_price)?; + } + + // todo: Discuss how we should handle the time jump that can happen when there is an outage? + let elapsed_time_s = u64::try_from(clock.unix_timestamp) + .unwrap() + .saturating_sub(u64::try_from(publish_time).unwrap()); + let elapsed_slot_estimate = elapsed_time_s * 1000 / clock::DEFAULT_MS_PER_SLOT; + let estimated_published_slot = clock.slot.saturating_sub(elapsed_slot_estimate); + let last_updated_slot = u64::min(estimated_published_slot, price_account.posted_slot); + Ok(DatedPrice { + price: final_price, + last_updated_slot, + unix_timestamp: publish_time.try_into().unwrap(), + ..Default::default() + }) +} + +pub fn validate_price_update_v2_info(price_info: &AccountInfo) -> Result<()> { + if cfg!(feature = "skip_price_validation") { + return Ok(()); + } + let _: PriceUpdateV2 = account_deserialize(price_info)?; + Ok(()) +} diff --git a/programs/scope/src/oracles/raydium_ammv3.rs b/programs/scope/src/oracles/raydium_ammv3.rs index 8e50697..468ad2a 100644 --- a/programs/scope/src/oracles/raydium_ammv3.rs +++ b/programs/scope/src/oracles/raydium_ammv3.rs @@ -16,7 +16,11 @@ pub fn get_price(a_to_b: bool, pool: &AccountInfo, clock: &Clock) -> Result Result { +pub fn get_price( + switchboard_feed_info: &AccountInfo, +) -> std::result::Result { let feed = AggregatorAccountData::new(switchboard_feed_info) .map_err(|_| ScopeError::SwitchboardV2Error)?; @@ -43,7 +45,7 @@ pub fn get_price(switchboard_feed_info: &AccountInfo) -> Result { stdev_mantissa, stdev_scale ); - return err!(ScopeError::SwitchboardV2Error); + return Err(ScopeError::SwitchboardV2Error); } }; @@ -209,7 +211,7 @@ mod switchboard { "Switchboard aggregator account has an invalid discriminator: {:?}", disc_bytes ); - return err!(ScopeError::SwitchboardV2Error); + return Err(ScopeError::InvalidAccountDiscriminator.into()); } Ok(Ref::map(data, |data| bytemuck::from_bytes(&data[8..]))) diff --git a/programs/scope/src/oracles/twap.rs b/programs/scope/src/oracles/twap.rs index fabc047..b471a17 100644 --- a/programs/scope/src/oracles/twap.rs +++ b/programs/scope/src/oracles/twap.rs @@ -1,8 +1,8 @@ use std::cmp::Ordering; -use crate::ScopeError; use crate::ScopeError::PriceAccountNotExpected; -use crate::{DatedPrice, OracleMappings, OracleTwaps, Price}; +use crate::{DatedPrice, OracleMappingsCore, OracleTwaps, Price}; +use crate::{ScopeError, ScopeResult}; use anchor_lang::prelude::*; use intbits::Bits; @@ -21,10 +21,14 @@ pub fn validate_price_account(account: &AccountInfo) -> Result<()> { Err(PriceAccountNotExpected.into()) } -pub fn update_twap(oracle_twaps: &mut OracleTwaps, token: usize, price: &DatedPrice) -> Result<()> { +pub fn update_twap( + oracle_twaps: &mut OracleTwaps, + entry_id: usize, + price: &DatedPrice, +) -> Result<()> { let twap = oracle_twaps .twaps - .get_mut(token) + .get_mut(entry_id) .ok_or(ScopeError::TwapSourceIndexOutOfRange)?; // if there is no previous twap, store the existent @@ -39,27 +43,27 @@ pub fn update_twap(oracle_twaps: &mut OracleTwaps, token: usize, price: &DatedPr pub fn reset_twap( oracle_twaps: &mut OracleTwaps, - token: usize, + entry_id: usize, price: Price, price_ts: u64, price_slot: u64, ) -> Result<()> { let twap = oracle_twaps .twaps - .get_mut(token) + .get_mut(entry_id) .ok_or(ScopeError::TwapSourceIndexOutOfRange)?; reset_ema_twap(twap, price, price_ts, price_slot); Ok(()) } pub fn get_price( - oracle_mappings: &OracleMappings, + oracle_mappings: &OracleMappingsCore, oracle_twaps: &OracleTwaps, - token: usize, + entry_id: usize, clock: &Clock, -) -> Result { - let source_index = usize::from(oracle_mappings.twap_source[token]); - msg!("Get twap price at index {source_index} for tk {token}",); +) -> ScopeResult { + let source_index = usize::from(oracle_mappings.twap_source[entry_id]); + msg!("Get twap price at index {source_index} for tk {entry_id}",); let twap = oracle_twaps .twaps diff --git a/programs/scope/src/program_id.rs b/programs/scope/src/program_id.rs index 4f7fc69..412a7ed 100644 --- a/programs/scope/src/program_id.rs +++ b/programs/scope/src/program_id.rs @@ -13,15 +13,30 @@ compile_error!("'localnet' and 'devnet' features are mutually exclusive"); #[cfg(all(feature = "mainnet", feature = "skip_price_validation"))] compile_error!("'mainnet' and 'skip_price_validation' features are mutually exclusive"); +#[cfg(all(feature = "mainnet", feature = "staging"))] +compile_error!("'mainnet' and 'staging' features are mutually exclusive"); + +#[cfg(all(feature = "localnet", feature = "staging"))] +compile_error!("'localnet' and 'staging' features are mutually exclusive"); + +#[cfg(all(feature = "devnet", feature = "staging"))] +compile_error!("'devnet' and 'staging' features are mutually exclusive"); + +#[cfg(all(feature = "skip_price_validation", feature = "staging"))] +compile_error!("'skip_price_validation' and 'staging' features are mutually exclusive"); + cfg_if::cfg_if! { if #[cfg(feature = "mainnet")] { pub const PROGRAM_ID:Pubkey = pubkey!("HFn8GnPADiny6XqUoWE8uRPPxb29ikn4yTuPa9MF2fWJ"); } + else if #[cfg(feature = "staging")] { + pub const PROGRAM_ID:Pubkey = pubkey!("scpStzYvKzE7DHwsGMP5XLhcMTuLr3feoiC9mJ3yHr5"); + } else if #[cfg(feature = "localnet")] { pub const PROGRAM_ID:Pubkey = pubkey!("2fU6YqiA2aj9Ct1tDagA8Tng7otgxHM5KwgnsUWsMFxM"); } else if #[cfg(feature = "devnet")] { pub const PROGRAM_ID:Pubkey = pubkey!("3Vw8Ngkh1MVJTPHthmUbmU2XKtFEkjYvJzMqrv2rh9yX"); } else { - compile_error!("At least one of 'mainnet', 'localnet' or 'devnet' feature need to be set"); + compile_error!("At least one of 'mainnet', 'staging', 'localnet' or 'devnet' feature need to be set"); } } diff --git a/programs/scope/src/states.rs b/programs/scope/src/states.rs index f632c76..db58630 100644 --- a/programs/scope/src/states.rs +++ b/programs/scope/src/states.rs @@ -1,5 +1,7 @@ use std::mem::size_of; +use std::ops::Deref; +use crate::utils::consts::*; use crate::{MAX_ENTRIES, MAX_ENTRIES_U16}; use anchor_lang::prelude::*; use decimal_wad::decimal::Decimal; @@ -7,6 +9,10 @@ use num_enum::{IntoPrimitive, TryFromPrimitive}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +pub use mapping::core::OracleMappings as OracleMappingsCore; +pub use mapping::extended::OracleMappings as OracleMappingsExtended; +pub use mapping::old::OracleMappings as OracleMappingsOld; + #[zero_copy] #[derive(Debug, Default)] pub struct Price { @@ -95,6 +101,8 @@ impl EmaTwap { } } +static_assertions::const_assert_eq!(ORACLE_TWAPS_SIZE, std::mem::size_of::()); +static_assertions::const_assert_eq!(0, std::mem::size_of::() % 8); // Account to store dated TWAP prices #[account(zero_copy)] pub struct OracleTwaps { @@ -103,6 +111,8 @@ pub struct OracleTwaps { pub twaps: [EmaTwap; MAX_ENTRIES], } +static_assertions::const_assert_eq!(ORACLE_PRICES_SIZE, std::mem::size_of::()); +static_assertions::const_assert_eq!(0, std::mem::size_of::() % 8); // Account to store dated prices #[account(zero_copy)] pub struct OraclePrices { @@ -110,27 +120,112 @@ pub struct OraclePrices { pub prices: [DatedPrice; MAX_ENTRIES], } -// Accounts holding source of prices -#[account(zero_copy)] -pub struct OracleMappings { - pub price_info_accounts: [Pubkey; MAX_ENTRIES], - pub price_types: [u8; MAX_ENTRIES], - pub twap_source: [u16; MAX_ENTRIES], // meaningful only if type == TWAP; the index of where we find the TWAP - pub twap_enabled: [u8; MAX_ENTRIES], // true or false - pub _reserved1: [u8; MAX_ENTRIES], - pub _reserved2: [u32; MAX_ENTRIES], -} - -impl OracleMappings { - pub fn is_twap_enabled(&self, token: usize) -> bool { - self.twap_enabled[token] > 0 +pub mod mapping { + use std::ops::DerefMut; + + use super::*; + + pub mod idl_trick { + use super::*; + #[zero_copy] + pub struct OracleMappingsCore { + pub data: [u8; 19456], + } + + #[account(zero_copy)] + pub struct OracleMappingsForIdl { + pub core: OracleMappingsCore, + pub _reserved0: [u16; MAX_ENTRIES], + pub _reserved1: [u8; MAX_ENTRIES], + } + } + + pub mod core { + use super::*; + #[account(zero_copy)] + #[derive(Debug, AnchorDeserialize)] + pub struct OracleMappings { + pub price_info_accounts: [Pubkey; MAX_ENTRIES], + pub price_types: [u8; MAX_ENTRIES], + pub twap_source: [u16; MAX_ENTRIES], // meaningful only if type == TWAP; the index of where we find the TWAP + pub twap_enabled: [u8; MAX_ENTRIES], // true or false + pub ref_price: [u16; MAX_ENTRIES], // reference price against which we check confidence within 5% + } + } + + pub mod old { + use super::*; + + static_assertions::const_assert_eq!( + ORACLE_MAPPING_SIZE, + std::mem::size_of::() + ); + static_assertions::const_assert_eq!(0, std::mem::size_of::() % 8); + // Accounts holding source of prices + #[account(zero_copy)] + pub struct OracleMappings { + pub core: OracleMappingsCore, + pub _reserved0: [u16; MAX_ENTRIES], + pub _reserved1: [u8; MAX_ENTRIES], + } + } + pub mod extended { + use super::*; + + static_assertions::const_assert_eq!( + ORACLE_MAPPING_EXTENDED_SIZE, + std::mem::size_of::() + ); + static_assertions::const_assert_eq!(0, std::mem::size_of::() % 8); + // Accounts holding source of prices + #[account(zero_copy)] + pub struct OracleMappings { + pub core: OracleMappingsCore, + pub generic: [[u8; 20]; MAX_ENTRIES], + } } - pub fn get_twap_source(&self, token: usize) -> usize { - usize::from(self.twap_source[token]) + impl core::OracleMappings { + pub fn is_twap_enabled(&self, entry_id: usize) -> bool { + self.twap_enabled[entry_id] > 0 + } + + pub fn get_twap_source(&self, entry_id: usize) -> usize { + usize::from(self.twap_source[entry_id]) + } + } + + impl Deref for old::OracleMappings { + type Target = core::OracleMappings; + + fn deref(&self) -> &Self::Target { + &self.core + } + } + + impl Deref for extended::OracleMappings { + type Target = core::OracleMappings; + + fn deref(&self) -> &Self::Target { + &self.core + } + } + + impl DerefMut for old::OracleMappings { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.core + } + } + + impl DerefMut for extended::OracleMappings { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.core + } } } +static_assertions::const_assert_eq!(TOKEN_METADATA_SIZE, std::mem::size_of::()); +static_assertions::const_assert_eq!(0, std::mem::size_of::() % 8); #[account(zero_copy)] pub struct TokenMetadatas { pub metadatas_array: [TokenMetadata; MAX_ENTRIES], @@ -140,10 +235,12 @@ pub struct TokenMetadatas { #[derive(AnchorSerialize, AnchorDeserialize, Debug, PartialEq, Eq, Default)] pub struct TokenMetadata { pub name: [u8; 32], - pub max_age_price_seconds: u64, + pub max_age_price_slots: u64, pub _reserved: [u64; 16], } +static_assertions::const_assert_eq!(CONFIGURATION_SIZE, std::mem::size_of::()); +static_assertions::const_assert_eq!(0, std::mem::size_of::() % 8); // Configuration account of the program #[account(zero_copy)] pub struct Configuration { diff --git a/programs/scope/src/utils/consts.rs b/programs/scope/src/utils/consts.rs new file mode 100644 index 0000000..46c652b --- /dev/null +++ b/programs/scope/src/utils/consts.rs @@ -0,0 +1,6 @@ +pub const CONFIGURATION_SIZE: usize = 10232; +pub const ORACLE_MAPPING_SIZE: usize = 20992; +pub const ORACLE_MAPPING_EXTENDED_SIZE: usize = 29696; +pub const ORACLE_PRICES_SIZE: usize = 28704; +pub const ORACLE_TWAPS_SIZE: usize = 344128; +pub const TOKEN_METADATA_SIZE: usize = 86016; diff --git a/programs/scope/src/utils/mod.rs b/programs/scope/src/utils/mod.rs index 23f6c59..1624782 100644 --- a/programs/scope/src/utils/mod.rs +++ b/programs/scope/src/utils/mod.rs @@ -1,10 +1,11 @@ +pub mod consts; pub mod macros; pub mod math; pub mod pdas; pub mod price_impl; pub mod scope_chain; -use std::cell::Ref; +use std::cell::{Ref, RefMut}; use anchor_lang::{ __private::bytemuck, @@ -67,6 +68,33 @@ pub fn zero_copy_deserialize<'info, T: bytemuck::AnyBitPattern + Discriminator>( ); return Err(ScopeError::InvalidAccountDiscriminator); } + let end = std::mem::size_of::() + 8; + Ok(Ref::map(data, |data| bytemuck::from_bytes(&data[8..end]))) +} + +pub fn zero_copy_deserialize_mut<'info, T: bytemuck::Pod + Discriminator>( + account: &'info AccountInfo, +) -> ScopeResult> { + let data = account.data.try_borrow_mut().unwrap(); - Ok(Ref::map(data, |data| bytemuck::from_bytes(&data[8..]))) + let disc_bytes = data.get(..8).ok_or_else(|| { + msg!( + "Account {:?} does not have enough bytes to be deserialized", + account.key() + ); + ScopeError::UnableToDeserializeAccount + })?; + if disc_bytes != T::discriminator() { + msg!( + "Expected discriminator for account {:?} ({:?}) is different from received {:?}", + account.key(), + T::discriminator(), + disc_bytes + ); + return Err(ScopeError::InvalidAccountDiscriminator); + } + let end = std::mem::size_of::() + 8; + Ok(RefMut::map(data, |data| { + bytemuck::from_bytes_mut(&mut data[8..end]) + })) } diff --git a/programs/scope/src/utils/price_impl.rs b/programs/scope/src/utils/price_impl.rs index e20b9a7..17972cb 100644 --- a/programs/scope/src/utils/price_impl.rs +++ b/programs/scope/src/utils/price_impl.rs @@ -1,9 +1,14 @@ -use decimal_wad::decimal::Decimal; +use decimal_wad::{common::PERCENT_SCALER, decimal::Decimal}; +use solana_program::msg; -use crate::Price; +use crate::{Price, ScopeError}; +use anchor_lang::prelude::*; use super::math::ten_pow; +pub const MAX_REF_RATIO_TOLERANCE_PCT: u64 = 5; +pub const MAX_REF_RATIO_TOLERANCE_SCALED: u64 = MAX_REF_RATIO_TOLERANCE_PCT * PERCENT_SCALER; + impl From for f64 { fn from(val: Price) -> Self { val.value as f64 / 10u64.pow(val.exp as u32) as f64 @@ -24,6 +29,27 @@ impl Price { } } +pub fn check_ref_price_difference(curr_price: Price, ref_price: Price) -> Result<()> { + let ref_price_decimal = Decimal::from(ref_price); + let curr_price_decimal = Decimal::from(curr_price); + let absolute_diff = if ref_price_decimal > curr_price_decimal { + ref_price_decimal - curr_price_decimal + } else { + curr_price_decimal - ref_price_decimal + }; + + if absolute_diff * 100 > ref_price_decimal * MAX_REF_RATIO_TOLERANCE_PCT { + msg!( + "Price diff is too high: absolute_diff {}, tolerance = {}", + absolute_diff, + ref_price_decimal * Decimal::from_percent(MAX_REF_RATIO_TOLERANCE_PCT) + ); + return Err(ScopeError::PriceNotValid.into()); + } + + Ok(()) +} + fn decimal_to_price(decimal: Decimal) -> Price { // this implementation aims to keep as much precision as possible // choose exp to be as big as possible (minimize what is needed for the integer part) diff --git a/programs/switchbord-itf/Cargo.toml b/programs/switchbord-itf/Cargo.toml index 6f00eb8..b5ff27b 100644 --- a/programs/switchbord-itf/Cargo.toml +++ b/programs/switchbord-itf/Cargo.toml @@ -11,11 +11,10 @@ name = "switchboard_itf" [features] no-entrypoint = [] cpi = ["no-entrypoint"] -idl-build = ["anchor-lang/idl-build"] test-bpf = [] debug = [] [dependencies] -anchor-lang = "0.29.0" +anchor-lang = ">=0.28.0" bytemuck = { version = "1.4.0", features = ["derive", "min_const_generics"] } rust_decimal = "1.32.0"