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..cadb4f4 --- /dev/null +++ b/contracts/neutron_slinky/src/contract.rs @@ -0,0 +1,251 @@ +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 + + // 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(), + )?; + 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" + )); + } + + // 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(), + )?; + 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 + + // 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" + )); + } + + // 3. check if block_timestamp is not too old + + // 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" + )); + } + + // 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..db7c5b6 --- /dev/null +++ b/contracts/neutron_slinky/src/lib.rs @@ -0,0 +1,3 @@ +pub mod contract; + +pub mod msg; diff --git a/contracts/neutron_slinky/src/msg.rs b/contracts/neutron_slinky/src/msg.rs new file mode 100644 index 0000000..0b3e960 --- /dev/null +++ b/contracts/neutron_slinky/src/msg.rs @@ -0,0 +1,26 @@ +// 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 { + GetPrice { + base_symbol: String, + quote_currency: String, + max_blocks_old: Uint64, + }, + 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 {}