From 07863212c31f72da839467e399e2da34d4a0db3a Mon Sep 17 00:00:00 2001 From: udit-gulati Date: Sun, 2 Jun 2024 20:02:43 +0530 Subject: [PATCH 1/4] add slinky demo contract to fetch prices with checks --- Cargo.lock | 13 ++ contracts/neutron_slinky/.cargo/config | 6 + contracts/neutron_slinky/Cargo.toml | 44 ++++ contracts/neutron_slinky/README.md | 3 + contracts/neutron_slinky/examples/schema.rs | 31 +++ contracts/neutron_slinky/src/contract.rs | 242 ++++++++++++++++++++ contracts/neutron_slinky/src/lib.rs | 4 + contracts/neutron_slinky/src/msg.rs | 28 +++ contracts/neutron_slinky/src/storage.rs | 0 9 files changed, 371 insertions(+) create mode 100644 contracts/neutron_slinky/.cargo/config create mode 100644 contracts/neutron_slinky/Cargo.toml create mode 100644 contracts/neutron_slinky/README.md create mode 100644 contracts/neutron_slinky/examples/schema.rs create mode 100644 contracts/neutron_slinky/src/contract.rs create mode 100644 contracts/neutron_slinky/src/lib.rs create mode 100644 contracts/neutron_slinky/src/msg.rs create mode 100644 contracts/neutron_slinky/src/storage.rs diff --git a/Cargo.lock b/Cargo.lock index 4b55a28..cef10ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -775,6 +775,19 @@ dependencies = [ "serde-json-wasm 1.0.1", ] +[[package]] +name = "neutron_slinky" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw2", + "neutron-sdk", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "neutron_validators_test" version = "0.1.0" diff --git a/contracts/neutron_slinky/.cargo/config b/contracts/neutron_slinky/.cargo/config new file mode 100644 index 0000000..7c11532 --- /dev/null +++ b/contracts/neutron_slinky/.cargo/config @@ -0,0 +1,6 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib --features backtraces" +integration-test = "test --test integration" +schema = "run --example schema" diff --git a/contracts/neutron_slinky/Cargo.toml b/contracts/neutron_slinky/Cargo.toml new file mode 100644 index 0000000..c8f82bd --- /dev/null +++ b/contracts/neutron_slinky/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "neutron_slinky" +version = "0.1.0" +edition = "2021" + + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[profile.release] +opt-level = 3 +debug = false +rpath = false +lto = true +debug-assertions = false +codegen-units = 1 +panic = 'abort' +incremental = false +overflow-checks = true + +[features] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +library = [] + +[dependencies] +cosmwasm-std = "1.3.1" +cw2 = "1.1.0" +schemars = "0.8.10" +serde = { version = "1.0.180", default-features = false, features = ["derive"] } +neutron-sdk = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +cosmwasm-schema = { version = "1.3.1", default-features = false } diff --git a/contracts/neutron_slinky/README.md b/contracts/neutron_slinky/README.md new file mode 100644 index 0000000..a73d1ed --- /dev/null +++ b/contracts/neutron_slinky/README.md @@ -0,0 +1,3 @@ +# Neutron Slinky + +This contract denomstrates using price feeds from `x/oracle` and `x/marketmap` modules. diff --git a/contracts/neutron_slinky/examples/schema.rs b/contracts/neutron_slinky/examples/schema.rs new file mode 100644 index 0000000..4238574 --- /dev/null +++ b/contracts/neutron_slinky/examples/schema.rs @@ -0,0 +1,31 @@ +// Copyright 2022 Neutron +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::env::current_dir; +use std::fs::create_dir_all; + +use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; +use neutron_slinky::msg::{InstantiateMsg, QueryMsg, GetPriceResponse, GetPricesResponse}; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + export_schema(&schema_for!(InstantiateMsg), &out_dir); + export_schema(&schema_for!(QueryMsg), &out_dir); + export_schema(&schema_for!(GetPriceResponse), &out_dir); + export_schema(&schema_for!(GetPricesResponse), &out_dir); +} diff --git a/contracts/neutron_slinky/src/contract.rs b/contracts/neutron_slinky/src/contract.rs new file mode 100644 index 0000000..4f4e260 --- /dev/null +++ b/contracts/neutron_slinky/src/contract.rs @@ -0,0 +1,242 @@ +use cosmwasm_std::{ + entry_point, to_json_binary, Binary, Deps, DepsMut, + Env, MessageInfo, Response, StdError, StdResult, Uint64, Int128, +}; +use cw2::set_contract_version; + +use neutron_sdk::bindings::marketmap::query::{MarketMapQuery, MarketMapResponse, MarketResponse}; +use neutron_sdk::bindings::oracle::types::CurrencyPair; +use neutron_sdk::bindings::{msg::NeutronMsg, query::NeutronQuery}; + +use neutron_sdk::bindings::oracle::query::{ + GetAllCurrencyPairsResponse, GetPriceResponse, GetPricesResponse, OracleQuery, +}; + +use crate::msg::{ + InstantiateMsg, ExecuteMsg, QueryMsg, +}; + +const CONTRACT_NAME: &str = concat!("crates.io:neutron-contracts__", env!("CARGO_PKG_NAME")); +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[entry_point] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: InstantiateMsg, +) -> StdResult { + deps.api.debug("WASMDEBUG: instantiate"); + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + Ok(Response::default()) +} + +#[entry_point] +pub fn execute( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: ExecuteMsg, +) -> StdResult> { + Ok(Default::default()) +} + +#[entry_point] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::GetPrice { + base_symbol, + quote_currency, + max_blocks_old, + } => query_recent_valid_price(deps, env, base_symbol, quote_currency, max_blocks_old), + QueryMsg::GetPrices { + base_symbols, + quote_currency, + max_blocks_old, + } => query_recent_valid_prices(deps, env, base_symbols, quote_currency, max_blocks_old), + } +} + +fn query_recent_valid_price( + deps: Deps, + env: Env, + base_symbol: String, + quote_currency: String, + max_blocks_old: Uint64, +) -> StdResult { + // 1. check if "symbol" in x/oracle and x/marketmap + + let currency_pair: CurrencyPair = CurrencyPair{ + base: base_symbol.clone(), quote: quote_currency.clone(), + }; + + let oracle_currency_pairs_query: OracleQuery = OracleQuery::GetAllCurrencyPairs{}; + let oracle_currency_pairs_response: GetAllCurrencyPairsResponse = deps.querier.query( + &oracle_currency_pairs_query.into(), + )?; + if oracle_currency_pairs_response.currency_pairs.contains(¤cy_pair) == false { + StdError::generic_err(format!( + "Market {base_symbol}, {quote_currency} not found in x/oracle module" + )); + } + + let marketmap_currency_pairs_query: MarketMapQuery = MarketMapQuery::MarketMap{}; + let marketmap_currency_pairs_response: MarketMapResponse = deps.querier.query( + &marketmap_currency_pairs_query.into(), + )?; + if marketmap_currency_pairs_response.market_map.markets.contains_key(&base_symbol.clone()) == false { + StdError::generic_err(format!( + "Market {base_symbol}, {quote_currency} not found in x/marketmap module" + )); + } + + // 2. check if "symbol" enabled in x/marketmap + + let marketmap_market_query: MarketMapQuery = MarketMapQuery::Market{ + currency_pair: currency_pair.clone(), + }; + let marketmap_market_response: MarketResponse = deps.querier.query( + &marketmap_market_query.into(), + )?; + if marketmap_market_response.market.ticker.enabled == false { + StdError::generic_err(format!( + "Market {base_symbol}, {quote_currency} not enabled in x/marketmap module" + )); + } + + // 3. check if block_timestamp is not too old + + // get current_block_height + let current_block_height: u64 = env.block.height; + + let oracle_price_query: OracleQuery = OracleQuery::GetPrice{ + currency_pair: currency_pair.clone(), + }; + let oracle_price_response: GetPriceResponse = deps.querier.query( + &oracle_price_query.into(), + )?; + if (current_block_height - oracle_price_response.price.block_height) > max_blocks_old.u64() { + StdError::generic_err(format!( + "Market {base_symbol}, {quote_currency} price is older than {max_blocks_old} blocks" + )); + } + + // 4. fetch the price from x/oracle module + let market_price: Int128 = oracle_price_response.price.price; + + // 5. make sure the price value is not None + if oracle_price_response.nonce == 0 { + StdError::generic_err(format!( + "Market {base_symbol}, {quote_currency} price is nil" + )); + } + + // 6. return the price as response with proper metadata + Ok( + to_json_binary(&oracle_price_response)? + ) +} + +fn query_recent_valid_prices( + deps: Deps, + env: Env, + base_symbols: Vec, + quote_currency: String, + max_blocks_old: Uint64, +) -> StdResult { + // 1. check if all vec<"symbol"> in x/oracle and x/marketmap + + let currency_pairs: Vec = base_symbols.iter().map(|symbol| CurrencyPair{ + base: symbol.to_string(), + quote: quote_currency.clone(), + }).collect(); + + let oracle_currency_pairs_query: OracleQuery = OracleQuery::GetAllCurrencyPairs{}; + let oracle_currency_pairs_response: GetAllCurrencyPairsResponse = deps.querier.query( + &oracle_currency_pairs_query.into(), + )?; + + let _ = currency_pairs.iter().map( + |curr_pair| + if oracle_currency_pairs_response.currency_pairs.contains(curr_pair) == false { + StdError::generic_err(format!( + "Market {0}, {1} not found in x/oracle module", curr_pair.base, curr_pair.quote, + )); + } + ); + + let marketmap_currency_pairs_query: MarketMapQuery = MarketMapQuery::MarketMap{}; + let marketmap_currency_pairs_response: MarketMapResponse = deps.querier.query( + &marketmap_currency_pairs_query.into(), + )?; + + let _ = currency_pairs.iter().map( + |curr_pair| + if marketmap_currency_pairs_response.market_map.markets.contains_key(&curr_pair.base) == false { + StdError::generic_err(format!( + "Market {0}, {1} not found in x/oracle module", curr_pair.base, curr_pair.quote, + )); + } + ); + + // 2. check if all vec<"symbol"> enabled in x/marketmap + + let _ = currency_pairs.iter().map( + |curr_pair| + if marketmap_currency_pairs_response.market_map.markets.get( + &curr_pair.base + ).unwrap().ticker.enabled == false { + StdError::generic_err(format!( + "Market {0}, {1} not enabled in x/oracle module", curr_pair.base, curr_pair.quote, + )); + } + ); + + // 3. check if block_timestamp is not too old + + // TODO: use GetPrices { currency_pair_ids } when calculation of + // `currency_pair_ids` is known + + // get current_block_height + let current_block_height: u64 = env.block.height; + + let oracle_price_responses: Vec = currency_pairs.iter().map(|curr_pair| + deps.querier.query( + &OracleQuery::GetPrice { + currency_pair: curr_pair.clone(), + }.into(), + ).unwrap() + ).collect(); + + let _ = oracle_price_responses.iter().enumerate().map( + |(index, price_response)| + if (current_block_height - price_response.price.block_height) + > max_blocks_old.u64() { + StdError::generic_err(format!( + "Market {0}, {1} not enabled in x/oracle module", currency_pairs[index].base, currency_pairs[index].quote, + )); + } + ); + + // 4. fetch the price from x/oracle module + let market_prices: Vec = oracle_price_responses.iter().map( + |price_response| price_response.price.price + ).collect(); + + // 5. make sure the price value is not None + let _ = oracle_price_responses.iter().enumerate().map( + |(index, price_response)| + if price_response.nonce == 0 { + StdError::generic_err(format!( + "Market {0}, {1} price is nil", currency_pairs[index].base, currency_pairs[index].quote, + )); + } + ); + + // 6. return the price as response with proper metadata + Ok( + to_json_binary(&GetPricesResponse { + prices: oracle_price_responses, + } + )?) +} diff --git a/contracts/neutron_slinky/src/lib.rs b/contracts/neutron_slinky/src/lib.rs new file mode 100644 index 0000000..2289d25 --- /dev/null +++ b/contracts/neutron_slinky/src/lib.rs @@ -0,0 +1,4 @@ +pub mod contract; + +pub mod msg; +mod storage; diff --git a/contracts/neutron_slinky/src/msg.rs b/contracts/neutron_slinky/src/msg.rs new file mode 100644 index 0000000..79c6519 --- /dev/null +++ b/contracts/neutron_slinky/src/msg.rs @@ -0,0 +1,28 @@ +// use crate::storage::{AcknowledgementResult, IntegrationTestsSudoFailureMock}; +use cosmwasm_std::Uint64; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + /// this query goes to neutron and get stored ICA with a specific query + GetPrice { + base_symbol: String, + quote_currency: String, + max_blocks_old: Uint64, + }, + // this query returns ICA from contract store, which saved from acknowledgement + GetPrices { + base_symbols: Vec, + quote_currency: String, + max_blocks_old: Uint64, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct InstantiateMsg {} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ExecuteMsg {} diff --git a/contracts/neutron_slinky/src/storage.rs b/contracts/neutron_slinky/src/storage.rs new file mode 100644 index 0000000..e69de29 From 7b709c9a639550c681331f49440f4f851c6786eb Mon Sep 17 00:00:00 2001 From: udit-gulati Date: Sun, 2 Jun 2024 20:36:58 +0530 Subject: [PATCH 2/4] remove unused storage.rs from neutron_slinky contract --- contracts/neutron_slinky/src/lib.rs | 1 - contracts/neutron_slinky/src/storage.rs | 0 2 files changed, 1 deletion(-) delete mode 100644 contracts/neutron_slinky/src/storage.rs diff --git a/contracts/neutron_slinky/src/lib.rs b/contracts/neutron_slinky/src/lib.rs index 2289d25..db7c5b6 100644 --- a/contracts/neutron_slinky/src/lib.rs +++ b/contracts/neutron_slinky/src/lib.rs @@ -1,4 +1,3 @@ pub mod contract; pub mod msg; -mod storage; diff --git a/contracts/neutron_slinky/src/storage.rs b/contracts/neutron_slinky/src/storage.rs deleted file mode 100644 index e69de29..0000000 From cac3d7b25b7f3f7adb433fd1f558c625d0eb9ae7 Mon Sep 17 00:00:00 2001 From: udit-gulati Date: Sun, 2 Jun 2024 20:42:21 +0530 Subject: [PATCH 3/4] remove incorrect/unused comments in contract neutron_slinky --- contracts/neutron_slinky/src/msg.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/neutron_slinky/src/msg.rs b/contracts/neutron_slinky/src/msg.rs index 79c6519..0b3e960 100644 --- a/contracts/neutron_slinky/src/msg.rs +++ b/contracts/neutron_slinky/src/msg.rs @@ -6,13 +6,11 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryMsg { - /// this query goes to neutron and get stored ICA with a specific query GetPrice { base_symbol: String, quote_currency: String, max_blocks_old: Uint64, }, - // this query returns ICA from contract store, which saved from acknowledgement GetPrices { base_symbols: Vec, quote_currency: String, From 9a3b6bbd7fe012efc06fcc55872173d270cea872 Mon Sep 17 00:00:00 2001 From: udit-gulati Date: Sun, 2 Jun 2024 21:11:00 +0530 Subject: [PATCH 4/4] add comments to neutron_slinky contract to clearify intermediate steps --- contracts/neutron_slinky/src/contract.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/contracts/neutron_slinky/src/contract.rs b/contracts/neutron_slinky/src/contract.rs index 4f4e260..cadb4f4 100644 --- a/contracts/neutron_slinky/src/contract.rs +++ b/contracts/neutron_slinky/src/contract.rs @@ -66,10 +66,12 @@ fn query_recent_valid_price( ) -> StdResult { // 1. check if "symbol" in x/oracle and x/marketmap + // create a CurrencyPair object let currency_pair: CurrencyPair = CurrencyPair{ base: base_symbol.clone(), quote: quote_currency.clone(), }; + // fetch all supported currency pairs in x/oracle module let oracle_currency_pairs_query: OracleQuery = OracleQuery::GetAllCurrencyPairs{}; let oracle_currency_pairs_response: GetAllCurrencyPairsResponse = deps.querier.query( &oracle_currency_pairs_query.into(), @@ -80,6 +82,7 @@ fn query_recent_valid_price( )); } + // fetch all supported currency pairs in x/marketmap module let marketmap_currency_pairs_query: MarketMapQuery = MarketMapQuery::MarketMap{}; let marketmap_currency_pairs_response: MarketMapResponse = deps.querier.query( &marketmap_currency_pairs_query.into(), @@ -92,12 +95,15 @@ fn query_recent_valid_price( // 2. check if "symbol" enabled in x/marketmap + // fetch market for currency_pair in x/marketmap module let marketmap_market_query: MarketMapQuery = MarketMapQuery::Market{ currency_pair: currency_pair.clone(), }; let marketmap_market_response: MarketResponse = deps.querier.query( &marketmap_market_query.into(), )?; + + // check if currency_pair is enabled if marketmap_market_response.market.ticker.enabled == false { StdError::generic_err(format!( "Market {base_symbol}, {quote_currency} not enabled in x/marketmap module" @@ -109,12 +115,15 @@ fn query_recent_valid_price( // get current_block_height let current_block_height: u64 = env.block.height; + // fetch price for currency_pair from x/oracle module let oracle_price_query: OracleQuery = OracleQuery::GetPrice{ currency_pair: currency_pair.clone(), }; let oracle_price_response: GetPriceResponse = deps.querier.query( &oracle_price_query.into(), )?; + + // check if block_height is not too old if (current_block_height - oracle_price_response.price.block_height) > max_blocks_old.u64() { StdError::generic_err(format!( "Market {base_symbol}, {quote_currency} price is older than {max_blocks_old} blocks"