diff --git a/Cargo.toml b/Cargo.toml index 6e76982..8d56249 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,8 @@ members = [ "contracts/mock_band", "contracts/mock_ojo", "contracts/oracle_router", + "contracts/dshd_oracle", + "contracts/mock_dshd", "contracts/shade_staking_derivatives_oracle", "contracts/shadeswap_market_oracle", "contracts/shadeswap_spot_oracle", diff --git a/contracts/dshd_oracle/Cargo.toml b/contracts/dshd_oracle/Cargo.toml new file mode 100644 index 0000000..0714f6e --- /dev/null +++ b/contracts/dshd_oracle/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "dshd_oracle" +version = "0.2.0" +authors = ["Jackson Swenson "] +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"] + +[features] +default = [] +# for quicker tests, cargo test --lib + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +shade-oracles = { path = "../../packages/shade_oracles", features = ["core"] } +shade-toolkit = { workspace = true } diff --git a/contracts/dshd_oracle/src/contract.rs b/contracts/dshd_oracle/src/contract.rs new file mode 100644 index 0000000..724cc14 --- /dev/null +++ b/contracts/dshd_oracle/src/contract.rs @@ -0,0 +1,213 @@ +use cosmwasm_std::{entry_point, QuerierWrapper, StdError, Uint128, Uint256}; +use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; + +use shade_oracles::better_secret_math::common::exp10; +use shade_oracles::core::{pad_query_result, validate_admin, AdminPermissions}; +use shade_oracles::interfaces::common::{OraclePrice, OracleQuery, PriceResponse}; +use shade_oracles::interfaces::providers::ReferenceData; +use shade_oracles::ssp::Item; +use shade_toolkit::{Contract, Query, BLOCK_SIZE}; + +use crate::{dshd, msg::*}; + +// Key used to query router +pub const UNDERLYING_KEY: &str = "SHD"; +// Key for the "price" (underlying * redemption_rate) +pub const PRICE_KEY: &str = "Shade Derivative"; +// Key for the redemption rate +pub const RATE_KEY: &str = "Shade Derivative Rate"; + +// Storage +const CONFIG: Item = Item::new("config"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> StdResult { + let admin_auth = msg.admin_auth.validate(deps.api)?; + let router = msg.router.validate(deps.api)?; + let dshd = msg.dshd.validate(deps.api)?; + + let config = Config { + admin_auth, + router, + dshd, + enabled: true, + }; + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::default()) +} + +fn require_enabled(config: &Config) -> StdResult<()> { + if !config.enabled { + return Err(StdError::generic_err("Contract is disabled")); + } + Ok(()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> StdResult { + let mut config = CONFIG.load(deps.storage)?; + match msg { + ExecuteMsg::UpdateConfig { + router, + dshd, + admin_auth, + enabled, + } => { + validate_admin( + &deps.querier, + AdminPermissions::OraclesAdmin, + info.sender, + &config.admin_auth, + )?; + let mut resp = Response::default(); + if let Some(router) = router { + config.router = router.validate(deps.api)?; + resp = resp.add_attribute("router", config.router.address.clone()); + } + if let Some(admin_auth) = admin_auth { + config.admin_auth = admin_auth.validate(deps.api)?; + resp = resp.add_attribute("admin_auth", config.admin_auth.address.clone()); + } + if let Some(dshd) = dshd { + config.dshd = dshd.validate(deps.api)?; + resp = resp.add_attribute("dshd", config.dshd.address.clone()); + } + if let Some(enabled) = enabled { + config.enabled = enabled; + resp = resp.add_attribute("enabled", config.enabled.to_string()); + } + CONFIG.save(deps.storage, &config)?; + Ok(resp.add_attribute("action", "update_config")) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + let config = CONFIG.load(deps.storage)?; + require_enabled(&config)?; + pad_query_result( + match msg { + QueryMsg::GetPrice { key } => to_binary(&query_oracle_price_by_key( + &deps, + &env, + key, + &config.router, + &config.dshd, + )?), + QueryMsg::GetPrices { keys } => { + let mut results = vec![]; + for key in keys { + results.push(query_oracle_price_by_key( + &deps, + &env, + key, + &config.router, + &config.dshd, + )?); + } + to_binary(&results) + } + QueryMsg::GetConfig {} => to_binary(&config), + }, + BLOCK_SIZE, + ) +} + +fn query_oracle_price_by_key( + deps: &Deps, + env: &Env, + key: String, + router: &Contract, + dshd: &Contract, +) -> StdResult { + if key == PRICE_KEY.to_string() { + query_price(deps, env, router, dshd) + } else if key == RATE_KEY.to_string() { + query_rate(deps, env, dshd) + } else { + Err(StdError::generic_err(format!( + "Invalid Key, expected one of {}, {}", + PRICE_KEY, RATE_KEY + ))) + } +} + +fn query_price( + deps: &Deps, + env: &Env, + router: &Contract, + dshd: &Contract, +) -> StdResult { + let underlying_price = query_router_price(&router, &deps.querier, UNDERLYING_KEY.to_string())?; + let rate = query_rate(deps, env, dshd)?; + Ok(OraclePrice { + key: PRICE_KEY.to_string(), + data: ReferenceData { + rate: (underlying_price.data.rate * rate.data.rate) + / Uint256::from(exp10(18).as_u128()), + last_updated_base: underlying_price.data.last_updated_base, + last_updated_quote: underlying_price.data.last_updated_quote, + }, + }) +} + +fn query_rate(deps: &Deps, env: &Env, dshd: &Contract) -> StdResult { + let staking_info = query_staking_info(&dshd, &deps.querier)?; + let now = env.block.time.seconds(); + // normalize from 10^6 to 10^18 + Ok(OraclePrice { + key: RATE_KEY.to_string(), + data: ReferenceData { + // price is in utkn (8 decimal SHD), upscaling by 10^10 to get 10^18 + rate: Uint256::from(staking_info.price * Uint128::new(exp10(10).as_u128())), + last_updated_base: now, + last_updated_quote: now, + }, + }) +} + +pub fn query_router_price( + router: &Contract, + querier: &QuerierWrapper, + key: impl Into, +) -> StdResult { + OracleQuery::GetPrice { key: key.into() }.query(querier, router) +} + +pub fn query_staking_info( + dshd: &Contract, + querier: &QuerierWrapper, +) -> StdResult { + match (dshd::QueryMsg::StakingInfo {}.query(querier, dshd)?) { + dshd::QueryResponse::StakingInfo { + unbonding_time, + bonded_shd, + rewards, + total_derivative_token_supply, + price, + fee_info, + status, + } => Ok(dshd::StakingInfoResponse { + unbonding_time, + bonded_shd, + rewards, + total_derivative_token_supply, + price, + fee_info, + status, + }), + } +} diff --git a/contracts/dshd_oracle/src/dshd.rs b/contracts/dshd_oracle/src/dshd.rs new file mode 100644 index 0000000..79956f8 --- /dev/null +++ b/contracts/dshd_oracle/src/dshd.rs @@ -0,0 +1,63 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{entry_point, Addr, QuerierWrapper, StdError, Storage, Uint128, Uint256}; +use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; + +use shade_oracles::better_secret_math::common::exp10; +use shade_oracles::core::{pad_query_result, validate_admin, AdminPermissions, ResponseStatus}; +use shade_oracles::interfaces::common::{OraclePrice, OracleQuery, PriceResponse}; +use shade_oracles::interfaces::providers::ReferenceData; +use shade_oracles::ssp::{Item, Map}; +use shade_toolkit::{Contract, Query, RawContract, BLOCK_SIZE}; + +#[cw_serde] +pub enum QueryMsg { + StakingInfo {}, +} + +impl Query for QueryMsg { + const BLOCK_SIZE: usize = BLOCK_SIZE; +} + +#[cw_serde] +pub struct Fee { + pub rate: u32, + pub decimal_places: u8, +} + +#[cw_serde] +pub struct FeeInfo { + pub staking: Fee, + pub unbonding: Fee, + pub collector: Addr, +} + +#[cw_serde] +pub enum ContractStatusLevel { + NormalRun, + Panicked, + StopAll, +} + +#[cw_serde] +pub enum QueryResponse { + StakingInfo { + unbonding_time: Uint128, + bonded_shd: Uint128, + rewards: Uint128, + total_derivative_token_supply: Uint128, + price: Uint128, + fee_info: FeeInfo, + status: ContractStatusLevel, + }, +} + +#[cw_serde] +pub struct StakingInfoResponse { + pub unbonding_time: Uint128, + pub bonded_shd: Uint128, + pub rewards: Uint128, + pub total_derivative_token_supply: Uint128, + pub price: Uint128, + pub fee_info: FeeInfo, + pub status: ContractStatusLevel, +} diff --git a/contracts/dshd_oracle/src/lib.rs b/contracts/dshd_oracle/src/lib.rs new file mode 100644 index 0000000..e6db38a --- /dev/null +++ b/contracts/dshd_oracle/src/lib.rs @@ -0,0 +1,3 @@ +pub mod contract; +pub mod dshd; +pub mod msg; diff --git a/contracts/dshd_oracle/src/msg.rs b/contracts/dshd_oracle/src/msg.rs new file mode 100644 index 0000000..a90126d --- /dev/null +++ b/contracts/dshd_oracle/src/msg.rs @@ -0,0 +1,35 @@ +use cosmwasm_schema::cw_serde; + +use shade_toolkit::{Contract, RawContract}; + +#[cw_serde] +pub struct Config { + pub router: Contract, + pub dshd: Contract, + pub admin_auth: Contract, + pub enabled: bool, +} + +#[cw_serde] +pub struct InstantiateMsg { + pub router: RawContract, + pub dshd: RawContract, + pub admin_auth: RawContract, +} + +#[cw_serde] +pub enum ExecuteMsg { + UpdateConfig { + router: Option, + dshd: Option, + admin_auth: Option, + enabled: Option, + }, +} + +#[cw_serde] +pub enum QueryMsg { + GetPrice { key: String }, + GetPrices { keys: Vec }, + GetConfig {}, +} diff --git a/contracts/mock_dshd/Cargo.toml b/contracts/mock_dshd/Cargo.toml new file mode 100644 index 0000000..268b22a --- /dev/null +++ b/contracts/mock_dshd/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "mock_dshd" +version = "0.2.0" +authors = ["Jackson Swenson "] +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"] + +[features] +default = [] +# for quicker tests, cargo test --lib + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +shade-oracles = { path = "../../packages/shade_oracles", features = ["core"] } +shade-toolkit = { workspace = true } diff --git a/contracts/mock_dshd/src/contract.rs b/contracts/mock_dshd/src/contract.rs new file mode 100644 index 0000000..67d2643 --- /dev/null +++ b/contracts/mock_dshd/src/contract.rs @@ -0,0 +1,75 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{entry_point, QuerierWrapper, StdError, Uint128, Uint256}; +use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; + +use shade_oracles::better_secret_math::common::exp10; +use shade_oracles::core::{pad_query_result, validate_admin, AdminPermissions}; +use shade_oracles::interfaces::common::{OraclePrice, OracleQuery, PriceResponse}; +use shade_oracles::interfaces::providers::ReferenceData; +use shade_oracles::ssp::Item; +use shade_toolkit::{Contract, Query, BLOCK_SIZE}; + +use crate::msg::*; + +// Storage +const STAKING_INFO: Item = Item::new("staking-info-response"); + +#[cw_serde] +pub struct InstantiateMsg { + pub price: Uint128, +} + +#[cw_serde] +pub enum ExecuteMsg {} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> StdResult { + let staking_info = StakingInfoResponse { + unbonding_time: Uint128::zero(), + bonded_shd: Uint128::zero(), + rewards: Uint128::zero(), + total_derivative_token_supply: Uint128::zero(), + price: msg.price, + fee_info: FeeInfo { + staking: Fee { + rate: 0, + decimal_places: 0, + }, + unbonding: Fee { + rate: 0, + decimal_places: 0, + }, + collector: info.sender, + }, + status: ContractStatusLevel::NormalRun, + }; + + STAKING_INFO.save(deps.storage, &staking_info)?; + + Ok(Response::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> StdResult { + Ok(Response::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + pad_query_result( + match msg { + QueryMsg::StakingInfo {} => to_binary(&STAKING_INFO.load(deps.storage)?), + }, + BLOCK_SIZE, + ) +} diff --git a/contracts/mock_dshd/src/lib.rs b/contracts/mock_dshd/src/lib.rs new file mode 100644 index 0000000..112ecad --- /dev/null +++ b/contracts/mock_dshd/src/lib.rs @@ -0,0 +1,2 @@ +pub mod contract; +pub mod msg; diff --git a/contracts/mock_dshd/src/msg.rs b/contracts/mock_dshd/src/msg.rs new file mode 100644 index 0000000..79956f8 --- /dev/null +++ b/contracts/mock_dshd/src/msg.rs @@ -0,0 +1,63 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{entry_point, Addr, QuerierWrapper, StdError, Storage, Uint128, Uint256}; +use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; + +use shade_oracles::better_secret_math::common::exp10; +use shade_oracles::core::{pad_query_result, validate_admin, AdminPermissions, ResponseStatus}; +use shade_oracles::interfaces::common::{OraclePrice, OracleQuery, PriceResponse}; +use shade_oracles::interfaces::providers::ReferenceData; +use shade_oracles::ssp::{Item, Map}; +use shade_toolkit::{Contract, Query, RawContract, BLOCK_SIZE}; + +#[cw_serde] +pub enum QueryMsg { + StakingInfo {}, +} + +impl Query for QueryMsg { + const BLOCK_SIZE: usize = BLOCK_SIZE; +} + +#[cw_serde] +pub struct Fee { + pub rate: u32, + pub decimal_places: u8, +} + +#[cw_serde] +pub struct FeeInfo { + pub staking: Fee, + pub unbonding: Fee, + pub collector: Addr, +} + +#[cw_serde] +pub enum ContractStatusLevel { + NormalRun, + Panicked, + StopAll, +} + +#[cw_serde] +pub enum QueryResponse { + StakingInfo { + unbonding_time: Uint128, + bonded_shd: Uint128, + rewards: Uint128, + total_derivative_token_supply: Uint128, + price: Uint128, + fee_info: FeeInfo, + status: ContractStatusLevel, + }, +} + +#[cw_serde] +pub struct StakingInfoResponse { + pub unbonding_time: Uint128, + pub bonded_shd: Uint128, + pub rewards: Uint128, + pub total_derivative_token_supply: Uint128, + pub price: Uint128, + pub fee_info: FeeInfo, + pub status: ContractStatusLevel, +} diff --git a/contracts/snip20/src/contract.rs b/contracts/snip20/src/contract.rs index e30914b..34cb7e6 100644 --- a/contracts/snip20/src/contract.rs +++ b/contracts/snip20/src/contract.rs @@ -1,7 +1,7 @@ /// This contract implements SNIP-20 standard: /// https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-20.md use cosmwasm_std::{ - entry_point, to_binary, Addr, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, + to_binary, Addr, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Storage, Uint128, }; use rand::RngCore; @@ -2248,7 +2248,7 @@ mod tests { assert_eq!(constants.admin, Addr::unchecked("admin".to_string())); assert_eq!(constants.symbol, "SECSEC".to_string()); assert_eq!(constants.decimals, 8); - assert_eq!(constants.total_supply_is_public, false); + assert!(!constants.total_supply_is_public); ViewingKey::set(deps.as_mut().storage, "lebron", "lolz fun yay"); let is_vk_correct = ViewingKey::check(&deps.storage, "lebron", "lolz fun yay"); @@ -2285,11 +2285,11 @@ mod tests { assert_eq!(constants.admin, Addr::unchecked("admin".to_string())); assert_eq!(constants.symbol, "SECSEC".to_string()); assert_eq!(constants.decimals, 8); - assert_eq!(constants.total_supply_is_public, false); - assert_eq!(constants.deposit_is_enabled, true); - assert_eq!(constants.redeem_is_enabled, true); - assert_eq!(constants.mint_is_enabled, true); - assert_eq!(constants.burn_is_enabled, true); + assert!(!constants.total_supply_is_public); + assert!(constants.deposit_is_enabled); + assert!(constants.redeem_is_enabled); + assert!(constants.mint_is_enabled); + assert!(constants.burn_is_enabled); ViewingKey::set(deps.as_mut().storage, "lebron", "lolz fun yay"); let is_vk_correct = ViewingKey::check(&deps.storage, "lebron", "lolz fun yay"); @@ -2491,8 +2491,7 @@ mod tests { .into_binary() .unwrap(), funds: vec![], - }) - .into(), + }), reply_on: match id { 0 => ReplyOn::Never, _ => ReplyOn::Always, @@ -2735,7 +2734,7 @@ mod tests { ); let query_result = query(deps.as_ref(), mock_env(), msg); - assert_eq!(query_result.is_err(), true); + assert!(query_result.is_err()); } #[test] @@ -2766,7 +2765,7 @@ mod tests { ); let query_result = query(deps.as_ref(), mock_env(), msg); - assert_eq!(query_result.is_ok(), true); + assert!(query_result.is_ok()); } #[test] @@ -3323,7 +3322,7 @@ mod tests { padding: None, expiration: None, }; - let info = mock_info(*name, &[]); + let info = mock_info(name, &[]); let handle_result = execute(deps.as_mut(), mock_env(), info, handle_msg); assert!( @@ -4579,7 +4578,7 @@ mod tests { name: init_name.clone(), admin: Some(init_admin.into_string()), symbol: init_symbol.clone(), - decimals: init_decimals.clone(), + decimals: init_decimals, initial_balances: Some(vec![InitialBalance { address: "giannis".to_string(), amount: init_supply, @@ -4647,7 +4646,7 @@ mod tests { name: init_name.clone(), admin: Some(init_admin.into_string()), symbol: init_symbol.clone(), - decimals: init_decimals.clone(), + decimals: init_decimals, initial_balances: Some(vec![InitialBalance { address: "giannis".to_string(), amount: init_supply, @@ -4680,11 +4679,11 @@ mod tests { burn_enabled, supported_denoms, } => { - assert_eq!(public_total_supply, true); - assert_eq!(deposit_enabled, false); - assert_eq!(redeem_enabled, false); - assert_eq!(mint_enabled, true); - assert_eq!(burn_enabled, false); + assert!(public_total_supply); + assert!(!deposit_enabled); + assert!(!redeem_enabled); + assert!(mint_enabled); + assert!(!burn_enabled); assert_eq!(supported_denoms.len(), 0); } _ => panic!("unexpected"), @@ -4720,7 +4719,7 @@ mod tests { name: init_name.clone(), admin: Some(init_admin.into_string()), symbol: init_symbol.clone(), - decimals: init_decimals.clone(), + decimals: init_decimals, initial_balances: Some(vec![InitialBalance { address: "giannis".to_string(), amount: init_supply, @@ -4779,7 +4778,7 @@ mod tests { name: init_name.clone(), admin: Some(init_admin.into_string()), symbol: init_symbol.clone(), - decimals: init_decimals.clone(), + decimals: init_decimals, initial_balances: Some(vec![InitialBalance { address: "giannis".to_string(), amount: init_supply, @@ -4838,7 +4837,7 @@ mod tests { name: init_name.clone(), admin: Some(init_admin.into_string()), symbol: init_symbol.clone(), - decimals: init_decimals.clone(), + decimals: init_decimals, initial_balances: Some(vec![InitialBalance { address: "giannis".to_string(), amount: init_supply, @@ -4885,7 +4884,7 @@ mod tests { name: init_name.clone(), admin: Some(init_admin.into_string()), symbol: init_symbol.clone(), - decimals: init_decimals.clone(), + decimals: init_decimals, initial_balances: Some(vec![InitialBalance { address: "giannis".to_string(), amount: init_supply, @@ -5043,7 +5042,6 @@ mod tests { let vk = "key".to_string(); let initial_balances: Vec = (0..num_owners) - .into_iter() .map(|i| InitialBalance { address: format!("owner{}", i), amount: Uint128::new(5000), diff --git a/packages/multi_helpers/src/helpers/mod.rs b/packages/multi_helpers/src/helpers/mod.rs index cd42244..682c618 100644 --- a/packages/multi_helpers/src/helpers/mod.rs +++ b/packages/multi_helpers/src/helpers/mod.rs @@ -2,8 +2,8 @@ use super::*; use cosmwasm_std::{Addr, StdError}; use serde::de::DeserializeOwned; use shade_toolkit::{ - multi::{nanoid::nanoid, AnyResult, MultiTestable, Tester}, - multi_test::{App, AppResponse}, + multi::{nanoid::nanoid, AnyResult, Tester}, + multi_test::{AppResponse}, }; use std::{ fmt::{Debug, Display}, diff --git a/packages/multi_helpers/src/lib.rs b/packages/multi_helpers/src/lib.rs index d0735c9..2f7b434 100644 --- a/packages/multi_helpers/src/lib.rs +++ b/packages/multi_helpers/src/lib.rs @@ -1,6 +1,6 @@ -pub(crate) use cosmwasm_std::{to_binary, Binary, Coin, ContractInfo, StdResult, Uint128}; +pub(crate) use cosmwasm_std::{to_binary, Binary, StdResult, Uint128}; -pub(crate) use shade_toolkit::{ExecuteCallback, InstantiateCallback, Query}; +pub(crate) use shade_toolkit::{Query}; #[cfg(not(target_arch = "wasm32"))] mod helpers; diff --git a/packages/multi_test/Cargo.toml b/packages/multi_test/Cargo.toml index 2a6b513..f74e3cf 100644 --- a/packages/multi_test/Cargo.toml +++ b/packages/multi_test/Cargo.toml @@ -23,6 +23,8 @@ mock_band = { path = "../../contracts/mock_band" } mock_ojo = { path = "../../contracts/mock_ojo" } index_oracle = { path = "../../contracts/index_oracle" } +mock_dshd = { path = "../../contracts/mock_dshd" } +dshd_oracle = { path = "../../contracts/dshd_oracle" } shade_staking_derivatives_oracle = { path = "../../contracts/shade_staking_derivatives_oracle" } stride_staking_derivatives_oracle = { path = "../../contracts/staking_derivatives/stride" } @@ -41,4 +43,4 @@ rstest = { workspace = true } [target.'cfg(not(target_arch="wasm32"))'.dependencies] multi-test-helpers = { path = "../multi_helpers" } -shade-toolkit = { workspace = true, features = ["testing"] } \ No newline at end of file +shade-toolkit = { workspace = true, features = ["testing"] } diff --git a/packages/multi_test/src/harness.rs b/packages/multi_test/src/harness.rs index bd61ac8..22c142d 100644 --- a/packages/multi_test/src/harness.rs +++ b/packages/multi_test/src/harness.rs @@ -11,6 +11,8 @@ create_harness!(band, MockBand, mock_band); create_harness!(ojo, MockOjo, mock_ojo); create_harness!(router, OracleRouter, oracle_router); create_harness!(index, IndexOracle, index_oracle); +create_harness!(dshd, MockDShd, mock_dshd); +create_harness!(dshd_oracle, DShdOracle, dshd_oracle); pub mod dex { pub mod siennaswap { diff --git a/packages/multi_test/src/helpers/dshd.rs b/packages/multi_test/src/helpers/dshd.rs new file mode 100644 index 0000000..30044b1 --- /dev/null +++ b/packages/multi_test/src/helpers/dshd.rs @@ -0,0 +1,271 @@ +use super::*; +use shade_oracles::{ + interfaces::{mock_dshd::msg::*, providers::RawProvider}, + status::ContractStatus, +}; + +create_test_helper!(DShdHelper); + +impl DShdHelper { + pub fn init(user: &User, app: &mut App, price: &Uint128) -> Self { + let contract = user + .init(app, &InstantiateMsg { price }, MockDShd::default(), "dshd") + .unwrap(); + Self(contract) + } + + pub fn query_staking_info( + &self, + app: &App, + keys: Vec, + ) -> StdResult { + QueryMsg::StakingInfo {}.test_query(&self.0, app) + } +} +/* + +#[cfg(test)] +mod test { + use super::*; + use multi_test_helpers::Asserter; + use shade_oracles::unit_test_interface::prices::PricesFixture; + use shade_toolkit::interfaces::admin_auth::AdminPermissions; + + // Tests updating config as admin + #[test] + fn admin_control_test() { + let prices = vec![("USD", 1_00 * 10u128.pow(16))]; + + let TestScenario { + mut app, + router, + admin, + provider, + admin_auth, + .. + } = TestScenario::new(prices); + let user = admin; + let app = &mut app; + + let dshd = DShdHelper::init(user); + let dshd_oracle = DShdOracleHelper::init(user, app, router, dshd, admin_auth); + + /* + let usd_protection = ProtectedKeyInfo::new( + "USD".to_string(), + Decimal256::percent(4), + Uint256::from_u128(1_00 * 10u128.pow(16)), + ); + let protection = vec![usd_protection.clone()]; + + // Set initial oracle key protection. + router + .set_protection(&user, app, protection.clone()) + .unwrap(); + assert!(router.query_price(app, "USD".to_string()).is_ok()); + assert!(router.query_prices(app, vec!["USD".to_string()]).is_ok()); + assert!(router + .query_protected_keys(app) + .unwrap() + .contains(&usd_protection)); + + // Set price to 1.05 which is greater than protection deviation of 4% from 1.00 so should fail. + let prices = vec![("USD", 1_05 * 10u128.pow(16))]; + let prices = OracleCore::create_prices_hashmap(prices).1; + provider.update_band_prices(&user, app, prices, Some(app.block_info().time.seconds())); + assert!(router.query_price(app, "USD".to_string()).is_err()); + assert!(router.query_prices(app, vec!["USD".to_string()]).is_err()); + + // Remove protection and see it works again. + router + .remove_key_protections(&user, app, vec!["USD".to_string()]) + .unwrap(); + assert!(router.query_price(app, "USD".to_string()).is_ok()); + assert!(router.query_prices(app, vec!["USD".to_string()]).is_ok()); + assert!(router.query_protected_keys(app).unwrap().is_empty()); + + // Set protection again. + router.set_protection(&user, app, protection).unwrap(); + + // Create a bot to update the protection price so that its equal to the current price. + let bot = User::new("bot"); + let resp = router.update_protected_keys( + &bot, + app, + vec![("USD".to_string(), Uint256::from_u128(1_05 * 10u128.pow(16)))], + ); + assert!(resp.is_err()); + + admin_auth.register_admin(&user, app, bot.clone().into()); + admin_auth.grant_access( + &user, + app, + bot.clone().into(), + vec![AdminPermissions::OraclesPriceBot.into_string()], + ); + + let resp = router.update_protected_keys( + &bot, + app, + vec![("USD".to_string(), Uint256::from_u128(1_05 * 10u128.pow(16)))], + ); + assert!(resp.is_ok()); + assert!(router.query_price(app, "USD".to_string()).is_ok()); + assert!(router.query_prices(app, vec!["USD".to_string()]).is_ok()); + */ + } + + #[rstest] + #[case(PricesFixture::basic_prices_1())] + #[case(PricesFixture::basic_prices_2())] + fn basic_query_test(#[case] prices: Vec<(&str, u128)>) { + let TestScenario { + app, + router, + keys, + prices, + .. + } = TestScenario::new(prices); + let resp = router.query_prices(&app, keys).unwrap(); + for price in resp { + let p: Uint256 = (*prices.get(price.key()).unwrap()).into(); + assert_eq!(price.data.rate, p); + } + } + + #[test] + fn duplicate_symbol_query_test() { + let TestScenario { + app, + router, + keys, + prices, + .. + } = TestScenario::new(PricesFixture::basic_prices_1()); + let keys = vec![keys[0].clone(), keys[0].clone(), keys[1].clone()]; + let resp = router.query_prices(&app, keys).unwrap(); + for price in resp { + let p: Uint256 = (*prices.get(price.key()).unwrap()).into(); + assert_eq!(price.data.rate, p); + } + } + + #[test] + fn registry_tests() { + let prices = PricesFixture::basic_prices_2(); + let test_prices = prices.clone(); + let random = User::new("random"); + let TestScenario { + mut app, + router, + admin, + keys, + provider, + .. + } = TestScenario::new(prices); + let user = admin; + + assert!(router + .set_status(&random, &mut app, ContractStatus::Deprecated) + .is_err()); + router + .set_status(&user, &mut app, ContractStatus::Deprecated) + .unwrap(); + assert!(router + .query_price(&app, test_prices[0].0.to_string()) + .is_err()); + + // Update config test. + router + .update_config( + &user, + &mut app, + UpdateConfig { + admin_auth: None, + provider: None, + quote_symbol: Some("JPY".to_string()), + }, + ) + .unwrap(); + assert!(router.query_config(&app).unwrap().config.quote_symbol == *"JPY"); + + router + .set_status(&user, &mut app, ContractStatus::Frozen) + .unwrap(); + assert!(router + .update_config( + &user, + &mut app, + UpdateConfig { + admin_auth: None, + provider: None, + quote_symbol: Some("USD".to_string()) + } + ) + .is_err()); + + router + .set_status(&user, &mut app, ContractStatus::Normal) + .unwrap(); + assert!(router + .update_config( + &user, + &mut app, + UpdateConfig { + admin_auth: None, + provider: None, + quote_symbol: Some("USD".to_string()) + } + ) + .is_ok()); + + router + .set_keys(&user, &mut app, provider.clone().into(), keys.clone()) + .unwrap(); + let oracles_resp = router.query_oracles(&app, keys.clone()).unwrap(); + let keys_resp = router.query_keys(&app).unwrap(); + assert_eq!(keys.len(), keys_resp.len()); + assert_eq!(keys.len(), oracles_resp.len()); + for oracle in oracles_resp { + assert_eq!(oracle.oracle, provider.clone().into()); + } + Asserter::equal_vecs(&keys, &keys_resp); + + let keys_to_remove = vec![test_prices[0].0.to_string(), test_prices[1].0.to_string()]; + router + .remove_keys(&user, &mut app, keys_to_remove.clone()) + .unwrap(); + let oracles_resp = router.query_oracles(&app, keys.clone()).unwrap(); + let keys_resp = router.query_keys(&app).unwrap(); + assert_eq!(keys.len() - 2, keys_resp.len()); + for oracle in oracles_resp { + if oracle.key == test_prices[0].0 || oracle.key == test_prices[1].0 { + assert_eq!(oracle.oracle, router.clone().into()); + } else { + assert_eq!(oracle.oracle, provider.clone().into()); + } + } + assert!( + !keys_resp.contains(&test_prices[0].0.to_string()) + && !keys_resp.contains(&test_prices[1].0.to_string()) + ); + let oracle = router.query_oracle(&app, &keys[0].clone()).unwrap(); + assert_eq!(oracle.oracle, router.clone().into()); + + let operations = vec![ + RegistryOperation::SetKeys { + oracle: provider.into(), + keys: keys_to_remove.clone(), + }, + RegistryOperation::RemoveKeys { + keys: keys_to_remove, + }, + ]; + router + .batch_update_registry(&user, &mut app, &operations) + .unwrap(); + let oracle = router.query_oracle(&app, &keys[0].clone()).unwrap(); + assert_eq!(oracle.oracle, router.clone().into()); + } +} +*/ diff --git a/packages/multi_test/src/helpers/dshd_oracle.rs b/packages/multi_test/src/helpers/dshd_oracle.rs new file mode 100644 index 0000000..aae0cde --- /dev/null +++ b/packages/multi_test/src/helpers/dshd_oracle.rs @@ -0,0 +1,300 @@ +use super::{ + App, AppResponse, Contract, OracleRouter, PricesResponse, RawContract, StdResult, User, +}; +use dshd_oracle::msg::*; +use shade_oracles::{interfaces::providers::RawProvider, status::ContractStatus}; + +create_test_helper!(DShdOracleHelper); + +impl DShdOracleHelper { + pub fn init( + user: &User, + app: &mut App, + router: &Contract, + dshd: &Contract, + admin_auth: &Contract, + ) -> Self { + let contract = user + .init( + app, + &InstantiateMsg { + router: router.clone().into(), + dshd: dshd.clone().into(), + admin_auth: admin_auth.clone().into(), + }, + OracleRouter::default(), + "oracle_router", + ) + .unwrap(); + Self(contract) + } + + pub fn update_config( + &self, + sender: &User, + app: &mut App, + router: &Option, + dshd: &Option, + admin_auth: &Option, + enabled: &Option, + ) -> AnyResult { + sender.exec(app, &ExecuteMsg::UpdateConfig(operation), &self.0) + } + + pub fn query_config(&self, app: &App) -> StdResult { + QueryMsg::GetConfig {}.test_query(&self.0, app) + } + + pub fn query_price(&self, app: &App, key: String) -> StdResult { + QueryMsg::GetPrice { key }.test_query(&self.0, app) + } + + pub fn query_prices(&self, app: &App, keys: Vec) -> StdResult { + QueryMsg::GetPrices { keys }.test_query(&self.0, app) + } +} + +#[cfg(test)] +mod test { + use super::*; + use multi_test_helpers::Asserter; + use shade_oracles::unit_test_interface::prices::PricesFixture; + use shade_toolkit::interfaces::admin_auth::AdminPermissions; + + // Tests updating config as admin + #[test] + fn admin_control_test() { + let prices = vec![("USD", 1_00 * 10u128.pow(16))]; + + let TestScenario { + mut app, + router, + admin, + provider, + admin_auth, + .. + } = TestScenario::new(prices); + let user = admin; + let app = &mut app; + + let dshd = DShdHelper::init(user); + let dshd_oracle = DShdOracleHelper::init(user, app, router, dshd, admin_auth); + + /* + let usd_protection = ProtectedKeyInfo::new( + "USD".to_string(), + Decimal256::percent(4), + Uint256::from_u128(1_00 * 10u128.pow(16)), + ); + let protection = vec![usd_protection.clone()]; + + // Set initial oracle key protection. + router + .set_protection(&user, app, protection.clone()) + .unwrap(); + assert!(router.query_price(app, "USD".to_string()).is_ok()); + assert!(router.query_prices(app, vec!["USD".to_string()]).is_ok()); + assert!(router + .query_protected_keys(app) + .unwrap() + .contains(&usd_protection)); + + // Set price to 1.05 which is greater than protection deviation of 4% from 1.00 so should fail. + let prices = vec![("USD", 1_05 * 10u128.pow(16))]; + let prices = OracleCore::create_prices_hashmap(prices).1; + provider.update_band_prices(&user, app, prices, Some(app.block_info().time.seconds())); + assert!(router.query_price(app, "USD".to_string()).is_err()); + assert!(router.query_prices(app, vec!["USD".to_string()]).is_err()); + + // Remove protection and see it works again. + router + .remove_key_protections(&user, app, vec!["USD".to_string()]) + .unwrap(); + assert!(router.query_price(app, "USD".to_string()).is_ok()); + assert!(router.query_prices(app, vec!["USD".to_string()]).is_ok()); + assert!(router.query_protected_keys(app).unwrap().is_empty()); + + // Set protection again. + router.set_protection(&user, app, protection).unwrap(); + + // Create a bot to update the protection price so that its equal to the current price. + let bot = User::new("bot"); + let resp = router.update_protected_keys( + &bot, + app, + vec![("USD".to_string(), Uint256::from_u128(1_05 * 10u128.pow(16)))], + ); + assert!(resp.is_err()); + + admin_auth.register_admin(&user, app, bot.clone().into()); + admin_auth.grant_access( + &user, + app, + bot.clone().into(), + vec![AdminPermissions::OraclesPriceBot.into_string()], + ); + + let resp = router.update_protected_keys( + &bot, + app, + vec![("USD".to_string(), Uint256::from_u128(1_05 * 10u128.pow(16)))], + ); + assert!(resp.is_ok()); + assert!(router.query_price(app, "USD".to_string()).is_ok()); + assert!(router.query_prices(app, vec!["USD".to_string()]).is_ok()); + */ + } + + #[rstest] + #[case(PricesFixture::basic_prices_1())] + #[case(PricesFixture::basic_prices_2())] + fn basic_query_test(#[case] prices: Vec<(&str, u128)>) { + let TestScenario { + app, + router, + keys, + prices, + .. + } = TestScenario::new(prices); + let resp = router.query_prices(&app, keys).unwrap(); + for price in resp { + let p: Uint256 = (*prices.get(price.key()).unwrap()).into(); + assert_eq!(price.data.rate, p); + } + } + + #[test] + fn duplicate_symbol_query_test() { + let TestScenario { + app, + router, + keys, + prices, + .. + } = TestScenario::new(PricesFixture::basic_prices_1()); + let keys = vec![keys[0].clone(), keys[0].clone(), keys[1].clone()]; + let resp = router.query_prices(&app, keys).unwrap(); + for price in resp { + let p: Uint256 = (*prices.get(price.key()).unwrap()).into(); + assert_eq!(price.data.rate, p); + } + } + + #[test] + fn registry_tests() { + let prices = PricesFixture::basic_prices_2(); + let test_prices = prices.clone(); + let random = User::new("random"); + let TestScenario { + mut app, + router, + admin, + keys, + provider, + .. + } = TestScenario::new(prices); + let user = admin; + + assert!(router + .set_status(&random, &mut app, ContractStatus::Deprecated) + .is_err()); + router + .set_status(&user, &mut app, ContractStatus::Deprecated) + .unwrap(); + assert!(router + .query_price(&app, test_prices[0].0.to_string()) + .is_err()); + + // Update config test. + router + .update_config( + &user, + &mut app, + UpdateConfig { + admin_auth: None, + provider: None, + quote_symbol: Some("JPY".to_string()), + }, + ) + .unwrap(); + assert!(router.query_config(&app).unwrap().config.quote_symbol == *"JPY"); + + router + .set_status(&user, &mut app, ContractStatus::Frozen) + .unwrap(); + assert!(router + .update_config( + &user, + &mut app, + UpdateConfig { + admin_auth: None, + provider: None, + quote_symbol: Some("USD".to_string()) + } + ) + .is_err()); + + router + .set_status(&user, &mut app, ContractStatus::Normal) + .unwrap(); + assert!(router + .update_config( + &user, + &mut app, + UpdateConfig { + admin_auth: None, + provider: None, + quote_symbol: Some("USD".to_string()) + } + ) + .is_ok()); + + router + .set_keys(&user, &mut app, provider.clone().into(), keys.clone()) + .unwrap(); + let oracles_resp = router.query_oracles(&app, keys.clone()).unwrap(); + let keys_resp = router.query_keys(&app).unwrap(); + assert_eq!(keys.len(), keys_resp.len()); + assert_eq!(keys.len(), oracles_resp.len()); + for oracle in oracles_resp { + assert_eq!(oracle.oracle, provider.clone().into()); + } + Asserter::equal_vecs(&keys, &keys_resp); + + let keys_to_remove = vec![test_prices[0].0.to_string(), test_prices[1].0.to_string()]; + router + .remove_keys(&user, &mut app, keys_to_remove.clone()) + .unwrap(); + let oracles_resp = router.query_oracles(&app, keys.clone()).unwrap(); + let keys_resp = router.query_keys(&app).unwrap(); + assert_eq!(keys.len() - 2, keys_resp.len()); + for oracle in oracles_resp { + if oracle.key == test_prices[0].0 || oracle.key == test_prices[1].0 { + assert_eq!(oracle.oracle, router.clone().into()); + } else { + assert_eq!(oracle.oracle, provider.clone().into()); + } + } + assert!( + !keys_resp.contains(&test_prices[0].0.to_string()) + && !keys_resp.contains(&test_prices[1].0.to_string()) + ); + let oracle = router.query_oracle(&app, &keys[0].clone()).unwrap(); + assert_eq!(oracle.oracle, router.clone().into()); + + let operations = vec![ + RegistryOperation::SetKeys { + oracle: provider.into(), + keys: keys_to_remove.clone(), + }, + RegistryOperation::RemoveKeys { + keys: keys_to_remove, + }, + ]; + router + .batch_update_registry(&user, &mut app, &operations) + .unwrap(); + let oracle = router.query_oracle(&app, &keys[0].clone()).unwrap(); + assert_eq!(oracle.oracle, router.clone().into()); + } +} diff --git a/packages/multi_test/src/helpers/mod.rs b/packages/multi_test/src/helpers/mod.rs index 7287a8b..2c88b3a 100644 --- a/packages/multi_test/src/helpers/mod.rs +++ b/packages/multi_test/src/helpers/mod.rs @@ -8,6 +8,8 @@ pub(crate) use rstest::*; pub mod common; pub mod derivatives; pub mod dex; +pub mod dshd; +pub mod dshd_oracle; pub mod index; pub mod router; pub use common::*;