From ba31eaaf0d0331f1aebc3178d8f838f872d6ad5a Mon Sep 17 00:00:00 2001 From: Pacman Date: Mon, 19 Jun 2023 19:54:23 +0200 Subject: [PATCH 1/9] chore: audit checklist changes --- contracts/incentives/src/state.rs | 1 - packages/testing/src/incentives_querier.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/contracts/incentives/src/state.rs b/contracts/incentives/src/state.rs index 3b6b0820..b8f9ede9 100644 --- a/contracts/incentives/src/state.rs +++ b/contracts/incentives/src/state.rs @@ -43,7 +43,6 @@ pub const USER_UNCLAIMED_REWARDS: Map<(&Addr, &str, &str), Uint128> = Map::new(" pub const DEFAULT_LIMIT: u32 = 5; /// The maximum limit for pagination -/// TODO: Remove MAX_LIMIT? What is the purpose? Surely better to have the limit be whatever is the max gas limit? pub const MAX_LIMIT: u32 = 10; /// Helper function to update unclaimed rewards for a given user, collateral denom and incentive diff --git a/packages/testing/src/incentives_querier.rs b/packages/testing/src/incentives_querier.rs index 315190b0..55421b3c 100644 --- a/packages/testing/src/incentives_querier.rs +++ b/packages/testing/src/incentives_querier.rs @@ -36,7 +36,6 @@ impl IncentivesQuerier { start_after_incentive_denom: _, limit: _, } => { - // TODO: implement pagination let unclaimed_rewards = self .unclaimed_rewards_at .iter() From e4164b0ae0040e7f06fc0a1a8813948c52490096 Mon Sep 17 00:00:00 2001 From: Sturdy <91910406+apollo-sturdy@users.noreply.github.com> Date: Mon, 19 Jun 2023 20:48:26 +0200 Subject: [PATCH 2/9] docs: remove unused todo comment --- contracts/oracle/wasm/src/price_source.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/oracle/wasm/src/price_source.rs b/contracts/oracle/wasm/src/price_source.rs index bd44ba20..8682f0fd 100644 --- a/contracts/oracle/wasm/src/price_source.rs +++ b/contracts/oracle/wasm/src/price_source.rs @@ -174,8 +174,6 @@ impl PriceSourceUnchecked for WasmPriceSourceUnch &route_assets, )?; - //TODO: Validate window_size and tolerance? - Ok(WasmPriceSourceChecked::AstroportTwap { pair_address: deps.api.addr_validate(&pair_address)?, window_size, From aa4b29a6191edbc5620dbdc131a05137c8687353 Mon Sep 17 00:00:00 2001 From: Pacman Date: Mon, 19 Jun 2023 20:50:45 +0200 Subject: [PATCH 3/9] feat: update integration-test make target --- Makefile.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.toml b/Makefile.toml index 6d305761..98cb3fbc 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -41,7 +41,7 @@ args = ["test", "--locked", "--workspace", "--exclude", "mars-integration-tests" [tasks.integration-test] toolchain = "stable" command = "cargo" -args = ["test", "--locked", "--package", "mars-integration-tests"] +args = ["test", "--locked", "-p", "mars-integration-tests", "-p", "mars-swapper-astroport", "-p", "mars-oracle-wasm", "--test", "*"] [tasks.fmt] toolchain = "nightly" From 99d8c1886273298542d5e719075a454d58dbd40f Mon Sep 17 00:00:00 2001 From: Pacman Date: Mon, 19 Jun 2023 21:13:01 +0200 Subject: [PATCH 4/9] chore: update unit-test make target --- Makefile.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.toml b/Makefile.toml index 98cb3fbc..d76df5c8 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -36,7 +36,7 @@ args = ["test", "--locked"] [tasks.unit-test] toolchain = "stable" command = "cargo" -args = ["test", "--locked", "--workspace", "--exclude", "mars-integration-tests"] +args = ["test", "--locked", "--workspace", "--exclude", "mars-integration-tests", "--exclude", "mars-swapper-astroport", "--exclude", "mars-oracle-wasm", "--exclude", "mars-swapper-osmosis"] [tasks.integration-test] toolchain = "stable" From 617076a392ea847f174871c88379ee1079c91d89 Mon Sep 17 00:00:00 2001 From: Pacman Date: Mon, 19 Jun 2023 21:16:58 +0200 Subject: [PATCH 5/9] chore: add osmosis-swapper to integration-test make target --- Makefile.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.toml b/Makefile.toml index d76df5c8..7b92c848 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -41,7 +41,7 @@ args = ["test", "--locked", "--workspace", "--exclude", "mars-integration-tests" [tasks.integration-test] toolchain = "stable" command = "cargo" -args = ["test", "--locked", "-p", "mars-integration-tests", "-p", "mars-swapper-astroport", "-p", "mars-oracle-wasm", "--test", "*"] +args = ["test", "--locked", "-p", "mars-integration-tests", "-p", "mars-swapper-astroport", "-p", "mars-oracle-wasm", "-p", "mars-swapper-osmosis", "--test", "*"] [tasks.fmt] toolchain = "nightly" From 34f28cdf206f0f8d725df57ee972633ffb00f2a5 Mon Sep 17 00:00:00 2001 From: pacmanifold <105084485+pacmanifold@users.noreply.github.com> Date: Tue, 20 Jun 2023 15:18:43 +0200 Subject: [PATCH 6/9] Revert "refactor: remove params contract" (#216) This reverts commit 3006045931d3150a5bc866ebc49ec5d9b8f4acf1. --- Cargo.lock | 17 + Cargo.toml | 1 + contracts/params/Cargo.toml | 35 + contracts/params/README.md | 16 + contracts/params/examples/schema.rs | 10 + contracts/params/src/contract.rs | 95 ++ contracts/params/src/emergency_powers.rs | 82 ++ contracts/params/src/error.rs | 18 + contracts/params/src/execute.rs | 118 ++ contracts/params/src/lib.rs | 8 + contracts/params/src/msg.rs | 87 ++ contracts/params/src/query.rs | 51 + contracts/params/src/state.rs | 10 + contracts/params/src/types/asset.rs | 83 ++ contracts/params/src/types/hls.rs | 82 ++ contracts/params/src/types/mod.rs | 3 + contracts/params/src/types/vault.rs | 59 + contracts/params/tests/helpers/assertions.rs | 25 + contracts/params/tests/helpers/contracts.rs | 11 + contracts/params/tests/helpers/generator.rs | 36 + contracts/params/tests/helpers/mock_env.rs | 240 +++ contracts/params/tests/helpers/mod.rs | 6 + .../params/tests/test_asset_validation.rs | 225 +++ contracts/params/tests/test_close_factor.rs | 65 + .../params/tests/test_emergency_powers.rs | 158 ++ contracts/params/tests/test_owner.rs | 47 + .../params/tests/test_update_asset_params.rs | 251 ++++ .../params/tests/test_vault_validation.rs | 174 +++ contracts/params/tests/test_vaults.rs | 227 +++ schema.Makefile.toml | 1 + schemas/mars-params/mars-params.json | 1308 +++++++++++++++++ scripts/deploy/base/deployer.ts | 9 + scripts/deploy/base/index.ts | 1 + scripts/deploy/neutron/config.ts | 3 +- .../mars-params/MarsParams.client.ts | 262 ++++ .../mars-params/MarsParams.react-query.ts | 294 ++++ .../generated/mars-params/MarsParams.types.ts | 195 +++ scripts/types/generated/mars-params/bundle.ts | 13 + .../types/generated/mars-red-bank/bundle.ts | 8 +- .../mars-rewards-collector/bundle.ts | 8 +- .../mars-swapper-astroport/bundle.ts | 8 +- .../generated/mars-swapper-osmosis/bundle.ts | 8 +- scripts/types/msg.ts | 2 + 43 files changed, 4342 insertions(+), 18 deletions(-) create mode 100644 contracts/params/Cargo.toml create mode 100644 contracts/params/README.md create mode 100644 contracts/params/examples/schema.rs create mode 100644 contracts/params/src/contract.rs create mode 100644 contracts/params/src/emergency_powers.rs create mode 100644 contracts/params/src/error.rs create mode 100644 contracts/params/src/execute.rs create mode 100644 contracts/params/src/lib.rs create mode 100644 contracts/params/src/msg.rs create mode 100644 contracts/params/src/query.rs create mode 100644 contracts/params/src/state.rs create mode 100644 contracts/params/src/types/asset.rs create mode 100644 contracts/params/src/types/hls.rs create mode 100644 contracts/params/src/types/mod.rs create mode 100644 contracts/params/src/types/vault.rs create mode 100644 contracts/params/tests/helpers/assertions.rs create mode 100644 contracts/params/tests/helpers/contracts.rs create mode 100644 contracts/params/tests/helpers/generator.rs create mode 100644 contracts/params/tests/helpers/mock_env.rs create mode 100644 contracts/params/tests/helpers/mod.rs create mode 100644 contracts/params/tests/test_asset_validation.rs create mode 100644 contracts/params/tests/test_close_factor.rs create mode 100644 contracts/params/tests/test_emergency_powers.rs create mode 100644 contracts/params/tests/test_owner.rs create mode 100644 contracts/params/tests/test_update_asset_params.rs create mode 100644 contracts/params/tests/test_vault_validation.rs create mode 100644 contracts/params/tests/test_vaults.rs create mode 100644 schemas/mars-params/mars-params.json create mode 100644 scripts/types/generated/mars-params/MarsParams.client.ts create mode 100644 scripts/types/generated/mars-params/MarsParams.react-query.ts create mode 100644 scripts/types/generated/mars-params/MarsParams.types.ts create mode 100644 scripts/types/generated/mars-params/bundle.ts diff --git a/Cargo.lock b/Cargo.lock index 004545de..c80456e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1877,6 +1877,23 @@ dependencies = [ "thiserror", ] +[[package]] +name = "mars-params" +version = "1.1.0" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test 0.16.5", + "cw-storage-plus 1.1.0", + "cw2 1.0.1 (git+https://github.com/CosmWasm/cw-plus?rev=de1fb0b)", + "mars-owner", + "mars-utils", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "mars-red-bank" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 247e4147..d09fd7d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "contracts/address-provider", "contracts/incentives", "contracts/oracle/*", + "contracts/params", "contracts/swapper/*", "contracts/red-bank", "contracts/rewards-collector", diff --git a/contracts/params/Cargo.toml b/contracts/params/Cargo.toml new file mode 100644 index 00000000..b5ee0b97 --- /dev/null +++ b/contracts/params/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "mars-params" +description = "Contract storing the asset params for Credit Manager and Red Bank." +version = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +keywords = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +library = [] + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw2 = { workspace = true } +cw-storage-plus = { workspace = true } +mars-owner = { workspace = true } +mars-utils = { workspace = true } +schemars = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +anyhow = { workspace = true } +cw-multi-test = { workspace = true } diff --git a/contracts/params/README.md b/contracts/params/README.md new file mode 100644 index 00000000..ebb813f4 --- /dev/null +++ b/contracts/params/README.md @@ -0,0 +1,16 @@ +# Mars Params Contract + +The Mars Params Contract is published to [Crates.io](https://crates.io/crates/mars-params) + +This contract holds the following values for all the assets in Mars Protocol: + +- **Max Loan To Value:** Max percentage of collateral that can be borrowed +- **Liquidation Threshold:** LTV at which the loan is defined as under collateralized and can be liquidated +- **Liquidation Bonus:** Percentage of extra collateral the liquidator gets as a bonus +- **Deposit Enabled:** Is the asset able to be deposited into the Red Bank +- **Borrow Enabled:** Is the asset able to be borrowed from the Red Bank +- **Deposit Cap:** Max amount that can be deposited into the Red Bank +- **Asset Settings:** Credit Manager and Red Bank Permission Settings + +Note: Credit Manager Vaults only utilize max loan to value, liquidation threshold, and deposit cap parameters, while Red Bank Markets utilize all of the above parameters. + diff --git a/contracts/params/examples/schema.rs b/contracts/params/examples/schema.rs new file mode 100644 index 00000000..29e8f6f7 --- /dev/null +++ b/contracts/params/examples/schema.rs @@ -0,0 +1,10 @@ +use cosmwasm_schema::write_api; +use mars_params::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg, + } +} diff --git a/contracts/params/src/contract.rs b/contracts/params/src/contract.rs new file mode 100644 index 00000000..87f73c63 --- /dev/null +++ b/contracts/params/src/contract.rs @@ -0,0 +1,95 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response}; +use cw2::set_contract_version; +use mars_owner::OwnerInit::SetInitialOwner; + +use crate::{ + emergency_powers::{disable_borrowing, disallow_coin, set_zero_deposit_cap, set_zero_max_ltv}, + error::ContractResult, + execute::{assert_mcf, update_asset_params, update_max_close_factor, update_vault_config}, + msg::{ + CmEmergencyUpdate, EmergencyUpdate, ExecuteMsg, InstantiateMsg, QueryMsg, + RedBankEmergencyUpdate, + }, + query::{query_all_asset_params, query_all_vault_configs, query_vault_config}, + state::{ASSET_PARAMS, MAX_CLOSE_FACTOR, OWNER}, +}; + +const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _: Env, + _: MessageInfo, + msg: InstantiateMsg, +) -> ContractResult { + set_contract_version(deps.storage, format!("crates.io:{CONTRACT_NAME}"), CONTRACT_VERSION)?; + + OWNER.initialize( + deps.storage, + deps.api, + SetInitialOwner { + owner: msg.owner, + }, + )?; + + assert_mcf(msg.max_close_factor)?; + MAX_CLOSE_FACTOR.save(deps.storage, &msg.max_close_factor)?; + + Ok(Response::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + _: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> ContractResult { + match msg { + ExecuteMsg::UpdateOwner(update) => Ok(OWNER.update(deps, info, update)?), + ExecuteMsg::UpdateAssetParams(update) => update_asset_params(deps, info, update), + ExecuteMsg::UpdateMaxCloseFactor(mcf) => update_max_close_factor(deps, info, mcf), + ExecuteMsg::UpdateVaultConfig(update) => update_vault_config(deps, info, update), + ExecuteMsg::EmergencyUpdate(update) => match update { + EmergencyUpdate::RedBank(rb_u) => match rb_u { + RedBankEmergencyUpdate::DisableBorrowing(denom) => { + disable_borrowing(deps, info, &denom) + } + }, + EmergencyUpdate::CreditManager(rv_u) => match rv_u { + CmEmergencyUpdate::DisallowCoin(denom) => disallow_coin(deps, info, &denom), + CmEmergencyUpdate::SetZeroMaxLtvOnVault(v) => set_zero_max_ltv(deps, info, &v), + CmEmergencyUpdate::SetZeroDepositCapOnVault(v) => { + set_zero_deposit_cap(deps, info, &v) + } + }, + }, + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> ContractResult { + let res = match msg { + QueryMsg::Owner {} => to_binary(&OWNER.query(deps.storage)?), + QueryMsg::AssetParams { + denom, + } => to_binary(&ASSET_PARAMS.load(deps.storage, &denom)?), + QueryMsg::AllAssetParams { + start_after, + limit, + } => to_binary(&query_all_asset_params(deps, start_after, limit)?), + QueryMsg::VaultConfig { + address, + } => to_binary(&query_vault_config(deps, &address)?), + QueryMsg::AllVaultConfigs { + start_after, + limit, + } => to_binary(&query_all_vault_configs(deps, start_after, limit)?), + QueryMsg::MaxCloseFactor {} => to_binary(&MAX_CLOSE_FACTOR.load(deps.storage)?), + }; + res.map_err(Into::into) +} diff --git a/contracts/params/src/emergency_powers.rs b/contracts/params/src/emergency_powers.rs new file mode 100644 index 00000000..982b6954 --- /dev/null +++ b/contracts/params/src/emergency_powers.rs @@ -0,0 +1,82 @@ +use cosmwasm_std::{Decimal, DepsMut, MessageInfo, Response, Uint128}; + +use crate::{ + error::ContractError, + state::{ASSET_PARAMS, OWNER, VAULT_CONFIGS}, +}; + +pub fn disable_borrowing( + deps: DepsMut, + info: MessageInfo, + denom: &str, +) -> Result { + OWNER.assert_emergency_owner(deps.storage, &info.sender)?; + + let mut params = ASSET_PARAMS.load(deps.storage, denom)?; + params.red_bank.borrow_enabled = false; + ASSET_PARAMS.save(deps.storage, denom, ¶ms)?; + + let response = Response::new() + .add_attribute("action", "emergency_disable_borrowing") + .add_attribute("denom", denom.to_string()); + + Ok(response) +} + +pub fn disallow_coin( + deps: DepsMut, + info: MessageInfo, + denom: &str, +) -> Result { + OWNER.assert_emergency_owner(deps.storage, &info.sender)?; + + let mut params = ASSET_PARAMS.load(deps.storage, denom)?; + params.credit_manager.whitelisted = false; + ASSET_PARAMS.save(deps.storage, denom, ¶ms)?; + + let response = Response::new() + .add_attribute("action", "emergency_disallow_coin") + .add_attribute("denom", denom.to_string()); + + Ok(response) +} + +pub fn set_zero_max_ltv( + deps: DepsMut, + info: MessageInfo, + vault: &str, +) -> Result { + OWNER.assert_emergency_owner(deps.storage, &info.sender)?; + + let vault_addr = deps.api.addr_validate(vault)?; + + let mut config = VAULT_CONFIGS.load(deps.storage, &vault_addr)?; + config.max_loan_to_value = Decimal::zero(); + VAULT_CONFIGS.save(deps.storage, &vault_addr, &config)?; + + let response = Response::new() + .add_attribute("action", "emergency_set_zero_max_ltv") + .add_attribute("vault", vault.to_string()); + + Ok(response) +} + +pub fn set_zero_deposit_cap( + deps: DepsMut, + info: MessageInfo, + vault: &str, +) -> Result { + OWNER.assert_emergency_owner(deps.storage, &info.sender)?; + + let vault_addr = deps.api.addr_validate(vault)?; + + let mut config = VAULT_CONFIGS.load(deps.storage, &vault_addr)?; + config.deposit_cap.amount = Uint128::zero(); + VAULT_CONFIGS.save(deps.storage, &vault_addr, &config)?; + + let response = Response::new() + .add_attribute("action", "emergency_set_zero_deposit_cap") + .add_attribute("vault", vault.to_string()); + + Ok(response) +} diff --git a/contracts/params/src/error.rs b/contracts/params/src/error.rs new file mode 100644 index 00000000..b19167c5 --- /dev/null +++ b/contracts/params/src/error.rs @@ -0,0 +1,18 @@ +use cosmwasm_std::StdError; +use mars_owner::OwnerError; +pub use mars_utils::error::ValidationError; +use thiserror::Error; + +pub type ContractResult = Result; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + Owner(#[from] OwnerError), + + #[error("{0}")] + Validation(#[from] ValidationError), +} diff --git a/contracts/params/src/execute.rs b/contracts/params/src/execute.rs new file mode 100644 index 00000000..bff2a7d9 --- /dev/null +++ b/contracts/params/src/execute.rs @@ -0,0 +1,118 @@ +use cosmwasm_std::{Decimal, DepsMut, MessageInfo, Response}; +use mars_utils::error::ValidationError; + +use crate::{ + error::ContractResult, + msg::{AssetParamsUpdate, VaultConfigUpdate}, + state::{ASSET_PARAMS, MAX_CLOSE_FACTOR, OWNER, VAULT_CONFIGS}, +}; + +pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub fn update_max_close_factor( + deps: DepsMut, + info: MessageInfo, + max_close_factor: Decimal, +) -> ContractResult { + OWNER.assert_owner(deps.storage, &info.sender)?; + + assert_mcf(max_close_factor)?; + MAX_CLOSE_FACTOR.save(deps.storage, &max_close_factor)?; + + let response = Response::new() + .add_attribute("action", "update_max_close_factor") + .add_attribute("value", max_close_factor.to_string()); + + Ok(response) +} + +pub fn update_asset_params( + deps: DepsMut, + info: MessageInfo, + update: AssetParamsUpdate, +) -> ContractResult { + OWNER.assert_owner(deps.storage, &info.sender)?; + + let mut response = Response::new().add_attribute("action", "update_asset_param"); + + match update { + AssetParamsUpdate::AddOrUpdate { + params: unchecked, + } => { + let params = unchecked.check(deps.api)?; + + ASSET_PARAMS.save(deps.storage, ¶ms.denom, ¶ms)?; + response = response + .add_attribute("action_type", "add_or_update") + .add_attribute("denom", params.denom); + } + } + + Ok(response) +} + +pub fn update_vault_config( + deps: DepsMut, + info: MessageInfo, + update: VaultConfigUpdate, +) -> ContractResult { + OWNER.assert_owner(deps.storage, &info.sender)?; + + let mut response = Response::new().add_attribute("action", "update_vault_config"); + + match update { + VaultConfigUpdate::AddOrUpdate { + config, + } => { + let checked = config.check(deps.api)?; + VAULT_CONFIGS.save(deps.storage, &checked.addr, &checked)?; + response = response + .add_attribute("action_type", "add_or_update") + .add_attribute("addr", checked.addr); + } + } + + Ok(response) +} + +pub fn assert_mcf(param_value: Decimal) -> Result<(), ValidationError> { + if !param_value.le(&Decimal::one()) { + Err(ValidationError::InvalidParam { + param_name: "max-close-factor".to_string(), + invalid_value: "max-close-factor".to_string(), + predicate: "<= 1".to_string(), + }) + } else { + Ok(()) + } +} + +/// liquidation_threshold should be greater than or equal to max_loan_to_value +pub fn assert_lqt_gt_max_ltv( + max_ltv: Decimal, + liq_threshold: Decimal, +) -> Result<(), ValidationError> { + if liq_threshold <= max_ltv { + return Err(ValidationError::InvalidParam { + param_name: "liquidation_threshold".to_string(), + invalid_value: liq_threshold.to_string(), + predicate: format!("> {} (max LTV)", max_ltv), + }); + } + Ok(()) +} + +pub fn assert_hls_lqt_gt_max_ltv( + max_ltv: Decimal, + liq_threshold: Decimal, +) -> Result<(), ValidationError> { + if liq_threshold <= max_ltv { + return Err(ValidationError::InvalidParam { + param_name: "hls_liquidation_threshold".to_string(), + invalid_value: liq_threshold.to_string(), + predicate: format!("> {} (hls max LTV)", max_ltv), + }); + } + Ok(()) +} diff --git a/contracts/params/src/lib.rs b/contracts/params/src/lib.rs new file mode 100644 index 00000000..2f486c2c --- /dev/null +++ b/contracts/params/src/lib.rs @@ -0,0 +1,8 @@ +pub mod contract; +pub mod emergency_powers; +pub mod error; +pub mod execute; +pub mod msg; +pub mod query; +pub mod state; +pub mod types; diff --git a/contracts/params/src/msg.rs b/contracts/params/src/msg.rs new file mode 100644 index 00000000..3029293f --- /dev/null +++ b/contracts/params/src/msg.rs @@ -0,0 +1,87 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Decimal; +use mars_owner::OwnerUpdate; + +use crate::types::{asset::AssetParamsUnchecked, vault::VaultConfigUnchecked}; + +#[cw_serde] +pub struct InstantiateMsg { + /// Contract's owner + pub owner: String, + /// The maximum percent a liquidator can decrease the debt amount of the liquidatee + pub max_close_factor: Decimal, +} + +#[cw_serde] +pub enum ExecuteMsg { + UpdateOwner(OwnerUpdate), + UpdateMaxCloseFactor(Decimal), + UpdateAssetParams(AssetParamsUpdate), + UpdateVaultConfig(VaultConfigUpdate), + EmergencyUpdate(EmergencyUpdate), +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(mars_owner::OwnerResponse)] + Owner {}, + + #[returns(crate::types::asset::AssetParams)] + AssetParams { + denom: String, + }, + + #[returns(Vec)] + AllAssetParams { + start_after: Option, + limit: Option, + }, + + #[returns(crate::types::vault::VaultConfig)] + VaultConfig { + /// Address of vault + address: String, + }, + + #[returns(Vec)] + AllVaultConfigs { + start_after: Option, + limit: Option, + }, + + #[returns(Decimal)] + MaxCloseFactor {}, +} + +#[cw_serde] +pub enum AssetParamsUpdate { + AddOrUpdate { + params: AssetParamsUnchecked, + }, +} + +#[cw_serde] +pub enum VaultConfigUpdate { + AddOrUpdate { + config: VaultConfigUnchecked, + }, +} + +#[cw_serde] +pub enum CmEmergencyUpdate { + SetZeroMaxLtvOnVault(String), + SetZeroDepositCapOnVault(String), + DisallowCoin(String), +} + +#[cw_serde] +pub enum RedBankEmergencyUpdate { + DisableBorrowing(String), +} + +#[cw_serde] +pub enum EmergencyUpdate { + CreditManager(CmEmergencyUpdate), + RedBank(RedBankEmergencyUpdate), +} diff --git a/contracts/params/src/query.rs b/contracts/params/src/query.rs new file mode 100644 index 00000000..b9ec3b10 --- /dev/null +++ b/contracts/params/src/query.rs @@ -0,0 +1,51 @@ +use cosmwasm_std::{Addr, Deps, Order, StdResult}; +use cw_storage_plus::Bound; + +use crate::{ + state::{ASSET_PARAMS, VAULT_CONFIGS}, + types::{asset::AssetParams, vault::VaultConfig}, +}; + +pub const DEFAULT_LIMIT: u32 = 10; + +pub fn query_all_asset_params( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult> { + let start = start_after.as_ref().map(|denom| Bound::exclusive(denom.as_str())); + let limit = limit.unwrap_or(DEFAULT_LIMIT) as usize; + ASSET_PARAMS + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| Ok(res?.1)) + .collect() +} + +pub fn query_vault_config(deps: Deps, unchecked: &str) -> StdResult { + let addr = deps.api.addr_validate(unchecked)?; + VAULT_CONFIGS.load(deps.storage, &addr) +} + +pub fn query_all_vault_configs( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult> { + let vault_addr: Addr; + let start = match &start_after { + Some(unchecked) => { + vault_addr = deps.api.addr_validate(unchecked)?; + Some(Bound::exclusive(&vault_addr)) + } + None => None, + }; + + let limit = limit.unwrap_or(DEFAULT_LIMIT) as usize; + + VAULT_CONFIGS + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| Ok(res?.1)) + .collect() +} diff --git a/contracts/params/src/state.rs b/contracts/params/src/state.rs new file mode 100644 index 00000000..00a07135 --- /dev/null +++ b/contracts/params/src/state.rs @@ -0,0 +1,10 @@ +use cosmwasm_std::{Addr, Decimal}; +use cw_storage_plus::{Item, Map}; +use mars_owner::Owner; + +use crate::types::{asset::AssetParams, vault::VaultConfig}; + +pub const OWNER: Owner = Owner::new("owner"); +pub const ASSET_PARAMS: Map<&str, AssetParams> = Map::new("asset_params"); +pub const VAULT_CONFIGS: Map<&Addr, VaultConfig> = Map::new("vault_configs"); +pub const MAX_CLOSE_FACTOR: Item = Item::new("max_close_factor"); diff --git a/contracts/params/src/types/asset.rs b/contracts/params/src/types/asset.rs new file mode 100644 index 00000000..1462f1db --- /dev/null +++ b/contracts/params/src/types/asset.rs @@ -0,0 +1,83 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Api, Decimal, Uint128}; +use mars_utils::helpers::{decimal_param_le_one, decimal_param_lt_one, validate_native_denom}; + +use crate::{ + error::ContractResult, + execute::{assert_hls_lqt_gt_max_ltv, assert_lqt_gt_max_ltv}, + types::hls::HlsParamsBase, +}; + +#[cw_serde] +pub struct CmSettings { + pub whitelisted: bool, + pub hls: Option>, +} + +#[cw_serde] +pub struct RedBankSettings { + pub deposit_enabled: bool, + pub borrow_enabled: bool, + pub deposit_cap: Uint128, +} + +#[cw_serde] +pub struct AssetParamsBase { + pub denom: String, + pub credit_manager: CmSettings, + pub red_bank: RedBankSettings, + pub max_loan_to_value: Decimal, + pub liquidation_threshold: Decimal, + pub liquidation_bonus: Decimal, +} + +pub type AssetParams = AssetParamsBase; +pub type AssetParamsUnchecked = AssetParamsBase; + +impl From for AssetParamsUnchecked { + fn from(p: AssetParams) -> Self { + Self { + denom: p.denom, + credit_manager: CmSettings { + whitelisted: p.credit_manager.whitelisted, + hls: p.credit_manager.hls.map(Into::into), + }, + red_bank: p.red_bank, + max_loan_to_value: p.max_loan_to_value, + liquidation_threshold: p.liquidation_threshold, + liquidation_bonus: p.liquidation_bonus, + } + } +} + +impl AssetParamsUnchecked { + pub fn check(&self, api: &dyn Api) -> ContractResult { + validate_native_denom(&self.denom)?; + + decimal_param_lt_one(self.max_loan_to_value, "max_loan_to_value")?; + decimal_param_le_one(self.liquidation_threshold, "liquidation_threshold")?; + assert_lqt_gt_max_ltv(self.max_loan_to_value, self.liquidation_threshold)?; + + decimal_param_le_one(self.liquidation_bonus, "liquidation_bonus")?; + + if let Some(hls) = self.credit_manager.hls.as_ref() { + decimal_param_lt_one(hls.max_loan_to_value, "hls_max_loan_to_value")?; + decimal_param_le_one(hls.liquidation_threshold, "hls_liquidation_threshold")?; + assert_hls_lqt_gt_max_ltv(hls.max_loan_to_value, hls.liquidation_threshold)?; + } + + let hls = self.credit_manager.hls.as_ref().map(|hls| hls.check(api)).transpose()?; + + Ok(AssetParams { + denom: self.denom.clone(), + credit_manager: CmSettings { + whitelisted: self.credit_manager.whitelisted, + hls, + }, + red_bank: self.red_bank.clone(), + max_loan_to_value: self.max_loan_to_value, + liquidation_threshold: self.liquidation_threshold, + liquidation_bonus: self.liquidation_bonus, + }) + } +} diff --git a/contracts/params/src/types/hls.rs b/contracts/params/src/types/hls.rs new file mode 100644 index 00000000..d6cd9653 --- /dev/null +++ b/contracts/params/src/types/hls.rs @@ -0,0 +1,82 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Api, Decimal}; +use mars_utils::helpers::validate_native_denom; + +use crate::error::ContractResult; + +#[cw_serde] +pub enum HlsAssetType { + Coin { + denom: String, + }, + Vault { + addr: T, + }, +} + +impl From> for HlsAssetType { + fn from(t: HlsAssetType) -> Self { + match t { + HlsAssetType::Coin { + denom, + } => HlsAssetType::Coin { + denom, + }, + HlsAssetType::Vault { + addr, + } => HlsAssetType::Vault { + addr: addr.to_string(), + }, + } + } +} + +#[cw_serde] +pub struct HlsParamsBase { + pub max_loan_to_value: Decimal, + pub liquidation_threshold: Decimal, + /// Given this asset is debt, correlations are the only allowed collateral + /// which are permitted to fulfill the HLS strategy + pub correlations: Vec>, +} + +pub type HlsParams = HlsParamsBase; +pub type HlsParamsUnchecked = HlsParamsBase; + +impl From for HlsParamsUnchecked { + fn from(hls: HlsParams) -> Self { + Self { + max_loan_to_value: hls.max_loan_to_value, + liquidation_threshold: hls.liquidation_threshold, + correlations: hls.correlations.into_iter().map(Into::into).collect(), + } + } +} + +impl HlsParamsUnchecked { + pub fn check(&self, api: &dyn Api) -> ContractResult { + Ok(HlsParamsBase { + max_loan_to_value: self.max_loan_to_value, + liquidation_threshold: self.liquidation_threshold, + correlations: self + .correlations + .iter() + .map(|c| match c { + HlsAssetType::Coin { + denom, + } => { + validate_native_denom(denom)?; + Ok(HlsAssetType::Coin { + denom: denom.clone(), + }) + } + HlsAssetType::Vault { + addr, + } => Ok(HlsAssetType::Vault { + addr: api.addr_validate(addr)?, + }), + }) + .collect::>>()?, + }) + } +} diff --git a/contracts/params/src/types/mod.rs b/contracts/params/src/types/mod.rs new file mode 100644 index 00000000..76b6ca7a --- /dev/null +++ b/contracts/params/src/types/mod.rs @@ -0,0 +1,3 @@ +pub mod asset; +pub mod hls; +pub mod vault; diff --git a/contracts/params/src/types/vault.rs b/contracts/params/src/types/vault.rs new file mode 100644 index 00000000..e7b8f6fc --- /dev/null +++ b/contracts/params/src/types/vault.rs @@ -0,0 +1,59 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Api, Coin, Decimal}; +use mars_utils::helpers::decimal_param_le_one; + +use crate::{ + error::ContractResult, + execute::{assert_hls_lqt_gt_max_ltv, assert_lqt_gt_max_ltv}, + types::hls::HlsParamsBase, +}; + +#[cw_serde] +pub struct VaultConfigBase { + pub addr: T, + pub deposit_cap: Coin, + pub max_loan_to_value: Decimal, + pub liquidation_threshold: Decimal, + pub whitelisted: bool, + pub hls: Option>, +} + +pub type VaultConfigUnchecked = VaultConfigBase; +pub type VaultConfig = VaultConfigBase; + +impl From for VaultConfigUnchecked { + fn from(v: VaultConfig) -> Self { + VaultConfigUnchecked { + addr: v.addr.to_string(), + deposit_cap: v.deposit_cap, + max_loan_to_value: v.max_loan_to_value, + liquidation_threshold: v.liquidation_threshold, + whitelisted: v.whitelisted, + hls: v.hls.map(Into::into), + } + } +} + +impl VaultConfigUnchecked { + pub fn check(&self, api: &dyn Api) -> ContractResult { + decimal_param_le_one(self.max_loan_to_value, "max_loan_to_value")?; + decimal_param_le_one(self.liquidation_threshold, "liquidation_threshold")?; + assert_lqt_gt_max_ltv(self.max_loan_to_value, self.liquidation_threshold)?; + + // High levered strategies + if let Some(hls) = self.hls.as_ref() { + decimal_param_le_one(hls.max_loan_to_value, "hls_max_loan_to_value")?; + decimal_param_le_one(hls.liquidation_threshold, "hls_liquidation_threshold")?; + assert_hls_lqt_gt_max_ltv(hls.max_loan_to_value, hls.liquidation_threshold)?; + } + + Ok(VaultConfig { + addr: api.addr_validate(&self.addr)?, + deposit_cap: self.deposit_cap.clone(), + max_loan_to_value: self.max_loan_to_value, + liquidation_threshold: self.liquidation_threshold, + whitelisted: self.whitelisted, + hls: self.hls.as_ref().map(|hls| hls.check(api)).transpose()?, + }) + } +} diff --git a/contracts/params/tests/helpers/assertions.rs b/contracts/params/tests/helpers/assertions.rs new file mode 100644 index 00000000..857060fd --- /dev/null +++ b/contracts/params/tests/helpers/assertions.rs @@ -0,0 +1,25 @@ +use std::{collections::HashSet, hash::Hash}; + +use anyhow::Result as AnyResult; +use cw_multi_test::AppResponse; +use mars_params::error::ContractError; + +pub fn assert_err(res: AnyResult, err: ContractError) { + match res { + Ok(_) => panic!("Result was not an error"), + Err(generic_err) => { + let contract_err: ContractError = generic_err.downcast().unwrap(); + assert_eq!(contract_err, err); + } + } +} + +pub fn assert_contents_equal(vec_a: &[T], vec_b: &[T]) +where + T: Eq + Hash, +{ + let set_a: HashSet<_> = vec_a.iter().collect(); + let set_b: HashSet<_> = vec_b.iter().collect(); + + assert!(set_a == set_b) +} diff --git a/contracts/params/tests/helpers/contracts.rs b/contracts/params/tests/helpers/contracts.rs new file mode 100644 index 00000000..52c2cfef --- /dev/null +++ b/contracts/params/tests/helpers/contracts.rs @@ -0,0 +1,11 @@ +use cosmwasm_std::Empty; +use cw_multi_test::{Contract, ContractWrapper}; + +pub fn mock_params_contract() -> Box> { + let contract = ContractWrapper::new( + mars_params::contract::execute, + mars_params::contract::instantiate, + mars_params::contract::query, + ); + Box::new(contract) +} diff --git a/contracts/params/tests/helpers/generator.rs b/contracts/params/tests/helpers/generator.rs new file mode 100644 index 00000000..2bcbd029 --- /dev/null +++ b/contracts/params/tests/helpers/generator.rs @@ -0,0 +1,36 @@ +use std::str::FromStr; + +use cosmwasm_std::{coin, Decimal, Uint128}; +use mars_params::types::{ + asset::{AssetParamsUnchecked, CmSettings, RedBankSettings}, + vault::VaultConfigUnchecked, +}; + +pub fn default_asset_params(denom: &str) -> AssetParamsUnchecked { + AssetParamsUnchecked { + denom: denom.to_string(), + credit_manager: CmSettings { + whitelisted: false, + hls: None, + }, + red_bank: RedBankSettings { + deposit_enabled: true, + borrow_enabled: false, + deposit_cap: Uint128::new(1_000_000_000), + }, + max_loan_to_value: Decimal::from_str("0.6").unwrap(), + liquidation_threshold: Decimal::from_str("0.7").unwrap(), + liquidation_bonus: Decimal::from_str("0.15").unwrap(), + } +} + +pub fn default_vault_config(addr: &str) -> VaultConfigUnchecked { + VaultConfigUnchecked { + addr: addr.to_string(), + deposit_cap: coin(100_000_000_000, "uusdc"), + max_loan_to_value: Decimal::from_str("0.47").unwrap(), + liquidation_threshold: Decimal::from_str("0.5").unwrap(), + whitelisted: true, + hls: None, + } +} diff --git a/contracts/params/tests/helpers/mock_env.rs b/contracts/params/tests/helpers/mock_env.rs new file mode 100644 index 00000000..0ae03033 --- /dev/null +++ b/contracts/params/tests/helpers/mock_env.rs @@ -0,0 +1,240 @@ +use std::{mem::take, str::FromStr}; + +use anyhow::Result as AnyResult; +use cosmwasm_std::{Addr, Decimal}; +use cw_multi_test::{App, AppResponse, BasicApp, Executor}; +use mars_owner::{OwnerResponse, OwnerUpdate}; +use mars_params::{ + msg::{ + AssetParamsUpdate, EmergencyUpdate, ExecuteMsg, InstantiateMsg, QueryMsg, VaultConfigUpdate, + }, + types::{asset::AssetParams, vault::VaultConfig}, +}; + +use crate::helpers::mock_params_contract; + +pub struct MockEnv { + pub app: BasicApp, + pub params_contract: Addr, +} + +pub struct MockEnvBuilder { + pub app: BasicApp, + pub max_close_factor: Option, + pub emergency_owner: Option, +} + +#[allow(clippy::new_ret_no_self)] +impl MockEnv { + pub fn new() -> MockEnvBuilder { + MockEnvBuilder { + app: App::default(), + max_close_factor: None, + emergency_owner: None, + } + } + + //-------------------------------------------------------------------------------------------------- + // Execute Msgs + //-------------------------------------------------------------------------------------------------- + + pub fn update_asset_params( + &mut self, + sender: &Addr, + update: AssetParamsUpdate, + ) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.params_contract.clone(), + &ExecuteMsg::UpdateAssetParams(update), + &[], + ) + } + + pub fn update_vault_config( + &mut self, + sender: &Addr, + update: VaultConfigUpdate, + ) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.params_contract.clone(), + &ExecuteMsg::UpdateVaultConfig(update), + &[], + ) + } + + pub fn update_owner(&mut self, sender: &Addr, update: OwnerUpdate) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.params_contract.clone(), + &ExecuteMsg::UpdateOwner(update), + &[], + ) + } + + pub fn update_max_close_factor( + &mut self, + sender: &Addr, + mcf: Decimal, + ) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.params_contract.clone(), + &ExecuteMsg::UpdateMaxCloseFactor(mcf), + &[], + ) + } + + pub fn emergency_update( + &mut self, + sender: &Addr, + update: EmergencyUpdate, + ) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.params_contract.clone(), + &ExecuteMsg::EmergencyUpdate(update), + &[], + ) + } + + //-------------------------------------------------------------------------------------------------- + // Queries + //-------------------------------------------------------------------------------------------------- + + pub fn query_owner(&self) -> Addr { + let res = self.query_ownership(); + Addr::unchecked(res.owner.unwrap()) + } + + pub fn query_ownership(&self) -> OwnerResponse { + self.app.wrap().query_wasm_smart(self.params_contract.clone(), &QueryMsg::Owner {}).unwrap() + } + + pub fn query_asset_params(&self, denom: &str) -> AssetParams { + self.app + .wrap() + .query_wasm_smart( + self.params_contract.clone(), + &QueryMsg::AssetParams { + denom: denom.to_string(), + }, + ) + .unwrap() + } + + pub fn query_all_asset_params( + &self, + start_after: Option, + limit: Option, + ) -> Vec { + self.app + .wrap() + .query_wasm_smart( + self.params_contract.clone(), + &QueryMsg::AllAssetParams { + start_after, + limit, + }, + ) + .unwrap() + } + + pub fn query_vault_config(&self, addr: &str) -> VaultConfig { + self.app + .wrap() + .query_wasm_smart( + self.params_contract.clone(), + &QueryMsg::VaultConfig { + address: addr.to_string(), + }, + ) + .unwrap() + } + + pub fn query_all_vault_configs( + &self, + start_after: Option, + limit: Option, + ) -> Vec { + self.app + .wrap() + .query_wasm_smart( + self.params_contract.clone(), + &QueryMsg::AllVaultConfigs { + start_after, + limit, + }, + ) + .unwrap() + } + + pub fn query_max_close_factor(&self) -> Decimal { + self.app + .wrap() + .query_wasm_smart(self.params_contract.clone(), &QueryMsg::MaxCloseFactor {}) + .unwrap() + } +} + +impl MockEnvBuilder { + pub fn build(&mut self) -> AnyResult { + let code_id = self.app.store_code(mock_params_contract()); + + let params_contract = self.app.instantiate_contract( + code_id, + Addr::unchecked("owner"), + &InstantiateMsg { + owner: "owner".to_string(), + max_close_factor: self.get_max_close_factor(), + }, + &[], + "mock-params-contract", + None, + )?; + + if self.emergency_owner.is_some() { + self.set_emergency_owner(¶ms_contract, &self.emergency_owner.clone().unwrap()); + } + + Ok(MockEnv { + app: take(&mut self.app), + params_contract, + }) + } + + fn set_emergency_owner(&mut self, params_contract: &Addr, eo: &str) { + self.app + .execute_contract( + Addr::unchecked("owner"), + params_contract.clone(), + &ExecuteMsg::UpdateOwner(OwnerUpdate::SetEmergencyOwner { + emergency_owner: eo.to_string(), + }), + &[], + ) + .unwrap(); + } + + //-------------------------------------------------------------------------------------------------- + // Get or defaults + //-------------------------------------------------------------------------------------------------- + + pub fn get_max_close_factor(&self) -> Decimal { + self.max_close_factor.unwrap_or(Decimal::from_str("0.5").unwrap()) + } + + //-------------------------------------------------------------------------------------------------- + // Setter functions + //-------------------------------------------------------------------------------------------------- + pub fn max_close_factor(&mut self, mcf: Decimal) -> &mut Self { + self.max_close_factor = Some(mcf); + self + } + + pub fn emergency_owner(&mut self, eo: &str) -> &mut Self { + self.emergency_owner = Some(eo.to_string()); + self + } +} diff --git a/contracts/params/tests/helpers/mod.rs b/contracts/params/tests/helpers/mod.rs new file mode 100644 index 00000000..2c580c0b --- /dev/null +++ b/contracts/params/tests/helpers/mod.rs @@ -0,0 +1,6 @@ +pub use self::{assertions::*, contracts::*, generator::*, mock_env::*}; + +mod assertions; +mod contracts; +mod generator; +mod mock_env; diff --git a/contracts/params/tests/test_asset_validation.rs b/contracts/params/tests/test_asset_validation.rs new file mode 100644 index 00000000..6ffc92c7 --- /dev/null +++ b/contracts/params/tests/test_asset_validation.rs @@ -0,0 +1,225 @@ +use std::str::FromStr; + +use cosmwasm_std::Decimal; +use mars_params::{ + error::ContractError::Validation, + msg::AssetParamsUpdate, + types::hls::{HlsAssetType, HlsParamsUnchecked}, +}; +use mars_utils::error::ValidationError::{InvalidDenom, InvalidParam}; + +use crate::helpers::{assert_err, default_asset_params, MockEnv}; + +pub mod helpers; + +#[test] +fn denom_must_be_native() { + let mut mock = MockEnv::new().build().unwrap(); + let denom = "AA".to_string(); // Invalid native denom length + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom), + }, + ); + assert_err( + res, + Validation(InvalidDenom { + reason: "Invalid denom length".to_string(), + }), + ); +} + +#[test] +fn max_ltv_less_than_one() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + params.max_loan_to_value = Decimal::from_str("1.1235").unwrap(); + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "max_loan_to_value".to_string(), + invalid_value: "1.1235".to_string(), + predicate: "< 1".to_string(), + }), + ); +} + +#[test] +fn liquidation_threshold_less_than_or_equal_to_one() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + params.liquidation_threshold = Decimal::from_str("1.1235").unwrap(); + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "liquidation_threshold".to_string(), + invalid_value: "1.1235".to_string(), + predicate: "<= 1".to_string(), + }), + ); +} + +#[test] +fn liquidation_bonus_less_than_or_equal_to_one() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + params.liquidation_bonus = Decimal::from_str("1.1235").unwrap(); + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "liquidation_bonus".to_string(), + invalid_value: "1.1235".to_string(), + predicate: "<= 1".to_string(), + }), + ); +} + +#[test] +fn liq_threshold_gt_max_ltv() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + params.liquidation_threshold = Decimal::from_str("0.5").unwrap(); + params.max_loan_to_value = Decimal::from_str("0.6").unwrap(); + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "liquidation_threshold".to_string(), + invalid_value: "0.5".to_string(), + predicate: "> 0.6 (max LTV)".to_string(), + }), + ); +} + +#[test] +fn hls_max_ltv_less_than_one() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + params.credit_manager.hls = Some(HlsParamsUnchecked { + max_loan_to_value: Decimal::from_str("1.1235").unwrap(), + liquidation_threshold: Decimal::from_str("0.5").unwrap(), + correlations: vec![], + }); + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "hls_max_loan_to_value".to_string(), + invalid_value: "1.1235".to_string(), + predicate: "< 1".to_string(), + }), + ); +} + +#[test] +fn hls_liquidation_threshold_less_than_or_equal_to_one() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + params.credit_manager.hls = Some(HlsParamsUnchecked { + max_loan_to_value: Decimal::from_str("0.6").unwrap(), + liquidation_threshold: Decimal::from_str("1.1235").unwrap(), + correlations: vec![], + }); + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "hls_liquidation_threshold".to_string(), + invalid_value: "1.1235".to_string(), + predicate: "<= 1".to_string(), + }), + ); +} + +#[test] +fn hls_liq_threshold_gt_hls_max_ltv() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + params.credit_manager.hls = Some(HlsParamsUnchecked { + max_loan_to_value: Decimal::from_str("0.6").unwrap(), + liquidation_threshold: Decimal::from_str("0.5").unwrap(), + correlations: vec![], + }); + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "hls_liquidation_threshold".to_string(), + invalid_value: "0.5".to_string(), + predicate: "> 0.6 (hls max LTV)".to_string(), + }), + ); +} + +#[test] +fn correlations_must_be_valid_denoms() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + params.credit_manager.hls = Some(HlsParamsUnchecked { + max_loan_to_value: Decimal::from_str("0.5").unwrap(), + liquidation_threshold: Decimal::from_str("0.7").unwrap(), + correlations: vec![HlsAssetType::Coin { + denom: "AA".to_string(), + }], + }); + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidDenom { + reason: "Invalid denom length".to_string(), + }), + ); +} diff --git a/contracts/params/tests/test_close_factor.rs b/contracts/params/tests/test_close_factor.rs new file mode 100644 index 00000000..7b23d0bd --- /dev/null +++ b/contracts/params/tests/test_close_factor.rs @@ -0,0 +1,65 @@ +use std::str::FromStr; + +use cosmwasm_std::{Addr, Decimal}; +use mars_owner::{OwnerError, OwnerUpdate}; +use mars_params::error::ContractError::{Owner, Validation}; +use mars_utils::error::ValidationError::InvalidParam; + +use crate::helpers::{assert_err, MockEnv}; + +pub mod helpers; + +#[test] +fn mcf_set_on_init() { + let mock = MockEnv::new().build().unwrap(); + let mcf = mock.query_max_close_factor(); + assert_eq!(mcf, Decimal::from_str("0.5").unwrap()) +} + +#[test] +fn mcf_validated_on_init() { + let res = MockEnv::new().max_close_factor(Decimal::from_str("1.23").unwrap()).build(); + if res.is_ok() { + panic!("Should have thrown an instantiate error"); + } +} + +#[test] +fn only_owner_can_update_mcf() { + let mut mock = MockEnv::new().build().unwrap(); + let bad_guy = Addr::unchecked("doctor_otto_983"); + let res = mock.update_owner( + &bad_guy, + OwnerUpdate::ProposeNewOwner { + proposed: bad_guy.to_string(), + }, + ); + assert_err(res, Owner(OwnerError::NotOwner {})); +} + +#[test] +fn validated_updates() { + let mut mock = MockEnv::new().build().unwrap(); + let res = mock.update_max_close_factor(&mock.query_owner(), Decimal::from_str("1.9").unwrap()); + assert_err( + res, + Validation(InvalidParam { + param_name: "max-close-factor".to_string(), + invalid_value: "max-close-factor".to_string(), + predicate: "<= 1".to_string(), + }), + ); +} + +#[test] +fn update_mcf() { + let mut mock = MockEnv::new().build().unwrap(); + let new_max_close_factor = Decimal::from_str("0.9").unwrap(); + let current_mcf = mock.query_max_close_factor(); + assert_ne!(current_mcf, new_max_close_factor); + + mock.update_max_close_factor(&mock.query_owner(), Decimal::from_str("0.9").unwrap()).unwrap(); + + let current_mcf = mock.query_max_close_factor(); + assert_eq!(current_mcf, new_max_close_factor); +} diff --git a/contracts/params/tests/test_emergency_powers.rs b/contracts/params/tests/test_emergency_powers.rs new file mode 100644 index 00000000..748c972e --- /dev/null +++ b/contracts/params/tests/test_emergency_powers.rs @@ -0,0 +1,158 @@ +use cosmwasm_std::Addr; +use mars_owner::OwnerError; +use mars_params::{ + error::ContractError::Owner, + msg::{ + AssetParamsUpdate, CmEmergencyUpdate, EmergencyUpdate, RedBankEmergencyUpdate, + VaultConfigUpdate, + }, +}; + +use crate::helpers::{assert_err, default_asset_params, default_vault_config, MockEnv}; + +pub mod helpers; + +#[test] +fn only_owner_can_invoke_emergency_powers() { + let mut mock = MockEnv::new().build().unwrap(); + let bad_guy = Addr::unchecked("doctor_otto_983"); + let res = mock.emergency_update( + &bad_guy, + EmergencyUpdate::RedBank(RedBankEmergencyUpdate::DisableBorrowing("xyz".to_string())), + ); + assert_err(res, Owner(OwnerError::NotEmergencyOwner {})); + + let res = mock.emergency_update( + &bad_guy, + EmergencyUpdate::CreditManager(CmEmergencyUpdate::DisallowCoin("xyz".to_string())), + ); + assert_err(res, Owner(OwnerError::NotEmergencyOwner {})); + + let res = mock.emergency_update( + &bad_guy, + EmergencyUpdate::CreditManager(CmEmergencyUpdate::SetZeroDepositCapOnVault( + "xyz".to_string(), + )), + ); + assert_err(res, Owner(OwnerError::NotEmergencyOwner {})); + + let res = mock.emergency_update( + &bad_guy, + EmergencyUpdate::CreditManager(CmEmergencyUpdate::SetZeroMaxLtvOnVault("xyz".to_string())), + ); + assert_err(res, Owner(OwnerError::NotEmergencyOwner {})); +} + +#[test] +fn disabling_borrowing() { + let emergency_owner = Addr::unchecked("miles_morales"); + let mut mock = MockEnv::new().emergency_owner(emergency_owner.as_str()).build().unwrap(); + let denom = "atom".to_string(); + + let mut params = default_asset_params(&denom); + params.red_bank.borrow_enabled = true; + + mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ) + .unwrap(); + + let params = mock.query_asset_params(&denom); + assert!(params.red_bank.borrow_enabled); + + mock.emergency_update( + &emergency_owner, + EmergencyUpdate::RedBank(RedBankEmergencyUpdate::DisableBorrowing(denom.clone())), + ) + .unwrap(); + + let params = mock.query_asset_params(&denom); + assert!(!params.red_bank.borrow_enabled); +} + +#[test] +fn disallow_coin() { + let emergency_owner = Addr::unchecked("miles_morales"); + let mut mock = MockEnv::new().emergency_owner(emergency_owner.as_str()).build().unwrap(); + let denom = "atom".to_string(); + + let mut params = default_asset_params(&denom); + params.credit_manager.whitelisted = true; + + mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ) + .unwrap(); + + let params = mock.query_asset_params(&denom); + assert!(params.credit_manager.whitelisted); + + mock.emergency_update( + &emergency_owner, + EmergencyUpdate::CreditManager(CmEmergencyUpdate::DisallowCoin(denom.clone())), + ) + .unwrap(); + + let params = mock.query_asset_params(&denom); + assert!(!params.credit_manager.whitelisted); +} + +#[test] +fn set_zero_max_ltv() { + let emergency_owner = Addr::unchecked("miles_morales"); + let mut mock = MockEnv::new().emergency_owner(emergency_owner.as_str()).build().unwrap(); + let vault = "vault_addr_123".to_string(); + + mock.update_vault_config( + &mock.query_owner(), + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault), + }, + ) + .unwrap(); + + let params = mock.query_vault_config(&vault); + assert!(!params.max_loan_to_value.is_zero()); + + mock.emergency_update( + &emergency_owner, + EmergencyUpdate::CreditManager(CmEmergencyUpdate::SetZeroMaxLtvOnVault(vault.clone())), + ) + .unwrap(); + + let params = mock.query_vault_config(&vault); + assert!(params.max_loan_to_value.is_zero()); +} + +#[test] +fn set_zero_deposit_cap() { + let emergency_owner = Addr::unchecked("miles_morales"); + let mut mock = MockEnv::new().emergency_owner(emergency_owner.as_str()).build().unwrap(); + let vault = "vault_addr_123".to_string(); + + mock.update_vault_config( + &mock.query_owner(), + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault), + }, + ) + .unwrap(); + + let params = mock.query_vault_config(&vault); + assert!(!params.deposit_cap.amount.is_zero()); + + mock.emergency_update( + &emergency_owner, + EmergencyUpdate::CreditManager(CmEmergencyUpdate::SetZeroDepositCapOnVault(vault.clone())), + ) + .unwrap(); + + let params = mock.query_vault_config(&vault); + assert!(params.deposit_cap.amount.is_zero()); +} diff --git a/contracts/params/tests/test_owner.rs b/contracts/params/tests/test_owner.rs new file mode 100644 index 00000000..df525b85 --- /dev/null +++ b/contracts/params/tests/test_owner.rs @@ -0,0 +1,47 @@ +use cosmwasm_std::Addr; +use mars_owner::{OwnerError, OwnerUpdate}; +use mars_params::error::ContractError::Owner; + +use crate::helpers::{assert_err, MockEnv}; + +pub mod helpers; + +#[test] +fn owner_set_on_init() { + let mock = MockEnv::new().build().unwrap(); + let owner = mock.query_owner(); + assert_eq!("owner", &owner.to_string()) +} + +#[test] +fn only_owner_can_execute_updates() { + let mut mock = MockEnv::new().build().unwrap(); + let bad_guy = Addr::unchecked("doctor_otto_983"); + let res = mock.update_owner( + &bad_guy, + OwnerUpdate::ProposeNewOwner { + proposed: bad_guy.to_string(), + }, + ); + assert_err(res, Owner(OwnerError::NotOwner {})); +} + +#[test] +fn owner_can_execute_updates() { + let mut mock = MockEnv::new().build().unwrap(); + + let ownership = mock.query_ownership(); + assert_eq!(ownership.emergency_owner, None); + + let em_owner = "miles_morales".to_string(); + mock.update_owner( + &mock.query_owner(), + OwnerUpdate::SetEmergencyOwner { + emergency_owner: em_owner.clone(), + }, + ) + .unwrap(); + + let ownership = mock.query_ownership(); + assert_eq!(ownership.emergency_owner, Some(em_owner)); +} diff --git a/contracts/params/tests/test_update_asset_params.rs b/contracts/params/tests/test_update_asset_params.rs new file mode 100644 index 00000000..f62b2792 --- /dev/null +++ b/contracts/params/tests/test_update_asset_params.rs @@ -0,0 +1,251 @@ +use cosmwasm_std::Addr; +use mars_owner::OwnerError; +use mars_params::{error::ContractError::Owner, msg::AssetParamsUpdate}; + +use crate::helpers::{assert_contents_equal, assert_err, default_asset_params, MockEnv}; + +pub mod helpers; + +#[test] +fn initial_state_of_params() { + let mock = MockEnv::new().build().unwrap(); + let params = mock.query_all_asset_params(None, None); + assert!(params.is_empty()); +} + +#[test] +fn only_owner_can_update_asset_params() { + let mut mock = MockEnv::new().build().unwrap(); + let bad_guy = Addr::unchecked("doctor_otto_983"); + let res = mock.update_asset_params( + &bad_guy, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params("xyz"), + }, + ); + assert_err(res, Owner(OwnerError::NotOwner {})); +} + +#[test] +fn initializing_asset_param() { + let mut mock = MockEnv::new().build().unwrap(); + let owner = mock.query_owner(); + let denom0 = "atom".to_string(); + let denom1 = "osmo".to_string(); + + let params = default_asset_params(&denom0); + + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: params.clone(), + }, + ) + .unwrap(); + + let all_asset_params = mock.query_all_asset_params(None, None); + assert_eq!(1, all_asset_params.len()); + let res = all_asset_params.first().unwrap(); + assert_eq!(&denom0, &res.denom); + + // Validate config set correctly + assert_eq!(params, res.clone().into()); + + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom1), + }, + ) + .unwrap(); + + let asset_params = mock.query_all_asset_params(None, None); + assert_eq!(2, asset_params.len()); + assert_eq!(&denom1, &asset_params.get(1).unwrap().denom); +} + +#[test] +fn add_same_denom_multiple_times() { + let mut mock = MockEnv::new().build().unwrap(); + let owner = mock.query_owner(); + let denom0 = "atom".to_string(); + + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom0), + }, + ) + .unwrap(); + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom0), + }, + ) + .unwrap(); + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom0), + }, + ) + .unwrap(); + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom0), + }, + ) + .unwrap(); + + let asset_params = mock.query_all_asset_params(None, None); + assert_eq!(1, asset_params.len()); + assert_eq!(denom0, asset_params.first().unwrap().denom); +} + +#[test] +fn update_existing_asset_params() { + let mut mock = MockEnv::new().build().unwrap(); + let owner = mock.query_owner(); + let denom0 = "atom".to_string(); + + let mut params = default_asset_params(&denom0); + + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: params.clone(), + }, + ) + .unwrap(); + + let asset_params = mock.query_asset_params(&denom0); + assert!(!asset_params.credit_manager.whitelisted); + assert!(asset_params.red_bank.deposit_enabled); + + params.credit_manager.whitelisted = true; + params.red_bank.deposit_enabled = false; + + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params, + }, + ) + .unwrap(); + + let all_asset_params = mock.query_all_asset_params(None, None); + assert_eq!(1, all_asset_params.len()); + + let asset_params = mock.query_asset_params(&denom0); + assert!(asset_params.credit_manager.whitelisted); + assert!(!asset_params.red_bank.deposit_enabled); +} + +#[test] +fn removing_from_asset_params() { + let mut mock = MockEnv::new().build().unwrap(); + let owner = mock.query_owner(); + let denom0 = "atom".to_string(); + let denom1 = "osmo".to_string(); + let denom2 = "juno".to_string(); + + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom0), + }, + ) + .unwrap(); + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom1), + }, + ) + .unwrap(); + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom2), + }, + ) + .unwrap(); + + let asset_params = mock.query_all_asset_params(None, None); + assert_eq!(3, asset_params.len()); +} + +#[test] +fn pagination_query() { + let mut mock = MockEnv::new().build().unwrap(); + let owner = mock.query_owner(); + let denom0 = "atom".to_string(); + let denom1 = "osmo".to_string(); + let denom2 = "juno".to_string(); + let denom3 = "mars".to_string(); + let denom4 = "ion".to_string(); + let denom5 = "usdc".to_string(); + + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom0), + }, + ) + .unwrap(); + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom1), + }, + ) + .unwrap(); + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom2), + }, + ) + .unwrap(); + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom3), + }, + ) + .unwrap(); + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom4), + }, + ) + .unwrap(); + mock.update_asset_params( + &owner, + AssetParamsUpdate::AddOrUpdate { + params: default_asset_params(&denom5), + }, + ) + .unwrap(); + + let asset_params_a = mock.query_all_asset_params(None, Some(2)); + let asset_params_b = + mock.query_all_asset_params(asset_params_a.last().map(|r| r.denom.clone()), Some(2)); + let asset_params_c = + mock.query_all_asset_params(asset_params_b.last().map(|r| r.denom.clone()), None); + + let combined = asset_params_a + .iter() + .cloned() + .chain(asset_params_b.iter().cloned()) + .chain(asset_params_c.iter().cloned()) + .map(|r| r.denom) + .collect::>(); + + assert_eq!(6, combined.len()); + + assert_contents_equal(&[denom0, denom1, denom2, denom3, denom4, denom5], &combined) +} diff --git a/contracts/params/tests/test_vault_validation.rs b/contracts/params/tests/test_vault_validation.rs new file mode 100644 index 00000000..d1b29a8d --- /dev/null +++ b/contracts/params/tests/test_vault_validation.rs @@ -0,0 +1,174 @@ +use std::str::FromStr; + +use cosmwasm_std::{Decimal, StdError::GenericErr}; +use mars_params::{ + error::ContractError::{Std, Validation}, + msg::VaultConfigUpdate, + types::hls::HlsParamsUnchecked, +}; +use mars_utils::error::ValidationError::InvalidParam; + +use crate::helpers::{assert_err, default_vault_config, MockEnv}; + +pub mod helpers; + +#[test] +fn vault_addr_must_be_valid() { + let mut mock = MockEnv::new().build().unwrap(); + + let res = mock.update_vault_config( + &mock.query_owner(), + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config("%"), + }, + ); + assert_err( + res, + Std(GenericErr { msg: "Invalid input: human address too short for this mock implementation (must be >= 3).".to_string() }), + ); +} + +#[test] +fn vault_max_ltv_less_than_or_equal_to_one() { + let mut mock = MockEnv::new().build().unwrap(); + let mut config = default_vault_config("vault_xyz"); + config.max_loan_to_value = Decimal::from_str("1.1235").unwrap(); + + let res = mock.update_vault_config( + &mock.query_owner(), + VaultConfigUpdate::AddOrUpdate { + config, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "max_loan_to_value".to_string(), + invalid_value: "1.1235".to_string(), + predicate: "<= 1".to_string(), + }), + ); +} + +#[test] +fn vault_liquidation_threshold_less_than_or_equal_to_one() { + let mut mock = MockEnv::new().build().unwrap(); + let mut config = default_vault_config("vault_xyz"); + config.liquidation_threshold = Decimal::from_str("1.1235").unwrap(); + + let res = mock.update_vault_config( + &mock.query_owner(), + VaultConfigUpdate::AddOrUpdate { + config, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "liquidation_threshold".to_string(), + invalid_value: "1.1235".to_string(), + predicate: "<= 1".to_string(), + }), + ); +} + +#[test] +fn vault_liq_threshold_gt_max_ltv() { + let mut mock = MockEnv::new().build().unwrap(); + let mut config = default_vault_config("vault_xyz"); + config.liquidation_threshold = Decimal::from_str("0.5").unwrap(); + config.max_loan_to_value = Decimal::from_str("0.6").unwrap(); + + let res = mock.update_vault_config( + &mock.query_owner(), + VaultConfigUpdate::AddOrUpdate { + config, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "liquidation_threshold".to_string(), + invalid_value: "0.5".to_string(), + predicate: "> 0.6 (max LTV)".to_string(), + }), + ); +} + +#[test] +fn vault_hls_max_ltv_less_than_or_equal_to_one() { + let mut mock = MockEnv::new().build().unwrap(); + let mut config = default_vault_config("vault_xyz"); + config.hls = Some(HlsParamsUnchecked { + max_loan_to_value: Decimal::from_str("1.1235").unwrap(), + liquidation_threshold: Decimal::from_str("2.1235").unwrap(), + correlations: vec![], + }); + + let res = mock.update_vault_config( + &mock.query_owner(), + VaultConfigUpdate::AddOrUpdate { + config, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "hls_max_loan_to_value".to_string(), + invalid_value: "1.1235".to_string(), + predicate: "<= 1".to_string(), + }), + ); +} + +#[test] +fn vault_hls_liquidation_threshold_less_than_or_equal_to_one() { + let mut mock = MockEnv::new().build().unwrap(); + let mut config = default_vault_config("vault_xyz"); + config.hls = Some(HlsParamsUnchecked { + max_loan_to_value: Decimal::from_str("0.8").unwrap(), + liquidation_threshold: Decimal::from_str("1.1235").unwrap(), + correlations: vec![], + }); + + let res = mock.update_vault_config( + &mock.query_owner(), + VaultConfigUpdate::AddOrUpdate { + config, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "hls_liquidation_threshold".to_string(), + invalid_value: "1.1235".to_string(), + predicate: "<= 1".to_string(), + }), + ); +} + +#[test] +fn vault_hls_liq_threshold_gt_max_ltv() { + let mut mock = MockEnv::new().build().unwrap(); + let mut config = default_vault_config("vault_xyz"); + config.hls = Some(HlsParamsUnchecked { + max_loan_to_value: Decimal::from_str("0.6").unwrap(), + liquidation_threshold: Decimal::from_str("0.5").unwrap(), + correlations: vec![], + }); + + let res = mock.update_vault_config( + &mock.query_owner(), + VaultConfigUpdate::AddOrUpdate { + config, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "hls_liquidation_threshold".to_string(), + invalid_value: "0.5".to_string(), + predicate: "> 0.6 (hls max LTV)".to_string(), + }), + ); +} diff --git a/contracts/params/tests/test_vaults.rs b/contracts/params/tests/test_vaults.rs new file mode 100644 index 00000000..84d0ab4d --- /dev/null +++ b/contracts/params/tests/test_vaults.rs @@ -0,0 +1,227 @@ +use std::str::FromStr; + +use cosmwasm_std::{Addr, Decimal}; +use mars_owner::OwnerError; +use mars_params::{ + error::ContractError::Owner, msg::VaultConfigUpdate, types::vault::VaultConfigUnchecked, +}; + +use crate::helpers::{assert_contents_equal, assert_err, default_vault_config, MockEnv}; + +pub mod helpers; + +#[test] +fn initial_state_of_vault_configs() { + let mock = MockEnv::new().build().unwrap(); + let configs = mock.query_all_vault_configs(None, None); + assert!(configs.is_empty()); +} + +#[test] +fn only_owner_can_update_vault_configs() { + let mut mock = MockEnv::new().build().unwrap(); + let bad_guy = Addr::unchecked("doctor_otto_983"); + let res = mock.update_vault_config( + &bad_guy, + VaultConfigUpdate::AddOrUpdate { + config: VaultConfigUnchecked { + addr: "xyz".to_string(), + deposit_cap: Default::default(), + max_loan_to_value: Default::default(), + liquidation_threshold: Default::default(), + whitelisted: false, + hls: None, + }, + }, + ); + assert_err(res, Owner(OwnerError::NotOwner {})); +} + +#[test] +fn initializing_asset_param() { + let mut mock = MockEnv::new().build().unwrap(); + let owner = mock.query_owner(); + let vault0 = "vault_addr_0".to_string(); + let vault1 = "vault_addr_1".to_string(); + + let starting_vault_config = default_vault_config(&vault0); + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: starting_vault_config.clone(), + }, + ) + .unwrap(); + + let all_vault_configs = mock.query_all_vault_configs(None, None); + assert_eq!(1, all_vault_configs.len()); + + // Validate config set correctly + let config = all_vault_configs.first().unwrap(); + assert_eq!(starting_vault_config, config.clone().into()); + + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault1), + }, + ) + .unwrap(); + + let vault_configs = mock.query_all_vault_configs(None, None); + assert_eq!(2, vault_configs.len()); + assert_eq!(&vault1, &vault_configs.get(1).unwrap().addr); +} + +#[test] +fn add_same_vault_multiple_times() { + let mut mock = MockEnv::new().build().unwrap(); + let owner = mock.query_owner(); + let vault0 = "vault_addr_0".to_string(); + + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault0), + }, + ) + .unwrap(); + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault0), + }, + ) + .unwrap(); + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault0), + }, + ) + .unwrap(); + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault0), + }, + ) + .unwrap(); + + let vault_configs = mock.query_all_vault_configs(None, None); + assert_eq!(1, vault_configs.len()); + assert_eq!(vault0, vault_configs.first().unwrap().addr); +} + +#[test] +fn update_existing_vault_configs() { + let mut mock = MockEnv::new().build().unwrap(); + let owner = mock.query_owner(); + let vault0 = "vault_addr_0".to_string(); + + let mut config = default_vault_config(&vault0); + + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: config.clone(), + }, + ) + .unwrap(); + + let vault_config = mock.query_vault_config(&vault0); + assert!(vault_config.whitelisted); + assert_eq!(vault_config.max_loan_to_value, Decimal::from_str("0.47").unwrap()); + + let new_max_ltv = Decimal::from_str("0.39").unwrap(); + config.whitelisted = false; + config.max_loan_to_value = new_max_ltv; + + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config, + }, + ) + .unwrap(); + + let all_vault_configs = mock.query_all_vault_configs(None, None); + assert_eq!(1, all_vault_configs.len()); + + let vault_config = mock.query_vault_config(&vault0); + assert!(!vault_config.whitelisted); + assert_eq!(vault_config.max_loan_to_value, new_max_ltv); +} + +#[test] +fn pagination_query() { + let mut mock = MockEnv::new().build().unwrap(); + let owner = mock.query_owner(); + let vault0 = "vault_addr_0".to_string(); + let vault1 = "vault_addr_1".to_string(); + let vault2 = "vault_addr_2".to_string(); + let vault3 = "vault_addr_3".to_string(); + let vault4 = "vault_addr_4".to_string(); + let vault5 = "vault_addr_5".to_string(); + + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault0), + }, + ) + .unwrap(); + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault1), + }, + ) + .unwrap(); + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault2), + }, + ) + .unwrap(); + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault3), + }, + ) + .unwrap(); + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault4), + }, + ) + .unwrap(); + mock.update_vault_config( + &owner, + VaultConfigUpdate::AddOrUpdate { + config: default_vault_config(&vault5), + }, + ) + .unwrap(); + + let vault_configs_a = mock.query_all_vault_configs(None, Some(2)); + let vault_configs_b = + mock.query_all_vault_configs(vault_configs_a.last().map(|r| r.addr.to_string()), Some(2)); + let vault_configs_c = + mock.query_all_vault_configs(vault_configs_b.last().map(|r| r.addr.to_string()), None); + + let combined = vault_configs_a + .iter() + .cloned() + .chain(vault_configs_b.iter().cloned()) + .chain(vault_configs_c.iter().cloned()) + .map(|r| r.addr.to_string()) + .collect::>(); + + assert_eq!(6, combined.len()); + + assert_contents_equal(&[vault0, vault1, vault2, vault3, vault4, vault5], &combined) +} diff --git a/schema.Makefile.toml b/schema.Makefile.toml index 5b4d3283..35a3ed62 100644 --- a/schema.Makefile.toml +++ b/schema.Makefile.toml @@ -15,6 +15,7 @@ fn main() -> std::io::Result<()> { "mars-incentives", "mars-red-bank", "mars-rewards-collector", + "mars-params", "mars-swapper-osmosis", "mars-swapper-astroport", "mars-oracle-osmosis", diff --git a/schemas/mars-params/mars-params.json b/schemas/mars-params/mars-params.json new file mode 100644 index 00000000..4ca016c8 --- /dev/null +++ b/schemas/mars-params/mars-params.json @@ -0,0 +1,1308 @@ +{ + "contract_name": "mars-params", + "contract_version": "1.1.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "max_close_factor", + "owner" + ], + "properties": { + "max_close_factor": { + "description": "The maximum percent a liquidator can decrease the debt amount of the liquidatee", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "owner": { + "description": "Contract's owner", + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "update_owner" + ], + "properties": { + "update_owner": { + "$ref": "#/definitions/OwnerUpdate" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_max_close_factor" + ], + "properties": { + "update_max_close_factor": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_asset_params" + ], + "properties": { + "update_asset_params": { + "$ref": "#/definitions/AssetParamsUpdate" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_vault_config" + ], + "properties": { + "update_vault_config": { + "$ref": "#/definitions/VaultConfigUpdate" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "emergency_update" + ], + "properties": { + "emergency_update": { + "$ref": "#/definitions/EmergencyUpdate" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "AssetParamsBase_for_String": { + "type": "object", + "required": [ + "credit_manager", + "denom", + "liquidation_bonus", + "liquidation_threshold", + "max_loan_to_value", + "red_bank" + ], + "properties": { + "credit_manager": { + "$ref": "#/definitions/CmSettings_for_String" + }, + "denom": { + "type": "string" + }, + "liquidation_bonus": { + "$ref": "#/definitions/Decimal" + }, + "liquidation_threshold": { + "$ref": "#/definitions/Decimal" + }, + "max_loan_to_value": { + "$ref": "#/definitions/Decimal" + }, + "red_bank": { + "$ref": "#/definitions/RedBankSettings" + } + }, + "additionalProperties": false + }, + "AssetParamsUpdate": { + "oneOf": [ + { + "type": "object", + "required": [ + "add_or_update" + ], + "properties": { + "add_or_update": { + "type": "object", + "required": [ + "params" + ], + "properties": { + "params": { + "$ref": "#/definitions/AssetParamsBase_for_String" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "CmEmergencyUpdate": { + "oneOf": [ + { + "type": "object", + "required": [ + "set_zero_max_ltv_on_vault" + ], + "properties": { + "set_zero_max_ltv_on_vault": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "set_zero_deposit_cap_on_vault" + ], + "properties": { + "set_zero_deposit_cap_on_vault": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "disallow_coin" + ], + "properties": { + "disallow_coin": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + }, + "CmSettings_for_String": { + "type": "object", + "required": [ + "whitelisted" + ], + "properties": { + "hls": { + "anyOf": [ + { + "$ref": "#/definitions/HlsParamsBase_for_String" + }, + { + "type": "null" + } + ] + }, + "whitelisted": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "EmergencyUpdate": { + "oneOf": [ + { + "type": "object", + "required": [ + "credit_manager" + ], + "properties": { + "credit_manager": { + "$ref": "#/definitions/CmEmergencyUpdate" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "red_bank" + ], + "properties": { + "red_bank": { + "$ref": "#/definitions/RedBankEmergencyUpdate" + } + }, + "additionalProperties": false + } + ] + }, + "HlsAssetType_for_String": { + "oneOf": [ + { + "type": "object", + "required": [ + "coin" + ], + "properties": { + "coin": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "vault" + ], + "properties": { + "vault": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "HlsParamsBase_for_String": { + "type": "object", + "required": [ + "correlations", + "liquidation_threshold", + "max_loan_to_value" + ], + "properties": { + "correlations": { + "description": "Given this asset is debt, correlations are the only allowed collateral which are permitted to fulfill the HLS strategy", + "type": "array", + "items": { + "$ref": "#/definitions/HlsAssetType_for_String" + } + }, + "liquidation_threshold": { + "$ref": "#/definitions/Decimal" + }, + "max_loan_to_value": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + }, + "OwnerUpdate": { + "oneOf": [ + { + "description": "Proposes a new owner to take role. Only current owner can execute.", + "type": "object", + "required": [ + "propose_new_owner" + ], + "properties": { + "propose_new_owner": { + "type": "object", + "required": [ + "proposed" + ], + "properties": { + "proposed": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Clears the currently proposed owner. Only current owner can execute.", + "type": "string", + "enum": [ + "clear_proposed" + ] + }, + { + "description": "Promotes the proposed owner to be the current one. Only the proposed owner can execute.", + "type": "string", + "enum": [ + "accept_proposed" + ] + }, + { + "description": "Throws away the keys to the Owner role forever. Once done, no owner can ever be set later.", + "type": "string", + "enum": [ + "abolish_owner_role" + ] + }, + { + "description": "A separate entity managed by Owner that can be used for granting specific emergency powers.", + "type": "object", + "required": [ + "set_emergency_owner" + ], + "properties": { + "set_emergency_owner": { + "type": "object", + "required": [ + "emergency_owner" + ], + "properties": { + "emergency_owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Remove the entity in the Emergency Owner role", + "type": "string", + "enum": [ + "clear_emergency_owner" + ] + } + ] + }, + "RedBankEmergencyUpdate": { + "oneOf": [ + { + "type": "object", + "required": [ + "disable_borrowing" + ], + "properties": { + "disable_borrowing": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + }, + "RedBankSettings": { + "type": "object", + "required": [ + "borrow_enabled", + "deposit_cap", + "deposit_enabled" + ], + "properties": { + "borrow_enabled": { + "type": "boolean" + }, + "deposit_cap": { + "$ref": "#/definitions/Uint128" + }, + "deposit_enabled": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "VaultConfigBase_for_String": { + "type": "object", + "required": [ + "addr", + "deposit_cap", + "liquidation_threshold", + "max_loan_to_value", + "whitelisted" + ], + "properties": { + "addr": { + "type": "string" + }, + "deposit_cap": { + "$ref": "#/definitions/Coin" + }, + "hls": { + "anyOf": [ + { + "$ref": "#/definitions/HlsParamsBase_for_String" + }, + { + "type": "null" + } + ] + }, + "liquidation_threshold": { + "$ref": "#/definitions/Decimal" + }, + "max_loan_to_value": { + "$ref": "#/definitions/Decimal" + }, + "whitelisted": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "VaultConfigUpdate": { + "oneOf": [ + { + "type": "object", + "required": [ + "add_or_update" + ], + "properties": { + "add_or_update": { + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "$ref": "#/definitions/VaultConfigBase_for_String" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "owner": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "asset_params" + ], + "properties": { + "asset_params": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "all_asset_params" + ], + "properties": { + "all_asset_params": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "vault_config" + ], + "properties": { + "vault_config": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "description": "Address of vault", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "all_vault_configs" + ], + "properties": { + "all_vault_configs": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "max_close_factor" + ], + "properties": { + "max_close_factor": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "migrate": null, + "sudo": null, + "responses": { + "all_asset_params": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_AssetParamsBase_for_Addr", + "type": "array", + "items": { + "$ref": "#/definitions/AssetParamsBase_for_Addr" + }, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "AssetParamsBase_for_Addr": { + "type": "object", + "required": [ + "credit_manager", + "denom", + "liquidation_bonus", + "liquidation_threshold", + "max_loan_to_value", + "red_bank" + ], + "properties": { + "credit_manager": { + "$ref": "#/definitions/CmSettings_for_Addr" + }, + "denom": { + "type": "string" + }, + "liquidation_bonus": { + "$ref": "#/definitions/Decimal" + }, + "liquidation_threshold": { + "$ref": "#/definitions/Decimal" + }, + "max_loan_to_value": { + "$ref": "#/definitions/Decimal" + }, + "red_bank": { + "$ref": "#/definitions/RedBankSettings" + } + }, + "additionalProperties": false + }, + "CmSettings_for_Addr": { + "type": "object", + "required": [ + "whitelisted" + ], + "properties": { + "hls": { + "anyOf": [ + { + "$ref": "#/definitions/HlsParamsBase_for_Addr" + }, + { + "type": "null" + } + ] + }, + "whitelisted": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "HlsAssetType_for_Addr": { + "oneOf": [ + { + "type": "object", + "required": [ + "coin" + ], + "properties": { + "coin": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "vault" + ], + "properties": { + "vault": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "HlsParamsBase_for_Addr": { + "type": "object", + "required": [ + "correlations", + "liquidation_threshold", + "max_loan_to_value" + ], + "properties": { + "correlations": { + "description": "Given this asset is debt, correlations are the only allowed collateral which are permitted to fulfill the HLS strategy", + "type": "array", + "items": { + "$ref": "#/definitions/HlsAssetType_for_Addr" + } + }, + "liquidation_threshold": { + "$ref": "#/definitions/Decimal" + }, + "max_loan_to_value": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + }, + "RedBankSettings": { + "type": "object", + "required": [ + "borrow_enabled", + "deposit_cap", + "deposit_enabled" + ], + "properties": { + "borrow_enabled": { + "type": "boolean" + }, + "deposit_cap": { + "$ref": "#/definitions/Uint128" + }, + "deposit_enabled": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "all_vault_configs": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_VaultConfigBase_for_Addr", + "type": "array", + "items": { + "$ref": "#/definitions/VaultConfigBase_for_Addr" + }, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "HlsAssetType_for_Addr": { + "oneOf": [ + { + "type": "object", + "required": [ + "coin" + ], + "properties": { + "coin": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "vault" + ], + "properties": { + "vault": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "HlsParamsBase_for_Addr": { + "type": "object", + "required": [ + "correlations", + "liquidation_threshold", + "max_loan_to_value" + ], + "properties": { + "correlations": { + "description": "Given this asset is debt, correlations are the only allowed collateral which are permitted to fulfill the HLS strategy", + "type": "array", + "items": { + "$ref": "#/definitions/HlsAssetType_for_Addr" + } + }, + "liquidation_threshold": { + "$ref": "#/definitions/Decimal" + }, + "max_loan_to_value": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "VaultConfigBase_for_Addr": { + "type": "object", + "required": [ + "addr", + "deposit_cap", + "liquidation_threshold", + "max_loan_to_value", + "whitelisted" + ], + "properties": { + "addr": { + "$ref": "#/definitions/Addr" + }, + "deposit_cap": { + "$ref": "#/definitions/Coin" + }, + "hls": { + "anyOf": [ + { + "$ref": "#/definitions/HlsParamsBase_for_Addr" + }, + { + "type": "null" + } + ] + }, + "liquidation_threshold": { + "$ref": "#/definitions/Decimal" + }, + "max_loan_to_value": { + "$ref": "#/definitions/Decimal" + }, + "whitelisted": { + "type": "boolean" + } + }, + "additionalProperties": false + } + } + }, + "asset_params": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AssetParamsBase_for_Addr", + "type": "object", + "required": [ + "credit_manager", + "denom", + "liquidation_bonus", + "liquidation_threshold", + "max_loan_to_value", + "red_bank" + ], + "properties": { + "credit_manager": { + "$ref": "#/definitions/CmSettings_for_Addr" + }, + "denom": { + "type": "string" + }, + "liquidation_bonus": { + "$ref": "#/definitions/Decimal" + }, + "liquidation_threshold": { + "$ref": "#/definitions/Decimal" + }, + "max_loan_to_value": { + "$ref": "#/definitions/Decimal" + }, + "red_bank": { + "$ref": "#/definitions/RedBankSettings" + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "CmSettings_for_Addr": { + "type": "object", + "required": [ + "whitelisted" + ], + "properties": { + "hls": { + "anyOf": [ + { + "$ref": "#/definitions/HlsParamsBase_for_Addr" + }, + { + "type": "null" + } + ] + }, + "whitelisted": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "HlsAssetType_for_Addr": { + "oneOf": [ + { + "type": "object", + "required": [ + "coin" + ], + "properties": { + "coin": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "vault" + ], + "properties": { + "vault": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "HlsParamsBase_for_Addr": { + "type": "object", + "required": [ + "correlations", + "liquidation_threshold", + "max_loan_to_value" + ], + "properties": { + "correlations": { + "description": "Given this asset is debt, correlations are the only allowed collateral which are permitted to fulfill the HLS strategy", + "type": "array", + "items": { + "$ref": "#/definitions/HlsAssetType_for_Addr" + } + }, + "liquidation_threshold": { + "$ref": "#/definitions/Decimal" + }, + "max_loan_to_value": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + }, + "RedBankSettings": { + "type": "object", + "required": [ + "borrow_enabled", + "deposit_cap", + "deposit_enabled" + ], + "properties": { + "borrow_enabled": { + "type": "boolean" + }, + "deposit_cap": { + "$ref": "#/definitions/Uint128" + }, + "deposit_enabled": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "max_close_factor": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Decimal", + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "owner": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OwnerResponse", + "description": "Returned from Owner.query()", + "type": "object", + "required": [ + "abolished", + "initialized" + ], + "properties": { + "abolished": { + "type": "boolean" + }, + "emergency_owner": { + "type": [ + "string", + "null" + ] + }, + "initialized": { + "type": "boolean" + }, + "owner": { + "type": [ + "string", + "null" + ] + }, + "proposed": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "vault_config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "VaultConfigBase_for_Addr", + "type": "object", + "required": [ + "addr", + "deposit_cap", + "liquidation_threshold", + "max_loan_to_value", + "whitelisted" + ], + "properties": { + "addr": { + "$ref": "#/definitions/Addr" + }, + "deposit_cap": { + "$ref": "#/definitions/Coin" + }, + "hls": { + "anyOf": [ + { + "$ref": "#/definitions/HlsParamsBase_for_Addr" + }, + { + "type": "null" + } + ] + }, + "liquidation_threshold": { + "$ref": "#/definitions/Decimal" + }, + "max_loan_to_value": { + "$ref": "#/definitions/Decimal" + }, + "whitelisted": { + "type": "boolean" + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "HlsAssetType_for_Addr": { + "oneOf": [ + { + "type": "object", + "required": [ + "coin" + ], + "properties": { + "coin": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "vault" + ], + "properties": { + "vault": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "HlsParamsBase_for_Addr": { + "type": "object", + "required": [ + "correlations", + "liquidation_threshold", + "max_loan_to_value" + ], + "properties": { + "correlations": { + "description": "Given this asset is debt, correlations are the only allowed collateral which are permitted to fulfill the HLS strategy", + "type": "array", + "items": { + "$ref": "#/definitions/HlsAssetType_for_Addr" + } + }, + "liquidation_threshold": { + "$ref": "#/definitions/Decimal" + }, + "max_loan_to_value": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + } + } +} diff --git a/scripts/deploy/base/deployer.ts b/scripts/deploy/base/deployer.ts index 2cdbf87d..5924f517 100644 --- a/scripts/deploy/base/deployer.ts +++ b/scripts/deploy/base/deployer.ts @@ -11,6 +11,7 @@ import assert from 'assert' import { SwapperExecuteMsg } from '../../types/config' import { InstantiateMsg as AstroportSwapperInstantiateMsg } from '../../types/generated/mars-swapper-astroport/MarsSwapperAstroport.types' import { InstantiateMsg as OsmosisSwapperInstantiateMsg } from '../../types/generated/mars-swapper-osmosis/MarsSwapperOsmosis.types' +import { InstantiateMsg as ParamsInstantiateMsg } from '../../types/generated/mars-params/MarsParams.types' import { InstantiateMsg as RedBankInstantiateMsg, QueryMsg as RedBankQueryMsg, @@ -165,6 +166,14 @@ export class Deployer { await this.instantiate('swapper', this.storage.codeIds.swapper!, msg) } + async instantiateParams() { + const msg: ParamsInstantiateMsg = { + owner: this.deployerAddress, + max_close_factor: this.config.maxCloseFactor, + } + await this.instantiate('params', this.storage.codeIds.params!, msg) + } + async setRoutes() { printBlue('Setting Swapper Routes') for (const route of this.config.swapRoutes) { diff --git a/scripts/deploy/base/index.ts b/scripts/deploy/base/index.ts index ea592577..f645ecbc 100644 --- a/scripts/deploy/base/index.ts +++ b/scripts/deploy/base/index.ts @@ -26,6 +26,7 @@ export const taskRunner = async (config: DeploymentConfig) => { await deployer.instantiateOracle(config.oracleCustomInitParams) await deployer.instantiateRewards() await deployer.instantiateSwapper() + await deployer.instantiateParams() await deployer.saveDeploymentAddrsToFile() // setup diff --git a/scripts/deploy/neutron/config.ts b/scripts/deploy/neutron/config.ts index 064bbd82..7b493e6c 100644 --- a/scripts/deploy/neutron/config.ts +++ b/scripts/deploy/neutron/config.ts @@ -110,8 +110,7 @@ export const neutronTestnetConfig: DeploymentConfig = { rewardCollectorTimeoutSeconds: 600, rpcEndpoint: 'https://rpc-palvus.pion-1.ntrn.tech:443', safetyFundFeeShare: '0.5', - deployerMnemonic: - 'bundle bundle orchard jeans office umbrella bird around taxi arrive infant discover elder they joy misery photo crunch gift fancy pledge attend adult eight', // TODO: Set mnemonic before deploying + deployerMnemonic: '', // TODO: Set mnemonic before deploying slippage_tolerance: '0.01', base_asset_symbol: 'NTRN', second_asset_symbol: 'ATOM', diff --git a/scripts/types/generated/mars-params/MarsParams.client.ts b/scripts/types/generated/mars-params/MarsParams.client.ts new file mode 100644 index 00000000..ba162860 --- /dev/null +++ b/scripts/types/generated/mars-params/MarsParams.client.ts @@ -0,0 +1,262 @@ +// @ts-nocheck +/** + * This file was automatically generated by @cosmwasm/ts-codegen@0.30.1. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run the @cosmwasm/ts-codegen generate command to regenerate this file. + */ + +import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from '@cosmjs/cosmwasm-stargate' +import { StdFee } from '@cosmjs/amino' +import { + Decimal, + InstantiateMsg, + ExecuteMsg, + OwnerUpdate, + AssetParamsUpdate, + HlsAssetTypeForString, + Uint128, + VaultConfigUpdate, + EmergencyUpdate, + CmEmergencyUpdate, + RedBankEmergencyUpdate, + AssetParamsBaseForString, + CmSettingsForString, + HlsParamsBaseForString, + RedBankSettings, + VaultConfigBaseForString, + Coin, + QueryMsg, + HlsAssetTypeForAddr, + Addr, + ArrayOfAssetParamsBaseForAddr, + AssetParamsBaseForAddr, + CmSettingsForAddr, + HlsParamsBaseForAddr, + ArrayOfVaultConfigBaseForAddr, + VaultConfigBaseForAddr, + OwnerResponse, +} from './MarsParams.types' +export interface MarsParamsReadOnlyInterface { + contractAddress: string + owner: () => Promise + assetParams: ({ denom }: { denom: string }) => Promise + allAssetParams: ({ + limit, + startAfter, + }: { + limit?: number + startAfter?: string + }) => Promise + vaultConfig: ({ address }: { address: string }) => Promise + allVaultConfigs: ({ + limit, + startAfter, + }: { + limit?: number + startAfter?: string + }) => Promise + maxCloseFactor: () => Promise +} +export class MarsParamsQueryClient implements MarsParamsReadOnlyInterface { + client: CosmWasmClient + contractAddress: string + + constructor(client: CosmWasmClient, contractAddress: string) { + this.client = client + this.contractAddress = contractAddress + this.owner = this.owner.bind(this) + this.assetParams = this.assetParams.bind(this) + this.allAssetParams = this.allAssetParams.bind(this) + this.vaultConfig = this.vaultConfig.bind(this) + this.allVaultConfigs = this.allVaultConfigs.bind(this) + this.maxCloseFactor = this.maxCloseFactor.bind(this) + } + + owner = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + owner: {}, + }) + } + assetParams = async ({ denom }: { denom: string }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + asset_params: { + denom, + }, + }) + } + allAssetParams = async ({ + limit, + startAfter, + }: { + limit?: number + startAfter?: string + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + all_asset_params: { + limit, + start_after: startAfter, + }, + }) + } + vaultConfig = async ({ address }: { address: string }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + vault_config: { + address, + }, + }) + } + allVaultConfigs = async ({ + limit, + startAfter, + }: { + limit?: number + startAfter?: string + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + all_vault_configs: { + limit, + start_after: startAfter, + }, + }) + } + maxCloseFactor = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + max_close_factor: {}, + }) + } +} +export interface MarsParamsInterface extends MarsParamsReadOnlyInterface { + contractAddress: string + sender: string + updateOwner: ( + ownerUpdate: OwnerUpdate, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[], + ) => Promise + updateMaxCloseFactor: ( + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[], + ) => Promise + updateAssetParams: ( + assetParamsUpdate: AssetParamsUpdate, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[], + ) => Promise + updateVaultConfig: ( + vaultConfigUpdate: VaultConfigUpdate, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[], + ) => Promise + emergencyUpdate: ( + emergencyUpdate: EmergencyUpdate, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[], + ) => Promise +} +export class MarsParamsClient extends MarsParamsQueryClient implements MarsParamsInterface { + client: SigningCosmWasmClient + sender: string + contractAddress: string + + constructor(client: SigningCosmWasmClient, sender: string, contractAddress: string) { + super(client, contractAddress) + this.client = client + this.sender = sender + this.contractAddress = contractAddress + this.updateOwner = this.updateOwner.bind(this) + this.updateMaxCloseFactor = this.updateMaxCloseFactor.bind(this) + this.updateAssetParams = this.updateAssetParams.bind(this) + this.updateVaultConfig = this.updateVaultConfig.bind(this) + this.emergencyUpdate = this.emergencyUpdate.bind(this) + } + + updateOwner = async ( + ownerUpdate: OwnerUpdate, + fee: number | StdFee | 'auto' = 'auto', + memo?: string, + _funds?: Coin[], + ): Promise => { + return await this.client.execute( + this.sender, + this.contractAddress, + { + update_owner: ownerUpdate, + }, + fee, + memo, + _funds, + ) + } + updateMaxCloseFactor = async ( + fee: number | StdFee | 'auto' = 'auto', + memo?: string, + _funds?: Coin[], + ): Promise => { + return await this.client.execute( + this.sender, + this.contractAddress, + { + update_max_close_factor: {}, + }, + fee, + memo, + _funds, + ) + } + updateAssetParams = async ( + assetParamsUpdate: AssetParamsUpdate, + fee: number | StdFee | 'auto' = 'auto', + memo?: string, + _funds?: Coin[], + ): Promise => { + return await this.client.execute( + this.sender, + this.contractAddress, + { + update_asset_params: assetParamsUpdate, + }, + fee, + memo, + _funds, + ) + } + updateVaultConfig = async ( + vaultConfigUpdate: VaultConfigUpdate, + fee: number | StdFee | 'auto' = 'auto', + memo?: string, + _funds?: Coin[], + ): Promise => { + return await this.client.execute( + this.sender, + this.contractAddress, + { + update_vault_config: vaultConfigUpdate, + }, + fee, + memo, + _funds, + ) + } + emergencyUpdate = async ( + emergencyUpdate: EmergencyUpdate, + fee: number | StdFee | 'auto' = 'auto', + memo?: string, + _funds?: Coin[], + ): Promise => { + return await this.client.execute( + this.sender, + this.contractAddress, + { + emergency_update: emergencyUpdate, + }, + fee, + memo, + _funds, + ) + } +} diff --git a/scripts/types/generated/mars-params/MarsParams.react-query.ts b/scripts/types/generated/mars-params/MarsParams.react-query.ts new file mode 100644 index 00000000..6f034c5a --- /dev/null +++ b/scripts/types/generated/mars-params/MarsParams.react-query.ts @@ -0,0 +1,294 @@ +// @ts-nocheck +/** + * This file was automatically generated by @cosmwasm/ts-codegen@0.30.1. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run the @cosmwasm/ts-codegen generate command to regenerate this file. + */ + +import { UseQueryOptions, useQuery, useMutation, UseMutationOptions } from '@tanstack/react-query' +import { ExecuteResult } from '@cosmjs/cosmwasm-stargate' +import { StdFee } from '@cosmjs/amino' +import { + Decimal, + InstantiateMsg, + ExecuteMsg, + OwnerUpdate, + AssetParamsUpdate, + HlsAssetTypeForString, + Uint128, + VaultConfigUpdate, + EmergencyUpdate, + CmEmergencyUpdate, + RedBankEmergencyUpdate, + AssetParamsBaseForString, + CmSettingsForString, + HlsParamsBaseForString, + RedBankSettings, + VaultConfigBaseForString, + Coin, + QueryMsg, + HlsAssetTypeForAddr, + Addr, + ArrayOfAssetParamsBaseForAddr, + AssetParamsBaseForAddr, + CmSettingsForAddr, + HlsParamsBaseForAddr, + ArrayOfVaultConfigBaseForAddr, + VaultConfigBaseForAddr, + OwnerResponse, +} from './MarsParams.types' +import { MarsParamsQueryClient, MarsParamsClient } from './MarsParams.client' +export const marsParamsQueryKeys = { + contract: [ + { + contract: 'marsParams', + }, + ] as const, + address: (contractAddress: string | undefined) => + [{ ...marsParamsQueryKeys.contract[0], address: contractAddress }] as const, + owner: (contractAddress: string | undefined, args?: Record) => + [{ ...marsParamsQueryKeys.address(contractAddress)[0], method: 'owner', args }] as const, + assetParams: (contractAddress: string | undefined, args?: Record) => + [{ ...marsParamsQueryKeys.address(contractAddress)[0], method: 'asset_params', args }] as const, + allAssetParams: (contractAddress: string | undefined, args?: Record) => + [ + { ...marsParamsQueryKeys.address(contractAddress)[0], method: 'all_asset_params', args }, + ] as const, + vaultConfig: (contractAddress: string | undefined, args?: Record) => + [{ ...marsParamsQueryKeys.address(contractAddress)[0], method: 'vault_config', args }] as const, + allVaultConfigs: (contractAddress: string | undefined, args?: Record) => + [ + { ...marsParamsQueryKeys.address(contractAddress)[0], method: 'all_vault_configs', args }, + ] as const, + maxCloseFactor: (contractAddress: string | undefined, args?: Record) => + [ + { ...marsParamsQueryKeys.address(contractAddress)[0], method: 'max_close_factor', args }, + ] as const, +} +export interface MarsParamsReactQuery { + client: MarsParamsQueryClient | undefined + options?: Omit< + UseQueryOptions, + "'queryKey' | 'queryFn' | 'initialData'" + > & { + initialData?: undefined + } +} +export interface MarsParamsMaxCloseFactorQuery + extends MarsParamsReactQuery {} +export function useMarsParamsMaxCloseFactorQuery({ + client, + options, +}: MarsParamsMaxCloseFactorQuery) { + return useQuery( + marsParamsQueryKeys.maxCloseFactor(client?.contractAddress), + () => (client ? client.maxCloseFactor() : Promise.reject(new Error('Invalid client'))), + { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, + ) +} +export interface MarsParamsAllVaultConfigsQuery + extends MarsParamsReactQuery { + args: { + limit?: number + startAfter?: string + } +} +export function useMarsParamsAllVaultConfigsQuery({ + client, + args, + options, +}: MarsParamsAllVaultConfigsQuery) { + return useQuery( + marsParamsQueryKeys.allVaultConfigs(client?.contractAddress, args), + () => + client + ? client.allVaultConfigs({ + limit: args.limit, + startAfter: args.startAfter, + }) + : Promise.reject(new Error('Invalid client')), + { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, + ) +} +export interface MarsParamsVaultConfigQuery + extends MarsParamsReactQuery { + args: { + address: string + } +} +export function useMarsParamsVaultConfigQuery({ + client, + args, + options, +}: MarsParamsVaultConfigQuery) { + return useQuery( + marsParamsQueryKeys.vaultConfig(client?.contractAddress, args), + () => + client + ? client.vaultConfig({ + address: args.address, + }) + : Promise.reject(new Error('Invalid client')), + { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, + ) +} +export interface MarsParamsAllAssetParamsQuery + extends MarsParamsReactQuery { + args: { + limit?: number + startAfter?: string + } +} +export function useMarsParamsAllAssetParamsQuery({ + client, + args, + options, +}: MarsParamsAllAssetParamsQuery) { + return useQuery( + marsParamsQueryKeys.allAssetParams(client?.contractAddress, args), + () => + client + ? client.allAssetParams({ + limit: args.limit, + startAfter: args.startAfter, + }) + : Promise.reject(new Error('Invalid client')), + { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, + ) +} +export interface MarsParamsAssetParamsQuery + extends MarsParamsReactQuery { + args: { + denom: string + } +} +export function useMarsParamsAssetParamsQuery({ + client, + args, + options, +}: MarsParamsAssetParamsQuery) { + return useQuery( + marsParamsQueryKeys.assetParams(client?.contractAddress, args), + () => + client + ? client.assetParams({ + denom: args.denom, + }) + : Promise.reject(new Error('Invalid client')), + { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, + ) +} +export interface MarsParamsOwnerQuery extends MarsParamsReactQuery {} +export function useMarsParamsOwnerQuery({ + client, + options, +}: MarsParamsOwnerQuery) { + return useQuery( + marsParamsQueryKeys.owner(client?.contractAddress), + () => (client ? client.owner() : Promise.reject(new Error('Invalid client'))), + { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, + ) +} +export interface MarsParamsEmergencyUpdateMutation { + client: MarsParamsClient + msg: EmergencyUpdate + args?: { + fee?: number | StdFee | 'auto' + memo?: string + funds?: Coin[] + } +} +export function useMarsParamsEmergencyUpdateMutation( + options?: Omit< + UseMutationOptions, + 'mutationFn' + >, +) { + return useMutation( + ({ client, msg, args: { fee, memo, funds } = {} }) => + client.emergencyUpdate(msg, fee, memo, funds), + options, + ) +} +export interface MarsParamsUpdateVaultConfigMutation { + client: MarsParamsClient + msg: VaultConfigUpdate + args?: { + fee?: number | StdFee | 'auto' + memo?: string + funds?: Coin[] + } +} +export function useMarsParamsUpdateVaultConfigMutation( + options?: Omit< + UseMutationOptions, + 'mutationFn' + >, +) { + return useMutation( + ({ client, msg, args: { fee, memo, funds } = {} }) => + client.updateVaultConfig(msg, fee, memo, funds), + options, + ) +} +export interface MarsParamsUpdateAssetParamsMutation { + client: MarsParamsClient + msg: AssetParamsUpdate + args?: { + fee?: number | StdFee | 'auto' + memo?: string + funds?: Coin[] + } +} +export function useMarsParamsUpdateAssetParamsMutation( + options?: Omit< + UseMutationOptions, + 'mutationFn' + >, +) { + return useMutation( + ({ client, msg, args: { fee, memo, funds } = {} }) => + client.updateAssetParams(msg, fee, memo, funds), + options, + ) +} +export interface MarsParamsUpdateMaxCloseFactorMutation { + client: MarsParamsClient + args?: { + fee?: number | StdFee | 'auto' + memo?: string + funds?: Coin[] + } +} +export function useMarsParamsUpdateMaxCloseFactorMutation( + options?: Omit< + UseMutationOptions, + 'mutationFn' + >, +) { + return useMutation( + ({ client, msg, args: { fee, memo, funds } = {} }) => + client.updateMaxCloseFactor(msg, fee, memo, funds), + options, + ) +} +export interface MarsParamsUpdateOwnerMutation { + client: MarsParamsClient + msg: OwnerUpdate + args?: { + fee?: number | StdFee | 'auto' + memo?: string + funds?: Coin[] + } +} +export function useMarsParamsUpdateOwnerMutation( + options?: Omit< + UseMutationOptions, + 'mutationFn' + >, +) { + return useMutation( + ({ client, msg, args: { fee, memo, funds } = {} }) => client.updateOwner(msg, fee, memo, funds), + options, + ) +} diff --git a/scripts/types/generated/mars-params/MarsParams.types.ts b/scripts/types/generated/mars-params/MarsParams.types.ts new file mode 100644 index 00000000..aa046b17 --- /dev/null +++ b/scripts/types/generated/mars-params/MarsParams.types.ts @@ -0,0 +1,195 @@ +// @ts-nocheck +/** + * This file was automatically generated by @cosmwasm/ts-codegen@0.30.1. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run the @cosmwasm/ts-codegen generate command to regenerate this file. + */ + +export type Decimal = string +export interface InstantiateMsg { + max_close_factor: Decimal + owner: string +} +export type ExecuteMsg = + | { + update_owner: OwnerUpdate + } + | { + update_max_close_factor: Decimal + } + | { + update_asset_params: AssetParamsUpdate + } + | { + update_vault_config: VaultConfigUpdate + } + | { + emergency_update: EmergencyUpdate + } +export type OwnerUpdate = + | { + propose_new_owner: { + proposed: string + } + } + | 'clear_proposed' + | 'accept_proposed' + | 'abolish_owner_role' + | { + set_emergency_owner: { + emergency_owner: string + } + } + | 'clear_emergency_owner' +export type AssetParamsUpdate = { + add_or_update: { + params: AssetParamsBaseForString + } +} +export type HlsAssetTypeForString = + | { + coin: { + denom: string + } + } + | { + vault: { + addr: string + } + } +export type Uint128 = string +export type VaultConfigUpdate = { + add_or_update: { + config: VaultConfigBaseForString + } +} +export type EmergencyUpdate = + | { + credit_manager: CmEmergencyUpdate + } + | { + red_bank: RedBankEmergencyUpdate + } +export type CmEmergencyUpdate = + | { + set_zero_max_ltv_on_vault: string + } + | { + set_zero_deposit_cap_on_vault: string + } + | { + disallow_coin: string + } +export type RedBankEmergencyUpdate = { + disable_borrowing: string +} +export interface AssetParamsBaseForString { + credit_manager: CmSettingsForString + denom: string + liquidation_bonus: Decimal + liquidation_threshold: Decimal + max_loan_to_value: Decimal + red_bank: RedBankSettings +} +export interface CmSettingsForString { + hls?: HlsParamsBaseForString | null + whitelisted: boolean +} +export interface HlsParamsBaseForString { + correlations: HlsAssetTypeForString[] + liquidation_threshold: Decimal + max_loan_to_value: Decimal +} +export interface RedBankSettings { + borrow_enabled: boolean + deposit_cap: Uint128 + deposit_enabled: boolean +} +export interface VaultConfigBaseForString { + addr: string + deposit_cap: Coin + hls?: HlsParamsBaseForString | null + liquidation_threshold: Decimal + max_loan_to_value: Decimal + whitelisted: boolean +} +export interface Coin { + amount: Uint128 + denom: string + [k: string]: unknown +} +export type QueryMsg = + | { + owner: {} + } + | { + asset_params: { + denom: string + } + } + | { + all_asset_params: { + limit?: number | null + start_after?: string | null + } + } + | { + vault_config: { + address: string + } + } + | { + all_vault_configs: { + limit?: number | null + start_after?: string | null + } + } + | { + max_close_factor: {} + } +export type HlsAssetTypeForAddr = + | { + coin: { + denom: string + } + } + | { + vault: { + addr: Addr + } + } +export type Addr = string +export type ArrayOfAssetParamsBaseForAddr = AssetParamsBaseForAddr[] +export interface AssetParamsBaseForAddr { + credit_manager: CmSettingsForAddr + denom: string + liquidation_bonus: Decimal + liquidation_threshold: Decimal + max_loan_to_value: Decimal + red_bank: RedBankSettings +} +export interface CmSettingsForAddr { + hls?: HlsParamsBaseForAddr | null + whitelisted: boolean +} +export interface HlsParamsBaseForAddr { + correlations: HlsAssetTypeForAddr[] + liquidation_threshold: Decimal + max_loan_to_value: Decimal +} +export type ArrayOfVaultConfigBaseForAddr = VaultConfigBaseForAddr[] +export interface VaultConfigBaseForAddr { + addr: Addr + deposit_cap: Coin + hls?: HlsParamsBaseForAddr | null + liquidation_threshold: Decimal + max_loan_to_value: Decimal + whitelisted: boolean +} +export interface OwnerResponse { + abolished: boolean + emergency_owner?: string | null + initialized: boolean + owner?: string | null + proposed?: string | null +} diff --git a/scripts/types/generated/mars-params/bundle.ts b/scripts/types/generated/mars-params/bundle.ts new file mode 100644 index 00000000..c78830ef --- /dev/null +++ b/scripts/types/generated/mars-params/bundle.ts @@ -0,0 +1,13 @@ +// @ts-nocheck +/** + * This file was automatically generated by @cosmwasm/ts-codegen@0.30.1. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run the @cosmwasm/ts-codegen generate command to regenerate this file. + */ + +import * as _12 from './MarsParams.types' +import * as _13 from './MarsParams.client' +import * as _14 from './MarsParams.react-query' +export namespace contracts { + export const MarsParams = { ..._12, ..._13, ..._14 } +} diff --git a/scripts/types/generated/mars-red-bank/bundle.ts b/scripts/types/generated/mars-red-bank/bundle.ts index 1277c55d..98116bdb 100644 --- a/scripts/types/generated/mars-red-bank/bundle.ts +++ b/scripts/types/generated/mars-red-bank/bundle.ts @@ -5,9 +5,9 @@ * and run the @cosmwasm/ts-codegen generate command to regenerate this file. */ -import * as _12 from './MarsRedBank.types' -import * as _13 from './MarsRedBank.client' -import * as _14 from './MarsRedBank.react-query' +import * as _15 from './MarsRedBank.types' +import * as _16 from './MarsRedBank.client' +import * as _17 from './MarsRedBank.react-query' export namespace contracts { - export const MarsRedBank = { ..._12, ..._13, ..._14 } + export const MarsRedBank = { ..._15, ..._16, ..._17 } } diff --git a/scripts/types/generated/mars-rewards-collector/bundle.ts b/scripts/types/generated/mars-rewards-collector/bundle.ts index f816bcd1..e234a6e8 100644 --- a/scripts/types/generated/mars-rewards-collector/bundle.ts +++ b/scripts/types/generated/mars-rewards-collector/bundle.ts @@ -5,9 +5,9 @@ * and run the @cosmwasm/ts-codegen generate command to regenerate this file. */ -import * as _15 from './MarsRewardsCollector.types' -import * as _16 from './MarsRewardsCollector.client' -import * as _17 from './MarsRewardsCollector.react-query' +import * as _18 from './MarsRewardsCollector.types' +import * as _19 from './MarsRewardsCollector.client' +import * as _20 from './MarsRewardsCollector.react-query' export namespace contracts { - export const MarsRewardsCollector = { ..._15, ..._16, ..._17 } + export const MarsRewardsCollector = { ..._18, ..._19, ..._20 } } diff --git a/scripts/types/generated/mars-swapper-astroport/bundle.ts b/scripts/types/generated/mars-swapper-astroport/bundle.ts index d829c0c4..9176313d 100644 --- a/scripts/types/generated/mars-swapper-astroport/bundle.ts +++ b/scripts/types/generated/mars-swapper-astroport/bundle.ts @@ -5,9 +5,9 @@ * and run the @cosmwasm/ts-codegen generate command to regenerate this file. */ -import * as _18 from './MarsSwapperAstroport.types' -import * as _19 from './MarsSwapperAstroport.client' -import * as _20 from './MarsSwapperAstroport.react-query' +import * as _21 from './MarsSwapperAstroport.types' +import * as _22 from './MarsSwapperAstroport.client' +import * as _23 from './MarsSwapperAstroport.react-query' export namespace contracts { - export const MarsSwapperAstroport = { ..._18, ..._19, ..._20 } + export const MarsSwapperAstroport = { ..._21, ..._22, ..._23 } } diff --git a/scripts/types/generated/mars-swapper-osmosis/bundle.ts b/scripts/types/generated/mars-swapper-osmosis/bundle.ts index 66a5acd8..6b318794 100644 --- a/scripts/types/generated/mars-swapper-osmosis/bundle.ts +++ b/scripts/types/generated/mars-swapper-osmosis/bundle.ts @@ -5,9 +5,9 @@ * and run the @cosmwasm/ts-codegen generate command to regenerate this file. */ -import * as _21 from './MarsSwapperOsmosis.types' -import * as _22 from './MarsSwapperOsmosis.client' -import * as _23 from './MarsSwapperOsmosis.react-query' +import * as _24 from './MarsSwapperOsmosis.types' +import * as _25 from './MarsSwapperOsmosis.client' +import * as _26 from './MarsSwapperOsmosis.react-query' export namespace contracts { - export const MarsSwapperOsmosis = { ..._21, ..._22, ..._23 } + export const MarsSwapperOsmosis = { ..._24, ..._25, ..._26 } } diff --git a/scripts/types/msg.ts b/scripts/types/msg.ts index 7a1a207e..da8eed8e 100644 --- a/scripts/types/msg.ts +++ b/scripts/types/msg.ts @@ -1,3 +1,4 @@ +import { InstantiateMsg as ParamsInstantiateMsg } from './generated/mars-params/MarsParams.types' import { InstantiateMsg as AstroportSwapperInstantiateMsg } from './generated/mars-swapper-astroport/MarsSwapperAstroport.types' import { InstantiateMsg as RedBankInstantiateMsg } from './generated/mars-red-bank/MarsRedBank.types' import { InstantiateMsg as AddressProviderInstantiateMsg } from './generated/mars-address-provider/MarsAddressProvider.types' @@ -13,6 +14,7 @@ export type InstantiateMsgs = | IncentivesInstantiateMsg | WasmOracleInstantiateMsg | RewardsInstantiateMsg + | ParamsInstantiateMsg | AstroportSwapperInstantiateMsg | OsmosisSwapperInstantiateMsg | OsmosisOracleInstantiateMsg From 83826564460654a9f0678b85cb53336c0b6f75b5 Mon Sep 17 00:00:00 2001 From: piobab Date: Tue, 20 Jun 2023 15:49:02 +0200 Subject: [PATCH 7/9] Dynamic lb cf from commons (#219) * New params for dynamic LB and CF (#16) * Add dynamic LB and CF params. * Add validators for dynamic lb. * Add tests for dynamic lb and cf. * Update schemas. * Update validation for THF. * Review fixes. * Bump params ver. --- Cargo.lock | 2 +- contracts/params/Cargo.toml | 2 +- contracts/params/src/contract.rs | 12 +- contracts/params/src/error.rs | 5 +- contracts/params/src/execute.rs | 34 ++-- contracts/params/src/msg.rs | 8 +- contracts/params/src/state.rs | 2 +- contracts/params/src/types/asset.rs | 104 +++++++++- contracts/params/tests/helpers/generator.rs | 10 +- contracts/params/tests/helpers/mock_env.rs | 24 +-- .../params/tests/test_asset_validation.rs | 187 +++++++++++++++-- contracts/params/tests/test_close_factor.rs | 65 ------ .../params/tests/test_target_health_factor.rs | 74 +++++++ schemas/mars-params/mars-params.json | 189 ++++++++++++++++-- .../mars-params/MarsParams.client.ts | 17 +- .../mars-params/MarsParams.react-query.ts | 25 +-- .../generated/mars-params/MarsParams.types.ts | 18 +- 17 files changed, 596 insertions(+), 182 deletions(-) delete mode 100644 contracts/params/tests/test_close_factor.rs create mode 100644 contracts/params/tests/test_target_health_factor.rs diff --git a/Cargo.lock b/Cargo.lock index c80456e5..ac77728f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1879,7 +1879,7 @@ dependencies = [ [[package]] name = "mars-params" -version = "1.1.0" +version = "1.0.7" dependencies = [ "anyhow", "cosmwasm-schema", diff --git a/contracts/params/Cargo.toml b/contracts/params/Cargo.toml index b5ee0b97..1f449d6f 100644 --- a/contracts/params/Cargo.toml +++ b/contracts/params/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "mars-params" description = "Contract storing the asset params for Credit Manager and Red Bank." -version = { workspace = true } +version = "1.0.7" authors = { workspace = true } license = { workspace = true } edition = { workspace = true } diff --git a/contracts/params/src/contract.rs b/contracts/params/src/contract.rs index 87f73c63..71ffd2b0 100644 --- a/contracts/params/src/contract.rs +++ b/contracts/params/src/contract.rs @@ -7,13 +7,13 @@ use mars_owner::OwnerInit::SetInitialOwner; use crate::{ emergency_powers::{disable_borrowing, disallow_coin, set_zero_deposit_cap, set_zero_max_ltv}, error::ContractResult, - execute::{assert_mcf, update_asset_params, update_max_close_factor, update_vault_config}, + execute::{assert_thf, update_asset_params, update_target_health_factor, update_vault_config}, msg::{ CmEmergencyUpdate, EmergencyUpdate, ExecuteMsg, InstantiateMsg, QueryMsg, RedBankEmergencyUpdate, }, query::{query_all_asset_params, query_all_vault_configs, query_vault_config}, - state::{ASSET_PARAMS, MAX_CLOSE_FACTOR, OWNER}, + state::{ASSET_PARAMS, OWNER, TARGET_HEALTH_FACTOR}, }; const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); @@ -36,8 +36,8 @@ pub fn instantiate( }, )?; - assert_mcf(msg.max_close_factor)?; - MAX_CLOSE_FACTOR.save(deps.storage, &msg.max_close_factor)?; + assert_thf(msg.target_health_factor)?; + TARGET_HEALTH_FACTOR.save(deps.storage, &msg.target_health_factor)?; Ok(Response::default()) } @@ -52,7 +52,7 @@ pub fn execute( match msg { ExecuteMsg::UpdateOwner(update) => Ok(OWNER.update(deps, info, update)?), ExecuteMsg::UpdateAssetParams(update) => update_asset_params(deps, info, update), - ExecuteMsg::UpdateMaxCloseFactor(mcf) => update_max_close_factor(deps, info, mcf), + ExecuteMsg::UpdateTargetHealthFactor(mcf) => update_target_health_factor(deps, info, mcf), ExecuteMsg::UpdateVaultConfig(update) => update_vault_config(deps, info, update), ExecuteMsg::EmergencyUpdate(update) => match update { EmergencyUpdate::RedBank(rb_u) => match rb_u { @@ -89,7 +89,7 @@ pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> ContractResult { start_after, limit, } => to_binary(&query_all_vault_configs(deps, start_after, limit)?), - QueryMsg::MaxCloseFactor {} => to_binary(&MAX_CLOSE_FACTOR.load(deps.storage)?), + QueryMsg::TargetHealthFactor {} => to_binary(&TARGET_HEALTH_FACTOR.load(deps.storage)?), }; res.map_err(Into::into) } diff --git a/contracts/params/src/error.rs b/contracts/params/src/error.rs index b19167c5..3d6497e7 100644 --- a/contracts/params/src/error.rs +++ b/contracts/params/src/error.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::StdError; +use cosmwasm_std::{DecimalRangeExceeded, StdError}; use mars_owner::OwnerError; pub use mars_utils::error::ValidationError; use thiserror::Error; @@ -10,6 +10,9 @@ pub enum ContractError { #[error("{0}")] Std(#[from] StdError), + #[error("{0}")] + DecimalRangeExceeded(#[from] DecimalRangeExceeded), + #[error("{0}")] Owner(#[from] OwnerError), diff --git a/contracts/params/src/execute.rs b/contracts/params/src/execute.rs index bff2a7d9..7d03967d 100644 --- a/contracts/params/src/execute.rs +++ b/contracts/params/src/execute.rs @@ -2,27 +2,27 @@ use cosmwasm_std::{Decimal, DepsMut, MessageInfo, Response}; use mars_utils::error::ValidationError; use crate::{ - error::ContractResult, + error::{ContractError, ContractResult}, msg::{AssetParamsUpdate, VaultConfigUpdate}, - state::{ASSET_PARAMS, MAX_CLOSE_FACTOR, OWNER, VAULT_CONFIGS}, + state::{ASSET_PARAMS, OWNER, TARGET_HEALTH_FACTOR, VAULT_CONFIGS}, }; pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); -pub fn update_max_close_factor( +pub fn update_target_health_factor( deps: DepsMut, info: MessageInfo, - max_close_factor: Decimal, + target_health_factor: Decimal, ) -> ContractResult { OWNER.assert_owner(deps.storage, &info.sender)?; - assert_mcf(max_close_factor)?; - MAX_CLOSE_FACTOR.save(deps.storage, &max_close_factor)?; + assert_thf(target_health_factor)?; + TARGET_HEALTH_FACTOR.save(deps.storage, &target_health_factor)?; let response = Response::new() - .add_attribute("action", "update_max_close_factor") - .add_attribute("value", max_close_factor.to_string()); + .add_attribute("action", "update_target_health_factor") + .add_attribute("value", target_health_factor.to_string()); Ok(response) } @@ -76,16 +76,16 @@ pub fn update_vault_config( Ok(response) } -pub fn assert_mcf(param_value: Decimal) -> Result<(), ValidationError> { - if !param_value.le(&Decimal::one()) { - Err(ValidationError::InvalidParam { - param_name: "max-close-factor".to_string(), - invalid_value: "max-close-factor".to_string(), - predicate: "<= 1".to_string(), - }) - } else { - Ok(()) +pub fn assert_thf(thf: Decimal) -> Result<(), ContractError> { + if thf < Decimal::one() || thf > Decimal::from_atomics(2u128, 0u32)? { + return Err(ValidationError::InvalidParam { + param_name: "target_health_factor".to_string(), + invalid_value: thf.to_string(), + predicate: "[1, 2]".to_string(), + } + .into()); } + Ok(()) } /// liquidation_threshold should be greater than or equal to max_loan_to_value diff --git a/contracts/params/src/msg.rs b/contracts/params/src/msg.rs index 3029293f..83669109 100644 --- a/contracts/params/src/msg.rs +++ b/contracts/params/src/msg.rs @@ -8,14 +8,14 @@ use crate::types::{asset::AssetParamsUnchecked, vault::VaultConfigUnchecked}; pub struct InstantiateMsg { /// Contract's owner pub owner: String, - /// The maximum percent a liquidator can decrease the debt amount of the liquidatee - pub max_close_factor: Decimal, + /// Determines the ideal HF a position should be left at immediately after the position has been liquidated. + pub target_health_factor: Decimal, } #[cw_serde] pub enum ExecuteMsg { UpdateOwner(OwnerUpdate), - UpdateMaxCloseFactor(Decimal), + UpdateTargetHealthFactor(Decimal), UpdateAssetParams(AssetParamsUpdate), UpdateVaultConfig(VaultConfigUpdate), EmergencyUpdate(EmergencyUpdate), @@ -51,7 +51,7 @@ pub enum QueryMsg { }, #[returns(Decimal)] - MaxCloseFactor {}, + TargetHealthFactor {}, } #[cw_serde] diff --git a/contracts/params/src/state.rs b/contracts/params/src/state.rs index 00a07135..efa0169a 100644 --- a/contracts/params/src/state.rs +++ b/contracts/params/src/state.rs @@ -7,4 +7,4 @@ use crate::types::{asset::AssetParams, vault::VaultConfig}; pub const OWNER: Owner = Owner::new("owner"); pub const ASSET_PARAMS: Map<&str, AssetParams> = Map::new("asset_params"); pub const VAULT_CONFIGS: Map<&Addr, VaultConfig> = Map::new("vault_configs"); -pub const MAX_CLOSE_FACTOR: Item = Item::new("max_close_factor"); +pub const TARGET_HEALTH_FACTOR: Item = Item::new("target_health_factor"); diff --git a/contracts/params/src/types/asset.rs b/contracts/params/src/types/asset.rs index 1462f1db..bd606a4e 100644 --- a/contracts/params/src/types/asset.rs +++ b/contracts/params/src/types/asset.rs @@ -1,6 +1,9 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Api, Decimal, Uint128}; -use mars_utils::helpers::{decimal_param_le_one, decimal_param_lt_one, validate_native_denom}; +use mars_utils::{ + error::ValidationError, + helpers::{decimal_param_le_one, decimal_param_lt_one, validate_native_denom}, +}; use crate::{ error::ContractResult, @@ -21,6 +24,95 @@ pub struct RedBankSettings { pub deposit_cap: Uint128, } +/// The LB will depend on the Health Factor and a couple other parameters as follows: +/// Liquidation Bonus = min( +/// b + (slope * (1 - HF)), +/// max( +/// min(CR - 1, max_lb), +/// min_lb +/// ) +/// ) +#[cw_serde] +pub struct LiquidationBonus { + /// Marks the level at which the LB starts when HF drops marginally below 1. + /// If set at 1%, at HF = 0.999 the LB will be 1%. If set at 0%, the LB starts increasing from 0% as the HF drops below 1. + pub starting_lb: Decimal, + /// Defines the slope at which the LB increases as the HF decreases. + /// The higher the slope, the faster the LB increases as the HF decreases. + pub slope: Decimal, + /// Minimum LB that will be granted to liquidators even when the position is undercollateralized. + pub min_lb: Decimal, + /// Maximum LB that can be granted to a liquidator; in other words, the maxLB establishes a ceiling to the LB. + /// This is a precautionary parameter to mitigate liquidated users being over-punished. + pub max_lb: Decimal, +} + +impl LiquidationBonus { + pub fn validate(&self) -> Result<(), ValidationError> { + assert_starting_lb_within_range(self.starting_lb)?; + assert_lb_slope_within_range(self.slope)?; + assert_min_lb_within_range(self.min_lb)?; + assert_max_lb_within_range(self.max_lb)?; + assert_max_lb_gt_min_lb(self.min_lb, self.max_lb)?; + Ok(()) + } +} + +fn assert_starting_lb_within_range(b: Decimal) -> Result<(), ValidationError> { + if b > Decimal::percent(10) { + return Err(ValidationError::InvalidParam { + param_name: "starting_lb".to_string(), + invalid_value: b.to_string(), + predicate: "[0, 0.1]".to_string(), + }); + } + Ok(()) +} + +fn assert_lb_slope_within_range(slope: Decimal) -> Result<(), ValidationError> { + if slope < Decimal::one() || slope > Decimal::from_ratio(5u8, 1u8) { + return Err(ValidationError::InvalidParam { + param_name: "slope".to_string(), + invalid_value: slope.to_string(), + predicate: "[1, 5]".to_string(), + }); + } + Ok(()) +} + +fn assert_min_lb_within_range(min_lb: Decimal) -> Result<(), ValidationError> { + if min_lb > Decimal::percent(10) { + return Err(ValidationError::InvalidParam { + param_name: "min_lb".to_string(), + invalid_value: min_lb.to_string(), + predicate: "[0, 0.1]".to_string(), + }); + } + Ok(()) +} + +fn assert_max_lb_within_range(max_lb: Decimal) -> Result<(), ValidationError> { + if max_lb < Decimal::percent(5) || max_lb > Decimal::percent(30) { + return Err(ValidationError::InvalidParam { + param_name: "max_lb".to_string(), + invalid_value: max_lb.to_string(), + predicate: "[0.05, 0.3]".to_string(), + }); + } + Ok(()) +} + +fn assert_max_lb_gt_min_lb(min_lb: Decimal, max_lb: Decimal) -> Result<(), ValidationError> { + if min_lb > max_lb { + return Err(ValidationError::InvalidParam { + param_name: "max_lb".to_string(), + invalid_value: max_lb.to_string(), + predicate: format!("> {} (min LB)", min_lb), + }); + } + Ok(()) +} + #[cw_serde] pub struct AssetParamsBase { pub denom: String, @@ -28,7 +120,8 @@ pub struct AssetParamsBase { pub red_bank: RedBankSettings, pub max_loan_to_value: Decimal, pub liquidation_threshold: Decimal, - pub liquidation_bonus: Decimal, + pub liquidation_bonus: LiquidationBonus, + pub protocol_liquidation_fee: Decimal, } pub type AssetParams = AssetParamsBase; @@ -46,6 +139,7 @@ impl From for AssetParamsUnchecked { max_loan_to_value: p.max_loan_to_value, liquidation_threshold: p.liquidation_threshold, liquidation_bonus: p.liquidation_bonus, + protocol_liquidation_fee: p.protocol_liquidation_fee, } } } @@ -58,7 +152,8 @@ impl AssetParamsUnchecked { decimal_param_le_one(self.liquidation_threshold, "liquidation_threshold")?; assert_lqt_gt_max_ltv(self.max_loan_to_value, self.liquidation_threshold)?; - decimal_param_le_one(self.liquidation_bonus, "liquidation_bonus")?; + self.liquidation_bonus.validate()?; + decimal_param_lt_one(self.protocol_liquidation_fee, "protocol_liquidation_fee")?; if let Some(hls) = self.credit_manager.hls.as_ref() { decimal_param_lt_one(hls.max_loan_to_value, "hls_max_loan_to_value")?; @@ -77,7 +172,8 @@ impl AssetParamsUnchecked { red_bank: self.red_bank.clone(), max_loan_to_value: self.max_loan_to_value, liquidation_threshold: self.liquidation_threshold, - liquidation_bonus: self.liquidation_bonus, + liquidation_bonus: self.liquidation_bonus.clone(), + protocol_liquidation_fee: self.protocol_liquidation_fee, }) } } diff --git a/contracts/params/tests/helpers/generator.rs b/contracts/params/tests/helpers/generator.rs index 2bcbd029..e5ee7dbc 100644 --- a/contracts/params/tests/helpers/generator.rs +++ b/contracts/params/tests/helpers/generator.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use cosmwasm_std::{coin, Decimal, Uint128}; use mars_params::types::{ - asset::{AssetParamsUnchecked, CmSettings, RedBankSettings}, + asset::{AssetParamsUnchecked, CmSettings, LiquidationBonus, RedBankSettings}, vault::VaultConfigUnchecked, }; @@ -20,7 +20,13 @@ pub fn default_asset_params(denom: &str) -> AssetParamsUnchecked { }, max_loan_to_value: Decimal::from_str("0.6").unwrap(), liquidation_threshold: Decimal::from_str("0.7").unwrap(), - liquidation_bonus: Decimal::from_str("0.15").unwrap(), + liquidation_bonus: LiquidationBonus { + starting_lb: Decimal::percent(4), + slope: Decimal::from_str("2.0").unwrap(), + min_lb: Decimal::percent(1), + max_lb: Decimal::percent(8), + }, + protocol_liquidation_fee: Decimal::percent(2), } } diff --git a/contracts/params/tests/helpers/mock_env.rs b/contracts/params/tests/helpers/mock_env.rs index 0ae03033..e5b291d2 100644 --- a/contracts/params/tests/helpers/mock_env.rs +++ b/contracts/params/tests/helpers/mock_env.rs @@ -20,7 +20,7 @@ pub struct MockEnv { pub struct MockEnvBuilder { pub app: BasicApp, - pub max_close_factor: Option, + pub target_health_factor: Option, pub emergency_owner: Option, } @@ -29,7 +29,7 @@ impl MockEnv { pub fn new() -> MockEnvBuilder { MockEnvBuilder { app: App::default(), - max_close_factor: None, + target_health_factor: None, emergency_owner: None, } } @@ -73,15 +73,15 @@ impl MockEnv { ) } - pub fn update_max_close_factor( + pub fn update_target_health_factor( &mut self, sender: &Addr, - mcf: Decimal, + thf: Decimal, ) -> AnyResult { self.app.execute_contract( sender.clone(), self.params_contract.clone(), - &ExecuteMsg::UpdateMaxCloseFactor(mcf), + &ExecuteMsg::UpdateTargetHealthFactor(thf), &[], ) } @@ -170,10 +170,10 @@ impl MockEnv { .unwrap() } - pub fn query_max_close_factor(&self) -> Decimal { + pub fn query_target_health_factor(&self) -> Decimal { self.app .wrap() - .query_wasm_smart(self.params_contract.clone(), &QueryMsg::MaxCloseFactor {}) + .query_wasm_smart(self.params_contract.clone(), &QueryMsg::TargetHealthFactor {}) .unwrap() } } @@ -187,7 +187,7 @@ impl MockEnvBuilder { Addr::unchecked("owner"), &InstantiateMsg { owner: "owner".to_string(), - max_close_factor: self.get_max_close_factor(), + target_health_factor: self.get_target_health_factor(), }, &[], "mock-params-contract", @@ -221,15 +221,15 @@ impl MockEnvBuilder { // Get or defaults //-------------------------------------------------------------------------------------------------- - pub fn get_max_close_factor(&self) -> Decimal { - self.max_close_factor.unwrap_or(Decimal::from_str("0.5").unwrap()) + pub fn get_target_health_factor(&self) -> Decimal { + self.target_health_factor.unwrap_or(Decimal::from_str("1.05").unwrap()) } //-------------------------------------------------------------------------------------------------- // Setter functions //-------------------------------------------------------------------------------------------------- - pub fn max_close_factor(&mut self, mcf: Decimal) -> &mut Self { - self.max_close_factor = Some(mcf); + pub fn target_health_factor(&mut self, thf: Decimal) -> &mut Self { + self.target_health_factor = Some(thf); self } diff --git a/contracts/params/tests/test_asset_validation.rs b/contracts/params/tests/test_asset_validation.rs index 6ffc92c7..0c7f7b35 100644 --- a/contracts/params/tests/test_asset_validation.rs +++ b/contracts/params/tests/test_asset_validation.rs @@ -75,28 +75,6 @@ fn liquidation_threshold_less_than_or_equal_to_one() { ); } -#[test] -fn liquidation_bonus_less_than_or_equal_to_one() { - let mut mock = MockEnv::new().build().unwrap(); - let mut params = default_asset_params("denom_xyz"); - params.liquidation_bonus = Decimal::from_str("1.1235").unwrap(); - - let res = mock.update_asset_params( - &mock.query_owner(), - AssetParamsUpdate::AddOrUpdate { - params, - }, - ); - assert_err( - res, - Validation(InvalidParam { - param_name: "liquidation_bonus".to_string(), - invalid_value: "1.1235".to_string(), - predicate: "<= 1".to_string(), - }), - ); -} - #[test] fn liq_threshold_gt_max_ltv() { let mut mock = MockEnv::new().build().unwrap(); @@ -223,3 +201,168 @@ fn correlations_must_be_valid_denoms() { }), ); } + +#[test] +fn protocol_liquidation_fee_less_than_one() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + params.protocol_liquidation_fee = Decimal::from_str("1").unwrap(); + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "protocol_liquidation_fee".to_string(), + invalid_value: "1".to_string(), + predicate: "< 1".to_string(), + }), + ); +} + +#[test] +fn liquidation_bonus_param_b_out_of_range() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + params.liquidation_bonus.starting_lb = Decimal::from_str("0.101").unwrap(); + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "starting_lb".to_string(), + invalid_value: "0.101".to_string(), + predicate: "[0, 0.1]".to_string(), + }), + ); +} + +#[test] +fn liquidation_bonus_param_slope_out_of_range() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + + params.liquidation_bonus.slope = Decimal::from_str("0.99").unwrap(); + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params: params.clone(), + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "slope".to_string(), + invalid_value: "0.99".to_string(), + predicate: "[1, 5]".to_string(), + }), + ); + + params.liquidation_bonus.slope = Decimal::from_str("5.01").unwrap(); + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "slope".to_string(), + invalid_value: "5.01".to_string(), + predicate: "[1, 5]".to_string(), + }), + ); +} + +#[test] +fn liquidation_bonus_param_min_lb_out_of_range() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + params.liquidation_bonus.min_lb = Decimal::from_str("0.101").unwrap(); + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "min_lb".to_string(), + invalid_value: "0.101".to_string(), + predicate: "[0, 0.1]".to_string(), + }), + ); +} + +#[test] +fn liquidation_bonus_param_max_lb_out_of_range() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + + params.liquidation_bonus.max_lb = Decimal::from_str("0.0499").unwrap(); + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params: params.clone(), + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "max_lb".to_string(), + invalid_value: "0.0499".to_string(), + predicate: "[0.05, 0.3]".to_string(), + }), + ); + + params.liquidation_bonus.max_lb = Decimal::from_str("0.31").unwrap(); + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "max_lb".to_string(), + invalid_value: "0.31".to_string(), + predicate: "[0.05, 0.3]".to_string(), + }), + ); +} + +#[test] +fn liquidation_bonus_param_max_lb_gt_min_lb() { + let mut mock = MockEnv::new().build().unwrap(); + let mut params = default_asset_params("denom_xyz"); + params.liquidation_bonus.min_lb = Decimal::from_str("0.08").unwrap(); + params.liquidation_bonus.max_lb = Decimal::from_str("0.07").unwrap(); + + let res = mock.update_asset_params( + &mock.query_owner(), + AssetParamsUpdate::AddOrUpdate { + params, + }, + ); + assert_err( + res, + Validation(InvalidParam { + param_name: "max_lb".to_string(), + invalid_value: "0.07".to_string(), + predicate: "> 0.08 (min LB)".to_string(), + }), + ); +} diff --git a/contracts/params/tests/test_close_factor.rs b/contracts/params/tests/test_close_factor.rs deleted file mode 100644 index 7b23d0bd..00000000 --- a/contracts/params/tests/test_close_factor.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::str::FromStr; - -use cosmwasm_std::{Addr, Decimal}; -use mars_owner::{OwnerError, OwnerUpdate}; -use mars_params::error::ContractError::{Owner, Validation}; -use mars_utils::error::ValidationError::InvalidParam; - -use crate::helpers::{assert_err, MockEnv}; - -pub mod helpers; - -#[test] -fn mcf_set_on_init() { - let mock = MockEnv::new().build().unwrap(); - let mcf = mock.query_max_close_factor(); - assert_eq!(mcf, Decimal::from_str("0.5").unwrap()) -} - -#[test] -fn mcf_validated_on_init() { - let res = MockEnv::new().max_close_factor(Decimal::from_str("1.23").unwrap()).build(); - if res.is_ok() { - panic!("Should have thrown an instantiate error"); - } -} - -#[test] -fn only_owner_can_update_mcf() { - let mut mock = MockEnv::new().build().unwrap(); - let bad_guy = Addr::unchecked("doctor_otto_983"); - let res = mock.update_owner( - &bad_guy, - OwnerUpdate::ProposeNewOwner { - proposed: bad_guy.to_string(), - }, - ); - assert_err(res, Owner(OwnerError::NotOwner {})); -} - -#[test] -fn validated_updates() { - let mut mock = MockEnv::new().build().unwrap(); - let res = mock.update_max_close_factor(&mock.query_owner(), Decimal::from_str("1.9").unwrap()); - assert_err( - res, - Validation(InvalidParam { - param_name: "max-close-factor".to_string(), - invalid_value: "max-close-factor".to_string(), - predicate: "<= 1".to_string(), - }), - ); -} - -#[test] -fn update_mcf() { - let mut mock = MockEnv::new().build().unwrap(); - let new_max_close_factor = Decimal::from_str("0.9").unwrap(); - let current_mcf = mock.query_max_close_factor(); - assert_ne!(current_mcf, new_max_close_factor); - - mock.update_max_close_factor(&mock.query_owner(), Decimal::from_str("0.9").unwrap()).unwrap(); - - let current_mcf = mock.query_max_close_factor(); - assert_eq!(current_mcf, new_max_close_factor); -} diff --git a/contracts/params/tests/test_target_health_factor.rs b/contracts/params/tests/test_target_health_factor.rs new file mode 100644 index 00000000..47c2cf2a --- /dev/null +++ b/contracts/params/tests/test_target_health_factor.rs @@ -0,0 +1,74 @@ +use std::str::FromStr; + +use cosmwasm_std::{Addr, Decimal}; +use mars_owner::OwnerError; +use mars_params::error::ContractError::{Owner, Validation}; +use mars_utils::error::ValidationError::InvalidParam; + +use crate::helpers::{assert_err, MockEnv}; + +pub mod helpers; + +#[test] +fn thf_set_on_init() { + let mock = MockEnv::new().build().unwrap(); + let thf = mock.query_target_health_factor(); + assert_eq!(thf, Decimal::from_str("1.05").unwrap()) +} + +#[test] +fn thf_validated_on_init() { + let res = MockEnv::new().target_health_factor(Decimal::from_str("0.99").unwrap()).build(); + if res.is_ok() { + panic!("Should have thrown an instantiate error"); + } +} + +#[test] +fn only_owner_can_update_thf() { + let mut mock = MockEnv::new().build().unwrap(); + let bad_guy = Addr::unchecked("doctor_otto_983"); + let res = mock.update_target_health_factor(&bad_guy, Decimal::from_str("1.1").unwrap()); + assert_err(res, Owner(OwnerError::NotOwner {})); +} + +#[test] +fn validated_updates() { + let mut mock = MockEnv::new().build().unwrap(); + + let res = + mock.update_target_health_factor(&mock.query_owner(), Decimal::from_str("0.99").unwrap()); + assert_err( + res, + Validation(InvalidParam { + param_name: "target_health_factor".to_string(), + invalid_value: "0.99".to_string(), + predicate: "[1, 2]".to_string(), + }), + ); + + let res = + mock.update_target_health_factor(&mock.query_owner(), Decimal::from_str("2.01").unwrap()); + assert_err( + res, + Validation(InvalidParam { + param_name: "target_health_factor".to_string(), + invalid_value: "2.01".to_string(), + predicate: "[1, 2]".to_string(), + }), + ); +} + +#[test] +fn update_thf() { + let mut mock = MockEnv::new().build().unwrap(); + let target_health_factor = Decimal::from_str("1.08").unwrap(); + let current_thf = mock.query_target_health_factor(); + assert_ne!(current_thf, target_health_factor); + + mock.update_target_health_factor(&mock.query_owner(), Decimal::from_str("1.08").unwrap()) + .unwrap(); + + let current_thf = mock.query_target_health_factor(); + assert_eq!(current_thf, target_health_factor); +} diff --git a/schemas/mars-params/mars-params.json b/schemas/mars-params/mars-params.json index 4ca016c8..3476df19 100644 --- a/schemas/mars-params/mars-params.json +++ b/schemas/mars-params/mars-params.json @@ -7,21 +7,21 @@ "title": "InstantiateMsg", "type": "object", "required": [ - "max_close_factor", - "owner" + "owner", + "target_health_factor" ], "properties": { - "max_close_factor": { - "description": "The maximum percent a liquidator can decrease the debt amount of the liquidatee", + "owner": { + "description": "Contract's owner", + "type": "string" + }, + "target_health_factor": { + "description": "Determines the ideal HF a position should be left at immediately after the position has been liquidated.", "allOf": [ { "$ref": "#/definitions/Decimal" } ] - }, - "owner": { - "description": "Contract's owner", - "type": "string" } }, "additionalProperties": false, @@ -51,10 +51,10 @@ { "type": "object", "required": [ - "update_max_close_factor" + "update_target_health_factor" ], "properties": { - "update_max_close_factor": { + "update_target_health_factor": { "$ref": "#/definitions/Decimal" } }, @@ -106,6 +106,7 @@ "liquidation_bonus", "liquidation_threshold", "max_loan_to_value", + "protocol_liquidation_fee", "red_bank" ], "properties": { @@ -116,7 +117,7 @@ "type": "string" }, "liquidation_bonus": { - "$ref": "#/definitions/Decimal" + "$ref": "#/definitions/LiquidationBonus" }, "liquidation_threshold": { "$ref": "#/definitions/Decimal" @@ -124,6 +125,9 @@ "max_loan_to_value": { "$ref": "#/definitions/Decimal" }, + "protocol_liquidation_fee": { + "$ref": "#/definitions/Decimal" + }, "red_bank": { "$ref": "#/definitions/RedBankSettings" } @@ -334,6 +338,51 @@ }, "additionalProperties": false }, + "LiquidationBonus": { + "description": "The LB will depend on the Health Factor and a couple other parameters as follows: Liquidation Bonus = min( b + (slope * (1 - HF)), max( min(CR - 1, max_lb), min_lb ) )", + "type": "object", + "required": [ + "max_lb", + "min_lb", + "slope", + "starting_lb" + ], + "properties": { + "max_lb": { + "description": "Maximum LB that can be granted to a liquidator; in other words, the maxLB establishes a ceiling to the LB. This is a precautionary parameter to mitigate liquidated users being over-punished.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "min_lb": { + "description": "Minimum LB that will be granted to liquidators even when the position is undercollateralized.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "slope": { + "description": "Defines the slope at which the LB increases as the HF decreases. The higher the slope, the faster the LB increases as the HF decreases.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "starting_lb": { + "description": "Marks the level at which the LB starts when HF drops marginally below 1. If set at 1%, at HF = 0.999 the LB will be 1%. If set at 0%, the LB starts increasing from 0% as the HF drops below 1.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, "OwnerUpdate": { "oneOf": [ { @@ -636,10 +685,10 @@ { "type": "object", "required": [ - "max_close_factor" + "target_health_factor" ], "properties": { - "max_close_factor": { + "target_health_factor": { "type": "object", "additionalProperties": false } @@ -671,6 +720,7 @@ "liquidation_bonus", "liquidation_threshold", "max_loan_to_value", + "protocol_liquidation_fee", "red_bank" ], "properties": { @@ -681,7 +731,7 @@ "type": "string" }, "liquidation_bonus": { - "$ref": "#/definitions/Decimal" + "$ref": "#/definitions/LiquidationBonus" }, "liquidation_threshold": { "$ref": "#/definitions/Decimal" @@ -689,6 +739,9 @@ "max_loan_to_value": { "$ref": "#/definitions/Decimal" }, + "protocol_liquidation_fee": { + "$ref": "#/definitions/Decimal" + }, "red_bank": { "$ref": "#/definitions/RedBankSettings" } @@ -791,6 +844,51 @@ }, "additionalProperties": false }, + "LiquidationBonus": { + "description": "The LB will depend on the Health Factor and a couple other parameters as follows: Liquidation Bonus = min( b + (slope * (1 - HF)), max( min(CR - 1, max_lb), min_lb ) )", + "type": "object", + "required": [ + "max_lb", + "min_lb", + "slope", + "starting_lb" + ], + "properties": { + "max_lb": { + "description": "Maximum LB that can be granted to a liquidator; in other words, the maxLB establishes a ceiling to the LB. This is a precautionary parameter to mitigate liquidated users being over-punished.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "min_lb": { + "description": "Minimum LB that will be granted to liquidators even when the position is undercollateralized.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "slope": { + "description": "Defines the slope at which the LB increases as the HF decreases. The higher the slope, the faster the LB increases as the HF decreases.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "starting_lb": { + "description": "Marks the level at which the LB starts when HF drops marginally below 1. If set at 1%, at HF = 0.999 the LB will be 1%. If set at 0%, the LB starts increasing from 0% as the HF drops below 1.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, "RedBankSettings": { "type": "object", "required": [ @@ -972,6 +1070,7 @@ "liquidation_bonus", "liquidation_threshold", "max_loan_to_value", + "protocol_liquidation_fee", "red_bank" ], "properties": { @@ -982,7 +1081,7 @@ "type": "string" }, "liquidation_bonus": { - "$ref": "#/definitions/Decimal" + "$ref": "#/definitions/LiquidationBonus" }, "liquidation_threshold": { "$ref": "#/definitions/Decimal" @@ -990,6 +1089,9 @@ "max_loan_to_value": { "$ref": "#/definitions/Decimal" }, + "protocol_liquidation_fee": { + "$ref": "#/definitions/Decimal" + }, "red_bank": { "$ref": "#/definitions/RedBankSettings" } @@ -1096,6 +1198,51 @@ }, "additionalProperties": false }, + "LiquidationBonus": { + "description": "The LB will depend on the Health Factor and a couple other parameters as follows: Liquidation Bonus = min( b + (slope * (1 - HF)), max( min(CR - 1, max_lb), min_lb ) )", + "type": "object", + "required": [ + "max_lb", + "min_lb", + "slope", + "starting_lb" + ], + "properties": { + "max_lb": { + "description": "Maximum LB that can be granted to a liquidator; in other words, the maxLB establishes a ceiling to the LB. This is a precautionary parameter to mitigate liquidated users being over-punished.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "min_lb": { + "description": "Minimum LB that will be granted to liquidators even when the position is undercollateralized.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "slope": { + "description": "Defines the slope at which the LB increases as the HF decreases. The higher the slope, the faster the LB increases as the HF decreases.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "starting_lb": { + "description": "Marks the level at which the LB starts when HF drops marginally below 1. If set at 1%, at HF = 0.999 the LB will be 1%. If set at 0%, the LB starts increasing from 0% as the HF drops below 1.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, "RedBankSettings": { "type": "object", "required": [ @@ -1122,12 +1269,6 @@ } } }, - "max_close_factor": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Decimal", - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, "owner": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "OwnerResponse", @@ -1165,6 +1306,12 @@ }, "additionalProperties": false }, + "target_health_factor": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Decimal", + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, "vault_config": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "VaultConfigBase_for_Addr", diff --git a/scripts/types/generated/mars-params/MarsParams.client.ts b/scripts/types/generated/mars-params/MarsParams.client.ts index ba162860..ef6a19f0 100644 --- a/scripts/types/generated/mars-params/MarsParams.client.ts +++ b/scripts/types/generated/mars-params/MarsParams.client.ts @@ -22,6 +22,7 @@ import { AssetParamsBaseForString, CmSettingsForString, HlsParamsBaseForString, + LiquidationBonus, RedBankSettings, VaultConfigBaseForString, Coin, @@ -55,7 +56,7 @@ export interface MarsParamsReadOnlyInterface { limit?: number startAfter?: string }) => Promise - maxCloseFactor: () => Promise + targetHealthFactor: () => Promise } export class MarsParamsQueryClient implements MarsParamsReadOnlyInterface { client: CosmWasmClient @@ -69,7 +70,7 @@ export class MarsParamsQueryClient implements MarsParamsReadOnlyInterface { this.allAssetParams = this.allAssetParams.bind(this) this.vaultConfig = this.vaultConfig.bind(this) this.allVaultConfigs = this.allVaultConfigs.bind(this) - this.maxCloseFactor = this.maxCloseFactor.bind(this) + this.targetHealthFactor = this.targetHealthFactor.bind(this) } owner = async (): Promise => { @@ -119,9 +120,9 @@ export class MarsParamsQueryClient implements MarsParamsReadOnlyInterface { }, }) } - maxCloseFactor = async (): Promise => { + targetHealthFactor = async (): Promise => { return this.client.queryContractSmart(this.contractAddress, { - max_close_factor: {}, + target_health_factor: {}, }) } } @@ -134,7 +135,7 @@ export interface MarsParamsInterface extends MarsParamsReadOnlyInterface { memo?: string, _funds?: Coin[], ) => Promise - updateMaxCloseFactor: ( + updateTargetHealthFactor: ( fee?: number | StdFee | 'auto', memo?: string, _funds?: Coin[], @@ -169,7 +170,7 @@ export class MarsParamsClient extends MarsParamsQueryClient implements MarsParam this.sender = sender this.contractAddress = contractAddress this.updateOwner = this.updateOwner.bind(this) - this.updateMaxCloseFactor = this.updateMaxCloseFactor.bind(this) + this.updateTargetHealthFactor = this.updateTargetHealthFactor.bind(this) this.updateAssetParams = this.updateAssetParams.bind(this) this.updateVaultConfig = this.updateVaultConfig.bind(this) this.emergencyUpdate = this.emergencyUpdate.bind(this) @@ -192,7 +193,7 @@ export class MarsParamsClient extends MarsParamsQueryClient implements MarsParam _funds, ) } - updateMaxCloseFactor = async ( + updateTargetHealthFactor = async ( fee: number | StdFee | 'auto' = 'auto', memo?: string, _funds?: Coin[], @@ -201,7 +202,7 @@ export class MarsParamsClient extends MarsParamsQueryClient implements MarsParam this.sender, this.contractAddress, { - update_max_close_factor: {}, + update_target_health_factor: {}, }, fee, memo, diff --git a/scripts/types/generated/mars-params/MarsParams.react-query.ts b/scripts/types/generated/mars-params/MarsParams.react-query.ts index 6f034c5a..9b4c1121 100644 --- a/scripts/types/generated/mars-params/MarsParams.react-query.ts +++ b/scripts/types/generated/mars-params/MarsParams.react-query.ts @@ -23,6 +23,7 @@ import { AssetParamsBaseForString, CmSettingsForString, HlsParamsBaseForString, + LiquidationBonus, RedBankSettings, VaultConfigBaseForString, Coin, @@ -60,9 +61,9 @@ export const marsParamsQueryKeys = { [ { ...marsParamsQueryKeys.address(contractAddress)[0], method: 'all_vault_configs', args }, ] as const, - maxCloseFactor: (contractAddress: string | undefined, args?: Record) => + targetHealthFactor: (contractAddress: string | undefined, args?: Record) => [ - { ...marsParamsQueryKeys.address(contractAddress)[0], method: 'max_close_factor', args }, + { ...marsParamsQueryKeys.address(contractAddress)[0], method: 'target_health_factor', args }, ] as const, } export interface MarsParamsReactQuery { @@ -74,15 +75,15 @@ export interface MarsParamsReactQuery { initialData?: undefined } } -export interface MarsParamsMaxCloseFactorQuery +export interface MarsParamsTargetHealthFactorQuery extends MarsParamsReactQuery {} -export function useMarsParamsMaxCloseFactorQuery({ +export function useMarsParamsTargetHealthFactorQuery({ client, options, -}: MarsParamsMaxCloseFactorQuery) { +}: MarsParamsTargetHealthFactorQuery) { return useQuery( - marsParamsQueryKeys.maxCloseFactor(client?.contractAddress), - () => (client ? client.maxCloseFactor() : Promise.reject(new Error('Invalid client'))), + marsParamsQueryKeys.targetHealthFactor(client?.contractAddress), + () => (client ? client.targetHealthFactor() : Promise.reject(new Error('Invalid client'))), { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, ) } @@ -252,7 +253,7 @@ export function useMarsParamsUpdateAssetParamsMutation( options, ) } -export interface MarsParamsUpdateMaxCloseFactorMutation { +export interface MarsParamsUpdateTargetHealthFactorMutation { client: MarsParamsClient args?: { fee?: number | StdFee | 'auto' @@ -260,15 +261,15 @@ export interface MarsParamsUpdateMaxCloseFactorMutation { funds?: Coin[] } } -export function useMarsParamsUpdateMaxCloseFactorMutation( +export function useMarsParamsUpdateTargetHealthFactorMutation( options?: Omit< - UseMutationOptions, + UseMutationOptions, 'mutationFn' >, ) { - return useMutation( + return useMutation( ({ client, msg, args: { fee, memo, funds } = {} }) => - client.updateMaxCloseFactor(msg, fee, memo, funds), + client.updateTargetHealthFactor(msg, fee, memo, funds), options, ) } diff --git a/scripts/types/generated/mars-params/MarsParams.types.ts b/scripts/types/generated/mars-params/MarsParams.types.ts index aa046b17..6c2e2762 100644 --- a/scripts/types/generated/mars-params/MarsParams.types.ts +++ b/scripts/types/generated/mars-params/MarsParams.types.ts @@ -7,15 +7,15 @@ export type Decimal = string export interface InstantiateMsg { - max_close_factor: Decimal owner: string + target_health_factor: Decimal } export type ExecuteMsg = | { update_owner: OwnerUpdate } | { - update_max_close_factor: Decimal + update_target_health_factor: Decimal } | { update_asset_params: AssetParamsUpdate @@ -86,9 +86,10 @@ export type RedBankEmergencyUpdate = { export interface AssetParamsBaseForString { credit_manager: CmSettingsForString denom: string - liquidation_bonus: Decimal + liquidation_bonus: LiquidationBonus liquidation_threshold: Decimal max_loan_to_value: Decimal + protocol_liquidation_fee: Decimal red_bank: RedBankSettings } export interface CmSettingsForString { @@ -100,6 +101,12 @@ export interface HlsParamsBaseForString { liquidation_threshold: Decimal max_loan_to_value: Decimal } +export interface LiquidationBonus { + max_lb: Decimal + min_lb: Decimal + slope: Decimal + starting_lb: Decimal +} export interface RedBankSettings { borrow_enabled: boolean deposit_cap: Uint128 @@ -145,7 +152,7 @@ export type QueryMsg = } } | { - max_close_factor: {} + target_health_factor: {} } export type HlsAssetTypeForAddr = | { @@ -163,9 +170,10 @@ export type ArrayOfAssetParamsBaseForAddr = AssetParamsBaseForAddr[] export interface AssetParamsBaseForAddr { credit_manager: CmSettingsForAddr denom: string - liquidation_bonus: Decimal + liquidation_bonus: LiquidationBonus liquidation_threshold: Decimal max_loan_to_value: Decimal + protocol_liquidation_fee: Decimal red_bank: RedBankSettings } export interface CmSettingsForAddr { From a625085502a09cc8fcd40d37c2802f7619ccc2ce Mon Sep 17 00:00:00 2001 From: Piotr Babel Date: Tue, 20 Jun 2023 17:04:34 +0200 Subject: [PATCH 8/9] Generate schema. --- schemas/mars-params/mars-params.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schemas/mars-params/mars-params.json b/schemas/mars-params/mars-params.json index 3476df19..873370b7 100644 --- a/schemas/mars-params/mars-params.json +++ b/schemas/mars-params/mars-params.json @@ -1,6 +1,6 @@ { "contract_name": "mars-params", - "contract_version": "1.1.0", + "contract_version": "1.0.7", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", From 56cfee57df1475f4e793a231469b8585fa475235 Mon Sep 17 00:00:00 2001 From: Piotr Babel Date: Tue, 20 Jun 2023 17:31:41 +0200 Subject: [PATCH 9/9] Bump cw2. --- Cargo.lock | 36 ++++++++++++------------------------ Cargo.toml | 2 +- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ac77728f..569b1b23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -870,7 +870,7 @@ checksum = "c80e93d1deccb8588db03945016a292c3c631e6325d349ebb35d2db6f4f946f7" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw2 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "cw2 1.1.0", "schemars", "semver", "serde", @@ -921,21 +921,9 @@ dependencies = [ [[package]] name = "cw2" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb70cee2cf0b4a8ff7253e6bc6647107905e8eb37208f87d54f67810faa62f8" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-storage-plus 1.1.0", - "schemars", - "serde", -] - -[[package]] -name = "cw2" -version = "1.0.1" -source = "git+https://github.com/CosmWasm/cw-plus?rev=de1fb0b#de1fb0b9836e56e5640575d246274d882509d714" +checksum = "29ac2dc7a55ad64173ca1e0a46697c31b7a5c51342f55a1e84a724da4eb99908" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1743,7 +1731,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.1.0", - "cw2 1.0.1 (git+https://github.com/CosmWasm/cw-plus?rev=de1fb0b)", + "cw2 1.1.0", "mars-owner", "mars-red-bank-types", "serde", @@ -1767,7 +1755,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.1.0", - "cw2 1.0.1 (git+https://github.com/CosmWasm/cw-plus?rev=de1fb0b)", + "cw2 1.1.0", "mars-owner", "mars-red-bank", "mars-red-bank-types", @@ -1805,7 +1793,7 @@ version = "1.1.0" dependencies = [ "cosmwasm-std", "cw-storage-plus 1.1.0", - "cw2 1.0.1 (git+https://github.com/CosmWasm/cw-plus?rev=de1fb0b)", + "cw2 1.1.0", "mars-owner", "mars-red-bank-types", "mars-utils", @@ -1822,7 +1810,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.1.0", - "cw2 1.0.1 (git+https://github.com/CosmWasm/cw-plus?rev=de1fb0b)", + "cw2 1.1.0", "mars-oracle-base", "mars-osmosis", "mars-owner", @@ -1845,7 +1833,7 @@ dependencies = [ "cosmwasm-std", "cw-it", "cw-storage-plus 1.1.0", - "cw2 1.0.1 (git+https://github.com/CosmWasm/cw-plus?rev=de1fb0b)", + "cw2 1.1.0", "mars-oracle-base", "mars-owner", "mars-red-bank-types", @@ -1886,7 +1874,7 @@ dependencies = [ "cosmwasm-std", "cw-multi-test 0.16.5", "cw-storage-plus 1.1.0", - "cw2 1.0.1 (git+https://github.com/CosmWasm/cw-plus?rev=de1fb0b)", + "cw2 1.1.0", "mars-owner", "mars-utils", "schemars", @@ -1902,7 +1890,7 @@ dependencies = [ "cosmwasm-std", "cw-storage-plus 1.1.0", "cw-utils 1.0.1", - "cw2 1.0.1 (git+https://github.com/CosmWasm/cw-plus?rev=de1fb0b)", + "cw2 1.1.0", "mars-health", "mars-owner", "mars-red-bank-types", @@ -1950,7 +1938,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-it", - "cw2 1.0.1 (git+https://github.com/CosmWasm/cw-plus?rev=de1fb0b)", + "cw2 1.1.0", "mars-oracle-wasm", "mars-owner", "mars-red-bank-types", @@ -1992,7 +1980,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-it", - "cw2 1.0.1 (git+https://github.com/CosmWasm/cw-plus?rev=de1fb0b)", + "cw2 1.1.0", "mars-osmosis", "mars-owner", "mars-red-bank-types", diff --git a/Cargo.toml b/Cargo.toml index d09fd7d5..f0a17844 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ anyhow = "1.0.71" bech32 = "0.9.1" cosmwasm-schema = "1.2.6" cosmwasm-std = "1.2.6" -cw2 = { git = "https://github.com/CosmWasm/cw-plus", rev = "de1fb0b" } +cw2 = "1.1.0" cw-multi-test = "0.16.5" cw-storage-plus = "1.0.1" cw-utils = "1.0.1"