From 9476cc19889ea756cd7bee4e5825234964fff96d 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 01/43] Add params contract. Co-authored-by: Gabe Rodriguez Co-authored-by: Piotr Babel Co-authored-by: brimigs <85972460+brimigs@users.noreply.github.com> --- 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 d8fff392d..1495c80b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1907,6 +1907,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.1.0", + "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 f9db9ef29..0a31cf95f 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 000000000..b5ee0b973 --- /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 000000000..ebb813f4e --- /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 000000000..29e8f6f7d --- /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 000000000..87f73c633 --- /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 000000000..982b6954b --- /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 000000000..b19167c53 --- /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 000000000..bff2a7d9b --- /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 000000000..2f486c2cc --- /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 000000000..3029293f6 --- /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 000000000..b9ec3b109 --- /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 000000000..00a071352 --- /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 000000000..1462f1db7 --- /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 000000000..d6cd96536 --- /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 000000000..76b6ca7a0 --- /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 000000000..e7b8f6fcf --- /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 000000000..857060fd9 --- /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 000000000..52c2cfef9 --- /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 000000000..2bcbd029f --- /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 000000000..0ae030333 --- /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 000000000..2c580c0be --- /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 000000000..6ffc92c7e --- /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 000000000..7b23d0bd6 --- /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 000000000..748c972e0 --- /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 000000000..df525b854 --- /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 000000000..f62b2792c --- /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 000000000..d1b29a8d9 --- /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 000000000..84d0ab4d2 --- /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 5b4d32833..35a3ed621 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 000000000..4ca016c89 --- /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 2cdbf87da..5924f5177 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 38eb9c0b6..d081ce108 100644 --- a/scripts/deploy/base/index.ts +++ b/scripts/deploy/base/index.ts @@ -25,6 +25,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 4928c0bcc..1662ebd07 100644 --- a/scripts/deploy/neutron/config.ts +++ b/scripts/deploy/neutron/config.ts @@ -110,8 +110,7 @@ export const neutronTestnetConfig: DeploymentConfig = { rpcEndpoint: 'https://rpc-palvus.pion-1.ntrn.tech:443', // rpcEndpoint: 'https://neutron-testnet-rpc.polkachu.com: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 000000000..ba1628600 --- /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 000000000..6f034c5a8 --- /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 000000000..aa046b17d --- /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 000000000..c78830ef8 --- /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 1277c55d3..98116bdb5 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 f816bcd1c..e234a6e87 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 d829c0c49..9176313d6 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 66a5acd81..6b3187946 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 7a1a207ec..da8eed8e8 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 77d87fd3c540ddb926b3750576a11f7c215bca2a Mon Sep 17 00:00:00 2001 From: piobab Date: Tue, 20 Jun 2023 15:49:02 +0200 Subject: [PATCH 02/43] 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 | 191 ++++++++++++++++-- .../mars-params/MarsParams.client.ts | 17 +- .../mars-params/MarsParams.react-query.ts | 25 +-- .../generated/mars-params/MarsParams.types.ts | 18 +- 17 files changed, 597 insertions(+), 183 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 1495c80b7..53b06ef0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1909,7 +1909,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 b5ee0b973..1f449d6fe 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 87f73c633..71ffd2b0c 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 b19167c53..3d6497e70 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 bff2a7d9b..7d03967d0 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 3029293f6..836691097 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 00a071352..efa0169a6 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 1462f1db7..bd606a4e1 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 2bcbd029f..e5ee7dbc1 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 0ae030333..e5b291d2f 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 6ffc92c7e..0c7f7b352 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 7b23d0bd6..000000000 --- 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 000000000..47c2cf2a6 --- /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 4ca016c89..873370b7f 100644 --- a/schemas/mars-params/mars-params.json +++ b/schemas/mars-params/mars-params.json @@ -1,27 +1,27 @@ { "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#", "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 ba1628600..ef6a19f05 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 6f034c5a8..9b4c1121e 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 aa046b17d..6c2e27625 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 9976a6ca74a73fe8054e933d5346cb9839b931c2 Mon Sep 17 00:00:00 2001 From: piobab Date: Wed, 21 Jun 2023 17:26:30 +0200 Subject: [PATCH 03/43] Mp 2525 pyth v2 (#217) * Add ActionKind to differentiate origin for querying prices. * Pyth v2 impl. * Refactor. * Bump deps. * Fix deploy. --- Cargo.lock | 258 ++++---- contracts/oracle/base/src/contract.rs | 48 +- contracts/oracle/base/src/pyth.rs | 221 ++++++- contracts/oracle/base/src/traits.rs | 3 +- contracts/oracle/osmosis/src/lib.rs | 2 +- contracts/oracle/osmosis/src/price_source.rs | 208 ++----- contracts/oracle/osmosis/tests/helpers.rs | 21 +- .../osmosis/tests/test_price_source_fmt.rs | 4 +- .../oracle/osmosis/tests/test_query_price.rs | 262 +------- .../tests/test_query_price_for_pyth.rs | 574 ++++++++++++++++++ .../osmosis/tests/test_set_price_source.rs | 53 +- contracts/oracle/wasm/Cargo.toml | 2 +- contracts/oracle/wasm/src/helpers.rs | 13 +- contracts/oracle/wasm/src/price_source.rs | 61 +- .../oracle/wasm/tests/test_price_source.rs | 16 +- contracts/swapper/astroport/src/route.rs | 1 + integration-tests/tests/test_oracles.rs | 16 + packages/health/src/query.rs | 3 +- packages/testing/src/oracle_querier.rs | 1 + packages/testing/src/wasm_oracle.rs | 2 + packages/types/src/oracle/msg.rs | 12 +- .../mars-oracle-osmosis.json | 50 +- .../mars-oracle-wasm/mars-oracle-wasm.json | 52 +- scripts/deploy/base/deployer.ts | 2 +- scripts/deploy/neutron/config.ts | 2 +- scripts/deploy/osmosis/config.ts | 8 +- scripts/types/config.ts | 2 +- .../MarsOracleOsmosis.client.ts | 11 +- .../MarsOracleOsmosis.react-query.ts | 5 + .../MarsOracleOsmosis.types.ts | 5 + .../mars-oracle-wasm/MarsOracleWasm.client.ts | 11 +- .../MarsOracleWasm.react-query.ts | 5 + .../mars-oracle-wasm/MarsOracleWasm.types.ts | 5 + 33 files changed, 1323 insertions(+), 616 deletions(-) create mode 100644 contracts/oracle/osmosis/tests/test_query_price_for_pyth.rs diff --git a/Cargo.lock b/Cargo.lock index 53b06ef0a..317b785c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,14 +45,14 @@ checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "anyhow" -version = "1.0.71" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" [[package]] name = "astro-satellite-package" version = "0.1.0" -source = "git+https://github.com/astroport-fi/astroport_ibc?branch=main#aad024371d0427f07e2676e37918bc40e8bcde4d" +source = "git+https://github.com/astroport-fi/astroport_ibc?branch=main#f9f4def037d117275de31fffef36ddda388baf48" dependencies = [ "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/astroport-governance?branch=feat/merge_hidden_2023_05_22)", "cosmwasm-schema", @@ -150,7 +150,7 @@ dependencies = [ [[package]] name = "astroport-governance" version = "1.2.0" -source = "git+https://github.com/astroport-fi/astroport-governance?branch=main#a5412c8c0c16f1c47ed6d23593be9ab0f648a20d" +source = "git+https://github.com/astroport-fi/astroport-governance?branch=main#f0ef7c6dde76fc77ce360262923366a5cde3c3f8" dependencies = [ "astroport 2.10.0", "cosmwasm-schema", @@ -300,7 +300,7 @@ checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.26", ] [[package]] @@ -365,7 +365,7 @@ version = "0.60.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "062dddbc1ba4aca46de6338e2bf87771414c335f7b2f2036e8f3e9befebf88e6" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cexpr", "clang-sys", "clap", @@ -421,6 +421,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" + [[package]] name = "block-buffer" version = "0.9.0" @@ -439,6 +445,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bnum" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "845141a4fade3f790628b7daaaa298a25b204fb28907eb54febe5142db6ce653" + [[package]] name = "borsh" version = "0.9.3" @@ -560,7 +572,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ "atty", - "bitflags", + "bitflags 1.3.2", "clap_lex", "indexmap", "strsim", @@ -598,9 +610,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6340df57935414636969091153f35f68d9f00bbc8fb4a9c6054706c213e6c6bc" +checksum = "795bc6e66a8e340f075fcf6227e417a2dc976b92b91f3cdc778bb858778b6747" [[package]] name = "core-foundation" @@ -652,9 +664,9 @@ dependencies = [ [[package]] name = "cosmwasm-crypto" -version = "1.2.7" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb64554a91d6a9231127f4355d351130a0b94e663d5d9dc8b3a54ca17d83de49" +checksum = "0d076a08ec01ed23c4396aca98ec73a38fa1fee5f310465add52b4108181c7a8" dependencies = [ "digest 0.10.7", "ed25519-zebra", @@ -665,18 +677,18 @@ dependencies = [ [[package]] name = "cosmwasm-derive" -version = "1.2.7" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0fb2ce09f41a3dae1a234d56a9988f9aff4c76441cd50ef1ee9a4f20415b028" +checksum = "dec361f3c09d7b41221948fc17be9b3c96cb58e55a02f82da36f888a651f2584" dependencies = [ "syn 1.0.109", ] [[package]] name = "cosmwasm-schema" -version = "1.2.7" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "230e5d1cefae5331db8934763c81b9c871db6a2cd899056a5694fa71d292c815" +checksum = "bb6b2fb76758ef59cddc77f2e2ae91c22f77da49037e9f182e9c2833f0e959b1" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -687,9 +699,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.2.7" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43dadf7c23406cb28079d69e6cb922c9c29b9157b0fe887e3b79c783b7d4bcb8" +checksum = "2bfa39422f0d9f1c9a6fd3711573258495314dfa3aae738ea825ecd9964bc659" dependencies = [ "proc-macro2", "quote", @@ -698,11 +710,12 @@ dependencies = [ [[package]] name = "cosmwasm-std" -version = "1.2.7" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4337eef8dfaf8572fe6b6b415d6ec25f9308c7bb09f2da63789209fb131363be" +checksum = "1f6dc2ee23313add5ecacc3ccac217b9967ad9d2d11bd56e5da6aa65a9da6138" dependencies = [ "base64", + "bnum", "cosmwasm-crypto", "cosmwasm-derive", "derivative", @@ -713,14 +726,13 @@ dependencies = [ "serde-json-wasm", "sha2 0.10.7", "thiserror", - "uint", ] [[package]] name = "cosmwasm-storage" -version = "1.2.7" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8601d284db8776e39fe99b3416516c5636ca73cef14666b7bb9648ca32c4b89" +checksum = "7ade8cae79dc08a06bcf119c0854ffaed11bd8cb1013c6b04abfe1f51f36211e" dependencies = [ "cosmwasm-std", "serde", @@ -809,7 +821,7 @@ dependencies = [ "cosmwasm-std", "cw-multi-test 0.16.2", "cw20 0.15.1", - "osmosis-std 0.16.0", + "osmosis-std 0.16.1", "osmosis-test-tube 15.1.0", "paste", "prost 0.11.9", @@ -1093,9 +1105,9 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "dyn-clone" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" +checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272" [[package]] name = "ecdsa" @@ -1217,12 +1229,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.9.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" [[package]] name = "ff" @@ -1321,7 +1330,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.26", ] [[package]] @@ -1444,7 +1453,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" dependencies = [ "base64", - "bitflags", + "bitflags 1.3.2", "bytes", "headers-core", "http", @@ -1628,15 +1637,6 @@ dependencies = [ "hashbrown 0.12.3", ] -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - [[package]] name = "integer-sqrt" version = "0.1.5" @@ -1646,17 +1646,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.2", - "libc", - "windows-sys", -] - [[package]] name = "itertools" version = "0.10.5" @@ -1668,9 +1657,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" @@ -1756,9 +1745,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.3.8" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" [[package]] name = "log" @@ -1804,7 +1793,7 @@ dependencies = [ "mars-red-bank-types", "mars-testing", "mars-utils", - "osmosis-std 0.16.0", + "osmosis-std 0.16.1", "test-case", "thiserror", ] @@ -1826,8 +1815,8 @@ dependencies = [ "mars-swapper-osmosis", "mars-testing", "mars-utils", - "osmosis-std 0.16.0", - "osmosis-test-tube 16.0.0", + "osmosis-std 0.16.1", + "osmosis-test-tube 16.0.1", "serde", ] @@ -1860,7 +1849,7 @@ dependencies = [ "mars-red-bank-types", "mars-testing", "mars-utils", - "osmosis-std 0.16.0", + "osmosis-std 0.16.1", "pyth-sdk-cw", "schemars", "serde", @@ -1890,7 +1879,7 @@ name = "mars-osmosis" version = "1.1.0" dependencies = [ "cosmwasm-std", - "osmosis-std 0.16.0", + "osmosis-std 0.16.1", "serde", ] @@ -1965,7 +1954,7 @@ dependencies = [ "mars-red-bank-types", "mars-testing", "mars-utils", - "osmosis-std 0.16.0", + "osmosis-std 0.16.1", "schemars", "serde", "thiserror", @@ -2026,7 +2015,7 @@ dependencies = [ "mars-owner", "mars-red-bank-types", "mars-swapper-base", - "osmosis-std 0.16.0", + "osmosis-std 0.16.1", ] [[package]] @@ -2048,7 +2037,7 @@ dependencies = [ "mars-red-bank-types", "mars-rewards-collector", "mars-swapper-astroport", - "osmosis-std 0.16.0", + "osmosis-std 0.16.1", "prost 0.11.9", "pyth-sdk-cw", ] @@ -2209,13 +2198,13 @@ dependencies = [ [[package]] name = "osmosis-std" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c254b73ab62f7e7a19eb7249cf1443e49999ecc8169bb4535a855b6e87d55498" +checksum = "2fa46d2ad5ae738572887974e000934374ce3546b820505c0ee19ca708e49622" dependencies = [ "chrono", "cosmwasm-std", - "osmosis-std-derive 0.16.0", + "osmosis-std-derive 0.16.1", "prost 0.11.9", "prost-types", "schemars", @@ -2236,9 +2225,9 @@ dependencies = [ [[package]] name = "osmosis-std-derive" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a6feeb2c5de684262032a6719ff58cfecfdffa5f31eba1835faa857ac3a762" +checksum = "11c2ba5535743617d6f44ae8d572d064fabab6d06ffcf403512f89c58954dbe9" dependencies = [ "itertools", "proc-macro2", @@ -2256,7 +2245,7 @@ dependencies = [ "bindgen", "cosmrs", "cosmwasm-std", - "osmosis-std 0.16.0", + "osmosis-std 0.16.1", "prost 0.11.9", "serde", "serde_json", @@ -2266,27 +2255,27 @@ dependencies = [ [[package]] name = "osmosis-test-tube" -version = "16.0.0" +version = "16.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4988b1215f3fffdb1af5ae4930ed97e0be2d61221fc27dff7b89973670cbb50" +checksum = "527375c01396e7e4de4ccc18a0141aeb6b342dc089d30c57282025f3a8753e72" dependencies = [ "base64", "bindgen", "cosmrs", "cosmwasm-std", - "osmosis-std 0.16.0", + "osmosis-std 0.16.1", "prost 0.11.9", "serde", "serde_json", - "test-tube 0.1.4", + "test-tube 0.1.5", "thiserror", ] [[package]] name = "paste" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4b27ab7be369122c218afc2079489cdcb4b517c0a3fc386ff11e1fedfcc2b35" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pathdiff" @@ -2344,9 +2333,9 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pest" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73935e4d55e2abf7f130186537b19e7a4abc886a0252380b59248af473a3fc9" +checksum = "0d2d1d55045829d65aad9d389139882ad623b33b904e7c9f1b10c5b8927298e5" dependencies = [ "thiserror", "ucd-trie", @@ -2354,9 +2343,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef623c9bbfa0eedf5a0efba11a5ee83209c326653ca31ff019bec3a95bfff2b" +checksum = "5f94bca7e7a599d89dea5dfa309e217e7906c3c007fb9c3299c40b10d6a315d3" dependencies = [ "pest", "pest_generator", @@ -2364,22 +2353,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e8cba4ec22bada7fc55ffe51e2deb6a0e0db2d0b7ab0b103acc80d2510c190" +checksum = "99d490fe7e8556575ff6911e45567ab95e71617f43781e5c05490dc8d75c965c" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.26", ] [[package]] name = "pest_meta" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01f71cb40bd8bb94232df14b946909e14660e33fc05db3e50ae2a82d7ea0ca0" +checksum = "2674c66ebb4b4d9036012091b537aae5878970d6999f81a265034d85b136b341" dependencies = [ "once_cell", "pest", @@ -2403,7 +2392,7 @@ checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.26", ] [[package]] @@ -2469,9 +2458,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.64" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] @@ -2483,7 +2472,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e35c06b98bf36aba164cc17cb25f7e232f5c4aeea73baa14b8a9f0d92dbfa65" dependencies = [ "bit-set", - "bitflags", + "bitflags 1.3.2", "byteorder", "lazy_static", "num-traits", @@ -2593,9 +2582,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.29" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" dependencies = [ "proc-macro2", ] @@ -2651,7 +2640,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -2663,18 +2652,18 @@ dependencies = [ "aho-corasick", "memchr", "regex-automata", - "regex-syntax 0.7.3", + "regex-syntax 0.7.4", ] [[package]] name = "regex-automata" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d3daa6976cffb758ec878f108ba0e062a45b2d6ca3a2cca965338855476caf" +checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.3", + "regex-syntax 0.7.4", ] [[package]] @@ -2685,9 +2674,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" [[package]] name = "rfc6979" @@ -2742,7 +2731,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" dependencies = [ "base64", - "bitflags", + "bitflags 1.3.2", "serde", ] @@ -2770,13 +2759,12 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.37.23" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" dependencies = [ - "bitflags", + "bitflags 2.3.3", "errno", - "io-lifetimes", "libc", "linux-raw-sys", "windows-sys", @@ -2809,9 +2797,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "rusty-fork" @@ -2827,9 +2815,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "same-file" @@ -2903,7 +2891,7 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -2922,15 +2910,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" [[package]] name = "serde" -version = "1.0.171" +version = "1.0.173" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" +checksum = "e91f70896d6720bc714a4a57d22fc91f1db634680e65c8efe13323f1fa38d53f" dependencies = [ "serde_derive", ] @@ -2955,22 +2943,22 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.11" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a16be4fe5320ade08736447e3198294a5ea9a6d44dde6f35f0a5e06859c427a" +checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.171" +version = "1.0.173" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" +checksum = "a6250dde8342e0232232be9ca3db7aa40aceb5a3e5dd9bddbc00d99a007cde49" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.26", ] [[package]] @@ -2986,9 +2974,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.100" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c" +checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b" dependencies = [ "itoa", "ryu", @@ -3003,7 +2991,7 @@ checksum = "1d89a8107374290037607734c0b73a85db7ed80cae314b3c5791f192a496e731" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.26", ] [[package]] @@ -3185,9 +3173,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.25" +version = "2.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e3fc8c0c74267e2df136e5e5fb656a464158aa57624053375eb9c8c6e25ae2" +checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970" dependencies = [ "proc-macro2", "quote", @@ -3196,11 +3184,10 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.6.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" dependencies = [ - "autocfg", "cfg-if", "fastrand", "redox_syscall", @@ -3356,7 +3343,7 @@ dependencies = [ "base64", "cosmrs", "cosmwasm-std", - "osmosis-std 0.16.0", + "osmosis-std 0.16.1", "prost 0.11.9", "serde", "serde_json", @@ -3365,13 +3352,14 @@ dependencies = [ [[package]] name = "test-tube" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2152b79646afbca662896e917b94fb839af6939eb592b7af9cd5dcb1cd42f99e" +checksum = "a520f35c8ca6665ea5231a81ce9110f49a6901a1cbedb751e72859e83320bfea" dependencies = [ "base64", "cosmrs", "cosmwasm-std", + "osmosis-std 0.16.1", "prost 0.11.9", "serde", "serde_json", @@ -3401,7 +3389,7 @@ checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.26", ] [[package]] @@ -3462,7 +3450,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.26", ] [[package]] @@ -3569,9 +3557,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" [[package]] name = "unicode-normalization" @@ -3666,7 +3654,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.26", "wasm-bindgen-shared", ] @@ -3688,7 +3676,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.26", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3862,5 +3850,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.25", + "syn 2.0.26", ] diff --git a/contracts/oracle/base/src/contract.rs b/contracts/oracle/base/src/contract.rs index 0d6669ecc..e0268d8d0 100644 --- a/contracts/oracle/base/src/contract.rs +++ b/contracts/oracle/base/src/contract.rs @@ -7,8 +7,8 @@ use cosmwasm_std::{ use cw_storage_plus::{Bound, Item, Map}; use mars_owner::{Owner, OwnerInit::SetInitialOwner, OwnerUpdate}; use mars_red_bank_types::oracle::msg::{ - Config, ConfigResponse, ExecuteMsg, InstantiateMsg, PriceResponse, PriceSourceResponse, - QueryMsg, + ActionKind, Config, ConfigResponse, ExecuteMsg, InstantiateMsg, PriceResponse, + PriceSourceResponse, QueryMsg, }; use crate::{ @@ -126,11 +126,24 @@ where } => to_binary(&self.query_price_sources(deps, start_after, limit)?), QueryMsg::Price { denom, - } => to_binary(&self.query_price(deps, env, denom)?), + kind, + } => to_binary(&self.query_price( + deps, + env, + denom, + kind.unwrap_or(ActionKind::Default), + )?), QueryMsg::Prices { start_after, limit, - } => to_binary(&self.query_prices(deps, env, start_after, limit)?), + kind, + } => to_binary(&self.query_prices( + deps, + env, + start_after, + limit, + kind.unwrap_or(ActionKind::Default), + )?), }; res.map_err(Into::into) } @@ -251,13 +264,26 @@ where .collect() } - fn query_price(&self, deps: Deps, env: Env, denom: String) -> ContractResult { + fn query_price( + &self, + deps: Deps, + env: Env, + denom: String, + kind: ActionKind, + ) -> ContractResult { let cfg = self.config.load(deps.storage)?; let price_source = self.query_price_source(deps, denom.clone())?.price_source; Ok(PriceResponse { - price: price_source.query_price(&deps, &env, &denom, &cfg, &self.price_sources)?, + price: price_source.query_price( + &deps, + &env, + &denom, + &cfg, + &self.price_sources, + kind, + )?, denom, }) } @@ -268,6 +294,7 @@ where env: Env, start_after: Option, limit: Option, + kind: ActionKind, ) -> ContractResult> { let cfg = self.config.load(deps.storage)?; @@ -280,7 +307,14 @@ where .map(|item| { let (k, v) = item?; Ok(PriceResponse { - price: v.query_price(&deps, &env, &k, &cfg, &self.price_sources)?, + price: v.query_price( + &deps, + &env, + &k, + &cfg, + &self.price_sources, + kind.clone(), + )?, denom: k, }) }) diff --git a/contracts/oracle/base/src/pyth.rs b/contracts/oracle/base/src/pyth.rs index be99a5b5f..751937e0b 100644 --- a/contracts/oracle/base/src/pyth.rs +++ b/contracts/oracle/base/src/pyth.rs @@ -1,44 +1,107 @@ use cosmwasm_std::{Addr, Decimal, Deps, Empty, Env, StdError, Uint128}; use cw_storage_plus::Map; -use mars_red_bank_types::oracle::Config; -use pyth_sdk_cw::query_price_feed; -pub use pyth_sdk_cw::PriceIdentifier; +use mars_red_bank_types::oracle::{ActionKind, Config}; +use pyth_sdk_cw::{query_price_feed, Price, PriceFeed, PriceFeedResponse, PriceIdentifier}; use super::*; use crate::error::ContractError::InvalidPrice; +/// We want to discriminate which actions should trigger a circuit breaker check. +/// The objective is to allow liquidations to happen without requiring too many checks (always be open for liquidations) +/// while not allowing other actions to be taken in cases of extreme volatility (which could indicate price manipulation attacks). +#[allow(clippy::too_many_arguments)] pub fn query_pyth_price>( deps: &Deps, env: &Env, contract_addr: Addr, price_feed_id: PriceIdentifier, max_staleness: u64, + max_confidence: Decimal, + max_deviation: Decimal, denom_decimals: u8, config: &Config, price_sources: &Map<&str, P>, + kind: ActionKind, ) -> ContractResult { // Use current price source for USD to check how much 1 USD is worth in base_denom let usd_price = price_sources .load(deps.storage, "usd") .map_err(|_| StdError::generic_err("Price source not found for denom 'usd'"))? - .query_price(deps, env, "usd", config, price_sources)?; - - let current_time = env.block.time.seconds(); + .query_price(deps, env, "usd", config, price_sources, kind.clone())?; let price_feed_response = query_price_feed(&deps.querier, contract_addr, price_feed_id)?; + + match kind { + ActionKind::Default => query_pyth_price_for_default( + env, + max_staleness, + max_confidence, + max_deviation, + denom_decimals, + usd_price, + price_feed_response, + ), + ActionKind::Liquidation => query_pyth_price_for_liquidation( + env, + max_staleness, + denom_decimals, + usd_price, + price_feed_response, + ), + } +} + +fn query_pyth_price_for_default( + env: &Env, + max_staleness: u64, + max_confidence: Decimal, + max_deviation: Decimal, + denom_decimals: u8, + usd_price: Decimal, + price_feed_response: PriceFeedResponse, +) -> ContractResult { let price_feed = price_feed_response.price_feed; - // Check if the current price is not too old - let current_price_opt = price_feed.get_price_no_older_than(current_time as i64, max_staleness); - let Some(current_price) = current_price_opt else { + let current_time = env.block.time.seconds(); + let current_price = + assert_pyth_current_price_not_too_old(price_feed, current_time, max_staleness)?; + let ema_price = assert_pyth_ema_price_not_too_old(price_feed, current_time, max_staleness)?; + + // Check if the current and EMA price is > 0 + if current_price.price <= 0 || ema_price.price <= 0 { return Err(InvalidPrice { - reason: format!( - "current price publish time is too old/stale. published: {}, now: {}", - price_feed.get_price_unchecked().publish_time, - current_time - ), + reason: "price can't be <= 0".to_string(), }); - }; + } + + let current_price_dec = scale_to_exponent(current_price.price as u128, current_price.expo)?; + let ema_price_dec = scale_to_exponent(ema_price.price as u128, ema_price.expo)?; + + assert_pyth_price_confidence(current_price, ema_price_dec, max_confidence)?; + assert_pyth_price_deviation(current_price_dec, ema_price_dec, max_deviation)?; + + let current_price_dec = scale_pyth_price( + current_price.price as u128, + current_price.expo, + denom_decimals, + usd_price, + )?; + + Ok(current_price_dec) +} + +fn query_pyth_price_for_liquidation( + env: &Env, + max_staleness: u64, + denom_decimals: u8, + usd_price: Decimal, + price_feed_response: PriceFeedResponse, +) -> ContractResult { + let price_feed = price_feed_response.price_feed; + + let current_time = env.block.time.seconds(); + let current_price = + assert_pyth_current_price_not_too_old(price_feed, current_time, max_staleness)?; // Check if the current price is > 0 if current_price.price <= 0 { @@ -57,17 +120,96 @@ pub fn query_pyth_price>( Ok(current_price_dec) } -/// Price feeds represent numbers in a fixed-point format. -/// The same exponent is used for both the price and confidence interval. -/// The integer representation of these values can be computed by multiplying by 10^exponent. -/// -/// As an example, imagine Pyth reported the following values for ATOM/USD: -/// expo: -8 -/// conf: 574566 -/// price: 1365133270 -/// The confidence interval is 574566 * 10^(-8) = $0.00574566, and the price is 1365133270 * 10^(-8) = $13.6513327. -/// -/// Moreover, we have to represent the price for utoken in base_denom. +/// Assert Pyth configuration +pub fn assert_pyth(max_confidence: Decimal, max_deviation: Decimal) -> ContractResult<()> { + if !max_confidence.le(&Decimal::percent(20u64)) { + return Err(ContractError::InvalidPriceSource { + reason: "max_confidence must be in the range of <0;0.2>".to_string(), + }); + } + + if !max_deviation.le(&Decimal::percent(20u64)) { + return Err(ContractError::InvalidPriceSource { + reason: "max_deviation must be in the range of <0;0.2>".to_string(), + }); + } + + Ok(()) +} + +/// Check if the current price is not too old +pub fn assert_pyth_current_price_not_too_old( + price_feed: PriceFeed, + current_time: u64, + max_staleness: u64, +) -> ContractResult { + let current_price_opt = price_feed.get_price_no_older_than(current_time as i64, max_staleness); + let Some(current_price) = current_price_opt else { + return Err(InvalidPrice { + reason: format!( + "current price publish time is too old/stale. published: {}, now: {}", + price_feed.get_price_unchecked().publish_time, + current_time + ), + }); + }; + Ok(current_price) +} + +/// Check if the ema price is not too old +pub fn assert_pyth_ema_price_not_too_old( + price_feed: PriceFeed, + current_time: u64, + max_staleness: u64, +) -> ContractResult { + let ema_price_opt = price_feed.get_ema_price_no_older_than(current_time as i64, max_staleness); + let Some(ema_price) = ema_price_opt else { + return Err(InvalidPrice { + reason: format!( + "EMA price publish time is too old/stale. published: {}, now: {}", + price_feed.get_ema_price_unchecked().publish_time, + current_time + ), + }); + }; + Ok(ema_price) +} + +/// Check price confidence +pub fn assert_pyth_price_confidence( + current_price: Price, + ema_price_dec: Decimal, + max_confidence: Decimal, +) -> ContractResult<()> { + let confidence = scale_to_exponent(current_price.conf as u128, current_price.expo)?; + let price_confidence = confidence.checked_div(ema_price_dec)?; + if price_confidence > max_confidence { + return Err(InvalidPrice { + reason: format!("price confidence deviation {price_confidence} exceeds max allowed {max_confidence}") + }); + } + Ok(()) +} + +/// Check price deviation +pub fn assert_pyth_price_deviation( + current_price_dec: Decimal, + ema_price_dec: Decimal, + max_deviation: Decimal, +) -> ContractResult<()> { + let delta = current_price_dec.abs_diff(ema_price_dec); + let price_deviation = delta.checked_div(ema_price_dec)?; + if price_deviation > max_deviation { + return Err(InvalidPrice { + reason: format!( + "price deviation {price_deviation} exceeds max allowed {max_deviation}" + ), + }); + } + Ok(()) +} + +/// We have to represent the price for utoken in base_denom. /// Pyth price should be normalized with token decimals. /// /// Let's try to convert ATOM/USD reported by Pyth to uatom/base_denom: @@ -92,13 +234,7 @@ pub fn scale_pyth_price( denom_decimals: u8, usd_price: Decimal, ) -> ContractResult { - let target_expo = Uint128::from(10u8).checked_pow(expo.unsigned_abs())?; - let pyth_price = if expo < 0 { - Decimal::checked_from_ratio(value, target_expo)? - } else { - let res = Uint128::from(value).checked_mul(target_expo)?; - Decimal::from_ratio(res, 1u128) - }; + let pyth_price = scale_to_exponent(value, expo)?; let denom_scaled = Decimal::from_atomics(1u128, denom_decimals as u32)?; @@ -122,6 +258,25 @@ pub fn scale_pyth_price( Ok(price) } +/// Price feeds represent numbers in a fixed-point format. +/// The same exponent is used for both the price and confidence interval. +/// The integer representation of these values can be computed by multiplying by 10^exponent. +/// +/// As an example, imagine Pyth reported the following values for ATOM/USD: +/// expo: -8 +/// conf: 574566 +/// price: 1365133270 +/// The confidence interval is 574566 * 10^(-8) = $0.00574566, and the price is 1365133270 * 10^(-8) = $13.6513327. +pub fn scale_to_exponent(value: u128, expo: i32) -> ContractResult { + let target_expo = Uint128::from(10u8).checked_pow(expo.unsigned_abs())?; + if expo < 0 { + Ok(Decimal::checked_from_ratio(value, target_expo)?) + } else { + let res = Uint128::from(value).checked_mul(target_expo)?; + Ok(Decimal::from_ratio(res, 1u128)) + } +} + /// Assert availability of usd price source pub fn assert_usd_price_source>( deps: &Deps, diff --git a/contracts/oracle/base/src/traits.rs b/contracts/oracle/base/src/traits.rs index 5821bb49f..2577bbc15 100644 --- a/contracts/oracle/base/src/traits.rs +++ b/contracts/oracle/base/src/traits.rs @@ -2,7 +2,7 @@ use std::fmt::{Debug, Display}; use cosmwasm_std::{CustomQuery, Decimal, Deps, Env}; use cw_storage_plus::Map; -use mars_red_bank_types::oracle::Config; +use mars_red_bank_types::oracle::{ActionKind, Config}; use schemars::JsonSchema; use serde::{de::DeserializeOwned, Serialize}; @@ -49,5 +49,6 @@ where denom: &str, config: &Config, price_sources: &Map<&str, Self>, + kind: ActionKind, ) -> ContractResult; } diff --git a/contracts/oracle/osmosis/src/lib.rs b/contracts/oracle/osmosis/src/lib.rs index 31089de17..0e8835960 100644 --- a/contracts/oracle/osmosis/src/lib.rs +++ b/contracts/oracle/osmosis/src/lib.rs @@ -6,6 +6,6 @@ mod price_source; pub mod stride; pub use price_source::{ - scale_pyth_price, Downtime, DowntimeDetector, GeometricTwap, OsmosisPriceSourceChecked, + Downtime, DowntimeDetector, GeometricTwap, OsmosisPriceSourceChecked, OsmosisPriceSourceUnchecked, RedemptionRate, }; diff --git a/contracts/oracle/osmosis/src/price_source.rs b/contracts/oracle/osmosis/src/price_source.rs index e6ac09707..f1c1ed599 100644 --- a/contracts/oracle/osmosis/src/price_source.rs +++ b/contracts/oracle/osmosis/src/price_source.rs @@ -3,14 +3,14 @@ use std::{cmp::min, fmt}; use cosmwasm_std::{Addr, Decimal, Decimal256, Deps, Empty, Env, Isqrt, Uint128, Uint256}; use cw_storage_plus::Map; use mars_oracle_base::{ - pyth::PriceIdentifier, ContractError::InvalidPrice, ContractResult, PriceSourceChecked, - PriceSourceUnchecked, + ContractError::InvalidPrice, ContractResult, PriceSourceChecked, PriceSourceUnchecked, }; use mars_osmosis::helpers::{ query_arithmetic_twap_price, query_geometric_twap_price, query_pool, query_spot_price, recovered_since_downtime_of_length, Pool, }; -use mars_red_bank_types::oracle::Config; +use mars_red_bank_types::oracle::{ActionKind, Config}; +use pyth_sdk_cw::PriceIdentifier; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -164,6 +164,16 @@ pub enum OsmosisPriceSource { /// rejecting the price as too stale max_staleness: u64, + /// The maximum confidence deviation allowed for an oracle price. + /// + /// The confidence is measured as the percent of the confidence interval + /// value provided by the oracle as compared to the weighted average value + /// of the price. + max_confidence: Decimal, + + /// The maximum deviation (percentage) between current and EMA price + max_deviation: Decimal, + /// Assets are represented in their smallest unit and every asset can have different decimals (e.g. OSMO - 6 decimals, WETH - 18 decimals). /// /// Pyth prices are denominated in USD so basically it means how much 1 USDC, 1 ATOM, 1 OSMO is worth in USD (NOT 1 uusdc, 1 uatom, 1 uosmo). @@ -276,9 +286,11 @@ impl fmt::Display for OsmosisPriceSourceChecked { contract_addr, price_feed_id, max_staleness, + max_confidence, + max_deviation, denom_decimals, } => { - format!("pyth:{contract_addr}:{price_feed_id}:{max_staleness}:{denom_decimals}") + format!("pyth:{contract_addr}:{price_feed_id}:{max_staleness}:{max_confidence}:{max_deviation}:{denom_decimals}") } OsmosisPriceSource::Lsd { transitive_denom, @@ -382,14 +394,18 @@ impl PriceSourceUnchecked for OsmosisPriceSour contract_addr, price_feed_id, max_staleness, + max_confidence, + max_deviation, denom_decimals, } => { + mars_oracle_base::pyth::assert_pyth(*max_confidence, *max_deviation)?; mars_oracle_base::pyth::assert_usd_price_source(deps, price_sources)?; - Ok(OsmosisPriceSourceChecked::Pyth { contract_addr: deps.api.addr_validate(contract_addr)?, price_feed_id: *price_feed_id, max_staleness: *max_staleness, + max_confidence: *max_confidence, + max_deviation: *max_deviation, denom_decimals: *denom_decimals, }) } @@ -425,6 +441,7 @@ impl PriceSourceChecked for OsmosisPriceSourceChecked { denom: &str, config: &Config, price_sources: &Map<&str, Self>, + kind: ActionKind, ) -> ContractResult { match self { OsmosisPriceSourceChecked::Fixed { @@ -470,7 +487,14 @@ impl PriceSourceChecked for OsmosisPriceSourceChecked { } OsmosisPriceSourceChecked::XykLiquidityToken { pool_id, - } => Self::query_xyk_liquidity_token_price(deps, env, *pool_id, config, price_sources), + } => Self::query_xyk_liquidity_token_price( + deps, + env, + *pool_id, + config, + price_sources, + kind, + ), OsmosisPriceSourceChecked::StakedGeometricTwap { transitive_denom, pool_id, @@ -488,12 +512,15 @@ impl PriceSourceChecked for OsmosisPriceSourceChecked { *window_size, config, price_sources, + kind, ) } OsmosisPriceSourceChecked::Pyth { contract_addr, price_feed_id, max_staleness, + max_confidence, + max_deviation, denom_decimals, } => Ok(mars_oracle_base::pyth::query_pyth_price( deps, @@ -501,9 +528,12 @@ impl PriceSourceChecked for OsmosisPriceSourceChecked { contract_addr.to_owned(), *price_feed_id, *max_staleness, + *max_confidence, + *max_deviation, *denom_decimals, config, price_sources, + kind, )?), OsmosisPriceSourceChecked::Lsd { transitive_denom, @@ -521,6 +551,7 @@ impl PriceSourceChecked for OsmosisPriceSourceChecked { redemption_rate.clone(), config, price_sources, + kind, ) } } @@ -558,6 +589,7 @@ impl OsmosisPriceSourceChecked { pool_id: u64, config: &Config, price_sources: &Map<&str, Self>, + kind: ActionKind, ) -> ContractResult { // XYK pool asserted during price source creation let pool = query_pool(&deps.querier, pool_id)?; @@ -571,6 +603,7 @@ impl OsmosisPriceSourceChecked { &coin0.denom, config, price_sources, + kind.clone(), )?; let coin1_price = price_sources.load(deps.storage, &coin1.denom)?.query_price( deps, @@ -578,6 +611,7 @@ impl OsmosisPriceSourceChecked { &coin1.denom, config, price_sources, + kind, )?; let coin0_value = Uint256::from_uint128(coin0.amount) * Decimal256::from(coin0_price); @@ -608,6 +642,7 @@ impl OsmosisPriceSourceChecked { window_size: u64, config: &Config, price_sources: &Map<&str, OsmosisPriceSourceChecked>, + kind: ActionKind, ) -> ContractResult { let start_time = env.block.time.seconds() - window_size; let staked_price = query_geometric_twap_price( @@ -625,6 +660,7 @@ impl OsmosisPriceSourceChecked { transitive_denom, config, price_sources, + kind, )?; staked_price.checked_mul(transitive_price).map_err(Into::into) @@ -645,6 +681,7 @@ impl OsmosisPriceSourceChecked { redemption_rate: RedemptionRate, config: &Config, price_sources: &Map<&str, OsmosisPriceSourceChecked>, + kind: ActionKind, ) -> ContractResult { let current_time = env.block.time.seconds(); let start_time = current_time - geometric_twap.window_size; @@ -683,166 +720,9 @@ impl OsmosisPriceSourceChecked { transitive_denom, config, price_sources, + kind, )?; min_price.checked_mul(transitive_price).map_err(Into::into) } } - -/// Price feeds represent numbers in a fixed-point format. -/// The same exponent is used for both the price and confidence interval. -/// The integer representation of these values can be computed by multiplying by 10^exponent. -/// -/// As an example, imagine Pyth reported the following values for ATOM/USD: -/// expo: -8 -/// conf: 574566 -/// price: 1365133270 -/// The confidence interval is 574566 * 10^(-8) = $0.00574566, and the price is 1365133270 * 10^(-8) = $13.6513327. -/// -/// Moreover, we have to represent the price for utoken in base_denom. -/// Pyth price should be normalized with token decimals. -/// -/// Let's try to convert ATOM/USD reported by Pyth to uatom/base_denom: -/// - base_denom = uusd -/// - price source set for usd (e.g. FIXED price source where 1 usd = 1000000 uusd) -/// - denom_decimals (ATOM) = 6 -/// -/// 1 ATOM = 10^6 uatom -/// -/// 1 ATOM = price * 10^expo USD -/// 10^6 uatom = price * 10^expo * 1000000 uusd -/// uatom = price * 10^expo * 1000000 / 10^6 uusd -/// uatom = price * 10^expo * 1000000 * 10^(-6) uusd -/// uatom/uusd = 1365133270 * 10^(-8) * 1000000 * 10^(-6) -/// uatom/uusd = 1365133270 * 10^(-8) = 13.6513327 -/// -/// Generalized formula: -/// utoken/uusd = price * 10^expo * usd_price_in_base_denom * 10^(-denom_decimals) -pub fn scale_pyth_price( - value: u128, - expo: i32, - denom_decimals: u8, - usd_price: Decimal, -) -> ContractResult { - let target_expo = Uint128::from(10u8).checked_pow(expo.unsigned_abs())?; - let pyth_price = if expo < 0 { - Decimal::checked_from_ratio(value, target_expo)? - } else { - let res = Uint128::from(value).checked_mul(target_expo)?; - Decimal::from_ratio(res, 1u128) - }; - - let denom_scaled = Decimal::from_atomics(1u128, denom_decimals as u32)?; - - // Multiplication order matters !!! It can overflow doing different ways. - // usd_price is represented in smallest unit so it can be quite big number and can be used to reduce number of decimals. - // - // Let's assume that: - // - usd_price = 1000000 = 10^6 - // - expo = -8 - // - denom_decimals = 18 - // - // If we multiply usd_price by denom_scaled firstly we will decrease number of decimals used in next multiplication by pyth_price: - // 10^6 * 10^(-18) = 10^(-12) - // 12 decimals used. - // - // BUT if we multiply pyth_price by denom_scaled: - // 10^(-8) * 10^(-18) = 10^(-26) - // 26 decimals used (overflow) !!! - let price = usd_price.checked_mul(denom_scaled)?.checked_mul(pyth_price)?; - - Ok(price) -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use super::*; - - #[test] - fn display_pyth_price_source() { - let ps = OsmosisPriceSourceChecked::Pyth { - contract_addr: Addr::unchecked("osmo12j43nf2f0qumnt2zrrmpvnsqgzndxefujlvr08"), - price_feed_id: PriceIdentifier::from_hex( - "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", - ) - .unwrap(), - max_staleness: 60, - denom_decimals: 6, - }; - assert_eq!( - ps.to_string(), - "pyth:osmo12j43nf2f0qumnt2zrrmpvnsqgzndxefujlvr08:0x61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3:60:6" - ) - } - - #[test] - fn display_lsd_price_source() { - let ps = OsmosisPriceSourceChecked::Lsd { - transitive_denom: "transitive".to_string(), - geometric_twap: GeometricTwap { - pool_id: 456, - window_size: 380, - downtime_detector: None, - }, - redemption_rate: RedemptionRate { - contract_addr: Addr::unchecked( - "osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc", - ), - max_staleness: 1234, - }, - }; - assert_eq!(ps.to_string(), "lsd:transitive:456:380:None:osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc:1234"); - - let ps = OsmosisPriceSourceChecked::Lsd { - transitive_denom: "transitive".to_string(), - geometric_twap: GeometricTwap { - pool_id: 456, - window_size: 380, - downtime_detector: Some(DowntimeDetector { - downtime: Downtime::Duration30m, - recovery: 552, - }), - }, - redemption_rate: RedemptionRate { - contract_addr: Addr::unchecked( - "osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc", - ), - max_staleness: 1234, - }, - }; - assert_eq!(ps.to_string(), "lsd:transitive:456:380:Some(Duration30m:552):osmo1zw4fxj4pt0pu0jdd7cs6gecdj3pvfxhhtgkm4w2y44jp60hywzvssud6uc:1234"); - } - - #[test] - fn scale_real_pyth_price() { - // ATOM - let uatom_price_in_uusd = - scale_pyth_price(1035200881u128, -8, 6u8, Decimal::from_str("1000000").unwrap()) - .unwrap(); - assert_eq!(uatom_price_in_uusd, Decimal::from_str("10.35200881").unwrap()); - - // ETH - let ueth_price_in_uusd = - scale_pyth_price(181598000001u128, -8, 18u8, Decimal::from_str("1000000").unwrap()) - .unwrap(); - assert_eq!(ueth_price_in_uusd, Decimal::from_str("0.00000000181598").unwrap()); - } - - #[test] - fn scale_pyth_price_if_expo_above_zero() { - let ueth_price_in_uusd = - scale_pyth_price(181598000001u128, 8, 18u8, Decimal::from_str("1000000").unwrap()) - .unwrap(); - assert_eq!(ueth_price_in_uusd, Decimal::from_atomics(181598000001u128, 4u32).unwrap()); - } - - #[test] - fn scale_big_eth_pyth_price() { - let ueth_price_in_uusd = - scale_pyth_price(100000098000001u128, -8, 18u8, Decimal::from_str("1000000").unwrap()) - .unwrap(); - assert_eq!(ueth_price_in_uusd, Decimal::from_atomics(100000098000001u128, 20u32).unwrap()); - } -} diff --git a/contracts/oracle/osmosis/tests/helpers.rs b/contracts/oracle/osmosis/tests/helpers.rs index 91f43b3e3..80b683e54 100644 --- a/contracts/oracle/osmosis/tests/helpers.rs +++ b/contracts/oracle/osmosis/tests/helpers.rs @@ -1,11 +1,11 @@ #![allow(dead_code)] -use std::marker::PhantomData; +use std::{marker::PhantomData, str::FromStr}; use cosmwasm_std::{ coin, from_binary, testing::{mock_env, MockApi, MockQuerier, MockStorage}, - Coin, Deps, DepsMut, OwnedDeps, + Coin, Decimal, Deps, DepsMut, OwnedDeps, }; use mars_oracle_base::ContractError; use mars_oracle_osmosis::{contract::entry, msg::ExecuteMsg, OsmosisPriceSourceUnchecked}; @@ -74,6 +74,21 @@ pub fn setup_test_with_pools() -> OwnedDeps OwnedDeps { + let mut deps = setup_test(); + + // price source used to convert USD to base_denom + set_price_source( + deps.as_mut(), + "usd", + OsmosisPriceSourceUnchecked::Fixed { + price: Decimal::from_str("1000000").unwrap(), + }, + ); + + deps +} + pub fn setup_test() -> OwnedDeps { let mut deps = OwnedDeps::<_, _, _> { storage: MockStorage::default(), @@ -148,6 +163,8 @@ pub fn set_pyth_price_source(deps: DepsMut, denom: &str, price_id: PriceIdentifi contract_addr: "pyth_contract".to_string(), price_feed_id: price_id, max_staleness: 30, + max_confidence: Decimal::percent(10u64), + max_deviation: Decimal::percent(15u64), denom_decimals: 6, }, ) diff --git a/contracts/oracle/osmosis/tests/test_price_source_fmt.rs b/contracts/oracle/osmosis/tests/test_price_source_fmt.rs index de4085329..99bd613d1 100644 --- a/contracts/oracle/osmosis/tests/test_price_source_fmt.rs +++ b/contracts/oracle/osmosis/tests/test_price_source_fmt.rs @@ -110,11 +110,13 @@ fn display_pyth_price_source() { ) .unwrap(), max_staleness: 60, + max_confidence: Decimal::percent(10u64), + max_deviation: Decimal::percent(15u64), denom_decimals: 18, }; assert_eq!( ps.to_string(), - "pyth:osmo12j43nf2f0qumnt2zrrmpvnsqgzndxefujlvr08:0x61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3:60:18" + "pyth:osmo12j43nf2f0qumnt2zrrmpvnsqgzndxefujlvr08:0x61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3:60:0.1:0.15:18" ) } diff --git a/contracts/oracle/osmosis/tests/test_query_price.rs b/contracts/oracle/osmosis/tests/test_query_price.rs index e36c73703..0b330b91d 100644 --- a/contracts/oracle/osmosis/tests/test_query_price.rs +++ b/contracts/oracle/osmosis/tests/test_query_price.rs @@ -38,6 +38,7 @@ fn querying_fixed_price() { deps.as_ref(), QueryMsg::Price { denom: "uosmo".to_string(), + kind: None, }, ); assert_eq!(res.price, Decimal::one()); @@ -52,6 +53,7 @@ fn querying_fixed_price_price_source_not_set() { mock_env(), QueryMsg::Price { denom: "uosmo".to_string(), + kind: None, }, ) .unwrap_err(); @@ -87,6 +89,7 @@ fn querying_spot_price() { deps.as_ref(), QueryMsg::Price { denom: "umars".to_string(), + kind: None, }, ); assert_eq!(res.price, Decimal::from_ratio(88888u128, 12345u128)); @@ -119,6 +122,7 @@ fn querying_arithmetic_twap_price() { deps.as_ref(), QueryMsg::Price { denom: "umars".to_string(), + kind: None, }, ); assert_eq!(res.price, Decimal::from_ratio(77777u128, 12345u128)); @@ -147,6 +151,7 @@ fn querying_arithmetic_twap_price_with_downtime_detector() { deps.as_ref(), QueryMsg::Price { denom: "umars".to_string(), + kind: None, }, ); assert_eq!( @@ -169,6 +174,7 @@ fn querying_arithmetic_twap_price_with_downtime_detector() { deps.as_ref(), QueryMsg::Price { denom: "umars".to_string(), + kind: None, }, ); assert_eq!(res.price, Decimal::from_ratio(77777u128, 12345u128)); @@ -201,6 +207,7 @@ fn querying_geometric_twap_price() { deps.as_ref(), QueryMsg::Price { denom: "umars".to_string(), + kind: None, }, ); assert_eq!(res.price, Decimal::from_ratio(66666u128, 12345u128)); @@ -229,6 +236,7 @@ fn querying_geometric_twap_price_with_downtime_detector() { deps.as_ref(), QueryMsg::Price { denom: "umars".to_string(), + kind: None, }, ); assert_eq!( @@ -251,6 +259,7 @@ fn querying_geometric_twap_price_with_downtime_detector() { deps.as_ref(), QueryMsg::Price { denom: "umars".to_string(), + kind: None, }, ); assert_eq!(res.price, Decimal::from_ratio(77777u128, 12345u128)); @@ -303,6 +312,7 @@ fn querying_staked_geometric_twap_price() { deps.as_ref(), QueryMsg::Price { denom: "ustatom".to_string(), + kind: None, }, ); let expected_price = ustatom_uatom_price * uatom_uosmo_price; @@ -338,6 +348,7 @@ fn querying_staked_geometric_twap_price_if_no_transitive_denom_price_source() { deps.as_ref(), QueryMsg::Price { denom: "ustatom".to_string(), + kind: None, }, ); assert_eq!( @@ -381,6 +392,7 @@ fn querying_staked_geometric_twap_price_with_downtime_detector() { deps.as_ref(), QueryMsg::Price { denom: "ustatom".to_string(), + kind: None, }, ); assert_eq!( @@ -415,6 +427,7 @@ fn querying_staked_geometric_twap_price_with_downtime_detector() { deps.as_ref(), QueryMsg::Price { denom: "ustatom".to_string(), + kind: None, }, ); let expected_price = ustatom_uatom_price * uatom_uosmo_price; @@ -470,6 +483,7 @@ fn querying_lsd_price() { mock_env_at_block_time(publish_time), QueryMsg::Price { denom: "ustatom".to_string(), + kind: None, }, ) .unwrap(); @@ -510,6 +524,7 @@ fn querying_lsd_price() { mock_env_at_block_time(publish_time), QueryMsg::Price { denom: "ustatom".to_string(), + kind: None, }, ) .unwrap(); @@ -535,6 +550,8 @@ fn setup_pyth_and_geometric_twap_for_lsd( contract_addr: "pyth_contract_addr".to_string(), price_feed_id: price_id, max_staleness: 1800u64, + max_confidence: Decimal::percent(10u64), + max_deviation: Decimal::percent(15u64), denom_decimals: 6u8, }, ); @@ -622,6 +639,7 @@ fn querying_lsd_price_if_no_transitive_denom_price_source() { mock_env_at_block_time(publish_time), QueryMsg::Price { denom: "ustatom".to_string(), + kind: None, }, ) .unwrap_err(); @@ -685,6 +703,7 @@ fn querying_lsd_price_if_redemption_rate_too_old() { mock_env_at_block_time(publish_time), QueryMsg::Price { denom: "ustatom".to_string(), + kind: None, }, ) .unwrap_err(); @@ -752,6 +771,7 @@ fn querying_lsd_price_with_downtime_detector() { mock_env_at_block_time(publish_time), QueryMsg::Price { denom: "ustatom".to_string(), + kind: None, }, ) .unwrap_err(); @@ -768,6 +788,7 @@ fn querying_lsd_price_with_downtime_detector() { mock_env_at_block_time(publish_time), QueryMsg::Price { denom: "ustatom".to_string(), + kind: None, }, ) .unwrap(); @@ -854,6 +875,7 @@ fn querying_xyk_lp_price() { deps.as_ref(), QueryMsg::Price { denom: "uatom_umars_lp".to_string(), + kind: None, }, ); assert_eq!(res.price, Decimal::from_ratio(1770000_u128, 10000_u128)); @@ -879,6 +901,7 @@ fn querying_xyk_lp_price() { deps.as_ref(), QueryMsg::Price { denom: "uatom_umars_lp".to_string(), + kind: None, }, ); // Atom price: 88.5 @@ -889,244 +912,6 @@ fn querying_xyk_lp_price() { assert_eq!(res.price, Decimal::from_ratio(1769874_u128, 10000_u128)); } -#[test] -fn querying_pyth_price_if_publish_price_too_old() { - let mut deps = helpers::setup_test(); - - // price source used to convert USD to base_denom - helpers::set_price_source( - deps.as_mut(), - "usd", - OsmosisPriceSourceUnchecked::Fixed { - price: Decimal::from_str("1000000").unwrap(), - }, - ); - - let price_id = PriceIdentifier::from_hex( - "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", - ) - .unwrap(); - - let max_staleness = 30u64; - helpers::set_price_source( - deps.as_mut(), - "uatom", - OsmosisPriceSourceUnchecked::Pyth { - contract_addr: "pyth_contract_addr".to_string(), - price_feed_id: price_id, - max_staleness, - denom_decimals: 6u8, - }, - ); - - let price_publish_time = 1677157333u64; - let ema_price_publish_time = price_publish_time + max_staleness; - deps.querier.set_pyth_price( - price_id, - PriceFeedResponse { - price_feed: PriceFeed::new( - price_id, - Price { - price: 1371155677, - conf: 646723, - expo: -8, - publish_time: price_publish_time as i64, - }, - Price { - price: 1365133270, - conf: 574566, - expo: -8, - publish_time: ema_price_publish_time as i64, - }, - ), - }, - ); - - let res_err = entry::query( - deps.as_ref(), - mock_env_at_block_time(price_publish_time + max_staleness + 1u64), - QueryMsg::Price { - denom: "uatom".to_string(), - }, - ) - .unwrap_err(); - assert_eq!( - res_err, - ContractError::InvalidPrice { - reason: - "current price publish time is too old/stale. published: 1677157333, now: 1677157364" - .to_string() - } - ); -} - -#[test] -fn querying_pyth_price_if_signed() { - let mut deps = helpers::setup_test(); - - // price source used to convert USD to base_denom - helpers::set_price_source( - deps.as_mut(), - "usd", - OsmosisPriceSourceUnchecked::Fixed { - price: Decimal::from_str("1000000").unwrap(), - }, - ); - - let price_id = PriceIdentifier::from_hex( - "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", - ) - .unwrap(); - - let max_staleness = 30u64; - helpers::set_price_source( - deps.as_mut(), - "uatom", - OsmosisPriceSourceUnchecked::Pyth { - contract_addr: "pyth_contract_addr".to_string(), - price_feed_id: price_id, - max_staleness, - denom_decimals: 6u8, - }, - ); - - let publish_time = 1677157333u64; - deps.querier.set_pyth_price( - price_id, - PriceFeedResponse { - price_feed: PriceFeed::new( - price_id, - Price { - price: -1371155677, - conf: 646723, - expo: -8, - publish_time: publish_time as i64, - }, - Price { - price: -1365133270, - conf: 574566, - expo: -8, - publish_time: publish_time as i64, - }, - ), - }, - ); - - let res_err = entry::query( - deps.as_ref(), - mock_env_at_block_time(publish_time), - QueryMsg::Price { - denom: "uatom".to_string(), - }, - ) - .unwrap_err(); - assert_eq!( - res_err, - ContractError::InvalidPrice { - reason: "price can't be <= 0".to_string() - } - ); -} - -#[test] -fn querying_pyth_price_successfully() { - let mut deps = helpers::setup_test(); - - // price source used to convert USD to base_denom - helpers::set_price_source( - deps.as_mut(), - "usd", - OsmosisPriceSourceUnchecked::Fixed { - price: Decimal::from_str("1000000").unwrap(), - }, - ); - - let price_id = PriceIdentifier::from_hex( - "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", - ) - .unwrap(); - - let max_staleness = 30u64; - helpers::set_price_source( - deps.as_mut(), - "uatom", - OsmosisPriceSourceUnchecked::Pyth { - contract_addr: "pyth_contract_addr".to_string(), - price_feed_id: price_id, - max_staleness, - denom_decimals: 6u8, - }, - ); - - let publish_time = 1677157333u64; - - // exp < 0 - deps.querier.set_pyth_price( - price_id, - PriceFeedResponse { - price_feed: PriceFeed::new( - price_id, - Price { - price: 1021000, - conf: 50000, - expo: -4, - publish_time: publish_time as i64, - }, - Price { - price: 1000000, - conf: 40000, - expo: -4, - publish_time: publish_time as i64, - }, - ), - }, - ); - - let res = entry::query( - deps.as_ref(), - mock_env_at_block_time(publish_time), - QueryMsg::Price { - denom: "uatom".to_string(), - }, - ) - .unwrap(); - let res: PriceResponse = from_binary(&res).unwrap(); - assert_eq!(res.price, Decimal::from_ratio(1021000u128, 10000u128)); - - // exp > 0 - deps.querier.set_pyth_price( - price_id, - PriceFeedResponse { - price_feed: PriceFeed::new( - price_id, - Price { - price: 102, - conf: 5, - expo: 3, - publish_time: publish_time as i64, - }, - Price { - price: 100, - conf: 4, - expo: 3, - publish_time: publish_time as i64, - }, - ), - }, - ); - - let res = entry::query( - deps.as_ref(), - mock_env_at_block_time(publish_time), - QueryMsg::Price { - denom: "uatom".to_string(), - }, - ) - .unwrap(); - let res: PriceResponse = from_binary(&res).unwrap(); - assert_eq!(res.price, Decimal::from_ratio(102000u128, 1u128)); -} - #[test] fn querying_all_prices() { let mut deps = helpers::setup_test_with_pools(); @@ -1176,6 +961,7 @@ fn querying_all_prices() { QueryMsg::Prices { start_after: None, limit: None, + kind: None, }, ); assert_eq!( diff --git a/contracts/oracle/osmosis/tests/test_query_price_for_pyth.rs b/contracts/oracle/osmosis/tests/test_query_price_for_pyth.rs new file mode 100644 index 000000000..741fcb9e3 --- /dev/null +++ b/contracts/oracle/osmosis/tests/test_query_price_for_pyth.rs @@ -0,0 +1,574 @@ +use cosmwasm_std::{from_binary, Decimal}; +use mars_oracle_base::ContractError; +use mars_oracle_osmosis::{contract::entry, OsmosisPriceSourceUnchecked}; +use mars_red_bank_types::oracle::{ActionKind, PriceResponse, QueryMsg}; +use mars_testing::mock_env_at_block_time; +use pyth_sdk_cw::{Price, PriceFeed, PriceFeedResponse, PriceIdentifier}; + +mod helpers; + +#[test] +fn querying_default_pyth_price_if_publish_price_too_old() { + let mut deps = helpers::setup_test_for_pyth(); + + let price_id = PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(); + + let max_staleness = 30u64; + helpers::set_price_source( + deps.as_mut(), + "uatom", + OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), + price_feed_id: price_id, + max_staleness, + max_confidence: Decimal::percent(10u64), + max_deviation: Decimal::percent(15u64), + denom_decimals: 6u8, + }, + ); + + let price_publish_time = 1677157333u64; + let ema_price_publish_time = price_publish_time + max_staleness; + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: 1371155677, + conf: 646723, + expo: -8, + publish_time: price_publish_time as i64, + }, + Price { + price: 1365133270, + conf: 574566, + expo: -8, + publish_time: ema_price_publish_time as i64, + }, + ), + }, + ); + + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(price_publish_time + max_staleness + 1u64), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Default), + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::InvalidPrice { + reason: + "current price publish time is too old/stale. published: 1677157333, now: 1677157364" + .to_string() + } + ); + + let ema_price_publish_time = 1677157333u64; + let price_publish_time = ema_price_publish_time + max_staleness; + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: 1371155677, + conf: 646723, + expo: -8, + publish_time: price_publish_time as i64, + }, + Price { + price: 1365133270, + conf: 574566, + expo: -8, + publish_time: ema_price_publish_time as i64, + }, + ), + }, + ); + + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(ema_price_publish_time + max_staleness + 1u64), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Default), + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::InvalidPrice { + reason: + "EMA price publish time is too old/stale. published: 1677157333, now: 1677157364" + .to_string() + } + ); +} + +#[test] +fn querying_liquidation_pyth_price_if_publish_price_too_old() { + let mut deps = helpers::setup_test_for_pyth(); + + let price_id = PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(); + + let max_staleness = 30u64; + helpers::set_price_source( + deps.as_mut(), + "uatom", + OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), + price_feed_id: price_id, + max_staleness, + max_confidence: Decimal::percent(10u64), + max_deviation: Decimal::percent(15u64), + denom_decimals: 6u8, + }, + ); + + let price_publish_time = 1677157333u64; + let ema_price_publish_time = price_publish_time + max_staleness; + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: 1371155677, + conf: 646723, + expo: -8, + publish_time: price_publish_time as i64, + }, + Price { + price: 1365133270, + conf: 574566, + expo: -8, + publish_time: ema_price_publish_time as i64, + }, + ), + }, + ); + + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(price_publish_time + max_staleness + 1u64), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Liquidation), + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::InvalidPrice { + reason: + "current price publish time is too old/stale. published: 1677157333, now: 1677157364" + .to_string() + } + ); +} + +#[test] +fn querying_default_pyth_price_if_signed() { + let mut deps = helpers::setup_test_for_pyth(); + + let price_id = PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(); + + let max_staleness = 30u64; + helpers::set_price_source( + deps.as_mut(), + "uatom", + OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), + price_feed_id: price_id, + max_staleness, + max_confidence: Decimal::percent(10u64), + max_deviation: Decimal::percent(15u64), + denom_decimals: 6u8, + }, + ); + + let publish_time = 1677157333u64; + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: -1371155677, + conf: 646723, + expo: -8, + publish_time: publish_time as i64, + }, + Price { + price: -1365133270, + conf: 574566, + expo: -8, + publish_time: publish_time as i64, + }, + ), + }, + ); + + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Default), + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::InvalidPrice { + reason: "price can't be <= 0".to_string() + } + ); +} + +#[test] +fn querying_liquidation_pyth_price_if_signed() { + let mut deps = helpers::setup_test_for_pyth(); + + let price_id = PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(); + + let max_staleness = 30u64; + helpers::set_price_source( + deps.as_mut(), + "uatom", + OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), + price_feed_id: price_id, + max_staleness, + max_confidence: Decimal::percent(10u64), + max_deviation: Decimal::percent(15u64), + denom_decimals: 6u8, + }, + ); + + let publish_time = 1677157333u64; + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: -1371155677, + conf: 646723, + expo: -8, + publish_time: publish_time as i64, + }, + Price { + price: -1365133270, + conf: 574566, + expo: -8, + publish_time: publish_time as i64, + }, + ), + }, + ); + + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Liquidation), + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::InvalidPrice { + reason: "price can't be <= 0".to_string() + } + ); +} + +#[test] +fn querying_default_pyth_price_if_confidence_exceeded() { + let mut deps = helpers::setup_test_for_pyth(); + + let price_id = PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(); + + let max_staleness = 30u64; + helpers::set_price_source( + deps.as_mut(), + "uatom", + OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), + price_feed_id: price_id, + max_staleness, + max_confidence: Decimal::percent(5u64), + max_deviation: Decimal::percent(6u64), + denom_decimals: 6u8, + }, + ); + + let publish_time = 1677157333u64; + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: 1010000, + conf: 51000, + expo: -4, + publish_time: publish_time as i64, + }, + Price { + price: 1000000, + conf: 40000, + expo: -4, + publish_time: publish_time as i64, + }, + ), + }, + ); + + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Default), + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::InvalidPrice { + reason: "price confidence deviation 0.051 exceeds max allowed 0.05".to_string() + } + ); +} + +#[test] +fn querying_default_pyth_price_if_deviation_exceeded() { + let mut deps = helpers::setup_test_for_pyth(); + + let price_id = PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(); + + let max_staleness = 30u64; + helpers::set_price_source( + deps.as_mut(), + "uatom", + OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), + price_feed_id: price_id, + max_staleness, + max_confidence: Decimal::percent(5u64), + max_deviation: Decimal::percent(6u64), + denom_decimals: 6u8, + }, + ); + + let publish_time = 1677157333u64; + + // price > ema_price + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: 1061000, + conf: 50000, + expo: -4, + publish_time: publish_time as i64, + }, + Price { + price: 1000000, + conf: 40000, + expo: -4, + publish_time: publish_time as i64, + }, + ), + }, + ); + + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Default), + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::InvalidPrice { + reason: "price deviation 0.061 exceeds max allowed 0.06".to_string() + } + ); + + // ema_price > price + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: 939999, + conf: 50000, + expo: -4, + publish_time: publish_time as i64, + }, + Price { + price: 1000000, + conf: 40000, + expo: -4, + publish_time: publish_time as i64, + }, + ), + }, + ); + + let res_err = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Default), + }, + ) + .unwrap_err(); + assert_eq!( + res_err, + ContractError::InvalidPrice { + reason: "price deviation 0.060001 exceeds max allowed 0.06".to_string() + } + ); +} + +#[test] +fn querying_pyth_price_successfully() { + let mut deps = helpers::setup_test_for_pyth(); + + let price_id = PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(); + + let max_staleness = 30u64; + helpers::set_price_source( + deps.as_mut(), + "uatom", + OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), + price_feed_id: price_id, + max_staleness, + max_confidence: Decimal::percent(10u64), + max_deviation: Decimal::percent(15u64), + denom_decimals: 6u8, + }, + ); + + let publish_time = 1677157333u64; + + // exp < 0 + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: 1021000, + conf: 50000, + expo: -4, + publish_time: publish_time as i64, + }, + Price { + price: 1000000, + conf: 40000, + expo: -4, + publish_time: publish_time as i64, + }, + ), + }, + ); + + let res = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: None, + }, + ) + .unwrap(); + let res: PriceResponse = from_binary(&res).unwrap(); + assert_eq!(res.price, Decimal::from_ratio(1021000u128, 10000u128)); + + // exp > 0 + deps.querier.set_pyth_price( + price_id, + PriceFeedResponse { + price_feed: PriceFeed::new( + price_id, + Price { + price: 102, + conf: 5, + expo: 3, + publish_time: publish_time as i64, + }, + Price { + price: 100, + conf: 4, + expo: 3, + publish_time: publish_time as i64, + }, + ), + }, + ); + + let default_res = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Default), + }, + ) + .unwrap(); + let default_res: PriceResponse = from_binary(&default_res).unwrap(); + assert_eq!(default_res.price, Decimal::from_ratio(102000u128, 1u128)); + + let liq_res = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Liquidation), + }, + ) + .unwrap(); + let liq_res: PriceResponse = from_binary(&liq_res).unwrap(); + // Price for default and liquidation actions should be the same + assert_eq!(liq_res.price, default_res.price); +} diff --git a/contracts/oracle/osmosis/tests/test_set_price_source.rs b/contracts/oracle/osmosis/tests/test_set_price_source.rs index 6712264eb..f7f23c349 100644 --- a/contracts/oracle/osmosis/tests/test_set_price_source.rs +++ b/contracts/oracle/osmosis/tests/test_set_price_source.rs @@ -944,6 +944,51 @@ fn setting_price_source_xyk_lp() { ); } +#[test] +fn setting_price_source_pyth_with_invalid_params() { + let mut deps = helpers::setup_test(); + + let mut set_price_source_pyth = |max_confidence: Decimal, max_deviation: Decimal| { + execute( + deps.as_mut(), + mock_env(), + mock_info("owner"), + ExecuteMsg::SetPriceSource { + denom: "uatom".to_string(), + price_source: OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), + price_feed_id: PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(), + max_staleness: 30, + max_confidence, + max_deviation, + denom_decimals: 6u8, + }, + }, + ) + }; + + // attempting to set max_confidence > 20%; should fail + let err = set_price_source_pyth(Decimal::percent(21), Decimal::percent(6)).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "max_confidence must be in the range of <0;0.2>".to_string() + } + ); + + // attempting to set max_deviation > 20%; should fail + let err = set_price_source_pyth(Decimal::percent(5), Decimal::percent(21)).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "max_deviation must be in the range of <0;0.2>".to_string() + } + ); +} + #[test] fn setting_price_source_pyth_if_missing_usd() { let mut deps = helpers::setup_test(); @@ -961,6 +1006,8 @@ fn setting_price_source_pyth_if_missing_usd() { ) .unwrap(), max_staleness: 30, + max_confidence: Decimal::percent(10), + max_deviation: Decimal::percent(10), denom_decimals: 8, }, }, @@ -1000,6 +1047,8 @@ fn setting_price_source_pyth_successfully() { ) .unwrap(), max_staleness: 30, + max_confidence: Decimal::percent(12), + max_deviation: Decimal::percent(14), denom_decimals: 8, }, }, @@ -1022,7 +1071,9 @@ fn setting_price_source_pyth_successfully() { ) .unwrap(), max_staleness: 30, - denom_decimals: 8 + max_confidence: Decimal::percent(12), + max_deviation: Decimal::percent(14), + denom_decimals: 8, }, ); } diff --git a/contracts/oracle/wasm/Cargo.toml b/contracts/oracle/wasm/Cargo.toml index 0a7147f40..eda692995 100644 --- a/contracts/oracle/wasm/Cargo.toml +++ b/contracts/oracle/wasm/Cargo.toml @@ -31,6 +31,7 @@ mars-oracle-base = { workspace = true, features = ["pyth"] } mars-red-bank-types = { workspace = true } cosmwasm-schema = { workspace = true } astroport = { workspace = true } +pyth-sdk-cw = { workspace = true } [dev-dependencies] cosmwasm-schema = { workspace = true } @@ -39,4 +40,3 @@ mars-owner = { workspace = true } cw-it = { workspace = true, features = ["astroport", "astroport-multi-test"] } test-case = { workspace = true } proptest = { workspace = true } -pyth-sdk-cw = { workspace = true } diff --git a/contracts/oracle/wasm/src/helpers.rs b/contracts/oracle/wasm/src/helpers.rs index 07fbfbd70..1305acb4c 100644 --- a/contracts/oracle/wasm/src/helpers.rs +++ b/contracts/oracle/wasm/src/helpers.rs @@ -10,7 +10,7 @@ use cosmwasm_std::{ }; use cw_storage_plus::Map; use mars_oracle_base::{ContractError, ContractResult, PriceSourceChecked}; -use mars_red_bank_types::oracle::{AstroportTwapSnapshot, Config}; +use mars_red_bank_types::oracle::{ActionKind, AstroportTwapSnapshot, Config}; use crate::WasmPriceSourceChecked; @@ -168,6 +168,7 @@ pub fn normalize_price( pair_info: &PairInfo, denom: &str, price: Decimal, + kind: ActionKind, ) -> ContractResult { let pair_denoms = get_astroport_pair_denoms(pair_info)?; @@ -177,8 +178,14 @@ pub fn normalize_price( let other_pair_denom = get_other_astroport_pair_denom(&pair_denoms, denom)?; let other_price_source = price_sources.load(deps.storage, &other_pair_denom)?; - let other_price = - other_price_source.query_price(deps, env, &other_pair_denom, config, price_sources)?; + let other_price = other_price_source.query_price( + deps, + env, + &other_pair_denom, + config, + price_sources, + kind, + )?; Ok(price.checked_mul(other_price)?) } diff --git a/contracts/oracle/wasm/src/price_source.rs b/contracts/oracle/wasm/src/price_source.rs index b90917470..f0c26bf7b 100644 --- a/contracts/oracle/wasm/src/price_source.rs +++ b/contracts/oracle/wasm/src/price_source.rs @@ -5,11 +5,11 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Decimal, Deps, Empty, Env, Uint128}; use cw_storage_plus::Map; use mars_oracle_base::{ - pyth::PriceIdentifier, ContractError::{self}, ContractResult, PriceSourceChecked, PriceSourceUnchecked, }; -use mars_red_bank_types::oracle::{AstroportTwapSnapshot, Config}; +use mars_red_bank_types::oracle::{ActionKind, AstroportTwapSnapshot, Config}; +use pyth_sdk_cw::PriceIdentifier; use crate::{ helpers::{ @@ -54,12 +54,24 @@ pub enum WasmPriceSource { contract_addr: A, /// Price feed id of an asset from the list: https://pyth.network/developers/price-feed-ids + /// We can't verify what denoms consist of the price feed. + /// Be very careful when adding it !!! price_feed_id: PriceIdentifier, /// The maximum number of seconds since the last price was by an oracle, before /// rejecting the price as too stale max_staleness: u64, + /// The maximum confidence deviation allowed for an oracle price. + /// + /// The confidence is measured as the percent of the confidence interval + /// value provided by the oracle as compared to the weighted average value + /// of the price. + max_confidence: Decimal, + + /// The maximum deviation (percentage) between current and EMA price + max_deviation: Decimal, + /// Assets are represented in their smallest unit and every asset can have different decimals (e.g. OSMO - 6 decimals, WETH - 18 decimals). /// /// Pyth prices are denominated in USD so basically it means how much 1 USDC, 1 ATOM, 1 OSMO is worth in USD (NOT 1 uusdc, 1 uatom, 1 uosmo). @@ -104,8 +116,10 @@ impl fmt::Display for WasmPriceSourceChecked { contract_addr, price_feed_id, max_staleness, - denom_decimals, - } => format!("pyth:{contract_addr}:{price_feed_id}:{max_staleness}:{denom_decimals}"), + max_confidence, + max_deviation, + denom_decimals + } => format!("pyth:{contract_addr}:{price_feed_id}:{max_staleness}:{max_confidence}:{max_deviation}:{denom_decimals}"), }; write!(f, "{label}") } @@ -183,14 +197,18 @@ impl PriceSourceUnchecked for WasmPriceSourceUnch contract_addr, price_feed_id, max_staleness, + max_confidence, + max_deviation, denom_decimals, } => { + mars_oracle_base::pyth::assert_pyth(max_confidence, max_deviation)?; mars_oracle_base::pyth::assert_usd_price_source(deps, price_sources)?; - Ok(WasmPriceSourceChecked::Pyth { contract_addr: deps.api.addr_validate(&contract_addr)?, price_feed_id, max_staleness, + max_confidence, + max_deviation, denom_decimals, }) } @@ -207,6 +225,7 @@ impl PriceSourceChecked for WasmPriceSourceChecked { denom: &str, config: &Config, price_sources: &Map<&str, Self>, + kind: ActionKind, ) -> ContractResult { match self { WasmPriceSource::Fixed { @@ -214,7 +233,15 @@ impl PriceSourceChecked for WasmPriceSourceChecked { } => Ok(*price), WasmPriceSource::AstroportSpot { pair_address, - } => query_astroport_spot_price(deps, env, denom, config, price_sources, pair_address), + } => query_astroport_spot_price( + deps, + env, + denom, + config, + price_sources, + pair_address, + kind, + ), WasmPriceSource::AstroportTwap { pair_address, window_size, @@ -228,11 +255,14 @@ impl PriceSourceChecked for WasmPriceSourceChecked { pair_address, *window_size, *tolerance, + kind, ), WasmPriceSource::Pyth { contract_addr, price_feed_id, max_staleness, + max_confidence, + max_deviation, denom_decimals, } => mars_oracle_base::pyth::query_pyth_price( deps, @@ -240,9 +270,12 @@ impl PriceSourceChecked for WasmPriceSourceChecked { contract_addr.clone(), *price_feed_id, *max_staleness, + *max_confidence, + *max_deviation, *denom_decimals, config, price_sources, + kind, ), } } @@ -256,6 +289,7 @@ fn query_astroport_spot_price( config: &Config, price_sources: &Map<&str, WasmPriceSourceChecked>, pair_address: &Addr, + kind: ActionKind, ) -> ContractResult { let astroport_factory = ASTROPORT_FACTORY.load(deps.storage)?; let pair_info = query_astroport_pair_info(&deps.querier, pair_address)?; @@ -270,7 +304,7 @@ fn query_astroport_spot_price( let price = Decimal::from_ratio(sim_res.return_amount, one); - normalize_price(deps, env, config, price_sources, &pair_info, denom, price) + normalize_price(deps, env, config, price_sources, &pair_info, denom, price, kind) } /// Queries the TWAP price of `denom` denominated in `base_denom` from the Astroport pair at `pair_address`. @@ -284,6 +318,7 @@ fn query_astroport_twap_price( pair_address: &Addr, window_size: u64, tolerance: u64, + kind: ActionKind, ) -> ContractResult { let snapshots = ASTROPORT_TWAP_SNAPSHOTS .may_load(deps.storage, denom)? @@ -382,7 +417,7 @@ fn query_astroport_twap_price( PairType::Custom(_) => return Err(ContractError::InvalidPairType {}), }; - normalize_price(deps, env, config, price_sources, &pair_info, denom, price) + normalize_price(deps, env, config, price_sources, &pair_info, denom, price, kind) } #[cfg(test)] @@ -398,11 +433,13 @@ mod tests { ) .unwrap(), max_staleness: 60, - denom_decimals: 6, + max_confidence: Decimal::percent(10u64), + max_deviation: Decimal::percent(15u64), + denom_decimals: 18, }; assert_eq!( - ps.to_string(), - "pyth:osmo12j43nf2f0qumnt2zrrmpvnsqgzndxefujlvr08:0x61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3:60:6" - ) + ps.to_string(), + "pyth:osmo12j43nf2f0qumnt2zrrmpvnsqgzndxefujlvr08:0x61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3:60:0.1:0.15:18" + ) } } diff --git a/contracts/oracle/wasm/tests/test_price_source.rs b/contracts/oracle/wasm/tests/test_price_source.rs index b8e445fa4..ca119203c 100644 --- a/contracts/oracle/wasm/tests/test_price_source.rs +++ b/contracts/oracle/wasm/tests/test_price_source.rs @@ -16,12 +16,13 @@ use cw_it::{ traits::CwItRunner, }; use cw_storage_plus::Map; -use mars_oracle_base::{pyth::PriceIdentifier, ContractError, PriceSourceUnchecked}; +use mars_oracle_base::{ContractError, PriceSourceUnchecked}; use mars_oracle_wasm::{ contract::entry::{self, execute}, WasmPriceSource, WasmPriceSourceChecked, WasmPriceSourceUnchecked, }; use mars_red_bank_types::oracle::{ExecuteMsg, PriceResponse, QueryMsg}; +use pyth_sdk_cw::PriceIdentifier; const ONE: Decimal = Decimal::one(); const TWO: Decimal = Decimal::new(Uint128::new(2_000_000_000_000_000_000u128)); @@ -285,6 +286,7 @@ fn test_query_astroport_twap_price_with_only_one_snapshot() { &robot.mars_oracle_contract_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap_err(); @@ -372,6 +374,8 @@ fn querying_pyth_price_if_publish_price_too_old() { contract_addr: "pyth_contract_addr".to_string(), price_feed_id: price_id, max_staleness, + max_confidence: Decimal::percent(12), + max_deviation: Decimal::percent(14), denom_decimals: 6, }, ); @@ -404,6 +408,7 @@ fn querying_pyth_price_if_publish_price_too_old() { mock_env_at_block_time(price_publish_time + max_staleness + 1u64), QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap_err(); @@ -451,6 +456,8 @@ fn querying_pyth_price_if_signed() { contract_addr: "pyth_contract_addr".to_string(), price_feed_id: price_id, max_staleness, + max_confidence: Decimal::percent(12), + max_deviation: Decimal::percent(14), denom_decimals: 6, }, ); @@ -482,6 +489,7 @@ fn querying_pyth_price_if_signed() { mock_env_at_block_time(publish_time), QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap_err(); @@ -527,6 +535,8 @@ fn querying_pyth_price_successfully() { contract_addr: "pyth_contract_addr".to_string(), price_feed_id: price_id, max_staleness, + max_confidence: Decimal::percent(12), + max_deviation: Decimal::percent(14), denom_decimals: 6, }, ); @@ -560,6 +570,7 @@ fn querying_pyth_price_successfully() { mock_env_at_block_time(publish_time), QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -593,6 +604,7 @@ fn querying_pyth_price_successfully() { mock_env_at_block_time(publish_time), QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -627,6 +639,8 @@ fn setting_price_source_pyth_if_missing_usd() { contract_addr: "new_pyth_contract_addr".to_string(), price_feed_id: price_id, max_staleness: 30, + max_confidence: Decimal::percent(10), + max_deviation: Decimal::percent(10), denom_decimals: 8, }, }, diff --git a/contracts/swapper/astroport/src/route.rs b/contracts/swapper/astroport/src/route.rs index 996667fbf..96cebe3a6 100644 --- a/contracts/swapper/astroport/src/route.rs +++ b/contracts/swapper/astroport/src/route.rs @@ -56,6 +56,7 @@ impl AstroportRoute { contract_addr: self.oracle.clone(), msg: to_binary(&mars_red_bank_types::oracle::QueryMsg::Price { denom: denom.to_string(), + kind: None, })?, })) .map(|res| res.price) diff --git a/integration-tests/tests/test_oracles.rs b/integration-tests/tests/test_oracles.rs index 58cda0106..2511bde6e 100644 --- a/integration-tests/tests/test_oracles.rs +++ b/integration-tests/tests/test_oracles.rs @@ -86,6 +86,7 @@ fn querying_xyk_lp_price_if_no_price_for_tokens() { &contract_addr, &QueryMsg::Price { denom: "umars_uatom_lp".to_string(), + kind: None, }, ) .unwrap_err(); @@ -197,6 +198,7 @@ fn querying_xyk_lp_price_success() { &contract_addr, &QueryMsg::Price { denom: "umars_uatom_lp".to_string(), + kind: None, }, ) .unwrap(); @@ -261,6 +263,7 @@ fn query_spot_price() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -396,6 +399,7 @@ fn update_spot_with_different_pool() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -422,6 +426,7 @@ fn update_spot_with_different_pool() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -471,6 +476,7 @@ fn query_spot_price_after_lp_change() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -483,6 +489,7 @@ fn query_spot_price_after_lp_change() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -555,6 +562,7 @@ fn query_geometric_twap_price_with_downtime_detector() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ); assert_err(res.unwrap_err(), "chain is recovering from downtime"); @@ -568,6 +576,7 @@ fn query_geometric_twap_price_with_downtime_detector() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -640,6 +649,7 @@ fn query_arithmetic_twap_price() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -654,6 +664,7 @@ fn query_arithmetic_twap_price() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -727,6 +738,7 @@ fn query_geometric_twap_price() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -741,6 +753,7 @@ fn query_geometric_twap_price() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -807,6 +820,7 @@ fn compare_spot_and_twap_price() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -847,6 +861,7 @@ fn compare_spot_and_twap_price() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); @@ -887,6 +902,7 @@ fn compare_spot_and_twap_price() { &oracle_addr, &QueryMsg::Price { denom: "uatom".to_string(), + kind: None, }, ) .unwrap(); diff --git a/packages/health/src/query.rs b/packages/health/src/query.rs index 0ecfc2bdc..1023f83b1 100644 --- a/packages/health/src/query.rs +++ b/packages/health/src/query.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{Addr, Decimal, QuerierWrapper, StdResult}; use mars_red_bank_types::{ - oracle::{self, PriceResponse}, + oracle::{self, ActionKind, PriceResponse}, red_bank::{self, Market}, }; @@ -40,6 +40,7 @@ impl<'a> MarsQuerier<'a> { self.oracle_addr, &oracle::QueryMsg::Price { denom: denom.to_string(), + kind: Some(ActionKind::Default), }, )?; Ok(price) diff --git a/packages/testing/src/oracle_querier.rs b/packages/testing/src/oracle_querier.rs index 8c1459377..3fbd243da 100644 --- a/packages/testing/src/oracle_querier.rs +++ b/packages/testing/src/oracle_querier.rs @@ -13,6 +13,7 @@ impl OracleQuerier { let ret: ContractResult = match query { QueryMsg::Price { denom, + kind: _, } => { let option_price = self.prices.get(&denom); diff --git a/packages/testing/src/wasm_oracle.rs b/packages/testing/src/wasm_oracle.rs index 88e0076b5..6b1f55b53 100644 --- a/packages/testing/src/wasm_oracle.rs +++ b/packages/testing/src/wasm_oracle.rs @@ -168,6 +168,7 @@ impl<'a> WasmOracleTestRobot<'a> { pub fn query_price(&self, denom: &str) -> mars_red_bank_types::oracle::PriceResponse { let msg = &mars_red_bank_types::oracle::msg::QueryMsg::Price { denom: denom.to_string(), + kind: None, }; self.wasm().query(&self.mars_oracle_contract_addr, &msg).unwrap() } @@ -180,6 +181,7 @@ impl<'a> WasmOracleTestRobot<'a> { let msg = &mars_red_bank_types::oracle::msg::QueryMsg::Prices { start_after, limit, + kind: None, }; self.wasm().query(&self.mars_oracle_contract_addr, &msg).unwrap() } diff --git a/packages/types/src/oracle/msg.rs b/packages/types/src/oracle/msg.rs index 96db6d1b0..393e1dc8a 100644 --- a/packages/types/src/oracle/msg.rs +++ b/packages/types/src/oracle/msg.rs @@ -41,6 +41,13 @@ pub enum ExecuteMsg { Custom(C), } +/// Differentiator for the action (liquidate, withdraw, borrow etc.) being performed. +#[cw_serde] +pub enum ActionKind { + Default, + Liquidation, +} + #[cw_serde] #[derive(QueryResponses)] pub enum QueryMsg { @@ -69,6 +76,7 @@ pub enum QueryMsg { #[returns(PriceResponse)] Price { denom: String, + kind: Option, }, /// Enumerate all coins' prices. /// @@ -78,6 +86,7 @@ pub enum QueryMsg { Prices { start_after: Option, limit: Option, + kind: Option, }, } @@ -106,7 +115,7 @@ pub struct PriceResponse { pub mod helpers { use cosmwasm_std::{Decimal, QuerierWrapper, StdError, StdResult}; - use super::{PriceResponse, QueryMsg}; + use super::{ActionKind, PriceResponse, QueryMsg}; pub fn query_price( querier: &QuerierWrapper, @@ -119,6 +128,7 @@ pub mod helpers { oracle.into(), &QueryMsg::Price { denom: denom.clone(), + kind: Some(ActionKind::Default), }, ) .map_err(|e| { diff --git a/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json b/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json index b6e331d9c..f8921e423 100644 --- a/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json +++ b/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json @@ -447,6 +447,8 @@ "required": [ "contract_addr", "denom_decimals", + "max_confidence", + "max_deviation", "max_staleness", "price_feed_id" ], @@ -461,6 +463,22 @@ "format": "uint8", "minimum": 0.0 }, + "max_confidence": { + "description": "The maximum confidence deviation allowed for an oracle price.\n\nThe confidence is measured as the percent of the confidence interval value provided by the oracle as compared to the weighted average value of the price.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "max_deviation": { + "description": "The maximum deviation (percentage) between current and EMA price", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, "max_staleness": { "description": "The maximum number of seconds since the last price was by an oracle, before rejecting the price as too stale", "type": "integer", @@ -704,6 +722,16 @@ "properties": { "denom": { "type": "string" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/definitions/ActionKind" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false @@ -721,6 +749,16 @@ "prices": { "type": "object", "properties": { + "kind": { + "anyOf": [ + { + "$ref": "#/definitions/ActionKind" + }, + { + "type": "null" + } + ] + }, "limit": { "type": [ "integer", @@ -741,7 +779,17 @@ }, "additionalProperties": false } - ] + ], + "definitions": { + "ActionKind": { + "description": "Differentiator for the action (liquidate, withdraw, borrow etc.) being performed.", + "type": "string", + "enum": [ + "default", + "liquidation" + ] + } + } }, "migrate": null, "sudo": null, diff --git a/schemas/mars-oracle-wasm/mars-oracle-wasm.json b/schemas/mars-oracle-wasm/mars-oracle-wasm.json index 6ccbd3a76..3578337a7 100644 --- a/schemas/mars-oracle-wasm/mars-oracle-wasm.json +++ b/schemas/mars-oracle-wasm/mars-oracle-wasm.json @@ -356,6 +356,8 @@ "required": [ "contract_addr", "denom_decimals", + "max_confidence", + "max_deviation", "max_staleness", "price_feed_id" ], @@ -370,6 +372,22 @@ "format": "uint8", "minimum": 0.0 }, + "max_confidence": { + "description": "The maximum confidence deviation allowed for an oracle price.\n\nThe confidence is measured as the percent of the confidence interval value provided by the oracle as compared to the weighted average value of the price.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "max_deviation": { + "description": "The maximum deviation (percentage) between current and EMA price", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, "max_staleness": { "description": "The maximum number of seconds since the last price was by an oracle, before rejecting the price as too stale", "type": "integer", @@ -377,7 +395,7 @@ "minimum": 0.0 }, "price_feed_id": { - "description": "Price feed id of an asset from the list: https://pyth.network/developers/price-feed-ids", + "description": "Price feed id of an asset from the list: https://pyth.network/developers/price-feed-ids We can't verify what denoms consist of the price feed. Be very careful when adding it !!!", "allOf": [ { "$ref": "#/definitions/Identifier" @@ -479,6 +497,16 @@ "properties": { "denom": { "type": "string" + }, + "kind": { + "anyOf": [ + { + "$ref": "#/definitions/ActionKind" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false @@ -496,6 +524,16 @@ "prices": { "type": "object", "properties": { + "kind": { + "anyOf": [ + { + "$ref": "#/definitions/ActionKind" + }, + { + "type": "null" + } + ] + }, "limit": { "type": [ "integer", @@ -516,7 +554,17 @@ }, "additionalProperties": false } - ] + ], + "definitions": { + "ActionKind": { + "description": "Differentiator for the action (liquidate, withdraw, borrow etc.) being performed.", + "type": "string", + "enum": [ + "default", + "liquidation" + ] + } + } }, "migrate": null, "sudo": null, diff --git a/scripts/deploy/base/deployer.ts b/scripts/deploy/base/deployer.ts index 5924f5177..8f925e110 100644 --- a/scripts/deploy/base/deployer.ts +++ b/scripts/deploy/base/deployer.ts @@ -169,7 +169,7 @@ export class Deployer { async instantiateParams() { const msg: ParamsInstantiateMsg = { owner: this.deployerAddress, - max_close_factor: this.config.maxCloseFactor, + target_health_factor: this.config.targetHealthFactor, } await this.instantiate('params', this.storage.codeIds.params!, msg) } diff --git a/scripts/deploy/neutron/config.ts b/scripts/deploy/neutron/config.ts index 1662ebd07..192aedab5 100644 --- a/scripts/deploy/neutron/config.ts +++ b/scripts/deploy/neutron/config.ts @@ -201,7 +201,7 @@ export const neutronTestnetConfig: DeploymentConfig = { swapperDexName: 'astroport', assets: [ntrnAsset, atomAsset], oracleConfigs: [axlUSDCOracleTestnet, ntrnOracleTestnet, atomOracleTestnet], - maxCloseFactor: '0.5', + targetHealthFactor: '1.2', oracleCustomInitParams: { astroport_factory: 'neutron1jj0scx400pswhpjes589aujlqagxgcztw04srynmhf0f6zplzn2qqmhwj7', }, diff --git a/scripts/deploy/osmosis/config.ts b/scripts/deploy/osmosis/config.ts index 176a950bf..b32c53a59 100644 --- a/scripts/deploy/osmosis/config.ts +++ b/scripts/deploy/osmosis/config.ts @@ -218,7 +218,7 @@ export const osmosisTestnetConfig: DeploymentConfig = { swapperDexName: 'osmosis', assets: [osmoAsset, atomAsset, axlUSDCAsset], oracleConfigs: [atomOracle, axlUSDCOracle], - maxCloseFactor: '0.5', + targetHealthFactor: '1.2', incentiveEpochDuration: 86400, maxWhitelistedIncentiveDenoms: 10, targetHealthFactor: '1.2', @@ -256,7 +256,7 @@ export const osmosisTestMultisig: DeploymentConfig = { swapperDexName: 'osmosis', assets: [osmoAsset, atomAsset, axlUSDCAsset], oracleConfigs: [atomOracle, axlUSDCOracle], - maxCloseFactor: '0.5', + targetHealthFactor: '1.2', incentiveEpochDuration: 86400, maxWhitelistedIncentiveDenoms: 10, targetHealthFactor: '1.2', @@ -321,7 +321,7 @@ export const osmosisMainnet: DeploymentConfig = { swapperDexName: 'osmosis', assets: [osmoAsset, atomAsset, axlUSDCAsset], oracleConfigs: [atomOracle, axlUSDCOracle], - maxCloseFactor: '0.5', + targetHealthFactor: '1.2', incentiveEpochDuration: 86400, maxWhitelistedIncentiveDenoms: 10, targetHealthFactor: '1.2', @@ -357,7 +357,7 @@ export const osmosisLocalConfig: DeploymentConfig = { swapperDexName: 'osmosis', assets: [osmoAsset, atomAsset, axlUSDCAsset], oracleConfigs: [atomOracle, axlUSDCOracle], - maxCloseFactor: '0.5', + targetHealthFactor: '1.2', incentiveEpochDuration: 86400, maxWhitelistedIncentiveDenoms: 10, targetHealthFactor: '1.2', diff --git a/scripts/types/config.ts b/scripts/types/config.ts index bf7733f58..cdbb4d1af 100644 --- a/scripts/types/config.ts +++ b/scripts/types/config.ts @@ -49,7 +49,7 @@ export interface DeploymentConfig { safetyFundAddr: string protocolAdminAddr: string feeCollectorAddr: string - maxCloseFactor: string + targetHealthFactor: string swapperDexName: string assets: AssetConfig[] oracleConfigs: OracleConfig[] diff --git a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts index c70068b26..1cf40c87d 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.client.ts @@ -20,6 +20,7 @@ import { GeometricTwap, RedemptionRateForString, QueryMsg, + ActionKind, ConfigResponse, PriceResponse, PriceSourceResponseForString, @@ -37,11 +38,13 @@ export interface MarsOracleOsmosisReadOnlyInterface { limit?: number startAfter?: string }) => Promise - price: ({ denom }: { denom: string }) => Promise + price: ({ denom, kind }: { denom: string; kind?: ActionKind }) => Promise prices: ({ + kind, limit, startAfter, }: { + kind?: ActionKind limit?: number startAfter?: string }) => Promise @@ -86,22 +89,26 @@ export class MarsOracleOsmosisQueryClient implements MarsOracleOsmosisReadOnlyIn }, }) } - price = async ({ denom }: { denom: string }): Promise => { + price = async ({ denom, kind }: { denom: string; kind?: ActionKind }): Promise => { return this.client.queryContractSmart(this.contractAddress, { price: { denom, + kind, }, }) } prices = async ({ + kind, limit, startAfter, }: { + kind?: ActionKind limit?: number startAfter?: string }): Promise => { return this.client.queryContractSmart(this.contractAddress, { prices: { + kind, limit, start_after: startAfter, }, diff --git a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.react-query.ts b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.react-query.ts index 739c3a028..3dd813bc3 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.react-query.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.react-query.ts @@ -21,6 +21,7 @@ import { GeometricTwap, RedemptionRateForString, QueryMsg, + ActionKind, ConfigResponse, PriceResponse, PriceSourceResponseForString, @@ -67,6 +68,7 @@ export interface MarsOracleOsmosisReactQuery { export interface MarsOracleOsmosisPricesQuery extends MarsOracleOsmosisReactQuery { args: { + kind?: ActionKind limit?: number startAfter?: string } @@ -81,6 +83,7 @@ export function useMarsOracleOsmosisPricesQuery({ () => client ? client.prices({ + kind: args.kind, limit: args.limit, startAfter: args.startAfter, }) @@ -92,6 +95,7 @@ export interface MarsOracleOsmosisPriceQuery extends MarsOracleOsmosisReactQuery { args: { denom: string + kind?: ActionKind } } export function useMarsOracleOsmosisPriceQuery({ @@ -105,6 +109,7 @@ export function useMarsOracleOsmosisPriceQuery({ client ? client.price({ denom: args.denom, + kind: args.kind, }) : Promise.reject(new Error('Invalid client')), { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, diff --git a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts index d1ee479fc..980e1835a 100644 --- a/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts +++ b/scripts/types/generated/mars-oracle-osmosis/MarsOracleOsmosis.types.ts @@ -84,6 +84,8 @@ export type OsmosisPriceSourceForString = pyth: { contract_addr: string denom_decimals: number + max_confidence: Decimal + max_deviation: Decimal max_staleness: number price_feed_id: Identifier [k: string]: unknown @@ -174,14 +176,17 @@ export type QueryMsg = | { price: { denom: string + kind?: ActionKind | null } } | { prices: { + kind?: ActionKind | null limit?: number | null start_after?: string | null } } +export type ActionKind = 'default' | 'liquidation' export interface ConfigResponse { base_denom: string owner?: string | null diff --git a/scripts/types/generated/mars-oracle-wasm/MarsOracleWasm.client.ts b/scripts/types/generated/mars-oracle-wasm/MarsOracleWasm.client.ts index b33fc1dce..71d56e287 100644 --- a/scripts/types/generated/mars-oracle-wasm/MarsOracleWasm.client.ts +++ b/scripts/types/generated/mars-oracle-wasm/MarsOracleWasm.client.ts @@ -17,6 +17,7 @@ import { OwnerUpdate, WasmOracleCustomExecuteMsg, QueryMsg, + ActionKind, ConfigResponse, PriceResponse, PriceSourceResponseForString, @@ -34,11 +35,13 @@ export interface MarsOracleWasmReadOnlyInterface { limit?: number startAfter?: string }) => Promise - price: ({ denom }: { denom: string }) => Promise + price: ({ denom, kind }: { denom: string; kind?: ActionKind }) => Promise prices: ({ + kind, limit, startAfter, }: { + kind?: ActionKind limit?: number startAfter?: string }) => Promise @@ -83,22 +86,26 @@ export class MarsOracleWasmQueryClient implements MarsOracleWasmReadOnlyInterfac }, }) } - price = async ({ denom }: { denom: string }): Promise => { + price = async ({ denom, kind }: { denom: string; kind?: ActionKind }): Promise => { return this.client.queryContractSmart(this.contractAddress, { price: { denom, + kind, }, }) } prices = async ({ + kind, limit, startAfter, }: { + kind?: ActionKind limit?: number startAfter?: string }): Promise => { return this.client.queryContractSmart(this.contractAddress, { prices: { + kind, limit, start_after: startAfter, }, diff --git a/scripts/types/generated/mars-oracle-wasm/MarsOracleWasm.react-query.ts b/scripts/types/generated/mars-oracle-wasm/MarsOracleWasm.react-query.ts index 07d20b201..fb528281f 100644 --- a/scripts/types/generated/mars-oracle-wasm/MarsOracleWasm.react-query.ts +++ b/scripts/types/generated/mars-oracle-wasm/MarsOracleWasm.react-query.ts @@ -18,6 +18,7 @@ import { OwnerUpdate, WasmOracleCustomExecuteMsg, QueryMsg, + ActionKind, ConfigResponse, PriceResponse, PriceSourceResponseForString, @@ -60,6 +61,7 @@ export interface MarsOracleWasmReactQuery { export interface MarsOracleWasmPricesQuery extends MarsOracleWasmReactQuery { args: { + kind?: ActionKind limit?: number startAfter?: string } @@ -74,6 +76,7 @@ export function useMarsOracleWasmPricesQuery({ () => client ? client.prices({ + kind: args.kind, limit: args.limit, startAfter: args.startAfter, }) @@ -85,6 +88,7 @@ export interface MarsOracleWasmPriceQuery extends MarsOracleWasmReactQuery { args: { denom: string + kind?: ActionKind } } export function useMarsOracleWasmPriceQuery({ @@ -98,6 +102,7 @@ export function useMarsOracleWasmPriceQuery({ client ? client.price({ denom: args.denom, + kind: args.kind, }) : Promise.reject(new Error('Invalid client')), { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, diff --git a/scripts/types/generated/mars-oracle-wasm/MarsOracleWasm.types.ts b/scripts/types/generated/mars-oracle-wasm/MarsOracleWasm.types.ts index bef4d7aaf..7cbe5f6af 100644 --- a/scripts/types/generated/mars-oracle-wasm/MarsOracleWasm.types.ts +++ b/scripts/types/generated/mars-oracle-wasm/MarsOracleWasm.types.ts @@ -58,6 +58,8 @@ export type WasmPriceSourceForString = pyth: { contract_addr: string denom_decimals: number + max_confidence: Decimal + max_deviation: Decimal max_staleness: number price_feed_id: Identifier } @@ -102,14 +104,17 @@ export type QueryMsg = | { price: { denom: string + kind?: ActionKind | null } } | { prices: { + kind?: ActionKind | null limit?: number | null start_after?: string | null } } +export type ActionKind = 'default' | 'liquidation' export interface ConfigResponse { base_denom: string owner?: string | null From 44730cf5ae8a7f20062903b34143ac411cab2447 Mon Sep 17 00:00:00 2001 From: piobab Date: Wed, 21 Jun 2023 18:36:13 +0200 Subject: [PATCH 04/43] Mp 2487 extract asset params (#191) * Read asset params from remote contract. * Fix tests. * Fix tests. * Fix integration tests. * Fix clippy. * Fix tests. * Remove close_factor. Use external contract. * Update schema. * Clean comment. * Update to latest mars-params version. * Include mars-params wasm file in github pipeline. * Update downloading artifacts. * Fix build. --- Cargo.lock | 87 +++- Cargo.toml | 1 + Makefile.toml | 8 + contracts/red-bank/Cargo.toml | 1 + contracts/red-bank/src/execute.rs | 198 +++----- contracts/red-bank/src/health.rs | 16 +- contracts/red-bank/src/helpers.rs | 22 + contracts/red-bank/src/lib.rs | 2 + contracts/red-bank/src/query.rs | 12 +- contracts/red-bank/tests/helpers.rs | 24 +- contracts/red-bank/tests/test_admin.rs | 423 +----------------- contracts/red-bank/tests/test_borrow.rs | 124 ++++- contracts/red-bank/tests/test_deposit.rs | 70 ++- contracts/red-bank/tests/test_liquidate.rs | 89 +++- contracts/red-bank/tests/test_misc.rs | 34 +- .../tests/test_update_emergency_owner.rs | 75 ---- contracts/red-bank/tests/test_withdraw.rs | 41 +- integration-tests/Cargo.toml | 1 + integration-tests/tests/helpers.rs | 63 ++- integration-tests/tests/test_incentives.rs | 63 ++- integration-tests/tests/test_liquidations.rs | 31 +- integration-tests/tests/test_oracles.rs | 53 ++- integration-tests/tests/test_rover_flow.rs | 11 +- integration-tests/tests/test_user_flow.rs | 51 ++- packages/health/Cargo.toml | 1 + packages/health/src/health.rs | 10 +- packages/health/src/query.rs | 22 +- .../tests/test_from_coins_to_positions.rs | 52 ++- .../health/tests/test_health_from_coins.rs | 67 ++- packages/testing/Cargo.toml | 1 + .../testing/src/integration/mock_contracts.rs | 9 + packages/testing/src/integration/mock_env.rs | 53 ++- packages/testing/src/lib.rs | 1 + packages/testing/src/mars_mock_querier.rs | 17 + packages/testing/src/params_querier.rs | 26 ++ packages/types/src/address_provider.rs | 3 + packages/types/src/red_bank/market.rs | 39 +- packages/types/src/red_bank/msg.rs | 15 - packages/types/src/red_bank/types.rs | 14 - .../mars-address-provider.json | 15 +- schemas/mars-red-bank/mars-red-bank.json | 200 +-------- .../MarsAddressProvider.types.ts | 2 +- .../mars-red-bank/MarsRedBank.client.ts | 2 +- .../mars-red-bank/MarsRedBank.react-query.ts | 2 +- .../mars-red-bank/MarsRedBank.types.ts | 17 +- 45 files changed, 957 insertions(+), 1111 deletions(-) create mode 100644 contracts/red-bank/src/helpers.rs delete mode 100644 contracts/red-bank/tests/test_update_emergency_owner.rs create mode 100644 packages/testing/src/params_querier.rs diff --git a/Cargo.lock b/Cargo.lock index 317b785c7..7c3d55765 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1765,7 +1765,7 @@ dependencies = [ "cw-storage-plus 1.1.0", "cw2 1.1.0", "mars-owner", - "mars-red-bank-types", + "mars-red-bank-types 1.1.0", "serde", "thiserror", ] @@ -1775,7 +1775,8 @@ name = "mars-health" version = "1.1.0" dependencies = [ "cosmwasm-std", - "mars-red-bank-types", + "mars-params 1.0.2", + "mars-red-bank-types 1.1.0", "mars-testing", "thiserror", ] @@ -1790,9 +1791,9 @@ dependencies = [ "cw2 1.1.0", "mars-owner", "mars-red-bank", - "mars-red-bank-types", + "mars-red-bank-types 1.1.0", "mars-testing", - "mars-utils", + "mars-utils 1.1.0", "osmosis-std 0.16.1", "test-case", "thiserror", @@ -1809,12 +1810,13 @@ dependencies = [ "mars-oracle-base", "mars-oracle-osmosis", "mars-osmosis", + "mars-params 1.0.2", "mars-red-bank", - "mars-red-bank-types", + "mars-red-bank-types 1.1.0", "mars-rewards-collector", "mars-swapper-osmosis", "mars-testing", - "mars-utils", + "mars-utils 1.1.0", "osmosis-std 0.16.1", "osmosis-test-tube 16.0.1", "serde", @@ -1828,7 +1830,7 @@ dependencies = [ "cw-storage-plus 1.1.0", "cw2 1.1.0", "mars-owner", - "mars-red-bank-types", + "mars-red-bank-types 1.1.0", "pyth-sdk-cw", "schemars", "serde", @@ -1846,9 +1848,9 @@ dependencies = [ "mars-oracle-base", "mars-osmosis", "mars-owner", - "mars-red-bank-types", + "mars-red-bank-types 1.1.0", "mars-testing", - "mars-utils", + "mars-utils 1.1.0", "osmosis-std 0.16.1", "pyth-sdk-cw", "schemars", @@ -1867,7 +1869,7 @@ dependencies = [ "cw2 1.1.0", "mars-oracle-base", "mars-owner", - "mars-red-bank-types", + "mars-red-bank-types 1.1.0", "mars-testing", "proptest", "pyth-sdk-cw", @@ -1896,6 +1898,24 @@ dependencies = [ "thiserror", ] +[[package]] +name = "mars-params" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e76688c883401778ab5f60c83e0e982f307438fbbcbfea10bbb97a950222636e" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.1.0", + "cw2 1.1.0", + "mars-owner", + "mars-red-bank-types 1.0.0", + "mars-utils 1.0.0", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "mars-params" version = "1.0.7" @@ -1907,7 +1927,7 @@ dependencies = [ "cw-storage-plus 1.1.0", "cw2 1.1.0", "mars-owner", - "mars-utils", + "mars-utils 1.1.0", "schemars", "serde", "thiserror", @@ -1924,9 +1944,23 @@ dependencies = [ "cw2 1.1.0", "mars-health", "mars-owner", - "mars-red-bank-types", + "mars-params 1.0.2", + "mars-red-bank-types 1.1.0", "mars-testing", - "mars-utils", + "mars-utils 1.1.0", + "thiserror", +] + +[[package]] +name = "mars-red-bank-types" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cca59bb17daa753c30d3c934e0779736200708cb89a34020fa805b1cb05e7278" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "mars-owner", + "mars-utils 1.0.0", "thiserror", ] @@ -1937,7 +1971,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "mars-owner", - "mars-utils", + "mars-utils 1.1.0", "strum", "thiserror", ] @@ -1951,9 +1985,9 @@ dependencies = [ "cw-storage-plus 1.1.0", "mars-osmosis", "mars-owner", - "mars-red-bank-types", + "mars-red-bank-types 1.1.0", "mars-testing", - "mars-utils", + "mars-utils 1.1.0", "osmosis-std 0.16.1", "schemars", "serde", @@ -1971,7 +2005,7 @@ dependencies = [ "cw-it", "cw2 1.1.0", "mars-oracle-wasm", - "mars-red-bank-types", + "mars-red-bank-types 1.1.0", "mars-swapper-base", "mars-testing", "test-case", @@ -1986,7 +2020,7 @@ dependencies = [ "cw-paginate", "cw-storage-plus 1.1.0", "mars-owner", - "mars-red-bank-types", + "mars-red-bank-types 1.1.0", "schemars", "serde", "thiserror", @@ -1999,7 +2033,7 @@ dependencies = [ "anyhow", "cosmwasm-std", "cw-multi-test 0.16.5", - "mars-red-bank-types", + "mars-red-bank-types 1.1.0", ] [[package]] @@ -2013,7 +2047,7 @@ dependencies = [ "cw2 1.1.0", "mars-osmosis", "mars-owner", - "mars-red-bank-types", + "mars-red-bank-types 1.1.0", "mars-swapper-base", "osmosis-std 0.16.1", ] @@ -2033,8 +2067,9 @@ dependencies = [ "mars-oracle-wasm", "mars-osmosis", "mars-owner", + "mars-params 1.0.2", "mars-red-bank", - "mars-red-bank-types", + "mars-red-bank-types 1.1.0", "mars-rewards-collector", "mars-swapper-astroport", "osmosis-std 0.16.1", @@ -2042,6 +2077,16 @@ dependencies = [ "pyth-sdk-cw", ] +[[package]] +name = "mars-utils" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bae572eda20842ade4bf8ab09ce0856cae5cff89dbeb7c51e9123489e48256" +dependencies = [ + "cosmwasm-std", + "thiserror", +] + [[package]] name = "mars-utils" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 0a31cf95f..f80c4770a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ proptest = "1.1.0" # packages mars-health = { version = "1.0.0", path = "./packages/health" } mars-osmosis = { version = "1.0.0", path = "./packages/chains/osmosis" } +mars-params = "=1.0.2" mars-red-bank-types = { version = "1.0.0", path = "./packages/types" } mars-testing = { version = "1.0.0", path = "./packages/testing" } mars-utils = { version = "1.0.0", path = "./packages/utils" } diff --git a/Makefile.toml b/Makefile.toml index 0f985ef21..9ee6396a1 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -49,6 +49,13 @@ docker run --rm -v "$(pwd)":/code \ ${image} """ +# Download artifacts used in integration tests. +# NOTE: use correct version of the artifact. +[tasks.download-artifacts] +script = """ +wget https://github.com/mars-protocol/mars-common/releases/download/v1.0.0-alpha/mars_params.wasm -O $ARTIFACTS_DIR_PATH/mars_params.wasm +""" + [tasks.test] toolchain = "${RUST_VERSION}" command = "cargo" @@ -92,6 +99,7 @@ dependencies = [ "fmt", "clippy", "build", + "download-artifacts", "test", "audit", "generate-all-schemas", diff --git a/contracts/red-bank/Cargo.toml b/contracts/red-bank/Cargo.toml index 1d56efd11..56de49c9f 100644 --- a/contracts/red-bank/Cargo.toml +++ b/contracts/red-bank/Cargo.toml @@ -28,6 +28,7 @@ cw-storage-plus = { workspace = true } cw-utils = { workspace = true } mars-health = { workspace = true } mars-owner = { workspace = true } +mars-params = { workspace = true } mars-red-bank-types = { workspace = true } mars-utils = { workspace = true } thiserror = { workspace = true } diff --git a/contracts/red-bank/src/execute.rs b/contracts/red-bank/src/execute.rs index 48bec8c42..9138edb9a 100644 --- a/contracts/red-bank/src/execute.rs +++ b/contracts/red-bank/src/execute.rs @@ -3,7 +3,8 @@ use std::{cmp::min, str}; use cosmwasm_std::{ Addr, Decimal, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Uint128, }; -use mars_owner::{OwnerError, OwnerInit::SetInitialOwner, OwnerUpdate}; +use mars_owner::{OwnerInit::SetInitialOwner, OwnerUpdate}; +use mars_params::types::AssetParams; use mars_red_bank_types::{ address_provider::{self, MarsAddressType}, error::MarsError, @@ -22,6 +23,7 @@ use crate::{ assert_below_liq_threshold_after_withdraw, assert_below_max_ltv_after_borrow, assert_liquidatable, }, + helpers::{query_asset_params, query_close_factor}, interest_rates::{ apply_accumulated_interests, get_scaled_debt_amount, get_scaled_liquidity_amount, get_underlying_debt_amount, get_underlying_liquidity_amount, update_interest_rates, @@ -40,23 +42,16 @@ pub fn instantiate(deps: DepsMut, msg: InstantiateMsg) -> Result Result { - if OWNER.is_owner(deps.storage, &info.sender)? { - update_asset_by_owner(deps, &env, &denom, params) - } else if OWNER.is_emergency_owner(deps.storage, &info.sender)? { - update_asset_by_emergency_owner(deps, &denom, params) - } else { - Err(OwnerError::NotOwner {}.into()) - } -} + OWNER.assert_owner(deps.storage, &info.sender)?; -fn update_asset_by_owner( - deps: DepsMut, - env: &Env, - denom: &str, - params: InitOrUpdateAssetParams, -) -> Result { - let market_option = MARKETS.may_load(deps.storage, denom)?; + let market_option = MARKETS.may_load(deps.storage, &denom)?; match market_option { None => Err(ContractError::AssetNotInitialized {}), Some(mut market) => { // Destructuring a struct’s fields into separate variables in order to force // compile error if we add more params let InitOrUpdateAssetParams { - max_loan_to_value, reserve_factor, - liquidation_threshold, - liquidation_bonus, interest_rate_model, - deposit_enabled, - borrow_enabled, - deposit_cap, } = params; // If reserve factor or interest rates are updated we update indexes with @@ -250,7 +202,7 @@ fn update_asset_by_owner( response = apply_accumulated_interests( deps.storage, - env, + &env, &mut market, rewards_collector_addr, incentives_addr, @@ -259,53 +211,23 @@ fn update_asset_by_owner( } let mut updated_market = Market { - max_loan_to_value: max_loan_to_value.unwrap_or(market.max_loan_to_value), reserve_factor: reserve_factor.unwrap_or(market.reserve_factor), - liquidation_threshold: liquidation_threshold - .unwrap_or(market.liquidation_threshold), - liquidation_bonus: liquidation_bonus.unwrap_or(market.liquidation_bonus), interest_rate_model: interest_rate_model.unwrap_or(market.interest_rate_model), - deposit_enabled: deposit_enabled.unwrap_or(market.deposit_enabled), - borrow_enabled: borrow_enabled.unwrap_or(market.borrow_enabled), - deposit_cap: deposit_cap.unwrap_or(market.deposit_cap), ..market }; updated_market.validate()?; if should_update_interest_rates { - response = update_interest_rates(env, &mut updated_market, response)?; + response = update_interest_rates(&env, &mut updated_market, response)?; } - MARKETS.save(deps.storage, denom, &updated_market)?; + MARKETS.save(deps.storage, &denom, &updated_market)?; Ok(response.add_attribute("action", "update_asset").add_attribute("denom", denom)) } } } -/// Emergency owner can only DISABLE BORROWING. -fn update_asset_by_emergency_owner( - deps: DepsMut, - denom: &str, - params: InitOrUpdateAssetParams, -) -> Result { - if let Some(mut market) = MARKETS.may_load(deps.storage, denom)? { - match params.borrow_enabled { - Some(borrow_enabled) if !borrow_enabled => { - market.borrow_enabled = borrow_enabled; - MARKETS.save(deps.storage, denom, &market)?; - - Ok(Response::new() - .add_attribute("action", "emergency_update_asset") - .add_attribute("denom", denom)) - } - _ => Err(MarsError::Unauthorized {}.into()), - } - } else { - Err(ContractError::AssetNotInitialized {}) - } -} - /// Update uncollateralized loan limit by a given amount in base asset pub fn update_uncollateralized_loan_limit( deps: DepsMut, @@ -368,7 +290,25 @@ pub fn deposit( }; let mut market = MARKETS.load(deps.storage, &denom)?; - if !market.deposit_enabled { + + let config = CONFIG.load(deps.storage)?; + + let addresses = address_provider::helpers::query_contract_addrs( + deps.as_ref(), + &config.address_provider, + vec![ + MarsAddressType::Incentives, + MarsAddressType::RewardsCollector, + MarsAddressType::Params, + ], + )?; + let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; + let incentives_addr = &addresses[&MarsAddressType::Incentives]; + let params_addr = &addresses[&MarsAddressType::Params]; + + let asset_params = query_asset_params(&deps.querier, params_addr, &denom)?; + + if !asset_params.red_bank.deposit_enabled { return Err(ContractError::DepositNotEnabled { denom, }); @@ -377,7 +317,7 @@ pub fn deposit( let total_scaled_deposits = market.collateral_total_scaled; let total_deposits = get_underlying_liquidity_amount(total_scaled_deposits, &market, env.block.time.seconds())?; - if total_deposits.checked_add(deposit_amount)? > market.deposit_cap { + if total_deposits.checked_add(deposit_amount)? > asset_params.red_bank.deposit_cap { return Err(ContractError::DepositCapExceeded { denom, }); @@ -385,17 +325,7 @@ pub fn deposit( let mut response = Response::new(); - let config = CONFIG.load(deps.storage)?; - // update indexes and interest rates - let addresses = address_provider::helpers::query_contract_addrs( - deps.as_ref(), - &config.address_provider, - vec![MarsAddressType::Incentives, MarsAddressType::RewardsCollector], - )?; - let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; - let incentives_addr = &addresses[&MarsAddressType::Incentives]; - response = apply_accumulated_interests( deps.storage, &env, @@ -484,11 +414,13 @@ pub fn withdraw( MarsAddressType::Oracle, MarsAddressType::Incentives, MarsAddressType::RewardsCollector, + MarsAddressType::Params, ], )?; let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; let incentives_addr = &addresses[&MarsAddressType::Incentives]; let oracle_addr = &addresses[&MarsAddressType::Oracle]; + let params_addr = &addresses[&MarsAddressType::Params]; // if asset is used as collateral and user is borrowing we need to validate health factor after withdraw, // otherwise no reasons to block the withdraw @@ -499,6 +431,7 @@ pub fn withdraw( &env, withdrawer.address(), oracle_addr, + params_addr, &denom, withdraw_amount, )? @@ -568,15 +501,34 @@ pub fn borrow( ) -> Result { let borrower = User(&info.sender); - // Load market and user state - let mut borrow_market = MARKETS.load(deps.storage, &denom)?; + let config = CONFIG.load(deps.storage)?; + + let addresses = address_provider::helpers::query_contract_addrs( + deps.as_ref(), + &config.address_provider, + vec![ + MarsAddressType::Oracle, + MarsAddressType::Incentives, + MarsAddressType::RewardsCollector, + MarsAddressType::Params, + ], + )?; + let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; + let incentives_addr = &addresses[&MarsAddressType::Incentives]; + let oracle_addr = &addresses[&MarsAddressType::Oracle]; + let params_addr = &addresses[&MarsAddressType::Params]; + + let asset_params = query_asset_params(&deps.querier, params_addr, &denom)?; - if !borrow_market.borrow_enabled { + if !asset_params.red_bank.borrow_enabled { return Err(ContractError::BorrowNotEnabled { denom, }); } + // Load market and user state + let mut borrow_market = MARKETS.load(deps.storage, &denom)?; + let collateral_balance_before = get_underlying_liquidity_amount( borrow_market.collateral_total_scaled, &borrow_market, @@ -592,21 +544,6 @@ pub fn borrow( let uncollateralized_loan_limit = borrower.uncollateralized_loan_limit(deps.storage, &denom)?; - let config = CONFIG.load(deps.storage)?; - - let addresses = address_provider::helpers::query_contract_addrs( - deps.as_ref(), - &config.address_provider, - vec![ - MarsAddressType::Oracle, - MarsAddressType::Incentives, - MarsAddressType::RewardsCollector, - ], - )?; - let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; - let incentives_addr = &addresses[&MarsAddressType::Incentives]; - let oracle_addr = &addresses[&MarsAddressType::Oracle]; - // Check if user can borrow specified amount let mut uncollateralized_debt = false; if uncollateralized_loan_limit.is_zero() { @@ -615,6 +552,7 @@ pub fn borrow( &env, borrower.address(), oracle_addr, + params_addr, &denom, borrow_amount, )? { @@ -815,14 +753,16 @@ pub fn liquidate( MarsAddressType::Oracle, MarsAddressType::Incentives, MarsAddressType::RewardsCollector, + MarsAddressType::Params, ], )?; let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; let incentives_addr = &addresses[&MarsAddressType::Incentives]; let oracle_addr = &addresses[&MarsAddressType::Oracle]; + let params_addr = &addresses[&MarsAddressType::Params]; let (liquidatable, assets_positions) = - assert_liquidatable(&deps.as_ref(), &env, &user_addr, oracle_addr)?; + assert_liquidatable(&deps.as_ref(), &env, &user_addr, oracle_addr, params_addr)?; if !liquidatable { return Err(ContractError::CannotLiquidateHealthyPosition {}); @@ -851,6 +791,9 @@ pub fn liquidate( let user_debt_amount = get_underlying_debt_amount(user_debt.amount_scaled, &debt_market, block_time)?; + let collateral_params = query_asset_params(&deps.querier, params_addr, &collateral_denom)?; + let close_factor = query_close_factor(&deps.querier, params_addr)?; + let ( debt_amount_to_repay, collateral_amount_to_liquidate, @@ -861,10 +804,11 @@ pub fn liquidate( user_debt_amount, sent_debt_amount, &collateral_market, + &collateral_params, collateral_price, debt_price, block_time, - config.close_factor, + close_factor, )?; // 4. Transfer collateral shares from the user to the liquidator @@ -967,6 +911,7 @@ pub fn liquidation_compute_amounts( user_debt_amount: Uint128, sent_debt_amount: Uint128, collateral_market: &Market, + collateral_params: &AssetParams, collateral_price: Decimal, debt_price: Decimal, block_time: u64, @@ -978,7 +923,7 @@ pub fn liquidation_compute_amounts( // Collateral: debt to repay in base asset times the liquidation bonus let mut collateral_amount_to_liquidate = math::divide_uint128_by_decimal( - debt_amount_to_repay * debt_price * (Decimal::one() + collateral_market.liquidation_bonus), + debt_amount_to_repay * debt_price * (Decimal::one() + collateral_params.liquidation_bonus), collateral_price, )?; let mut collateral_amount_to_liquidate_scaled = @@ -998,7 +943,7 @@ pub fn liquidation_compute_amounts( collateral_amount_to_liquidate * collateral_price, debt_price, )?, - Decimal::one() + collateral_market.liquidation_bonus, + Decimal::one() + collateral_params.liquidation_bonus, )?; } @@ -1052,14 +997,17 @@ pub fn update_asset_collateral_status( // user is not liquidatable after disabling if previously_enabled && !enable { let config = CONFIG.load(deps.storage)?; - let oracle_addr = address_provider::helpers::query_contract_addr( + + let addresses = address_provider::helpers::query_contract_addrs( deps.as_ref(), &config.address_provider, - MarsAddressType::Oracle, + vec![MarsAddressType::Oracle, MarsAddressType::Params], )?; + let oracle_addr = &addresses[&MarsAddressType::Oracle]; + let params_addr = &addresses[&MarsAddressType::Params]; let (liquidatable, _) = - assert_liquidatable(&deps.as_ref(), &env, user.address(), &oracle_addr)?; + assert_liquidatable(&deps.as_ref(), &env, user.address(), oracle_addr, params_addr)?; if liquidatable { return Err(ContractError::InvalidHealthFactorAfterDisablingCollateral {}); diff --git a/contracts/red-bank/src/health.rs b/contracts/red-bank/src/health.rs index 5be29e812..bce9c0fd1 100644 --- a/contracts/red-bank/src/health.rs +++ b/contracts/red-bank/src/health.rs @@ -6,6 +6,7 @@ use mars_red_bank_types::{oracle, red_bank::Position}; use crate::{ error::ContractError, + helpers::query_asset_params, interest_rates::{get_underlying_debt_amount, get_underlying_liquidity_amount}, state::{COLLATERALS, DEBTS, MARKETS}, }; @@ -16,8 +17,9 @@ pub fn assert_liquidatable( env: &Env, user_addr: &Addr, oracle_addr: &Addr, + params_addr: &Addr, ) -> Result<(bool, HashMap), ContractError> { - let positions = get_user_positions_map(deps, env, user_addr, oracle_addr)?; + let positions = get_user_positions_map(deps, env, user_addr, oracle_addr, params_addr)?; let health = compute_position_health(&positions)?; Ok((health.is_liquidatable(), positions)) @@ -29,10 +31,11 @@ pub fn assert_below_liq_threshold_after_withdraw( env: &Env, user_addr: &Addr, oracle_addr: &Addr, + params_addr: &Addr, denom: &str, withdraw_amount: Uint128, ) -> Result { - let mut positions = get_user_positions_map(deps, env, user_addr, oracle_addr)?; + let mut positions = get_user_positions_map(deps, env, user_addr, oracle_addr, params_addr)?; // Update position to compute health factor after withdraw match positions.get_mut(denom) { @@ -52,10 +55,11 @@ pub fn assert_below_max_ltv_after_borrow( env: &Env, user_addr: &Addr, oracle_addr: &Addr, + params_addr: &Addr, denom: &str, borrow_amount: Uint128, ) -> Result { - let mut positions = get_user_positions_map(deps, env, user_addr, oracle_addr)?; + let mut positions = get_user_positions_map(deps, env, user_addr, oracle_addr, params_addr)?; // Update position to compute health factor after borrow positions @@ -107,6 +111,7 @@ pub fn get_user_positions_map( env: &Env, user_addr: &Addr, oracle_addr: &Addr, + params_addr: &Addr, ) -> StdResult> { let block_time = env.block.time.seconds(); @@ -131,6 +136,7 @@ pub fn get_user_positions_map( .into_iter() .map(|denom| { let market = MARKETS.load(deps.storage, &denom)?; + let params = query_asset_params(&deps.querier, params_addr, &denom)?; let collateral_amount = match COLLATERALS.may_load(deps.storage, (user_addr, &denom))? { Some(collateral) if collateral.enabled => { @@ -157,8 +163,8 @@ pub fn get_user_positions_map( collateral_amount, debt_amount, uncollateralized_debt, - max_ltv: market.max_loan_to_value, - liquidation_threshold: market.liquidation_threshold, + max_ltv: params.max_loan_to_value, + liquidation_threshold: params.liquidation_threshold, asset_price, }; diff --git a/contracts/red-bank/src/helpers.rs b/contracts/red-bank/src/helpers.rs new file mode 100644 index 000000000..3d1d55c27 --- /dev/null +++ b/contracts/red-bank/src/helpers.rs @@ -0,0 +1,22 @@ +use cosmwasm_std::{Decimal, QuerierWrapper, StdResult}; +use mars_params::{msg::QueryMsg, types::AssetParams}; + +pub fn query_asset_params( + querier: &QuerierWrapper, + params: impl Into, + denom: impl Into, +) -> StdResult { + querier.query_wasm_smart( + params.into(), + &QueryMsg::AssetParams { + denom: denom.into(), + }, + ) +} + +pub fn query_close_factor( + querier: &QuerierWrapper, + params: impl Into, +) -> StdResult { + querier.query_wasm_smart(params.into(), &QueryMsg::MaxCloseFactor {}) +} diff --git a/contracts/red-bank/src/lib.rs b/contracts/red-bank/src/lib.rs index d3d5c1ef0..3e65712c9 100644 --- a/contracts/red-bank/src/lib.rs +++ b/contracts/red-bank/src/lib.rs @@ -7,3 +7,5 @@ pub mod interest_rates; pub mod query; pub mod state; pub mod user; + +pub mod helpers; diff --git a/contracts/red-bank/src/query.rs b/contracts/red-bank/src/query.rs index 5fea2642b..dbb9698c2 100644 --- a/contracts/red-bank/src/query.rs +++ b/contracts/red-bank/src/query.rs @@ -27,9 +27,7 @@ pub fn query_config(deps: Deps) -> StdResult { Ok(ConfigResponse { owner: owner_state.owner, proposed_new_owner: owner_state.proposed, - emergency_owner: owner_state.emergency_owner, address_provider: config.address_provider.to_string(), - close_factor: config.close_factor, }) } @@ -252,13 +250,17 @@ pub fn query_user_position( user_addr: Addr, ) -> Result { let config = CONFIG.load(deps.storage)?; - let oracle_addr = address_provider::helpers::query_contract_addr( + + let addresses = address_provider::helpers::query_contract_addrs( deps, &config.address_provider, - MarsAddressType::Oracle, + vec![MarsAddressType::Oracle, MarsAddressType::Params], )?; + let oracle_addr = &addresses[&MarsAddressType::Oracle]; + let params_addr = &addresses[&MarsAddressType::Params]; - let positions = health::get_user_positions_map(&deps, &env, &user_addr, &oracle_addr)?; + let positions = + health::get_user_positions_map(&deps, &env, &user_addr, oracle_addr, params_addr)?; let health = health::compute_position_health(&positions)?; let health_status = if let (Some(max_ltv_hf), Some(liq_threshold_hf)) = diff --git a/contracts/red-bank/tests/helpers.rs b/contracts/red-bank/tests/helpers.rs index 143bc09e8..39bdc5bfe 100644 --- a/contracts/red-bank/tests/helpers.rs +++ b/contracts/red-bank/tests/helpers.rs @@ -6,6 +6,7 @@ use cosmwasm_std::{ testing::{MockApi, MockStorage}, Addr, Coin, Decimal, Deps, DepsMut, Event, OwnedDeps, Uint128, }; +use mars_params::types::{AssetParams, HighLeverageStrategyParams, RedBankSettings, RoverSettings}; use mars_red_bank::{ contract::{instantiate, query}, interest_rates::{ @@ -76,7 +77,6 @@ pub fn th_setup(contract_balances: &[Coin]) -> OwnedDeps OwnedDeps Market { new_market } +pub fn th_default_asset_params() -> AssetParams { + AssetParams { + rover: RoverSettings { + whitelisted: false, + hls: HighLeverageStrategyParams { + max_loan_to_value: Decimal::percent(90), + liquidation_threshold: Decimal::one(), + }, + }, + red_bank: RedBankSettings { + deposit_enabled: true, + borrow_enabled: true, + deposit_cap: Uint128::MAX, + }, + max_loan_to_value: Decimal::zero(), + liquidation_threshold: Decimal::one(), + liquidation_bonus: Decimal::zero(), + } +} + #[derive(Default, Debug)] pub struct TestInterestResults { pub borrow_index: Decimal, diff --git a/contracts/red-bank/tests/test_admin.rs b/contracts/red-bank/tests/test_admin.rs index 0aa0dc9bb..53428ba80 100644 --- a/contracts/red-bank/tests/test_admin.rs +++ b/contracts/red-bank/tests/test_admin.rs @@ -1,5 +1,5 @@ use cosmwasm_std::{attr, coin, from_binary, testing::mock_info, Addr, Decimal, Event, Uint128}; -use mars_owner::{OwnerError::NotOwner, OwnerUpdate}; +use mars_owner::OwnerError::NotOwner; use mars_red_bank::{ contract::{execute, instantiate, query}, error::ContractError, @@ -29,7 +29,6 @@ fn proper_initialization() { // Config with base params valid (just update the rest) let base_config = CreateOrUpdateConfig { address_provider: Some("address_provider".to_string()), - close_factor: None, }; // * @@ -37,7 +36,6 @@ fn proper_initialization() { // * let empty_config = CreateOrUpdateConfig { address_provider: None, - close_factor: None, }; let msg = InstantiateMsg { owner: "owner".to_string(), @@ -47,36 +45,10 @@ fn proper_initialization() { let error_res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap_err(); assert_eq!(error_res, MarsError::InstantiateParamsUnavailable {}.into()); - // * - // init config with close_factor greater than 1 - // * - let mut close_factor = Decimal::from_ratio(13u128, 10u128); - let config = CreateOrUpdateConfig { - close_factor: Some(close_factor), - ..base_config.clone() - }; - let msg = InstantiateMsg { - owner: "owner".to_string(), - config, - }; - let info = mock_info("owner", &[]); - let error_res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!( - error_res, - ValidationError::InvalidParam { - param_name: "close_factor".to_string(), - invalid_value: "1.3".to_string(), - predicate: "<= 1".to_string(), - } - .into() - ); - // * // init config with valid params // * - close_factor = Decimal::from_ratio(1u128, 2u128); let config = CreateOrUpdateConfig { - close_factor: Some(close_factor), ..base_config }; let msg = InstantiateMsg { @@ -104,10 +76,8 @@ fn update_config() { // * // init config with valid params // * - let mut close_factor = Decimal::from_ratio(1u128, 4u128); let init_config = CreateOrUpdateConfig { address_provider: Some("address_provider".to_string()), - close_factor: Some(close_factor), }; let msg = InstantiateMsg { owner: "owner".to_string(), @@ -121,42 +91,17 @@ fn update_config() { // non owner is not authorized // * let msg = ExecuteMsg::UpdateConfig { - config: init_config.clone(), + config: init_config, }; let info = mock_info("somebody", &[]); let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); assert_eq!(error_res, ContractError::Owner(NotOwner {})); - // * - // update config with close_factor - // * - close_factor = Decimal::from_ratio(13u128, 10u128); - let config = CreateOrUpdateConfig { - close_factor: Some(close_factor), - ..init_config - }; - let msg = ExecuteMsg::UpdateConfig { - config, - }; - let info = mock_info("owner", &[]); - let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!( - error_res, - ValidationError::InvalidParam { - param_name: "close_factor".to_string(), - invalid_value: "1.3".to_string(), - predicate: "<= 1".to_string(), - } - .into() - ); - // * // update config with all new params // * - close_factor = Decimal::from_ratio(1u128, 20u128); let config = CreateOrUpdateConfig { address_provider: Some("new_address_provider".to_string()), - close_factor: Some(close_factor), }; let msg = ExecuteMsg::UpdateConfig { config: config.clone(), @@ -173,7 +118,6 @@ fn update_config() { assert_eq!(new_config.owner.unwrap(), "owner".to_string()); assert_eq!(new_config.address_provider, Addr::unchecked(config.address_provider.unwrap())); - assert_eq!(new_config.close_factor, config.close_factor.unwrap()); } #[test] @@ -183,7 +127,6 @@ fn init_asset() { let config = CreateOrUpdateConfig { address_provider: Some("address_provider".to_string()), - close_factor: Some(Decimal::from_ratio(1u128, 2u128)), }; let msg = InstantiateMsg { owner: "owner".to_string(), @@ -200,14 +143,8 @@ fn init_asset() { }; let params = InitOrUpdateAssetParams { - max_loan_to_value: Some(Decimal::from_ratio(8u128, 10u128)), reserve_factor: Some(Decimal::from_ratio(1u128, 100u128)), - liquidation_threshold: Some(Decimal::one()), - liquidation_bonus: Some(Decimal::zero()), interest_rate_model: Some(ir_model.clone()), - deposit_enabled: Some(true), - borrow_enabled: Some(true), - deposit_cap: None, }; // non owner is not authorized @@ -273,10 +210,8 @@ fn init_asset() { // init asset with empty params { let empty_asset_params = InitOrUpdateAssetParams { - max_loan_to_value: None, - liquidation_threshold: None, - liquidation_bonus: None, - ..params.clone() + reserve_factor: None, + interest_rate_model: None, }; let msg = ExecuteMsg::InitAsset { denom: "someasset".to_string(), @@ -310,99 +245,6 @@ fn init_asset() { ); } - // init asset with max_loan_to_value greater than 1 - { - let invalid_asset_params = InitOrUpdateAssetParams { - max_loan_to_value: Some(Decimal::from_ratio(11u128, 10u128)), - ..params.clone() - }; - let msg = ExecuteMsg::InitAsset { - denom: "someasset".to_string(), - params: invalid_asset_params, - }; - let info = mock_info("owner", &[]); - let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!( - error_res, - ValidationError::InvalidParam { - param_name: "max_loan_to_value".to_string(), - invalid_value: "1.1".to_string(), - predicate: "<= 1".to_string(), - } - .into() - ); - } - - // init asset with liquidation_threshold greater than 1 - { - let invalid_asset_params = InitOrUpdateAssetParams { - liquidation_threshold: Some(Decimal::from_ratio(11u128, 10u128)), - ..params.clone() - }; - let msg = ExecuteMsg::InitAsset { - denom: "someasset".to_string(), - params: invalid_asset_params, - }; - let info = mock_info("owner", &[]); - let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!( - error_res, - ValidationError::InvalidParam { - param_name: "liquidation_threshold".to_string(), - invalid_value: "1.1".to_string(), - predicate: "<= 1".to_string(), - } - .into() - ); - } - - // init asset with liquidation_bonus greater than 1 - { - let invalid_asset_params = InitOrUpdateAssetParams { - liquidation_bonus: Some(Decimal::from_ratio(11u128, 10u128)), - ..params.clone() - }; - let msg = ExecuteMsg::InitAsset { - denom: "someasset".to_string(), - params: invalid_asset_params, - }; - let info = mock_info("owner", &[]); - let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!( - error_res, - ValidationError::InvalidParam { - param_name: "liquidation_bonus".to_string(), - invalid_value: "1.1".to_string(), - predicate: "<= 1".to_string(), - } - .into() - ); - } - - // init asset where LTV >= liquidity threshold - { - let invalid_asset_params = InitOrUpdateAssetParams { - max_loan_to_value: Some(Decimal::from_ratio(5u128, 10u128)), - liquidation_threshold: Some(Decimal::from_ratio(5u128, 10u128)), - ..params.clone() - }; - let msg = ExecuteMsg::InitAsset { - denom: "someasset".to_string(), - params: invalid_asset_params, - }; - let info = mock_info("owner", &[]); - let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!( - error_res, - ValidationError::InvalidParam { - param_name: "liquidation_threshold".to_string(), - invalid_value: "0.5".to_string(), - predicate: "> 0.5 (max LTV)".to_string() - } - .into() - ); - } - // init asset where optimal utilization rate > 1 { let invalid_asset_params = InitOrUpdateAssetParams { @@ -443,7 +285,7 @@ fn init_asset() { assert_eq!(market.denom, "someasset"); // should have unlimited deposit cap - assert_eq!(market.deposit_cap, Uint128::MAX); + assert_eq!(market.reserve_factor, Decimal::from_ratio(1u128, 100u128)); assert_eq!(res.attributes, vec![attr("action", "init_asset"), attr("denom", "someasset")]); } @@ -468,7 +310,6 @@ fn update_asset() { let config = CreateOrUpdateConfig { address_provider: Some("address_provider".to_string()), - close_factor: Some(Decimal::from_ratio(1u128, 2u128)), }; let msg = InstantiateMsg { owner: "owner".to_string(), @@ -477,6 +318,8 @@ fn update_asset() { let info = mock_info("owner", &[]); instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + deps.querier.set_close_factor(Decimal::from_ratio(1u128, 2u128)); + let ir_model = InterestRateModel { optimal_utilization_rate: Decimal::one(), base: Decimal::percent(5), @@ -485,14 +328,8 @@ fn update_asset() { }; let params = InitOrUpdateAssetParams { - max_loan_to_value: Some(Decimal::from_ratio(50u128, 100u128)), reserve_factor: Some(Decimal::from_ratio(1u128, 100u128)), - liquidation_threshold: Some(Decimal::from_ratio(80u128, 100u128)), - liquidation_bonus: Some(Decimal::from_ratio(10u128, 100u128)), interest_rate_model: Some(ir_model.clone()), - deposit_enabled: Some(true), - borrow_enabled: Some(true), - deposit_cap: None, }; // non owner is not authorized @@ -527,99 +364,6 @@ fn update_asset() { let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); } - // update asset with max_loan_to_value greater than 1 - { - let invalid_asset_params = InitOrUpdateAssetParams { - max_loan_to_value: Some(Decimal::from_ratio(11u128, 10u128)), - ..params.clone() - }; - let msg = ExecuteMsg::UpdateAsset { - denom: "someasset".to_string(), - params: invalid_asset_params, - }; - let info = mock_info("owner", &[]); - let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!( - error_res, - ValidationError::InvalidParam { - param_name: "max_loan_to_value".to_string(), - invalid_value: "1.1".to_string(), - predicate: "<= 1".to_string(), - } - .into() - ); - } - - // update asset with liquidation_threshold greater than 1 - { - let invalid_asset_params = InitOrUpdateAssetParams { - liquidation_threshold: Some(Decimal::from_ratio(11u128, 10u128)), - ..params.clone() - }; - let msg = ExecuteMsg::UpdateAsset { - denom: "someasset".to_string(), - params: invalid_asset_params, - }; - let info = mock_info("owner", &[]); - let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!( - error_res, - ValidationError::InvalidParam { - param_name: "liquidation_threshold".to_string(), - invalid_value: "1.1".to_string(), - predicate: "<= 1".to_string(), - } - .into() - ); - } - - // update asset with liquidation_bonus greater than 1 - { - let invalid_asset_params = InitOrUpdateAssetParams { - liquidation_bonus: Some(Decimal::from_ratio(11u128, 10u128)), - ..params.clone() - }; - let msg = ExecuteMsg::UpdateAsset { - denom: "someasset".to_string(), - params: invalid_asset_params, - }; - let info = mock_info("owner", &[]); - let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!( - error_res, - ValidationError::InvalidParam { - param_name: "liquidation_bonus".to_string(), - invalid_value: "1.1".to_string(), - predicate: "<= 1".to_string(), - } - .into() - ); - } - - // update asset where LTV >= liquidity threshold - { - let invalid_asset_params = InitOrUpdateAssetParams { - max_loan_to_value: Some(Decimal::from_ratio(6u128, 10u128)), - liquidation_threshold: Some(Decimal::from_ratio(5u128, 10u128)), - ..params - }; - let msg = ExecuteMsg::UpdateAsset { - denom: "someasset".to_string(), - params: invalid_asset_params, - }; - let info = mock_info("owner", &[]); - let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!( - error_res, - ValidationError::InvalidParam { - param_name: "liquidation_threshold".to_string(), - invalid_value: "0.5".to_string(), - predicate: "> 0.6 (max LTV)".to_string() - } - .into() - ); - } - // update asset where optimal utilization rate > 1 { let invalid_asset_params = InitOrUpdateAssetParams { @@ -649,14 +393,8 @@ fn update_asset() { // update asset with new params { let params = InitOrUpdateAssetParams { - max_loan_to_value: Some(Decimal::from_ratio(60u128, 100u128)), reserve_factor: Some(Decimal::from_ratio(10u128, 100u128)), - liquidation_threshold: Some(Decimal::from_ratio(90u128, 100u128)), - liquidation_bonus: Some(Decimal::from_ratio(12u128, 100u128)), interest_rate_model: Some(ir_model), - deposit_enabled: Some(true), - borrow_enabled: Some(true), - deposit_cap: Some(Uint128::new(10_000_000)), }; let msg = ExecuteMsg::UpdateAsset { denom: "someasset".to_string(), @@ -672,10 +410,7 @@ fn update_asset() { ); let new_market = MARKETS.load(&deps.storage, "someasset").unwrap(); - assert_eq!(params.max_loan_to_value.unwrap(), new_market.max_loan_to_value); assert_eq!(params.reserve_factor.unwrap(), new_market.reserve_factor); - assert_eq!(params.liquidation_threshold.unwrap(), new_market.liquidation_threshold); - assert_eq!(params.liquidation_bonus.unwrap(), new_market.liquidation_bonus); assert_eq!(params.interest_rate_model.unwrap(), new_market.interest_rate_model); } @@ -684,14 +419,8 @@ fn update_asset() { let market_before = MARKETS.load(&deps.storage, "someasset").unwrap(); let empty_asset_params = InitOrUpdateAssetParams { - max_loan_to_value: None, reserve_factor: None, - liquidation_threshold: None, - liquidation_bonus: None, interest_rate_model: None, - deposit_enabled: None, - borrow_enabled: None, - deposit_cap: None, }; let msg = ExecuteMsg::UpdateAsset { denom: "someasset".to_string(), @@ -706,11 +435,7 @@ fn update_asset() { let new_market = MARKETS.load(&deps.storage, "someasset").unwrap(); // should keep old params assert_eq!(market_before.borrow_rate, new_market.borrow_rate); - assert_eq!(market_before.max_loan_to_value, new_market.max_loan_to_value); assert_eq!(market_before.reserve_factor, new_market.reserve_factor); - assert_eq!(market_before.liquidation_threshold, new_market.liquidation_threshold); - assert_eq!(market_before.liquidation_bonus, new_market.liquidation_bonus); - assert_eq!(market_before.deposit_cap, new_market.deposit_cap); assert_eq!(market_before.interest_rate_model, new_market.interest_rate_model); } } @@ -721,7 +446,6 @@ fn update_asset_with_new_interest_rate_model_params() { let config = CreateOrUpdateConfig { address_provider: Some("address_provider".to_string()), - close_factor: Some(Decimal::from_ratio(1u128, 2u128)), }; let msg = InstantiateMsg { owner: "owner".to_string(), @@ -731,6 +455,8 @@ fn update_asset_with_new_interest_rate_model_params() { let env = mock_env(MockEnvParams::default()); instantiate(deps.as_mut(), env, info, msg).unwrap(); + deps.querier.set_close_factor(Decimal::from_ratio(1u128, 2u128)); + let ir_model = InterestRateModel { optimal_utilization_rate: Decimal::one(), base: Decimal::percent(5), @@ -739,14 +465,8 @@ fn update_asset_with_new_interest_rate_model_params() { }; let params = InitOrUpdateAssetParams { - max_loan_to_value: Some(Decimal::from_ratio(50u128, 100u128)), reserve_factor: Some(Decimal::from_ratio(2u128, 100u128)), - liquidation_threshold: Some(Decimal::from_ratio(80u128, 100u128)), - liquidation_bonus: Some(Decimal::from_ratio(10u128, 100u128)), interest_rate_model: Some(ir_model.clone()), - deposit_enabled: Some(true), - borrow_enabled: Some(true), - deposit_cap: None, }; let msg = ExecuteMsg::InitAsset { @@ -857,14 +577,8 @@ fn update_asset_new_reserve_factor_accrues_interest_rate() { ); let params = InitOrUpdateAssetParams { - max_loan_to_value: None, reserve_factor: Some(Decimal::from_ratio(2_u128, 10_u128)), - liquidation_threshold: None, - liquidation_bonus: None, interest_rate_model: None, - deposit_enabled: None, - borrow_enabled: None, - deposit_cap: None, }; let msg = ExecuteMsg::UpdateAsset { denom: "somecoin".to_string(), @@ -944,122 +658,3 @@ fn update_asset_new_reserve_factor_accrues_interest_rate() { .unwrap(); assert_eq!(collateral.amount_scaled, expected_rewards_scaled); } - -#[test] -fn update_asset_by_emergency_owner() { - let mut deps = mock_dependencies(&[]); - let start_time = 100000000; - let env = mock_env_at_block_time(start_time); - - let config = CreateOrUpdateConfig { - address_provider: Some("address_provider".to_string()), - close_factor: Some(Decimal::from_ratio(1u128, 2u128)), - }; - let msg = InstantiateMsg { - owner: "owner".to_string(), - config, - }; - let info = mock_info("owner", &[]); - instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); - - let ir_model = InterestRateModel { - optimal_utilization_rate: Decimal::one(), - base: Decimal::percent(5), - slope_1: Decimal::zero(), - slope_2: Decimal::zero(), - }; - - let params = InitOrUpdateAssetParams { - max_loan_to_value: Some(Decimal::from_ratio(50u128, 100u128)), - reserve_factor: Some(Decimal::from_ratio(1u128, 100u128)), - liquidation_threshold: Some(Decimal::from_ratio(80u128, 100u128)), - liquidation_bonus: Some(Decimal::from_ratio(10u128, 100u128)), - interest_rate_model: Some(ir_model.clone()), - deposit_enabled: Some(true), - borrow_enabled: Some(true), - deposit_cap: None, - }; - - execute( - deps.as_mut(), - env.clone(), - mock_info("owner", &[]), - ExecuteMsg::UpdateOwner(OwnerUpdate::SetEmergencyOwner { - emergency_owner: "emergency_owner".to_string(), - }), - ) - .unwrap(); - - // emergency owner is authorized but can't update asset if not initialized first - { - let msg = ExecuteMsg::UpdateAsset { - denom: "someasset".to_string(), - params: params.clone(), - }; - let info = mock_info("emergency_owner", &[]); - let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!(error_res, ContractError::AssetNotInitialized {}); - } - - // initialize asset - { - let msg = ExecuteMsg::InitAsset { - denom: "someasset".to_string(), - params: params.clone(), - }; - let info = mock_info("owner", &[]); - let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); - } - - // update asset with borrow_enabled = true, should have not effect on the saved market - { - let old_market = MARKETS.load(&deps.storage, "someasset").unwrap(); - - let new_asset_params = InitOrUpdateAssetParams { - borrow_enabled: Some(true), - ..params - }; - let msg = ExecuteMsg::UpdateAsset { - denom: "someasset".to_string(), - params: new_asset_params, - }; - let info = mock_info("emergency_owner", &[]); - let res_err = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!(res_err, ContractError::Mars(MarsError::Unauthorized {})); - - let new_market = MARKETS.load(&deps.storage, "someasset").unwrap(); - assert_eq!(old_market, new_market) - } - - // update asset with new params, only borrow_enabled = false should have effect on the saved market - { - let mut old_market = MARKETS.load(&deps.storage, "someasset").unwrap(); - - let params = InitOrUpdateAssetParams { - max_loan_to_value: Some(Decimal::from_ratio(60u128, 100u128)), - reserve_factor: Some(Decimal::from_ratio(10u128, 100u128)), - liquidation_threshold: Some(Decimal::from_ratio(90u128, 100u128)), - liquidation_bonus: Some(Decimal::from_ratio(12u128, 100u128)), - interest_rate_model: Some(ir_model), - deposit_enabled: Some(false), - borrow_enabled: Some(false), - deposit_cap: Some(Uint128::new(10_000_000)), - }; - let msg = ExecuteMsg::UpdateAsset { - denom: "someasset".to_string(), - params, - }; - let info = mock_info("emergency_owner", &[]); - let res = execute(deps.as_mut(), env, info, msg).unwrap(); - assert!(res.messages.is_empty()); - assert_eq!( - res.attributes, - vec![attr("action", "emergency_update_asset"), attr("denom", "someasset")], - ); - - let new_market = MARKETS.load(&deps.storage, "someasset").unwrap(); - // old market should have only borrow_enabled updated - old_market.borrow_enabled = false; - assert_eq!(old_market, new_market); - } -} diff --git a/contracts/red-bank/tests/test_borrow.rs b/contracts/red-bank/tests/test_borrow.rs index b6b672e38..71fc93ba4 100644 --- a/contracts/red-bank/tests/test_borrow.rs +++ b/contracts/red-bank/tests/test_borrow.rs @@ -6,6 +6,7 @@ use helpers::{ has_collateral_position, has_debt_position, set_collateral, th_build_interests_updated_event, th_get_expected_indices_and_rates, th_init_market, th_setup, TestUtilizationDeltaInfo, }; +use mars_params::types::{AssetParams, HighLeverageStrategyParams, RedBankSettings, RoverSettings}; use mars_red_bank::{ contract::execute, error::ContractError, @@ -19,6 +20,8 @@ use mars_red_bank_types::red_bank::{ExecuteMsg, Market}; use mars_testing::{mock_env, mock_env_at_block_time, MockEnvParams}; use mars_utils::math; +use crate::helpers::th_default_asset_params; + mod helpers; #[test] @@ -60,7 +63,6 @@ fn borrow_and_repay() { let mock_market_3 = Market { borrow_index: Decimal::one(), liquidity_index: Decimal::from_ratio(11u128, 10u128), - max_loan_to_value: Decimal::from_ratio(7u128, 10u128), borrow_rate: Decimal::from_ratio(30u128, 100u128), reserve_factor: Decimal::from_ratio(3u128, 100u128), liquidity_rate: Decimal::from_ratio(20u128, 100u128), @@ -73,6 +75,16 @@ fn borrow_and_repay() { let market_2_initial = th_init_market(deps.as_mut(), "uusd", &mock_market_2); th_init_market(deps.as_mut(), "uatom", &mock_market_3); + deps.querier.set_redbank_params("uosmo", th_default_asset_params()); + deps.querier.set_redbank_params("uusd", th_default_asset_params()); + deps.querier.set_redbank_params( + "uatom", + AssetParams { + max_loan_to_value: Decimal::from_ratio(7u128, 10u128), + ..th_default_asset_params() + }, + ); + let borrower_addr = Addr::unchecked("borrower"); // Set user as having the market_collateral deposited @@ -482,7 +494,6 @@ fn repay_without_refund_on_behalf_of() { let mock_market = Market { liquidity_index: Decimal::one(), borrow_index: Decimal::one(), - max_loan_to_value: Decimal::from_ratio(50u128, 100u128), collateral_total_scaled: Uint128::new(1_000_000_000_000u128), ..Default::default() }; @@ -490,6 +501,21 @@ fn repay_without_refund_on_behalf_of() { let market_1_initial = th_init_market(deps.as_mut(), "depositedcoinnative", &mock_market); // collateral let market_2_initial = th_init_market(deps.as_mut(), "borrowedcoinnative", &mock_market); + deps.querier.set_redbank_params( + "depositedcoinnative", + AssetParams { + max_loan_to_value: Decimal::from_ratio(50u128, 100u128), + ..th_default_asset_params() + }, + ); + deps.querier.set_redbank_params( + "borrowedcoinnative", + AssetParams { + max_loan_to_value: Decimal::from_ratio(50u128, 100u128), + ..th_default_asset_params() + }, + ); + let borrower_addr = Addr::unchecked("borrower"); let user_addr = Addr::unchecked("user"); @@ -563,7 +589,6 @@ fn repay_with_refund_on_behalf_of() { let mock_market = Market { liquidity_index: Decimal::one(), borrow_index: Decimal::one(), - max_loan_to_value: Decimal::from_ratio(50u128, 100u128), collateral_total_scaled: Uint128::new(1_000_000_000_000u128), ..Default::default() }; @@ -571,6 +596,21 @@ fn repay_with_refund_on_behalf_of() { let market_1_initial = th_init_market(deps.as_mut(), "depositedcoinnative", &mock_market); // collateral let market_2_initial = th_init_market(deps.as_mut(), "borrowedcoinnative", &mock_market); + deps.querier.set_redbank_params( + "depositedcoinnative", + AssetParams { + max_loan_to_value: Decimal::from_ratio(50u128, 100u128), + ..th_default_asset_params() + }, + ); + deps.querier.set_redbank_params( + "borrowedcoinnative", + AssetParams { + max_loan_to_value: Decimal::from_ratio(50u128, 100u128), + ..th_default_asset_params() + }, + ); + let borrower_addr = Addr::unchecked("borrower"); let user_addr = Addr::unchecked("user"); @@ -672,7 +712,6 @@ fn borrow_uusd() { let mock_market = Market { liquidity_index: Decimal::one(), - max_loan_to_value: ltv, borrow_index: Decimal::from_ratio(20u128, 10u128), borrow_rate: Decimal::one(), liquidity_rate: Decimal::one(), @@ -683,6 +722,14 @@ fn borrow_uusd() { }; let market = th_init_market(deps.as_mut(), "uusd", &mock_market); + deps.querier.set_redbank_params( + "uusd", + AssetParams { + max_loan_to_value: ltv, + ..th_default_asset_params() + }, + ); + // Set user as having the market_collateral deposited let deposit_amount_scaled = Uint128::new(110_000) * SCALING_FACTOR; set_collateral(deps.as_mut(), &borrower_addr, "uusd", deposit_amount_scaled, true); @@ -751,7 +798,6 @@ fn borrow_full_liquidity_and_then_repay() { let mock_market = Market { liquidity_index: Decimal::one(), - max_loan_to_value: ltv, borrow_index: Decimal::one(), borrow_rate: Decimal::one(), liquidity_rate: Decimal::one(), @@ -763,6 +809,14 @@ fn borrow_full_liquidity_and_then_repay() { }; th_init_market(deps.as_mut(), "uusd", &mock_market); + deps.querier.set_redbank_params( + "uusd", + AssetParams { + max_loan_to_value: ltv, + ..th_default_asset_params() + }, + ); + // User should have amount of collateral more than initial liquidity in order to borrow full liquidity let deposit_amount = initial_liquidity + 1000u128; set_collateral( @@ -831,7 +885,6 @@ fn borrow_collateral_check() { // NOTE: base asset price (asset3) should be set to 1 by the oracle helper let mock_market_1 = Market { - max_loan_to_value: Decimal::from_ratio(8u128, 10u128), collateral_total_scaled: Uint128::new(10_000_000_000_000u128), debt_total_scaled: Uint128::zero(), liquidity_index: Decimal::one(), @@ -839,7 +892,6 @@ fn borrow_collateral_check() { ..Default::default() }; let mock_market_2 = Market { - max_loan_to_value: Decimal::from_ratio(6u128, 10u128), collateral_total_scaled: Uint128::new(10_000_000_000_000u128), debt_total_scaled: Uint128::zero(), liquidity_index: Decimal::one(), @@ -847,7 +899,6 @@ fn borrow_collateral_check() { ..Default::default() }; let mock_market_3 = Market { - max_loan_to_value: Decimal::from_ratio(4u128, 10u128), collateral_total_scaled: Uint128::new(10_000_000_000_000u128), debt_total_scaled: Uint128::zero(), liquidity_index: Decimal::one(), @@ -862,6 +913,22 @@ fn borrow_collateral_check() { // should get index 2 let market_3_initial = th_init_market(deps.as_mut(), "uusd", &mock_market_3); + let asset_params_1 = AssetParams { + max_loan_to_value: Decimal::from_ratio(8u128, 10u128), + ..th_default_asset_params() + }; + deps.querier.set_redbank_params("uatom", asset_params_1.clone()); + let asset_params_2 = AssetParams { + max_loan_to_value: Decimal::from_ratio(6u128, 10u128), + ..th_default_asset_params() + }; + deps.querier.set_redbank_params("uosmo", asset_params_2.clone()); + let asset_params_3 = AssetParams { + max_loan_to_value: Decimal::from_ratio(4u128, 10u128), + ..th_default_asset_params() + }; + deps.querier.set_redbank_params("uusd", asset_params_3.clone()); + let borrower_addr = Addr::unchecked("borrower"); let balance_1 = Uint128::new(4_000_000) * SCALING_FACTOR; @@ -873,7 +940,7 @@ fn borrow_collateral_check() { set_collateral(deps.as_mut(), &borrower_addr, &market_2_initial.denom, balance_2, true); set_collateral(deps.as_mut(), &borrower_addr, &market_3_initial.denom, balance_3, true); - let max_borrow_allowed_in_base_asset = (market_1_initial.max_loan_to_value + let max_borrow_allowed_in_base_asset = (asset_params_1.max_loan_to_value * compute_underlying_amount( balance_1, market_1_initial.liquidity_index, @@ -881,7 +948,7 @@ fn borrow_collateral_check() { ) .unwrap() * exchange_rate_1) - + (market_2_initial.max_loan_to_value + + (asset_params_2.max_loan_to_value * compute_underlying_amount( balance_2, market_2_initial.liquidity_index, @@ -889,7 +956,7 @@ fn borrow_collateral_check() { ) .unwrap() * exchange_rate_2) - + (market_3_initial.max_loan_to_value + + (asset_params_3.max_loan_to_value * compute_underlying_amount( balance_3, market_3_initial.liquidity_index, @@ -928,11 +995,27 @@ fn borrow_collateral_check() { fn cannot_borrow_if_market_not_enabled() { let mut deps = th_setup(&[]); - let mock_market = Market { - borrow_enabled: false, - ..Default::default() - }; - th_init_market(deps.as_mut(), "somecoin", &mock_market); + th_init_market(deps.as_mut(), "somecoin", &Market::default()); + + deps.querier.set_redbank_params( + "somecoin", + AssetParams { + rover: RoverSettings { + whitelisted: false, + + hls: HighLeverageStrategyParams { + max_loan_to_value: Decimal::percent(90), + liquidation_threshold: Decimal::one(), + }, + }, + red_bank: RedBankSettings { + deposit_enabled: false, + borrow_enabled: false, + deposit_cap: Default::default(), + }, + ..th_default_asset_params() + }, + ); // Check error when borrowing not allowed on market let env = mock_env(MockEnvParams::default()); @@ -962,13 +1045,20 @@ fn borrow_and_send_funds_to_another_user() { let mock_market = Market { liquidity_index: Decimal::one(), borrow_index: Decimal::one(), - max_loan_to_value: Decimal::from_ratio(5u128, 10u128), collateral_total_scaled: Uint128::new(1_000_000_000_000u128), debt_total_scaled: Uint128::zero(), ..Default::default() }; let market = th_init_market(deps.as_mut(), "uusd", &mock_market); + deps.querier.set_redbank_params( + "uusd", + AssetParams { + max_loan_to_value: Decimal::from_ratio(5u128, 10u128), + ..th_default_asset_params() + }, + ); + // Set user as having the market_collateral deposited let deposit_amount_scaled = Uint128::new(100_000) * SCALING_FACTOR; set_collateral(deps.as_mut(), &borrower_addr, &market.denom, deposit_amount_scaled, true); diff --git a/contracts/red-bank/tests/test_deposit.rs b/contracts/red-bank/tests/test_deposit.rs index c64a7ecf4..9a2b51619 100644 --- a/contracts/red-bank/tests/test_deposit.rs +++ b/contracts/red-bank/tests/test_deposit.rs @@ -9,6 +9,7 @@ use cw_utils::PaymentError; use helpers::{ set_collateral, th_build_interests_updated_event, th_get_expected_indices_and_rates, th_setup, }; +use mars_params::types::{AssetParams, HighLeverageStrategyParams, RedBankSettings, RoverSettings}; use mars_red_bank::{ contract::execute, error::ContractError, @@ -22,6 +23,8 @@ use mars_red_bank_types::{ }; use mars_testing::{mock_env_at_block_time, MarsMockQuerier}; +use crate::helpers::th_default_asset_params; + mod helpers; struct TestSuite { @@ -40,7 +43,6 @@ fn setup_test() -> TestSuite { let market = Market { denom: denom.to_string(), liquidity_index: Decimal::from_ratio(11u128, 10u128), - max_loan_to_value: Decimal::one(), borrow_index: Decimal::from_ratio(1u128, 1u128), borrow_rate: Decimal::from_ratio(10u128, 100u128), liquidity_rate: Decimal::from_ratio(10u128, 100u128), @@ -48,12 +50,32 @@ fn setup_test() -> TestSuite { collateral_total_scaled: Uint128::new(10_000_000) * SCALING_FACTOR, debt_total_scaled: Uint128::new(10_000_000) * SCALING_FACTOR, indexes_last_updated: 10000000, - deposit_cap: Uint128::new(12_000_000), ..Default::default() }; MARKETS.save(deps.as_mut().storage, denom, &market).unwrap(); + deps.querier.set_redbank_params( + denom, + AssetParams { + max_loan_to_value: Decimal::one(), + liquidation_threshold: Default::default(), + liquidation_bonus: Default::default(), + rover: RoverSettings { + whitelisted: false, + hls: HighLeverageStrategyParams { + max_loan_to_value: Decimal::percent(90), + liquidation_threshold: Decimal::one(), + }, + }, + red_bank: RedBankSettings { + deposit_enabled: true, + borrow_enabled: true, + deposit_cap: Uint128::new(12_000_000), + }, + }, + ); + TestSuite { deps, denom, @@ -137,13 +159,24 @@ fn depositing_to_disabled_market() { } = setup_test(); // disable the market - MARKETS - .update(deps.as_mut().storage, denom, |opt| -> StdResult<_> { - let mut market = opt.unwrap(); - market.deposit_enabled = false; - Ok(market) - }) - .unwrap(); + deps.querier.set_redbank_params( + denom, + AssetParams { + rover: RoverSettings { + whitelisted: false, + hls: HighLeverageStrategyParams { + max_loan_to_value: Decimal::percent(90), + liquidation_threshold: Decimal::one(), + }, + }, + red_bank: RedBankSettings { + deposit_enabled: false, + borrow_enabled: true, + deposit_cap: Default::default(), + }, + ..th_default_asset_params() + }, + ); let err = execute( deps.as_mut(), @@ -176,10 +209,27 @@ fn depositing_above_cap() { .update(deps.as_mut().storage, denom, |opt| -> StdResult<_> { let mut market = opt.unwrap(); market.collateral_total_scaled = Uint128::new(9_000_000) * SCALING_FACTOR; - market.deposit_cap = Uint128::new(10_000_000); Ok(market) }) .unwrap(); + deps.querier.set_redbank_params( + denom, + AssetParams { + rover: RoverSettings { + whitelisted: false, + hls: HighLeverageStrategyParams { + max_loan_to_value: Decimal::percent(90), + liquidation_threshold: Decimal::one(), + }, + }, + red_bank: RedBankSettings { + deposit_enabled: true, + borrow_enabled: true, + deposit_cap: Uint128::new(10_000_000), + }, + ..th_default_asset_params() + }, + ); // try deposit with a big amount, should fail let err = execute( diff --git a/contracts/red-bank/tests/test_liquidate.rs b/contracts/red-bank/tests/test_liquidate.rs index 8d1872841..747488453 100644 --- a/contracts/red-bank/tests/test_liquidate.rs +++ b/contracts/red-bank/tests/test_liquidate.rs @@ -1,10 +1,12 @@ +#![allow(dead_code)] + use std::cmp::min; use cosmwasm_std::{ attr, coin, coins, testing::{mock_info, MockApi, MockStorage}, - to_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal, Deps, OwnedDeps, StdError, StdResult, - SubMsg, Uint128, WasmMsg, + to_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal, Deps, OwnedDeps, StdError, SubMsg, Uint128, + WasmMsg, }; use cw_utils::PaymentError; use helpers::{ @@ -12,6 +14,7 @@ use helpers::{ th_get_expected_indices, th_get_expected_indices_and_rates, th_init_market, th_setup, TestUtilizationDeltaInfo, }; +use mars_params::types::AssetParams; use mars_red_bank::{ contract::execute, error::ContractError, @@ -20,7 +23,7 @@ use mars_red_bank::{ compute_scaled_amount, compute_underlying_amount, get_scaled_liquidity_amount, ScalingOperation, SCALING_FACTOR, }, - state::{COLLATERALS, CONFIG, DEBTS, MARKETS}, + state::{COLLATERALS, DEBTS, MARKETS}, }; use mars_red_bank_types::{ address_provider::MarsAddressType, @@ -30,7 +33,7 @@ use mars_red_bank_types::{ use mars_testing::{mock_env, mock_env_at_block_time, MarsMockQuerier, MockEnvParams}; use mars_utils::math; -use crate::helpers::{set_debt, TestInterestResults}; +use crate::helpers::{set_debt, th_default_asset_params, TestInterestResults}; mod helpers; @@ -44,6 +47,8 @@ struct TestSuite { close_factor: Decimal, collateral_market: Market, debt_market: Market, + collateral_asset_params: AssetParams, + debt_asset_params: AssetParams, } fn setup_test() -> TestSuite { @@ -52,12 +57,7 @@ fn setup_test() -> TestSuite { let mut deps = th_setup(&[initial_collateral_coin.clone(), initial_debt_coin.clone()]); let close_factor = Decimal::from_ratio(1u128, 2u128); - CONFIG - .update(deps.as_mut().storage, |mut config| -> StdResult<_> { - config.close_factor = close_factor; - Ok(config) - }) - .unwrap(); + deps.querier.set_close_factor(close_factor); let collateral_price = Decimal::from_ratio(2_u128, 1_u128); let debt_price = Decimal::from_ratio(11_u128, 10_u128); @@ -76,9 +76,6 @@ fn setup_test() -> TestSuite { }; let collateral_market = Market { - max_loan_to_value: Decimal::from_ratio(5u128, 10u128), - liquidation_threshold: Decimal::from_ratio(6u128, 10u128), - liquidation_bonus: Decimal::from_ratio(1u128, 10u128), collateral_total_scaled: Uint128::new(1_500_000_000) * SCALING_FACTOR, debt_total_scaled: Uint128::new(800_000_000) * SCALING_FACTOR, liquidity_index: Decimal::one(), @@ -92,7 +89,6 @@ fn setup_test() -> TestSuite { }; let debt_market = Market { - max_loan_to_value: Decimal::from_ratio(6u128, 10u128), collateral_total_scaled: Uint128::new(3_500_000_000) * SCALING_FACTOR, debt_total_scaled: Uint128::new(1_800_000_000) * SCALING_FACTOR, liquidity_index: Decimal::from_ratio(12u128, 10u128), @@ -115,6 +111,22 @@ fn setup_test() -> TestSuite { let debt_market = th_init_market(deps.as_mut(), &initial_debt_coin.denom, &debt_market); th_init_market(deps.as_mut(), uncollateralized_denom, &uncollateralized_debt_market); + let asset_params_1_collateral = AssetParams { + max_loan_to_value: Decimal::from_ratio(5u128, 10u128), + liquidation_threshold: Decimal::from_ratio(6u128, 10u128), + liquidation_bonus: Decimal::from_ratio(1u128, 10u128), + ..th_default_asset_params() + }; + deps.querier + .set_redbank_params(&initial_collateral_coin.denom, asset_params_1_collateral.clone()); + let asset_params_2_debt = AssetParams { + max_loan_to_value: Decimal::from_ratio(6u128, 10u128), + ..th_default_asset_params() + }; + deps.querier.set_redbank_params(&initial_debt_coin.denom, asset_params_2_debt.clone()); + + deps.querier.set_redbank_params(uncollateralized_denom, th_default_asset_params()); + TestSuite { deps, collateral_coin: initial_collateral_coin, @@ -125,6 +137,8 @@ fn setup_test() -> TestSuite { close_factor, collateral_market, debt_market, + collateral_asset_params: asset_params_1_collateral, + debt_asset_params: asset_params_2_debt, } } @@ -183,7 +197,7 @@ fn expected_amounts( let expected_liquidated_collateral_amount = math::divide_uint128_by_decimal( amount_to_repay * test_suite.debt_price - * (Decimal::one() + test_suite.collateral_market.liquidation_bonus), + * (Decimal::one() + test_suite.collateral_asset_params.liquidation_bonus), test_suite.collateral_price, ) .unwrap(); @@ -623,6 +637,7 @@ fn liquidate_fully() { debt_price, collateral_market, debt_market, + collateral_asset_params, .. } = setup_test(); @@ -669,7 +684,7 @@ fn liquidate_fully() { let expected_less_debt = math::divide_uint128_by_decimal( math::divide_uint128_by_decimal(collateral_price * user_collateral_balance, debt_price) .unwrap(), - Decimal::one() + collateral_market.liquidation_bonus, + Decimal::one() + collateral_asset_params.liquidation_bonus, ) .unwrap(); @@ -754,6 +769,7 @@ fn liquidate_partially_if_same_asset_for_debt_and_collateral() { mut deps, collateral_price, collateral_market, + collateral_asset_params, .. } = setup_test(); let debt_price = collateral_price; @@ -808,7 +824,7 @@ fn liquidate_partially_if_same_asset_for_debt_and_collateral() { let debt_market_after = MARKETS.load(&deps.storage, &collateral_market.denom).unwrap(); let expected_liquidated_collateral_amount = math::divide_uint128_by_decimal( - debt_to_repay * debt_price * (Decimal::one() + collateral_market.liquidation_bonus), + debt_to_repay * debt_price * (Decimal::one() + collateral_asset_params.liquidation_bonus), collateral_price, ) .unwrap(); @@ -901,6 +917,7 @@ fn liquidate_with_refund_if_same_asset_for_debt_and_collateral() { collateral_price, close_factor, collateral_market, + collateral_asset_params, .. } = setup_test(); let debt_price = collateral_price; @@ -965,7 +982,9 @@ fn liquidate_with_refund_if_same_asset_for_debt_and_collateral() { ); let expected_liquidated_collateral_amount = math::divide_uint128_by_decimal( - expected_less_debt * debt_price * (Decimal::one() + collateral_market.liquidation_bonus), + expected_less_debt + * debt_price + * (Decimal::one() + collateral_asset_params.liquidation_bonus), collateral_price, ) .unwrap(); @@ -1211,16 +1230,12 @@ fn liquidation_health_factor_check() { let collateral_liquidation_bonus = Decimal::from_ratio(1u128, 10u128); let collateral_market = Market { - max_loan_to_value: collateral_ltv, - liquidation_threshold: collateral_liquidation_threshold, - liquidation_bonus: collateral_liquidation_bonus, debt_total_scaled: Uint128::zero(), liquidity_index: Decimal::one(), borrow_index: Decimal::one(), ..Default::default() }; let debt_market = Market { - max_loan_to_value: Decimal::from_ratio(6u128, 10u128), debt_total_scaled: Uint128::new(20_000_000) * SCALING_FACTOR, liquidity_index: Decimal::one(), borrow_index: Decimal::one(), @@ -1236,6 +1251,24 @@ fn liquidation_health_factor_check() { th_init_market(deps.as_mut(), "debt", &debt_market); th_init_market(deps.as_mut(), "uncollateralized_debt", &uncollateralized_debt_market); + deps.querier.set_redbank_params( + "collateral", + AssetParams { + max_loan_to_value: collateral_ltv, + liquidation_threshold: collateral_liquidation_threshold, + liquidation_bonus: collateral_liquidation_bonus, + ..th_default_asset_params() + }, + ); + deps.querier.set_redbank_params( + "debt", + AssetParams { + max_loan_to_value: Decimal::from_ratio(6u128, 10u128), + ..th_default_asset_params() + }, + ); + deps.querier.set_redbank_params("uncollateralized_debt", th_default_asset_params()); + // test health factor check let healthy_user_addr = Addr::unchecked("healthy_user"); @@ -1335,14 +1368,18 @@ fn liquidate_if_collateral_disabled() { fn liquidator_cannot_receive_collaterals_without_spending_coins() { let market = Market { liquidity_index: Decimal::one(), - liquidation_bonus: Decimal::from_ratio(1u128, 10u128), ..Default::default() }; + let asset_params = AssetParams { + liquidation_bonus: Decimal::from_ratio(1u128, 10u128), + ..th_default_asset_params() + }; let res_err = liquidation_compute_amounts( Uint128::new(320000000), Uint128::new(800), Uint128::new(2), &market, + &asset_params, Decimal::one(), Decimal::from_ratio(300u128, 1u128), 0, @@ -1356,14 +1393,18 @@ fn liquidator_cannot_receive_collaterals_without_spending_coins() { fn cannot_liquidate_without_receiving_collaterals() { let market = Market { liquidity_index: Decimal::one(), - liquidation_bonus: Decimal::from_ratio(1u128, 10u128), ..Default::default() }; + let asset_params = AssetParams { + liquidation_bonus: Decimal::from_ratio(1u128, 10u128), + ..th_default_asset_params() + }; let res_err = liquidation_compute_amounts( Uint128::new(320000000), Uint128::new(20), Uint128::new(30), &market, + &asset_params, Decimal::from_ratio(12u128, 1u128), Decimal::one(), 0, diff --git a/contracts/red-bank/tests/test_misc.rs b/contracts/red-bank/tests/test_misc.rs index 78ec0db60..0cddaf106 100644 --- a/contracts/red-bank/tests/test_misc.rs +++ b/contracts/red-bank/tests/test_misc.rs @@ -7,6 +7,7 @@ use helpers::{ TestUtilizationDeltaInfo, }; use mars_owner::OwnerError::NotOwner; +use mars_params::types::AssetParams; use mars_red_bank::{ contract::execute, error::ContractError, @@ -21,6 +22,8 @@ use mars_red_bank_types::red_bank::{Debt, ExecuteMsg, Market}; use mars_testing::{mock_env, mock_env_at_block_time, MockEnvParams}; use mars_utils::math; +use crate::helpers::th_default_asset_params; + mod helpers; #[test] @@ -42,6 +45,7 @@ fn uncollateralized_loan_limits() { // should get index 0 let market_initial = th_init_market(deps.as_mut(), "somecoin", &mock_market); + deps.querier.set_redbank_params("somecoin", th_default_asset_params()); let mut block_time = mock_market.indexes_last_updated + 10000u64; let initial_uncollateralized_loan_limit = Uint128::from(2400_u128); @@ -207,24 +211,18 @@ fn update_asset_collateral() { let mock_market_1 = Market { liquidity_index: Decimal::one(), borrow_index: Decimal::one(), - max_loan_to_value: Decimal::from_ratio(40u128, 100u128), - liquidation_threshold: Decimal::from_ratio(60u128, 100u128), ..Default::default() }; let denom_2 = "depositedcoin2"; let mock_market_2 = Market { liquidity_index: Decimal::from_ratio(1u128, 2u128), borrow_index: Decimal::one(), - max_loan_to_value: Decimal::from_ratio(50u128, 100u128), - liquidation_threshold: Decimal::from_ratio(80u128, 100u128), ..Default::default() }; let denom_3 = "depositedcoin3"; let mock_market_3 = Market { liquidity_index: Decimal::one(), borrow_index: Decimal::from_ratio(2u128, 1u128), - max_loan_to_value: Decimal::from_ratio(20u128, 100u128), - liquidation_threshold: Decimal::from_ratio(40u128, 100u128), ..Default::default() }; @@ -232,6 +230,25 @@ fn update_asset_collateral() { let market_2_initial = th_init_market(deps.as_mut(), denom_2, &mock_market_2); let market_3_initial = th_init_market(deps.as_mut(), denom_3, &mock_market_3); + let asset_params_1 = AssetParams { + max_loan_to_value: Decimal::from_ratio(40u128, 100u128), + liquidation_threshold: Decimal::from_ratio(60u128, 100u128), + ..th_default_asset_params() + }; + deps.querier.set_redbank_params(denom_1, asset_params_1.clone()); + let asset_params_2 = AssetParams { + max_loan_to_value: Decimal::from_ratio(50u128, 100u128), + liquidation_threshold: Decimal::from_ratio(80u128, 100u128), + ..th_default_asset_params() + }; + deps.querier.set_redbank_params(denom_2, asset_params_2.clone()); + let asset_params_3 = AssetParams { + max_loan_to_value: Decimal::from_ratio(20u128, 100u128), + liquidation_threshold: Decimal::from_ratio(40u128, 100u128), + ..th_default_asset_params() + }; + deps.querier.set_redbank_params(denom_3, asset_params_3); + // Set the querier to return exchange rates let token_1_exchange_rate = Decimal::from_ratio(2u128, 1u128); let token_2_exchange_rate = Decimal::from_ratio(3u128, 1u128); @@ -309,7 +326,7 @@ fn update_asset_collateral() { ScalingOperation::Truncate, ) .unwrap() - * market_1_initial.liquidation_threshold + * asset_params_1.liquidation_threshold * token_1_exchange_rate; let token_2_weighted_lt_in_base_asset = compute_underlying_amount( token_2_balance_scaled, @@ -317,7 +334,7 @@ fn update_asset_collateral() { ScalingOperation::Truncate, ) .unwrap() - * market_2_initial.liquidation_threshold + * asset_params_2.liquidation_threshold * token_2_exchange_rate; let weighted_liquidation_threshold_in_base_asset = token_1_weighted_lt_in_base_asset + token_2_weighted_lt_in_base_asset; @@ -345,6 +362,7 @@ fn update_asset_collateral() { &env, &user_addr, &Addr::unchecked("oracle"), + &Addr::unchecked("params"), ) .unwrap(); let health = health::compute_position_health(&positions).unwrap(); diff --git a/contracts/red-bank/tests/test_update_emergency_owner.rs b/contracts/red-bank/tests/test_update_emergency_owner.rs deleted file mode 100644 index c984b83d9..000000000 --- a/contracts/red-bank/tests/test_update_emergency_owner.rs +++ /dev/null @@ -1,75 +0,0 @@ -use cosmwasm_std::testing::{mock_env, mock_info}; -use mars_owner::{OwnerError::NotOwner, OwnerUpdate}; -use mars_red_bank::{contract::execute, error::ContractError}; -use mars_red_bank_types::red_bank::{ConfigResponse, ExecuteMsg, QueryMsg}; - -use crate::helpers::{th_query, th_setup}; - -mod helpers; - -#[test] -fn initialized_state() { - let deps = th_setup(&[]); - - let config: ConfigResponse = th_query(deps.as_ref(), QueryMsg::Config {}); - assert!(config.emergency_owner.is_none()); -} - -#[test] -fn only_owner_can_set_emergency_owner() { - let mut deps = th_setup(&[]); - - // only admin can propose new admins - let bad_guy = "bad_guy"; - let err = execute( - deps.as_mut(), - mock_env(), - mock_info(bad_guy, &[]), - ExecuteMsg::UpdateOwner(OwnerUpdate::SetEmergencyOwner { - emergency_owner: "new_emergency_owner".to_string(), - }), - ) - .unwrap_err(); - assert_eq!(err, ContractError::Owner(NotOwner {})); -} - -#[test] -fn set_and_clear_emergency_owner() { - let mut deps = th_setup(&[]); - - let original_config: ConfigResponse = th_query(deps.as_ref(), QueryMsg::Config {}); - - let emergency_owner = "new_emergency_owner"; - - execute( - deps.as_mut(), - mock_env(), - mock_info(&original_config.owner.clone().unwrap(), &[]), - ExecuteMsg::UpdateOwner(OwnerUpdate::SetEmergencyOwner { - emergency_owner: emergency_owner.to_string(), - }), - ) - .unwrap(); - - let new_config: ConfigResponse = th_query(deps.as_ref(), QueryMsg::Config {}); - - assert_eq!(new_config.owner, original_config.owner); - assert_eq!(new_config.proposed_new_owner, original_config.proposed_new_owner); - assert_eq!(new_config.emergency_owner, Some(emergency_owner.to_string())); - - // clear emergency owner - - execute( - deps.as_mut(), - mock_env(), - mock_info(&original_config.owner.clone().unwrap(), &[]), - ExecuteMsg::UpdateOwner(OwnerUpdate::ClearEmergencyOwner {}), - ) - .unwrap(); - - let new_config: ConfigResponse = th_query(deps.as_ref(), QueryMsg::Config {}); - - assert_eq!(new_config.owner, original_config.owner); - assert_eq!(new_config.proposed_new_owner, original_config.proposed_new_owner); - assert_eq!(new_config.emergency_owner, None); -} diff --git a/contracts/red-bank/tests/test_withdraw.rs b/contracts/red-bank/tests/test_withdraw.rs index 03aa82c30..77db18ea4 100644 --- a/contracts/red-bank/tests/test_withdraw.rs +++ b/contracts/red-bank/tests/test_withdraw.rs @@ -7,6 +7,7 @@ use helpers::{ has_collateral_position, set_collateral, th_build_interests_updated_event, th_get_expected_indices_and_rates, th_setup, TestUtilizationDeltaInfo, }; +use mars_params::types::AssetParams; use mars_red_bank::{ contract::execute, error::ContractError, @@ -24,6 +25,8 @@ use mars_red_bank_types::{ use mars_testing::{mock_env_at_block_time, MarsMockQuerier}; use mars_utils::math; +use crate::helpers::th_default_asset_params; + mod helpers; struct TestSuite { @@ -447,6 +450,7 @@ struct HealthCheckTestSuite { deps: OwnedDeps, denoms: [&'static str; 3], markets: [Market; 3], + asset_params: [AssetParams; 3], prices: [Decimal; 3], collaterals: [Collateral; 3], debts: [Debt; 3], @@ -466,8 +470,6 @@ fn setup_health_check_test() -> HealthCheckTestSuite { denom: denoms[0].to_string(), liquidity_index: Decimal::one(), borrow_index: Decimal::one(), - max_loan_to_value: Decimal::from_ratio(40u128, 100u128), - liquidation_threshold: Decimal::from_ratio(60u128, 100u128), collateral_total_scaled: Uint128::new(100_000) * SCALING_FACTOR, ..Default::default() }, @@ -475,8 +477,6 @@ fn setup_health_check_test() -> HealthCheckTestSuite { denom: denoms[1].to_string(), liquidity_index: Decimal::one(), borrow_index: Decimal::one(), - max_loan_to_value: Decimal::from_ratio(50u128, 100u128), - liquidation_threshold: Decimal::from_ratio(80u128, 100u128), collateral_total_scaled: Uint128::new(100_000) * SCALING_FACTOR, ..Default::default() }, @@ -484,13 +484,29 @@ fn setup_health_check_test() -> HealthCheckTestSuite { denom: denoms[2].to_string(), liquidity_index: Decimal::one(), borrow_index: Decimal::one(), - max_loan_to_value: Decimal::from_ratio(20u128, 100u128), - liquidation_threshold: Decimal::from_ratio(40u128, 100u128), collateral_total_scaled: Uint128::new(100_000) * SCALING_FACTOR, ..Default::default() }, ]; + let asset_params = [ + AssetParams { + max_loan_to_value: Decimal::from_ratio(40u128, 100u128), + liquidation_threshold: Decimal::from_ratio(60u128, 100u128), + ..th_default_asset_params() + }, + AssetParams { + max_loan_to_value: Decimal::from_ratio(50u128, 100u128), + liquidation_threshold: Decimal::from_ratio(80u128, 100u128), + ..th_default_asset_params() + }, + AssetParams { + max_loan_to_value: Decimal::from_ratio(20u128, 100u128), + liquidation_threshold: Decimal::from_ratio(40u128, 100u128), + ..th_default_asset_params() + }, + ]; + let prices = [ Decimal::from_ratio(3u128, 1u128), Decimal::from_ratio(2u128, 1u128), @@ -533,6 +549,11 @@ fn setup_health_check_test() -> HealthCheckTestSuite { .try_for_each(|(denom, market)| MARKETS.save(deps.as_mut().storage, denom, market)) .unwrap(); + denoms + .iter() + .zip(asset_params.iter()) + .for_each(|(denom, ap)| deps.querier.set_redbank_params(denom, ap.clone())); + denoms .iter() .zip(prices.iter()) @@ -554,6 +575,7 @@ fn setup_health_check_test() -> HealthCheckTestSuite { deps, denoms, markets, + asset_params, prices, collaterals, debts, @@ -565,6 +587,7 @@ fn setup_health_check_test() -> HealthCheckTestSuite { fn how_much_to_withdraw(suite: &HealthCheckTestSuite, block_time: u64) -> Uint128 { let HealthCheckTestSuite { markets, + asset_params, prices, collaterals, debts, @@ -577,7 +600,7 @@ fn how_much_to_withdraw(suite: &HealthCheckTestSuite, block_time: u64) -> Uint12 ScalingOperation::Truncate, ) .unwrap() - * markets[0].liquidation_threshold + * asset_params[0].liquidation_threshold * prices[0]; let token_3_weighted_lt_in_base_asset = compute_underlying_amount( @@ -586,7 +609,7 @@ fn how_much_to_withdraw(suite: &HealthCheckTestSuite, block_time: u64) -> Uint12 ScalingOperation::Truncate, ) .unwrap() - * markets[2].liquidation_threshold + * asset_params[2].liquidation_threshold * prices[2]; let weighted_liquidation_threshold_in_base_asset = @@ -603,7 +626,7 @@ fn how_much_to_withdraw(suite: &HealthCheckTestSuite, block_time: u64) -> Uint12 // How much to withdraw in base asset to have health factor equal to one let how_much_to_withdraw_in_base_asset = math::divide_uint128_by_decimal( weighted_liquidation_threshold_in_base_asset - total_collateralized_debt_in_base_asset, - markets[2].liquidation_threshold, + asset_params[2].liquidation_threshold, ) .unwrap(); diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 12b0316ed..3524937c8 100755 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -30,6 +30,7 @@ cw-it = { workspace = true, features = ["osmosis-test-t mars-oracle-osmosis = { workspace = true } mars-oracle-base = { workspace = true } mars-osmosis = { workspace = true } +mars-params = { workspace = true } mars-red-bank = { workspace = true } mars-red-bank-types = { workspace = true } mars-rewards-collector = { workspace = true } diff --git a/integration-tests/tests/helpers.rs b/integration-tests/tests/helpers.rs index aeb6c59c4..53d0283ff 100644 --- a/integration-tests/tests/helpers.rs +++ b/integration-tests/tests/helpers.rs @@ -1,8 +1,9 @@ #![allow(dead_code)] use anyhow::Result as AnyResult; -use cosmwasm_std::{Coin, Decimal}; +use cosmwasm_std::{Coin, Decimal, Uint128}; use cw_multi_test::AppResponse; +use mars_params::types::{AssetParams, HighLeverageStrategyParams, RedBankSettings, RoverSettings}; use mars_red_bank::error::ContractError; use mars_red_bank_types::red_bank::{ InitOrUpdateAssetParams, InterestRateModel, UserHealthStatus, UserPositionResponse, @@ -13,44 +14,68 @@ use osmosis_std::types::osmosis::{ }; use osmosis_test_tube::{Account, ExecuteResponse, OsmosisTestApp, Runner, SigningAccount}; -pub fn default_asset_params() -> InitOrUpdateAssetParams { - InitOrUpdateAssetParams { +pub fn default_asset_params() -> (InitOrUpdateAssetParams, AssetParams) { + let market_params = InitOrUpdateAssetParams { reserve_factor: Some(Decimal::percent(20)), - max_loan_to_value: Some(Decimal::percent(60)), - liquidation_threshold: Some(Decimal::percent(80)), - liquidation_bonus: Some(Decimal::percent(10)), interest_rate_model: Some(InterestRateModel { optimal_utilization_rate: Decimal::percent(10), base: Decimal::percent(30), slope_1: Decimal::percent(25), slope_2: Decimal::percent(30), }), - deposit_enabled: Some(true), - borrow_enabled: Some(true), - deposit_cap: None, - } + }; + let asset_params = AssetParams { + rover: RoverSettings { + whitelisted: false, + hls: HighLeverageStrategyParams { + max_loan_to_value: Decimal::percent(90), + liquidation_threshold: Decimal::one(), + }, + }, + red_bank: RedBankSettings { + deposit_enabled: true, + borrow_enabled: true, + deposit_cap: Uint128::MAX, + }, + max_loan_to_value: Decimal::percent(60), + liquidation_threshold: Decimal::percent(80), + liquidation_bonus: Decimal::percent(10), + }; + (market_params, asset_params) } pub fn default_asset_params_with( max_loan_to_value: Decimal, liquidation_threshold: Decimal, liquidation_bonus: Decimal, -) -> InitOrUpdateAssetParams { - InitOrUpdateAssetParams { +) -> (InitOrUpdateAssetParams, AssetParams) { + let market_params = InitOrUpdateAssetParams { reserve_factor: Some(Decimal::percent(20)), - max_loan_to_value: Some(max_loan_to_value), - liquidation_threshold: Some(liquidation_threshold), - liquidation_bonus: Some(liquidation_bonus), interest_rate_model: Some(InterestRateModel { optimal_utilization_rate: Decimal::percent(10), base: Decimal::percent(30), slope_1: Decimal::percent(25), slope_2: Decimal::percent(30), }), - deposit_enabled: Some(true), - borrow_enabled: Some(true), - deposit_cap: None, - } + }; + let asset_params = AssetParams { + rover: RoverSettings { + whitelisted: false, + hls: HighLeverageStrategyParams { + max_loan_to_value: Decimal::percent(90), + liquidation_threshold: Decimal::one(), + }, + }, + red_bank: RedBankSettings { + deposit_enabled: true, + borrow_enabled: true, + deposit_cap: Uint128::MAX, + }, + max_loan_to_value, + liquidation_threshold, + liquidation_bonus, + }; + (market_params, asset_params) } pub fn is_user_liquidatable(position: &UserPositionResponse) -> bool { diff --git a/integration-tests/tests/test_incentives.rs b/integration-tests/tests/test_incentives.rs index 35b6c698e..bdc575d33 100644 --- a/integration-tests/tests/test_incentives.rs +++ b/integration-tests/tests/test_incentives.rs @@ -18,7 +18,11 @@ fn rewards_claim() { let mut mock_env = MockEnvBuilder::new(None, owner).build(); let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); + let params = mock_env.params.clone(); + + let (market_params, asset_params) = default_asset_params(); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, "uusdc", asset_params); let incentives = mock_env.incentives.clone(); incentives.whitelist_incentive_denoms(&mut mock_env, &[("umars", 3)]); @@ -71,9 +75,15 @@ fn emissions_rates() { let mut mock_env = MockEnvBuilder::new(None, owner).build(); let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uosmo", default_asset_params()); - red_bank.init_asset(&mut mock_env, "umars", default_asset_params()); + let params = mock_env.params.clone(); + + let (market_params, asset_params) = default_asset_params(); + red_bank.init_asset(&mut mock_env, "uusdc", market_params.clone()); + red_bank.init_asset(&mut mock_env, "uosmo", market_params.clone()); + red_bank.init_asset(&mut mock_env, "umars", market_params); + params.init_params(&mut mock_env, "uusdc", asset_params.clone()); + params.init_params(&mut mock_env, "uosmo", asset_params.clone()); + params.init_params(&mut mock_env, "umars", asset_params); let incentives = mock_env.incentives.clone(); incentives.whitelist_incentive_denoms(&mut mock_env, &[("umars", 3)]); @@ -157,9 +167,15 @@ fn no_incentives_accrued_after_withdraw() { let mut mock_env = MockEnvBuilder::new(None, owner).build(); let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uosmo", default_asset_params()); - red_bank.init_asset(&mut mock_env, "umars", default_asset_params()); + let params = mock_env.params.clone(); + + let (market_params, asset_params) = default_asset_params(); + red_bank.init_asset(&mut mock_env, "uusdc", market_params.clone()); + red_bank.init_asset(&mut mock_env, "uosmo", market_params.clone()); + red_bank.init_asset(&mut mock_env, "umars", market_params); + params.init_params(&mut mock_env, "uusdc", asset_params.clone()); + params.init_params(&mut mock_env, "uosmo", asset_params.clone()); + params.init_params(&mut mock_env, "umars", asset_params); let incentives = mock_env.incentives.clone(); incentives.whitelist_incentive_denoms(&mut mock_env, &[("umars", 3)]); @@ -226,10 +242,17 @@ fn multiple_assets() { let mut mock_env = MockEnvBuilder::new(None, owner).build(); let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uosmo", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uatom", default_asset_params()); - red_bank.init_asset(&mut mock_env, "umars", default_asset_params()); + let params = mock_env.params.clone(); + + let (market_params, asset_params) = default_asset_params(); + red_bank.init_asset(&mut mock_env, "uusdc", market_params.clone()); + red_bank.init_asset(&mut mock_env, "uosmo", market_params.clone()); + red_bank.init_asset(&mut mock_env, "uatom", market_params.clone()); + red_bank.init_asset(&mut mock_env, "umars", market_params); + params.init_params(&mut mock_env, "uusdc", asset_params.clone()); + params.init_params(&mut mock_env, "uosmo", asset_params.clone()); + params.init_params(&mut mock_env, "uatom", asset_params.clone()); + params.init_params(&mut mock_env, "umars", asset_params); // set incentives let incentives = mock_env.incentives.clone(); @@ -302,7 +325,11 @@ fn multiple_users() { let mut mock_env = MockEnvBuilder::new(None, owner).build(); let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); + let params = mock_env.params.clone(); + + let (market_params, asset_params) = default_asset_params(); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, "uusdc", asset_params); // set incentives let incentives = mock_env.incentives.clone(); @@ -385,9 +412,15 @@ fn rewards_distributed_among_users_and_rewards_collector() { // setup red-bank assets let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uosmo", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uatom", default_asset_params()); + let params = mock_env.params.clone(); + + let (market_params, asset_params) = default_asset_params(); + red_bank.init_asset(&mut mock_env, "uusdc", market_params.clone()); + red_bank.init_asset(&mut mock_env, "uosmo", market_params.clone()); + red_bank.init_asset(&mut mock_env, "uatom", market_params); + params.init_params(&mut mock_env, "uusdc", asset_params.clone()); + params.init_params(&mut mock_env, "uosmo", asset_params.clone()); + params.init_params(&mut mock_env, "uatom", asset_params); // fund user accounts let user_a = Addr::unchecked("user_a"); diff --git a/integration-tests/tests/test_liquidations.rs b/integration-tests/tests/test_liquidations.rs index 32406a01c..ad17808cc 100644 --- a/integration-tests/tests/test_liquidations.rs +++ b/integration-tests/tests/test_liquidations.rs @@ -28,17 +28,18 @@ fn liquidate_collateralized_loan() { oracle.set_price_source_fixed(&mut mock_env, "uosmo", osmo_price); oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::one()); let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset( - &mut mock_env, - "uatom", - default_asset_params_with(atom_max_ltv, atom_liq_threshold, atom_liq_bonus), - ); - red_bank.init_asset( - &mut mock_env, - "uosmo", - default_asset_params_with(osmo_max_ltv, osmo_liq_threshold, osmo_liq_bonus), - ); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); + let params = mock_env.params.clone(); + let (market_params, asset_params) = + default_asset_params_with(atom_max_ltv, atom_liq_threshold, atom_liq_bonus); + red_bank.init_asset(&mut mock_env, "uatom", market_params); + params.init_params(&mut mock_env, "uatom", asset_params); + let (market_params, asset_params) = + default_asset_params_with(osmo_max_ltv, osmo_liq_threshold, osmo_liq_bonus); + red_bank.init_asset(&mut mock_env, "uosmo", market_params); + params.init_params(&mut mock_env, "uosmo", asset_params); + let (market_params, asset_params) = default_asset_params(); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, "uusdc", asset_params); // fund provider account with usdc let provider = Addr::unchecked("provider"); @@ -208,8 +209,12 @@ fn liquidate_uncollateralized_loan() { oracle.set_price_source_fixed(&mut mock_env, "uatom", Decimal::from_ratio(14u128, 1u128)); oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::one()); let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset(&mut mock_env, "uatom", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); + let params = mock_env.params.clone(); + let (market_params, asset_params) = default_asset_params(); + red_bank.init_asset(&mut mock_env, "uatom", market_params.clone()); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, "uatom", asset_params.clone()); + params.init_params(&mut mock_env, "uusdc", asset_params); // fund provider account with usdc let provider = Addr::unchecked("provider"); diff --git a/integration-tests/tests/test_oracles.rs b/integration-tests/tests/test_oracles.rs index 2511bde6e..6281f5291 100644 --- a/integration-tests/tests/test_oracles.rs +++ b/integration-tests/tests/test_oracles.rs @@ -6,6 +6,7 @@ use mars_oracle_osmosis::{ msg::PriceSourceResponse, Downtime, DowntimeDetector, OsmosisPriceSourceChecked, OsmosisPriceSourceUnchecked, }; +use mars_params::types::AssetParamsUpdate; use mars_red_bank_types::{ address_provider::{ ExecuteMsg::SetAddress, InstantiateMsg as InstantiateAddr, MarsAddressType, @@ -36,6 +37,7 @@ const OSMOSIS_RED_BANK_CONTRACT_NAME: &str = "mars-red-bank"; const OSMOSIS_ADDR_PROVIDER_CONTRACT_NAME: &str = "mars-address-provider"; const OSMOSIS_REWARDS_CONTRACT_NAME: &str = "mars-rewards-collector"; const OSMOSIS_INCENTIVES_CONTRACT_NAME: &str = "mars-incentives"; +const OSMOSIS_PARAMS_CONTRACT_NAME: &str = "mars-params"; #[test] fn querying_xyk_lp_price_if_no_price_for_tokens() { @@ -1063,7 +1065,6 @@ fn setup_redbank(wasm: &Wasm, signer: &SigningAccount) -> (Strin owner: signer.address(), config: CreateOrUpdateConfig { address_provider: Some(addr_provider_addr.clone()), - close_factor: Some(Decimal::percent(10)), }, }, ); @@ -1096,6 +1097,16 @@ fn setup_redbank(wasm: &Wasm, signer: &SigningAccount) -> (Strin }, ); + let params_addr = instantiate_contract( + wasm, + signer, + OSMOSIS_PARAMS_CONTRACT_NAME, + &mars_params::msg::InstantiateMsg { + owner: (signer.address()), + max_close_factor: Decimal::percent(10), + }, + ); + wasm.execute( &addr_provider_addr, &SetAddress { @@ -1140,11 +1151,24 @@ fn setup_redbank(wasm: &Wasm, signer: &SigningAccount) -> (Strin ) .unwrap(); + wasm.execute( + &addr_provider_addr, + &SetAddress { + address_type: MarsAddressType::Params, + address: params_addr.clone(), + }, + &[], + signer, + ) + .unwrap(); + + let (market_params, asset_params) = default_asset_params(); + wasm.execute( &red_bank_addr, &ExecuteRedBank::InitAsset { denom: "uosmo".to_string(), - params: default_asset_params(), + params: market_params.clone(), }, &[], signer, @@ -1155,11 +1179,34 @@ fn setup_redbank(wasm: &Wasm, signer: &SigningAccount) -> (Strin &red_bank_addr, &ExecuteRedBank::InitAsset { denom: "uatom".to_string(), - params: default_asset_params(), + params: market_params, }, &[], signer, ) .unwrap(); + + wasm.execute( + ¶ms_addr, + &mars_params::msg::ExecuteMsg::UpdateAssetParams(AssetParamsUpdate::AddOrUpdate { + denom: "uosmo".to_string(), + params: asset_params.clone(), + }), + &[], + signer, + ) + .unwrap(); + + wasm.execute( + ¶ms_addr, + &mars_params::msg::ExecuteMsg::UpdateAssetParams(AssetParamsUpdate::AddOrUpdate { + denom: "uatom".to_string(), + params: asset_params, + }), + &[], + signer, + ) + .unwrap(); + (oracle_addr, red_bank_addr) } diff --git a/integration-tests/tests/test_rover_flow.rs b/integration-tests/tests/test_rover_flow.rs index 8aeb72c32..6aa228a27 100644 --- a/integration-tests/tests/test_rover_flow.rs +++ b/integration-tests/tests/test_rover_flow.rs @@ -18,9 +18,14 @@ fn rover_flow() { oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(5u128, 10u128)); oracle.set_price_source_fixed(&mut mock_env, "uatom", Decimal::from_ratio(12u128, 1u128)); let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset(&mut mock_env, "uosmo", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uatom", default_asset_params()); + let params = mock_env.params.clone(); + let (market_params, asset_params) = default_asset_params(); + red_bank.init_asset(&mut mock_env, "uosmo", market_params.clone()); + red_bank.init_asset(&mut mock_env, "uusdc", market_params.clone()); + red_bank.init_asset(&mut mock_env, "uatom", market_params); + params.init_params(&mut mock_env, "uosmo", asset_params.clone()); + params.init_params(&mut mock_env, "uusdc", asset_params.clone()); + params.init_params(&mut mock_env, "uatom", asset_params); let rover = Addr::unchecked("rover"); diff --git a/integration-tests/tests/test_user_flow.rs b/integration-tests/tests/test_user_flow.rs index 713fe5a9c..8de1efaba 100644 --- a/integration-tests/tests/test_user_flow.rs +++ b/integration-tests/tests/test_user_flow.rs @@ -18,8 +18,12 @@ fn user_flow() { oracle.set_price_source_fixed(&mut mock_env, "uatom", Decimal::from_ratio(12u128, 1u128)); oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::one()); let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset(&mut mock_env, "uatom", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); + let params = mock_env.params.clone(); + let (market_params, asset_params) = default_asset_params(); + red_bank.init_asset(&mut mock_env, "uatom", market_params.clone()); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, "uatom", asset_params.clone()); + params.init_params(&mut mock_env, "uusdc", asset_params); // fund user_1 account with atom let user_1 = Addr::unchecked("user_1"); @@ -108,8 +112,12 @@ fn borrow_exact_liquidity() { oracle.set_price_source_fixed(&mut mock_env, "uatom", Decimal::from_ratio(12u128, 1u128)); oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::one()); let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset(&mut mock_env, "uatom", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); + let params = mock_env.params.clone(); + let (market_params, asset_params) = default_asset_params(); + red_bank.init_asset(&mut mock_env, "uatom", market_params.clone()); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, "uatom", asset_params.clone()); + params.init_params(&mut mock_env, "uusdc", asset_params); // fund provider account with usdc let provider = Addr::unchecked("provider"); @@ -229,8 +237,12 @@ fn prepare_debt_for_repayment() -> (MockEnv, RedBank, Addr) { oracle.set_price_source_fixed(&mut mock_env, "uatom", Decimal::from_ratio(12u128, 1u128)); oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::one()); let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset(&mut mock_env, "uatom", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); + let params = mock_env.params.clone(); + let (market_params, asset_params) = default_asset_params(); + red_bank.init_asset(&mut mock_env, "uatom", market_params.clone()); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, "uatom", asset_params.clone()); + params.init_params(&mut mock_env, "uusdc", asset_params); // fund user_1 account with atom let user_1 = Addr::unchecked("user_1"); @@ -294,16 +306,15 @@ fn internally_tracked_balances_used_for_borrow() { // setup Red Bank assets let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset( - &mut mock_env, - "uatom", - default_asset_params_with(atom_max_ltv, atom_liq_threshold, atom_liq_bonus), - ); - red_bank.init_asset( - &mut mock_env, - "uosmo", - default_asset_params_with(osmo_max_ltv, osmo_liq_threshold, osmo_liq_bonus), - ); + let params = mock_env.params.clone(); + let (market_params, asset_params) = + default_asset_params_with(atom_max_ltv, atom_liq_threshold, atom_liq_bonus); + red_bank.init_asset(&mut mock_env, "uatom", market_params); + params.init_params(&mut mock_env, "uatom", asset_params); + let (market_params, asset_params) = + default_asset_params_with(osmo_max_ltv, osmo_liq_threshold, osmo_liq_bonus); + red_bank.init_asset(&mut mock_env, "uosmo", market_params); + params.init_params(&mut mock_env, "uusdc", asset_params); // initial deposit amount let funded_atom = 1u128; // 1 uatom @@ -356,8 +367,12 @@ fn interest_rates_accured_based_on_internally_tracked_balances() { oracle.set_price_source_fixed(&mut mock_env, "uatom", Decimal::from_ratio(12u128, 1u128)); oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::one()); let red_bank = mock_env.red_bank.clone(); - red_bank.init_asset(&mut mock_env, "uatom", default_asset_params()); - red_bank.init_asset(&mut mock_env, "uusdc", default_asset_params()); + let params = mock_env.params.clone(); + let (market_params, asset_params) = default_asset_params(); + red_bank.init_asset(&mut mock_env, "uatom", market_params.clone()); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, "uatom", asset_params.clone()); + params.init_params(&mut mock_env, "uusdc", asset_params); // fund user_1 account with usdc let user_1 = Addr::unchecked("user_1"); diff --git a/packages/health/Cargo.toml b/packages/health/Cargo.toml index 536d1e4c1..4be4d5a53 100644 --- a/packages/health/Cargo.toml +++ b/packages/health/Cargo.toml @@ -20,6 +20,7 @@ backtraces = ["cosmwasm-std/backtraces"] [dependencies] cosmwasm-std = { workspace = true } +mars-params = { workspace = true } mars-red-bank-types = { workspace = true } thiserror = { workspace = true } diff --git a/packages/health/src/health.rs b/packages/health/src/health.rs index e35573c35..0db936818 100644 --- a/packages/health/src/health.rs +++ b/packages/health/src/health.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, fmt}; use cosmwasm_std::{Addr, Coin, Decimal, Fraction, QuerierWrapper, StdResult, Uint128}; -use mars_red_bank_types::red_bank::Market; +use mars_params::types::AssetParams; use crate::{error::HealthError, query::MarsQuerier}; @@ -123,11 +123,11 @@ impl Health { p.collateral_amount += c.amount; } None => { - let Market { + let AssetParams { max_loan_to_value, liquidation_threshold, .. - } = querier.query_market(&c.denom)?; + } = querier.query_asset_params(&c.denom)?; positions.insert( c.denom.clone(), @@ -151,11 +151,11 @@ impl Health { p.debt_amount += d.amount; } None => { - let Market { + let AssetParams { max_loan_to_value, liquidation_threshold, .. - } = querier.query_market(&d.denom)?; + } = querier.query_asset_params(&d.denom)?; positions.insert( d.denom.clone(), diff --git a/packages/health/src/query.rs b/packages/health/src/query.rs index 1023f83b1..83697ea54 100644 --- a/packages/health/src/query.rs +++ b/packages/health/src/query.rs @@ -1,32 +1,26 @@ use cosmwasm_std::{Addr, Decimal, QuerierWrapper, StdResult}; -use mars_red_bank_types::{ - oracle::{self, ActionKind, PriceResponse}, - red_bank::{self, Market}, -}; +use mars_params::types::AssetParams; +use mars_red_bank_types::oracle::{self, ActionKind, PriceResponse}; pub struct MarsQuerier<'a> { querier: &'a QuerierWrapper<'a>, oracle_addr: &'a Addr, - red_bank_addr: &'a Addr, + params_addr: &'a Addr, } impl<'a> MarsQuerier<'a> { - pub fn new( - querier: &'a QuerierWrapper, - oracle_addr: &'a Addr, - red_bank_addr: &'a Addr, - ) -> Self { + pub fn new(querier: &'a QuerierWrapper, oracle_addr: &'a Addr, params_addr: &'a Addr) -> Self { MarsQuerier { querier, oracle_addr, - red_bank_addr, + params_addr, } } - pub fn query_market(&self, denom: &str) -> StdResult { + pub fn query_asset_params(&self, denom: &str) -> StdResult { self.querier.query_wasm_smart( - self.red_bank_addr, - &red_bank::QueryMsg::Market { + self.params_addr, + &mars_params::msg::QueryMsg::AssetParams { denom: denom.to_string(), }, ) diff --git a/packages/health/tests/test_from_coins_to_positions.rs b/packages/health/tests/test_from_coins_to_positions.rs index 58451d1cc..10a525bae 100644 --- a/packages/health/tests/test_from_coins_to_positions.rs +++ b/packages/health/tests/test_from_coins_to_positions.rs @@ -7,6 +7,7 @@ use mars_health::{ health::{Health, Position}, query::MarsQuerier, }; +use mars_params::types::{AssetParams, HighLeverageStrategyParams, RedBankSettings, RoverSettings}; use mars_red_bank_types::red_bank::Market; use mars_testing::MarsMockQuerier; @@ -102,9 +103,10 @@ fn from_coins_to_positions() { assert_eq!( positions, - StdError::generic_err( - "Querier contract error: [mock]: could not find the market for invalid_denom" - ) + StdError::GenericErr { + msg: "Querier contract error: [mock]: could not find the params for invalid_denom" + .to_string() + } ); } @@ -120,18 +122,54 @@ fn mock_setup() -> MarsMockQuerier { // Set Markets let osmo_market = Market { denom: "osmo".to_string(), - max_loan_to_value: Decimal::from_atomics(50u128, 2).unwrap(), - liquidation_threshold: Decimal::from_atomics(55u128, 2).unwrap(), ..Default::default() }; mock_querier.set_redbank_market(osmo_market); + mock_querier.set_redbank_params( + "osmo", + AssetParams { + rover: RoverSettings { + whitelisted: false, + hls: HighLeverageStrategyParams { + max_loan_to_value: Decimal::percent(90), + liquidation_threshold: Decimal::one(), + }, + }, + red_bank: RedBankSettings { + deposit_enabled: false, + borrow_enabled: false, + deposit_cap: Default::default(), + }, + max_loan_to_value: Decimal::from_atomics(50u128, 2).unwrap(), + liquidation_threshold: Decimal::from_atomics(55u128, 2).unwrap(), + liquidation_bonus: Default::default(), + }, + ); let atom_market = Market { denom: "atom".to_string(), - max_loan_to_value: Decimal::from_atomics(70u128, 2).unwrap(), - liquidation_threshold: Decimal::from_atomics(75u128, 2).unwrap(), ..Default::default() }; mock_querier.set_redbank_market(atom_market); + mock_querier.set_redbank_params( + "atom", + AssetParams { + rover: RoverSettings { + whitelisted: false, + hls: HighLeverageStrategyParams { + max_loan_to_value: Decimal::percent(90), + liquidation_threshold: Decimal::one(), + }, + }, + red_bank: RedBankSettings { + deposit_enabled: false, + borrow_enabled: false, + deposit_cap: Default::default(), + }, + max_loan_to_value: Decimal::from_atomics(70u128, 2).unwrap(), + liquidation_threshold: Decimal::from_atomics(75u128, 2).unwrap(), + liquidation_bonus: Default::default(), + }, + ); // Set prices in the oracle mock_querier.set_oracle_price("osmo", Decimal::from_atomics(23654u128, 4).unwrap()); diff --git a/packages/health/tests/test_health_from_coins.rs b/packages/health/tests/test_health_from_coins.rs index c3eac2122..9fe6a9691 100644 --- a/packages/health/tests/test_health_from_coins.rs +++ b/packages/health/tests/test_health_from_coins.rs @@ -5,6 +5,7 @@ use cosmwasm_std::{ Uint128, }; use mars_health::{error::HealthError, health::Health}; +use mars_params::types::{AssetParams, HighLeverageStrategyParams, RedBankSettings, RoverSettings}; use mars_red_bank_types::red_bank::Market; use mars_testing::MarsMockQuerier; @@ -15,18 +16,54 @@ fn health_success_from_coins() { // Set Markets let osmo_market = Market { denom: "osmo".to_string(), - max_loan_to_value: Decimal::from_atomics(50u128, 2).unwrap(), - liquidation_threshold: Decimal::from_atomics(55u128, 2).unwrap(), ..Default::default() }; mock_querier.set_redbank_market(osmo_market); + mock_querier.set_redbank_params( + "osmo", + AssetParams { + rover: RoverSettings { + whitelisted: false, + hls: HighLeverageStrategyParams { + max_loan_to_value: Decimal::percent(90), + liquidation_threshold: Decimal::one(), + }, + }, + red_bank: RedBankSettings { + deposit_enabled: true, + borrow_enabled: true, + deposit_cap: Uint128::MAX, + }, + max_loan_to_value: Decimal::from_atomics(50u128, 2).unwrap(), + liquidation_threshold: Decimal::from_atomics(55u128, 2).unwrap(), + liquidation_bonus: Default::default(), + }, + ); let atom_market = Market { denom: "atom".to_string(), - max_loan_to_value: Decimal::from_atomics(70u128, 2).unwrap(), - liquidation_threshold: Decimal::from_atomics(75u128, 2).unwrap(), ..Default::default() }; mock_querier.set_redbank_market(atom_market); + mock_querier.set_redbank_params( + "atom", + AssetParams { + rover: RoverSettings { + whitelisted: false, + hls: HighLeverageStrategyParams { + max_loan_to_value: Decimal::percent(90), + liquidation_threshold: Decimal::one(), + }, + }, + red_bank: RedBankSettings { + deposit_enabled: true, + borrow_enabled: true, + deposit_cap: Uint128::MAX, + }, + max_loan_to_value: Decimal::from_atomics(70u128, 2).unwrap(), + liquidation_threshold: Decimal::from_atomics(75u128, 2).unwrap(), + liquidation_bonus: Default::default(), + }, + ); // Set prices in the oracle mock_querier.set_oracle_price("osmo", Decimal::from_atomics(23654u128, 4).unwrap()); @@ -68,11 +105,29 @@ fn health_error_from_coins() { // Set Markets let osmo_market = Market { denom: "osmo".to_string(), - max_loan_to_value: Decimal::from_atomics(50u128, 2).unwrap(), - liquidation_threshold: Decimal::from_atomics(55u128, 2).unwrap(), ..Default::default() }; mock_querier.set_redbank_market(osmo_market); + mock_querier.set_redbank_params( + "osmo", + AssetParams { + rover: RoverSettings { + whitelisted: false, + hls: HighLeverageStrategyParams { + max_loan_to_value: Decimal::percent(90), + liquidation_threshold: Decimal::one(), + }, + }, + red_bank: RedBankSettings { + deposit_enabled: false, + borrow_enabled: false, + deposit_cap: Default::default(), + }, + max_loan_to_value: Decimal::from_atomics(50u128, 2).unwrap(), + liquidation_threshold: Decimal::from_atomics(55u128, 2).unwrap(), + liquidation_bonus: Default::default(), + }, + ); // Set prices in the oracle mock_querier.set_oracle_price("osmo", Decimal::MAX); diff --git a/packages/testing/Cargo.toml b/packages/testing/Cargo.toml index c113d3e4b..7cd04417f 100644 --- a/packages/testing/Cargo.toml +++ b/packages/testing/Cargo.toml @@ -32,6 +32,7 @@ mars-oracle-osmosis = { workspace = true } mars-oracle-wasm = { workspace = true } mars-osmosis = { workspace = true } mars-owner = { workspace = true } +mars-params = { workspace = true } mars-red-bank = { workspace = true } mars-red-bank-types = { workspace = true } mars-rewards-collector = { workspace = true } diff --git a/packages/testing/src/integration/mock_contracts.rs b/packages/testing/src/integration/mock_contracts.rs index 91f537303..e68096233 100644 --- a/packages/testing/src/integration/mock_contracts.rs +++ b/packages/testing/src/integration/mock_contracts.rs @@ -49,3 +49,12 @@ pub fn mock_rewards_collector_osmosis_contract() -> Box> { ); Box::new(contract) } + +pub fn mock_params_osmosis_contract() -> Box> { + let contract = ContractWrapper::new( + mars_params::contract::execute, + mars_params::contract::instantiate, + mars_params::contract::query, + ); + Box::new(contract) +} diff --git a/packages/testing/src/integration/mock_env.rs b/packages/testing/src/integration/mock_env.rs index 00696eab6..6784b5e11 100644 --- a/packages/testing/src/integration/mock_env.rs +++ b/packages/testing/src/integration/mock_env.rs @@ -6,6 +6,7 @@ use anyhow::Result as AnyResult; use cosmwasm_std::{coin, Addr, Coin, Decimal, Empty, StdResult, Uint128}; use cw_multi_test::{App, AppResponse, BankSudo, BasicApp, Executor, SudoMsg}; use mars_oracle_osmosis::OsmosisPriceSourceUnchecked; +use mars_params::types::{AssetParams, AssetParamsUpdate}; use mars_red_bank_types::{ address_provider::{self, MarsAddressType}, incentives, oracle, @@ -19,7 +20,7 @@ use mars_red_bank_types::{ use crate::integration::mock_contracts::{ mock_address_provider_contract, mock_incentives_contract, mock_oracle_osmosis_contract, - mock_red_bank_contract, mock_rewards_collector_osmosis_contract, + mock_params_osmosis_contract, mock_red_bank_contract, mock_rewards_collector_osmosis_contract, }; pub struct MockEnv { @@ -30,6 +31,7 @@ pub struct MockEnv { pub oracle: Oracle, pub red_bank: RedBank, pub rewards_collector: RewardsCollector, + pub params: Params, } #[derive(Clone)] @@ -57,6 +59,11 @@ pub struct RewardsCollector { pub contract_addr: Addr, } +#[derive(Clone)] +pub struct Params { + pub contract_addr: Addr, +} + impl MockEnv { pub fn increment_by_blocks(&mut self, num_of_blocks: u64) { self.app.update_block(|block| { @@ -446,11 +453,26 @@ impl RewardsCollector { } } +impl Params { + pub fn init_params(&self, env: &mut MockEnv, denom: &str, params: AssetParams) { + env.app + .execute_contract( + env.owner.clone(), + self.contract_addr.clone(), + &mars_params::msg::ExecuteMsg::UpdateAssetParams(AssetParamsUpdate::AddOrUpdate { + denom: denom.to_string(), + params, + }), + &[], + ) + .unwrap(); + } +} + pub struct MockEnvBuilder { app: BasicApp, admin: Option, owner: Addr, - emergency_owner: Addr, chain_prefix: String, mars_denom: String, @@ -472,8 +494,7 @@ impl MockEnvBuilder { Self { app: App::default(), admin, - owner: owner.clone(), - emergency_owner: owner, + owner, chain_prefix: "".to_string(), // empty prefix for multitest because deployed contracts have addresses such as contract1, contract2 etc which are invalid in address-provider mars_denom: "umars".to_string(), base_denom: "uosmo".to_string(), @@ -539,6 +560,7 @@ impl MockEnvBuilder { let oracle_addr = self.deploy_oracle_osmosis(); let red_bank_addr = self.deploy_red_bank(&address_provider_addr); let rewards_collector_addr = self.deploy_rewards_collector_osmosis(&address_provider_addr); + let params_addr = self.deploy_params_osmosis(); self.update_address_provider( &address_provider_addr, @@ -556,6 +578,7 @@ impl MockEnvBuilder { MarsAddressType::RewardsCollector, &rewards_collector_addr, ); + self.update_address_provider(&address_provider_addr, MarsAddressType::Params, ¶ms_addr); MockEnv { app: take(&mut self.app), @@ -575,6 +598,9 @@ impl MockEnvBuilder { rewards_collector: RewardsCollector { contract_addr: rewards_collector_addr, }, + params: Params { + contract_addr: params_addr, + }, } } @@ -646,7 +672,6 @@ impl MockEnvBuilder { owner: self.owner.to_string(), config: CreateOrUpdateConfig { address_provider: Some(address_provider_addr.to_string()), - close_factor: Some(self.close_factor), }, }, &[], @@ -680,6 +705,24 @@ impl MockEnvBuilder { .unwrap() } + fn deploy_params_osmosis(&mut self) -> Addr { + let code_id = self.app.store_code(mock_params_osmosis_contract()); + + self.app + .instantiate_contract( + code_id, + self.owner.clone(), + &mars_params::msg::InstantiateMsg { + owner: self.owner.to_string(), + max_close_factor: self.close_factor, + }, + &[], + "params", + None, + ) + .unwrap() + } + fn update_address_provider( &mut self, address_provider_addr: &Addr, diff --git a/packages/testing/src/lib.rs b/packages/testing/src/lib.rs index 62e49d5b9..eceaed296 100644 --- a/packages/testing/src/lib.rs +++ b/packages/testing/src/lib.rs @@ -12,6 +12,7 @@ mod mock_address_provider; mod mocks; mod oracle_querier; mod osmosis_querier; +mod params_querier; mod pyth_querier; mod red_bank_querier; mod redemption_rate_querier; diff --git a/packages/testing/src/mars_mock_querier.rs b/packages/testing/src/mars_mock_querier.rs index 6b95abb4a..d5f37ba98 100644 --- a/packages/testing/src/mars_mock_querier.rs +++ b/packages/testing/src/mars_mock_querier.rs @@ -10,6 +10,7 @@ use mars_oracle_osmosis::{ DowntimeDetector, }; use mars_osmosis::helpers::QueryPoolResponse; +use mars_params::types::AssetParams; use mars_red_bank_types::{address_provider, incentives, oracle, red_bank}; use osmosis_std::types::osmosis::{ downtimedetector::v1beta1::RecoveredSinceDowntimeOfLengthResponse, @@ -23,6 +24,7 @@ use crate::{ mock_address_provider, oracle_querier::OracleQuerier, osmosis_querier::{OsmosisQuerier, PriceKey}, + params_querier::ParamsQuerier, pyth_querier::PythQuerier, red_bank_querier::RedBankQuerier, redemption_rate_querier::RedemptionRateQuerier, @@ -36,6 +38,7 @@ pub struct MarsMockQuerier { pyth_querier: PythQuerier, redbank_querier: RedBankQuerier, redemption_rate_querier: RedemptionRateQuerier, + params_querier: ParamsQuerier, } impl Querier for MarsMockQuerier { @@ -64,6 +67,7 @@ impl MarsMockQuerier { pyth_querier: PythQuerier::default(), redbank_querier: RedBankQuerier::default(), redemption_rate_querier: Default::default(), + params_querier: ParamsQuerier::default(), } } @@ -190,6 +194,14 @@ impl MarsMockQuerier { self.redbank_querier.users_positions.insert(user_address, position); } + pub fn set_redbank_params(&mut self, denom: &str, params: AssetParams) { + self.params_querier.params.insert(denom.to_string(), params); + } + + pub fn set_close_factor(&mut self, close_factor: Decimal) { + self.params_querier.close_factor = close_factor; + } + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { match &request { QueryRequest::Wasm(WasmQuery::Smart { @@ -240,6 +252,11 @@ impl MarsMockQuerier { return self.redemption_rate_querier.handle_query(redemption_rate_req); } + // Params Queries + if let Ok(params_query) = from_binary::(msg) { + return self.params_querier.handle_query(params_query); + } + panic!("[mock]: Unsupported wasm query: {msg:?}"); } diff --git a/packages/testing/src/params_querier.rs b/packages/testing/src/params_querier.rs new file mode 100644 index 000000000..5840e3922 --- /dev/null +++ b/packages/testing/src/params_querier.rs @@ -0,0 +1,26 @@ +use std::collections::HashMap; + +use cosmwasm_std::{to_binary, Binary, ContractResult, Decimal, QuerierResult}; +use mars_params::{msg::QueryMsg, types::AssetParams}; + +#[derive(Default)] +pub struct ParamsQuerier { + pub close_factor: Decimal, + pub params: HashMap, +} + +impl ParamsQuerier { + pub fn handle_query(&self, query: QueryMsg) -> QuerierResult { + let ret: ContractResult = match query { + QueryMsg::MaxCloseFactor {} => to_binary(&self.close_factor).into(), + QueryMsg::AssetParams { + denom, + } => match self.params.get(&denom) { + Some(params) => to_binary(¶ms).into(), + None => Err(format!("[mock]: could not find the params for {denom}")).into(), + }, + _ => Err("[mock]: Unsupported params query".to_string()).into(), + }; + Ok(ret).into() + } +} diff --git a/packages/types/src/address_provider.rs b/packages/types/src/address_provider.rs index 7fdfd7d13..bbcb134a5 100644 --- a/packages/types/src/address_provider.rs +++ b/packages/types/src/address_provider.rs @@ -12,6 +12,7 @@ pub enum MarsAddressType { Oracle, RedBank, RewardsCollector, + Params, /// Protocol admin is an ICS-27 interchain account controlled by Mars Hub's x/gov module. /// This account will take the owner and admin roles of red-bank contracts. /// @@ -41,6 +42,7 @@ impl fmt::Display for MarsAddressType { MarsAddressType::FeeCollector => "fee_collector", MarsAddressType::Incentives => "incentives", MarsAddressType::Oracle => "oracle", + MarsAddressType::Params => "params", MarsAddressType::ProtocolAdmin => "protocol_admin", MarsAddressType::RedBank => "red_bank", MarsAddressType::RewardsCollector => "rewards_collector", @@ -59,6 +61,7 @@ impl FromStr for MarsAddressType { "fee_collector" => Ok(MarsAddressType::FeeCollector), "incentives" => Ok(MarsAddressType::Incentives), "oracle" => Ok(MarsAddressType::Oracle), + "params" => Ok(MarsAddressType::Params), "protocol_admin" => Ok(MarsAddressType::ProtocolAdmin), "red_bank" => Ok(MarsAddressType::RedBank), "rewards_collector" => Ok(MarsAddressType::RewardsCollector), diff --git a/packages/types/src/red_bank/market.rs b/packages/types/src/red_bank/market.rs index cb5f96884..2ffe0a041 100644 --- a/packages/types/src/red_bank/market.rs +++ b/packages/types/src/red_bank/market.rs @@ -1,9 +1,6 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Decimal, StdResult, Uint128}; -use mars_utils::{ - error::ValidationError, - helpers::{decimal_param_le_one, decimal_param_lt_one}, -}; +use mars_utils::{error::ValidationError, helpers::decimal_param_lt_one}; use crate::red_bank::InterestRateModel; @@ -11,14 +8,6 @@ use crate::red_bank::InterestRateModel; pub struct Market { /// Denom of the asset pub denom: String, - - /// Max base asset that can be borrowed per "base asset" collateral when using the asset as collateral - pub max_loan_to_value: Decimal, - /// Base asset amount in debt position per "base asset" of asset collateral that if surpassed makes the user's position liquidatable. - pub liquidation_threshold: Decimal, - /// Bonus amount of collateral liquidator get when repaying user's debt (Will get collateral - /// from user in an amount equal to debt repayed + bonus) - pub liquidation_bonus: Decimal, /// Portion of the borrow rate that is kept as protocol rewards pub reserve_factor: Decimal, @@ -40,13 +29,6 @@ pub struct Market { pub collateral_total_scaled: Uint128, /// Total debt scaled for the market's currency pub debt_total_scaled: Uint128, - - /// If false cannot deposit - pub deposit_enabled: bool, - /// If false cannot borrow - pub borrow_enabled: bool, - /// Deposit Cap (defined in terms of the asset) - pub deposit_cap: Uint128, } impl Default for Market { @@ -57,18 +39,11 @@ impl Default for Market { liquidity_index: Decimal::one(), borrow_rate: Decimal::zero(), liquidity_rate: Decimal::zero(), - max_loan_to_value: Decimal::zero(), reserve_factor: Decimal::zero(), indexes_last_updated: 0, collateral_total_scaled: Uint128::zero(), debt_total_scaled: Uint128::zero(), - liquidation_threshold: Decimal::one(), - liquidation_bonus: Decimal::zero(), interest_rate_model: InterestRateModel::default(), - deposit_enabled: true, - borrow_enabled: true, - // By default the cap should be unlimited (no cap) - deposit_cap: Uint128::MAX, } } } @@ -76,18 +51,6 @@ impl Default for Market { impl Market { pub fn validate(&self) -> Result<(), ValidationError> { decimal_param_lt_one(self.reserve_factor, "reserve_factor")?; - decimal_param_le_one(self.max_loan_to_value, "max_loan_to_value")?; - decimal_param_le_one(self.liquidation_threshold, "liquidation_threshold")?; - decimal_param_le_one(self.liquidation_bonus, "liquidation_bonus")?; - - // liquidation_threshold should be greater than max_loan_to_value - if self.liquidation_threshold <= self.max_loan_to_value { - return Err(ValidationError::InvalidParam { - param_name: "liquidation_threshold".to_string(), - invalid_value: self.liquidation_threshold.to_string(), - predicate: format!("> {} (max LTV)", self.max_loan_to_value), - }); - } self.interest_rate_model.validate()?; diff --git a/packages/types/src/red_bank/msg.rs b/packages/types/src/red_bank/msg.rs index 3a29d96e2..4e051d070 100644 --- a/packages/types/src/red_bank/msg.rs +++ b/packages/types/src/red_bank/msg.rs @@ -112,30 +112,15 @@ pub enum ExecuteMsg { #[cw_serde] pub struct CreateOrUpdateConfig { pub address_provider: Option, - pub close_factor: Option, } #[cw_serde] pub struct InitOrUpdateAssetParams { /// Portion of the borrow rate that is kept as protocol rewards pub reserve_factor: Option, - /// Max uusd that can be borrowed per uusd of collateral when using the asset as collateral - pub max_loan_to_value: Option, - /// uusd amount in debt position per uusd of asset collateral that if surpassed makes the user's position liquidatable. - pub liquidation_threshold: Option, - /// Bonus amount of collateral liquidator get when repaying user's debt (Will get collateral - /// from user in an amount equal to debt repayed + bonus) - pub liquidation_bonus: Option, /// Interest rate strategy to calculate borrow_rate and liquidity_rate pub interest_rate_model: Option, - - /// If false cannot deposit - pub deposit_enabled: Option, - /// If false cannot borrow - pub borrow_enabled: Option, - /// Deposit Cap defined in terms of the asset (Unlimited by default) - pub deposit_cap: Option, } #[cw_serde] diff --git a/packages/types/src/red_bank/types.rs b/packages/types/src/red_bank/types.rs index 20b37bc10..6495d3b99 100644 --- a/packages/types/src/red_bank/types.rs +++ b/packages/types/src/red_bank/types.rs @@ -1,21 +1,11 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Decimal, Uint128}; -use mars_utils::{error::ValidationError, helpers::decimal_param_le_one}; /// Global configuration #[cw_serde] pub struct Config { /// Address provider returns addresses for all protocol contracts pub address_provider: T, - /// Maximum percentage of outstanding debt that can be covered by a liquidator - pub close_factor: Decimal, -} - -impl Config { - pub fn validate(&self) -> Result<(), ValidationError> { - decimal_param_le_one(self.close_factor, "close_factor")?; - Ok(()) - } } #[cw_serde] @@ -70,12 +60,8 @@ pub struct ConfigResponse { pub owner: Option, /// The contract's proposed owner pub proposed_new_owner: Option, - /// The contract's emergency owner - pub emergency_owner: Option, /// Address provider returns addresses for all protocol contracts pub address_provider: String, - /// Maximum percentage of outstanding debt that can be covered by a liquidator - pub close_factor: Decimal, } #[cw_serde] diff --git a/schemas/mars-address-provider/mars-address-provider.json b/schemas/mars-address-provider/mars-address-provider.json index a0c593c9d..ea4a34e9c 100644 --- a/schemas/mars-address-provider/mars-address-provider.json +++ b/schemas/mars-address-provider/mars-address-provider.json @@ -76,7 +76,8 @@ "incentives", "oracle", "red_bank", - "rewards_collector" + "rewards_collector", + "params" ] }, { @@ -278,7 +279,8 @@ "incentives", "oracle", "red_bank", - "rewards_collector" + "rewards_collector", + "params" ] }, { @@ -348,7 +350,8 @@ "incentives", "oracle", "red_bank", - "rewards_collector" + "rewards_collector", + "params" ] }, { @@ -421,7 +424,8 @@ "incentives", "oracle", "red_bank", - "rewards_collector" + "rewards_collector", + "params" ] }, { @@ -494,7 +498,8 @@ "incentives", "oracle", "red_bank", - "rewards_collector" + "rewards_collector", + "params" ] }, { diff --git a/schemas/mars-red-bank/mars-red-bank.json b/schemas/mars-red-bank/mars-red-bank.json index d961e6207..114365ce5 100644 --- a/schemas/mars-red-bank/mars-red-bank.json +++ b/schemas/mars-red-bank/mars-red-bank.json @@ -34,23 +34,9 @@ "string", "null" ] - }, - "close_factor": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] } }, "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" } } }, @@ -393,16 +379,6 @@ "string", "null" ] - }, - "close_factor": { - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] } }, "additionalProperties": false @@ -414,31 +390,6 @@ "InitOrUpdateAssetParams": { "type": "object", "properties": { - "borrow_enabled": { - "description": "If false cannot borrow", - "type": [ - "boolean", - "null" - ] - }, - "deposit_cap": { - "description": "Deposit Cap defined in terms of the asset (Unlimited by default)", - "anyOf": [ - { - "$ref": "#/definitions/Uint128" - }, - { - "type": "null" - } - ] - }, - "deposit_enabled": { - "description": "If false cannot deposit", - "type": [ - "boolean", - "null" - ] - }, "interest_rate_model": { "description": "Interest rate strategy to calculate borrow_rate and liquidity_rate", "anyOf": [ @@ -450,39 +401,6 @@ } ] }, - "liquidation_bonus": { - "description": "Bonus amount of collateral liquidator get when repaying user's debt (Will get collateral from user in an amount equal to debt repayed + bonus)", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "liquidation_threshold": { - "description": "uusd amount in debt position per uusd of asset collateral that if surpassed makes the user's position liquidatable.", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, - "max_loan_to_value": { - "description": "Max uusd that can be borrowed per uusd of collateral when using the asset as collateral", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, "reserve_factor": { "description": "Portion of the borrow rate that is kept as protocol rewards", "anyOf": [ @@ -1021,29 +939,13 @@ "title": "ConfigResponse", "type": "object", "required": [ - "address_provider", - "close_factor" + "address_provider" ], "properties": { "address_provider": { "description": "Address provider returns addresses for all protocol contracts", "type": "string" }, - "close_factor": { - "description": "Maximum percentage of outstanding debt that can be covered by a liquidator", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, - "emergency_owner": { - "description": "The contract's emergency owner", - "type": [ - "string", - "null" - ] - }, "owner": { "description": "The contract's owner", "type": [ @@ -1059,41 +961,25 @@ ] } }, - "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" - } - } + "additionalProperties": false }, "market": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Market", "type": "object", "required": [ - "borrow_enabled", "borrow_index", "borrow_rate", "collateral_total_scaled", "debt_total_scaled", "denom", - "deposit_cap", - "deposit_enabled", "indexes_last_updated", "interest_rate_model", - "liquidation_bonus", - "liquidation_threshold", "liquidity_index", "liquidity_rate", - "max_loan_to_value", "reserve_factor" ], "properties": { - "borrow_enabled": { - "description": "If false cannot borrow", - "type": "boolean" - }, "borrow_index": { "description": "Borrow index (Used to compute borrow interest)", "allOf": [ @@ -1130,18 +1016,6 @@ "description": "Denom of the asset", "type": "string" }, - "deposit_cap": { - "description": "Deposit Cap (defined in terms of the asset)", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "deposit_enabled": { - "description": "If false cannot deposit", - "type": "boolean" - }, "indexes_last_updated": { "description": "Timestamp (seconds) where indexes and where last updated", "type": "integer", @@ -1156,22 +1030,6 @@ } ] }, - "liquidation_bonus": { - "description": "Bonus amount of collateral liquidator get when repaying user's debt (Will get collateral from user in an amount equal to debt repayed + bonus)", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, - "liquidation_threshold": { - "description": "Base asset amount in debt position per \"base asset\" of asset collateral that if surpassed makes the user's position liquidatable.", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, "liquidity_index": { "description": "Liquidity index (Used to compute deposit interest)", "allOf": [ @@ -1188,14 +1046,6 @@ } ] }, - "max_loan_to_value": { - "description": "Max base asset that can be borrowed per \"base asset\" collateral when using the asset as collateral", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, "reserve_factor": { "description": "Portion of the borrow rate that is kept as protocol rewards", "allOf": [ @@ -1320,28 +1170,18 @@ "Market": { "type": "object", "required": [ - "borrow_enabled", "borrow_index", "borrow_rate", "collateral_total_scaled", "debt_total_scaled", "denom", - "deposit_cap", - "deposit_enabled", "indexes_last_updated", "interest_rate_model", - "liquidation_bonus", - "liquidation_threshold", "liquidity_index", "liquidity_rate", - "max_loan_to_value", "reserve_factor" ], "properties": { - "borrow_enabled": { - "description": "If false cannot borrow", - "type": "boolean" - }, "borrow_index": { "description": "Borrow index (Used to compute borrow interest)", "allOf": [ @@ -1378,18 +1218,6 @@ "description": "Denom of the asset", "type": "string" }, - "deposit_cap": { - "description": "Deposit Cap (defined in terms of the asset)", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "deposit_enabled": { - "description": "If false cannot deposit", - "type": "boolean" - }, "indexes_last_updated": { "description": "Timestamp (seconds) where indexes and where last updated", "type": "integer", @@ -1404,22 +1232,6 @@ } ] }, - "liquidation_bonus": { - "description": "Bonus amount of collateral liquidator get when repaying user's debt (Will get collateral from user in an amount equal to debt repayed + bonus)", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, - "liquidation_threshold": { - "description": "Base asset amount in debt position per \"base asset\" of asset collateral that if surpassed makes the user's position liquidatable.", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, "liquidity_index": { "description": "Liquidity index (Used to compute deposit interest)", "allOf": [ @@ -1436,14 +1248,6 @@ } ] }, - "max_loan_to_value": { - "description": "Max base asset that can be borrowed per \"base asset\" collateral when using the asset as collateral", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, "reserve_factor": { "description": "Portion of the borrow rate that is kept as protocol rewards", "allOf": [ diff --git a/scripts/types/generated/mars-address-provider/MarsAddressProvider.types.ts b/scripts/types/generated/mars-address-provider/MarsAddressProvider.types.ts index 09c1df316..e568853d7 100644 --- a/scripts/types/generated/mars-address-provider/MarsAddressProvider.types.ts +++ b/scripts/types/generated/mars-address-provider/MarsAddressProvider.types.ts @@ -20,7 +20,7 @@ export type ExecuteMsg = update_owner: OwnerUpdate } export type MarsAddressType = - | ('incentives' | 'oracle' | 'red_bank' | 'rewards_collector') + | ('incentives' | 'oracle' | 'red_bank' | 'rewards_collector' | 'params') | 'protocol_admin' | 'fee_collector' | 'safety_fund' diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts index a29545a4b..d15791cb3 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts @@ -8,11 +8,11 @@ import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from '@cosmjs/cosmwasm-stargate' import { Coin, StdFee } from '@cosmjs/amino' import { - Decimal, InstantiateMsg, CreateOrUpdateConfig, ExecuteMsg, OwnerUpdate, + Decimal, Uint128, InitOrUpdateAssetParams, InterestRateModel, diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts index 394377bf0..7c4939e6e 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts @@ -9,11 +9,11 @@ import { UseQueryOptions, useQuery, useMutation, UseMutationOptions } from '@tan import { ExecuteResult } from '@cosmjs/cosmwasm-stargate' import { StdFee, Coin } from '@cosmjs/amino' import { - Decimal, InstantiateMsg, CreateOrUpdateConfig, ExecuteMsg, OwnerUpdate, + Decimal, Uint128, InitOrUpdateAssetParams, InterestRateModel, diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts index 10a39d40e..ad55cbf81 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts @@ -5,14 +5,12 @@ * and run the @cosmwasm/ts-codegen generate command to regenerate this file. */ -export type Decimal = string export interface InstantiateMsg { config: CreateOrUpdateConfig owner: string } export interface CreateOrUpdateConfig { address_provider?: string | null - close_factor?: Decimal | null } export type ExecuteMsg = | { @@ -94,15 +92,10 @@ export type OwnerUpdate = } } | 'clear_emergency_owner' +export type Decimal = string export type Uint128 = string export interface InitOrUpdateAssetParams { - borrow_enabled?: boolean | null - deposit_cap?: Uint128 | null - deposit_enabled?: boolean | null interest_rate_model?: InterestRateModel | null - liquidation_bonus?: Decimal | null - liquidation_threshold?: Decimal | null - max_loan_to_value?: Decimal | null reserve_factor?: Decimal | null } export interface InterestRateModel { @@ -196,27 +189,19 @@ export type QueryMsg = } export interface ConfigResponse { address_provider: string - close_factor: Decimal - emergency_owner?: string | null owner?: string | null proposed_new_owner?: string | null } export interface Market { - borrow_enabled: boolean borrow_index: Decimal borrow_rate: Decimal collateral_total_scaled: Uint128 debt_total_scaled: Uint128 denom: string - deposit_cap: Uint128 - deposit_enabled: boolean indexes_last_updated: number interest_rate_model: InterestRateModel - liquidation_bonus: Decimal - liquidation_threshold: Decimal liquidity_index: Decimal liquidity_rate: Decimal - max_loan_to_value: Decimal reserve_factor: Decimal } export type ArrayOfMarket = Market[] From 201183d607b9f7b057f31cfe3430d4a50b463174 Mon Sep 17 00:00:00 2001 From: Gabe Rodriguez Date: Thu, 29 Jun 2023 11:24:47 +0200 Subject: [PATCH 05/43] Params de-listing documentation (#224) --- contracts/params/README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/contracts/params/README.md b/contracts/params/README.md index ebb813f4e..00c8264b3 100644 --- a/contracts/params/README.md +++ b/contracts/params/README.md @@ -14,3 +14,21 @@ This contract holds the following values for all the assets in Mars Protocol: 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. +## High Levered Strategies (HLS) + +An HLS is a position where the borrowed asset is highly correlated to the collateral asset (e.g. atom debt -> stAtom collateral). +This has a low risk of liquidation. For this reason, Credit Manager grants higher MaxLTV & LiqThreshold parameters, +granting higher leverage. An asset's HLS parameters are stored in this contract and are applied to credit accounts +of the HLS type during a health check. + +### De-listing an HLS asset + +There are a few scenarios depending on what denom is being de-listed. Always communicate each step to the users! +- **De-listing a collateral denom**: + - Set the MaxLTV of the denom to zero. + - Gradually reduce the HLS Liquidation Threshold to zero. + - _Do not_ set HLS parameters to None or remove it from correlations list for debt denom. This would result in freezing the HLS accounts that have that collateral. +- **De-listing a debt denom**: + - Set the MaxLTV of all denoms in the debt denom's correlations list to zero. + - Gradually reduce the HLS Liquidation Threshold to zero. + - _Do not_ set HLS parameters to None. This would result in freezing the HLS accounts that have that debt denom. From b8914f5146de3801160de961b21bf4f542d36371 Mon Sep 17 00:00:00 2001 From: Gabe Rodriguez Date: Tue, 4 Jul 2023 08:37:50 +0200 Subject: [PATCH 06/43] MP-2840. Remove deposit 'on behalf of' [audit] (#230) --- contracts/red-bank/src/contract.rs | 6 +- contracts/red-bank/src/execute.rs | 12 +- contracts/red-bank/tests/test_deposit.rs | 128 +----- integration-tests/tests/test_oracles.rs | 20 +- packages/testing/src/integration/mock_env.rs | 4 +- packages/types/src/red_bank/msg.rs | 5 +- schemas/mars-red-bank/mars-red-bank.json | 9 - scripts/deploy/base/deployer.ts | 1 - scripts/package.json | 16 +- .../mars-red-bank/MarsRedBank.client.ts | 14 +- .../mars-red-bank/MarsRedBank.react-query.ts | 5 +- .../mars-red-bank/MarsRedBank.types.ts | 4 +- scripts/yarn.lock | 400 +++++++++--------- 13 files changed, 224 insertions(+), 400 deletions(-) diff --git a/contracts/red-bank/src/contract.rs b/contracts/red-bank/src/contract.rs index cbbb0687b..8ea37a7af 100644 --- a/contracts/red-bank/src/contract.rs +++ b/contracts/red-bank/src/contract.rs @@ -41,11 +41,9 @@ pub fn execute( let user_addr = deps.api.addr_validate(&user)?; execute::update_uncollateralized_loan_limit(deps, info, user_addr, denom, new_limit) } - ExecuteMsg::Deposit { - on_behalf_of, - } => { + ExecuteMsg::Deposit {} => { let sent_coin = cw_utils::one_coin(&info)?; - execute::deposit(deps, env, info, on_behalf_of, sent_coin.denom, sent_coin.amount) + execute::deposit(deps, env, info, sent_coin.denom, sent_coin.amount) } ExecuteMsg::Withdraw { denom, diff --git a/contracts/red-bank/src/execute.rs b/contracts/red-bank/src/execute.rs index 9138edb9a..89e0d7bcc 100644 --- a/contracts/red-bank/src/execute.rs +++ b/contracts/red-bank/src/execute.rs @@ -277,18 +277,9 @@ pub fn deposit( deps: DepsMut, env: Env, info: MessageInfo, - on_behalf_of: Option, denom: String, deposit_amount: Uint128, ) -> Result { - let user_addr: Addr; - let user = if let Some(address) = on_behalf_of { - user_addr = deps.api.addr_validate(&address)?; - User(&user_addr) - } else { - User(&info.sender) - }; - let mut market = MARKETS.load(deps.storage, &denom)?; let config = CONFIG.load(deps.storage)?; @@ -341,7 +332,7 @@ pub fn deposit( let deposit_amount_scaled = get_scaled_liquidity_amount(deposit_amount, &market, env.block.time.seconds())?; - response = user.increase_collateral( + response = User(&info.sender).increase_collateral( deps.storage, &market, deposit_amount_scaled, @@ -358,7 +349,6 @@ pub fn deposit( Ok(response .add_attribute("action", "deposit") .add_attribute("sender", &info.sender) - .add_attribute("on_behalf_of", user) .add_attribute("denom", denom) .add_attribute("amount", deposit_amount) .add_attribute("amount_scaled", deposit_amount_scaled)) diff --git a/contracts/red-bank/tests/test_deposit.rs b/contracts/red-bank/tests/test_deposit.rs index 9a2b51619..05607b90b 100644 --- a/contracts/red-bank/tests/test_deposit.rs +++ b/contracts/red-bank/tests/test_deposit.rs @@ -96,9 +96,7 @@ fn depositing_with_no_coin_sent() { deps.as_mut(), mock_env(), mock_info(depositor_addr.as_str(), &[]), - ExecuteMsg::Deposit { - on_behalf_of: None, - }, + ExecuteMsg::Deposit {}, ) .unwrap_err(); assert_eq!(err, PaymentError::NoFunds {}.into()); @@ -118,9 +116,7 @@ fn depositing_with_multiple_coins_sent() { deps.as_mut(), mock_env(), mock_info(depositor_addr.as_str(), &sent_coins), - ExecuteMsg::Deposit { - on_behalf_of: None, - }, + ExecuteMsg::Deposit {}, ) .unwrap_err(); assert_eq!(err, PaymentError::MultipleDenoms {}.into()); @@ -141,9 +137,7 @@ fn depositing_to_non_existent_market() { deps.as_mut(), mock_env(), mock_info(depositor_addr.as_str(), &coins(123, false_denom)), - ExecuteMsg::Deposit { - on_behalf_of: None, - }, + ExecuteMsg::Deposit {}, ) .unwrap_err(); assert_eq!(err, StdError::not_found(type_name::()).into()); @@ -182,9 +176,7 @@ fn depositing_to_disabled_market() { deps.as_mut(), mock_env(), mock_info(depositor_addr.as_str(), &coins(123, denom)), - ExecuteMsg::Deposit { - on_behalf_of: None, - }, + ExecuteMsg::Deposit {}, ) .unwrap_err(); assert_eq!( @@ -236,9 +228,7 @@ fn depositing_above_cap() { deps.as_mut(), mock_env_at_block_time(10000100), mock_info(depositor_addr.as_str(), &coins(1_000_001, denom)), - ExecuteMsg::Deposit { - on_behalf_of: None, - }, + ExecuteMsg::Deposit {}, ) .unwrap_err(); assert_eq!( @@ -253,9 +243,7 @@ fn depositing_above_cap() { deps.as_mut(), mock_env_at_block_time(10000100), mock_info(depositor_addr.as_str(), &coins(123, denom)), - ExecuteMsg::Deposit { - on_behalf_of: None, - }, + ExecuteMsg::Deposit {}, ); assert!(result.is_ok()); } @@ -286,9 +274,7 @@ fn depositing_without_existing_position() { deps.as_mut(), mock_env_at_block_time(block_time), mock_info(depositor_addr.as_str(), &coins(deposit_amount, denom)), - ExecuteMsg::Deposit { - on_behalf_of: None, - }, + ExecuteMsg::Deposit {}, ) .unwrap(); @@ -315,7 +301,6 @@ fn depositing_without_existing_position() { vec![ attr("action", "deposit"), attr("sender", &depositor_addr), - attr("on_behalf_of", &depositor_addr), attr("denom", denom), attr("amount", deposit_amount.to_string()), attr("amount_scaled", expected_mint_amount), @@ -376,9 +361,7 @@ fn depositing_with_existing_position() { deps.as_mut(), mock_env_at_block_time(block_time), mock_info(depositor_addr.as_str(), &coins(deposit_amount, denom)), - ExecuteMsg::Deposit { - on_behalf_of: None, - }, + ExecuteMsg::Deposit {}, ) .unwrap(); @@ -414,93 +397,6 @@ fn depositing_with_existing_position() { ); } -#[test] -fn depositing_on_behalf_of() { - let TestSuite { - mut deps, - denom, - depositor_addr, - initial_market, - } = setup_test(); - - let deposit_amount = 123456u128; - let on_behalf_of_addr = Addr::unchecked("jake"); - - // compute expected market parameters - let block_time = 10000300; - let expected_params = - th_get_expected_indices_and_rates(&initial_market, block_time, Default::default()); - let expected_mint_amount = compute_scaled_amount( - Uint128::from(deposit_amount), - expected_params.liquidity_index, - ScalingOperation::Truncate, - ) - .unwrap(); - let expected_reward_amount_scaled = compute_scaled_amount( - expected_params.protocol_rewards_to_distribute, - expected_params.liquidity_index, - ScalingOperation::Truncate, - ) - .unwrap(); - - let res = execute( - deps.as_mut(), - mock_env_at_block_time(block_time), - mock_info(depositor_addr.as_str(), &coins(deposit_amount, denom)), - ExecuteMsg::Deposit { - on_behalf_of: Some(on_behalf_of_addr.clone().into()), - }, - ) - .unwrap(); - - // NOTE: For this test, the accrued protocol reward is non-zero, so we do expect a message to - // update the index of the rewards collector. - assert_eq!( - res.messages, - vec![ - SubMsg::new(WasmMsg::Execute { - contract_addr: MarsAddressType::Incentives.to_string(), - msg: to_binary(&incentives::ExecuteMsg::BalanceChange { - user_addr: Addr::unchecked(MarsAddressType::RewardsCollector.to_string()), - denom: initial_market.denom.clone(), - user_amount_scaled_before: Uint128::zero(), - total_amount_scaled_before: initial_market.collateral_total_scaled, - }) - .unwrap(), - funds: vec![] - }), - SubMsg::new(WasmMsg::Execute { - contract_addr: MarsAddressType::Incentives.to_string(), - msg: to_binary(&incentives::ExecuteMsg::BalanceChange { - user_addr: on_behalf_of_addr.clone(), - denom: initial_market.denom.clone(), - user_amount_scaled_before: Uint128::zero(), - // NOTE: New collateral shares were minted to the rewards collector first, so - // for the depositor this should be initial total supply + rewards shares minted - total_amount_scaled_before: initial_market.collateral_total_scaled - + expected_reward_amount_scaled, - }) - .unwrap(), - funds: vec![] - }) - ] - ); - - // depositor should not have created a new collateral position - let opt = COLLATERALS.may_load(deps.as_ref().storage, (&depositor_addr, denom)).unwrap(); - assert!(opt.is_none()); - - // the recipient should have created a new collateral position - let collateral = COLLATERALS.load(deps.as_ref().storage, (&on_behalf_of_addr, denom)).unwrap(); - assert_eq!( - collateral, - Collateral { - amount_scaled: expected_mint_amount, - enabled: true, - } - ); -} - #[test] fn depositing_on_behalf_of_cannot_enable_collateral() { let TestSuite { @@ -521,9 +417,7 @@ fn depositing_on_behalf_of_cannot_enable_collateral() { deps.as_mut(), mock_env_at_block_time(block_time), mock_info(on_behalf_of_addr.as_str(), &coins(1u128, denom)), - ExecuteMsg::Deposit { - on_behalf_of: None, - }, + ExecuteMsg::Deposit {}, ) .unwrap(); @@ -552,9 +446,7 @@ fn depositing_on_behalf_of_cannot_enable_collateral() { deps.as_mut(), mock_env_at_block_time(block_time), mock_info(depositor_addr.as_str(), &coins(1u128, denom)), - ExecuteMsg::Deposit { - on_behalf_of: Some(on_behalf_of_addr.to_string()), - }, + ExecuteMsg::Deposit {}, ) .unwrap(); diff --git a/integration-tests/tests/test_oracles.rs b/integration-tests/tests/test_oracles.rs index 6281f5291..5c4804051 100644 --- a/integration-tests/tests/test_oracles.rs +++ b/integration-tests/tests/test_oracles.rs @@ -949,15 +949,7 @@ fn redbank_should_fail_if_no_price() { ) .unwrap(); - wasm.execute( - &red_bank_addr, - &Deposit { - on_behalf_of: None, - }, - &[coin(1_000_000, "uatom")], - depositor, - ) - .unwrap(); + wasm.execute(&red_bank_addr, &Deposit {}, &[coin(1_000_000, "uatom")], depositor).unwrap(); // execute msg should fail since it is attempting to query an asset from the oracle contract that doesn't have an LP pool set up wasm.execute( @@ -1011,15 +1003,7 @@ fn redbank_quering_oracle_successfully() { ) .unwrap(); - wasm.execute( - &red_bank_addr, - &Deposit { - on_behalf_of: None, - }, - &[coin(1_000_000, "uatom")], - depositor, - ) - .unwrap(); + wasm.execute(&red_bank_addr, &Deposit {}, &[coin(1_000_000, "uatom")], depositor).unwrap(); wasm.execute( &red_bank_addr, diff --git a/packages/testing/src/integration/mock_env.rs b/packages/testing/src/integration/mock_env.rs index 6784b5e11..5d1723ebc 100644 --- a/packages/testing/src/integration/mock_env.rs +++ b/packages/testing/src/integration/mock_env.rs @@ -228,9 +228,7 @@ impl RedBank { env.app.execute_contract( sender.clone(), self.contract_addr.clone(), - &red_bank::ExecuteMsg::Deposit { - on_behalf_of: None, - }, + &red_bank::ExecuteMsg::Deposit {}, &[coin], ) } diff --git a/packages/types/src/red_bank/msg.rs b/packages/types/src/red_bank/msg.rs index 4e051d070..12359b4c3 100644 --- a/packages/types/src/red_bank/msg.rs +++ b/packages/types/src/red_bank/msg.rs @@ -53,10 +53,7 @@ pub enum ExecuteMsg { /// Deposit native coins. Deposited coins must be sent in the transaction /// this call is made - Deposit { - /// Address that will receive the coins - on_behalf_of: Option, - }, + Deposit {}, /// Withdraw native coins Withdraw { diff --git a/schemas/mars-red-bank/mars-red-bank.json b/schemas/mars-red-bank/mars-red-bank.json index 114365ce5..a42ba09b7 100644 --- a/schemas/mars-red-bank/mars-red-bank.json +++ b/schemas/mars-red-bank/mars-red-bank.json @@ -189,15 +189,6 @@ "properties": { "deposit": { "type": "object", - "properties": { - "on_behalf_of": { - "description": "Address that will receive the coins", - "type": [ - "string", - "null" - ] - } - }, "additionalProperties": false } }, diff --git a/scripts/deploy/base/deployer.ts b/scripts/deploy/base/deployer.ts index 8f925e110..789f0fe5a 100644 --- a/scripts/deploy/base/deployer.ts +++ b/scripts/deploy/base/deployer.ts @@ -119,7 +119,6 @@ export class Deployer { owner: this.deployerAddress, config: { address_provider: this.storage.addresses['address-provider']!, - close_factor: '0.5', }, } await this.instantiate('red-bank', this.storage.codeIds['red-bank']!, msg) diff --git a/scripts/package.json b/scripts/package.json index 12f8eab99..15a31bb6f 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -16,9 +16,9 @@ "format-check": "prettier --ignore-path .gitignore --check ." }, "dependencies": { - "@cosmjs/cosmwasm-stargate": "^0.30.1", - "@cosmjs/proto-signing": "^0.30.1", - "@cosmjs/stargate": "^0.30.1", + "@cosmjs/cosmwasm-stargate": "^0.31.0", + "@cosmjs/proto-signing": "^0.31.0", + "@cosmjs/stargate": "^0.31.0", "@cosmwasm/ts-codegen": "^0.30.1", "chalk": "4.1.2", "cosmjs-types": "^0.8.0", @@ -26,13 +26,13 @@ "ts-codegen": "^0.0.0" }, "devDependencies": { - "@types/node": "^20.2.5", - "@typescript-eslint/eslint-plugin": "^5.59.9", - "@typescript-eslint/parser": "^5.59.9", + "@types/node": "^20.3.3", + "@typescript-eslint/eslint-plugin": "^5.61.0", + "@typescript-eslint/parser": "^5.61.0", "cosmjs-types": "^0.8.0", - "eslint": "^8.42.0", + "eslint": "^8.44.0", "eslint-config-prettier": "^8.8.0", "prettier": "^2.8.8", - "typescript": "^5.1.3" + "typescript": "^5.1.6" } } diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts index d15791cb3..e603cb6ef 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts @@ -364,11 +364,6 @@ export interface MarsRedBankInterface extends MarsRedBankReadOnlyInterface { _funds?: Coin[], ) => Promise deposit: ( - { - onBehalfOf, - }: { - onBehalfOf?: string - }, fee?: number | StdFee | 'auto', memo?: string, _funds?: Coin[], @@ -583,11 +578,6 @@ export class MarsRedBankClient extends MarsRedBankQueryClient implements MarsRed ) } deposit = async ( - { - onBehalfOf, - }: { - onBehalfOf?: string - }, fee: number | StdFee | 'auto' = 'auto', memo?: string, _funds?: Coin[], @@ -596,9 +586,7 @@ export class MarsRedBankClient extends MarsRedBankQueryClient implements MarsRed this.sender, this.contractAddress, { - deposit: { - on_behalf_of: onBehalfOf, - }, + deposit: {}, }, fee, memo, diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts index 7c4939e6e..8ba59b9b5 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts @@ -559,9 +559,6 @@ export function useMarsRedBankWithdrawMutation( } export interface MarsRedBankDepositMutation { client: MarsRedBankClient - msg: { - onBehalfOf?: string - } args?: { fee?: number | StdFee | 'auto' memo?: string @@ -575,7 +572,7 @@ export function useMarsRedBankDepositMutation( >, ) { return useMutation( - ({ client, msg, args: { fee, memo, funds } = {} }) => client.deposit(msg, fee, memo, funds), + ({ client, args: { fee, memo, funds } = {} }) => client.deposit(fee, memo, funds), options, ) } diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts index ad55cbf81..54fc21ed7 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts @@ -41,9 +41,7 @@ export type ExecuteMsg = } } | { - deposit: { - on_behalf_of?: string | null - } + deposit: {} } | { withdraw: { diff --git a/scripts/yarn.lock b/scripts/yarn.lock index ecbfacbcf..18c9665e7 100644 --- a/scripts/yarn.lock +++ b/scripts/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@aashutoshrathi/word-wrap@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" + integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== + "@ampproject/remapping@^2.1.0", "@ampproject/remapping@^2.2.0": version "2.2.1" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" @@ -1054,138 +1059,138 @@ "@noble/hashes" "^1.0.0" protobufjs "^6.8.8" -"@cosmjs/amino@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/amino/-/amino-0.30.1.tgz#7c18c14627361ba6c88e3495700ceea1f76baace" - integrity sha512-yNHnzmvAlkETDYIpeCTdVqgvrdt1qgkOXwuRVi8s27UKI5hfqyE9fJ/fuunXE6ZZPnKkjIecDznmuUOMrMvw4w== - dependencies: - "@cosmjs/crypto" "^0.30.1" - "@cosmjs/encoding" "^0.30.1" - "@cosmjs/math" "^0.30.1" - "@cosmjs/utils" "^0.30.1" - -"@cosmjs/cosmwasm-stargate@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/cosmwasm-stargate/-/cosmwasm-stargate-0.30.1.tgz#6f9ca310f75433a3e30d683bc6aa24eadb345d79" - integrity sha512-W/6SLUCJAJGBN+sJLXouLZikVgmqDd9LCdlMzQaxczcCHTWeJAmRvOiZGSZaSy3shw/JN1qc6g6PKpvTVgj10A== - dependencies: - "@cosmjs/amino" "^0.30.1" - "@cosmjs/crypto" "^0.30.1" - "@cosmjs/encoding" "^0.30.1" - "@cosmjs/math" "^0.30.1" - "@cosmjs/proto-signing" "^0.30.1" - "@cosmjs/stargate" "^0.30.1" - "@cosmjs/tendermint-rpc" "^0.30.1" - "@cosmjs/utils" "^0.30.1" - cosmjs-types "^0.7.1" +"@cosmjs/amino@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/amino/-/amino-0.31.0.tgz#49b33047295002804ad51bdf7ec0c2c97f1b553d" + integrity sha512-xJ5CCEK7H79FTpOuEmlpSzVI+ZeYESTVvO3wHDgbnceIyAne3C68SvyaKqLUR4uJB0Z4q4+DZHbqW6itUiv4lA== + dependencies: + "@cosmjs/crypto" "^0.31.0" + "@cosmjs/encoding" "^0.31.0" + "@cosmjs/math" "^0.31.0" + "@cosmjs/utils" "^0.31.0" + +"@cosmjs/cosmwasm-stargate@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/cosmwasm-stargate/-/cosmwasm-stargate-0.31.0.tgz#a9ea82471ca035b8d7f6ae640ad44b5f497be8c6" + integrity sha512-l6aX++3LhaAGZO46qIgrrNF40lYhOrdPfl35Z32ks6Wf3mwgbQEZwaxnoGzwUePY7/yaIiEFJ1JO6MlVPZVuag== + dependencies: + "@cosmjs/amino" "^0.31.0" + "@cosmjs/crypto" "^0.31.0" + "@cosmjs/encoding" "^0.31.0" + "@cosmjs/math" "^0.31.0" + "@cosmjs/proto-signing" "^0.31.0" + "@cosmjs/stargate" "^0.31.0" + "@cosmjs/tendermint-rpc" "^0.31.0" + "@cosmjs/utils" "^0.31.0" + cosmjs-types "^0.8.0" long "^4.0.0" pako "^2.0.2" -"@cosmjs/crypto@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/crypto/-/crypto-0.30.1.tgz#21e94d5ca8f8ded16eee1389d2639cb5c43c3eb5" - integrity sha512-rAljUlake3MSXs9xAm87mu34GfBLN0h/1uPPV6jEwClWjNkAMotzjC0ab9MARy5FFAvYHL3lWb57bhkbt2GtzQ== +"@cosmjs/crypto@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/crypto/-/crypto-0.31.0.tgz#0be3867ada0155da19c45a51f5fde08e84f9ec4b" + integrity sha512-UaqCe6Tgh0pe1QlZ66E13t6FlIF86QrnBXXq+EN7Xe1Rouza3fJ1ojGlPleJZkBoq3tAyYVIOOqdZIxtVj/sIQ== dependencies: - "@cosmjs/encoding" "^0.30.1" - "@cosmjs/math" "^0.30.1" - "@cosmjs/utils" "^0.30.1" + "@cosmjs/encoding" "^0.31.0" + "@cosmjs/math" "^0.31.0" + "@cosmjs/utils" "^0.31.0" "@noble/hashes" "^1" bn.js "^5.2.0" elliptic "^6.5.4" - libsodium-wrappers "^0.7.6" + libsodium-wrappers-sumo "^0.7.11" -"@cosmjs/encoding@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/encoding/-/encoding-0.30.1.tgz#b5c4e0ef7ceb1f2753688eb96400ed70f35c6058" - integrity sha512-rXmrTbgqwihORwJ3xYhIgQFfMSrwLu1s43RIK9I8EBudPx3KmnmyAKzMOVsRDo9edLFNuZ9GIvysUCwQfq3WlQ== +"@cosmjs/encoding@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/encoding/-/encoding-0.31.0.tgz#9a6fd80b59c35fc20638a6436128ad0be681eafc" + integrity sha512-NYGQDRxT7MIRSlcbAezwxK0FqnaSPKCH7O32cmfpHNWorFxhy9lwmBoCvoe59Kd0HmArI4h+NGzLEfX3OLnA4Q== dependencies: base64-js "^1.3.0" bech32 "^1.1.4" readonly-date "^1.0.0" -"@cosmjs/json-rpc@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/json-rpc/-/json-rpc-0.30.1.tgz#16f21305fc167598c8a23a45549b85106b2372bc" - integrity sha512-pitfC/2YN9t+kXZCbNuyrZ6M8abnCC2n62m+JtU9vQUfaEtVsgy+1Fk4TRQ175+pIWSdBMFi2wT8FWVEE4RhxQ== +"@cosmjs/json-rpc@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/json-rpc/-/json-rpc-0.31.0.tgz#38fda21188f2046db4a111fb5463ccde3c3751d7" + integrity sha512-Ix2Cil2qysiLNrX+E0w3vtwCrqxGVq8jklpLA7B2vtMrw7tru/rS65fdFSy8ep0wUNLL6Ud32VXa5K0YObDOMA== dependencies: - "@cosmjs/stream" "^0.30.1" + "@cosmjs/stream" "^0.31.0" xstream "^11.14.0" -"@cosmjs/math@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/math/-/math-0.30.1.tgz#8b816ef4de5d3afa66cb9fdfb5df2357a7845b8a" - integrity sha512-yaoeI23pin9ZiPHIisa6qqLngfnBR/25tSaWpkTm8Cy10MX70UF5oN4+/t1heLaM6SSmRrhk3psRkV4+7mH51Q== +"@cosmjs/math@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/math/-/math-0.31.0.tgz#c9fc5f8191df7c2375945d2eacce327dfbf26414" + integrity sha512-Sb/8Ry/+gKJaYiV6X8q45kxXC9FoV98XCY1WXtu0JQwOi61VCG2VXsURQnVvZ/EhR/CuT/swOlNKrqEs3da0fw== dependencies: bn.js "^5.2.0" -"@cosmjs/proto-signing@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/proto-signing/-/proto-signing-0.30.1.tgz#f0dda372488df9cd2677150b89b3e9c72b3cb713" - integrity sha512-tXh8pPYXV4aiJVhTKHGyeZekjj+K9s2KKojMB93Gcob2DxUjfKapFYBMJSgfKPuWUPEmyr8Q9km2hplI38ILgQ== - dependencies: - "@cosmjs/amino" "^0.30.1" - "@cosmjs/crypto" "^0.30.1" - "@cosmjs/encoding" "^0.30.1" - "@cosmjs/math" "^0.30.1" - "@cosmjs/utils" "^0.30.1" - cosmjs-types "^0.7.1" +"@cosmjs/proto-signing@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/proto-signing/-/proto-signing-0.31.0.tgz#7056963457cd967f53f56c2ab4491638e5ade2c0" + integrity sha512-JNlyOJRkn8EKB9mCthkjr6lVX6eyVQ09PFdmB4/DR874E62dFTvQ+YvyKMAgN7K7Dcjj26dVlAD3f6Xs7YOGDg== + dependencies: + "@cosmjs/amino" "^0.31.0" + "@cosmjs/crypto" "^0.31.0" + "@cosmjs/encoding" "^0.31.0" + "@cosmjs/math" "^0.31.0" + "@cosmjs/utils" "^0.31.0" + cosmjs-types "^0.8.0" long "^4.0.0" -"@cosmjs/socket@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/socket/-/socket-0.30.1.tgz#00b22f4b5e2ab01f4d82ccdb7b2e59536bfe5ce0" - integrity sha512-r6MpDL+9N+qOS/D5VaxnPaMJ3flwQ36G+vPvYJsXArj93BjgyFB7BwWwXCQDzZ+23cfChPUfhbINOenr8N2Kow== +"@cosmjs/socket@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/socket/-/socket-0.31.0.tgz#ffcae48251a68b4a1c37a1c996d8b123cd8ad5ac" + integrity sha512-WDh9gTyiP3OCXvSAJJn33+Ef3XqMWag+bpR1TdMBxTmlTxuvU+kPy4cf6P2OF+jkkUBEA5Se2EAju0eFbJMT+w== dependencies: - "@cosmjs/stream" "^0.30.1" + "@cosmjs/stream" "^0.31.0" isomorphic-ws "^4.0.1" ws "^7" xstream "^11.14.0" -"@cosmjs/stargate@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/stargate/-/stargate-0.30.1.tgz#e1b22e1226cffc6e93914a410755f1f61057ba04" - integrity sha512-RdbYKZCGOH8gWebO7r6WvNnQMxHrNXInY/gPHPzMjbQF6UatA6fNM2G2tdgS5j5u7FTqlCI10stNXrknaNdzog== +"@cosmjs/stargate@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/stargate/-/stargate-0.31.0.tgz#a7df1eaf1363513529607abaa52a5045aaaee0fd" + integrity sha512-GYhk9lzZPj/QmYHC0VV/4AMoRzVcOP+EnB1YZCoWlBdLuVmpBYKRagJqWIrIwdk1E0gF2ZoESd2TYfdh1fqIpg== dependencies: "@confio/ics23" "^0.6.8" - "@cosmjs/amino" "^0.30.1" - "@cosmjs/encoding" "^0.30.1" - "@cosmjs/math" "^0.30.1" - "@cosmjs/proto-signing" "^0.30.1" - "@cosmjs/stream" "^0.30.1" - "@cosmjs/tendermint-rpc" "^0.30.1" - "@cosmjs/utils" "^0.30.1" - cosmjs-types "^0.7.1" + "@cosmjs/amino" "^0.31.0" + "@cosmjs/encoding" "^0.31.0" + "@cosmjs/math" "^0.31.0" + "@cosmjs/proto-signing" "^0.31.0" + "@cosmjs/stream" "^0.31.0" + "@cosmjs/tendermint-rpc" "^0.31.0" + "@cosmjs/utils" "^0.31.0" + cosmjs-types "^0.8.0" long "^4.0.0" protobufjs "~6.11.3" xstream "^11.14.0" -"@cosmjs/stream@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/stream/-/stream-0.30.1.tgz#ba038a2aaf41343696b1e6e759d8e03a9516ec1a" - integrity sha512-Fg0pWz1zXQdoxQZpdHRMGvUH5RqS6tPv+j9Eh7Q953UjMlrwZVo0YFLC8OTf/HKVf10E4i0u6aM8D69Q6cNkgQ== +"@cosmjs/stream@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/stream/-/stream-0.31.0.tgz#7faf0f5ccd5ceffdd3b5d9fb81e292bb7a930b2c" + integrity sha512-Y+aSHwhHkLGIaQOdqRob+yga2zr9ifl9gZDKD+B7+R5pdWN5f2TTDhYWxA6YZcZ6xRmfr7u8a7tDh7iYLC/zKA== dependencies: xstream "^11.14.0" -"@cosmjs/tendermint-rpc@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/tendermint-rpc/-/tendermint-rpc-0.30.1.tgz#c16378892ba1ac63f72803fdf7567eab9d4f0aa0" - integrity sha512-Z3nCwhXSbPZJ++v85zHObeUggrEHVfm1u18ZRwXxFE9ZMl5mXTybnwYhczuYOl7KRskgwlB+rID0WYACxj4wdQ== - dependencies: - "@cosmjs/crypto" "^0.30.1" - "@cosmjs/encoding" "^0.30.1" - "@cosmjs/json-rpc" "^0.30.1" - "@cosmjs/math" "^0.30.1" - "@cosmjs/socket" "^0.30.1" - "@cosmjs/stream" "^0.30.1" - "@cosmjs/utils" "^0.30.1" +"@cosmjs/tendermint-rpc@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/tendermint-rpc/-/tendermint-rpc-0.31.0.tgz#df82f634ff08fc377dfdccea43a31d92b5b0eaf1" + integrity sha512-yo9xbeuI6UoEKIhFZ9g0dvUKLqnBzwdpEc/uldQygQc51j38gQVwFko+6sjmhieJqRYYvrYumcbJMiV6GFM9aA== + dependencies: + "@cosmjs/crypto" "^0.31.0" + "@cosmjs/encoding" "^0.31.0" + "@cosmjs/json-rpc" "^0.31.0" + "@cosmjs/math" "^0.31.0" + "@cosmjs/socket" "^0.31.0" + "@cosmjs/stream" "^0.31.0" + "@cosmjs/utils" "^0.31.0" axios "^0.21.2" readonly-date "^1.0.0" xstream "^11.14.0" -"@cosmjs/utils@^0.30.1": - version "0.30.1" - resolved "https://registry.yarnpkg.com/@cosmjs/utils/-/utils-0.30.1.tgz#6d92582341be3c2ec8d82090253cfa4b7f959edb" - integrity sha512-KvvX58MGMWh7xA+N+deCfunkA/ZNDvFLw4YbOmX3f/XBIkqrVY7qlotfy2aNb1kgp6h4B6Yc8YawJPDTfvWX7g== +"@cosmjs/utils@^0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@cosmjs/utils/-/utils-0.31.0.tgz#3a7ac16856dcff63bbf1bb11e31f975f71ef4f21" + integrity sha512-nNcycZWUYLNJlrIXgpcgVRqdl6BXjF4YlXdxobQWpW9Tikk61bEGeAFhDYtC0PwHlokCNw0KxWiHGJL4nL7Q5A== "@cosmwasm/ts-codegen@^0.30.1": version "0.30.1" @@ -1232,14 +1237,14 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz#cdd35dce4fa1a89a4fd42b1599eb35b3af408884" integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ== -"@eslint/eslintrc@^2.0.3": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.3.tgz#4910db5505f4d503f27774bf356e3704818a0331" - integrity sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ== +"@eslint/eslintrc@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.0.tgz#82256f164cc9e0b59669efc19d57f8092706841d" + integrity sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.5.2" + espree "^9.6.0" globals "^13.19.0" ignore "^5.2.0" import-fresh "^3.2.1" @@ -1247,10 +1252,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.42.0": - version "8.42.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.42.0.tgz#484a1d638de2911e6f5a30c12f49c7e4a3270fb6" - integrity sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw== +"@eslint/js@8.44.0": + version "8.44.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.44.0.tgz#961a5903c74139390478bdc808bcde3fc45ab7af" + integrity sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw== "@humanwhocodes/config-array@^0.11.10": version "0.11.10" @@ -1536,11 +1541,16 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== -"@types/node@*", "@types/node@>=13.7.0", "@types/node@^20.2.5": +"@types/node@*", "@types/node@>=13.7.0": version "20.3.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.0.tgz#719498898d5defab83c3560f45d8498f58d11938" integrity sha512-cumHmIAf6On83X7yP+LrsEyUOf/YlociZelmpRYaGFydoaPdxdt80MAbu6vWerQT2COCp2nPvHdsbD7tHn/YlQ== +"@types/node@^20.3.3": + version "20.3.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.3.tgz#329842940042d2b280897150e023e604d11657d6" + integrity sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw== + "@types/prettier@^2.6.1": version "2.7.3" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.3.tgz#3e51a17e291d01d17d3fc61422015a933af7a08f" @@ -1563,88 +1573,88 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^5.59.9": - version "5.59.9" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.9.tgz#2604cfaf2b306e120044f901e20c8ed926debf15" - integrity sha512-4uQIBq1ffXd2YvF7MAvehWKW3zVv/w+mSfRAu+8cKbfj3nwzyqJLNcZJpQ/WZ1HLbJDiowwmQ6NO+63nCA+fqA== +"@typescript-eslint/eslint-plugin@^5.61.0": + version "5.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.61.0.tgz#a1a5290cf33863b4db3fb79350b3c5275a7b1223" + integrity sha512-A5l/eUAug103qtkwccSCxn8ZRwT+7RXWkFECdA4Cvl1dOlDUgTpAOfSEElZn2uSUxhdDpnCdetrf0jvU4qrL+g== dependencies: "@eslint-community/regexpp" "^4.4.0" - "@typescript-eslint/scope-manager" "5.59.9" - "@typescript-eslint/type-utils" "5.59.9" - "@typescript-eslint/utils" "5.59.9" + "@typescript-eslint/scope-manager" "5.61.0" + "@typescript-eslint/type-utils" "5.61.0" + "@typescript-eslint/utils" "5.61.0" debug "^4.3.4" - grapheme-splitter "^1.0.4" + graphemer "^1.4.0" ignore "^5.2.0" natural-compare-lite "^1.4.0" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/parser@^5.59.9": - version "5.59.9" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.9.tgz#a85c47ccdd7e285697463da15200f9a8561dd5fa" - integrity sha512-FsPkRvBtcLQ/eVK1ivDiNYBjn3TGJdXy2fhXX+rc7czWl4ARwnpArwbihSOHI2Peg9WbtGHrbThfBUkZZGTtvQ== +"@typescript-eslint/parser@^5.61.0": + version "5.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.61.0.tgz#7fbe3e2951904bb843f8932ebedd6e0635bffb70" + integrity sha512-yGr4Sgyh8uO6fSi9hw3jAFXNBHbCtKKFMdX2IkT3ZqpKmtAq3lHS4ixB/COFuAIJpwl9/AqF7j72ZDWYKmIfvg== dependencies: - "@typescript-eslint/scope-manager" "5.59.9" - "@typescript-eslint/types" "5.59.9" - "@typescript-eslint/typescript-estree" "5.59.9" + "@typescript-eslint/scope-manager" "5.61.0" + "@typescript-eslint/types" "5.61.0" + "@typescript-eslint/typescript-estree" "5.61.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.59.9": - version "5.59.9" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.9.tgz#eadce1f2733389cdb58c49770192c0f95470d2f4" - integrity sha512-8RA+E+w78z1+2dzvK/tGZ2cpGigBZ58VMEHDZtpE1v+LLjzrYGc8mMaTONSxKyEkz3IuXFM0IqYiGHlCsmlZxQ== +"@typescript-eslint/scope-manager@5.61.0": + version "5.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.61.0.tgz#b670006d069c9abe6415c41f754b1b5d949ef2b2" + integrity sha512-W8VoMjoSg7f7nqAROEmTt6LoBpn81AegP7uKhhW5KzYlehs8VV0ZW0fIDVbcZRcaP3aPSW+JZFua+ysQN+m/Nw== dependencies: - "@typescript-eslint/types" "5.59.9" - "@typescript-eslint/visitor-keys" "5.59.9" + "@typescript-eslint/types" "5.61.0" + "@typescript-eslint/visitor-keys" "5.61.0" -"@typescript-eslint/type-utils@5.59.9": - version "5.59.9" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.9.tgz#53bfaae2e901e6ac637ab0536d1754dfef4dafc2" - integrity sha512-ksEsT0/mEHg9e3qZu98AlSrONAQtrSTljL3ow9CGej8eRo7pe+yaC/mvTjptp23Xo/xIf2mLZKC6KPv4Sji26Q== +"@typescript-eslint/type-utils@5.61.0": + version "5.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.61.0.tgz#e90799eb2045c4435ea8378cb31cd8a9fddca47a" + integrity sha512-kk8u//r+oVK2Aj3ph/26XdH0pbAkC2RiSjUYhKD+PExemG4XSjpGFeyZ/QM8lBOa7O8aGOU+/yEbMJgQv/DnCg== dependencies: - "@typescript-eslint/typescript-estree" "5.59.9" - "@typescript-eslint/utils" "5.59.9" + "@typescript-eslint/typescript-estree" "5.61.0" + "@typescript-eslint/utils" "5.61.0" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.59.9": - version "5.59.9" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.9.tgz#3b4e7ae63718ce1b966e0ae620adc4099a6dcc52" - integrity sha512-uW8H5NRgTVneSVTfiCVffBb8AbwWSKg7qcA4Ot3JI3MPCJGsB4Db4BhvAODIIYE5mNj7Q+VJkK7JxmRhk2Lyjw== +"@typescript-eslint/types@5.61.0": + version "5.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.61.0.tgz#e99ff11b5792d791554abab0f0370936d8ca50c0" + integrity sha512-ldyueo58KjngXpzloHUog/h9REmHl59G1b3a5Sng1GfBo14BkS3ZbMEb3693gnP1k//97lh7bKsp6/V/0v1veQ== -"@typescript-eslint/typescript-estree@5.59.9": - version "5.59.9" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.9.tgz#6bfea844e468427b5e72034d33c9fffc9557392b" - integrity sha512-pmM0/VQ7kUhd1QyIxgS+aRvMgw+ZljB3eDb+jYyp6d2bC0mQWLzUDF+DLwCTkQ3tlNyVsvZRXjFyV0LkU/aXjA== +"@typescript-eslint/typescript-estree@5.61.0": + version "5.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.61.0.tgz#4c7caca84ce95bb41aa585d46a764bcc050b92f3" + integrity sha512-Fud90PxONnnLZ36oR5ClJBLTLfU4pIWBmnvGwTbEa2cXIqj70AEDEmOmpkFComjBZ/037ueKrOdHuYmSFVD7Rw== dependencies: - "@typescript-eslint/types" "5.59.9" - "@typescript-eslint/visitor-keys" "5.59.9" + "@typescript-eslint/types" "5.61.0" + "@typescript-eslint/visitor-keys" "5.61.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.59.9": - version "5.59.9" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.9.tgz#adee890107b5ffe02cd46fdaa6c2125fb3c6c7c4" - integrity sha512-1PuMYsju/38I5Ggblaeb98TOoUvjhRvLpLa1DoTOFaLWqaXl/1iQ1eGurTXgBY58NUdtfTXKP5xBq7q9NDaLKg== +"@typescript-eslint/utils@5.61.0": + version "5.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.61.0.tgz#5064838a53e91c754fffbddd306adcca3fe0af36" + integrity sha512-mV6O+6VgQmVE6+xzlA91xifndPW9ElFW8vbSF0xCT/czPXVhwDewKila1jOyRwa9AE19zKnrr7Cg5S3pJVrTWQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.59.9" - "@typescript-eslint/types" "5.59.9" - "@typescript-eslint/typescript-estree" "5.59.9" + "@typescript-eslint/scope-manager" "5.61.0" + "@typescript-eslint/types" "5.61.0" + "@typescript-eslint/typescript-estree" "5.61.0" eslint-scope "^5.1.1" semver "^7.3.7" -"@typescript-eslint/visitor-keys@5.59.9": - version "5.59.9" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.9.tgz#9f86ef8e95aca30fb5a705bb7430f95fc58b146d" - integrity sha512-bT7s0td97KMaLwpEBckbzj/YohnvXtqbe2XgqNvTl6RJVakY5mvENOTPvw5u66nljfZxthESpDozs86U+oLY8Q== +"@typescript-eslint/visitor-keys@5.61.0": + version "5.61.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.61.0.tgz#c79414fa42158fd23bd2bb70952dc5cdbb298140" + integrity sha512-50XQ5VdbWrX06mQXhy93WywSFZZGsv3EOjq+lqp6WC2t+j3mb6A9xYVdrRxafvK88vg9k9u+CT4l6D8PEatjKg== dependencies: - "@typescript-eslint/types" "5.59.9" + "@typescript-eslint/types" "5.61.0" eslint-visitor-keys "^3.3.0" acorn-jsx@^5.3.2: @@ -1652,10 +1662,10 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.8.0: - version "8.8.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" - integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== +acorn@^8.9.0: + version "8.9.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.9.0.tgz#78a16e3b2bcc198c10822786fa6679e245db5b59" + integrity sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ== ajv@^6.10.0, ajv@^6.12.4: version "6.12.6" @@ -2001,14 +2011,6 @@ core-js-compat@^3.21.0, core-js-compat@^3.22.1: dependencies: browserslist "^4.21.5" -cosmjs-types@^0.7.1: - version "0.7.2" - resolved "https://registry.yarnpkg.com/cosmjs-types/-/cosmjs-types-0.7.2.tgz#a757371abd340949c5bd5d49c6f8379ae1ffd7e2" - integrity sha512-vf2uLyktjr/XVAgEq0DjMxeAWh1yYREe7AMHDKd7EiHVqxBPCaBS+qEEQUkXbR9ndnckqr1sUG8BQhazh4X5lA== - dependencies: - long "^4.0.0" - protobufjs "~6.11.2" - cosmjs-types@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/cosmjs-types/-/cosmjs-types-0.8.0.tgz#2ed78f3e990f770229726f95f3ef5bf9e2b6859b" @@ -2178,15 +2180,15 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== -eslint@^8.42.0: - version "8.42.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.42.0.tgz#7bebdc3a55f9ed7167251fe7259f75219cade291" - integrity sha512-ulg9Ms6E1WPf67PHaEY4/6E2tEn5/f7FXGzr3t9cBMugOmf1INYvuUwwh1aXQN4MfJ6a5K2iNwP3w4AColvI9A== +eslint@^8.44.0: + version "8.44.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.44.0.tgz#51246e3889b259bbcd1d7d736a0c10add4f0e500" + integrity sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.4.0" - "@eslint/eslintrc" "^2.0.3" - "@eslint/js" "8.42.0" + "@eslint/eslintrc" "^2.1.0" + "@eslint/js" "8.44.0" "@humanwhocodes/config-array" "^0.11.10" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" @@ -2198,7 +2200,7 @@ eslint@^8.42.0: escape-string-regexp "^4.0.0" eslint-scope "^7.2.0" eslint-visitor-keys "^3.4.1" - espree "^9.5.2" + espree "^9.6.0" esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" @@ -2218,17 +2220,17 @@ eslint@^8.42.0: lodash.merge "^4.6.2" minimatch "^3.1.2" natural-compare "^1.4.0" - optionator "^0.9.1" + optionator "^0.9.3" strip-ansi "^6.0.1" strip-json-comments "^3.1.0" text-table "^0.2.0" -espree@^9.5.2: - version "9.5.2" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.2.tgz#e994e7dc33a082a7a82dceaf12883a829353215b" - integrity sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw== +espree@^9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.0.tgz#80869754b1c6560f32e3b6929194a3fe07c5b82f" + integrity sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A== dependencies: - acorn "^8.8.0" + acorn "^8.9.0" acorn-jsx "^5.3.2" eslint-visitor-keys "^3.4.1" @@ -2519,11 +2521,6 @@ graceful-fs@^4.1.15, graceful-fs@^4.2.9: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -grapheme-splitter@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" - integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== - graphemer@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" @@ -2864,17 +2861,17 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -libsodium-wrappers@^0.7.6: +libsodium-sumo@^0.7.11: version "0.7.11" - resolved "https://registry.yarnpkg.com/libsodium-wrappers/-/libsodium-wrappers-0.7.11.tgz#53bd20606dffcc54ea2122133c7da38218f575f7" - integrity sha512-SrcLtXj7BM19vUKtQuyQKiQCRJPgbpauzl3s0rSwD+60wtHqSUuqcoawlMDheCJga85nKOQwxNYQxf/CKAvs6Q== - dependencies: - libsodium "^0.7.11" + resolved "https://registry.yarnpkg.com/libsodium-sumo/-/libsodium-sumo-0.7.11.tgz#ab0389e2424fca5c1dc8c4fd394906190da88a11" + integrity sha512-bY+7ph7xpk51Ez2GbE10lXAQ5sJma6NghcIDaSPbM/G9elfrjLa0COHl/7P6Wb/JizQzl5UQontOOP1z0VwbLA== -libsodium@^0.7.11: +libsodium-wrappers-sumo@^0.7.11: version "0.7.11" - resolved "https://registry.yarnpkg.com/libsodium/-/libsodium-0.7.11.tgz#cd10aae7bcc34a300cc6ad0ac88fcca674cfbc2e" - integrity sha512-WPfJ7sS53I2s4iM58QxY3Inb83/6mjlYgcmZs7DJsvDlnmVUwNinBCi5vBT43P6bHRy01O4zsMU2CoVR6xJ40A== + resolved "https://registry.yarnpkg.com/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.11.tgz#d96329ee3c0e7ec7f5fcf4cdde16cc3a1ae91d82" + integrity sha512-DGypHOmJbB1nZn89KIfGOAkDgfv5N6SBGC3Qvmy/On0P0WD1JQvNRS/e3UL3aFF+xC0m+MYz5M+MnRnK2HMrKQ== + dependencies: + libsodium-sumo "^0.7.11" locate-path@^5.0.0: version "5.0.0" @@ -3099,17 +3096,17 @@ onetime@^2.0.0: dependencies: mimic-fn "^1.0.0" -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== +optionator@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" + integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== dependencies: + "@aashutoshrathi/word-wrap" "^1.2.3" deep-is "^0.1.3" fast-levenshtein "^2.0.6" levn "^0.4.1" prelude-ls "^1.2.1" type-check "^0.4.0" - word-wrap "^1.2.3" os-tmpdir@~1.0.2: version "1.0.2" @@ -3627,10 +3624,10 @@ type@^2.7.2: resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== -typescript@^5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.3.tgz#8d84219244a6b40b6fb2b33cc1c062f715b9e826" - integrity sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw== +typescript@^5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" + integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" @@ -3708,11 +3705,6 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -word-wrap@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== - wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" From a4ab76f7fe2b6c5d8784e4ea0e62a5cd2ba82a6f Mon Sep 17 00:00:00 2001 From: brimigs <85972460+brimigs@users.noreply.github.com> Date: Mon, 10 Jul 2023 22:01:09 -0400 Subject: [PATCH 07/43] Mp 3015 add in build scripts for mars params contract (#225) --- scripts/deploy/addresses/osmo-test-5.json | 12 +- scripts/deploy/base/deployer.ts | 111 ++++++++----- scripts/deploy/base/index.ts | 7 +- scripts/deploy/base/storage.ts | 2 +- scripts/deploy/neutron/config.ts | 91 ++++++----- scripts/deploy/osmosis/mainIndex.ts | 2 +- scripts/deploy/osmosis/mainnetConfig.ts | 168 +++++++++++++++++++ scripts/deploy/osmosis/multisig.ts | 2 +- scripts/deploy/osmosis/testIndex.ts | 2 +- scripts/deploy/osmosis/testnetConfig.ts | 189 ++++++++++++++++++++++ scripts/types/config.ts | 38 +++-- scripts/types/storageItems.ts | 4 +- 12 files changed, 520 insertions(+), 108 deletions(-) create mode 100644 scripts/deploy/osmosis/mainnetConfig.ts create mode 100644 scripts/deploy/osmosis/testnetConfig.ts diff --git a/scripts/deploy/addresses/osmo-test-5.json b/scripts/deploy/addresses/osmo-test-5.json index b113d001d..cf6020bfa 100644 --- a/scripts/deploy/addresses/osmo-test-5.json +++ b/scripts/deploy/addresses/osmo-test-5.json @@ -1,7 +1,9 @@ { - "address-provider": "osmo1wlm6dc0vnncu2v5z26rv97plmlkmalm84uwqatrlftc4gmp8ahgqs6r4py", - "red-bank": "osmo1dl4rylasnd7mtfzlkdqn2gr0ss4gvyykpvr6d7t5ylzf6z535n9s5jjt8u", - "incentives": "osmo1zyz57xf82963mcsgqu3hq5y0h9mrltm4ttq2qe5mjth9ezp3375qe0sm7d", - "oracle": "osmo1khe29uw3t85nmmp3mtr8dls7v2qwsfk3tndu5h4w5g2r5tzlz5qqarq2e2", - "rewards-collector": "osmo1u5pcjue4grmg8lh7xrz2nvpy79xlzknwqkczfkyeyx9zzzj76tpq4tgrcs" + "address-provider": "osmo1xlf93me2979mvgj0gmluzuw22zxwd9rt2jaafzymw7d8kghkhwsqlwgh2r", + "red-bank": "osmo1r9ks824qewvpa9sqgzs3w2ylxx582c6d0hrnzqf3csufchqneydq60hgrv", + "incentives": "osmo1uq2y7h5sw8xtzhluhh7m9l3h9jdv5zjz7jhvavc3sgq0ps8ggq9s39qgcf", + "oracle": "osmo1vae2gsgeqw8q2x5ycvcrspsuqrnydveqgca03v8k9p4uvl2fgdlqp5r8fc", + "rewards-collector": "osmo1uw8g8mxlfk4at27xq26vqzttddtae7fgup4sd59ndrh0ymk99prsezh896", + "swapper": "osmo1q97xatqr3c0zrlgck60yd6f6nqvvtcg5772pngthzrkmdxkc6kqqdj56v2", + "params": "osmo1et0qv7acfv0wv3wlqmjtyflw5dectct24nuwjeqdkfzm9fznfunste0hnc" } diff --git a/scripts/deploy/base/deployer.ts b/scripts/deploy/base/deployer.ts index 789f0fe5a..7ff2db106 100644 --- a/scripts/deploy/base/deployer.ts +++ b/scripts/deploy/base/deployer.ts @@ -1,4 +1,10 @@ -import { AssetConfig, DeploymentConfig, OracleConfig, isAstroportRoute } from '../../types/config' +import { + AssetConfig, + DeploymentConfig, + OracleConfig, + isAstroportRoute, + VaultConfig, +} from '../../types/config' import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' import * as fs from 'fs' import { printBlue, printGreen, printRed, printYellow } from '../../utils/chalk' @@ -12,6 +18,7 @@ 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 { ExecuteMsg as ParamsExecuteMsg } from '../../types/generated/mars-params/MarsParams.types' import { InstantiateMsg as RedBankInstantiateMsg, QueryMsg as RedBankQueryMsg, @@ -52,7 +59,7 @@ export class Deployer { Number(accountBalance.amount) / 1e6 } ${this.config.chainPrefix})`, ) - if (Number(accountBalance.amount) < 1_000_000 && this.config.chainId === 'osmo-test-4') { + if (Number(accountBalance.amount) < 1_000_000 && this.config.chainId === 'osmo-test-5') { printRed( `not enough ${this.config.chainPrefix} tokens to complete action, you may need to go to a test faucet to get more tokens.`, ) @@ -173,6 +180,66 @@ export class Deployer { await this.instantiate('params', this.storage.codeIds.params!, msg) } + async updateAssetParams(assetConfig: AssetConfig) { + if (this.storage.execute.assetsUpdated.includes(assetConfig.denom)) { + printBlue(`${assetConfig.symbol} already updated in Params contract`) + return + } + printBlue(`Updating ${assetConfig.symbol}...`) + + const msg: ParamsExecuteMsg = { + update_asset_params: { + add_or_update: { + params: { + credit_manager: { + hls: assetConfig.credit_manager.hls, + whitelisted: assetConfig.credit_manager.whitelisted, + }, + denom: assetConfig.denom, + liquidation_bonus: assetConfig.liquidation_bonus, + liquidation_threshold: assetConfig.liquidation_threshold, + protocol_liquidation_fee: assetConfig.protocol_liquidation_fee, + max_loan_to_value: assetConfig.max_loan_to_value, + red_bank: { + borrow_enabled: assetConfig.red_bank.borrow_enabled, + deposit_enabled: assetConfig.red_bank.borrow_enabled, + deposit_cap: assetConfig.red_bank.deposit_cap, + }, + }, + }, + }, + } + + await this.client.execute(this.deployerAddress, this.storage.addresses['params']!, msg, 'auto') + + printYellow(`${assetConfig.symbol} updated.`) + } + + async updateVaultConfig(vaultConfig: VaultConfig) { + if (this.storage.execute.vaultsUpdated.includes(vaultConfig.addr)) { + printBlue(`${vaultConfig.symbol} already updated in Params contract`) + return + } + printBlue(`Updating ${vaultConfig.symbol}...`) + + const msg: ParamsExecuteMsg = { + update_vault_config: { + add_or_update: { + config: { + addr: vaultConfig.addr, + deposit_cap: vaultConfig.deposit_cap, + liquidation_threshold: vaultConfig.liquidation_threshold, + whitelisted: vaultConfig.whitelisted, + max_loan_to_value: vaultConfig.max_loan_to_value, + }, + }, + }, + } + + await this.client.execute(this.deployerAddress, this.storage.addresses['params']!, msg, 'auto') + + printYellow(`${vaultConfig.symbol} updated.`) + } async setRoutes() { printBlue('Setting Swapper Routes') for (const route of this.config.swapRoutes) { @@ -256,46 +323,6 @@ export class Deployer { printGreen('Address Provider update completed') } - async initializeAsset(assetConfig: AssetConfig) { - if (this.storage.execute.assetsInitialized.includes(assetConfig.denom)) { - printBlue(`${assetConfig.symbol} already initialized.`) - return - } - printBlue(`Initializing ${assetConfig.symbol}...`) - - const msg = { - init_asset: { - denom: assetConfig.denom, - params: { - max_loan_to_value: assetConfig.max_loan_to_value, - reserve_factor: assetConfig.reserve_factor, - liquidation_threshold: assetConfig.liquidation_threshold, - liquidation_bonus: assetConfig.liquidation_bonus, - interest_rate_model: { - optimal_utilization_rate: assetConfig.interest_rate_model.optimal_utilization_rate, - base: assetConfig.interest_rate_model.base, - slope_1: assetConfig.interest_rate_model.slope_1, - slope_2: assetConfig.interest_rate_model.slope_2, - }, - deposit_cap: assetConfig.deposit_cap, - deposit_enabled: assetConfig.deposit_enabled, - borrow_enabled: assetConfig.borrow_enabled, - }, - }, - } - - await this.client.execute( - this.deployerAddress, - this.storage.addresses['red-bank']!, - msg, - 'auto', - ) - - printYellow(`${assetConfig.symbol} initialized`) - - this.storage.execute.assetsInitialized.push(assetConfig.denom) - } - async recordTwapSnapshots(denoms: string[]) { const msg: WasmOracleExecuteMsg = { custom: { diff --git a/scripts/deploy/base/index.ts b/scripts/deploy/base/index.ts index d081ce108..5f09a6595 100644 --- a/scripts/deploy/base/index.ts +++ b/scripts/deploy/base/index.ts @@ -30,10 +30,13 @@ export const taskRunner = async (config: DeploymentConfig) => { // setup await deployer.updateAddressProvider() - await deployer.setRoutes() for (const asset of config.assets) { - await deployer.initializeAsset(asset) + await deployer.updateAssetParams(asset) + } + for (const vault of config.vaults) { + await deployer.updateVaultConfig(vault) } + await deployer.setRoutes() for (const oracleConfig of config.oracleConfigs) { await deployer.setOracle(oracleConfig) } diff --git a/scripts/deploy/base/storage.ts b/scripts/deploy/base/storage.ts index b23fd319c..6f429224e 100644 --- a/scripts/deploy/base/storage.ts +++ b/scripts/deploy/base/storage.ts @@ -28,7 +28,7 @@ export class Storage implements StorageItems { return new this(chainId, { addresses: {}, codeIds: {}, - execute: { assetsInitialized: [], addressProviderUpdated: {} }, + execute: { assetsUpdated: [], vaultsUpdated: [], addressProviderUpdated: {} }, }) } } diff --git a/scripts/deploy/neutron/config.ts b/scripts/deploy/neutron/config.ts index 192aedab5..d1a208f64 100644 --- a/scripts/deploy/neutron/config.ts +++ b/scripts/deploy/neutron/config.ts @@ -1,4 +1,4 @@ -import { DeploymentConfig, AssetConfig, OracleConfig } from '../../types/config' +import { AssetConfig, OracleConfig } from '../../types/config' const axlUSDCTestnet = 'ibc/EFB00E728F98F0C4BBE8CA362123ACAB466EDA2826DC6837E49F4C1902F21BBA' // TODO: This is actually ASTRO since there is no pool for axlUSDC on testnet const atomTestnet = 'ibc/C4CFF46FD6DE35CA4CF4CE031E643C8FDC9BA4B99AE598E9B0ED98FE3A2319F9' @@ -7,62 +7,74 @@ const protocolAdminAddrTestnet = 'neutron1ke0vqqzyymlp5esr8gjwuzh94ysnpvj8er5hm7 const astroportFactoryTestnet = 'neutron1jj0scx400pswhpjes589aujlqagxgcztw04srynmhf0f6zplzn2qqmhwj7' const astroportRouterTestnet = 'neutron12jm24l9lr9cupufqjuxpdjnnweana4h66tsx5cl800mke26td26sq7m05p' -// note the following three addresses are all 'mars' bech32 prefix +// note the following addresses are all 'mars' bech32 prefix const safetyFundAddr = 'mars1s4hgh56can3e33e0zqpnjxh0t5wdf7u3pze575' const feeCollectorAddr = 'mars17xpfvakm2amg962yls6f84z3kell8c5ldy6e7x' export const ntrnAsset: AssetConfig = { + credit_manager: { + whitelisted: true, + }, + symbol: 'NTRM', denom: 'untrn', - max_loan_to_value: '0.59', - reserve_factor: '0.2', + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', liquidation_threshold: '0.61', - liquidation_bonus: '0.15', - interest_rate_model: { - optimal_utilization_rate: '0.6', - base: '0', - slope_1: '0.15', - slope_2: '3', + max_loan_to_value: '0.59', + red_bank: { + borrow_enabled: true, + deposit_cap: '2500000000000', + deposit_enabled: true, }, - deposit_cap: '2500000000000', - deposit_enabled: true, - borrow_enabled: true, - symbol: 'NTRN', } export const atomAsset: AssetConfig = { + credit_manager: { + whitelisted: true, + }, + symbol: 'ATOM', denom: atomTestnet, - max_loan_to_value: '0.68', - reserve_factor: '0.2', + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', liquidation_threshold: '0.7', - liquidation_bonus: '0.15', - interest_rate_model: { - optimal_utilization_rate: '0.6', - base: '0', - slope_1: '0.15', - slope_2: '3', + max_loan_to_value: '0.68', + red_bank: { + borrow_enabled: true, + deposit_cap: '100000000000', + deposit_enabled: true, }, - deposit_cap: '100000000000', - deposit_enabled: true, - borrow_enabled: true, - symbol: 'ATOM', } export const axlUSDCAsset: AssetConfig = { denom: axlUSDCTestnet, - max_loan_to_value: '0.74', - reserve_factor: '0.2', - liquidation_threshold: '0.75', - liquidation_bonus: '0.1', - interest_rate_model: { - optimal_utilization_rate: '0.8', - base: '0', - slope_1: '0.2', - slope_2: '2', + credit_manager: { + whitelisted: true, }, - deposit_cap: '500000000000', - deposit_enabled: true, - borrow_enabled: true, symbol: 'axlUSDC', + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + liquidation_threshold: '0.75', + max_loan_to_value: '0.74', + red_bank: { + borrow_enabled: true, + deposit_cap: '500000000000', + deposit_enabled: true, + }, } export const ntrnOracleTestnet: OracleConfig = { @@ -96,7 +108,7 @@ export const axlUSDCOracleTestnet: OracleConfig = { }, } -export const neutronTestnetConfig: DeploymentConfig = { +export const neutronTestnetConfig = { oracleName: 'wasm', atomDenom: atomTestnet, baseAssetDenom: 'untrn', @@ -200,6 +212,7 @@ export const neutronTestnetConfig: DeploymentConfig = { feeCollectorAddr: feeCollectorAddr, swapperDexName: 'astroport', assets: [ntrnAsset, atomAsset], + vaults: [], oracleConfigs: [axlUSDCOracleTestnet, ntrnOracleTestnet, atomOracleTestnet], targetHealthFactor: '1.2', oracleCustomInitParams: { diff --git a/scripts/deploy/osmosis/mainIndex.ts b/scripts/deploy/osmosis/mainIndex.ts index 6b0cb614c..c1b5b291e 100644 --- a/scripts/deploy/osmosis/mainIndex.ts +++ b/scripts/deploy/osmosis/mainIndex.ts @@ -1,5 +1,5 @@ import { taskRunner } from '../base' -import { osmosisMainnet } from './config.js' +import { osmosisMainnet } from './mainnetConfig' void (async function () { await taskRunner(osmosisMainnet) diff --git a/scripts/deploy/osmosis/mainnetConfig.ts b/scripts/deploy/osmosis/mainnetConfig.ts new file mode 100644 index 000000000..7e07d1047 --- /dev/null +++ b/scripts/deploy/osmosis/mainnetConfig.ts @@ -0,0 +1,168 @@ +import { DeploymentConfig, AssetConfig, OracleConfig } from '../../types/config' + +// Mainnet: +const osmo = 'uosmo' +const atom = 'ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2' +const axlUSDC = 'ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858' +const mars = 'ibc/573FCD90FACEE750F55A8864EF7D38265F07E5A9273FA0E8DAFD39951332B580' + +const pythContractAddr = 'UPDATE' +const protocolAdminAddr = 'osmo14w4x949nwcrqgfe53pxs3k7x53p0gvlrq34l5n' + +// note the following addresses are all 'mars' bech32 prefix +const safetyFundAddr = 'mars1s4hgh56can3e33e0zqpnjxh0t5wdf7u3pze575' +const feeCollectorAddr = 'mars17xpfvakm2amg962yls6f84z3kell8c5ldy6e7x' + +export const osmoAsset: AssetConfig = { + credit_manager: { + whitelisted: true, + }, + symbol: 'OSMO', + denom: osmo, + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + liquidation_threshold: '0.61', + max_loan_to_value: '0.59', + red_bank: { + borrow_enabled: true, + deposit_cap: '2500000000000', + deposit_enabled: true, + }, +} + +export const atomAsset: AssetConfig = { + credit_manager: { + whitelisted: true, + }, + symbol: 'ATOM', + denom: atom, + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + liquidation_threshold: '0.7', + max_loan_to_value: '0.68', + red_bank: { + borrow_enabled: true, + deposit_cap: '100000000000', + deposit_enabled: true, + }, +} + +export const axlUSDCAsset: AssetConfig = { + credit_manager: { + whitelisted: true, + }, + symbol: 'axlUSDC', + denom: axlUSDC, + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + liquidation_threshold: '0.75', + max_loan_to_value: '0.74', + red_bank: { + borrow_enabled: true, + deposit_cap: '500000000000', + deposit_enabled: true, + }, +} + +export const atomOracle: OracleConfig = { + denom: atom, + price_source: { + pyth: { + contract_addr: pythContractAddr, + price_feed_id: 'UPDATE', + max_staleness: 60, + denom_decimals: 6, + max_confidence: '5', + max_deviation: '4', + }, + }, +} +export const axlUSDCOracle: OracleConfig = { + denom: axlUSDC, + price_source: { + geometric_twap: { + pool_id: 678, + window_size: 1800, + downtime_detector: { downtime: 'duration30m', recovery: 7200 }, + }, + }, +} + +export const osmosisMainnet: DeploymentConfig = { + oracleName: 'osmosis', + atomDenom: atom, + baseAssetDenom: osmo, + gasPrice: '0.1uosmo', + chainId: 'osmosis-1', + chainPrefix: 'osmo', + channelId: 'channel-557', + marsDenom: mars, + rewardCollectorTimeoutSeconds: 600, + rpcEndpoint: 'https://rpc.osmosis.zone', + safetyFundFeeShare: '0.5', + deployerMnemonic: 'TO BE INSERTED AT TIME OF DEPLOYMENT', + slippage_tolerance: '0.01', + base_asset_symbol: 'OSMO', + multisigAddr: 'osmo14w4x949nwcrqgfe53pxs3k7x53p0gvlrq34l5n', + runTests: false, + mainnet: true, + feeCollectorDenom: mars, + safetyFundDenom: axlUSDC, + swapRoutes: [ + { denom_in: osmo, denom_out: axlUSDC, route: [{ pool_id: 678, token_out_denom: axlUSDC }] }, + { + denom_in: atom, + denom_out: axlUSDC, + route: [ + { pool_id: 1, token_out_denom: osmo }, + { pool_id: 678, token_out_denom: axlUSDC }, + ], + }, + { + denom_in: osmo, + denom_out: mars, + route: [{ pool_id: 907, token_out_denom: mars }], + }, + { + denom_in: atom, + denom_out: mars, + route: [ + { pool_id: 1, token_out_denom: 'uosmo' }, + { pool_id: 907, token_out_denom: mars }, + ], + }, + { + denom_in: axlUSDC, + denom_out: mars, + route: [ + { pool_id: 678, token_out_denom: osmo }, + { pool_id: 907, token_out_denom: mars }, + ], + }, + ], + safetyFundAddr: safetyFundAddr, + protocolAdminAddr: protocolAdminAddr, + feeCollectorAddr: feeCollectorAddr, + swapperDexName: 'osmosis', + assets: [osmoAsset, atomAsset, axlUSDCAsset], + vaults: [], + oracleConfigs: [atomOracle, axlUSDCOracle], + targetHealthFactor: '1.2', + incentiveEpochDuration: 86400, + maxWhitelistedIncentiveDenoms: 10, +} diff --git a/scripts/deploy/osmosis/multisig.ts b/scripts/deploy/osmosis/multisig.ts index 78676d111..49edbf6f4 100644 --- a/scripts/deploy/osmosis/multisig.ts +++ b/scripts/deploy/osmosis/multisig.ts @@ -1,5 +1,5 @@ import { taskRunner } from '../base' -import { osmosisTestMultisig } from './config.js' +import { osmosisTestMultisig } from './testnetConfig' void (async function () { await taskRunner(osmosisTestMultisig) diff --git a/scripts/deploy/osmosis/testIndex.ts b/scripts/deploy/osmosis/testIndex.ts index ead3a96be..4f22107d6 100644 --- a/scripts/deploy/osmosis/testIndex.ts +++ b/scripts/deploy/osmosis/testIndex.ts @@ -1,5 +1,5 @@ import { taskRunner } from '../base' -import { osmosisTestnetConfig } from './config.js' +import { osmosisTestnetConfig } from './testnetConfig' void (async function () { await taskRunner(osmosisTestnetConfig) diff --git a/scripts/deploy/osmosis/testnetConfig.ts b/scripts/deploy/osmosis/testnetConfig.ts new file mode 100644 index 000000000..8cce3b5ec --- /dev/null +++ b/scripts/deploy/osmosis/testnetConfig.ts @@ -0,0 +1,189 @@ +import { DeploymentConfig, AssetConfig, OracleConfig, VaultConfig } from '../../types/config' + +// assets based off of OSMO-TEST-5: https://docs.osmosis.zone/osmosis-core/asset-info/ +const osmo = 'uosmo' +const atom = 'ibc/A8C2D23A1E6F95DA4E48BA349667E322BD7A6C996D8A4AAE8BA72E190F3D1477' +const nUSDC = 'ibc/40F1B2458AEDA66431F9D44F48413240B8D28C072463E2BF53655728683583E3' // noble +const mars = 'ibc/2E7368A14AC9AB7870F32CFEA687551C5064FA861868EDF7437BC877358A81F9' + +const pythContractAddr = 'UPDATE' +const protocolAdminAddr = 'osmo14w4x949nwcrqgfe53pxs3k7x53p0gvlrq34l5n' + +// note the following addresses are all 'mars' bech32 prefix +const safetyFundAddr = 'mars1s4hgh56can3e33e0zqpnjxh0t5wdf7u3pze575' +const feeCollectorAddr = 'mars17xpfvakm2amg962yls6f84z3kell8c5ldy6e7x' + +export const osmoAsset: AssetConfig = { + credit_manager: { + whitelisted: true, + }, + symbol: 'OSMO', + denom: osmo, + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + liquidation_threshold: '0.61', + max_loan_to_value: '0.59', + red_bank: { + borrow_enabled: true, + deposit_cap: '2500000000000', + deposit_enabled: true, + }, +} + +export const atomAsset: AssetConfig = { + credit_manager: { + whitelisted: true, + }, + symbol: 'ATOM', + denom: atom, + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + liquidation_threshold: '0.7', + max_loan_to_value: '0.68', + red_bank: { + borrow_enabled: true, + deposit_cap: '100000000000', + deposit_enabled: true, + }, +} + +export const USDCAsset: AssetConfig = { + credit_manager: { + whitelisted: true, + }, + symbol: 'nUSDC', + denom: nUSDC, + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', + }, + protocol_liquidation_fee: '0.5', + liquidation_threshold: '0.75', + max_loan_to_value: '0.74', + red_bank: { + borrow_enabled: true, + deposit_cap: '500000000000', + deposit_enabled: true, + }, +} + +export const usdcOsmoVault: VaultConfig = { + addr: 'osmo1fmq9hw224fgz8lk48wyd0gfg028kvvzggt6c3zvnaqkw23x68cws5nd5em', + symbol: 'usdcOsmoVault', + deposit_cap: { + denom: nUSDC, + amount: '1000000000', + }, + liquidation_threshold: '0.65', + max_loan_to_value: '0.63', + whitelisted: true, +} + +export const atomOracle: OracleConfig = { + denom: atom, + price_source: { + pyth: { + contract_addr: pythContractAddr, + price_feed_id: 'UPDATE', + max_staleness: 60, + denom_decimals: 6, + max_confidence: '5', + max_deviation: '4', + }, + }, +} + +export const USDCOracle: OracleConfig = { + denom: nUSDC, + price_source: { + staked_geometric_twap: { + transitive_denom: osmo, + pool_id: 6, + window_size: 1800, + downtime_detector: { downtime: 'duration30m', recovery: 7200 }, + }, + }, +} + +export const osmosisTestnetConfig = { + oracleName: 'osmosis', + atomDenom: atom, + baseAssetDenom: osmo, + gasPrice: '0.1uosmo', + chainId: 'osmo-test-5', + chainPrefix: 'osmo', + channelId: 'channel-2083', + marsDenom: mars, + rewardCollectorTimeoutSeconds: 600, + rpcEndpoint: 'https://rpc.osmotest5.osmosis.zone', + safetyFundFeeShare: '0.5', + deployerMnemonic: + 'elevator august inherit simple buddy giggle zone despair marine rich swim danger blur people hundred faint ladder wet toe strong blade utility trial process', + slippage_tolerance: '0.01', + base_asset_symbol: 'OSMO', + runTests: false, + mainnet: false, + feeCollectorDenom: mars, + safetyFundDenom: nUSDC, + swapRoutes: [ + { denom_in: atom, denom_out: osmo, route: [{ pool_id: 12, token_out_denom: osmo }] }, + ], + safetyFundAddr: safetyFundAddr, + protocolAdminAddr: protocolAdminAddr, + feeCollectorAddr: feeCollectorAddr, + swapperDexName: 'osmosis', + assets: [osmoAsset, atomAsset, USDCAsset], + vaults: [usdcOsmoVault], + oracleConfigs: [atomOracle, USDCOracle], + targetHealthFactor: '1.2', + incentiveEpochDuration: 86400, + maxWhitelistedIncentiveDenoms: 10, +} + +export const osmosisTestMultisig: DeploymentConfig = { + oracleName: 'osmosis', + atomDenom: atom, + baseAssetDenom: 'uosmo', + gasPrice: '0.1uosmo', + chainId: 'osmo-test-5', + chainPrefix: 'osmo', + channelId: 'channel-2083', + marsDenom: mars, + rewardCollectorTimeoutSeconds: 600, + rpcEndpoint: 'https://rpc.osmotest5.osmosis.zone', + safetyFundFeeShare: '0.5', + deployerMnemonic: + 'elevator august inherit simple buddy giggle zone despair marine rich swim danger blur people hundred faint ladder wet toe strong blade utility trial process', + slippage_tolerance: '0.01', + base_asset_symbol: 'OSMO', + multisigAddr: 'osmo14w4x949nwcrqgfe53pxs3k7x53p0gvlrq34l5n', + runTests: false, + mainnet: false, + feeCollectorDenom: mars, + safetyFundDenom: nUSDC, + swapRoutes: [ + { denom_in: atom, denom_out: 'uosmo', route: [{ pool_id: 1, token_out_denom: 'uosmo' }] }, + ], + safetyFundAddr: safetyFundAddr, + protocolAdminAddr: protocolAdminAddr, + feeCollectorAddr: feeCollectorAddr, + swapperDexName: 'osmosis', + assets: [osmoAsset, atomAsset, USDCAsset], + vaults: [usdcOsmoVault], + oracleConfigs: [atomOracle, USDCOracle], + targetHealthFactor: '1.2', + incentiveEpochDuration: 86400, + maxWhitelistedIncentiveDenoms: 10, +} diff --git a/scripts/types/config.ts b/scripts/types/config.ts index cdbb4d1af..1e2b52563 100644 --- a/scripts/types/config.ts +++ b/scripts/types/config.ts @@ -5,6 +5,14 @@ import { WasmOracleCustomInitParams, WasmPriceSourceForString, } from './generated/mars-oracle-wasm/MarsOracleWasm.types' +import { + CmSettingsForString, + Coin, + Decimal, + HlsParamsBaseForString, + LiquidationBonus, + RedBankSettings, +} from './generated/mars-params/MarsParams.types' type SwapRoute = { denom_in: string @@ -41,7 +49,6 @@ export interface DeploymentConfig { deployerMnemonic: string slippage_tolerance: string base_asset_symbol: string - second_asset_symbol: string multisigAddr?: string runTests: boolean mainnet: boolean @@ -52,6 +59,7 @@ export interface DeploymentConfig { targetHealthFactor: string swapperDexName: string assets: AssetConfig[] + vaults: VaultConfig[] oracleConfigs: OracleConfig[] oracleCustomInitParams?: WasmOracleCustomInitParams incentiveEpochDuration: number @@ -60,21 +68,23 @@ export interface DeploymentConfig { } export interface AssetConfig { + symbol: string + credit_manager: CmSettingsForString denom: string - max_loan_to_value: string - reserve_factor: string - liquidation_threshold: string - liquidation_bonus: string - interest_rate_model: { - optimal_utilization_rate: string - base: string - slope_1: string - slope_2: string - } - deposit_cap: string - deposit_enabled: boolean - borrow_enabled: boolean + liquidation_bonus: LiquidationBonus + liquidation_threshold: Decimal + max_loan_to_value: Decimal + protocol_liquidation_fee: Decimal + red_bank: RedBankSettings +} +export interface VaultConfig { + addr: string symbol: string + deposit_cap: Coin + hls?: HlsParamsBaseForString | null + liquidation_threshold: Decimal + max_loan_to_value: Decimal + whitelisted: boolean } export interface OracleConfig { diff --git a/scripts/types/storageItems.ts b/scripts/types/storageItems.ts index 48e361593..f5a134753 100644 --- a/scripts/types/storageItems.ts +++ b/scripts/types/storageItems.ts @@ -20,8 +20,8 @@ export interface StorageItems { execute: { addressProviderUpdated: Record - assetsInitialized: string[] - secondAssetInitialized?: boolean + assetsUpdated: string[] + vaultsUpdated: string[] oraclePriceSet?: boolean smokeTest?: boolean } From 78dcf32d30728ad090035d173fc43b0f2db8714d Mon Sep 17 00:00:00 2001 From: Gabe Rodriguez Date: Tue, 18 Jul 2023 08:58:37 +0200 Subject: [PATCH 08/43] MP-2645. Guard against self liquidations (#254) --- contracts/red-bank/src/error.rs | 3 +++ contracts/red-bank/src/execute.rs | 6 ++++++ contracts/red-bank/tests/test_liquidate.rs | 18 ++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/contracts/red-bank/src/error.rs b/contracts/red-bank/src/error.rs index a65dd2592..91d79390c 100644 --- a/contracts/red-bank/src/error.rs +++ b/contracts/red-bank/src/error.rs @@ -81,6 +81,9 @@ pub enum ContractError { #[error("Amount to repay is greater than total debt")] CannotRepayMoreThanDebt {}, + #[error("User cannot issue liquidation of own account")] + CannotLiquidateSelf {}, + #[error("User has a positive uncollateralized loan limit and thus cannot be liquidated")] CannotLiquidateWhenPositiveUncollateralizedLoanLimit {}, diff --git a/contracts/red-bank/src/execute.rs b/contracts/red-bank/src/execute.rs index 89e0d7bcc..f3f0919b5 100644 --- a/contracts/red-bank/src/execute.rs +++ b/contracts/red-bank/src/execute.rs @@ -709,6 +709,12 @@ pub fn liquidate( let recipient = User(&recipient_addr); // 1. Validate liquidation + + // User cannot liquidate themselves + if info.sender == user_addr { + return Err(ContractError::CannotLiquidateSelf {}); + } + // If user (contract) has a positive uncollateralized limit then the user // cannot be liquidated if !user.uncollateralized_loan_limit(deps.storage, &debt_denom)?.is_zero() { diff --git a/contracts/red-bank/tests/test_liquidate.rs b/contracts/red-bank/tests/test_liquidate.rs index 747488453..b763d3505 100644 --- a/contracts/red-bank/tests/test_liquidate.rs +++ b/contracts/red-bank/tests/test_liquidate.rs @@ -309,6 +309,24 @@ fn liquidate_if_no_coins_sent() { assert_eq!(error_res, PaymentError::NoFunds {}.into()); } +#[test] +fn cannot_self_liquidate() { + let TestSuite { + mut deps, + .. + } = setup_test(); + + let env = mock_env(MockEnvParams::default()); + let info = mock_info("liquidator", &[coin(100, "somecoin1")]); + let msg = ExecuteMsg::Liquidate { + user: "liquidator".to_string(), + collateral_denom: "collateral".to_string(), + recipient: None, + }; + let error_res = execute(deps.as_mut(), env, info, msg).unwrap_err(); + assert_eq!(error_res, ContractError::CannotLiquidateSelf {}.into()); +} + #[test] fn liquidate_if_many_coins_sent() { let TestSuite { From 51217176a58f6291b234d2b803d949384491b1c9 Mon Sep 17 00:00:00 2001 From: piobab Date: Tue, 25 Jul 2023 16:16:53 +0200 Subject: [PATCH 09/43] New dynamic liquidation mechanics. (#270) * New dynamic liquidation mechanics. * Extract liquidation to sperate crate. * Disable test coverage. --- .github/workflows/coverage.yml | 14 +- Cargo.lock | 104 +- Cargo.toml | 4 +- Makefile.toml | 8 - contracts/red-bank/Cargo.toml | 3 + contracts/red-bank/src/contract.rs | 4 +- contracts/red-bank/src/error.rs | 12 +- contracts/red-bank/src/execute.rs | 302 +-- contracts/red-bank/src/health.rs | 8 +- contracts/red-bank/src/helpers.rs | 6 +- contracts/red-bank/src/lib.rs | 1 + contracts/red-bank/src/liquidate.rs | 226 ++ ...ank - Dynamic LB & CF test cases v1.1.xlsx | Bin 0 -> 20427 bytes contracts/red-bank/tests/helpers.rs | 182 +- contracts/red-bank/tests/test_admin.rs | 4 +- contracts/red-bank/tests/test_borrow.rs | 9 +- contracts/red-bank/tests/test_deposit.rs | 32 +- contracts/red-bank/tests/test_liquidate.rs | 2257 ++++++++--------- contracts/red-bank/tests/test_misc.rs | 2 +- contracts/red-bank/tests/test_withdraw.rs | 2 +- integration-tests/tests/helpers.rs | 63 +- integration-tests/tests/test_incentives.rs | 69 +- integration-tests/tests/test_liquidations.rs | 254 -- integration-tests/tests/test_oracles.rs | 33 +- integration-tests/tests/test_rover_flow.rs | 14 +- integration-tests/tests/test_user_flow.rs | 56 +- packages/health/src/health.rs | 2 +- packages/health/src/query.rs | 2 +- .../tests/test_from_coins_to_positions.rs | 34 +- .../health/tests/test_health_from_coins.rs | 50 +- packages/liquidation/Cargo.toml | 25 + packages/liquidation/README.md | 7 + packages/liquidation/src/error.rs | 23 + packages/liquidation/src/lib.rs | 2 + packages/liquidation/src/liquidation.rs | 156 ++ packages/testing/src/integration/mock_env.rs | 159 +- packages/testing/src/mars_mock_querier.rs | 6 +- packages/testing/src/params_querier.rs | 6 +- scripts/deploy/neutron/config.ts | 1 - scripts/deploy/osmosis/config.ts | 198 +- scripts/types/config.ts | 1 - 41 files changed, 2145 insertions(+), 2196 deletions(-) create mode 100644 contracts/red-bank/src/liquidate.rs create mode 100644 contracts/red-bank/tests/files/Red Bank - Dynamic LB & CF test cases v1.1.xlsx delete mode 100644 integration-tests/tests/test_liquidations.rs create mode 100644 packages/liquidation/Cargo.toml create mode 100644 packages/liquidation/README.md create mode 100644 packages/liquidation/src/error.rs create mode 100644 packages/liquidation/src/lib.rs create mode 100644 packages/liquidation/src/liquidation.rs diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index bbced13ee..29be0e235 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -25,16 +25,18 @@ jobs: - name: Install stable Rust run: cargo make install-stable - # selecting a toolchain should happen before the plugin, as the cache uses the current rustc version as its cache key - - name: Cache dependencies - uses: Swatinem/rust-cache@v2 - # Artifacts used by tests - name: Compile workspace run: cargo make build - - name: Run test - run: cargo make test + # FIXME: enable it one problem with `no space left fixed` + # - remove `target/release` deps + # - name: Cleanup + # run: | + # rm -rf target/release + + # - name: Run test + # run: cargo make test # disabled because of "no space left" error. # - name: Run test coverage diff --git a/Cargo.lock b/Cargo.lock index 7c3d55765..f1fa53331 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1765,7 +1765,7 @@ dependencies = [ "cw-storage-plus 1.1.0", "cw2 1.1.0", "mars-owner", - "mars-red-bank-types 1.1.0", + "mars-red-bank-types", "serde", "thiserror", ] @@ -1775,8 +1775,8 @@ name = "mars-health" version = "1.1.0" dependencies = [ "cosmwasm-std", - "mars-params 1.0.2", - "mars-red-bank-types 1.1.0", + "mars-params", + "mars-red-bank-types", "mars-testing", "thiserror", ] @@ -1791,9 +1791,9 @@ dependencies = [ "cw2 1.1.0", "mars-owner", "mars-red-bank", - "mars-red-bank-types 1.1.0", + "mars-red-bank-types", "mars-testing", - "mars-utils 1.1.0", + "mars-utils", "osmosis-std 0.16.1", "test-case", "thiserror", @@ -1810,18 +1810,28 @@ dependencies = [ "mars-oracle-base", "mars-oracle-osmosis", "mars-osmosis", - "mars-params 1.0.2", + "mars-params", "mars-red-bank", - "mars-red-bank-types 1.1.0", + "mars-red-bank-types", "mars-rewards-collector", "mars-swapper-osmosis", "mars-testing", - "mars-utils 1.1.0", + "mars-utils", "osmosis-std 0.16.1", "osmosis-test-tube 16.0.1", "serde", ] +[[package]] +name = "mars-liquidation" +version = "1.0.0" +dependencies = [ + "cosmwasm-std", + "mars-health", + "mars-params", + "thiserror", +] + [[package]] name = "mars-oracle-base" version = "1.1.0" @@ -1830,7 +1840,7 @@ dependencies = [ "cw-storage-plus 1.1.0", "cw2 1.1.0", "mars-owner", - "mars-red-bank-types 1.1.0", + "mars-red-bank-types", "pyth-sdk-cw", "schemars", "serde", @@ -1848,9 +1858,9 @@ dependencies = [ "mars-oracle-base", "mars-osmosis", "mars-owner", - "mars-red-bank-types 1.1.0", + "mars-red-bank-types", "mars-testing", - "mars-utils 1.1.0", + "mars-utils", "osmosis-std 0.16.1", "pyth-sdk-cw", "schemars", @@ -1869,7 +1879,7 @@ dependencies = [ "cw2 1.1.0", "mars-oracle-base", "mars-owner", - "mars-red-bank-types 1.1.0", + "mars-red-bank-types", "mars-testing", "proptest", "pyth-sdk-cw", @@ -1898,24 +1908,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "mars-params" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e76688c883401778ab5f60c83e0e982f307438fbbcbfea10bbb97a950222636e" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-storage-plus 1.1.0", - "cw2 1.1.0", - "mars-owner", - "mars-red-bank-types 1.0.0", - "mars-utils 1.0.0", - "schemars", - "serde", - "thiserror", -] - [[package]] name = "mars-params" version = "1.0.7" @@ -1927,7 +1919,7 @@ dependencies = [ "cw-storage-plus 1.1.0", "cw2 1.1.0", "mars-owner", - "mars-utils 1.1.0", + "mars-utils", "schemars", "serde", "thiserror", @@ -1937,30 +1929,20 @@ dependencies = [ name = "mars-red-bank" version = "1.1.0" dependencies = [ + "anyhow", "cosmwasm-schema", "cosmwasm-std", + "cw-multi-test 0.16.5", "cw-storage-plus 1.1.0", "cw-utils 1.0.1", "cw2 1.1.0", "mars-health", + "mars-liquidation", "mars-owner", - "mars-params 1.0.2", - "mars-red-bank-types 1.1.0", + "mars-params", + "mars-red-bank-types", "mars-testing", - "mars-utils 1.1.0", - "thiserror", -] - -[[package]] -name = "mars-red-bank-types" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cca59bb17daa753c30d3c934e0779736200708cb89a34020fa805b1cb05e7278" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "mars-owner", - "mars-utils 1.0.0", + "mars-utils", "thiserror", ] @@ -1971,7 +1953,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "mars-owner", - "mars-utils 1.1.0", + "mars-utils", "strum", "thiserror", ] @@ -1985,9 +1967,9 @@ dependencies = [ "cw-storage-plus 1.1.0", "mars-osmosis", "mars-owner", - "mars-red-bank-types 1.1.0", + "mars-red-bank-types", "mars-testing", - "mars-utils 1.1.0", + "mars-utils", "osmosis-std 0.16.1", "schemars", "serde", @@ -2005,7 +1987,7 @@ dependencies = [ "cw-it", "cw2 1.1.0", "mars-oracle-wasm", - "mars-red-bank-types 1.1.0", + "mars-red-bank-types", "mars-swapper-base", "mars-testing", "test-case", @@ -2020,7 +2002,7 @@ dependencies = [ "cw-paginate", "cw-storage-plus 1.1.0", "mars-owner", - "mars-red-bank-types 1.1.0", + "mars-red-bank-types", "schemars", "serde", "thiserror", @@ -2033,7 +2015,7 @@ dependencies = [ "anyhow", "cosmwasm-std", "cw-multi-test 0.16.5", - "mars-red-bank-types 1.1.0", + "mars-red-bank-types", ] [[package]] @@ -2047,7 +2029,7 @@ dependencies = [ "cw2 1.1.0", "mars-osmosis", "mars-owner", - "mars-red-bank-types 1.1.0", + "mars-red-bank-types", "mars-swapper-base", "osmosis-std 0.16.1", ] @@ -2067,9 +2049,9 @@ dependencies = [ "mars-oracle-wasm", "mars-osmosis", "mars-owner", - "mars-params 1.0.2", + "mars-params", "mars-red-bank", - "mars-red-bank-types 1.1.0", + "mars-red-bank-types", "mars-rewards-collector", "mars-swapper-astroport", "osmosis-std 0.16.1", @@ -2077,16 +2059,6 @@ dependencies = [ "pyth-sdk-cw", ] -[[package]] -name = "mars-utils" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bae572eda20842ade4bf8ab09ce0856cae5cff89dbeb7c51e9123489e48256" -dependencies = [ - "cosmwasm-std", - "thiserror", -] - [[package]] name = "mars-utils" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index f80c4770a..2e9f2dc7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "contracts/rewards-collector", "packages/chains/*", "packages/health", + "packages/liquidation", "packages/testing", "packages/types", "packages/utils", @@ -66,8 +67,9 @@ proptest = "1.1.0" # packages mars-health = { version = "1.0.0", path = "./packages/health" } +mars-liquidation = { version = "1.0.0", path = "./packages/liquidation" } mars-osmosis = { version = "1.0.0", path = "./packages/chains/osmosis" } -mars-params = "=1.0.2" +mars-params = { version = "1.0.7", path = "./contracts/params" } mars-red-bank-types = { version = "1.0.0", path = "./packages/types" } mars-testing = { version = "1.0.0", path = "./packages/testing" } mars-utils = { version = "1.0.0", path = "./packages/utils" } diff --git a/Makefile.toml b/Makefile.toml index 9ee6396a1..0f985ef21 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -49,13 +49,6 @@ docker run --rm -v "$(pwd)":/code \ ${image} """ -# Download artifacts used in integration tests. -# NOTE: use correct version of the artifact. -[tasks.download-artifacts] -script = """ -wget https://github.com/mars-protocol/mars-common/releases/download/v1.0.0-alpha/mars_params.wasm -O $ARTIFACTS_DIR_PATH/mars_params.wasm -""" - [tasks.test] toolchain = "${RUST_VERSION}" command = "cargo" @@ -99,7 +92,6 @@ dependencies = [ "fmt", "clippy", "build", - "download-artifacts", "test", "audit", "generate-all-schemas", diff --git a/contracts/red-bank/Cargo.toml b/contracts/red-bank/Cargo.toml index 56de49c9f..d9c406421 100644 --- a/contracts/red-bank/Cargo.toml +++ b/contracts/red-bank/Cargo.toml @@ -27,6 +27,7 @@ cw2 = { workspace = true } cw-storage-plus = { workspace = true } cw-utils = { workspace = true } mars-health = { workspace = true } +mars-liquidation = { workspace = true } mars-owner = { workspace = true } mars-params = { workspace = true } mars-red-bank-types = { workspace = true } @@ -34,5 +35,7 @@ mars-utils = { workspace = true } thiserror = { workspace = true } [dev-dependencies] +anyhow = { workspace = true } cosmwasm-schema = { workspace = true } +cw-multi-test = { workspace = true } mars-testing = { workspace = true } diff --git a/contracts/red-bank/src/contract.rs b/contracts/red-bank/src/contract.rs index 8ea37a7af..7017c542a 100644 --- a/contracts/red-bank/src/contract.rs +++ b/contracts/red-bank/src/contract.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response}; use mars_red_bank_types::red_bank::{ExecuteMsg, InstantiateMsg, QueryMsg}; -use crate::{error::ContractError, execute, query}; +use crate::{error::ContractError, execute, liquidate, query}; #[entry_point] pub fn instantiate( @@ -74,7 +74,7 @@ pub fn execute( } => { let user_addr = deps.api.addr_validate(&user)?; let sent_coin = cw_utils::one_coin(&info)?; - execute::liquidate( + liquidate::liquidate( deps, env, info, diff --git a/contracts/red-bank/src/error.rs b/contracts/red-bank/src/error.rs index 91d79390c..8f07ce8b1 100644 --- a/contracts/red-bank/src/error.rs +++ b/contracts/red-bank/src/error.rs @@ -1,6 +1,7 @@ -use cosmwasm_std::{OverflowError, StdError}; +use cosmwasm_std::{CheckedFromRatioError, CheckedMultiplyFractionError, OverflowError, StdError}; use cw_utils::PaymentError; use mars_health::error::HealthError; +use mars_liquidation::error::LiquidationError; use mars_owner::OwnerError; use mars_red_bank_types::error::MarsError; use mars_utils::error::ValidationError; @@ -26,9 +27,18 @@ pub enum ContractError { #[error("{0}")] Overflow(#[from] OverflowError), + #[error("{0}")] + CheckedFromRatio(#[from] CheckedFromRatioError), + + #[error("{0}")] + CheckedMultiplyFraction(#[from] CheckedMultiplyFractionError), + #[error("{0}")] Health(#[from] HealthError), + #[error("{0}")] + Liquidation(#[from] LiquidationError), + #[error("Price not found for asset: {denom:?}")] PriceNotFound { denom: String, diff --git a/contracts/red-bank/src/execute.rs b/contracts/red-bank/src/execute.rs index f3f0919b5..10b157932 100644 --- a/contracts/red-bank/src/execute.rs +++ b/contracts/red-bank/src/execute.rs @@ -1,10 +1,5 @@ -use std::{cmp::min, str}; - -use cosmwasm_std::{ - Addr, Decimal, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Uint128, -}; +use cosmwasm_std::{Addr, Decimal, DepsMut, Env, MessageInfo, Response, StdResult, Uint128}; use mars_owner::{OwnerInit::SetInitialOwner, OwnerUpdate}; -use mars_params::types::AssetParams; use mars_red_bank_types::{ address_provider::{self, MarsAddressType}, error::MarsError, @@ -12,18 +7,17 @@ use mars_red_bank_types::{ Config, CreateOrUpdateConfig, Debt, InitOrUpdateAssetParams, InstantiateMsg, Market, }, }; -use mars_utils::{ - helpers::{build_send_asset_msg, option_string_to_addr, validate_native_denom, zero_address}, - math, +use mars_utils::helpers::{ + build_send_asset_msg, option_string_to_addr, validate_native_denom, zero_address, }; use crate::{ error::ContractError, health::{ assert_below_liq_threshold_after_withdraw, assert_below_max_ltv_after_borrow, - assert_liquidatable, + get_health_and_positions, }, - helpers::{query_asset_params, query_close_factor}, + helpers::query_asset_params, interest_rates::{ apply_accumulated_interests, get_scaled_debt_amount, get_scaled_liquidity_amount, get_underlying_debt_amount, get_underlying_liquidity_amount, update_interest_rates, @@ -691,281 +685,6 @@ pub fn repay( .add_attribute("amount_scaled", debt_amount_scaled_delta)) } -/// Execute loan liquidations on under-collateralized loans -pub fn liquidate( - deps: DepsMut, - env: Env, - info: MessageInfo, - collateral_denom: String, - debt_denom: String, - user_addr: Addr, - sent_debt_amount: Uint128, - recipient: Option, -) -> Result { - let block_time = env.block.time.seconds(); - let user = User(&user_addr); - // The recipient address for receiving underlying collateral - let recipient_addr = option_string_to_addr(deps.api, recipient, info.sender.clone())?; - let recipient = User(&recipient_addr); - - // 1. Validate liquidation - - // User cannot liquidate themselves - if info.sender == user_addr { - return Err(ContractError::CannotLiquidateSelf {}); - } - - // If user (contract) has a positive uncollateralized limit then the user - // cannot be liquidated - if !user.uncollateralized_loan_limit(deps.storage, &debt_denom)?.is_zero() { - return Err(ContractError::CannotLiquidateWhenPositiveUncollateralizedLoanLimit {}); - }; - - // check if the user has enabled the collateral asset as collateral - let user_collateral = COLLATERALS - .may_load(deps.storage, (&user_addr, &collateral_denom))? - .ok_or(ContractError::CannotLiquidateWhenNoCollateralBalance {})?; - if !user_collateral.enabled { - return Err(ContractError::CannotLiquidateWhenCollateralUnset { - denom: collateral_denom, - }); - } - - // check if user has available collateral in specified collateral asset to be liquidated - let collateral_market = MARKETS.load(deps.storage, &collateral_denom)?; - - // check if user has outstanding debt in the deposited asset that needs to be repayed - let user_debt = DEBTS - .may_load(deps.storage, (&user_addr, &debt_denom))? - .ok_or(ContractError::CannotLiquidateWhenNoDebtBalance {})?; - - // 2. Compute health factor - let config = CONFIG.load(deps.storage)?; - - let addresses = address_provider::helpers::query_contract_addrs( - deps.as_ref(), - &config.address_provider, - vec![ - MarsAddressType::Oracle, - MarsAddressType::Incentives, - MarsAddressType::RewardsCollector, - MarsAddressType::Params, - ], - )?; - let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; - let incentives_addr = &addresses[&MarsAddressType::Incentives]; - let oracle_addr = &addresses[&MarsAddressType::Oracle]; - let params_addr = &addresses[&MarsAddressType::Params]; - - let (liquidatable, assets_positions) = - assert_liquidatable(&deps.as_ref(), &env, &user_addr, oracle_addr, params_addr)?; - - if !liquidatable { - return Err(ContractError::CannotLiquidateHealthyPosition {}); - } - - let collateral_and_debt_are_the_same_asset = debt_denom == collateral_denom; - - let debt_market = if !collateral_and_debt_are_the_same_asset { - MARKETS.load(deps.storage, &debt_denom)? - } else { - collateral_market.clone() - }; - - // 3. Compute debt to repay and collateral to liquidate - let collateral_price = assets_positions - .get(&collateral_denom) - .ok_or(ContractError::CannotLiquidateWhenNoCollateralBalance {})? - .asset_price; - let debt_price = assets_positions - .get(&debt_denom) - .ok_or(ContractError::CannotLiquidateWhenNoDebtBalance {})? - .asset_price; - - let mut response = Response::new(); - - let user_debt_amount = - get_underlying_debt_amount(user_debt.amount_scaled, &debt_market, block_time)?; - - let collateral_params = query_asset_params(&deps.querier, params_addr, &collateral_denom)?; - let close_factor = query_close_factor(&deps.querier, params_addr)?; - - let ( - debt_amount_to_repay, - collateral_amount_to_liquidate, - collateral_amount_to_liquidate_scaled, - refund_amount, - ) = liquidation_compute_amounts( - user_collateral.amount_scaled, - user_debt_amount, - sent_debt_amount, - &collateral_market, - &collateral_params, - collateral_price, - debt_price, - block_time, - close_factor, - )?; - - // 4. Transfer collateral shares from the user to the liquidator - response = user.decrease_collateral( - deps.storage, - &collateral_market, - collateral_amount_to_liquidate_scaled, - incentives_addr, - response, - )?; - response = recipient.increase_collateral( - deps.storage, - &collateral_market, - collateral_amount_to_liquidate_scaled, - incentives_addr, - response, - )?; - - // 5. Reduce the user's debt shares - let user_debt_amount_after = user_debt_amount.checked_sub(debt_amount_to_repay)?; - let user_debt_amount_scaled_after = - get_scaled_debt_amount(user_debt_amount_after, &debt_market, block_time)?; - - // Compute delta so it can be substracted to total debt - let debt_amount_scaled_delta = - user_debt.amount_scaled.checked_sub(user_debt_amount_scaled_after)?; - - user.decrease_debt(deps.storage, &debt_denom, debt_amount_scaled_delta)?; - - let debt_market_debt_total_scaled_after = - debt_market.debt_total_scaled.checked_sub(debt_amount_scaled_delta)?; - - // 6. Update markets depending on whether the collateral and debt markets are the same - // and whether the liquidator receives coins (no change in liquidity) or underlying asset - // (changes liquidity) - if collateral_and_debt_are_the_same_asset { - // NOTE: for the sake of clarity copy attributes from collateral market and - // give generic naming. Debt market could have been used as well - let mut asset_market_after = collateral_market; - let denom = &collateral_denom; - - response = apply_accumulated_interests( - deps.storage, - &env, - &mut asset_market_after, - rewards_collector_addr, - incentives_addr, - response, - )?; - - asset_market_after.debt_total_scaled = debt_market_debt_total_scaled_after; - - response = update_interest_rates(&env, &mut asset_market_after, response)?; - - MARKETS.save(deps.storage, denom, &asset_market_after)?; - } else { - let mut debt_market_after = debt_market; - - response = apply_accumulated_interests( - deps.storage, - &env, - &mut debt_market_after, - rewards_collector_addr, - incentives_addr, - response, - )?; - - debt_market_after.debt_total_scaled = debt_market_debt_total_scaled_after; - - response = update_interest_rates(&env, &mut debt_market_after, response)?; - - MARKETS.save(deps.storage, &debt_denom, &debt_market_after)?; - } - - // 7. Build response - // refund sent amount in excess of actual debt amount to liquidate - if !refund_amount.is_zero() { - response = - response.add_message(build_send_asset_msg(&info.sender, &debt_denom, refund_amount)); - } - - Ok(response - .add_attribute("action", "liquidate") - .add_attribute("user", user) - .add_attribute("liquidator", info.sender.to_string()) - .add_attribute("recipient", recipient) - .add_attribute("collateral_denom", collateral_denom) - .add_attribute("collateral_amount", collateral_amount_to_liquidate) - .add_attribute("collateral_amount_scaled", collateral_amount_to_liquidate_scaled) - .add_attribute("debt_denom", debt_denom) - .add_attribute("debt_amount", debt_amount_to_repay) - .add_attribute("debt_amount_scaled", debt_amount_scaled_delta)) -} - -/// Computes debt to repay (in debt asset), -/// collateral to liquidate (in collateral asset) and -/// amount to refund the liquidator (in debt asset) -pub fn liquidation_compute_amounts( - user_collateral_amount_scaled: Uint128, - user_debt_amount: Uint128, - sent_debt_amount: Uint128, - collateral_market: &Market, - collateral_params: &AssetParams, - collateral_price: Decimal, - debt_price: Decimal, - block_time: u64, - close_factor: Decimal, -) -> StdResult<(Uint128, Uint128, Uint128, Uint128)> { - // Debt: Only up to a fraction of the total debt (determined by the close factor) can be - // repayed. - let mut debt_amount_to_repay = min(sent_debt_amount, close_factor * user_debt_amount); - - // Collateral: debt to repay in base asset times the liquidation bonus - let mut collateral_amount_to_liquidate = math::divide_uint128_by_decimal( - debt_amount_to_repay * debt_price * (Decimal::one() + collateral_params.liquidation_bonus), - collateral_price, - )?; - let mut collateral_amount_to_liquidate_scaled = - get_scaled_liquidity_amount(collateral_amount_to_liquidate, collateral_market, block_time)?; - - // If collateral amount to liquidate is higher than user_collateral_balance, - // liquidate the full balance and adjust the debt amount to repay accordingly - if collateral_amount_to_liquidate_scaled > user_collateral_amount_scaled { - collateral_amount_to_liquidate_scaled = user_collateral_amount_scaled; - collateral_amount_to_liquidate = get_underlying_liquidity_amount( - collateral_amount_to_liquidate_scaled, - collateral_market, - block_time, - )?; - debt_amount_to_repay = math::divide_uint128_by_decimal( - math::divide_uint128_by_decimal( - collateral_amount_to_liquidate * collateral_price, - debt_price, - )?, - Decimal::one() + collateral_params.liquidation_bonus, - )?; - } - - // In some edges scenarios: - // - if debt_amount_to_repay = 0, some liquidators could drain collaterals and all their coins - // would be refunded, i.e.: without spending coins. - // - if collateral_amount_to_liquidate is 0, some users could liquidate without receiving collaterals - // in return. - if (!collateral_amount_to_liquidate.is_zero() && debt_amount_to_repay.is_zero()) - || (collateral_amount_to_liquidate.is_zero() && !debt_amount_to_repay.is_zero()) - { - return Err(StdError::generic_err( - format!("Can't process liquidation. Invalid collateral_amount_to_liquidate ({collateral_amount_to_liquidate}) and debt_amount_to_repay ({debt_amount_to_repay})") - )); - } - - let refund_amount = sent_debt_amount - debt_amount_to_repay; - - Ok(( - debt_amount_to_repay, - collateral_amount_to_liquidate, - collateral_amount_to_liquidate_scaled, - refund_amount, - )) -} - /// Update (enable / disable) collateral asset for specific user pub fn update_asset_collateral_status( deps: DepsMut, @@ -1002,10 +721,15 @@ pub fn update_asset_collateral_status( let oracle_addr = &addresses[&MarsAddressType::Oracle]; let params_addr = &addresses[&MarsAddressType::Params]; - let (liquidatable, _) = - assert_liquidatable(&deps.as_ref(), &env, user.address(), oracle_addr, params_addr)?; + let (health, _) = get_health_and_positions( + &deps.as_ref(), + &env, + user.address(), + oracle_addr, + params_addr, + )?; - if liquidatable { + if health.is_liquidatable() { return Err(ContractError::InvalidHealthFactorAfterDisablingCollateral {}); } } diff --git a/contracts/red-bank/src/health.rs b/contracts/red-bank/src/health.rs index bce9c0fd1..1793cbc6b 100644 --- a/contracts/red-bank/src/health.rs +++ b/contracts/red-bank/src/health.rs @@ -11,18 +11,18 @@ use crate::{ state::{COLLATERALS, DEBTS, MARKETS}, }; -/// Check the Health Factor for a given user -pub fn assert_liquidatable( +/// Get health and positions for a given user +pub fn get_health_and_positions( deps: &Deps, env: &Env, user_addr: &Addr, oracle_addr: &Addr, params_addr: &Addr, -) -> Result<(bool, HashMap), ContractError> { +) -> Result<(Health, HashMap), ContractError> { let positions = get_user_positions_map(deps, env, user_addr, oracle_addr, params_addr)?; let health = compute_position_health(&positions)?; - Ok((health.is_liquidatable(), positions)) + Ok((health, positions)) } /// Check the Health Factor for a given user after a withdraw diff --git a/contracts/red-bank/src/helpers.rs b/contracts/red-bank/src/helpers.rs index 3d1d55c27..b6de4095e 100644 --- a/contracts/red-bank/src/helpers.rs +++ b/contracts/red-bank/src/helpers.rs @@ -1,5 +1,5 @@ use cosmwasm_std::{Decimal, QuerierWrapper, StdResult}; -use mars_params::{msg::QueryMsg, types::AssetParams}; +use mars_params::{msg::QueryMsg, types::asset::AssetParams}; pub fn query_asset_params( querier: &QuerierWrapper, @@ -14,9 +14,9 @@ pub fn query_asset_params( ) } -pub fn query_close_factor( +pub fn query_target_health_factor( querier: &QuerierWrapper, params: impl Into, ) -> StdResult { - querier.query_wasm_smart(params.into(), &QueryMsg::MaxCloseFactor {}) + querier.query_wasm_smart(params.into(), &QueryMsg::TargetHealthFactor {}) } diff --git a/contracts/red-bank/src/lib.rs b/contracts/red-bank/src/lib.rs index 3e65712c9..165013c9a 100644 --- a/contracts/red-bank/src/lib.rs +++ b/contracts/red-bank/src/lib.rs @@ -4,6 +4,7 @@ pub mod error; pub mod execute; pub mod health; pub mod interest_rates; +pub mod liquidate; pub mod query; pub mod state; pub mod user; diff --git a/contracts/red-bank/src/liquidate.rs b/contracts/red-bank/src/liquidate.rs new file mode 100644 index 000000000..17ac92393 --- /dev/null +++ b/contracts/red-bank/src/liquidate.rs @@ -0,0 +1,226 @@ +use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response, Uint128}; +use mars_liquidation::liquidation::calculate_liquidation_amounts; +use mars_red_bank_types::address_provider::{self, MarsAddressType}; +use mars_utils::helpers::{build_send_asset_msg, option_string_to_addr}; + +use crate::{ + error::ContractError, + health::get_health_and_positions, + helpers::{query_asset_params, query_target_health_factor}, + interest_rates::{ + apply_accumulated_interests, get_scaled_debt_amount, get_scaled_liquidity_amount, + get_underlying_debt_amount, get_underlying_liquidity_amount, update_interest_rates, + }, + state::{COLLATERALS, CONFIG, DEBTS, MARKETS}, + user::User, +}; + +/// Execute loan liquidations on under-collateralized loans +pub fn liquidate( + deps: DepsMut, + env: Env, + info: MessageInfo, + collateral_denom: String, + debt_denom: String, + liquidatee_addr: Addr, + sent_debt_amount: Uint128, + recipient: Option, +) -> Result { + let block_time = env.block.time.seconds(); + + let liquidatee = User(&liquidatee_addr); + + // The recipient address for receiving collateral + let recipient_addr = option_string_to_addr(deps.api, recipient, info.sender.clone())?; + let recipient = User(&recipient_addr); + + // 1. Validate liquidation + + // User cannot liquidate themselves + if info.sender == liquidatee_addr { + return Err(ContractError::CannotLiquidateSelf {}); + } + + // If user (contract) has a positive uncollateralized limit then the user + // cannot be liquidated + if !liquidatee.uncollateralized_loan_limit(deps.storage, &debt_denom)?.is_zero() { + return Err(ContractError::CannotLiquidateWhenPositiveUncollateralizedLoanLimit {}); + }; + + // check if the user has enabled the collateral asset as collateral + let user_collateral = COLLATERALS + .may_load(deps.storage, (&liquidatee_addr, &collateral_denom))? + .ok_or(ContractError::CannotLiquidateWhenNoCollateralBalance {})?; + if !user_collateral.enabled { + return Err(ContractError::CannotLiquidateWhenCollateralUnset { + denom: collateral_denom, + }); + } + + // check if user has outstanding debt in the deposited asset that needs to be repayed + let user_debt = DEBTS + .may_load(deps.storage, (&liquidatee_addr, &debt_denom))? + .ok_or(ContractError::CannotLiquidateWhenNoDebtBalance {})?; + + // check if user has available collateral in specified collateral asset to be liquidated + let collateral_market = MARKETS.load(deps.storage, &collateral_denom)?; + + // 2. Compute health factor + let config = CONFIG.load(deps.storage)?; + + let addresses = address_provider::helpers::query_contract_addrs( + deps.as_ref(), + &config.address_provider, + vec![ + MarsAddressType::Oracle, + MarsAddressType::Incentives, + MarsAddressType::RewardsCollector, + MarsAddressType::Params, + ], + )?; + let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; + let incentives_addr = &addresses[&MarsAddressType::Incentives]; + let oracle_addr = &addresses[&MarsAddressType::Oracle]; + let params_addr = &addresses[&MarsAddressType::Params]; + + let (health, assets_positions) = + get_health_and_positions(&deps.as_ref(), &env, &liquidatee_addr, oracle_addr, params_addr)?; + + if !health.is_liquidatable() { + return Err(ContractError::CannotLiquidateHealthyPosition {}); + } + + let debt_market = if debt_denom != collateral_denom { + MARKETS.load(deps.storage, &debt_denom)? + } else { + collateral_market.clone() + }; + + // 3. Compute debt to repay and collateral to liquidate + let collateral_price = assets_positions + .get(&collateral_denom) + .ok_or(ContractError::CannotLiquidateWhenNoCollateralBalance {})? + .asset_price; + let debt_price = assets_positions + .get(&debt_denom) + .ok_or(ContractError::CannotLiquidateWhenNoDebtBalance {})? + .asset_price; + + let mut response = Response::new(); + + let user_debt_amount = + get_underlying_debt_amount(user_debt.amount_scaled, &debt_market, block_time)?; + + let collateral_params = query_asset_params(&deps.querier, params_addr, &collateral_denom)?; + let target_health_factor = query_target_health_factor(&deps.querier, params_addr)?; + + let user_collateral_amount = get_underlying_liquidity_amount( + user_collateral.amount_scaled, + &collateral_market, + block_time, + )?; + let ( + debt_amount_to_repay, + collateral_amount_to_liquidate, + collateral_amount_received_by_liquidator, + ) = calculate_liquidation_amounts( + user_collateral_amount, + collateral_price, + &collateral_params, + user_debt_amount, + sent_debt_amount, + debt_price, + target_health_factor, + &health, + )?; + let protocol_fee = collateral_amount_to_liquidate - collateral_amount_received_by_liquidator; + + let refund_amount = sent_debt_amount - debt_amount_to_repay; + + let collateral_amount_to_liquidate_scaled = get_scaled_liquidity_amount( + collateral_amount_to_liquidate, + &collateral_market, + block_time, + )?; + + let collateral_amount_received_by_liquidator_scaled = get_scaled_liquidity_amount( + collateral_amount_received_by_liquidator, + &collateral_market, + block_time, + )?; + + let protocol_fee_scaled = + get_scaled_liquidity_amount(protocol_fee, &collateral_market, block_time)?; + + // 4. Transfer collateral shares from the user to the liquidator and rewards-collector (protocol fee) + response = liquidatee.decrease_collateral( + deps.storage, + &collateral_market, + collateral_amount_to_liquidate_scaled, + incentives_addr, + response, + )?; + response = recipient.increase_collateral( + deps.storage, + &collateral_market, + collateral_amount_received_by_liquidator_scaled, + incentives_addr, + response, + )?; + if !protocol_fee.is_zero() { + response = User(rewards_collector_addr).increase_collateral( + deps.storage, + &collateral_market, + protocol_fee_scaled, + incentives_addr, + response, + )?; + } + + // 5. Reduce the user's debt shares + let user_debt_amount_after = user_debt_amount.checked_sub(debt_amount_to_repay)?; + let user_debt_amount_scaled_after = + get_scaled_debt_amount(user_debt_amount_after, &debt_market, block_time)?; + + // Compute delta so it can be substracted to total debt + let debt_amount_scaled_delta = + user_debt.amount_scaled.checked_sub(user_debt_amount_scaled_after)?; + + liquidatee.decrease_debt(deps.storage, &debt_denom, debt_amount_scaled_delta)?; + + let market_debt_total_scaled_after = + debt_market.debt_total_scaled.checked_sub(debt_amount_scaled_delta)?; + + // 6. Update markets + let mut debt_market_after = debt_market; + response = apply_accumulated_interests( + deps.storage, + &env, + &mut debt_market_after, + rewards_collector_addr, + incentives_addr, + response, + )?; + debt_market_after.debt_total_scaled = market_debt_total_scaled_after; + response = update_interest_rates(&env, &mut debt_market_after, response)?; + MARKETS.save(deps.storage, &debt_denom, &debt_market_after)?; + + // 7. Build response + // refund sent amount in excess of actual debt amount to liquidate + if !refund_amount.is_zero() { + response = + response.add_message(build_send_asset_msg(&info.sender, &debt_denom, refund_amount)); + } + + Ok(response + .add_attribute("action", "liquidate") + .add_attribute("user", liquidatee) + .add_attribute("liquidator", info.sender.to_string()) + .add_attribute("recipient", recipient) + .add_attribute("collateral_denom", collateral_denom) + .add_attribute("collateral_amount", collateral_amount_to_liquidate) + .add_attribute("collateral_amount_scaled", collateral_amount_to_liquidate_scaled) + .add_attribute("debt_denom", debt_denom) + .add_attribute("debt_amount", debt_amount_to_repay) + .add_attribute("debt_amount_scaled", debt_amount_scaled_delta)) +} diff --git a/contracts/red-bank/tests/files/Red Bank - Dynamic LB & CF test cases v1.1.xlsx b/contracts/red-bank/tests/files/Red Bank - Dynamic LB & CF test cases v1.1.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..5d50903e1da697fe1e22a4e7cc3c929cc7aad216 GIT binary patch literal 20427 zcmeIa1yGz@x95$!YvaKk8VK(0?(S}Z;7)LtpuwGB!QI{6HE3`rK(HY>cjn}rIrGl_ z-n#WwP1SvXrkm>ivwHP@_WJF$*LoDB!67g}U|?WCpoH(#Kz=&_ulL<-7)=}vT`ld* z-~Z1$Obl+eHW`z;cHKY(=yUXAB-M3GEdaVOhTfzc5a}zq9bR0_FjC&C&y*CKr6(w% z?Fj$<*_fSE{hf#rx)}|}V*gEV2{{+5$ z^(?y(SkDS~E0}@*gM&B_%jgH>t=ZSI&(IZ$yb6z?{k|Vq!ofn=l4u%_sFx&!o_Jt6 zo^fy129?@}=ky2HFa|&gS8kVdv z&;Rl33lr)FDTKlW#>3W)S%Gz5LPxZis7T?U*Svxc^|zWWr`0p0rDR8i49CsKtmLJ3 zw`M?iZz6L0>FH_A4Y)nuRf5jd<`>=XDmYot&bYpe;Y2q?rBUQ2MqkqW;4_Yto#`%D z%2Wk|17z!E|@Xz{KxL{K~p6lF{z8auGvOYC0s*lMJ&BG!=-=9uC%Vni) z$Y097&B@5u4wk|xF#)7i-ihK+n5NA4)CSq5Dhvlo1qa_a>c@E)&Z1gjG<}R|D=v6)`PTojj9o{3-QTBO=`1UyY;{ zy=f;3hE*gK%@QR7t83X6{g46y!_W_g`V9htknh$lFgTn^BoYA_%tq?KA{Ndh6p0TM zgCfnic4JZtWFkQTF?=i$r3_~hj3hRYq3h3NMNh;}&L2avQxNl;1`V?vOazZ#j4c2c z^1^$6((TvV;$SGiu+|KG!>z`E5PIv0O|kDleWdfFg5k78&PNz7@Ic8(2tD9&d=&Sp zZ=pBDhn*OqU5|$TYN=k)DJyBojDxDi_R6K1%z5eL@D$kbe13nmz>;&LGucj_m)c;f zVciFm8y&8O3En5K+d8=}RF>wh>rT0{R;DMHIH6a;>xr#DBQ4UqaA`igW>!8cFGzMM zzHQqnhxg>hM#5!m+~_jqKahl3lMTzu=m#zm6dF4BFN>hyTc5gk;OTzJ3yOOy`6LNK zdJjfzE2--MmJb?m~7=k-%pEj&Gt#jJ0H$ zKp%LX*(GWz*71;Egi;W zP8`%CY;t<1|bd=yHmMe1=*z}iU{$MC5K83&ml*A zS5IJXtlHAF5(Sd?+ekE_ykhZWy8{fOc>_Z_GO@!Scc)o3F{Se6-GNN*EfQk(*cTG? zHQzLZZ#1K~dhmm^8Nv?U&M$Yfn@8+FMn(pui3?F8)+zDJbaU*X!v9Dwov_3pnx$Hm zmxxqhblrH=anra?yC~sfwpo$&ubQgEV4fplNR5fFqJIZO%~7_M&ejcTG=R;)02t&s&sC++{GxkxFcGbo+Evz*NO0;c#{(&)=(HdYAf`)$N z({-n^c(0T%8^<7MRQVaDX(KtNxd6DT1USw?BSkKu1GQw$4J zjP0GCD1UTUjpkZQw&+PN#wB(7y;jYzcG9CArq<34 z+1XUlDHmcj<65)yY=c8|-f^*!Tuf`^sY)TA2X=x}9jlool0^P8Ghw@V&2g23vl-f~ zdAY#ns$K*?xVuUQW^?g-iRn&87&SW<4)b(c7@>6}x8-_`>|V3E<0{K0lk;YGXSK7T zTTD=MjN_{KQ~@D$!?c2fMG%))P4USG`0CXGBf;q95o>g*9}C0`ikxr0`&Yi9~AobL@dy*fY3mq5ZQ*1>|$yqEh_hG z_wML~SC2j`GD{6!5CbjhqKILLpDEpqmOdzJyJ<5p8!7=uPsF7CmuoGsHZ7=5v+q zhI}}ZoG#+h^>)xC=vk8n1rtI7*}LcEww`!%ED_zXFX1=nKYdVMd?Z`*`VMJcm&E_q zaX>J%5SGsp=ZB5 zn`kHP=;Acy3Byj;OjNiOtR<$rJg1*P$4Dv`-ftR76rR3|#b>)`<6YH7=y-AGg$)U~ z1Q69<@bQyj_vlMjBy+K-|#BJ&P}a zUi*9GezlYGj%ho$9J@QkE12L>y04Sf2^tP$+gxEqR5#wad!)b&LmT$RIAsy~&bd&3 z8i?5Z%rQzfAd;#$G#dE4WL$6GH4)e`X16ms7{)cvkAC(FCJV%`U;=p9Ix*|R>q}^h zdIb}^xW3lHM=iR&`-4V##4g-qtnNB!g>KHpa`iK2yWFL1pbuqK%%(J&-J6-&yr;BZ=ujt7k_3TBiVZf6~tcXu} zd*hN4@_V7b`jq{px8@7ul2+H|8)@XRiMyufdouG6)VAa&%qqhQYiG{ zLi*Z~JEeYg=GiYv5Ag^NE09ggT(@368pAjc;N1yu z26)cP%Hp*jMardct@~o_elz0D1ZN#X(6mxArw3+}Z z+(dn1TrHo=#WIlqYk{c4>z)lwYXahg6K5hrckB@M6-~-&M!*2M$+O{0OR}X&^;SVl z<_m>dXl@EW*`zy`?aE*EOG-w$cH8Kz=gJfwaT^hty>q22{ATu0d<)vR+l0$xQ-Vgk zC5-TzkyVT~sVxaiy55lG&Lu^?oHYElMSSzSht*aQDLkbQ686NOq4RoA6s@+VE79=5 zD~$b&XwA%tmKWhB7)w943P+rZNnlfcC`Au52;?zVjiVQ!c$2OKcyl%f0!b$Vddby} zKph#Ic@`0S<^dsAJc8Q=;{F9{@Rl;igw8*?C&!r1&%xcc42i5%=@> zHqi!ulnD)9sj%e*nMso%9W%@4-T~fY&cSLiI_3$gB)`kdYB{GO0U;HOVtChAIAM+? zE=DkhVd%j^7)C@4e(M@BUait{nJH3?_6MBM5(l*rGqvEF$;MES2uPY2)GpE*zSUN2 zSSlrk$@e}Q;U7R z(7BgkS7E@q@i}grX?PgyYs`3eb1CJaO=!#- zOM?ihx`j!Bm{WA>&{q?EIri&v1+NE}k0eqEVI9~- zY=+=K)nxSsa`6KB&|RYS-vv`o+r^$2Amv(TkCP*OLxWN->>ZS*zKX05=%(0}^3PFk z9H(53mp7PCNI?mbqAZz$O=0hreeN7B}zcN`gTx@3D;}` z;p}*QICAZt{9~*E)#3hv(oq;@fDM_}*qjV*RpdCf>XcAOtou+uMzvr1?b$(0(|9Jn zxx5>%?lKUlx&q{F!&tyc+>Q*-yyS3;2o8Ue8+>vd8TP@y?1va|JD!N&V$h^`@aA0H zDH4AAhG(p@ZohA{Mka!&KiVMrm_T?6)kn9chY)d7?-TrPc0&OB1?*=aPGah!5PpRb z@qZsieud+If{}W7RLkWkm^CTh&R7|uMGE4unDuoV40r~e@yAj2N7JWtN*78ZF>A6N z9&j-+P2Nv>-37e1x1B;Fc@EXVWONwW6>Phy6p;rM2zl^y#1s+$BPVF$54aZno#mt- z_%KWAWoAc zam_0Vo8IA?RrxgxQZdm;waViUA_Kj_to?Usw4>ln)nzchEwDh!Exv#5@} z{m`>%c-TDmgKzBPFg4+H+pjbUXXtC1q``w~#$*Pt%bo0@_jOOQo~Qv~=8_)2&n2yV zrqsxLbml3CoO(vr5oJ^Tc1d`yYIh=(VPf7?5pyM=hQV=&j6|d5;EKx@EZhTh_ozW6 zfpT|ZyGKNUFOv*bqcX{AZ0RUKNWqE zd3+fV;Ymgj=tD2-Tj`OS52yCUiy971C%MuTzOPH42;VkLKAHKlTg*f6|Y&6 zh%j0ruaVPJZ5z@frUgXPhO zNbxkMGdLb~AK4BD7u74}YUki>#mtkHjiL8f50dS$ z1k0Msw#BtM!@*1;qKwNx%br@ILekn5h-nH1wztJ05Alc>&7kgFTxWHtaST6U1S!x} z4WD+izMCDkO$uqqOASjtuUbu3VV)>SsH4OX{l27kW}u1vJ(0EY1^wi3pesdkD^pG| ztRe-X(G-cs!+4I1kUr{<&{!~`jW^f$_I^wXk)TENDKfQq$U$;>sS9x!MM3M0B9Wu1f3Nr8G#IfS(RmI+Qs%k@lcK$`+vtNGM$7}Dl#!`<)fP8M2g%rwmeB{G% zD6{<8)+^>LTRibw$yB@kJmxhv`yy#2#+I@Qq^qMC{dO;IRK1@l!kN*u5?tqkaMkNk zLl;QxF;wox-FC1#Jzfs8cf9N*&EenRBz3w;OZE?_FTC-f}T8s9@1t! zKGA`BtNBj8`@Yj=vLHdj$`XQQ&5*0hAy*cnD{RlDZ)LWbfLZIR~Up5lDerYlhFh5^aM1EWCRJ)Y?SssVoFLC0Y-{eSfi$_6He( z9fg!PFLMw5@l4&7IS}bNHvfe%SaKax(p?PjwL*ZiEnZ)Y@I3= z<_71m3n!GR7~UA>up989r4lm0N;rb}WGa`Pu1?!s-e?wvI`&%0XjDvMb3QgD4cCnR z6+_-};5*ysdsid(&ye3Nf2JwvHfs9O_`{2o%aD%C(55yxWeaVY8J)yam`Ag+xCfe0 zg`Q32BRbW*a!FBMXz@%HY=yZCHPMYpz=M8}<*fwPk_QYCn_s~NZn6_OV^AjD9J(&q)_W$BI^dHt zEruh>_C)fuJEkkDye>b+HpOhWJb_Jjf5)B$xVvy(!fC3|eSxl==3T`RcZ5tbvf$eB zBwq7gVmi1dj!-S0pUJcD~BLb?}rL~LC802{ENa`HEKP-z%5N3p2KDC=+%G8Ra& z{S`319104!NH%GydhU_VIi;o(=SG19lVHT)rRfKW1LwoWmC%wh{H~FWZAe`lt8O}$ z+!|XcdeI1s;!vfWEQ}Hsy$-DAYyF7s)1`5m6|x3`A5$u=E!VzI>b*!2$BDP$;^;*o zB(Y){In3RUCeEl1uBx|SfkI&*m6W?`BW#QSH6F<*rg!}>K8IeBZxl+h3zAd@g-UQt z!lqP^BP^gnNxuRK3-wi$H#A&XfRJos1;wh8tif193Jkd41TqaLaxpmaTbJch7%ewm?Yq4wXoM6EFp4L?dx6x=A0<)B81(5P z@L_RNj`k$@)a1hXWOWc?aq-Ww@y)A_I6g8}P>vrE=$yoXl_%xHpFK&I zS42ipFk`mk{QBA#u@OZ~b%d|{$CwuCK5j3=A{W8q1q;&Cr}(q=IrHu|X6AbGATi|T zY-cB5eqGCrCMs%0>|(Y1aaG$YjeTIJ9yQ}-??w~D?$m5Gk`p{(p2qAx{M|Lr z@slk>|IhS@P~#y{_J5>DXkh?4;;%#^^6wMLuaNvtM4~1r^2vo%s6h!CNHzmqFDbE= z*T9a~_W?ech5riCSCO|UZfz)}3m2Ai!Y@uy4Y}~+J4?AE-b$1y3;c3oSQg0QVO57h zRO4pYr>|hrp|EH_0BeGw*&d&O?~VrZ^Xq)NQ^Yo4Lk`0U!eo<90$+EUZ&yYumBxZcigsFR@&L6FP4d9M64 zsrT~s+au6JW`u?|8r%4->pv4Z;R%FR*1Tk=E{?;;6s5ZaZhs0hyE=6Fh}O2E^Pu4{&92r zN6ICKRVKROhIbeb-u1Fy!cVB>W1Pj63PbYE_I?5h`QoK{0K-K6E08>Rw9`9|_tGtL z9bB>5BEoq>?XEOv`~;Gjo=8PLAgzx{NZIq;GPxxoV}H?Q<5zqZOEJutuF(CI?%)dqUbZ&_UyDvQiYGUZ<$HFBFd8JcNZM%=;UsQxeN72 z6rG1tuY7e2L$(f#M%fsM?vmEl7RLN)w78Tip3c>i_*9p_Db_^!V)RYyC6855<{N)m z@ngwYQC7viw_W}$usy}FMp-0GfF06Nqw77xH2K8`uto2^@Fq+1<U>(M>pCs|;a-dw*Gay>-?{)nGq2f!L!KSU0W z10?!84f6e%CIJ^-oe?=?2#7++D8cqxPGmTw0aMVCiZKobV-j}J(z7Q>C5ljf(_pX< zv9$1Hhr;t%Fpdu?)_71ht$27)#|7H2H_=gu;9E~!!52_EQi0MN>_fV&Y5E_*44ZcV z`U3zTN{{HKW9w6)j*siny_4UBMy#6bG1n+foAF|Xoi@f9=8Gj;%045N{vwhzV7YnZ zcYN98@pe;F#F0?+gOh5IpZhj8E)Jx#Mmb41?_J7EMG1qf>~oe;f@{-|%RH8jjYzN2 zbkA(%?fty4?E<13B#)oMIW4f1lY2-RNx<=hRFzGri--~GJ8StG`nAO0aJ22s1kbeW zbA7-PQ=)YKMi{vRpWjF-3UxqFc4LrU%qypB!leeG$^Y*2B(5%7%iWvY5gsD;rdD>W zwU}5d`N3m(st>4_wW^}^AP0VvGGaf|BV=#Ec5b|Ii~z)}{20U=Hz<(`S*#yGFZB>( z&y*3scb33sgL*gZ@Ir&Ej*E|06g?>p@!`ei9rK3~2s=DW~Zs z7uv|1T}YMlg;Dw6MW}&ZBxSpNUAek!SRlUFSo8Y4@k?EN-)k7y#3~|R7te~SBwXgS z@lyy;p5PQx>`Z42=SFXsqA1kV)17NZZd=XJcXBH42`3BW;1+qP4cyE-Rc7iyn@J)a zc52T2m0{I*k+n}v&e^({ik0h6sfFL~Y@b>SB$pg4ww4>Zt?mLUj%FTwDYDqIC`SWv zxMK1S-0|ghnW0ZvHFWnX-P3x`NBfwBjPu5P*~0>AU_UXj%csg6zh+2uCo0|fd(Ht} zOghy3PBScf>S|_YX?ul}hwv5{%a~}Im<(ao>mLjkz5*&@4?f8*h9#F@5WMenDw!AG z7kCpUCGtTg=-krJ7|h$B!!d zdYPUUQFpFAAi65y^r<;DwnK--jrK_WchmR3kd>w2&ZUQbOv{lVC#~Yi=={*nqKU$^)(JY7fiB=GRLR2YExR=UIrwQiO~!N(74IpG<(bYpoYJcyOrX**b95 z`z}t2-S zOn!yteYOxJ9MdFz=1QP3<>S%AnjDlRrTVImsS>qK*;^i@ z$V`+`zk-=?`euO!Ej&n#Xwe6iRKX0qAL%n_DWxIsqzesuS_wG zQ5-(QujYZ3em3{9$BuEwIJM5(u|N;h;fCFwwF2zjljARKqMYTX*|J&+-sO&wtK7F;ad*sZ%G#`x|to-xD(SAxsGM2rL!=|;@i-H>V@mzP3 z%S}kf_P&l2UrY(xbEYYO34i@VrXsedHg@{MYDPG-^txyiyiL^iDPkUi5oec2%JpRA zN8HpV6}^{F8RUzp?sB3XBhjKy%9VA&^HgCod#-mnFMJW)>yfodW_Tnvp|v?qVo%t% z#g#e3$f~HHY;_E`rSP`YM!bih@>~QZL_>TUNLGVG4txi}&B`%nSPa?2zQB8+Cv95A znSWE%GmTPKBVyz66&>q8Qavw~r>(8bqEUVxo0<-+qgW_oBPKwnleOw?7M$E$k4%9j zkleQjgh1gU5PB;N9$8`H#3}{{SmPsQWATWengQS#TysU5u*Hh%qy50m0f%{tz#@?7 z7ch1LHJ&ua*I-E+bBIbiH6V~eR=|DZL*!`e2jM-~57Jc})5K~hu)UTF%uZf^z2UB> zKq$WTMDNFupte>1QNO$@I`>FGt59g|@6ZCEKdWsSKoMerm{}Jd#ZS4MAF^^Ne&Pvq zuWb->cPc&`XHU$dt{?0 zL>dJ|Lsbsbn>7yWLijhZZ`CKA6;`;|240i6NKTDXR)!z8_eY)Hj-;%QZL^;Z^!VF^|qI7Lz%aEW|$BFL3)2Hoz|vj#S%DtnGx zCYJp8RDy<}I@ckf+G5VYddeQeQD#>Xuno-q(!T6odSmur_;3(STRed|tDf79a54ih zqSIRbR1tH=s3>mb{8Tsxsv`HD}$<#fRU@F?-V z_Fnnaym2-{dFp*r82~B9@o@Kj<6_{Xh$+S}T>neJ&{194!nuTGf(TlV&8cv*+=S`w zxHqewr@ArEE^OwzqCnlRs-H2YifqAR#Qah9`uw^p+^p_TKiSa|Sk@!t3EO@ScF2|A z#u(1+gb7()a2ovv-ae?#1O72g>y2E2lP>dqjwzIkfPnvixM6akt_ABx` zV>IN9tjXO(!HBMdbb)`)w^~s2UQoQ_paecG$90}clL{e4hC0npAq&__me@hZzK$8si@L+MnMp@0QlfbmxB8dlw0!U2@XYKm9}|lEI*1gdc3VUhBRz+quu0TN?9qv z(x(w0eE=^ezK3F?Fyvw6qJiJ8(1?d7=E4x~>`K^?^EowPyCGs@Z^Hc|&r2eHTLKM# zc;uqz;*R6}n&VZhiNnE8%#a}8(yk~L(e9VNHf=sE$<}g8Q_r7!A!ZI90Ct3$Emr({ zyJWlgBd_hriR`1)VS4M}5F) zQk;0e-g@Du*lG#{p&{JXXk9ONn_)8jl6SzID`mp;g}MVH!rh1KbVm5H0FYQD1NHJv zwR$3paiIF$T<5~|^QH!3V?@)+SSkNwD#b*>g}X}P#VyZ^;#2kdcZ$@-G_K9*y*(Ae zt78UBnb-@9wBxOr=f=@n--4Cx0Q@|;&gP3R_wS*Aj%(Rhd!n zd|#Fsh-7QN{7r#1#N|EUeahV~e&HU_HpCM7Ek@SV zeA9bmY-a;daBE;gC@$c}gHjn-4hFsVvG2J9CpNW}*iaqQRqL}K1+#2cO6ly)Ze*z$ z;JM#*1D@JslcuV&^^&>~uPQjdb1nfjIPi?91Zp|@Sdni+>3($~XM z;A=XQVm_U1@X4vzJMkJs5SlcJ(v{3c$#qYCptJ7Hg=;X*NkCT3SRauHO^EQQJOn}= za0S279Y85N!iq#LE$@omt6bJjnw+MItyKamP<>-ra9$}m8kMY>I41%T?N^HRJz9B~ z#^gpsR#BQxA?qLvOW-*sc&CznV8Hv|Gh+)LBp*2`RGf@cHVMspa#xOV_#-cd9c!3rAMqq}1 zYLFH8^&M%E^#KefD~8K=pQkJt(nu?*t*XwoaMv9<;7%6*Agt`Gr4P34 zLbnL{Y*X$}FQ}>E8wRTFk(TF{mUnJb0?cQsKg1!u^c!!he0_2Od4`r9`uTZ2aCm;e z)mNUcf?qZtUfHoKa-1P)t4w=uvMKFw1*&fWktTa6+q{|?yET=r=L$A;m?KLvZ`LP5~b=_>pn*6TsH{%p+?vI&kH+iULY2 z4eFfm_f7>y-djbuB zD+UVDLKw3geEVXPro8-`*!u>92Dz?KWXWo%Aqz;54N`=*?T_k#9#(=EWtwvArgH={ zRW^i>ir4w!&M4W>N*_)#o1Y@?c*|HLGLE;%-jqaU7e_|b0+$C?faJDZ;-sa;tN&w} zZ9n9QP`GqOZ({>ga9iX->Rg+aY7uURg7p(aQ_*(lBMDXqOn4sPQZ|Bqpn7P^Q>4UG zcd01Y+x_99$d*&&u*7FiZs$8&;!VyiE-Ea)(T5BFTNa%oW^-_86T4QBzG+y-0%<%b zmoJDclb>})s0SRw*O45~yD@OTkEOqV8LNhS5*Sxs4Iy@jVjtF2TBRH@>0lI3dvq#( z5i^J^kw(ra$Mc>&@AJ~`ojBO^Q5l^+*=^LYKUzSwyA7%D`c!PvkH!`u|%Mg^Q*d%)ehnv7B|~^?xS^^grraoh(dkO&NdP{%SKBQ(v)Lee;?W zcn05U>sn}|Zc(gGQzg<;J*+=usk$iWS_EUWd~OGL+ImUnnvt67gKe{fu&{K-j+A!b zKq3c5yfwGB(APjg!P+q#-eP^cdH>=L>K~gDl(H9Dwuz2m>Zp)F6zZ>gWS2?t)4~bCewV`8iO7|kZMx5>Jnbt=NQ6LLVdaQy-VM45C}-eWzlmsUX)<03WLf8kxY;dTqy zAh2p`TGG<>Y3TC=Z#~jw^lAvW)oSHOJF+ofB52*%GVgISXKy(v3s}ewEIjt{tGC_Q zu(Ngo4|DvnYw8vu)Ia|DF*SWz9OwDtRU6p;R>{ebpBIfpAb3#A9(5r(uO2Fo9dd69 z!A)pQLfZ@tJrgtdoT2$zamNwq=e%eoDdfwr_%A}zXq>V=ZOn&ZE>`h6WL6rC3tfHq zM@(Ov9y5k_V!sN}8y{gBDJlS}bySvdG9jsDA(`mQtvb!V-X=<8Z zeUD|k_P~Ny_WL5K&)BU|M%zt*%H*m~*8_wv!#EY-)8}x0Y4T0K z!qnv>GaM@Gx~1{eAB|f{^8+LJhuEnisIWVM1sNswm_z#u>>oxXeN=LhCGTKP_v2nV zX%wGV8PEZRLTpP2%62xVY!PioC4Awej66?%IieqqqK)+SFm+<^IK}X_Ect2PCUv*C z`9@bfR4boNr(3+a8qz*~`qY{;OrX9Sv+g_+bmNum<|z!!r|&Ol+MX`-7{DQd=y$9M zYvuen=Vi>kf2L*9N_$gwE59;SeGmO}wlU4~H>7&)BQ8h!SF;VqA1B&-3qwa!6J;mI zpS{^Xd*gysR_vAqex7MPgI1RcL4Z#bCAiyEC@^s{N{Z9T#mEu7!L|!66|Tub9qq>H zLOXRIDSJ81?ace}bZnVHt?AyXSD)R;wGd`Us&?;IpYe_lWK0lR^q6!9#xLI17q^w3 z>t@%1!X-AJl}8SB+_gumr!aY!I41Pwmkg%lbo5Oxxck#h(TaS8eKBtE+%%7rMm91n zIPf&*rLiV|v31ZO&SGcvLSBBYfFmV7DgeMhKpr6f)yP8pkCEl%Ze#juNR6qF z*sTCj9$zQ#E>EG3PyPP+o%Gqc$=^}1_Y;$-B4CKXB#Ru|+oh+%UnvVioN6mT_LRXq zcm#7H+|HVM5}YN;8gu(%7lyQuIpcj@^u>g!heK4Hv&ycDMW0ctDb@xB5y?^ghqc5= zP6(Lc{zj*YX5wBm@{hB6hvC%c7|Rle23GUEYQa;hKSY&chb){#$#&tO1JgWs@|IAe zcT%U+uuqw1;`ejE;&6+7`pB8189^3f$h9M3b;>>m*{L>rfr-6pi>JyF23MXgnz~Mf zy?rnqEAkzcR{_)=S1?xs%~Z=GVAKP1`z--0k-eHqHA*8e9nRj$={Pc;*0wbcV)nd^ z1p01);Ld;3=FxB03xJXiI`gREw=Kl8yN8YxO!k3{%S;6hy%vwL0rMR-RXWH_ZyO5z zU9*!u;@Wq8qq?t63sujTJo;DegA0cV0r&>(sbg94l-fstg!1wr()xL(^Y!WL-t`a0 z0`;YRTORX$^{!6Uto#aDw6>0olgn@!U*|=Z1<_#DIA&K3nOw+M1u3&kw}d|q-c=tr z<%Y?U6qRPeznPYPFE^4&EA);925%YB-~k7HYdqkQCeslJRopBjmzf8=t z*pjn6s8WxvrE1jTN2w%qc>Tb=*Lg}=5N&Ec%uCv@{-#HnHNw`i zv048xp~VczcKl*g-F}^C8OU|7Ee2-=D{F=A1&-QMS9S`j2{!>;xLEAKU znXDR}qetHMi0e*;N^rt*t95d;3BL*e!!?Vi| za79I1i&E4sCQe#_jD@xq3?Abwpim_wGk;#RWHUr>F;Edm5@~o>J&V@i>0Z4*jfplX zK=Pn*z_o5s@7}3N9}v?nqkLd1PXazv6}Jdmz7C_w**_@6k$DJ9Y+b!*oi}6s&3Pcb z;TS__iiN^8%0*7_2;1D+j*uGU92TF`_|_mGT2FWM6sk`h4nO57Bvp)9DTqfO&k_2rxx`RRHGA_AMP|rL3dMwWlOo7W-}hDOX(G;kmqtjouuS=p|@vV z&r%nRUV>iGd!gs6I03emi3(H7+6pEu0kpnO1?)l_C;G46FOD5hdlH9A^u6qIjaVBUc=tcGOdC z)Zr>ym_h4_JRariKbI7Cnk(aT&pH`HQ2JtdXVGI|F;hs&KawJ5 z-}uog0qUM%;&!zIPr*B-YzoXP58mM9@SG^E*-pWY#5rrKp9oVwj7-`d2&X~uOvb++ z+E16$SsL%lqtW&&VCLKBys3VDBUJygkL~B0M_bt5&dJoyNngeNo#}hspR>qV(uP&f z8;t%egEN#?f+*XDM5a&QjeHpzx|*Rm<~~x)7P}pHG-OFGcS%9P=hLw(ULD+fhuv*D zwKJ;Jii$T)!Q%A^W949p?Rg|tPV|j@%@o}clvWTk?naWZ;cNNCfP_U27r{zSceT#< zPL8ApjEW7*SwU$V?%Z%+ZS{jj58S;QNBTR%TJaO;_OdRs8A=($Jg;kw4n}E8gfi78 z5F)P&g-Fqfhd1+VBTO$n&E-kvW>Dg!Rl=%cIdlz9-bgSsiGdC0<~j^jnBDo73<|ha z)ZOtZWx}xu#6N`kxVPt3JIvYxuD&P)=6Zd(YmjKQI^Eh%ey)jG$-D#H^bRyq^4WQ8 z89Q9Z|cI3SzTTl3nlaA2j~_r_3}tL9XfgNX}#lKd%;?jyKRANnBlSMI(h4= z^^P%xWd|894OykYN%H%qmY-~~RB;w|se7zW6n*E0J3(mX$}W%SqM|8!-e|GbLy>;L@+m0xWW|9O?9*EsbLD!)n{ z|MM!FugiviQ2AAs@Sj(qdp)uLLFL!x$A4bM^)+Gg4=TSBA%9o-y=V5XpHcT~LB^}f z-+M%VB^Caz@_X0m&uyGP54G1I|5ueirlvoygYj#U`n%xo-7|kROAF=if`7KI{#_;L zxAv32nzfDnca`7F`m^um?}ERV(Ein|{nv+oeV@O~`o~f9dkyMe&2oP8ca`7F`la=c z-uk_M=C5XX5dU59ms$TfEPpRR`KwvT)PGm`&8$BwZ~pGB-?#n#Y8D2~-vxh}^^fEH z_kFX!nso;JyUK57{kdWIce8%aDgM>0Yo@;o{xa(yPn_SAa(^|8n)B}}znS%CHuCRg z{hoaI8Q12Cl literal 0 HcmV?d00001 diff --git a/contracts/red-bank/tests/helpers.rs b/contracts/red-bank/tests/helpers.rs index 39bdc5bfe..8f999fbfd 100644 --- a/contracts/red-bank/tests/helpers.rs +++ b/contracts/red-bank/tests/helpers.rs @@ -1,14 +1,19 @@ #![allow(dead_code)] +use std::collections::HashMap; + +use anyhow::Result as AnyResult; use cosmwasm_schema::serde; use cosmwasm_std::{ from_binary, testing::{MockApi, MockStorage}, Addr, Coin, Decimal, Deps, DepsMut, Event, OwnedDeps, Uint128, }; -use mars_params::types::{AssetParams, HighLeverageStrategyParams, RedBankSettings, RoverSettings}; +use cw_multi_test::AppResponse; +use mars_params::types::asset::{AssetParams, CmSettings, LiquidationBonus, RedBankSettings}; use mars_red_bank::{ contract::{instantiate, query}, + error::ContractError, interest_rates::{ calculate_applied_linear_interest_rate, compute_scaled_amount, compute_underlying_amount, ScalingOperation, @@ -17,6 +22,7 @@ use mars_red_bank::{ }; use mars_red_bank_types::red_bank::{ Collateral, CreateOrUpdateConfig, Debt, InstantiateMsg, Market, QueryMsg, + UserCollateralResponse, UserDebtResponse, UserHealthStatus, UserPositionResponse, }; use mars_testing::{mock_dependencies, mock_env, mock_info, MarsMockQuerier, MockEnvParams}; @@ -86,7 +92,7 @@ pub fn th_setup(contract_balances: &[Coin]) -> OwnedDeps Market { pub fn th_default_asset_params() -> AssetParams { AssetParams { - rover: RoverSettings { + denom: "todo".to_string(), + credit_manager: CmSettings { whitelisted: false, - hls: HighLeverageStrategyParams { - max_loan_to_value: Decimal::percent(90), - liquidation_threshold: Decimal::one(), - }, + hls: None, }, red_bank: RedBankSettings { deposit_enabled: true, @@ -122,7 +126,13 @@ pub fn th_default_asset_params() -> AssetParams { }, max_loan_to_value: Decimal::zero(), liquidation_threshold: Decimal::one(), - liquidation_bonus: Decimal::zero(), + liquidation_bonus: LiquidationBonus { + starting_lb: Decimal::percent(0u64), + slope: Decimal::one(), + min_lb: Decimal::percent(0u64), + max_lb: Decimal::percent(5u64), + }, + protocol_liquidation_fee: Decimal::percent(2u64), } } @@ -176,31 +186,20 @@ pub fn th_get_expected_indices_and_rates( th_get_expected_protocol_rewards(market, &expected_indices); // When borrowing, new computed index is used for scaled amount - let more_debt_scaled = compute_scaled_amount( - delta_info.more_debt, - expected_indices.borrow, - ScalingOperation::Ceil, - ) - .unwrap(); + let more_debt_scaled = th_get_scaled_debt_amount(delta_info.more_debt, expected_indices.borrow); // When repaying, new computed index is used to get current debt and deduct amount let less_debt_scaled = if !delta_info.less_debt.is_zero() { - let user_current_debt = compute_underlying_amount( + let user_current_debt = th_get_underlying_debt_amount( delta_info.user_current_debt_scaled, expected_indices.borrow, - ScalingOperation::Ceil, - ) - .unwrap(); + ); - let user_new_debt = if delta_info.less_debt >= user_current_debt { - Uint128::zero() - } else { - user_current_debt - delta_info.less_debt - }; + let user_new_debt = + user_current_debt.checked_sub(delta_info.less_debt).unwrap_or(Uint128::zero()); let user_new_debt_scaled = - compute_scaled_amount(user_new_debt, expected_indices.borrow, ScalingOperation::Ceil) - .unwrap(); + th_get_scaled_debt_amount(user_new_debt, expected_indices.borrow); delta_info.user_current_debt_scaled - user_new_debt_scaled } else { @@ -209,25 +208,15 @@ pub fn th_get_expected_indices_and_rates( // NOTE: Don't panic here so that the total repay of debt can be simulated // when less debt is greater than outstanding debt - let new_debt_total_scaled = if (market.debt_total_scaled + more_debt_scaled) > less_debt_scaled - { - market.debt_total_scaled + more_debt_scaled - less_debt_scaled - } else { - Uint128::zero() - }; - let debt_total = compute_underlying_amount( - new_debt_total_scaled, - expected_indices.borrow, - ScalingOperation::Ceil, - ) - .unwrap(); + let new_debt_total_scaled = (market.debt_total_scaled + more_debt_scaled) + .checked_sub(less_debt_scaled) + .unwrap_or(Uint128::zero()); + let debt_total = th_get_underlying_debt_amount(new_debt_total_scaled, expected_indices.borrow); - let total_collateral = compute_underlying_amount( + let total_collateral = th_get_underlying_liquidity_amount( market.collateral_total_scaled, expected_indices.liquidity, - ScalingOperation::Truncate, - ) - .unwrap(); + ); // Total collateral increased by accured protocol rewards let total_collateral = total_collateral + expected_protocol_rewards_to_distribute; @@ -258,23 +247,12 @@ pub fn th_get_expected_protocol_rewards( expected_indices: &TestExpectedIndices, ) -> Uint128 { let previous_borrow_index = market.borrow_index; - let previous_debt_total = compute_underlying_amount( - market.debt_total_scaled, - previous_borrow_index, - ScalingOperation::Ceil, - ) - .unwrap(); - let current_debt_total = compute_underlying_amount( - market.debt_total_scaled, - expected_indices.borrow, - ScalingOperation::Ceil, - ) - .unwrap(); - let interest_accrued = if current_debt_total > previous_debt_total { - current_debt_total - previous_debt_total - } else { - Uint128::zero() - }; + let previous_debt_total = + th_get_underlying_debt_amount(market.debt_total_scaled, previous_borrow_index); + let current_debt_total = + th_get_underlying_debt_amount(market.debt_total_scaled, expected_indices.borrow); + let interest_accrued = + current_debt_total.checked_sub(previous_debt_total).unwrap_or(Uint128::zero()); interest_accrued * market.reserve_factor } @@ -306,3 +284,91 @@ pub fn th_get_expected_indices(market: &Market, block_time: u64) -> TestExpected borrow: expected_borrow_index, } } + +pub fn th_get_scaled_liquidity_amount(amount: Uint128, liquidity_index: Decimal) -> Uint128 { + compute_scaled_amount(amount, liquidity_index, ScalingOperation::Truncate).unwrap() +} + +pub fn th_get_scaled_debt_amount(amount: Uint128, borrow_index: Decimal) -> Uint128 { + compute_scaled_amount(amount, borrow_index, ScalingOperation::Ceil).unwrap() +} + +pub fn th_get_underlying_liquidity_amount( + amount_scaled: Uint128, + liquidity_index: Decimal, +) -> Uint128 { + compute_underlying_amount(amount_scaled, liquidity_index, ScalingOperation::Truncate).unwrap() +} + +pub fn th_get_underlying_debt_amount(amount_scaled: Uint128, borrow_index: Decimal) -> Uint128 { + compute_underlying_amount(amount_scaled, borrow_index, ScalingOperation::Ceil).unwrap() +} + +pub fn liq_threshold_hf(position: &UserPositionResponse) -> Decimal { + match position.health_status { + UserHealthStatus::Borrowing { + liq_threshold_hf, + .. + } => liq_threshold_hf, + _ => panic!("User is not borrowing"), + } +} + +// Merge collaterals and debts for users. +// Return total amount_scaled for collateral / debt and balance amounts for denoms. +pub fn merge_collaterals_and_debts( + users_collaterals: &[&HashMap], + users_debts: &[&HashMap], +) -> (HashMap, HashMap, HashMap) { + let mut balances: HashMap = HashMap::new(); + + let mut merged_collaterals: HashMap = HashMap::new(); + + for user_collaterals in users_collaterals { + for (denom, collateral) in user_collaterals.iter() { + merged_collaterals + .entry(denom.clone()) + .and_modify(|v| { + *v += collateral.amount_scaled; + }) + .or_insert(collateral.amount_scaled); + balances + .entry(denom.clone()) + .and_modify(|v| { + *v += collateral.amount; + }) + .or_insert(collateral.amount); + } + } + + let mut merged_debts: HashMap = HashMap::new(); + + for user_debts in users_debts { + for (denom, debt) in user_debts.iter() { + merged_debts + .entry(denom.clone()) + .and_modify(|v| { + *v += debt.amount_scaled; + }) + .or_insert(debt.amount_scaled); + balances + .entry(denom.clone()) + .and_modify(|v| { + *v -= debt.amount; + }) + .or_insert(Uint128::zero()); // balance can't be negative + } + } + + (merged_collaterals, merged_debts, balances) +} + +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); + } + } +} diff --git a/contracts/red-bank/tests/test_admin.rs b/contracts/red-bank/tests/test_admin.rs index 53428ba80..66c921311 100644 --- a/contracts/red-bank/tests/test_admin.rs +++ b/contracts/red-bank/tests/test_admin.rs @@ -318,7 +318,7 @@ fn update_asset() { let info = mock_info("owner", &[]); instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); - deps.querier.set_close_factor(Decimal::from_ratio(1u128, 2u128)); + deps.querier.set_target_health_factor(Decimal::from_ratio(1u128, 2u128)); let ir_model = InterestRateModel { optimal_utilization_rate: Decimal::one(), @@ -455,7 +455,7 @@ fn update_asset_with_new_interest_rate_model_params() { let env = mock_env(MockEnvParams::default()); instantiate(deps.as_mut(), env, info, msg).unwrap(); - deps.querier.set_close_factor(Decimal::from_ratio(1u128, 2u128)); + deps.querier.set_target_health_factor(Decimal::from_ratio(1u128, 2u128)); let ir_model = InterestRateModel { optimal_utilization_rate: Decimal::one(), diff --git a/contracts/red-bank/tests/test_borrow.rs b/contracts/red-bank/tests/test_borrow.rs index 71fc93ba4..20d71a1e2 100644 --- a/contracts/red-bank/tests/test_borrow.rs +++ b/contracts/red-bank/tests/test_borrow.rs @@ -6,7 +6,7 @@ use helpers::{ has_collateral_position, has_debt_position, set_collateral, th_build_interests_updated_event, th_get_expected_indices_and_rates, th_init_market, th_setup, TestUtilizationDeltaInfo, }; -use mars_params::types::{AssetParams, HighLeverageStrategyParams, RedBankSettings, RoverSettings}; +use mars_params::types::asset::{AssetParams, CmSettings, RedBankSettings}; use mars_red_bank::{ contract::execute, error::ContractError, @@ -1000,13 +1000,10 @@ fn cannot_borrow_if_market_not_enabled() { deps.querier.set_redbank_params( "somecoin", AssetParams { - rover: RoverSettings { + credit_manager: CmSettings { whitelisted: false, - hls: HighLeverageStrategyParams { - max_loan_to_value: Decimal::percent(90), - liquidation_threshold: Decimal::one(), - }, + hls: None, }, red_bank: RedBankSettings { deposit_enabled: false, diff --git a/contracts/red-bank/tests/test_deposit.rs b/contracts/red-bank/tests/test_deposit.rs index 05607b90b..7f77e71d0 100644 --- a/contracts/red-bank/tests/test_deposit.rs +++ b/contracts/red-bank/tests/test_deposit.rs @@ -9,7 +9,7 @@ use cw_utils::PaymentError; use helpers::{ set_collateral, th_build_interests_updated_event, th_get_expected_indices_and_rates, th_setup, }; -use mars_params::types::{AssetParams, HighLeverageStrategyParams, RedBankSettings, RoverSettings}; +use mars_params::types::asset::{AssetParams, CmSettings, LiquidationBonus, RedBankSettings}; use mars_red_bank::{ contract::execute, error::ContractError, @@ -58,21 +58,25 @@ fn setup_test() -> TestSuite { deps.querier.set_redbank_params( denom, AssetParams { + denom: denom.to_string(), max_loan_to_value: Decimal::one(), liquidation_threshold: Default::default(), - liquidation_bonus: Default::default(), - rover: RoverSettings { + liquidation_bonus: LiquidationBonus { + starting_lb: Decimal::percent(0u64), + slope: Decimal::one(), + min_lb: Decimal::percent(0u64), + max_lb: Decimal::percent(5u64), + }, + credit_manager: CmSettings { whitelisted: false, - hls: HighLeverageStrategyParams { - max_loan_to_value: Decimal::percent(90), - liquidation_threshold: Decimal::one(), - }, + hls: None, }, red_bank: RedBankSettings { deposit_enabled: true, borrow_enabled: true, deposit_cap: Uint128::new(12_000_000), }, + protocol_liquidation_fee: Decimal::percent(2u64), }, ); @@ -156,12 +160,9 @@ fn depositing_to_disabled_market() { deps.querier.set_redbank_params( denom, AssetParams { - rover: RoverSettings { + credit_manager: CmSettings { whitelisted: false, - hls: HighLeverageStrategyParams { - max_loan_to_value: Decimal::percent(90), - liquidation_threshold: Decimal::one(), - }, + hls: None, }, red_bank: RedBankSettings { deposit_enabled: false, @@ -207,12 +208,9 @@ fn depositing_above_cap() { deps.querier.set_redbank_params( denom, AssetParams { - rover: RoverSettings { + credit_manager: CmSettings { whitelisted: false, - hls: HighLeverageStrategyParams { - max_loan_to_value: Decimal::percent(90), - liquidation_threshold: Decimal::one(), - }, + hls: None, }, red_bank: RedBankSettings { deposit_enabled: true, diff --git a/contracts/red-bank/tests/test_liquidate.rs b/contracts/red-bank/tests/test_liquidate.rs index b763d3505..06e268562 100644 --- a/contracts/red-bank/tests/test_liquidate.rs +++ b/contracts/red-bank/tests/test_liquidate.rs @@ -1,340 +1,69 @@ -#![allow(dead_code)] - -use std::cmp::min; +use std::{collections::HashMap, str::FromStr}; use cosmwasm_std::{ - attr, coin, coins, - testing::{mock_info, MockApi, MockStorage}, - to_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal, Deps, OwnedDeps, StdError, SubMsg, Uint128, - WasmMsg, + attr, coin, + testing::{mock_dependencies, mock_env, mock_info}, + to_binary, Addr, Decimal, SubMsg, Uint128, WasmMsg, }; use cw_utils::PaymentError; -use helpers::{ - has_collateral_position, set_collateral, th_build_interests_updated_event, - th_get_expected_indices, th_get_expected_indices_and_rates, th_init_market, th_setup, - TestUtilizationDeltaInfo, -}; -use mars_params::types::AssetParams; -use mars_red_bank::{ - contract::execute, - error::ContractError, - execute::liquidation_compute_amounts, - interest_rates::{ - compute_scaled_amount, compute_underlying_amount, get_scaled_liquidity_amount, - ScalingOperation, SCALING_FACTOR, - }, - state::{COLLATERALS, DEBTS, MARKETS}, -}; +use helpers::{th_get_expected_indices_and_rates, th_query, th_setup, TestUtilizationDeltaInfo}; +use mars_params::types::asset::{AssetParams, CmSettings, LiquidationBonus, RedBankSettings}; +use mars_red_bank::{contract::execute, error::ContractError}; use mars_red_bank_types::{ address_provider::MarsAddressType, incentives, - red_bank::{Collateral, Debt, ExecuteMsg, InterestRateModel, Market}, + red_bank::{ + ExecuteMsg, InitOrUpdateAssetParams, InterestRateModel, Market, QueryMsg, + UserCollateralResponse, UserDebtResponse, + }, +}; +use mars_testing::{ + integration::mock_env::{MockEnv, MockEnvBuilder}, + mock_env_at_block_time, }; -use mars_testing::{mock_env, mock_env_at_block_time, MarsMockQuerier, MockEnvParams}; -use mars_utils::math; -use crate::helpers::{set_debt, th_default_asset_params, TestInterestResults}; +use crate::helpers::{ + assert_err, liq_threshold_hf, merge_collaterals_and_debts, th_build_interests_updated_event, + th_get_scaled_liquidity_amount, +}; mod helpers; -struct TestSuite { - deps: OwnedDeps, - collateral_coin: Coin, - debt_coin: Coin, - uncollateralized_denom: &'static str, - collateral_price: Decimal, - debt_price: Decimal, - close_factor: Decimal, - collateral_market: Market, - debt_market: Market, - collateral_asset_params: AssetParams, - debt_asset_params: AssetParams, -} - -fn setup_test() -> TestSuite { - let initial_collateral_coin = coin(1_000_000_000u128, "collateral"); - let initial_debt_coin = coin(2_000_000_000u128, "debt"); - let mut deps = th_setup(&[initial_collateral_coin.clone(), initial_debt_coin.clone()]); - - let close_factor = Decimal::from_ratio(1u128, 2u128); - deps.querier.set_close_factor(close_factor); - - let collateral_price = Decimal::from_ratio(2_u128, 1_u128); - let debt_price = Decimal::from_ratio(11_u128, 10_u128); - let uncollateralized_debt_price = Decimal::from_ratio(15_u128, 10_u128); - let uncollateralized_denom = "uncollateralized_debt"; - deps.querier.set_oracle_price(&initial_collateral_coin.denom, collateral_price); - deps.querier.set_oracle_price(&initial_debt_coin.denom, debt_price); - deps.querier.set_oracle_price(uncollateralized_denom, uncollateralized_debt_price); - - // for the test to pass, we need an interest rate model that gives non-zero rates - let mock_ir_model = InterestRateModel { - optimal_utilization_rate: Decimal::percent(80), - base: Decimal::percent(5), - slope_1: Decimal::zero(), - slope_2: Decimal::zero(), - }; - - let collateral_market = Market { - collateral_total_scaled: Uint128::new(1_500_000_000) * SCALING_FACTOR, - debt_total_scaled: Uint128::new(800_000_000) * SCALING_FACTOR, - liquidity_index: Decimal::one(), - borrow_index: Decimal::one(), - liquidity_rate: Decimal::from_ratio(2u128, 10u128), - borrow_rate: Decimal::from_ratio(2u128, 10u128), - interest_rate_model: mock_ir_model.clone(), - reserve_factor: Decimal::from_ratio(2u128, 100u128), - indexes_last_updated: 0, - ..Default::default() - }; - - let debt_market = Market { - collateral_total_scaled: Uint128::new(3_500_000_000) * SCALING_FACTOR, - debt_total_scaled: Uint128::new(1_800_000_000) * SCALING_FACTOR, - liquidity_index: Decimal::from_ratio(12u128, 10u128), - borrow_index: Decimal::from_ratio(14u128, 10u128), - liquidity_rate: Decimal::from_ratio(2u128, 10u128), - borrow_rate: Decimal::from_ratio(2u128, 10u128), - interest_rate_model: mock_ir_model, - reserve_factor: Decimal::from_ratio(3u128, 100u128), - indexes_last_updated: 0, - ..Default::default() - }; - - let uncollateralized_debt_market = Market { - denom: uncollateralized_denom.to_string(), - ..Default::default() - }; - - let collateral_market = - th_init_market(deps.as_mut(), &initial_collateral_coin.denom, &collateral_market); - let debt_market = th_init_market(deps.as_mut(), &initial_debt_coin.denom, &debt_market); - th_init_market(deps.as_mut(), uncollateralized_denom, &uncollateralized_debt_market); - - let asset_params_1_collateral = AssetParams { - max_loan_to_value: Decimal::from_ratio(5u128, 10u128), - liquidation_threshold: Decimal::from_ratio(6u128, 10u128), - liquidation_bonus: Decimal::from_ratio(1u128, 10u128), - ..th_default_asset_params() - }; - deps.querier - .set_redbank_params(&initial_collateral_coin.denom, asset_params_1_collateral.clone()); - let asset_params_2_debt = AssetParams { - max_loan_to_value: Decimal::from_ratio(6u128, 10u128), - ..th_default_asset_params() - }; - deps.querier.set_redbank_params(&initial_debt_coin.denom, asset_params_2_debt.clone()); - - deps.querier.set_redbank_params(uncollateralized_denom, th_default_asset_params()); - - TestSuite { - deps, - collateral_coin: initial_collateral_coin, - debt_coin: initial_debt_coin, - uncollateralized_denom, - collateral_price, - debt_price, - close_factor, - collateral_market, - debt_market, - collateral_asset_params: asset_params_1_collateral, - debt_asset_params: asset_params_2_debt, - } -} - -fn rewards_collector_collateral(deps: Deps, denom: &str) -> Collateral { - COLLATERALS - .load( - deps.storage, - (&Addr::unchecked(MarsAddressType::RewardsCollector.to_string()), denom), - ) - .unwrap() -} - -struct TestExpectedAmountResults { - user_debt_repayed: Uint128, - user_debt_repayed_scaled: Uint128, - expected_refund_amount: Uint128, - expected_liquidated_collateral_amount: Uint128, - expected_liquidated_collateral_amount_scaled: Uint128, - expected_reward_amount_scaled: Uint128, - expected_debt_rates: TestInterestResults, -} - -fn expected_amounts( - block_time: u64, - user_debt_scaled: Uint128, - repay_amount: Uint128, - test_suite: &TestSuite, -) -> TestExpectedAmountResults { - let expected_debt_indices = th_get_expected_indices(&test_suite.debt_market, block_time); - let user_debt = compute_underlying_amount( - user_debt_scaled, - expected_debt_indices.borrow, - ScalingOperation::Ceil, - ) - .unwrap(); - - let max_repayable_debt = user_debt * test_suite.close_factor; - let amount_to_repay = min(repay_amount, max_repayable_debt); - let expected_refund_amount = if amount_to_repay < repay_amount { - repay_amount - amount_to_repay - } else { - Uint128::zero() - }; - - let expected_debt_rates = th_get_expected_indices_and_rates( - &test_suite.debt_market, - block_time, - TestUtilizationDeltaInfo { - less_debt: amount_to_repay, - user_current_debt_scaled: user_debt_scaled, - less_liquidity: expected_refund_amount, - ..Default::default() - }, - ); - - let expected_liquidated_collateral_amount = math::divide_uint128_by_decimal( - amount_to_repay - * test_suite.debt_price - * (Decimal::one() + test_suite.collateral_asset_params.liquidation_bonus), - test_suite.collateral_price, - ) - .unwrap(); - - let expected_collateral_rates = th_get_expected_indices_and_rates( - &test_suite.collateral_market, - block_time, - TestUtilizationDeltaInfo { - less_liquidity: expected_liquidated_collateral_amount, - ..Default::default() - }, - ); - - let expected_liquidated_collateral_amount_scaled = compute_scaled_amount( - expected_liquidated_collateral_amount, - expected_collateral_rates.liquidity_index, - ScalingOperation::Truncate, - ) - .unwrap(); - - let expected_reward_amount_scaled = compute_scaled_amount( - expected_debt_rates.protocol_rewards_to_distribute, - expected_debt_rates.liquidity_index, - ScalingOperation::Truncate, - ) - .unwrap(); - - TestExpectedAmountResults { - user_debt_repayed: amount_to_repay, - user_debt_repayed_scaled: expected_debt_rates.less_debt_scaled, - expected_refund_amount, - expected_liquidated_collateral_amount, - expected_liquidated_collateral_amount_scaled, - expected_reward_amount_scaled, - expected_debt_rates, - } -} - -// recipient - can be liquidator or another address which can receive collateral -fn expected_messages( - user_addr: &Addr, - recipient_addr: &Addr, - user_collateral_scaled: Uint128, - recipient_collateral_scaled: Uint128, - collateral_market: &Market, - debt_market: &Market, -) -> Vec { - // there should be up to three messages updating indices at the incentives contract, in the - // order: - // - collateral denom, user - // - collateral denom, liquidator - // - debt denom, rewards collector (if rewards accrued > 0) - // - // NOTE that we don't expect a message to update rewards collector's index of the - // **collateral** asset, because the liquidation action does NOT change the collateral - // asset's utilization rate, it's interest rate does not need to be updated. - vec![ - SubMsg::new(WasmMsg::Execute { - contract_addr: MarsAddressType::Incentives.to_string(), - msg: to_binary(&incentives::ExecuteMsg::BalanceChange { - user_addr: user_addr.clone(), - denom: collateral_market.denom.clone(), - user_amount_scaled_before: user_collateral_scaled, - total_amount_scaled_before: collateral_market.collateral_total_scaled, - }) - .unwrap(), - funds: vec![], - }), - SubMsg::new(WasmMsg::Execute { - contract_addr: MarsAddressType::Incentives.to_string(), - msg: to_binary(&incentives::ExecuteMsg::BalanceChange { - user_addr: recipient_addr.clone(), - denom: collateral_market.denom.clone(), - user_amount_scaled_before: recipient_collateral_scaled, - total_amount_scaled_before: collateral_market.collateral_total_scaled, - }) - .unwrap(), - funds: vec![], - }), - SubMsg::new(WasmMsg::Execute { - contract_addr: MarsAddressType::Incentives.to_string(), - msg: to_binary(&incentives::ExecuteMsg::BalanceChange { - user_addr: Addr::unchecked(MarsAddressType::RewardsCollector.to_string()), - denom: debt_market.denom.clone(), - user_amount_scaled_before: Uint128::zero(), - total_amount_scaled_before: debt_market.collateral_total_scaled, - }) - .unwrap(), - funds: vec![], - }), - ] -} +// NOTE: See spreadsheet with liquidation numbers for reference: +// contracts/red-bank/tests/files/Red Bank - Dynamic LB & CF test cases v1.1.xlsx #[test] -fn liquidate_if_no_coins_sent() { - let TestSuite { - mut deps, - .. - } = setup_test(); - - let env = mock_env(MockEnvParams::default()); - let info = mock_info("liquidator", &[]); +fn cannot_self_liquidate() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("liquidator", &[coin(100, "somecoin")]); let msg = ExecuteMsg::Liquidate { - user: "user".to_string(), + user: "liquidator".to_string(), collateral_denom: "collateral".to_string(), recipient: None, }; let error_res = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(error_res, PaymentError::NoFunds {}.into()); + assert_eq!(error_res, ContractError::CannotLiquidateSelf {}); } #[test] -fn cannot_self_liquidate() { - let TestSuite { - mut deps, - .. - } = setup_test(); - - let env = mock_env(MockEnvParams::default()); - let info = mock_info("liquidator", &[coin(100, "somecoin1")]); +fn liquidate_if_no_coins_sent() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("liquidator", &[]); let msg = ExecuteMsg::Liquidate { - user: "liquidator".to_string(), + user: "user".to_string(), collateral_denom: "collateral".to_string(), recipient: None, }; let error_res = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(error_res, ContractError::CannotLiquidateSelf {}.into()); + assert_eq!(error_res, PaymentError::NoFunds {}.into()); } #[test] fn liquidate_if_many_coins_sent() { - let TestSuite { - mut deps, - .. - } = setup_test(); - - let env = mock_env(MockEnvParams::default()); + let mut deps = mock_dependencies(); + let env = mock_env(); let info = mock_info("liquidator", &[coin(100, "somecoin1"), coin(200, "somecoin2")]); let msg = ExecuteMsg::Liquidate { user: "user".to_string(), @@ -346,1088 +75,1094 @@ fn liquidate_if_many_coins_sent() { } #[test] -fn liquidate_if_no_collateral() { - let TestSuite { - mut deps, - collateral_coin, - debt_coin, - .. - } = setup_test(); - - let liquidate_msg = ExecuteMsg::Liquidate { - user: "user".to_string(), - collateral_denom: collateral_coin.denom, - recipient: None, - }; +fn liquidate_if_no_requested_collateral() { + let mut mock_env = MockEnvBuilder::new(None, Addr::unchecked("owner")).build(); - let env = mock_env(MockEnvParams::default()); - let info = mock_info("liquidator", &coins(400_000_u128, debt_coin.denom)); - let error_res = execute(deps.as_mut(), env, info, liquidate_msg).unwrap_err(); - assert_eq!(error_res, ContractError::CannotLiquidateWhenNoCollateralBalance {}); -} + let red_bank = mock_env.red_bank.clone(); + let oracle = mock_env.oracle.clone(); -#[test] -fn liquidate_if_only_uncollateralized_debt_exists() { - let TestSuite { - mut deps, - collateral_coin, - debt_coin, - uncollateralized_denom, - collateral_market, - .. - } = setup_test(); - - let user_addr = Addr::unchecked("user"); - - set_collateral( - deps.as_mut(), - &user_addr, - &collateral_market.denom, - Uint128::new(2_000_000), - true, - ); - set_debt(deps.as_mut(), &user_addr, uncollateralized_denom, Uint128::new(10_000), true); + let (_, _, liquidatee, liquidator) = setup_env(&mut mock_env); - let liquidate_msg = ExecuteMsg::Liquidate { - user: user_addr.to_string(), - collateral_denom: collateral_coin.denom, - recipient: None, - }; + // change price to be able to liquidate + oracle.set_price_source_fixed(&mut mock_env, "uosmo", Decimal::from_ratio(3u128, 1u128)); + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(85u128, 10u128)); - let env = mock_env(MockEnvParams::default()); - let info = mock_info("liquidator", &coins(400_000_u128, debt_coin.denom)); - // trying to liquidate user with zero outstanding debt should fail (uncollateralized has not impact) - let error_res = execute(deps.as_mut(), env, info, liquidate_msg).unwrap_err(); - assert_eq!(error_res, ContractError::CannotLiquidateWhenNoDebtBalance {}); + // liquidate user + let error_res = red_bank.liquidate( + &mut mock_env, + &liquidator, + &liquidatee, + "other", + &[coin(1000, "uusdc")], + ); + assert_err(error_res, ContractError::CannotLiquidateWhenNoCollateralBalance {}); } #[test] -fn liquidate_partially() { - let mut ts = setup_test(); +fn liquidate_if_no_requested_debt() { + let mut mock_env = MockEnvBuilder::new(None, Addr::unchecked("owner")).build(); - let user_addr = Addr::unchecked("user"); - let liquidator_addr = Addr::unchecked("liquidator"); + let red_bank = mock_env.red_bank.clone(); + let oracle = mock_env.oracle.clone(); - let user_collateral_scaled_before = Uint128::from(2_000_000u64) * SCALING_FACTOR; - let user_debt_scaled_before = compute_scaled_amount( - Uint128::from(3_000_000u64), - ts.debt_market.borrow_index, - ScalingOperation::Ceil, - ) - .unwrap(); + let (_, _, liquidatee, liquidator) = setup_env(&mut mock_env); - set_collateral( - ts.deps.as_mut(), - &user_addr, - &ts.collateral_market.denom, - user_collateral_scaled_before, - true, - ); - set_debt(ts.deps.as_mut(), &user_addr, &ts.debt_market.denom, user_debt_scaled_before, false); - set_debt( - ts.deps.as_mut(), - &user_addr, - ts.uncollateralized_denom, - Uint128::new(10_000) * SCALING_FACTOR, - true, + // change price to be able to liquidate + oracle.set_price_source_fixed(&mut mock_env, "uosmo", Decimal::from_ratio(3u128, 1u128)); + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(85u128, 10u128)); + + // liquidate user + let error_res = red_bank.liquidate( + &mut mock_env, + &liquidator, + &liquidatee, + "uosmo", + &[coin(1000, "other")], ); + assert_err(error_res, ContractError::CannotLiquidateWhenNoDebtBalance {}); +} - let liquidate_msg = ExecuteMsg::Liquidate { - user: user_addr.to_string(), - collateral_denom: ts.collateral_market.denom.clone(), - recipient: None, - }; +#[test] +fn liquidate_if_requested_collateral_disabled() { + let mut mock_env = MockEnvBuilder::new(None, Addr::unchecked("owner")).build(); - let debt_to_repay = Uint128::from(400_000_u64); - let block_time = 15_000_000; - let env = mock_env_at_block_time(block_time); - let info = mock_info( - liquidator_addr.as_str(), - &coins(debt_to_repay.u128(), ts.debt_market.denom.clone()), - ); - let res = execute(ts.deps.as_mut(), env, info, liquidate_msg).unwrap(); - - let TestExpectedAmountResults { - user_debt_repayed, - user_debt_repayed_scaled, - expected_liquidated_collateral_amount, - expected_liquidated_collateral_amount_scaled, - expected_reward_amount_scaled, - expected_debt_rates, - .. - } = expected_amounts(block_time, user_debt_scaled_before, debt_to_repay, &ts); + let red_bank = mock_env.red_bank.clone(); + let oracle = mock_env.oracle.clone(); - let expected_msgs = expected_messages( - &user_addr, - &liquidator_addr, - user_collateral_scaled_before, - Uint128::zero(), - &ts.collateral_market, - &ts.debt_market, - ); - assert_eq!(res.messages, expected_msgs); + let (_, _, liquidatee, liquidator) = setup_env(&mut mock_env); - mars_testing::assert_eq_vec( - res.attributes, - vec![ - attr("action", "liquidate"), - attr("user", user_addr.as_str()), - attr("liquidator", liquidator_addr.as_str()), - attr("recipient", liquidator_addr.as_str()), - attr("collateral_denom", ts.collateral_market.denom.as_str()), - attr("collateral_amount", expected_liquidated_collateral_amount), - attr("collateral_amount_scaled", expected_liquidated_collateral_amount_scaled), - attr("debt_denom", ts.debt_market.denom.as_str()), - attr("debt_amount", user_debt_repayed), - attr("debt_amount_scaled", user_debt_repayed_scaled), - ], + // disable osmo collateral for liquidatee + red_bank.update_user_collateral_status(&mut mock_env, &liquidatee, "ujake", false); + + // change price to be able to liquidate + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(85u128, 10u128)); + + // liquidate user + let error_res = red_bank.liquidate( + &mut mock_env, + &liquidator, + &liquidatee, + "ujake", + &[coin(1000, "uusdc")], ); - assert_eq!( - res.events, - vec![th_build_interests_updated_event(&ts.debt_market.denom, &expected_debt_rates)] + assert_err( + error_res, + ContractError::CannotLiquidateWhenCollateralUnset { + denom: "ujake".to_string(), + }, ); +} - let debt_market_after = MARKETS.load(&ts.deps.storage, &ts.debt_market.denom).unwrap(); - - // user's collateral scaled amount should have been correctly decreased - let collateral = COLLATERALS - .load(ts.deps.as_ref().storage, (&user_addr, &ts.collateral_market.denom)) - .unwrap(); - assert_eq!( - collateral.amount_scaled, - user_collateral_scaled_before - expected_liquidated_collateral_amount_scaled +#[test] +fn cannot_liquidate_healthy_position() { + let mut mock_env = MockEnvBuilder::new(None, Addr::unchecked("owner")) + .target_health_factor(Decimal::from_ratio(12u128, 10u128)) + .build(); + + let red_bank = mock_env.red_bank.clone(); + + let (_, _, liquidatee, liquidator) = setup_env(&mut mock_env); + + // liquidate user + let error_res = red_bank.liquidate( + &mut mock_env, + &liquidator, + &liquidatee, + "uosmo", + &[coin(1000, "uusdc")], ); + assert_err(error_res, ContractError::CannotLiquidateHealthyPosition {}); +} - // liquidator's collateral scaled amount should have been correctly increased - let collateral = COLLATERALS - .load(ts.deps.as_ref().storage, (&liquidator_addr, &ts.collateral_market.denom)) +#[test] +fn target_health_factor_reached_after_max_debt_repayed() { + let mut mock_env = MockEnvBuilder::new(None, Addr::unchecked("owner")) + .target_health_factor(Decimal::from_ratio(12u128, 10u128)) + .build(); + + let red_bank = mock_env.red_bank.clone(); + let oracle = mock_env.oracle.clone(); + let rewards_collector = mock_env.rewards_collector.clone(); + + let (funded_amt, provider, liquidatee, liquidator) = setup_env(&mut mock_env); + + // change price to be able to liquidate + oracle.set_price_source_fixed(&mut mock_env, "uosmo", Decimal::from_ratio(3u128, 1u128)); + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(85u128, 10u128)); + + // liquidatee should be liquidatable + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let prev_liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + + // liquidate user + let usdc_repay_amt = 2373; + red_bank + .liquidate( + &mut mock_env, + &liquidator, + &liquidatee, + "uosmo", + &[coin(usdc_repay_amt, "uusdc")], + ) .unwrap(); - assert_eq!(collateral.amount_scaled, expected_liquidated_collateral_amount_scaled); - // check user's debt decreased by the appropriate amount - let debt = DEBTS.load(&ts.deps.storage, (&user_addr, &ts.debt_market.denom)).unwrap(); - assert_eq!(debt.amount_scaled, user_debt_scaled_before - user_debt_repayed_scaled); - - // check global debt decreased by the appropriate amount - assert_eq!( - debt_market_after.debt_total_scaled, - ts.debt_market.debt_total_scaled - user_debt_repayed_scaled + // check provider positions + let provider_collaterals = red_bank.query_user_collaterals(&mut mock_env, &provider); + assert_eq!(provider_collaterals.len(), 2); + assert_eq!(provider_collaterals.get("uusdc").unwrap().amount.u128(), 1000000); + assert_eq!(provider_collaterals.get("untrn").unwrap().amount.u128(), 1000000); + let provider_debts = red_bank.query_user_debts(&mut mock_env, &provider); + assert_eq!(provider_debts.len(), 0); + + // check liquidatee positions + let liquidatee_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_collaterals.len(), 3); + assert_eq!(liquidatee_collaterals.get("uosmo").unwrap().amount.u128(), 2809); + assert_eq!(liquidatee_collaterals.get("ujake").unwrap().amount.u128(), 2000); + assert_eq!(liquidatee_collaterals.get("uatom").unwrap().amount.u128(), 900); + let liquidatee_debts = red_bank.query_user_debts(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_debts.len(), 2); + assert_eq!(liquidatee_debts.get("uusdc").unwrap().amount.u128(), 627); + assert_eq!(liquidatee_debts.get("untrn").unwrap().amount.u128(), 1200); + + // check liquidator positions + let liquidator_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidator); + assert_eq!(liquidator_collaterals.len(), 1); + assert_eq!(liquidator_collaterals.get("uosmo").unwrap().amount.u128(), 7180); + let liquidator_debts = red_bank.query_user_debts(&mut mock_env, &liquidator); + assert_eq!(liquidator_debts.len(), 0); + + // check rewards-collector positions (protocol fee) + let rc_collaterals = + red_bank.query_user_collaterals(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_collaterals.len(), 1); + assert_eq!(rc_collaterals.get("uosmo").unwrap().amount.u128(), 11); + let rc_debts = red_bank.query_user_debts(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_debts.len(), 0); + + let (merged_collaterals, merged_debts, merged_balances) = merge_collaterals_and_debts( + &[&provider_collaterals, &liquidatee_collaterals, &liquidator_collaterals, &rc_collaterals], + &[&provider_debts, &liquidatee_debts, &liquidator_debts, &rc_debts], ); - // rewards collector's collateral scaled amount **of the debt asset** should have been correctly increased - let collateral = rewards_collector_collateral(ts.deps.as_ref(), &ts.debt_market.denom); - assert_eq!(collateral.amount_scaled, expected_reward_amount_scaled); + // check if users collaterals and debts are equal to markets scaled amounts + assert_users_and_markets_scaled_amounts(&mut mock_env, merged_collaterals, merged_debts); - // global collateral scaled amount **of the debt asset** should have been correctly increased - assert_eq!( - debt_market_after.collateral_total_scaled, - ts.debt_market.collateral_total_scaled + expected_reward_amount_scaled - ); -} + // check red bank underlying balances + assert_underlying_balances(&mock_env, merged_balances); -#[test] -fn liquidate_up_to_close_factor_with_refund() { - let mut ts = setup_test(); + // check liquidator account balance + let omso_liquidator_balance = mock_env.query_balance(&liquidator, "uosmo").unwrap(); + assert_eq!(omso_liquidator_balance.amount.u128(), funded_amt); + let usdc_liquidator_balance = mock_env.query_balance(&liquidator, "uusdc").unwrap(); + assert_eq!(usdc_liquidator_balance.amount.u128(), funded_amt - usdc_repay_amt); - let user_addr = Addr::unchecked("user"); - let liquidator_addr = Addr::unchecked("liquidator"); + // liquidatee hf should improve + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + assert!(liq_threshold_hf > prev_liq_threshold_hf); + // it should be 1.2, but because of roundings it is hard to achieve an exact number + assert_eq!(liq_threshold_hf, Decimal::from_str("1.200016765864699471").unwrap()); +} - let user_collateral_scaled_before = Uint128::from(2_000_000u64) * SCALING_FACTOR; - let user_debt_scaled_before = compute_scaled_amount( - Uint128::from(3_000_000u64), - ts.debt_market.borrow_index, - ScalingOperation::Ceil, - ) - .unwrap(); +#[test] +fn debt_amt_adjusted_to_total_debt_then_refund() { + let mut mock_env = MockEnvBuilder::new(None, Addr::unchecked("owner")) + .target_health_factor(Decimal::from_ratio(12u128, 10u128)) + .build(); + + let red_bank = mock_env.red_bank.clone(); + let oracle = mock_env.oracle.clone(); + let rewards_collector = mock_env.rewards_collector.clone(); + + let (funded_amt, provider, liquidatee, liquidator) = setup_env(&mut mock_env); + + // change price to be able to liquidate + oracle.set_price_source_fixed(&mut mock_env, "uosmo", Decimal::from_ratio(25u128, 10u128)); + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(755u128, 100u128)); + + // liquidatee should be liquidatable + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let prev_liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + + // liquidate user + let usdc_repay_amt = 3250; + red_bank + .liquidate( + &mut mock_env, + &liquidator, + &liquidatee, + "uosmo", + &[coin(usdc_repay_amt, "uusdc")], + ) + .unwrap(); - set_collateral( - ts.deps.as_mut(), - &user_addr, - &ts.collateral_market.denom, - user_collateral_scaled_before, - true, + // check provider positions + let provider_collaterals = red_bank.query_user_collaterals(&mut mock_env, &provider); + assert_eq!(provider_collaterals.len(), 2); + assert_eq!(provider_collaterals.get("uusdc").unwrap().amount.u128(), 1000000); + assert_eq!(provider_collaterals.get("untrn").unwrap().amount.u128(), 1000000); + let provider_debts = red_bank.query_user_debts(&mut mock_env, &provider); + assert_eq!(provider_debts.len(), 0); + + // check liquidatee positions (no usdc debt, fully repayed) + let liquidatee_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_collaterals.len(), 3); + assert_eq!(liquidatee_collaterals.get("uosmo").unwrap().amount.u128(), 34); + assert_eq!(liquidatee_collaterals.get("ujake").unwrap().amount.u128(), 2000); + assert_eq!(liquidatee_collaterals.get("uatom").unwrap().amount.u128(), 900); + let liquidatee_debts = red_bank.query_user_debts(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_debts.len(), 1); + assert_eq!(liquidatee_debts.get("untrn").unwrap().amount.u128(), 1200); + + // check liquidator positions + let liquidator_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidator); + assert_eq!(liquidator_collaterals.len(), 1); + assert_eq!(liquidator_collaterals.get("uosmo").unwrap().amount.u128(), 9946); + let liquidator_debts = red_bank.query_user_debts(&mut mock_env, &liquidator); + assert_eq!(liquidator_debts.len(), 0); + + // check rewards-collector positions (protocol fee) + let rc_collaterals = + red_bank.query_user_collaterals(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_collaterals.len(), 1); + assert_eq!(rc_collaterals.get("uosmo").unwrap().amount.u128(), 20); + let rc_debts = red_bank.query_user_debts(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_debts.len(), 0); + + let (merged_collaterals, merged_debts, merged_balances) = merge_collaterals_and_debts( + &[&provider_collaterals, &liquidatee_collaterals, &liquidator_collaterals, &rc_collaterals], + &[&provider_debts, &liquidatee_debts, &liquidator_debts, &rc_debts], ); - set_debt(ts.deps.as_mut(), &user_addr, &ts.debt_market.denom, user_debt_scaled_before, false); - let liquidate_msg = ExecuteMsg::Liquidate { - user: user_addr.to_string(), - collateral_denom: ts.collateral_market.denom.clone(), - recipient: None, - }; + // check if users collaterals and debts are equal to markets scaled amounts + assert_users_and_markets_scaled_amounts(&mut mock_env, merged_collaterals, merged_debts); - let debt_to_repay = Uint128::from(10_000_000_u64); - let block_time = 16_000_000; - let env = mock_env_at_block_time(block_time); - let info = mock_info( - liquidator_addr.as_str(), - &coins(debt_to_repay.u128(), ts.debt_market.denom.clone()), - ); - let res = execute(ts.deps.as_mut(), env, info, liquidate_msg).unwrap(); - - let TestExpectedAmountResults { - user_debt_repayed, - user_debt_repayed_scaled, - expected_refund_amount, - expected_liquidated_collateral_amount, - expected_liquidated_collateral_amount_scaled, - expected_reward_amount_scaled, - expected_debt_rates, - .. - } = expected_amounts(block_time, user_debt_scaled_before, debt_to_repay, &ts); - - let mut expected_msgs = expected_messages( - &user_addr, - &liquidator_addr, - user_collateral_scaled_before, - Uint128::zero(), - &ts.collateral_market, - &ts.debt_market, - ); - expected_msgs.push(SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: liquidator_addr.to_string(), - amount: coins(expected_refund_amount.u128(), ts.debt_market.denom.clone()), - }))); - assert_eq!(res.messages, expected_msgs); + // check red bank underlying balances + assert_underlying_balances(&mock_env, merged_balances); - mars_testing::assert_eq_vec( - vec![ - attr("action", "liquidate"), - attr("user", user_addr.as_str()), - attr("liquidator", liquidator_addr.as_str()), - attr("recipient", liquidator_addr.as_str()), - attr("collateral_denom", ts.collateral_market.denom.as_str()), - attr("collateral_amount", expected_liquidated_collateral_amount), - attr("collateral_amount_scaled", expected_liquidated_collateral_amount_scaled), - attr("debt_denom", ts.debt_market.denom.as_str()), - attr("debt_amount", user_debt_repayed), - attr("debt_amount_scaled", user_debt_repayed_scaled), - ], - res.attributes, - ); - assert_eq!( - res.events, - vec![th_build_interests_updated_event(&ts.debt_market.denom, &expected_debt_rates)], - ); + // check liquidator account balance + let omso_liquidator_balance = mock_env.query_balance(&liquidator, "uosmo").unwrap(); + assert_eq!(omso_liquidator_balance.amount.u128(), funded_amt); + let usdc_liquidator_balance = mock_env.query_balance(&liquidator, "uusdc").unwrap(); + assert_eq!(usdc_liquidator_balance.amount.u128(), funded_amt - usdc_repay_amt + 250); // 250 refunded - let debt_market_after = MARKETS.load(&ts.deps.storage, &ts.debt_market.denom).unwrap(); + // liquidatee hf should improve + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + assert!(liq_threshold_hf > prev_liq_threshold_hf); +} - // user's collateral scaled amount should have been correctly decreased - let collateral = COLLATERALS - .load(ts.deps.as_ref().storage, (&user_addr, &ts.collateral_market.denom)) +#[test] +fn debt_amt_adjusted_to_max_allowed_by_requested_coin() { + let mut mock_env = MockEnvBuilder::new(None, Addr::unchecked("owner")) + .target_health_factor(Decimal::from_ratio(12u128, 10u128)) + .build(); + + let red_bank = mock_env.red_bank.clone(); + let oracle = mock_env.oracle.clone(); + let rewards_collector = mock_env.rewards_collector.clone(); + + let (funded_amt, provider, liquidatee, liquidator) = setup_env(&mut mock_env); + + // change price to be able to liquidate + oracle.set_price_source_fixed(&mut mock_env, "uosmo", Decimal::from_ratio(2u128, 1u128)); + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(64u128, 10u128)); + + // liquidatee should be liquidatable + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let prev_liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + + // liquidate user + let usdc_repay_amt = 2840; + red_bank + .liquidate( + &mut mock_env, + &liquidator, + &liquidatee, + "uosmo", + &[coin(usdc_repay_amt, "uusdc")], + ) .unwrap(); - assert_eq!( - collateral.amount_scaled, - user_collateral_scaled_before - expected_liquidated_collateral_amount_scaled - ); - // liquidator's collateral scaled amount should have been correctly increased - let collateral = COLLATERALS - .load(ts.deps.as_ref().storage, (&liquidator_addr, &ts.collateral_market.denom)) - .unwrap(); - assert_eq!(collateral.amount_scaled, expected_liquidated_collateral_amount_scaled); + // check provider positions + let provider_collaterals = red_bank.query_user_collaterals(&mut mock_env, &provider); + assert_eq!(provider_collaterals.len(), 2); + assert_eq!(provider_collaterals.get("uusdc").unwrap().amount.u128(), 1000000); + assert_eq!(provider_collaterals.get("untrn").unwrap().amount.u128(), 1000000); + let provider_debts = red_bank.query_user_debts(&mut mock_env, &provider); + assert_eq!(provider_debts.len(), 0); + + // check liquidatee positions + let liquidatee_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_collaterals.len(), 3); + assert_eq!(liquidatee_collaterals.get("uosmo").unwrap().amount.u128(), 4); + assert_eq!(liquidatee_collaterals.get("ujake").unwrap().amount.u128(), 2000); + assert_eq!(liquidatee_collaterals.get("uatom").unwrap().amount.u128(), 900); + let liquidatee_debts = red_bank.query_user_debts(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_debts.len(), 2); + assert_eq!(liquidatee_debts.get("uusdc").unwrap().amount.u128(), 160); + assert_eq!(liquidatee_debts.get("untrn").unwrap().amount.u128(), 1200); + + // check liquidator positions + let liquidator_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidator); + assert_eq!(liquidator_collaterals.len(), 1); + assert_eq!(liquidator_collaterals.get("uosmo").unwrap().amount.u128(), 9976); + let liquidator_debts = red_bank.query_user_debts(&mut mock_env, &liquidator); + assert_eq!(liquidator_debts.len(), 0); + + // check rewards-collector positions (protocol fee) + let rc_collaterals = + red_bank.query_user_collaterals(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_collaterals.len(), 1); + assert_eq!(rc_collaterals.get("uosmo").unwrap().amount.u128(), 20); + let rc_debts = red_bank.query_user_debts(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_debts.len(), 0); + + let (merged_collaterals, merged_debts, merged_balances) = merge_collaterals_and_debts( + &[&provider_collaterals, &liquidatee_collaterals, &liquidator_collaterals, &rc_collaterals], + &[&provider_debts, &liquidatee_debts, &liquidator_debts, &rc_debts], + ); - // check user's debt decreased by the appropriate amount - let debt = DEBTS.load(&ts.deps.storage, (&user_addr, &ts.debt_market.denom)).unwrap(); - assert_eq!(debt.amount_scaled, user_debt_scaled_before - expected_debt_rates.less_debt_scaled); + // check if users collaterals and debts are equal to markets scaled amounts + assert_users_and_markets_scaled_amounts(&mut mock_env, merged_collaterals, merged_debts); - // check global debt decreased by the appropriate amount - assert_eq!( - debt_market_after.debt_total_scaled, - ts.debt_market.debt_total_scaled - expected_debt_rates.less_debt_scaled - ); + // check red bank underlying balances + assert_underlying_balances(&mock_env, merged_balances); - // rewards collector's collateral scaled amount **of the debt asset** should have been correctly increased - let collateral = rewards_collector_collateral(ts.deps.as_ref(), &ts.debt_market.denom); - assert_eq!(collateral.amount_scaled, expected_reward_amount_scaled); + // check liquidator account balance + let omso_liquidator_balance = mock_env.query_balance(&liquidator, "uosmo").unwrap(); + assert_eq!(omso_liquidator_balance.amount.u128(), funded_amt); + let usdc_liquidator_balance = mock_env.query_balance(&liquidator, "uusdc").unwrap(); + assert_eq!(usdc_liquidator_balance.amount.u128(), funded_amt - usdc_repay_amt); - // global collateral scaled amount **of the debt asset** should have been correctly increased - assert_eq!( - debt_market_after.collateral_total_scaled, - ts.debt_market.collateral_total_scaled + expected_reward_amount_scaled - ); + // liquidatee hf should improve + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + assert!(liq_threshold_hf > prev_liq_threshold_hf); } #[test] -fn liquidate_fully() { - let TestSuite { - mut deps, - collateral_price, - debt_price, - collateral_market, - debt_market, - collateral_asset_params, - .. - } = setup_test(); - - let user_addr = Addr::unchecked("user"); - let liquidator_addr = Addr::unchecked("liquidator"); - - let user_collateral_scaled_before = Uint128::new(100) * SCALING_FACTOR; - let user_debt_scaled_before = Uint128::new(400) * SCALING_FACTOR; - - set_collateral( - deps.as_mut(), - &user_addr, - &collateral_market.denom, - user_collateral_scaled_before, - true, - ); - set_debt(deps.as_mut(), &user_addr, &debt_market.denom, user_debt_scaled_before, false); - - let liquidate_msg = ExecuteMsg::Liquidate { - user: user_addr.to_string(), - collateral_denom: collateral_market.denom.clone(), - recipient: None, - }; +fn debt_amt_no_adjustment_with_different_recipient() { + let mut mock_env = MockEnvBuilder::new(None, Addr::unchecked("owner")) + .target_health_factor(Decimal::from_ratio(12u128, 10u128)) + .build(); + + let red_bank = mock_env.red_bank.clone(); + let oracle = mock_env.oracle.clone(); + let rewards_collector = mock_env.rewards_collector.clone(); + + let (funded_amt, provider, liquidatee, liquidator) = setup_env(&mut mock_env); + + // change price to be able to liquidate + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(68u128, 10u128)); + + // liquidatee should be liquidatable + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let prev_liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + + // liquidate user + let usdc_repay_amt = 120; + let recipient = Addr::unchecked("recipient"); + red_bank + .liquidate_with_different_recipient( + &mut mock_env, + &liquidator, + &liquidatee, + "uosmo", + &[coin(usdc_repay_amt, "uusdc")], + Some(recipient.to_string()), + ) + .unwrap(); - let debt_to_repay = Uint128::from(300u128); - let block_time = 16_000_000; - let env = mock_env_at_block_time(block_time); - let info = mock_info( - liquidator_addr.as_str(), - &coins(debt_to_repay.u128(), debt_market.denom.clone()), + // check provider positions + let provider_collaterals = red_bank.query_user_collaterals(&mut mock_env, &provider); + assert_eq!(provider_collaterals.len(), 2); + assert_eq!(provider_collaterals.get("uusdc").unwrap().amount.u128(), 1000000); + assert_eq!(provider_collaterals.get("untrn").unwrap().amount.u128(), 1000000); + let provider_debts = red_bank.query_user_debts(&mut mock_env, &provider); + assert_eq!(provider_debts.len(), 0); + + // check liquidatee positions + let liquidatee_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_collaterals.len(), 3); + assert_eq!(liquidatee_collaterals.get("uosmo").unwrap().amount.u128(), 9593); + assert_eq!(liquidatee_collaterals.get("ujake").unwrap().amount.u128(), 2000); + assert_eq!(liquidatee_collaterals.get("uatom").unwrap().amount.u128(), 900); + let liquidatee_debts = red_bank.query_user_debts(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_debts.len(), 2); + assert_eq!(liquidatee_debts.get("uusdc").unwrap().amount.u128(), 2880); + assert_eq!(liquidatee_debts.get("untrn").unwrap().amount.u128(), 1200); + + // check liquidator positions + let liquidator_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidator); + assert_eq!(liquidator_collaterals.len(), 0); + let liquidator_debts = red_bank.query_user_debts(&mut mock_env, &liquidator); + assert_eq!(liquidator_debts.len(), 0); + + // check recipient positions + let recipient_collaterals = red_bank.query_user_collaterals(&mut mock_env, &recipient); + assert_eq!(recipient_collaterals.len(), 1); + assert_eq!(recipient_collaterals.get("uosmo").unwrap().amount.u128(), 406); + let recipient_debts = red_bank.query_user_debts(&mut mock_env, &recipient); + assert_eq!(recipient_debts.len(), 0); + + // check rewards-collector positions (protocol fee) + let rc_collaterals = + red_bank.query_user_collaterals(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_collaterals.len(), 1); + assert_eq!(rc_collaterals.get("uosmo").unwrap().amount.u128(), 1); + let rc_debts = red_bank.query_user_debts(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_debts.len(), 0); + + let (merged_collaterals, merged_debts, merged_balances) = merge_collaterals_and_debts( + &[ + &provider_collaterals, + &liquidatee_collaterals, + &liquidator_collaterals, + &recipient_collaterals, + &rc_collaterals, + ], + &[&provider_debts, &liquidatee_debts, &liquidator_debts, &recipient_debts, &rc_debts], ); - let res = execute(deps.as_mut(), env, info, liquidate_msg).unwrap(); - - // get expected indices and rates for debt and collateral markets - let expected_collateral_indices = th_get_expected_indices(&collateral_market, block_time); - let user_collateral_balance = compute_underlying_amount( - user_collateral_scaled_before, - expected_collateral_indices.liquidity, - ScalingOperation::Truncate, - ) - .unwrap(); - - // Since debt is being over_repayed, we expect to liquidate total collateral - let expected_less_debt = math::divide_uint128_by_decimal( - math::divide_uint128_by_decimal(collateral_price * user_collateral_balance, debt_price) - .unwrap(), - Decimal::one() + collateral_asset_params.liquidation_bonus, - ) - .unwrap(); - let expected_refund_amount = debt_to_repay - expected_less_debt; + // check if users collaterals and debts are equal to markets scaled amounts + assert_users_and_markets_scaled_amounts(&mut mock_env, merged_collaterals, merged_debts); - let expected_debt_rates = th_get_expected_indices_and_rates( - &debt_market, - block_time, - TestUtilizationDeltaInfo { - less_debt: expected_less_debt, - user_current_debt_scaled: user_debt_scaled_before, - less_liquidity: expected_refund_amount, - ..Default::default() - }, - ); + // check red bank underlying balances + assert_underlying_balances(&mock_env, merged_balances); - let debt_market_after = MARKETS.load(&deps.storage, &debt_market.denom).unwrap(); + // check liquidator account balance + let omso_liquidator_balance = mock_env.query_balance(&liquidator, "uosmo").unwrap(); + assert_eq!(omso_liquidator_balance.amount.u128(), funded_amt); + let usdc_liquidator_balance = mock_env.query_balance(&liquidator, "uusdc").unwrap(); + assert_eq!(usdc_liquidator_balance.amount.u128(), funded_amt - usdc_repay_amt); - // since this is a full liquidation, the full amount of user's collateral shares should have - // been transferred to the liquidator - let expected_liquidated_collateral_amount_scaled = user_collateral_scaled_before; + // liquidatee hf should improve + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + assert!(liq_threshold_hf > prev_liq_threshold_hf); +} - let mut expected_msgs = expected_messages( - &user_addr, - &liquidator_addr, - user_collateral_scaled_before, - Uint128::zero(), - &collateral_market, - &debt_market, - ); - expected_msgs.push(SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: liquidator_addr.to_string(), - amount: coins(expected_refund_amount.u128(), debt_market.denom.clone()), - }))); - assert_eq!(res.messages, expected_msgs); +#[test] +fn same_asset_for_debt_and_collateral_with_refund() { + let mut mock_env = MockEnvBuilder::new(None, Addr::unchecked("owner")) + .target_health_factor(Decimal::from_ratio(12u128, 10u128)) + .build(); + + let red_bank = mock_env.red_bank.clone(); + let params = mock_env.params.clone(); + let oracle = mock_env.oracle.clone(); + let rewards_collector = mock_env.rewards_collector.clone(); + + let funded_amt = 1_000_000_000_000u128; + let provider = Addr::unchecked("provider"); // provides collateral to be borrowed by others + let liquidatee = Addr::unchecked("liquidatee"); + let liquidator = Addr::unchecked("liquidator"); + + // setup red-bank + let (market_params, asset_params) = + default_asset_params_with("uosmo", Decimal::percent(70), Decimal::percent(78)); + red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = + default_asset_params_with("uatom", Decimal::percent(82), Decimal::percent(90)); + red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params); + params.init_params(&mut mock_env, asset_params); + + // setup oracle + oracle.set_price_source_fixed(&mut mock_env, "uosmo", Decimal::from_ratio(15u128, 10u128)); + oracle.set_price_source_fixed(&mut mock_env, "uatom", Decimal::from_ratio(10u128, 1u128)); + + // fund accounts + mock_env.fund_accounts(&[&provider, &liquidatee, &liquidator], funded_amt, &["uosmo", "uatom"]); + + // provider deposits collaterals + red_bank.deposit(&mut mock_env, &provider, coin(1000000, "uosmo")).unwrap(); + + // liquidatee deposits and borrows + red_bank.deposit(&mut mock_env, &liquidatee, coin(1000, "uosmo")).unwrap(); + red_bank.deposit(&mut mock_env, &liquidatee, coin(1000, "uatom")).unwrap(); + red_bank.borrow(&mut mock_env, &liquidatee, "uosmo", 3000).unwrap(); + + // change price to be able to liquidate + oracle.set_price_source_fixed(&mut mock_env, "uatom", Decimal::from_ratio(2u128, 1u128)); + + // liquidatee should be liquidatable + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let prev_liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + + // liquidate user + let osmo_repay_amt = 1000; + red_bank + .liquidate( + &mut mock_env, + &liquidator, + &liquidatee, + "uosmo", + &[coin(osmo_repay_amt, "uosmo")], + ) + .unwrap(); - mars_testing::assert_eq_vec( - vec![ - attr("action", "liquidate"), - attr("user", user_addr.as_str()), - attr("liquidator", liquidator_addr.as_str()), - attr("recipient", liquidator_addr.as_str()), - attr("collateral_denom", collateral_market.denom.as_str()), - attr("collateral_amount", user_collateral_balance), - attr("collateral_amount_scaled", expected_liquidated_collateral_amount_scaled), - attr("debt_denom", debt_market.denom.as_str()), - attr("debt_amount", expected_less_debt), - attr("debt_amount_scaled", expected_debt_rates.less_debt_scaled), - ], - res.attributes, - ); - assert_eq!( - res.events, - vec![th_build_interests_updated_event(&debt_market.denom, &expected_debt_rates)], + // check provider positions + let provider_collaterals = red_bank.query_user_collaterals(&mut mock_env, &provider); + assert_eq!(provider_collaterals.len(), 1); + assert_eq!(provider_collaterals.get("uosmo").unwrap().amount.u128(), 1000000); + let provider_debts = red_bank.query_user_debts(&mut mock_env, &provider); + assert_eq!(provider_debts.len(), 0); + + // check liquidatee positions + let liquidatee_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_collaterals.len(), 2); + assert_eq!(liquidatee_collaterals.get("uosmo").unwrap().amount.u128(), 1); + assert_eq!(liquidatee_collaterals.get("uatom").unwrap().amount.u128(), 1000); + let liquidatee_debts = red_bank.query_user_debts(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_debts.len(), 1); + assert_eq!(liquidatee_debts.get("uosmo").unwrap().amount.u128(), 2020); + + // check liquidator positions + let liquidator_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidator); + assert_eq!(liquidator_collaterals.len(), 1); + assert_eq!(liquidator_collaterals.get("uosmo").unwrap().amount.u128(), 998); + let liquidator_debts = red_bank.query_user_debts(&mut mock_env, &liquidator); + assert_eq!(liquidator_debts.len(), 0); + + // check rewards-collector positions (protocol fee) + let rc_collaterals = + red_bank.query_user_collaterals(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_collaterals.len(), 1); + assert_eq!(rc_collaterals.get("uosmo").unwrap().amount.u128(), 1); + let rc_debts = red_bank.query_user_debts(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_debts.len(), 0); + + let (merged_collaterals, merged_debts, merged_balances) = merge_collaterals_and_debts( + &[&provider_collaterals, &liquidatee_collaterals, &liquidator_collaterals, &rc_collaterals], + &[&provider_debts, &liquidatee_debts, &liquidator_debts, &rc_debts], ); - // since this is a full liquidation, the user's collateral position should have been deleted - assert!(!has_collateral_position(deps.as_ref(), &user_addr, &collateral_market.denom)); + // check if users collaterals and debts are equal to markets scaled amounts + let markets = red_bank.query_markets(&mut mock_env); + assert_eq!(markets.len(), 2); + let osmo_market = markets.get("uosmo").unwrap(); + let atom_market = markets.get("uatom").unwrap(); + assert_eq!(merged_collaterals.get_or_default("uosmo"), osmo_market.collateral_total_scaled); + assert_eq!(merged_debts.get_or_default("uosmo"), osmo_market.debt_total_scaled); + assert_eq!(merged_collaterals.get_or_default("uatom"), atom_market.collateral_total_scaled); + assert_eq!(merged_debts.get_or_default("uatom"), atom_market.debt_total_scaled); + + // check red bank underlying balances + let balances = mock_env.query_all_balances(&red_bank.contract_addr); + assert_eq!(merged_balances.get("uosmo"), balances.get("uosmo")); + assert_eq!(merged_balances.get("uatom"), balances.get("uatom")); + + // check liquidator account balance + let omso_liquidator_balance = mock_env.query_balance(&liquidator, "uosmo").unwrap(); + assert_eq!(omso_liquidator_balance.amount.u128(), funded_amt - osmo_repay_amt + 20); // 20 refunded + + // liquidatee hf should improve + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + assert!(liq_threshold_hf < prev_liq_threshold_hf); // FIXME: is it ok? we should block liquidation? +} - // liquidator's collateral scaled amount should have been correctly increased - let collateral = COLLATERALS - .load(deps.as_ref().storage, (&liquidator_addr, &collateral_market.denom)) +#[test] +fn liquidate_uncollateralized_loan() { + let owner = Addr::unchecked("owner"); + let mut mock_env = MockEnvBuilder::new(None, owner.clone()).build(); + + // setup oracle and red-bank + let oracle = mock_env.oracle.clone(); + oracle.set_price_source_fixed(&mut mock_env, "uatom", Decimal::from_ratio(14u128, 1u128)); + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::one()); + let red_bank = mock_env.red_bank.clone(); + let params = mock_env.params.clone(); + let (market_params, asset_params) = + default_asset_params_with("uusdc", Decimal::percent(70), Decimal::percent(78)); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = + default_asset_params_with("uatom", Decimal::percent(70), Decimal::percent(78)); + red_bank.init_asset(&mut mock_env, "uatom", market_params); + params.init_params(&mut mock_env, asset_params); + + // fund provider account with usdc + let provider = Addr::unchecked("provider"); + let funded_usdc = 1_000_000_000_000u128; + mock_env.fund_account(&provider, &[coin(1_000_000_000_000u128, "uusdc")]); + + // fund provider account with usdc + let liquidator = Addr::unchecked("liquidator"); + mock_env.fund_account(&liquidator, &[coin(1_000_000_000_000u128, "uusdc")]); + + // deposits usdc to redbank + red_bank.deposit(&mut mock_env, &provider, coin(funded_usdc, "uusdc")).unwrap(); + + let borrower = Addr::unchecked("borrower"); + + // set uncollateralized loan limit for borrower + red_bank + .update_uncollateralized_loan_limit( + &mut mock_env, + &owner, + &borrower, + "uusdc", + Uint128::from(10_000_000_000u128), + ) .unwrap(); - assert_eq!(collateral.amount_scaled, expected_liquidated_collateral_amount_scaled); - - // check user's debt decreased by the appropriate amount - let debt = DEBTS.load(&deps.storage, (&user_addr, &debt_market.denom)).unwrap(); - assert_eq!(debt.amount_scaled, user_debt_scaled_before - expected_debt_rates.less_debt_scaled); - // check global debt decreased by the appropriate amount - assert_eq!( - debt_market_after.debt_total_scaled, - debt_market.debt_total_scaled - expected_debt_rates.less_debt_scaled + // borrower borrows usdc + let borrow_amount = 98_000_000u128; + red_bank.borrow(&mut mock_env, &borrower, "uusdc", borrow_amount).unwrap(); + let balance = mock_env.query_balance(&borrower, "uusdc").unwrap(); + assert_eq!(balance.amount.u128(), borrow_amount); + + // try to liquidate, should fail because there are no collateralized loans + let error_res = red_bank.liquidate( + &mut mock_env, + &liquidator, + &borrower, + "uatom", + &[coin(borrow_amount, "uusdc")], ); + assert_err(error_res, ContractError::CannotLiquidateWhenPositiveUncollateralizedLoanLimit {}); } -/// FIXME: new clippy version warns to remove clone() from "collateral_market.clone()" but then it breaks compilation -#[allow(clippy::redundant_clone)] #[test] -fn liquidate_partially_if_same_asset_for_debt_and_collateral() { - let TestSuite { - mut deps, - collateral_price, - collateral_market, - collateral_asset_params, - .. - } = setup_test(); - let debt_price = collateral_price; - - let user_addr = Addr::unchecked("user"); - let liquidator_addr = Addr::unchecked("liquidator"); - - let user_collateral_scaled_before = Uint128::from(2_000_000u64) * SCALING_FACTOR; - let user_debt_scaled_before = compute_scaled_amount( - Uint128::from(3_000_000u64), - collateral_market.borrow_index, - ScalingOperation::Ceil, - ) - .unwrap(); +fn response_verification() { + let provider = Addr::unchecked("provider"); // provides collateral to be borrowed by others + let liquidatee = Addr::unchecked("liquidatee"); + let liquidator = Addr::unchecked("liquidator"); - set_collateral( - deps.as_mut(), - &user_addr, - &collateral_market.denom, - user_collateral_scaled_before, - true, - ); - set_debt(deps.as_mut(), &user_addr, &collateral_market.denom, user_debt_scaled_before, false); + let mut deps = th_setup(&[]); - let liquidate_msg = ExecuteMsg::Liquidate { - user: user_addr.to_string(), - collateral_denom: collateral_market.denom.clone(), - recipient: None, - }; + let env = mock_env_at_block_time(100_000); + let info = mock_info("owner", &[]); - let debt_to_repay = Uint128::from(400_000_u64); - let block_time = 15_000_000; - let env = mock_env_at_block_time(block_time); - let info = mock_info( - liquidator_addr.as_str(), - &coins(debt_to_repay.u128(), collateral_market.denom.clone()), - ); - let res = execute(deps.as_mut(), env.clone(), info, liquidate_msg).unwrap(); - - // get expected indices and rates for debt market - let expected_debt_rates = th_get_expected_indices_and_rates( - &collateral_market, - block_time, - TestUtilizationDeltaInfo { - less_debt: debt_to_repay, - user_current_debt_scaled: user_debt_scaled_before, - ..Default::default() + let (market_params, asset_params) = + default_asset_params_with("uosmo", Decimal::percent(70), Decimal::percent(78)); + execute( + deps.as_mut(), + env.clone(), + info.clone(), + ExecuteMsg::InitAsset { + denom: asset_params.denom.clone(), + params: market_params, }, - ); + ) + .unwrap(); + deps.querier.set_redbank_params(&asset_params.denom.clone(), asset_params); + let (market_params, asset_params) = + default_asset_params_with("uatom", Decimal::percent(82), Decimal::percent(90)); + execute( + deps.as_mut(), + env.clone(), + info.clone(), + ExecuteMsg::InitAsset { + denom: asset_params.denom.clone(), + params: market_params, + }, + ) + .unwrap(); + deps.querier.set_redbank_params(&asset_params.denom.clone(), asset_params); + let (market_params, asset_params) = + default_asset_params_with("uusdc", Decimal::percent(90), Decimal::percent(95)); + execute( + deps.as_mut(), + env.clone(), + info.clone(), + ExecuteMsg::InitAsset { + denom: asset_params.denom.clone(), + params: market_params, + }, + ) + .unwrap(); + deps.querier.set_redbank_params(&asset_params.denom.clone(), asset_params); + let (market_params, asset_params) = + default_asset_params_with("untrn", Decimal::percent(90), Decimal::percent(96)); + execute( + deps.as_mut(), + env.clone(), + info, + ExecuteMsg::InitAsset { + denom: asset_params.denom.clone(), + params: market_params, + }, + ) + .unwrap(); + deps.querier.set_redbank_params(&asset_params.denom.clone(), asset_params); - let collateral_market_after = MARKETS.load(&deps.storage, &collateral_market.denom).unwrap(); - let debt_market_after = MARKETS.load(&deps.storage, &collateral_market.denom).unwrap(); + deps.querier.set_oracle_price("uosmo", Decimal::from_ratio(4u128, 1u128)); + deps.querier.set_oracle_price("uatom", Decimal::from_ratio(82u128, 10u128)); + deps.querier.set_oracle_price("uusdc", Decimal::from_ratio(68u128, 10u128)); + deps.querier.set_oracle_price("untrn", Decimal::from_ratio(55u128, 10u128)); - let expected_liquidated_collateral_amount = math::divide_uint128_by_decimal( - debt_to_repay * debt_price * (Decimal::one() + collateral_asset_params.liquidation_bonus), - collateral_price, + // provider deposits collaterals + execute( + deps.as_mut(), + env.clone(), + mock_info(provider.as_str(), &[coin(1000000, "uusdc")]), + ExecuteMsg::Deposit {}, ) .unwrap(); - - let expected_liquidated_collateral_amount_scaled = get_scaled_liquidity_amount( - expected_liquidated_collateral_amount, - &collateral_market_after, - env.block.time.seconds(), + execute( + deps.as_mut(), + env.clone(), + mock_info(provider.as_str(), &[coin(1000000, "untrn")]), + ExecuteMsg::Deposit {}, ) .unwrap(); - let expected_reward_amount_scaled = compute_scaled_amount( - expected_debt_rates.protocol_rewards_to_distribute, - expected_debt_rates.liquidity_index, - ScalingOperation::Truncate, + // liquidatee deposits and borrows + execute( + deps.as_mut(), + env.clone(), + mock_info(liquidatee.as_str(), &[coin(10000, "uosmo")]), + ExecuteMsg::Deposit {}, + ) + .unwrap(); + execute( + deps.as_mut(), + env.clone(), + mock_info(liquidatee.as_str(), &[coin(900, "uatom")]), + ExecuteMsg::Deposit {}, + ) + .unwrap(); + execute( + deps.as_mut(), + env.clone(), + mock_info(liquidatee.as_str(), &[]), + ExecuteMsg::Borrow { + denom: "uusdc".to_string(), + amount: Uint128::from(3000u128), + recipient: None, + }, + ) + .unwrap(); + execute( + deps.as_mut(), + env, + mock_info(liquidatee.as_str(), &[]), + ExecuteMsg::Borrow { + denom: "untrn".to_string(), + amount: Uint128::from(1200u128), + recipient: None, + }, ) .unwrap(); - let expected_msgs = expected_messages( - &user_addr, - &liquidator_addr, - user_collateral_scaled_before, - Uint128::zero(), - &collateral_market, - &collateral_market, - ); - assert_eq!(res.messages, expected_msgs); - - mars_testing::assert_eq_vec( - res.attributes, - vec![ - attr("action", "liquidate"), - attr("user", user_addr.as_str()), - attr("liquidator", liquidator_addr.as_str()), - attr("recipient", liquidator_addr.as_str()), - attr("collateral_denom", collateral_market.denom.as_str()), - attr("collateral_amount", expected_liquidated_collateral_amount), - attr("collateral_amount_scaled", expected_liquidated_collateral_amount_scaled), - attr("debt_denom", collateral_market.denom.as_str()), - attr("debt_amount", debt_to_repay), - attr("debt_amount_scaled", expected_debt_rates.less_debt_scaled), - ], - ); - assert_eq!( - res.events, - vec![th_build_interests_updated_event(&collateral_market.denom, &expected_debt_rates)] - ); + // change price to be able to liquidate + deps.querier.set_oracle_price("uosmo", Decimal::from_ratio(2u128, 1u128)); - // user's collateral scaled amount should have been correctly decreased - let collateral = - COLLATERALS.load(deps.as_ref().storage, (&user_addr, &collateral_market.denom)).unwrap(); - assert_eq!( - collateral.amount_scaled, - user_collateral_scaled_before - expected_liquidated_collateral_amount_scaled + let collateral_market: Market = th_query( + deps.as_ref(), + QueryMsg::Market { + denom: "uosmo".to_string(), + }, ); - - // liquidator's collateral scaled amount should have been correctly increased - let collateral = COLLATERALS - .load(deps.as_ref().storage, (&liquidator_addr, &collateral_market.denom)) - .unwrap(); - assert_eq!(collateral.amount_scaled, expected_liquidated_collateral_amount_scaled); - - // check user's debt decreased by the appropriate amount - let debt = DEBTS.load(&deps.storage, (&user_addr, &collateral_market.denom)).unwrap(); - assert_eq!(debt.amount_scaled, user_debt_scaled_before - expected_debt_rates.less_debt_scaled); - - // check global debt decreased by the appropriate amount - assert_eq!( - debt_market_after.debt_total_scaled, - collateral_market.debt_total_scaled - expected_debt_rates.less_debt_scaled + let debt_market: Market = th_query( + deps.as_ref(), + QueryMsg::Market { + denom: "uusdc".to_string(), + }, ); - - // rewards collector's collateral scaled amount **of the debt asset** should have been correctly increased - let collateral = rewards_collector_collateral(deps.as_ref(), &collateral_market.denom); - assert_eq!(collateral.amount_scaled, expected_reward_amount_scaled); - - // global collateral scaled amount **of the debt asset** should have been correctly increased - assert_eq!( - debt_market_after.collateral_total_scaled, - collateral_market.collateral_total_scaled + expected_reward_amount_scaled + let liquidatee_collateral: UserCollateralResponse = th_query( + deps.as_ref(), + QueryMsg::UserCollateral { + user: liquidatee.to_string(), + denom: "uosmo".to_string(), + }, ); -} - -/// FIXME: new clippy version warns to remove clone() from "collateral_market.clone()" but then it breaks compilation -#[allow(clippy::redundant_clone)] -#[test] -fn liquidate_with_refund_if_same_asset_for_debt_and_collateral() { - let TestSuite { - mut deps, - collateral_price, - close_factor, - collateral_market, - collateral_asset_params, - .. - } = setup_test(); - let debt_price = collateral_price; - - let user_addr = Addr::unchecked("user"); - let liquidator_addr = Addr::unchecked("liquidator"); - - let user_collateral_scaled_before = Uint128::from(2_000_000u64) * SCALING_FACTOR; - let user_debt_scaled_before = compute_scaled_amount( - Uint128::from(3_000_000u64), - collateral_market.borrow_index, - ScalingOperation::Ceil, - ) - .unwrap(); - - set_collateral( - deps.as_mut(), - &user_addr, - &collateral_market.denom, - user_collateral_scaled_before, - true, + let liquidatee_debt: UserDebtResponse = th_query( + deps.as_ref(), + QueryMsg::UserDebt { + user: liquidatee.to_string(), + denom: "uusdc".to_string(), + }, ); - set_debt(deps.as_mut(), &user_addr, &collateral_market.denom, user_debt_scaled_before, false); - - let liquidate_msg = ExecuteMsg::Liquidate { - user: user_addr.to_string(), - collateral_denom: collateral_market.denom.clone(), - recipient: None, - }; - let debt_to_repay = Uint128::from(10_000_000_u64); - let block_time = 16_000_000; + let debt_to_repay = 2883_u128; + let block_time = 200_000_000; let env = mock_env_at_block_time(block_time); - let info = mock_info( - liquidator_addr.as_str(), - &coins(debt_to_repay.u128(), collateral_market.denom.clone()), - ); - let res = execute(deps.as_mut(), env, info, liquidate_msg).unwrap(); - - // get expected indices and rates for debt and collateral markets - let expected_debt_indices = th_get_expected_indices(&collateral_market, block_time); - let user_debt_asset_total_debt = compute_underlying_amount( - user_debt_scaled_before, - expected_debt_indices.borrow, - ScalingOperation::Ceil, + let info = mock_info(liquidator.as_str(), &[coin(debt_to_repay, "uusdc")]); + let res = execute( + deps.as_mut(), + env, + info, + ExecuteMsg::Liquidate { + user: liquidatee.to_string(), + collateral_denom: "uosmo".to_string(), + recipient: None, + }, ) .unwrap(); - // since debt is being over_repayed, we expect to max out the liquidatable debt - let expected_less_debt = user_debt_asset_total_debt * close_factor; - - let expected_refund_amount = debt_to_repay - expected_less_debt; let expected_debt_rates = th_get_expected_indices_and_rates( - &collateral_market, + &debt_market, block_time, TestUtilizationDeltaInfo { - less_debt: expected_less_debt, - user_current_debt_scaled: user_debt_scaled_before, - less_liquidity: expected_refund_amount, + less_debt: Uint128::new(2883u128), + user_current_debt_scaled: liquidatee_debt.amount_scaled, ..Default::default() }, ); - let expected_liquidated_collateral_amount = math::divide_uint128_by_decimal( - expected_less_debt - * debt_price - * (Decimal::one() + collateral_asset_params.liquidation_bonus), - collateral_price, - ) - .unwrap(); - let expected_collateral_rates = th_get_expected_indices_and_rates( &collateral_market, block_time, - TestUtilizationDeltaInfo { - less_liquidity: expected_liquidated_collateral_amount, - ..Default::default() - }, + TestUtilizationDeltaInfo::default(), ); - let debt_market_after = MARKETS.load(&deps.storage, &collateral_market.denom).unwrap(); - - let expected_liquidated_collateral_amount_scaled = compute_scaled_amount( - expected_liquidated_collateral_amount, - expected_collateral_rates.liquidity_index, - ScalingOperation::Truncate, - ) - .unwrap(); - - let expected_reward_amount_scaled = compute_scaled_amount( - expected_debt_rates.protocol_rewards_to_distribute, - expected_debt_rates.liquidity_index, - ScalingOperation::Truncate, - ) - .unwrap(); - - let mut expected_msgs = expected_messages( - &user_addr, - &liquidator_addr, - user_collateral_scaled_before, - Uint128::zero(), - &collateral_market, - &collateral_market, + let debt_market_after: Market = th_query( + deps.as_ref(), + QueryMsg::Market { + denom: "uusdc".to_string(), + }, ); - expected_msgs.push(SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: liquidator_addr.to_string(), - amount: coins(expected_refund_amount.u128(), collateral_market.denom.clone()), - }))); - assert_eq!(res.messages, expected_msgs); + + assert_eq!(debt_market_after.borrow_index, expected_debt_rates.borrow_index); + assert_eq!(debt_market_after.liquidity_index, expected_debt_rates.liquidity_index); mars_testing::assert_eq_vec( + res.attributes, vec![ attr("action", "liquidate"), - attr("user", user_addr.as_str()), - attr("liquidator", liquidator_addr.as_str()), - attr("recipient", liquidator_addr.as_str()), - attr("collateral_denom", collateral_market.denom.as_str()), - attr("collateral_amount", expected_liquidated_collateral_amount), - attr("collateral_amount_scaled", expected_liquidated_collateral_amount_scaled), - attr("debt_denom", collateral_market.denom.as_str()), - attr("debt_amount", expected_less_debt), + attr("user", liquidatee.as_str()), + attr("liquidator", liquidator.as_str()), + attr("recipient", liquidator.as_str()), + attr("collateral_denom", "uosmo"), + attr("collateral_amount", Uint128::new(9998u128)), + attr( + "collateral_amount_scaled", + th_get_scaled_liquidity_amount( + Uint128::new(9998u128), + expected_collateral_rates.liquidity_index, + ), + ), + attr("debt_denom", "uusdc"), + attr("debt_amount", Uint128::new(2883u128)), attr("debt_amount_scaled", expected_debt_rates.less_debt_scaled), ], - res.attributes, - ); - assert_eq!( - res.events, - vec![th_build_interests_updated_event(&collateral_market.denom, &expected_debt_rates)], - ); - - // user's collateral scaled amount should have been correctly decreased - let collateral = - COLLATERALS.load(deps.as_ref().storage, (&user_addr, &collateral_market.denom)).unwrap(); - assert_eq!( - collateral.amount_scaled, - user_collateral_scaled_before - expected_liquidated_collateral_amount_scaled - ); - - // liquidator's collateral scaled amount should have been correctly increased - let collateral = COLLATERALS - .load(deps.as_ref().storage, (&liquidator_addr, &collateral_market.denom)) - .unwrap(); - assert_eq!(collateral.amount_scaled, expected_liquidated_collateral_amount_scaled); - - // check user's debt decreased by the appropriate amount - let debt = DEBTS.load(&deps.storage, (&user_addr, &collateral_market.denom)).unwrap(); - assert_eq!(debt.amount_scaled, user_debt_scaled_before - expected_debt_rates.less_debt_scaled); - - // check global debt decreased by the appropriate amount - assert_eq!( - debt_market_after.debt_total_scaled, - collateral_market.debt_total_scaled - expected_debt_rates.less_debt_scaled - ); - - // rewards collector's collateral scaled amount **of the debt asset** should have been correctly increased - let collateral = rewards_collector_collateral(deps.as_ref(), &collateral_market.denom); - assert_eq!(collateral.amount_scaled, expected_reward_amount_scaled); - - // global collateral scaled amount **of the debt asset** should have been correctly increased - assert_eq!( - debt_market_after.collateral_total_scaled, - collateral_market.collateral_total_scaled + expected_reward_amount_scaled ); -} -#[test] -fn liquidate_with_recipient_for_underlying_collateral() { - let mut ts = setup_test(); - - let user_addr = Addr::unchecked("user"); - let liquidator_addr = Addr::unchecked("liquidator"); - let recipient_addr = Addr::unchecked("recipient"); - - let user_collateral_scaled_before = Uint128::from(2_000_000u64) * SCALING_FACTOR; - let user_debt_scaled_before = compute_scaled_amount( - Uint128::from(3_000_000u64), - ts.debt_market.borrow_index, - ScalingOperation::Ceil, - ) - .unwrap(); + assert_eq!(res.events, vec![th_build_interests_updated_event("uusdc", &expected_debt_rates)]); - set_collateral( - ts.deps.as_mut(), - &user_addr, - &ts.collateral_market.denom, - user_collateral_scaled_before, - true, - ); - set_debt(ts.deps.as_mut(), &user_addr, &ts.debt_market.denom, user_debt_scaled_before, false); - - let liquidate_msg = ExecuteMsg::Liquidate { - user: user_addr.to_string(), - collateral_denom: ts.collateral_market.denom.clone(), - recipient: Some(recipient_addr.to_string()), - }; - - let debt_to_repay = Uint128::from(10_000_000_u64); - let block_time = 16_000_000; - let env = mock_env_at_block_time(block_time); - let info = mock_info( - liquidator_addr.as_str(), - &coins(debt_to_repay.u128(), ts.debt_market.denom.clone()), - ); - let res = execute(ts.deps.as_mut(), env, info, liquidate_msg).unwrap(); - - let TestExpectedAmountResults { - user_debt_repayed, - user_debt_repayed_scaled, - expected_refund_amount, - expected_liquidated_collateral_amount, - expected_liquidated_collateral_amount_scaled, - expected_reward_amount_scaled, - expected_debt_rates, - .. - } = expected_amounts(block_time, user_debt_scaled_before, debt_to_repay, &ts); - - let mut expected_msgs = expected_messages( - &user_addr, - &recipient_addr, - user_collateral_scaled_before, + let expected_msgs = expected_messages( + &liquidatee, + &liquidator, + liquidatee_collateral.amount_scaled, Uint128::zero(), - &ts.collateral_market, - &ts.debt_market, + &collateral_market, + &debt_market, ); - expected_msgs.push(SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: liquidator_addr.to_string(), - amount: coins(expected_refund_amount.u128(), ts.debt_market.denom.clone()), - }))); assert_eq!(res.messages, expected_msgs); +} - mars_testing::assert_eq_vec( - vec![ - attr("action", "liquidate"), - attr("user", user_addr.as_str()), - attr("liquidator", liquidator_addr.as_str()), - attr("recipient", recipient_addr.as_str()), - attr("collateral_denom", ts.collateral_market.denom.as_str()), - attr("collateral_amount", expected_liquidated_collateral_amount), - attr("collateral_amount_scaled", expected_liquidated_collateral_amount_scaled), - attr("debt_denom", ts.debt_market.denom.as_str()), - attr("debt_amount", user_debt_repayed), - attr("debt_amount_scaled", user_debt_repayed_scaled), - ], - res.attributes, - ); - assert_eq!( - res.events, - vec![th_build_interests_updated_event(&ts.debt_market.denom, &expected_debt_rates)], - ); - - let debt_market_after = MARKETS.load(&ts.deps.storage, &ts.debt_market.denom).unwrap(); +// recipient - can be liquidator or another address which can receive collateral +fn expected_messages( + user_addr: &Addr, + recipient_addr: &Addr, + user_collateral_scaled: Uint128, + recipient_collateral_scaled: Uint128, + collateral_market: &Market, + debt_market: &Market, +) -> Vec { + // there should be up to three messages updating indices at the incentives contract, in the + // order: + // - collateral denom, user + // - collateral denom, liquidator + // - debt denom, rewards collector (if rewards accrued > 0) + // + // NOTE that we don't expect a message to update rewards collector's index of the + // **collateral** asset, because the liquidation action does NOT change the collateral + // asset's utilization rate, it's interest rate does not need to be updated. + vec![ + SubMsg::new(WasmMsg::Execute { + contract_addr: MarsAddressType::Incentives.to_string(), + msg: to_binary(&incentives::ExecuteMsg::BalanceChange { + user_addr: user_addr.clone(), + denom: collateral_market.denom.clone(), + user_amount_scaled_before: user_collateral_scaled, + total_amount_scaled_before: collateral_market.collateral_total_scaled, + }) + .unwrap(), + funds: vec![], + }), + SubMsg::new(WasmMsg::Execute { + contract_addr: MarsAddressType::Incentives.to_string(), + msg: to_binary(&incentives::ExecuteMsg::BalanceChange { + user_addr: recipient_addr.clone(), + denom: collateral_market.denom.clone(), + user_amount_scaled_before: recipient_collateral_scaled, + total_amount_scaled_before: collateral_market.collateral_total_scaled, + }) + .unwrap(), + funds: vec![], + }), + SubMsg::new(WasmMsg::Execute { + contract_addr: MarsAddressType::Incentives.to_string(), + msg: to_binary(&incentives::ExecuteMsg::BalanceChange { + user_addr: Addr::unchecked(MarsAddressType::RewardsCollector.to_string()), + denom: collateral_market.denom.clone(), + user_amount_scaled_before: Uint128::zero(), + total_amount_scaled_before: collateral_market.collateral_total_scaled, + }) + .unwrap(), + funds: vec![], + }), + SubMsg::new(WasmMsg::Execute { + contract_addr: MarsAddressType::Incentives.to_string(), + msg: to_binary(&incentives::ExecuteMsg::BalanceChange { + user_addr: Addr::unchecked(MarsAddressType::RewardsCollector.to_string()), + denom: debt_market.denom.clone(), + user_amount_scaled_before: Uint128::zero(), + total_amount_scaled_before: debt_market.collateral_total_scaled, + }) + .unwrap(), + funds: vec![], + }), + ] +} - // user's collateral scaled amount should have been correctly decreased - let collateral = COLLATERALS - .load(ts.deps.as_ref().storage, (&user_addr, &ts.collateral_market.denom)) - .unwrap(); - assert_eq!( - collateral.amount_scaled, - user_collateral_scaled_before - expected_liquidated_collateral_amount_scaled +fn setup_env(mock_env: &mut MockEnv) -> (u128, Addr, Addr, Addr) { + let funded_amt = 1_000_000_000_000u128; + let provider = Addr::unchecked("provider"); // provides collateral to be borrowed by others + let liquidatee = Addr::unchecked("liquidatee"); + let liquidator = Addr::unchecked("liquidator"); + + // setup red-bank + let red_bank = mock_env.red_bank.clone(); + let params = mock_env.params.clone(); + let (market_params, asset_params) = + default_asset_params_with("uosmo", Decimal::percent(70), Decimal::percent(78)); + red_bank.init_asset(mock_env, &asset_params.denom, market_params); + params.init_params(mock_env, asset_params); + let (market_params, asset_params) = + default_asset_params_with("ujake", Decimal::percent(50), Decimal::percent(55)); + red_bank.init_asset(mock_env, &asset_params.denom, market_params); + params.init_params(mock_env, asset_params); + let (market_params, asset_params) = + default_asset_params_with("uatom", Decimal::percent(82), Decimal::percent(90)); + red_bank.init_asset(mock_env, &asset_params.denom, market_params); + params.init_params(mock_env, asset_params); + let (market_params, asset_params) = + default_asset_params_with("uusdc", Decimal::percent(90), Decimal::percent(95)); + red_bank.init_asset(mock_env, &asset_params.denom, market_params); + params.init_params(mock_env, asset_params); + let (market_params, asset_params) = + default_asset_params_with("untrn", Decimal::percent(90), Decimal::percent(96)); + red_bank.init_asset(mock_env, &asset_params.denom, market_params); + params.init_params(mock_env, asset_params); + + // setup oracle + let oracle = mock_env.oracle.clone(); + oracle.set_price_source_fixed(mock_env, "uosmo", Decimal::from_ratio(22u128, 10u128)); + oracle.set_price_source_fixed(mock_env, "ujake", Decimal::one()); + oracle.set_price_source_fixed(mock_env, "uatom", Decimal::from_ratio(82u128, 10u128)); + oracle.set_price_source_fixed(mock_env, "uusdc", Decimal::one()); + oracle.set_price_source_fixed(mock_env, "untrn", Decimal::from_ratio(55u128, 10u128)); + + // fund accounts + mock_env.fund_accounts( + &[&provider, &liquidatee, &liquidator], + funded_amt, + &["uosmo", "ujake", "uatom", "uusdc", "untrn", "other"], ); - // liquidator's collateral should be empty - COLLATERALS - .load(ts.deps.as_ref().storage, (&liquidator_addr, &ts.collateral_market.denom)) - .unwrap_err(); + // provider deposits collaterals + red_bank.deposit(mock_env, &provider, coin(1000000, "uusdc")).unwrap(); + red_bank.deposit(mock_env, &provider, coin(1000000, "untrn")).unwrap(); - // recipient's collateral scaled amount should have been correctly increased - let collateral = COLLATERALS - .load(ts.deps.as_ref().storage, (&recipient_addr, &ts.collateral_market.denom)) - .unwrap(); - assert_eq!(collateral.amount_scaled, expected_liquidated_collateral_amount_scaled); + // liquidatee deposits and borrows + red_bank.deposit(mock_env, &liquidatee, coin(10000, "uosmo")).unwrap(); + red_bank.deposit(mock_env, &liquidatee, coin(2000, "ujake")).unwrap(); + red_bank.deposit(mock_env, &liquidatee, coin(900, "uatom")).unwrap(); + red_bank.borrow(mock_env, &liquidatee, "uusdc", 3000).unwrap(); + red_bank.borrow(mock_env, &liquidatee, "untrn", 1200).unwrap(); - // check user's debt decreased by the appropriate amount - let debt = DEBTS.load(&ts.deps.storage, (&user_addr, &ts.debt_market.denom)).unwrap(); - assert_eq!(debt.amount_scaled, user_debt_scaled_before - expected_debt_rates.less_debt_scaled); + (funded_amt, provider, liquidatee, liquidator) +} - // check global debt decreased by the appropriate amount - assert_eq!( - debt_market_after.debt_total_scaled, - ts.debt_market.debt_total_scaled - expected_debt_rates.less_debt_scaled - ); +fn assert_users_and_markets_scaled_amounts( + mock_env: &mut MockEnv, + merged_collaterals: HashMap, + merged_debts: HashMap, +) { + let red_bank = mock_env.red_bank.clone(); + + let markets = red_bank.query_markets(mock_env); + assert_eq!(markets.len(), 5); + let osmo_market = markets.get("uosmo").unwrap(); + let jake_market = markets.get("ujake").unwrap(); + let atom_market = markets.get("uatom").unwrap(); + let usdc_market = markets.get("uusdc").unwrap(); + let ntrn_market = markets.get("untrn").unwrap(); + assert_eq!(merged_collaterals.get_or_default("uosmo"), osmo_market.collateral_total_scaled); + assert_eq!(merged_debts.get_or_default("uosmo"), osmo_market.debt_total_scaled); + assert_eq!(merged_collaterals.get_or_default("ujake"), jake_market.collateral_total_scaled); + assert_eq!(merged_debts.get_or_default("ujake"), jake_market.debt_total_scaled); + assert_eq!(merged_collaterals.get_or_default("uatom"), atom_market.collateral_total_scaled); + assert_eq!(merged_debts.get_or_default("uatom"), atom_market.debt_total_scaled); + assert_eq!(merged_collaterals.get_or_default("uusdc"), usdc_market.collateral_total_scaled); + assert_eq!(merged_debts.get_or_default("uusdc"), usdc_market.debt_total_scaled); + assert_eq!(merged_collaterals.get_or_default("untrn"), ntrn_market.collateral_total_scaled); + assert_eq!(merged_debts.get_or_default("untrn"), ntrn_market.debt_total_scaled); +} - // rewards collector's collateral scaled amount **of the debt asset** should have been correctly increased - let collateral = rewards_collector_collateral(ts.deps.as_ref(), &ts.debt_market.denom); - assert_eq!(collateral.amount_scaled, expected_reward_amount_scaled); +fn assert_underlying_balances(mock_env: &MockEnv, merged_balances: HashMap) { + let red_bank = mock_env.red_bank.clone(); - // global collateral scaled amount **of the debt asset** should have been correctly increased - assert_eq!( - debt_market_after.collateral_total_scaled, - ts.debt_market.collateral_total_scaled + expected_reward_amount_scaled - ); + let balances = mock_env.query_all_balances(&red_bank.contract_addr); + assert_eq!(merged_balances.get("uosmo"), balances.get("uosmo")); + assert_eq!(merged_balances.get("ujake"), balances.get("ujake")); + assert_eq!(merged_balances.get("uatom"), balances.get("uatom")); + assert_eq!(merged_balances.get("uusdc"), balances.get("uusdc")); + assert_eq!(merged_balances.get("untrn"), balances.get("untrn")); } -#[test] -fn liquidation_health_factor_check() { - // initialize collateral and debt markets - let available_liquidity_collateral = Uint128::from(1000000000u128); - let available_liquidity_debt = Uint128::from(2000000000u128); - let mut deps = th_setup(&[ - coin(available_liquidity_collateral.into(), "collateral"), - coin(available_liquidity_debt.into(), "debt"), - ]); - - deps.querier.set_oracle_price("collateral", Decimal::one()); - deps.querier.set_oracle_price("debt", Decimal::one()); - deps.querier.set_oracle_price("uncollateralized_debt", Decimal::one()); - - let collateral_ltv = Decimal::from_ratio(5u128, 10u128); - let collateral_liquidation_threshold = Decimal::from_ratio(7u128, 10u128); - let collateral_liquidation_bonus = Decimal::from_ratio(1u128, 10u128); - - let collateral_market = Market { - debt_total_scaled: Uint128::zero(), - liquidity_index: Decimal::one(), - borrow_index: Decimal::one(), - ..Default::default() - }; - let debt_market = Market { - debt_total_scaled: Uint128::new(20_000_000) * SCALING_FACTOR, - liquidity_index: Decimal::one(), - borrow_index: Decimal::one(), - ..Default::default() - }; - let uncollateralized_debt_market = Market { - denom: "uncollateralized_debt".to_string(), - ..Default::default() +fn default_asset_params_with( + denom: &str, + max_loan_to_value: Decimal, + liquidation_threshold: Decimal, +) -> (InitOrUpdateAssetParams, AssetParams) { + let market_params = InitOrUpdateAssetParams { + reserve_factor: Some(Decimal::percent(20)), + interest_rate_model: Some(InterestRateModel { + optimal_utilization_rate: Decimal::percent(10), + base: Decimal::percent(30), + slope_1: Decimal::percent(25), + slope_2: Decimal::percent(30), + }), }; - - // initialize markets - th_init_market(deps.as_mut(), "collateral", &collateral_market); - th_init_market(deps.as_mut(), "debt", &debt_market); - th_init_market(deps.as_mut(), "uncollateralized_debt", &uncollateralized_debt_market); - - deps.querier.set_redbank_params( - "collateral", - AssetParams { - max_loan_to_value: collateral_ltv, - liquidation_threshold: collateral_liquidation_threshold, - liquidation_bonus: collateral_liquidation_bonus, - ..th_default_asset_params() + let asset_params = AssetParams { + denom: denom.to_string(), + credit_manager: CmSettings { + whitelisted: false, + hls: None, }, - ); - deps.querier.set_redbank_params( - "debt", - AssetParams { - max_loan_to_value: Decimal::from_ratio(6u128, 10u128), - ..th_default_asset_params() + red_bank: RedBankSettings { + deposit_enabled: true, + borrow_enabled: true, + deposit_cap: Uint128::MAX, }, - ); - deps.querier.set_redbank_params("uncollateralized_debt", th_default_asset_params()); - - // test health factor check - let healthy_user_addr = Addr::unchecked("healthy_user"); - - // set initial collateral and debt balances for user - let healthy_user_collateral_balance_scaled = Uint128::new(10_000_000) * SCALING_FACTOR; - set_collateral( - deps.as_mut(), - &healthy_user_addr, - "collateral", - healthy_user_collateral_balance_scaled, - true, - ); - - let healthy_user_debt_amount_scaled = - Uint128::new(healthy_user_collateral_balance_scaled.u128()) - * collateral_liquidation_threshold; - let healthy_user_debt = Debt { - amount_scaled: healthy_user_debt_amount_scaled, - uncollateralized: false, - }; - let uncollateralized_debt = Debt { - amount_scaled: Uint128::new(10_000) * SCALING_FACTOR, - uncollateralized: true, - }; - DEBTS.save(deps.as_mut().storage, (&healthy_user_addr, "debt"), &healthy_user_debt).unwrap(); - DEBTS - .save( - deps.as_mut().storage, - (&healthy_user_addr, "uncollateralized_debt"), - &uncollateralized_debt, - ) - .unwrap(); - - // perform liquidation (should fail because health factor is > 1) - let liquidator_addr = Addr::unchecked("liquidator"); - let debt_to_cover = Uint128::from(1_000_000u64); - - let liquidate_msg = ExecuteMsg::Liquidate { - user: healthy_user_addr.to_string(), - collateral_denom: "collateral".to_string(), - recipient: None, + max_loan_to_value, + liquidation_threshold, + liquidation_bonus: LiquidationBonus { + starting_lb: Decimal::percent(1), + slope: Decimal::from_str("2.0").unwrap(), + min_lb: Decimal::percent(2), + max_lb: Decimal::percent(10), + }, + protocol_liquidation_fee: Decimal::percent(2), }; - - let env = mock_env(MockEnvParams::default()); - let info = mock_info(liquidator_addr.as_str(), &coins(debt_to_cover.u128(), "debt")); - let error_res = execute(deps.as_mut(), env, info, liquidate_msg).unwrap_err(); - assert_eq!(error_res, ContractError::CannotLiquidateHealthyPosition {}); + (market_params, asset_params) } -#[test] -fn liquidate_if_collateral_disabled() { - // initialize collateral and debt markets - let mut deps = th_setup(&[]); - - let collateral_market_1 = Market { - ..Default::default() - }; - let collateral_market_2 = Market { - ..Default::default() - }; - let debt_market = Market { - ..Default::default() - }; - - // initialize markets - th_init_market(deps.as_mut(), "collateral1", &collateral_market_1); - th_init_market(deps.as_mut(), "collateral2", &collateral_market_2); - th_init_market(deps.as_mut(), "debt", &debt_market); - - // Set user as having collateral and debt in respective markets - let user_addr = Addr::unchecked("user"); - set_collateral(deps.as_mut(), &user_addr, "collateral1", Uint128::new(123), true); - set_collateral(deps.as_mut(), &user_addr, "collateral2", Uint128::new(123), false); - - // perform liquidation (should fail because collateral2 isn't set as collateral for user) - let liquidator_addr = Addr::unchecked("liquidator"); - let debt_to_cover = Uint128::from(1_000_000u64); - - let liquidate_msg = ExecuteMsg::Liquidate { - user: user_addr.to_string(), - collateral_denom: "collateral2".to_string(), - recipient: None, - }; - - let env = mock_env(MockEnvParams::default()); - let info = mock_info(liquidator_addr.as_str(), &coins(debt_to_cover.u128(), "debt")); - let error_res = execute(deps.as_mut(), env, info, liquidate_msg).unwrap_err(); - assert_eq!( - error_res, - ContractError::CannotLiquidateWhenCollateralUnset { - denom: "collateral2".to_string() - } - ); +trait MapDefaultValue { + fn get_or_default(&self, key: &str) -> Uint128; } -#[test] -fn liquidator_cannot_receive_collaterals_without_spending_coins() { - let market = Market { - liquidity_index: Decimal::one(), - ..Default::default() - }; - let asset_params = AssetParams { - liquidation_bonus: Decimal::from_ratio(1u128, 10u128), - ..th_default_asset_params() - }; - let res_err = liquidation_compute_amounts( - Uint128::new(320000000), - Uint128::new(800), - Uint128::new(2), - &market, - &asset_params, - Decimal::one(), - Decimal::from_ratio(300u128, 1u128), - 0, - Decimal::from_ratio(1u128, 2u128), - ) - .unwrap_err(); - assert_eq!(res_err, StdError::generic_err("Can't process liquidation. Invalid collateral_amount_to_liquidate (320) and debt_amount_to_repay (0)")) -} - -#[test] -fn cannot_liquidate_without_receiving_collaterals() { - let market = Market { - liquidity_index: Decimal::one(), - ..Default::default() - }; - let asset_params = AssetParams { - liquidation_bonus: Decimal::from_ratio(1u128, 10u128), - ..th_default_asset_params() - }; - let res_err = liquidation_compute_amounts( - Uint128::new(320000000), - Uint128::new(20), - Uint128::new(30), - &market, - &asset_params, - Decimal::from_ratio(12u128, 1u128), - Decimal::one(), - 0, - Decimal::from_ratio(1u128, 2u128), - ) - .unwrap_err(); - assert_eq!(res_err, StdError::generic_err("Can't process liquidation. Invalid collateral_amount_to_liquidate (0) and debt_amount_to_repay (10)")) +impl MapDefaultValue for HashMap { + fn get_or_default(&self, key: &str) -> Uint128 { + self.get(key).cloned().unwrap_or(Uint128::zero()) + } } diff --git a/contracts/red-bank/tests/test_misc.rs b/contracts/red-bank/tests/test_misc.rs index 0cddaf106..c15b24aa7 100644 --- a/contracts/red-bank/tests/test_misc.rs +++ b/contracts/red-bank/tests/test_misc.rs @@ -7,7 +7,7 @@ use helpers::{ TestUtilizationDeltaInfo, }; use mars_owner::OwnerError::NotOwner; -use mars_params::types::AssetParams; +use mars_params::types::asset::AssetParams; use mars_red_bank::{ contract::execute, error::ContractError, diff --git a/contracts/red-bank/tests/test_withdraw.rs b/contracts/red-bank/tests/test_withdraw.rs index 77db18ea4..1020d6698 100644 --- a/contracts/red-bank/tests/test_withdraw.rs +++ b/contracts/red-bank/tests/test_withdraw.rs @@ -7,7 +7,7 @@ use helpers::{ has_collateral_position, set_collateral, th_build_interests_updated_event, th_get_expected_indices_and_rates, th_setup, TestUtilizationDeltaInfo, }; -use mars_params::types::AssetParams; +use mars_params::types::asset::AssetParams; use mars_red_bank::{ contract::execute, error::ContractError, diff --git a/integration-tests/tests/helpers.rs b/integration-tests/tests/helpers.rs index 53d0283ff..868b8d380 100644 --- a/integration-tests/tests/helpers.rs +++ b/integration-tests/tests/helpers.rs @@ -1,9 +1,9 @@ #![allow(dead_code)] use anyhow::Result as AnyResult; -use cosmwasm_std::{Coin, Decimal, Uint128}; +use cosmwasm_std::{Coin, Decimal, Fraction, Uint128}; use cw_multi_test::AppResponse; -use mars_params::types::{AssetParams, HighLeverageStrategyParams, RedBankSettings, RoverSettings}; +use mars_params::types::asset::{AssetParams, CmSettings, LiquidationBonus, RedBankSettings}; use mars_red_bank::error::ContractError; use mars_red_bank_types::red_bank::{ InitOrUpdateAssetParams, InterestRateModel, UserHealthStatus, UserPositionResponse, @@ -14,7 +14,7 @@ use osmosis_std::types::osmosis::{ }; use osmosis_test_tube::{Account, ExecuteResponse, OsmosisTestApp, Runner, SigningAccount}; -pub fn default_asset_params() -> (InitOrUpdateAssetParams, AssetParams) { +pub fn default_asset_params(denom: &str) -> (InitOrUpdateAssetParams, AssetParams) { let market_params = InitOrUpdateAssetParams { reserve_factor: Some(Decimal::percent(20)), interest_rate_model: Some(InterestRateModel { @@ -25,12 +25,10 @@ pub fn default_asset_params() -> (InitOrUpdateAssetParams, AssetParams) { }), }; let asset_params = AssetParams { - rover: RoverSettings { + denom: denom.to_string(), + credit_manager: CmSettings { whitelisted: false, - hls: HighLeverageStrategyParams { - max_loan_to_value: Decimal::percent(90), - liquidation_threshold: Decimal::one(), - }, + hls: None, }, red_bank: RedBankSettings { deposit_enabled: true, @@ -39,15 +37,22 @@ pub fn default_asset_params() -> (InitOrUpdateAssetParams, AssetParams) { }, max_loan_to_value: Decimal::percent(60), liquidation_threshold: Decimal::percent(80), - liquidation_bonus: Decimal::percent(10), + liquidation_bonus: LiquidationBonus { + starting_lb: Decimal::percent(0u64), + slope: Decimal::one(), + min_lb: Decimal::percent(0u64), + max_lb: Decimal::percent(5u64), + }, + protocol_liquidation_fee: Decimal::percent(2u64), }; (market_params, asset_params) } pub fn default_asset_params_with( + denom: &str, max_loan_to_value: Decimal, liquidation_threshold: Decimal, - liquidation_bonus: Decimal, + liquidation_bonus: LiquidationBonus, ) -> (InitOrUpdateAssetParams, AssetParams) { let market_params = InitOrUpdateAssetParams { reserve_factor: Some(Decimal::percent(20)), @@ -59,12 +64,10 @@ pub fn default_asset_params_with( }), }; let asset_params = AssetParams { - rover: RoverSettings { + denom: denom.to_string(), + credit_manager: CmSettings { whitelisted: false, - hls: HighLeverageStrategyParams { - max_loan_to_value: Decimal::percent(90), - liquidation_threshold: Decimal::one(), - }, + hls: None, }, red_bank: RedBankSettings { deposit_enabled: true, @@ -74,6 +77,7 @@ pub fn default_asset_params_with( max_loan_to_value, liquidation_threshold, liquidation_bonus, + protocol_liquidation_fee: Decimal::percent(2u64), }; (market_params, asset_params) } @@ -88,6 +92,35 @@ pub fn is_user_liquidatable(position: &UserPositionResponse) -> bool { } } +pub fn liq_threshold_hf(position: &UserPositionResponse) -> Decimal { + match position.health_status { + UserHealthStatus::Borrowing { + liq_threshold_hf, + .. + } if liq_threshold_hf < Decimal::one() => liq_threshold_hf, + _ => panic!("User is not liquidatable"), + } +} + +pub fn calculate_max_debt_repayable( + thf: Decimal, + tlf: Decimal, + collateral_liq_th: Decimal, + debt_price: Decimal, + position: &UserPositionResponse, +) -> Uint128 { + let max_debt_repayable_numerator = (thf * position.total_collateralized_debt) + - position.weighted_liquidation_threshold_collateral; + let max_debt_repayable_denominator = thf - (collateral_liq_th * (Decimal::one() + tlf)); + + let max_debt_repayable_value = max_debt_repayable_numerator.multiply_ratio( + max_debt_repayable_denominator.denominator(), + max_debt_repayable_denominator.numerator(), + ); + + max_debt_repayable_value.div_floor(debt_price) +} + pub mod osmosis { use std::fmt::Display; diff --git a/integration-tests/tests/test_incentives.rs b/integration-tests/tests/test_incentives.rs index bdc575d33..fe449fdde 100644 --- a/integration-tests/tests/test_incentives.rs +++ b/integration-tests/tests/test_incentives.rs @@ -20,9 +20,9 @@ fn rewards_claim() { let red_bank = mock_env.red_bank.clone(); let params = mock_env.params.clone(); - let (market_params, asset_params) = default_asset_params(); + let (market_params, asset_params) = default_asset_params("uusdc"); red_bank.init_asset(&mut mock_env, "uusdc", market_params); - params.init_params(&mut mock_env, "uusdc", asset_params); + params.init_params(&mut mock_env, asset_params); let incentives = mock_env.incentives.clone(); incentives.whitelist_incentive_denoms(&mut mock_env, &[("umars", 3)]); @@ -77,13 +77,15 @@ fn emissions_rates() { let red_bank = mock_env.red_bank.clone(); let params = mock_env.params.clone(); - let (market_params, asset_params) = default_asset_params(); - red_bank.init_asset(&mut mock_env, "uusdc", market_params.clone()); - red_bank.init_asset(&mut mock_env, "uosmo", market_params.clone()); + let (market_params, asset_params) = default_asset_params("uusdc"); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("uosmo"); + red_bank.init_asset(&mut mock_env, "uosmo", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("umars"); red_bank.init_asset(&mut mock_env, "umars", market_params); - params.init_params(&mut mock_env, "uusdc", asset_params.clone()); - params.init_params(&mut mock_env, "uosmo", asset_params.clone()); - params.init_params(&mut mock_env, "umars", asset_params); + params.init_params(&mut mock_env, asset_params); let incentives = mock_env.incentives.clone(); incentives.whitelist_incentive_denoms(&mut mock_env, &[("umars", 3)]); @@ -169,13 +171,15 @@ fn no_incentives_accrued_after_withdraw() { let red_bank = mock_env.red_bank.clone(); let params = mock_env.params.clone(); - let (market_params, asset_params) = default_asset_params(); - red_bank.init_asset(&mut mock_env, "uusdc", market_params.clone()); - red_bank.init_asset(&mut mock_env, "uosmo", market_params.clone()); + let (market_params, asset_params) = default_asset_params("uusdc"); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("uosmo"); + red_bank.init_asset(&mut mock_env, "uosmo", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("umars"); red_bank.init_asset(&mut mock_env, "umars", market_params); - params.init_params(&mut mock_env, "uusdc", asset_params.clone()); - params.init_params(&mut mock_env, "uosmo", asset_params.clone()); - params.init_params(&mut mock_env, "umars", asset_params); + params.init_params(&mut mock_env, asset_params); let incentives = mock_env.incentives.clone(); incentives.whitelist_incentive_denoms(&mut mock_env, &[("umars", 3)]); @@ -244,15 +248,18 @@ fn multiple_assets() { let red_bank = mock_env.red_bank.clone(); let params = mock_env.params.clone(); - let (market_params, asset_params) = default_asset_params(); - red_bank.init_asset(&mut mock_env, "uusdc", market_params.clone()); - red_bank.init_asset(&mut mock_env, "uosmo", market_params.clone()); - red_bank.init_asset(&mut mock_env, "uatom", market_params.clone()); + let (market_params, asset_params) = default_asset_params("uusdc"); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("uosmo"); + red_bank.init_asset(&mut mock_env, "uosmo", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("uatom"); + red_bank.init_asset(&mut mock_env, "uatom", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("umars"); red_bank.init_asset(&mut mock_env, "umars", market_params); - params.init_params(&mut mock_env, "uusdc", asset_params.clone()); - params.init_params(&mut mock_env, "uosmo", asset_params.clone()); - params.init_params(&mut mock_env, "uatom", asset_params.clone()); - params.init_params(&mut mock_env, "umars", asset_params); + params.init_params(&mut mock_env, asset_params); // set incentives let incentives = mock_env.incentives.clone(); @@ -327,9 +334,9 @@ fn multiple_users() { let red_bank = mock_env.red_bank.clone(); let params = mock_env.params.clone(); - let (market_params, asset_params) = default_asset_params(); + let (market_params, asset_params) = default_asset_params("uusdc"); red_bank.init_asset(&mut mock_env, "uusdc", market_params); - params.init_params(&mut mock_env, "uusdc", asset_params); + params.init_params(&mut mock_env, asset_params); // set incentives let incentives = mock_env.incentives.clone(); @@ -414,13 +421,15 @@ fn rewards_distributed_among_users_and_rewards_collector() { let red_bank = mock_env.red_bank.clone(); let params = mock_env.params.clone(); - let (market_params, asset_params) = default_asset_params(); - red_bank.init_asset(&mut mock_env, "uusdc", market_params.clone()); - red_bank.init_asset(&mut mock_env, "uosmo", market_params.clone()); + let (market_params, asset_params) = default_asset_params("uusdc"); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("uosmo"); + red_bank.init_asset(&mut mock_env, "uosmo", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("uatom"); red_bank.init_asset(&mut mock_env, "uatom", market_params); - params.init_params(&mut mock_env, "uusdc", asset_params.clone()); - params.init_params(&mut mock_env, "uosmo", asset_params.clone()); - params.init_params(&mut mock_env, "uatom", asset_params); + params.init_params(&mut mock_env, asset_params); // fund user accounts let user_a = Addr::unchecked("user_a"); diff --git a/integration-tests/tests/test_liquidations.rs b/integration-tests/tests/test_liquidations.rs deleted file mode 100644 index ad17808cc..000000000 --- a/integration-tests/tests/test_liquidations.rs +++ /dev/null @@ -1,254 +0,0 @@ -use cosmwasm_std::{coin, Addr, Decimal, Uint128}; -use mars_red_bank_types::red_bank::UserHealthStatus; -use mars_testing::integration::mock_env::MockEnvBuilder; -use mars_utils::math; - -use crate::helpers::{default_asset_params, default_asset_params_with, is_user_liquidatable}; - -mod helpers; - -#[test] -fn liquidate_collateralized_loan() { - let close_factor = Decimal::percent(40); - let atom_price = Decimal::from_ratio(12u128, 1u128); - let osmo_price = Decimal::from_ratio(15u128, 10u128); - let atom_max_ltv = Decimal::percent(60); - let osmo_max_ltv = Decimal::percent(80); - let atom_liq_threshold = Decimal::percent(75); - let osmo_liq_threshold = Decimal::percent(90); - let atom_liq_bonus = Decimal::percent(2); - let osmo_liq_bonus = Decimal::percent(5); - - let owner = Addr::unchecked("owner"); - let mut mock_env = MockEnvBuilder::new(None, owner).close_factor(close_factor).build(); - - // setup oracle and red-bank - let oracle = mock_env.oracle.clone(); - oracle.set_price_source_fixed(&mut mock_env, "uatom", atom_price); - oracle.set_price_source_fixed(&mut mock_env, "uosmo", osmo_price); - oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::one()); - let red_bank = mock_env.red_bank.clone(); - let params = mock_env.params.clone(); - let (market_params, asset_params) = - default_asset_params_with(atom_max_ltv, atom_liq_threshold, atom_liq_bonus); - red_bank.init_asset(&mut mock_env, "uatom", market_params); - params.init_params(&mut mock_env, "uatom", asset_params); - let (market_params, asset_params) = - default_asset_params_with(osmo_max_ltv, osmo_liq_threshold, osmo_liq_bonus); - red_bank.init_asset(&mut mock_env, "uosmo", market_params); - params.init_params(&mut mock_env, "uosmo", asset_params); - let (market_params, asset_params) = default_asset_params(); - red_bank.init_asset(&mut mock_env, "uusdc", market_params); - params.init_params(&mut mock_env, "uusdc", asset_params); - - // fund provider account with usdc - let provider = Addr::unchecked("provider"); - let funded_usdc = 1_000_000_000_000u128; - mock_env.fund_account(&provider, &[coin(funded_usdc, "uusdc")]); - - // fund borrow account with atom and osmo - let borrower = Addr::unchecked("borrower"); - let funded_atom = 1_250_000_000u128; - let funded_osmo = 15_200_000_000u128; - mock_env.fund_account(&borrower, &[coin(funded_atom, "uatom")]); - mock_env.fund_account(&borrower, &[coin(funded_osmo, "uosmo")]); - - // fund liquidator account with usdc - let liquidator = Addr::unchecked("liquidator"); - mock_env.fund_account(&liquidator, &[coin(1_000_000_000_000u128, "uusdc")]); - - // deposits collaterals - red_bank.deposit(&mut mock_env, &provider, coin(funded_usdc, "uusdc")).unwrap(); - red_bank.deposit(&mut mock_env, &borrower, coin(funded_atom, "uatom")).unwrap(); - red_bank.deposit(&mut mock_env, &borrower, coin(funded_osmo, "uosmo")).unwrap(); - - // check HF for borrower - let borrower_position = red_bank.query_user_position(&mut mock_env, &borrower); - assert_eq!(borrower_position.health_status, UserHealthStatus::NotBorrowing); - - // try to borrow more than max LTV, should fail - let max_borrow = atom_max_ltv * (atom_price * Uint128::from(funded_atom)) - + osmo_max_ltv * (osmo_price * Uint128::from(funded_osmo)); - red_bank.borrow(&mut mock_env, &borrower, "uusdc", max_borrow.u128() + 1).unwrap_err(); - - // borrow max allowed amount - red_bank.borrow(&mut mock_env, &borrower, "uusdc", max_borrow.u128()).unwrap(); - let borrower_position = red_bank.query_user_position(&mut mock_env, &borrower); - assert!(!is_user_liquidatable(&borrower_position)); - - // decrease atom price - let atom_price = Decimal::from_ratio(6u128, 1u128); - oracle.set_price_source_fixed(&mut mock_env, "uatom", atom_price); - - // check HF after atom price decrease, should be < 1 - let borrower_position = red_bank.query_user_position(&mut mock_env, &borrower); - assert!(is_user_liquidatable(&borrower_position)); - - // values before liquidation - let redbank_osmo_balance_before = - mock_env.query_balance(&red_bank.contract_addr, "uosmo").unwrap(); - let redbank_usdc_balance_before = - mock_env.query_balance(&red_bank.contract_addr, "uusdc").unwrap(); - let liquidator_osmo_balance_before = mock_env.query_balance(&liquidator, "uosmo").unwrap(); - let liquidator_usdc_balance_before = mock_env.query_balance(&liquidator, "uusdc").unwrap(); - let market_osmo_before = red_bank.query_market(&mut mock_env, "uosmo"); - let market_usdc_before = red_bank.query_market(&mut mock_env, "uusdc"); - let borrower_osmo_collateral_before = - red_bank.query_user_collateral(&mut mock_env, &borrower, "uosmo"); - let borrower_usdc_debt_before = red_bank.query_user_debt(&mut mock_env, &borrower, "uusdc"); - let liquidator_osmo_collateral_before = - red_bank.query_user_collateral(&mut mock_env, &liquidator, "uosmo"); - let borrower_position_before = red_bank.query_user_position(&mut mock_env, &borrower); - - // liquidate borrower (more than close factor in order to get refund) - let max_amount_to_repay = - Uint128::one() * (close_factor * borrower_position_before.total_collateralized_debt); - let osmo_amount_to_liquidate = math::divide_uint128_by_decimal( - max_amount_to_repay * (Decimal::one() + osmo_liq_bonus), - osmo_price, - ) - .unwrap(); - let refund_amount = 15_000_000u128; - red_bank - .liquidate( - &mut mock_env, - &liquidator, - &borrower, - "uosmo", - coin(max_amount_to_repay.u128() + refund_amount, "uusdc"), - ) - .unwrap(); - - // redbank usdc balance is increased by repayed amount - let redbank_usdc_balance = mock_env.query_balance(&red_bank.contract_addr, "uusdc").unwrap(); - assert_eq!( - redbank_usdc_balance.amount, - redbank_usdc_balance_before.amount + max_amount_to_repay - ); - // redbank osmo balance is the same - we need to withdraw funds (collateral) manually - let redbank_osmo_balance = mock_env.query_balance(&red_bank.contract_addr, "uosmo").unwrap(); - assert_eq!(redbank_osmo_balance.amount, redbank_osmo_balance_before.amount); - - // liquidator usdc balance should be decreased by repayed amount - let liquidator_usdc_balance = mock_env.query_balance(&liquidator, "uusdc").unwrap(); - assert_eq!( - liquidator_usdc_balance.amount, - liquidator_usdc_balance_before.amount - max_amount_to_repay - ); - // liquidator osmo balance is the same - we need to withdraw funds (collateral) manually - let liquidator_osmo_balance = mock_env.query_balance(&liquidator, "uosmo").unwrap(); - assert_eq!(liquidator_osmo_balance.amount, liquidator_osmo_balance_before.amount); - - // usdc debt market is decreased by scaled repayed amount - let market_usdc = red_bank.query_market(&mut mock_env, "uusdc"); - let scaled_max_amount_to_repay = - red_bank.query_scaled_debt_amount(&mut mock_env, coin(max_amount_to_repay.u128(), "uusdc")); - assert_eq!( - market_usdc.debt_total_scaled, - market_usdc_before.debt_total_scaled - scaled_max_amount_to_repay - ); - // osmo collateral market is the same - we need to withdraw funds (collateral) manually - let market_osmo = red_bank.query_market(&mut mock_env, "uosmo"); - assert_eq!(market_osmo.collateral_total_scaled, market_osmo_before.collateral_total_scaled); - - // borrower usdc debt is decreased by repayed amount - let borrower_usdc_debt = red_bank.query_user_debt(&mut mock_env, &borrower, "uusdc"); - assert_eq!(borrower_usdc_debt.amount, borrower_usdc_debt_before.amount - max_amount_to_repay); - // borrower osmo collateral is decreased by liquidated amount - let borrower_osmo_collateral = - red_bank.query_user_collateral(&mut mock_env, &borrower, "uosmo"); - assert_eq!( - borrower_osmo_collateral.amount, - borrower_osmo_collateral_before.amount - osmo_amount_to_liquidate - ); - // liquidator osmo collateral is increased by liquidated amount - let liquidator_osmo_collateral = - red_bank.query_user_collateral(&mut mock_env, &liquidator, "uosmo"); - assert_eq!( - liquidator_osmo_collateral.amount, - liquidator_osmo_collateral_before.amount + osmo_amount_to_liquidate - ); - - // withdraw collateral for liquidator - red_bank.withdraw(&mut mock_env, &liquidator, "uosmo", None).unwrap(); - // redbank osmo balance is decreased by liquidated amount - let redbank_osmo_balance = mock_env.query_balance(&red_bank.contract_addr, "uosmo").unwrap(); - assert_eq!( - redbank_osmo_balance.amount, - redbank_osmo_balance_before.amount - osmo_amount_to_liquidate - ); - // liquidator osmo balance is increased by liquidated amount - let liquidator_osmo_balance = mock_env.query_balance(&liquidator, "uosmo").unwrap(); - assert_eq!( - liquidator_osmo_balance.amount, - liquidator_osmo_balance_before.amount + osmo_amount_to_liquidate - ); - // liquidator osmo collateral after withdraw is the same as before liquidation - let liquidator_osmo_collateral = - red_bank.query_user_collateral(&mut mock_env, &liquidator, "uosmo"); - assert_eq!(liquidator_osmo_collateral.amount, liquidator_osmo_collateral_before.amount); - // osmo collateral market is decreased by liquidated amount - let market_osmo = red_bank.query_market(&mut mock_env, "uosmo"); - let scaled_amount_to_liquidate = red_bank.query_scaled_liquidity_amount( - &mut mock_env, - coin(osmo_amount_to_liquidate.u128(), "uosmo"), - ); - assert_eq!( - market_osmo.collateral_total_scaled, - market_osmo_before.collateral_total_scaled - scaled_amount_to_liquidate - ); -} - -#[test] -fn liquidate_uncollateralized_loan() { - let owner = Addr::unchecked("owner"); - let mut mock_env = MockEnvBuilder::new(None, owner.clone()).build(); - - // setup oracle and red-bank - let oracle = mock_env.oracle.clone(); - oracle.set_price_source_fixed(&mut mock_env, "uatom", Decimal::from_ratio(14u128, 1u128)); - oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::one()); - let red_bank = mock_env.red_bank.clone(); - let params = mock_env.params.clone(); - let (market_params, asset_params) = default_asset_params(); - red_bank.init_asset(&mut mock_env, "uatom", market_params.clone()); - red_bank.init_asset(&mut mock_env, "uusdc", market_params); - params.init_params(&mut mock_env, "uatom", asset_params.clone()); - params.init_params(&mut mock_env, "uusdc", asset_params); - - // fund provider account with usdc - let provider = Addr::unchecked("provider"); - let funded_usdc = 1_000_000_000_000u128; - mock_env.fund_account(&provider, &[coin(1_000_000_000_000u128, "uusdc")]); - - // fund provider account with usdc - let liquidator = Addr::unchecked("liquidator"); - mock_env.fund_account(&liquidator, &[coin(1_000_000_000_000u128, "uusdc")]); - - // deposits usdc to redbank - red_bank.deposit(&mut mock_env, &provider, coin(funded_usdc, "uusdc")).unwrap(); - - let borrower = Addr::unchecked("borrower"); - - // set uncollateralized loan limit for borrower - red_bank - .update_uncollateralized_loan_limit( - &mut mock_env, - &owner, - &borrower, - "uusdc", - Uint128::from(10_000_000_000u128), - ) - .unwrap(); - - // borrower borrows usdc - let borrow_amount = 98_000_000u128; - red_bank.borrow(&mut mock_env, &borrower, "uusdc", borrow_amount).unwrap(); - let balance = mock_env.query_balance(&borrower, "uusdc").unwrap(); - assert_eq!(balance.amount.u128(), borrow_amount); - - // try to liquidate, should fail because there are no collateralized loans - red_bank - .liquidate(&mut mock_env, &liquidator, &borrower, "uatom", coin(borrow_amount, "uusdc")) - .unwrap_err(); -} diff --git a/integration-tests/tests/test_oracles.rs b/integration-tests/tests/test_oracles.rs index 5c4804051..f11355abc 100644 --- a/integration-tests/tests/test_oracles.rs +++ b/integration-tests/tests/test_oracles.rs @@ -6,7 +6,7 @@ use mars_oracle_osmosis::{ msg::PriceSourceResponse, Downtime, DowntimeDetector, OsmosisPriceSourceChecked, OsmosisPriceSourceUnchecked, }; -use mars_params::types::AssetParamsUpdate; +use mars_params::msg::AssetParamsUpdate; use mars_red_bank_types::{ address_provider::{ ExecuteMsg::SetAddress, InstantiateMsg as InstantiateAddr, MarsAddressType, @@ -1087,7 +1087,7 @@ fn setup_redbank(wasm: &Wasm, signer: &SigningAccount) -> (Strin OSMOSIS_PARAMS_CONTRACT_NAME, &mars_params::msg::InstantiateMsg { owner: (signer.address()), - max_close_factor: Decimal::percent(10), + target_health_factor: Decimal::from_str("1.05").unwrap(), }, ); @@ -1146,36 +1146,36 @@ fn setup_redbank(wasm: &Wasm, signer: &SigningAccount) -> (Strin ) .unwrap(); - let (market_params, asset_params) = default_asset_params(); + let (market_params, asset_params) = default_asset_params("uosmo"); wasm.execute( &red_bank_addr, &ExecuteRedBank::InitAsset { denom: "uosmo".to_string(), - params: market_params.clone(), + params: market_params, }, &[], signer, ) .unwrap(); - wasm.execute( - &red_bank_addr, - &ExecuteRedBank::InitAsset { - denom: "uatom".to_string(), - params: market_params, - }, + ¶ms_addr, + &mars_params::msg::ExecuteMsg::UpdateAssetParams(AssetParamsUpdate::AddOrUpdate { + params: asset_params.into(), + }), &[], signer, ) .unwrap(); + let (market_params, asset_params) = default_asset_params("uatom"); + wasm.execute( - ¶ms_addr, - &mars_params::msg::ExecuteMsg::UpdateAssetParams(AssetParamsUpdate::AddOrUpdate { - denom: "uosmo".to_string(), - params: asset_params.clone(), - }), + &red_bank_addr, + &ExecuteRedBank::InitAsset { + denom: "uatom".to_string(), + params: market_params, + }, &[], signer, ) @@ -1184,8 +1184,7 @@ fn setup_redbank(wasm: &Wasm, signer: &SigningAccount) -> (Strin wasm.execute( ¶ms_addr, &mars_params::msg::ExecuteMsg::UpdateAssetParams(AssetParamsUpdate::AddOrUpdate { - denom: "uatom".to_string(), - params: asset_params, + params: asset_params.into(), }), &[], signer, diff --git a/integration-tests/tests/test_rover_flow.rs b/integration-tests/tests/test_rover_flow.rs index 6aa228a27..e8a61fde1 100644 --- a/integration-tests/tests/test_rover_flow.rs +++ b/integration-tests/tests/test_rover_flow.rs @@ -19,13 +19,15 @@ fn rover_flow() { oracle.set_price_source_fixed(&mut mock_env, "uatom", Decimal::from_ratio(12u128, 1u128)); let red_bank = mock_env.red_bank.clone(); let params = mock_env.params.clone(); - let (market_params, asset_params) = default_asset_params(); - red_bank.init_asset(&mut mock_env, "uosmo", market_params.clone()); - red_bank.init_asset(&mut mock_env, "uusdc", market_params.clone()); + let (market_params, asset_params) = default_asset_params("uusdc"); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("uosmo"); + red_bank.init_asset(&mut mock_env, "uosmo", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("uatom"); red_bank.init_asset(&mut mock_env, "uatom", market_params); - params.init_params(&mut mock_env, "uosmo", asset_params.clone()); - params.init_params(&mut mock_env, "uusdc", asset_params.clone()); - params.init_params(&mut mock_env, "uatom", asset_params); + params.init_params(&mut mock_env, asset_params); let rover = Addr::unchecked("rover"); diff --git a/integration-tests/tests/test_user_flow.rs b/integration-tests/tests/test_user_flow.rs index 8de1efaba..52ecfbe27 100644 --- a/integration-tests/tests/test_user_flow.rs +++ b/integration-tests/tests/test_user_flow.rs @@ -1,6 +1,7 @@ use std::str::FromStr; use cosmwasm_std::{coin, Addr, Decimal, Uint128}; +use mars_params::types::asset::LiquidationBonus; use mars_red_bank::error::ContractError; use mars_testing::integration::mock_env::{MockEnv, MockEnvBuilder, RedBank}; @@ -19,11 +20,12 @@ fn user_flow() { oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::one()); let red_bank = mock_env.red_bank.clone(); let params = mock_env.params.clone(); - let (market_params, asset_params) = default_asset_params(); - red_bank.init_asset(&mut mock_env, "uatom", market_params.clone()); + let (market_params, asset_params) = default_asset_params("uatom"); + red_bank.init_asset(&mut mock_env, "uatom", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("uusdc"); red_bank.init_asset(&mut mock_env, "uusdc", market_params); - params.init_params(&mut mock_env, "uatom", asset_params.clone()); - params.init_params(&mut mock_env, "uusdc", asset_params); + params.init_params(&mut mock_env, asset_params); // fund user_1 account with atom let user_1 = Addr::unchecked("user_1"); @@ -113,11 +115,12 @@ fn borrow_exact_liquidity() { oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::one()); let red_bank = mock_env.red_bank.clone(); let params = mock_env.params.clone(); - let (market_params, asset_params) = default_asset_params(); - red_bank.init_asset(&mut mock_env, "uatom", market_params.clone()); + let (market_params, asset_params) = default_asset_params("uatom"); + red_bank.init_asset(&mut mock_env, "uatom", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("uusdc"); red_bank.init_asset(&mut mock_env, "uusdc", market_params); - params.init_params(&mut mock_env, "uatom", asset_params.clone()); - params.init_params(&mut mock_env, "uusdc", asset_params); + params.init_params(&mut mock_env, asset_params); // fund provider account with usdc let provider = Addr::unchecked("provider"); @@ -238,11 +241,12 @@ fn prepare_debt_for_repayment() -> (MockEnv, RedBank, Addr) { oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::one()); let red_bank = mock_env.red_bank.clone(); let params = mock_env.params.clone(); - let (market_params, asset_params) = default_asset_params(); - red_bank.init_asset(&mut mock_env, "uatom", market_params.clone()); + let (market_params, asset_params) = default_asset_params("uatom"); + red_bank.init_asset(&mut mock_env, "uatom", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("uusdc"); red_bank.init_asset(&mut mock_env, "uusdc", market_params); - params.init_params(&mut mock_env, "uatom", asset_params.clone()); - params.init_params(&mut mock_env, "uusdc", asset_params); + params.init_params(&mut mock_env, asset_params); // fund user_1 account with atom let user_1 = Addr::unchecked("user_1"); @@ -287,17 +291,20 @@ fn internally_tracked_balances_used_for_borrow() { let borrower = Addr::unchecked("borrower"); let borrower2 = Addr::unchecked("borrower2"); - let close_factor = Decimal::percent(40); let atom_price = Decimal::from_ratio(12u128, 1u128); let osmo_price = Decimal::from_ratio(15u128, 10u128); let atom_max_ltv = Decimal::percent(60); let osmo_max_ltv = Decimal::percent(80); let atom_liq_threshold = Decimal::percent(75); let osmo_liq_threshold = Decimal::percent(90); - let atom_liq_bonus = Decimal::percent(2); - let osmo_liq_bonus = Decimal::percent(5); + let liq_bonus = LiquidationBonus { + starting_lb: Decimal::percent(0u64), + slope: Decimal::one(), + min_lb: Decimal::percent(0u64), + max_lb: Decimal::percent(5u64), + }; - let mut mock_env = MockEnvBuilder::new(None, owner).close_factor(close_factor).build(); + let mut mock_env = MockEnvBuilder::new(None, owner).build(); // setup oracle prices let oracle = mock_env.oracle.clone(); @@ -308,13 +315,13 @@ fn internally_tracked_balances_used_for_borrow() { let red_bank = mock_env.red_bank.clone(); let params = mock_env.params.clone(); let (market_params, asset_params) = - default_asset_params_with(atom_max_ltv, atom_liq_threshold, atom_liq_bonus); + default_asset_params_with("uatom", atom_max_ltv, atom_liq_threshold, liq_bonus.clone()); red_bank.init_asset(&mut mock_env, "uatom", market_params); - params.init_params(&mut mock_env, "uatom", asset_params); + params.init_params(&mut mock_env, asset_params); let (market_params, asset_params) = - default_asset_params_with(osmo_max_ltv, osmo_liq_threshold, osmo_liq_bonus); + default_asset_params_with("uosmo", osmo_max_ltv, osmo_liq_threshold, liq_bonus); red_bank.init_asset(&mut mock_env, "uosmo", market_params); - params.init_params(&mut mock_env, "uusdc", asset_params); + params.init_params(&mut mock_env, asset_params); // initial deposit amount let funded_atom = 1u128; // 1 uatom @@ -368,11 +375,12 @@ fn interest_rates_accured_based_on_internally_tracked_balances() { oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::one()); let red_bank = mock_env.red_bank.clone(); let params = mock_env.params.clone(); - let (market_params, asset_params) = default_asset_params(); - red_bank.init_asset(&mut mock_env, "uatom", market_params.clone()); + let (market_params, asset_params) = default_asset_params("uatom"); + red_bank.init_asset(&mut mock_env, "uatom", market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = default_asset_params("uusdc"); red_bank.init_asset(&mut mock_env, "uusdc", market_params); - params.init_params(&mut mock_env, "uatom", asset_params.clone()); - params.init_params(&mut mock_env, "uusdc", asset_params); + params.init_params(&mut mock_env, asset_params); // fund user_1 account with usdc let user_1 = Addr::unchecked("user_1"); diff --git a/packages/health/src/health.rs b/packages/health/src/health.rs index 0db936818..f48e1e508 100644 --- a/packages/health/src/health.rs +++ b/packages/health/src/health.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, fmt}; use cosmwasm_std::{Addr, Coin, Decimal, Fraction, QuerierWrapper, StdResult, Uint128}; -use mars_params::types::AssetParams; +use mars_params::types::asset::AssetParams; use crate::{error::HealthError, query::MarsQuerier}; diff --git a/packages/health/src/query.rs b/packages/health/src/query.rs index 83697ea54..da649ead8 100644 --- a/packages/health/src/query.rs +++ b/packages/health/src/query.rs @@ -1,5 +1,5 @@ use cosmwasm_std::{Addr, Decimal, QuerierWrapper, StdResult}; -use mars_params::types::AssetParams; +use mars_params::types::asset::AssetParams; use mars_red_bank_types::oracle::{self, ActionKind, PriceResponse}; pub struct MarsQuerier<'a> { diff --git a/packages/health/tests/test_from_coins_to_positions.rs b/packages/health/tests/test_from_coins_to_positions.rs index 10a525bae..48508bac2 100644 --- a/packages/health/tests/test_from_coins_to_positions.rs +++ b/packages/health/tests/test_from_coins_to_positions.rs @@ -7,7 +7,7 @@ use mars_health::{ health::{Health, Position}, query::MarsQuerier, }; -use mars_params::types::{AssetParams, HighLeverageStrategyParams, RedBankSettings, RoverSettings}; +use mars_params::types::asset::{AssetParams, CmSettings, LiquidationBonus, RedBankSettings}; use mars_red_bank_types::red_bank::Market; use mars_testing::MarsMockQuerier; @@ -128,12 +128,10 @@ fn mock_setup() -> MarsMockQuerier { mock_querier.set_redbank_params( "osmo", AssetParams { - rover: RoverSettings { + denom: "osmo".to_string(), + credit_manager: CmSettings { whitelisted: false, - hls: HighLeverageStrategyParams { - max_loan_to_value: Decimal::percent(90), - liquidation_threshold: Decimal::one(), - }, + hls: None, }, red_bank: RedBankSettings { deposit_enabled: false, @@ -142,7 +140,13 @@ fn mock_setup() -> MarsMockQuerier { }, max_loan_to_value: Decimal::from_atomics(50u128, 2).unwrap(), liquidation_threshold: Decimal::from_atomics(55u128, 2).unwrap(), - liquidation_bonus: Default::default(), + liquidation_bonus: LiquidationBonus { + starting_lb: Decimal::percent(0u64), + slope: Decimal::one(), + min_lb: Decimal::percent(0u64), + max_lb: Decimal::percent(5u64), + }, + protocol_liquidation_fee: Decimal::zero(), }, ); let atom_market = Market { @@ -153,12 +157,10 @@ fn mock_setup() -> MarsMockQuerier { mock_querier.set_redbank_params( "atom", AssetParams { - rover: RoverSettings { + denom: "atom".to_string(), + credit_manager: CmSettings { whitelisted: false, - hls: HighLeverageStrategyParams { - max_loan_to_value: Decimal::percent(90), - liquidation_threshold: Decimal::one(), - }, + hls: None, }, red_bank: RedBankSettings { deposit_enabled: false, @@ -167,7 +169,13 @@ fn mock_setup() -> MarsMockQuerier { }, max_loan_to_value: Decimal::from_atomics(70u128, 2).unwrap(), liquidation_threshold: Decimal::from_atomics(75u128, 2).unwrap(), - liquidation_bonus: Default::default(), + liquidation_bonus: LiquidationBonus { + starting_lb: Decimal::percent(0u64), + slope: Decimal::one(), + min_lb: Decimal::percent(0u64), + max_lb: Decimal::percent(5u64), + }, + protocol_liquidation_fee: Decimal::zero(), }, ); diff --git a/packages/health/tests/test_health_from_coins.rs b/packages/health/tests/test_health_from_coins.rs index 9fe6a9691..20da4adb5 100644 --- a/packages/health/tests/test_health_from_coins.rs +++ b/packages/health/tests/test_health_from_coins.rs @@ -5,7 +5,7 @@ use cosmwasm_std::{ Uint128, }; use mars_health::{error::HealthError, health::Health}; -use mars_params::types::{AssetParams, HighLeverageStrategyParams, RedBankSettings, RoverSettings}; +use mars_params::types::asset::{AssetParams, CmSettings, LiquidationBonus, RedBankSettings}; use mars_red_bank_types::red_bank::Market; use mars_testing::MarsMockQuerier; @@ -22,12 +22,10 @@ fn health_success_from_coins() { mock_querier.set_redbank_params( "osmo", AssetParams { - rover: RoverSettings { + denom: "osmo".to_string(), + credit_manager: CmSettings { whitelisted: false, - hls: HighLeverageStrategyParams { - max_loan_to_value: Decimal::percent(90), - liquidation_threshold: Decimal::one(), - }, + hls: None, }, red_bank: RedBankSettings { deposit_enabled: true, @@ -36,7 +34,13 @@ fn health_success_from_coins() { }, max_loan_to_value: Decimal::from_atomics(50u128, 2).unwrap(), liquidation_threshold: Decimal::from_atomics(55u128, 2).unwrap(), - liquidation_bonus: Default::default(), + liquidation_bonus: LiquidationBonus { + starting_lb: Decimal::percent(0u64), + slope: Decimal::one(), + min_lb: Decimal::percent(0u64), + max_lb: Decimal::percent(5u64), + }, + protocol_liquidation_fee: Decimal::zero(), }, ); let atom_market = Market { @@ -47,12 +51,10 @@ fn health_success_from_coins() { mock_querier.set_redbank_params( "atom", AssetParams { - rover: RoverSettings { + denom: "atom".to_string(), + credit_manager: CmSettings { whitelisted: false, - hls: HighLeverageStrategyParams { - max_loan_to_value: Decimal::percent(90), - liquidation_threshold: Decimal::one(), - }, + hls: None, }, red_bank: RedBankSettings { deposit_enabled: true, @@ -61,7 +63,13 @@ fn health_success_from_coins() { }, max_loan_to_value: Decimal::from_atomics(70u128, 2).unwrap(), liquidation_threshold: Decimal::from_atomics(75u128, 2).unwrap(), - liquidation_bonus: Default::default(), + liquidation_bonus: LiquidationBonus { + starting_lb: Decimal::percent(0u64), + slope: Decimal::one(), + min_lb: Decimal::percent(0u64), + max_lb: Decimal::percent(5u64), + }, + protocol_liquidation_fee: Decimal::zero(), }, ); @@ -111,12 +119,10 @@ fn health_error_from_coins() { mock_querier.set_redbank_params( "osmo", AssetParams { - rover: RoverSettings { + denom: "osmo".to_string(), + credit_manager: CmSettings { whitelisted: false, - hls: HighLeverageStrategyParams { - max_loan_to_value: Decimal::percent(90), - liquidation_threshold: Decimal::one(), - }, + hls: None, }, red_bank: RedBankSettings { deposit_enabled: false, @@ -125,7 +131,13 @@ fn health_error_from_coins() { }, max_loan_to_value: Decimal::from_atomics(50u128, 2).unwrap(), liquidation_threshold: Decimal::from_atomics(55u128, 2).unwrap(), - liquidation_bonus: Default::default(), + liquidation_bonus: LiquidationBonus { + starting_lb: Decimal::percent(0u64), + slope: Decimal::one(), + min_lb: Decimal::percent(0u64), + max_lb: Decimal::percent(5u64), + }, + protocol_liquidation_fee: Decimal::zero(), }, ); diff --git a/packages/liquidation/Cargo.toml b/packages/liquidation/Cargo.toml new file mode 100644 index 000000000..6d3015bbe --- /dev/null +++ b/packages/liquidation/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "mars-liquidation" +description = "Helper functions to calculate liquidation amounts" +version = "1.0.0" +authors = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +keywords = { workspace = true } + +[lib] +doctest = false + +[features] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] + +[dependencies] +cosmwasm-std = { workspace = true } +mars-health = { workspace = true } +mars-params = { workspace = true } +thiserror = { workspace = true } diff --git a/packages/liquidation/README.md b/packages/liquidation/README.md new file mode 100644 index 000000000..37dc7216f --- /dev/null +++ b/packages/liquidation/README.md @@ -0,0 +1,7 @@ +# Mars Health + +Functions used for evaluating the liquidation of user positions at Mars Protocol. + +## License + +Contents of this crate are open source under [GNU General Public License v3](../../LICENSE) or later. diff --git a/packages/liquidation/src/error.rs b/packages/liquidation/src/error.rs new file mode 100644 index 000000000..0d1988d29 --- /dev/null +++ b/packages/liquidation/src/error.rs @@ -0,0 +1,23 @@ +use cosmwasm_std::{ + CheckedFromRatioError, CheckedMultiplyFractionError, CheckedMultiplyRatioError, OverflowError, + StdError, +}; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum LiquidationError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + Overflow(#[from] OverflowError), + + #[error("{0}")] + CheckedMultiplyRatio(#[from] CheckedMultiplyRatioError), + + #[error("{0}")] + CheckedMultiplyFraction(#[from] CheckedMultiplyFractionError), + + #[error("{0}")] + CheckedFromRatio(#[from] CheckedFromRatioError), +} diff --git a/packages/liquidation/src/lib.rs b/packages/liquidation/src/lib.rs new file mode 100644 index 000000000..26187e200 --- /dev/null +++ b/packages/liquidation/src/lib.rs @@ -0,0 +1,2 @@ +pub mod error; +pub mod liquidation; diff --git a/packages/liquidation/src/liquidation.rs b/packages/liquidation/src/liquidation.rs new file mode 100644 index 000000000..0a04a4cf6 --- /dev/null +++ b/packages/liquidation/src/liquidation.rs @@ -0,0 +1,156 @@ +use std::{ + cmp::{max, min}, + ops::Add, +}; + +use cosmwasm_std::{Decimal, StdError, Uint128}; +use mars_health::health::Health; +use mars_params::types::asset::AssetParams; + +use crate::error::LiquidationError; + +/// Within this new system, the close factor (CF) will be determined dynamically using a parameter +/// known as the Target Health Factor (THF). The THF determines the ideal HF a position should be left +/// at immediately after the position has been liquidated. The CF, in turn, is a result of this parameter: +/// the maximum amount of debt that can be repaid to take the position to the THF. +/// For example, if the THF is 1.10 and a position gets liquidated at HF = 0.98, then the maximum +/// amount of debt a liquidator can repay (in other words, the CF) will be an amount such that the HF +/// after the liquidation is at maximum 1.10. +/// +/// The formula to calculate the maximum debt that can be repaid by a liquidator is as follows: +/// MDR_value = (THF * total_debt_value - liq_th_collateral_value) / (THF - (requested_collateral_liq_th * (1 + LB))) +/// where: +/// MDR - Maximum Debt Repayable +/// THF - Target Health Factor +/// total_debt_value - Value of debt before the liquidation happens +/// liq_th_collateral_value - Value of collateral before the liquidation happens adjusted to liquidation threshold +/// requested_collateral_liq_th - Liquidation threshold of requested collateral +/// LB - Liquidation Bonus +/// +/// PLF (Protocol Liqudiation Fee) is charged as a % of the LB. +/// For example, if we define the PLF as 10%, then the PLF would be deducted from the LB, so upon a liquidation: +/// - The liquidator receives 90% of the LB. +/// - The remaining 10% is sent to the protocol as PLF. +#[allow(clippy::too_many_arguments)] +pub fn calculate_liquidation_amounts( + collateral_amount: Uint128, + collateral_price: Decimal, + collateral_params: &AssetParams, + debt_amount: Uint128, + debt_requested_to_repay: Uint128, + debt_price: Decimal, + target_health_factor: Decimal, + health: &Health, +) -> Result<(Uint128, Uint128, Uint128), LiquidationError> { + // if health.liquidatable == true, save to unwrap + let liquidation_health_factor = health.liquidation_health_factor.unwrap(); + + let user_collateral_value = collateral_amount.checked_mul_floor(collateral_price)?; + + let liquidation_bonus = calculate_liquidation_bonus( + liquidation_health_factor, + health.total_collateral_value, + health.total_debt_value, + collateral_params, + )?; + + let max_debt_repayable_numerator = (target_health_factor * health.total_debt_value) + - health.liquidation_threshold_adjusted_collateral; + let max_debt_repayable_denominator = target_health_factor + - (collateral_params.liquidation_threshold * (Decimal::one() + liquidation_bonus)); + + let max_debt_repayable_value = + max_debt_repayable_numerator.checked_div_floor(max_debt_repayable_denominator)?; + + let max_debt_repayable_amount = max_debt_repayable_value.checked_div_floor(debt_price)?; + + // calculate possible debt to repay based on available collateral + let debt_amount_possible_to_repay = user_collateral_value + .checked_div_floor(Decimal::one().add(liquidation_bonus))? + .checked_div_floor(debt_price)?; + + let debt_amount_to_repay = *[ + debt_amount, + debt_requested_to_repay, + max_debt_repayable_amount, + debt_amount_possible_to_repay, + ] + .iter() + .min() + .ok_or_else(|| StdError::generic_err("Minimum not found"))?; + + let collateral_amount_to_liquidate = debt_amount_to_repay + .checked_mul_floor(debt_price)? + .checked_mul_floor(liquidation_bonus.add(Decimal::one()))? + .checked_div_floor(collateral_price)?; + + // In some edges scenarios: + // - if debt_amount_to_repay = 0, some liquidators could drain collaterals and all their coins + // would be refunded, i.e.: without spending coins. + // - if collateral_amount_to_liquidate is 0, some users could liquidate without receiving collaterals + // in return. + if (!collateral_amount_to_liquidate.is_zero() && debt_amount_to_repay.is_zero()) + || (collateral_amount_to_liquidate.is_zero() && !debt_amount_to_repay.is_zero()) + { + return Err(LiquidationError::Std(StdError::generic_err( + format!("Can't process liquidation. Invalid collateral_amount_to_liquidate ({collateral_amount_to_liquidate}) and debt_amount_to_repay ({debt_amount_to_repay})") + ))); + } + + let lb_amount = collateral_amount_to_liquidate.checked_mul_floor(liquidation_bonus)?; + + // Use ceiling in favour of protocol + let protocol_fee_amount = + lb_amount.checked_mul_ceil(collateral_params.protocol_liquidation_fee)?; + + let collateral_amount_received_by_liquidator = + collateral_amount_to_liquidate - protocol_fee_amount; + + Ok(( + debt_amount_to_repay, + collateral_amount_to_liquidate, + collateral_amount_received_by_liquidator, + )) +} + +/// The LB will depend on the Health Factor and a couple other parameters as follows: +/// Liquidation Bonus = min( +/// starting_lb + (slope * (1 - HF)), +/// max( +/// min(CR - 1, max_lb), +/// min_lb +/// ) +/// ) +/// `CR` is the Collateralization Ratio of the position calculated as `CR = Total Assets / Total Debt`. +fn calculate_liquidation_bonus( + liquidation_health_factor: Decimal, + total_collateral_value: Uint128, + total_debt_value: Uint128, + collateral_params: &AssetParams, +) -> Result { + let collateralization_ratio = + Decimal::checked_from_ratio(total_collateral_value, total_debt_value)?; + + // (CR - 1) can't be negative + let collateralization_ratio_adjusted = if collateralization_ratio > Decimal::one() { + collateralization_ratio - Decimal::one() + } else { + Decimal::zero() + }; + + let max_lb_adjusted = max( + min(collateralization_ratio_adjusted, collateral_params.liquidation_bonus.max_lb), + collateral_params.liquidation_bonus.min_lb, + ); + + let calculated_bonus = collateral_params.liquidation_bonus.starting_lb.checked_add( + collateral_params + .liquidation_bonus + .slope + .checked_mul(Decimal::one() - liquidation_health_factor)?, + )?; + + let liquidation_bonus = min(calculated_bonus, max_lb_adjusted); + + Ok(liquidation_bonus) +} diff --git a/packages/testing/src/integration/mock_env.rs b/packages/testing/src/integration/mock_env.rs index 5d1723ebc..42a5de50b 100644 --- a/packages/testing/src/integration/mock_env.rs +++ b/packages/testing/src/integration/mock_env.rs @@ -1,15 +1,16 @@ #![allow(dead_code)] -use std::mem::take; +use std::{collections::HashMap, mem::take, str::FromStr}; use anyhow::Result as AnyResult; use cosmwasm_std::{coin, Addr, Coin, Decimal, Empty, StdResult, Uint128}; use cw_multi_test::{App, AppResponse, BankSudo, BasicApp, Executor, SudoMsg}; use mars_oracle_osmosis::OsmosisPriceSourceUnchecked; -use mars_params::types::{AssetParams, AssetParamsUpdate}; +use mars_params::{msg::AssetParamsUpdate, types::asset::AssetParams}; use mars_red_bank_types::{ address_provider::{self, MarsAddressType}, - incentives, oracle, + incentives, + oracle::{self, PriceResponse}, red_bank::{ self, CreateOrUpdateConfig, InitOrUpdateAssetParams, Market, UncollateralizedLoanLimitResponse, UserCollateralResponse, UserDebtResponse, @@ -81,6 +82,13 @@ impl MockEnv { }) } + pub fn fund_accounts(&mut self, addrs: &[&Addr], amount: u128, denoms: &[&str]) { + for addr in addrs { + let coins: Vec<_> = denoms.iter().map(|&d| coin(amount, d)).collect(); + self.fund_account(addr, &coins); + } + } + pub fn fund_account(&mut self, addr: &Addr, coins: &[Coin]) { self.app .sudo(SudoMsg::Bank(BankSudo::Mint { @@ -93,6 +101,11 @@ impl MockEnv { pub fn query_balance(&self, addr: &Addr, denom: &str) -> StdResult { self.app.wrap().query_balance(addr, denom) } + + pub fn query_all_balances(&self, addr: &Addr) -> HashMap { + let res: Vec = self.app.wrap().query_all_balances(addr).unwrap(); + res.into_iter().map(|r| (r.denom, r.amount)).collect() + } } impl Incentives { @@ -207,6 +220,19 @@ impl Oracle { ) .unwrap(); } + + pub fn query_price(&self, env: &mut MockEnv, denom: &str) -> PriceResponse { + env.app + .wrap() + .query_wasm_smart( + self.contract_addr.clone(), + &oracle::QueryMsg::Price { + denom: denom.to_string(), + kind: None, + }, + ) + .unwrap() + } } impl RedBank { @@ -224,6 +250,26 @@ impl RedBank { .unwrap(); } + pub fn update_user_collateral_status( + &self, + env: &mut MockEnv, + sender: &Addr, + denom: &str, + enabled: bool, + ) { + env.app + .execute_contract( + sender.clone(), + self.contract_addr.clone(), + &red_bank::ExecuteMsg::UpdateAssetCollateralStatus { + denom: denom.to_string(), + enable: enabled, + }, + &[], + ) + .unwrap(); + } + pub fn deposit(&self, env: &mut MockEnv, sender: &Addr, coin: Coin) -> AnyResult { env.app.execute_contract( sender.clone(), @@ -288,7 +334,26 @@ impl RedBank { liquidator: &Addr, user: &Addr, collateral_denom: &str, - coin: Coin, + send_funds: &[Coin], + ) -> AnyResult { + self.liquidate_with_different_recipient( + env, + liquidator, + user, + collateral_denom, + send_funds, + None, + ) + } + + pub fn liquidate_with_different_recipient( + &self, + env: &mut MockEnv, + liquidator: &Addr, + user: &Addr, + collateral_denom: &str, + send_funds: &[Coin], + recipient: Option, ) -> AnyResult { env.app.execute_contract( liquidator.clone(), @@ -296,9 +361,9 @@ impl RedBank { &red_bank::ExecuteMsg::Liquidate { user: user.to_string(), collateral_denom: collateral_denom.to_string(), - recipient: None, + recipient, }, - &[coin], + send_funds, ) } @@ -334,6 +399,21 @@ impl RedBank { .unwrap() } + pub fn query_markets(&self, env: &mut MockEnv) -> HashMap { + let res: Vec = env + .app + .wrap() + .query_wasm_smart( + self.contract_addr.clone(), + &red_bank::QueryMsg::Markets { + start_after: None, + limit: Some(100), + }, + ) + .unwrap(); + res.into_iter().map(|r| (r.denom.clone(), r)).collect() + } + pub fn query_user_debt(&self, env: &mut MockEnv, user: &Addr, denom: &str) -> UserDebtResponse { env.app .wrap() @@ -347,6 +427,26 @@ impl RedBank { .unwrap() } + pub fn query_user_debts( + &self, + env: &mut MockEnv, + user: &Addr, + ) -> HashMap { + let res: Vec = env + .app + .wrap() + .query_wasm_smart( + self.contract_addr.clone(), + &red_bank::QueryMsg::UserDebts { + user: user.to_string(), + start_after: None, + limit: Some(100), + }, + ) + .unwrap(); + res.into_iter().map(|r| (r.denom.clone(), r)).collect() + } + pub fn query_user_collateral( &self, env: &mut MockEnv, @@ -365,6 +465,26 @@ impl RedBank { .unwrap() } + pub fn query_user_collaterals( + &self, + env: &mut MockEnv, + user: &Addr, + ) -> HashMap { + let res: Vec = env + .app + .wrap() + .query_wasm_smart( + self.contract_addr.clone(), + &red_bank::QueryMsg::UserCollaterals { + user: user.to_string(), + start_after: None, + limit: Some(100), + }, + ) + .unwrap(); + res.into_iter().map(|r| (r.denom.clone(), r)).collect() + } + pub fn query_user_position(&self, env: &mut MockEnv, user: &Addr) -> UserPositionResponse { env.app .wrap() @@ -452,19 +572,30 @@ impl RewardsCollector { } impl Params { - pub fn init_params(&self, env: &mut MockEnv, denom: &str, params: AssetParams) { + pub fn init_params(&self, env: &mut MockEnv, params: AssetParams) { env.app .execute_contract( env.owner.clone(), self.contract_addr.clone(), &mars_params::msg::ExecuteMsg::UpdateAssetParams(AssetParamsUpdate::AddOrUpdate { - denom: denom.to_string(), - params, + params: params.into(), }), &[], ) .unwrap(); } + + pub fn query_params(&self, env: &mut MockEnv, denom: &str) -> AssetParams { + env.app + .wrap() + .query_wasm_smart( + self.contract_addr.clone(), + &mars_params::msg::QueryMsg::AssetParams { + denom: denom.to_string(), + }, + ) + .unwrap() + } } pub struct MockEnvBuilder { @@ -476,7 +607,7 @@ pub struct MockEnvBuilder { mars_denom: String, base_denom: String, base_denom_decimals: u8, - close_factor: Decimal, + target_health_factor: Decimal, // rewards-collector params safety_tax_rate: Decimal, @@ -497,7 +628,7 @@ impl MockEnvBuilder { mars_denom: "umars".to_string(), base_denom: "uosmo".to_string(), base_denom_decimals: 6u8, - close_factor: Decimal::percent(80), + target_health_factor: Decimal::from_str("1.05").unwrap(), safety_tax_rate: Decimal::percent(50), safety_fund_denom: "uusdc".to_string(), fee_collector_denom: "uusdc".to_string(), @@ -522,8 +653,8 @@ impl MockEnvBuilder { self } - pub fn close_factor(&mut self, percentage: Decimal) -> &mut Self { - self.close_factor = percentage; + pub fn target_health_factor(&mut self, thf: Decimal) -> &mut Self { + self.target_health_factor = thf; self } @@ -712,7 +843,7 @@ impl MockEnvBuilder { self.owner.clone(), &mars_params::msg::InstantiateMsg { owner: self.owner.to_string(), - max_close_factor: self.close_factor, + target_health_factor: self.target_health_factor, }, &[], "params", diff --git a/packages/testing/src/mars_mock_querier.rs b/packages/testing/src/mars_mock_querier.rs index d5f37ba98..ea89017ea 100644 --- a/packages/testing/src/mars_mock_querier.rs +++ b/packages/testing/src/mars_mock_querier.rs @@ -10,7 +10,7 @@ use mars_oracle_osmosis::{ DowntimeDetector, }; use mars_osmosis::helpers::QueryPoolResponse; -use mars_params::types::AssetParams; +use mars_params::types::asset::AssetParams; use mars_red_bank_types::{address_provider, incentives, oracle, red_bank}; use osmosis_std::types::osmosis::{ downtimedetector::v1beta1::RecoveredSinceDowntimeOfLengthResponse, @@ -198,8 +198,8 @@ impl MarsMockQuerier { self.params_querier.params.insert(denom.to_string(), params); } - pub fn set_close_factor(&mut self, close_factor: Decimal) { - self.params_querier.close_factor = close_factor; + pub fn set_target_health_factor(&mut self, thf: Decimal) { + self.params_querier.target_health_factor = thf; } pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { diff --git a/packages/testing/src/params_querier.rs b/packages/testing/src/params_querier.rs index 5840e3922..21f18e09e 100644 --- a/packages/testing/src/params_querier.rs +++ b/packages/testing/src/params_querier.rs @@ -1,18 +1,18 @@ use std::collections::HashMap; use cosmwasm_std::{to_binary, Binary, ContractResult, Decimal, QuerierResult}; -use mars_params::{msg::QueryMsg, types::AssetParams}; +use mars_params::{msg::QueryMsg, types::asset::AssetParams}; #[derive(Default)] pub struct ParamsQuerier { - pub close_factor: Decimal, + pub target_health_factor: Decimal, pub params: HashMap, } impl ParamsQuerier { pub fn handle_query(&self, query: QueryMsg) -> QuerierResult { let ret: ContractResult = match query { - QueryMsg::MaxCloseFactor {} => to_binary(&self.close_factor).into(), + QueryMsg::TargetHealthFactor {} => to_binary(&self.target_health_factor).into(), QueryMsg::AssetParams { denom, } => match self.params.get(&denom) { diff --git a/scripts/deploy/neutron/config.ts b/scripts/deploy/neutron/config.ts index d1a208f64..238045d93 100644 --- a/scripts/deploy/neutron/config.ts +++ b/scripts/deploy/neutron/config.ts @@ -214,7 +214,6 @@ export const neutronTestnetConfig = { assets: [ntrnAsset, atomAsset], vaults: [], oracleConfigs: [axlUSDCOracleTestnet, ntrnOracleTestnet, atomOracleTestnet], - targetHealthFactor: '1.2', oracleCustomInitParams: { astroport_factory: 'neutron1jj0scx400pswhpjes589aujlqagxgcztw04srynmhf0f6zplzn2qqmhwj7', }, diff --git a/scripts/deploy/osmosis/config.ts b/scripts/deploy/osmosis/config.ts index b32c53a59..bad61f140 100644 --- a/scripts/deploy/osmosis/config.ts +++ b/scripts/deploy/osmosis/config.ts @@ -26,109 +26,175 @@ const marsOsmoPool = 907 export const osmoAsset: AssetConfig = { denom: 'uosmo', max_loan_to_value: '0.59', - reserve_factor: '0.2', liquidation_threshold: '0.61', - liquidation_bonus: '0.15', - interest_rate_model: { - optimal_utilization_rate: '0.6', - base: '0', - slope_1: '0.15', - slope_2: '3', + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', }, - deposit_cap: '2500000000000', - deposit_enabled: true, - borrow_enabled: true, + protocol_liquidation_fee: '0.5', + // reserve_factor: '0.2', + // interest_rate_model: { + // optimal_utilization_rate: '0.6', + // base: '0', + // slope_1: '0.15', + // slope_2: '3', + // }, symbol: 'OSMO', + credit_manager: { + whitelisted: true, + }, + red_bank: { + borrow_enabled: true, + deposit_cap: '2500000000000', + deposit_enabled: true, + }, } export const atomAsset: AssetConfig = { denom: atom, max_loan_to_value: '0.68', - reserve_factor: '0.2', liquidation_threshold: '0.7', - liquidation_bonus: '0.15', - interest_rate_model: { - optimal_utilization_rate: '0.6', - base: '0', - slope_1: '0.15', - slope_2: '3', + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', }, - deposit_cap: '100000000000', - deposit_enabled: true, - borrow_enabled: true, + protocol_liquidation_fee: '0.5', + // reserve_factor: '0.2', + // interest_rate_model: { + // optimal_utilization_rate: '0.6', + // base: '0', + // slope_1: '0.15', + // slope_2: '3', + // }, symbol: 'ATOM', + credit_manager: { + whitelisted: true, + }, + red_bank: { + borrow_enabled: true, + deposit_cap: '100000000000', + deposit_enabled: true, + }, } export const atomAssetTest: AssetConfig = { denom: atomTest, max_loan_to_value: '0.68', - reserve_factor: '0.2', liquidation_threshold: '0.7', - liquidation_bonus: '0.15', - interest_rate_model: { - optimal_utilization_rate: '0.6', - base: '0', - slope_1: '0.15', - slope_2: '3', + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', }, - deposit_cap: '100000000000', - deposit_enabled: true, - borrow_enabled: true, + protocol_liquidation_fee: '0.5', + // reserve_factor: '0.2', + // interest_rate_model: { + // optimal_utilization_rate: '0.6', + // base: '0', + // slope_1: '0.15', + // slope_2: '3', + // }, symbol: 'ATOM', + credit_manager: { + whitelisted: true, + }, + red_bank: { + borrow_enabled: true, + deposit_cap: '100000000000', + deposit_enabled: true, + }, } export const axlUSDCAsset: AssetConfig = { denom: axlUSDC, max_loan_to_value: '0.74', - reserve_factor: '0.2', liquidation_threshold: '0.75', - liquidation_bonus: '0.1', - interest_rate_model: { - optimal_utilization_rate: '0.8', - base: '0', - slope_1: '0.2', - slope_2: '2', + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', }, - deposit_cap: '500000000000', - deposit_enabled: true, - borrow_enabled: true, + protocol_liquidation_fee: '0.5', + // reserve_factor: '0.2', + // interest_rate_model: { + // optimal_utilization_rate: '0.8', + // base: '0', + // slope_1: '0.2', + // slope_2: '2', + // }, symbol: 'axlUSDC', + credit_manager: { + whitelisted: true, + }, + red_bank: { + borrow_enabled: true, + deposit_cap: '500000000000', + deposit_enabled: true, + }, } export const axlUSDCAssetTest: AssetConfig = { denom: usdcTest, max_loan_to_value: '0.74', - reserve_factor: '0.2', liquidation_threshold: '0.75', - liquidation_bonus: '0.1', - interest_rate_model: { - optimal_utilization_rate: '0.8', - base: '0', - slope_1: '0.2', - slope_2: '2', + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', }, - deposit_cap: '500000000000', - deposit_enabled: true, - borrow_enabled: true, + protocol_liquidation_fee: '0.5', + // reserve_factor: '0.2', + // interest_rate_model: { + // optimal_utilization_rate: '0.8', + // base: '0', + // slope_1: '0.2', + // slope_2: '2', + // }, symbol: 'axlUSDC', + credit_manager: { + whitelisted: true, + }, + red_bank: { + borrow_enabled: true, + deposit_cap: '500000000000', + deposit_enabled: true, + }, } export const marsAssetTest: AssetConfig = { denom: marsTest, max_loan_to_value: '0.74', - reserve_factor: '0.2', liquidation_threshold: '0.75', - liquidation_bonus: '0.1', - interest_rate_model: { - optimal_utilization_rate: '0.8', - base: '0', - slope_1: '0.2', - slope_2: '2', + liquidation_bonus: { + max_lb: '0.05', + min_lb: '0', + slope: '2', + starting_lb: '0', }, - deposit_cap: '500000000000', - deposit_enabled: true, - borrow_enabled: true, + protocol_liquidation_fee: '0.5', + // reserve_factor: '0.2', + // interest_rate_model: { + // optimal_utilization_rate: '0.8', + // base: '0', + // slope_1: '0.2', + // slope_2: '2', + // }, symbol: 'mars', + credit_manager: { + whitelisted: true, + }, + red_bank: { + borrow_enabled: true, + deposit_cap: '500000000000', + deposit_enabled: true, + }, } // export const osmoOracle: OracleConfig = { @@ -204,7 +270,6 @@ export const osmosisTestnetConfig: DeploymentConfig = { 'elevator august inherit simple buddy giggle zone despair marine rich swim danger blur people hundred faint ladder wet toe strong blade utility trial process', slippage_tolerance: '0.01', base_asset_symbol: 'OSMO', - second_asset_symbol: 'ATOM', runTests: false, mainnet: false, feeCollectorDenom: marsTest, @@ -217,8 +282,8 @@ export const osmosisTestnetConfig: DeploymentConfig = { feeCollectorAddr: feeCollectorAddr, swapperDexName: 'osmosis', assets: [osmoAsset, atomAsset, axlUSDCAsset], + vaults: [], oracleConfigs: [atomOracle, axlUSDCOracle], - targetHealthFactor: '1.2', incentiveEpochDuration: 86400, maxWhitelistedIncentiveDenoms: 10, targetHealthFactor: '1.2', @@ -241,7 +306,6 @@ export const osmosisTestMultisig: DeploymentConfig = { 'elevator august inherit simple buddy giggle zone despair marine rich swim danger blur people hundred faint ladder wet toe strong blade utility trial process', slippage_tolerance: '0.01', base_asset_symbol: 'OSMO', - second_asset_symbol: 'ATOM', multisigAddr: 'osmo14w4x949nwcrqgfe53pxs3k7x53p0gvlrq34l5n', runTests: false, mainnet: false, @@ -255,8 +319,8 @@ export const osmosisTestMultisig: DeploymentConfig = { feeCollectorAddr: feeCollectorAddr, swapperDexName: 'osmosis', assets: [osmoAsset, atomAsset, axlUSDCAsset], + vaults: [], oracleConfigs: [atomOracle, axlUSDCOracle], - targetHealthFactor: '1.2', incentiveEpochDuration: 86400, maxWhitelistedIncentiveDenoms: 10, targetHealthFactor: '1.2', @@ -277,7 +341,6 @@ export const osmosisMainnet: DeploymentConfig = { deployerMnemonic: 'TO BE INSERTED AT TIME OF DEPLOYMENT', slippage_tolerance: '0.01', base_asset_symbol: 'OSMO', - second_asset_symbol: 'ATOM', multisigAddr: 'osmo14w4x949nwcrqgfe53pxs3k7x53p0gvlrq34l5n', runTests: false, mainnet: true, @@ -320,8 +383,8 @@ export const osmosisMainnet: DeploymentConfig = { feeCollectorAddr: feeCollectorAddr, swapperDexName: 'osmosis', assets: [osmoAsset, atomAsset, axlUSDCAsset], + vaults: [], oracleConfigs: [atomOracle, axlUSDCOracle], - targetHealthFactor: '1.2', incentiveEpochDuration: 86400, maxWhitelistedIncentiveDenoms: 10, targetHealthFactor: '1.2', @@ -343,7 +406,6 @@ export const osmosisLocalConfig: DeploymentConfig = { 'notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius', slippage_tolerance: '0.05', base_asset_symbol: 'OSMO', - second_asset_symbol: 'ATOM', runTests: false, mainnet: false, feeCollectorDenom: axlUSDC, @@ -356,8 +418,8 @@ export const osmosisLocalConfig: DeploymentConfig = { feeCollectorAddr: feeCollectorAddr, swapperDexName: 'osmosis', assets: [osmoAsset, atomAsset, axlUSDCAsset], + vaults: [], oracleConfigs: [atomOracle, axlUSDCOracle], - targetHealthFactor: '1.2', incentiveEpochDuration: 86400, maxWhitelistedIncentiveDenoms: 10, targetHealthFactor: '1.2', diff --git a/scripts/types/config.ts b/scripts/types/config.ts index 1e2b52563..e9a4ea44b 100644 --- a/scripts/types/config.ts +++ b/scripts/types/config.ts @@ -56,7 +56,6 @@ export interface DeploymentConfig { safetyFundAddr: string protocolAdminAddr: string feeCollectorAddr: string - targetHealthFactor: string swapperDexName: string assets: AssetConfig[] vaults: VaultConfig[] From cf28835abb717b00a2f30eef06db4ba32d3eeae2 Mon Sep 17 00:00:00 2001 From: piobab Date: Tue, 25 Jul 2023 20:21:24 +0200 Subject: [PATCH 10/43] Split execute to smaller files. (#271) --- contracts/red-bank/src/asset.rs | 144 ++++ contracts/red-bank/src/borrow.rs | 140 ++++ contracts/red-bank/src/collateral.rs | 68 ++ contracts/red-bank/src/config.rs | 42 + contracts/red-bank/src/contract.rs | 29 +- contracts/red-bank/src/deposit.rs | 94 +++ contracts/red-bank/src/execute.rs | 742 ------------------ contracts/red-bank/src/instantiate.rs | 46 ++ contracts/red-bank/src/lib.rs | 10 +- contracts/red-bank/src/repay.rs | 98 +++ .../red-bank/src/uncollateralized_loan.rs | 51 ++ contracts/red-bank/src/withdraw.rs | 139 ++++ 12 files changed, 848 insertions(+), 755 deletions(-) create mode 100644 contracts/red-bank/src/asset.rs create mode 100644 contracts/red-bank/src/borrow.rs create mode 100644 contracts/red-bank/src/collateral.rs create mode 100644 contracts/red-bank/src/config.rs create mode 100644 contracts/red-bank/src/deposit.rs delete mode 100644 contracts/red-bank/src/execute.rs create mode 100644 contracts/red-bank/src/instantiate.rs create mode 100644 contracts/red-bank/src/repay.rs create mode 100644 contracts/red-bank/src/uncollateralized_loan.rs create mode 100644 contracts/red-bank/src/withdraw.rs diff --git a/contracts/red-bank/src/asset.rs b/contracts/red-bank/src/asset.rs new file mode 100644 index 000000000..587c44011 --- /dev/null +++ b/contracts/red-bank/src/asset.rs @@ -0,0 +1,144 @@ +use cosmwasm_std::{Decimal, DepsMut, Env, MessageInfo, Response, Uint128}; +use mars_red_bank_types::{ + address_provider, + address_provider::MarsAddressType, + error::MarsError, + red_bank::{InitOrUpdateAssetParams, Market}, +}; +use mars_utils::helpers::validate_native_denom; + +use crate::{ + error::ContractError, + interest_rates::{apply_accumulated_interests, update_interest_rates}, + state::{CONFIG, MARKETS, OWNER}, +}; + +/// Initialize asset if not exist. +/// Initialization requires that all params are provided and there is no asset in state. +pub fn init_asset( + deps: DepsMut, + env: Env, + info: MessageInfo, + denom: String, + params: InitOrUpdateAssetParams, +) -> Result { + OWNER.assert_owner(deps.storage, &info.sender)?; + + validate_native_denom(&denom)?; + + if MARKETS.may_load(deps.storage, &denom)?.is_some() { + return Err(ContractError::AssetAlreadyInitialized {}); + } + + let new_market = create_market(env.block.time.seconds(), &denom, params)?; + MARKETS.save(deps.storage, &denom, &new_market)?; + + Ok(Response::new().add_attribute("action", "init_asset").add_attribute("denom", denom)) +} + +/// Initialize new market +pub fn create_market( + block_time: u64, + denom: &str, + params: InitOrUpdateAssetParams, +) -> Result { + // Destructuring a struct’s fields into separate variables in order to force + // compile error if we add more params + let InitOrUpdateAssetParams { + reserve_factor, + interest_rate_model, + } = params; + + // All fields should be available + let available = reserve_factor.is_some() && interest_rate_model.is_some(); + + if !available { + return Err(MarsError::InstantiateParamsUnavailable {}.into()); + } + + let new_market = Market { + denom: denom.to_string(), + borrow_index: Decimal::one(), + liquidity_index: Decimal::one(), + borrow_rate: Decimal::zero(), + liquidity_rate: Decimal::zero(), + reserve_factor: reserve_factor.unwrap(), + indexes_last_updated: block_time, + collateral_total_scaled: Uint128::zero(), + debt_total_scaled: Uint128::zero(), + interest_rate_model: interest_rate_model.unwrap(), + }; + + new_market.validate()?; + + Ok(new_market) +} + +/// Update asset with new params. +pub fn update_asset( + deps: DepsMut, + env: Env, + info: MessageInfo, + denom: String, + params: InitOrUpdateAssetParams, +) -> Result { + OWNER.assert_owner(deps.storage, &info.sender)?; + + let market_option = MARKETS.may_load(deps.storage, &denom)?; + match market_option { + None => Err(ContractError::AssetNotInitialized {}), + Some(mut market) => { + // Destructuring a struct’s fields into separate variables in order to force + // compile error if we add more params + let InitOrUpdateAssetParams { + reserve_factor, + interest_rate_model, + } = params; + + // If reserve factor or interest rates are updated we update indexes with + // current values before applying the change to prevent applying this + // new params to a period where they were not valid yet. Interests rates are + // recalculated after changes are applied. + let should_update_interest_rates = (reserve_factor.is_some() + && reserve_factor.unwrap() != market.reserve_factor) + || interest_rate_model.is_some(); + + let mut response = Response::new(); + + if should_update_interest_rates { + let config = CONFIG.load(deps.storage)?; + let addresses = address_provider::helpers::query_contract_addrs( + deps.as_ref(), + &config.address_provider, + vec![MarsAddressType::Incentives, MarsAddressType::RewardsCollector], + )?; + let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; + let incentives_addr = &addresses[&MarsAddressType::Incentives]; + + response = apply_accumulated_interests( + deps.storage, + &env, + &mut market, + rewards_collector_addr, + incentives_addr, + response, + )?; + } + + let mut updated_market = Market { + reserve_factor: reserve_factor.unwrap_or(market.reserve_factor), + interest_rate_model: interest_rate_model.unwrap_or(market.interest_rate_model), + ..market + }; + + updated_market.validate()?; + + if should_update_interest_rates { + response = update_interest_rates(&env, &mut updated_market, response)?; + } + MARKETS.save(deps.storage, &denom, &updated_market)?; + + Ok(response.add_attribute("action", "update_asset").add_attribute("denom", denom)) + } + } +} diff --git a/contracts/red-bank/src/borrow.rs b/contracts/red-bank/src/borrow.rs new file mode 100644 index 000000000..04c89cf29 --- /dev/null +++ b/contracts/red-bank/src/borrow.rs @@ -0,0 +1,140 @@ +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Uint128}; +use mars_red_bank_types::{address_provider, address_provider::MarsAddressType}; +use mars_utils::helpers::build_send_asset_msg; + +use crate::{ + error::ContractError, + health::assert_below_max_ltv_after_borrow, + helpers::query_asset_params, + interest_rates::{ + apply_accumulated_interests, get_scaled_debt_amount, get_underlying_debt_amount, + get_underlying_liquidity_amount, update_interest_rates, + }, + state::{CONFIG, MARKETS}, + user::User, +}; + +/// Add debt for the borrower and send the borrowed funds +pub fn borrow( + deps: DepsMut, + env: Env, + info: MessageInfo, + denom: String, + borrow_amount: Uint128, + recipient: Option, +) -> Result { + let borrower = User(&info.sender); + + let config = CONFIG.load(deps.storage)?; + + let addresses = address_provider::helpers::query_contract_addrs( + deps.as_ref(), + &config.address_provider, + vec![ + MarsAddressType::Oracle, + MarsAddressType::Incentives, + MarsAddressType::RewardsCollector, + MarsAddressType::Params, + ], + )?; + let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; + let incentives_addr = &addresses[&MarsAddressType::Incentives]; + let oracle_addr = &addresses[&MarsAddressType::Oracle]; + let params_addr = &addresses[&MarsAddressType::Params]; + + let asset_params = query_asset_params(&deps.querier, params_addr, &denom)?; + + if !asset_params.red_bank.borrow_enabled { + return Err(ContractError::BorrowNotEnabled { + denom, + }); + } + + // Load market and user state + let mut borrow_market = MARKETS.load(deps.storage, &denom)?; + + let collateral_balance_before = get_underlying_liquidity_amount( + borrow_market.collateral_total_scaled, + &borrow_market, + env.block.time.seconds(), + )?; + + // Cannot borrow zero amount or more than available collateral + if borrow_amount.is_zero() || borrow_amount > collateral_balance_before { + return Err(ContractError::InvalidBorrowAmount { + denom, + }); + } + + let uncollateralized_loan_limit = borrower.uncollateralized_loan_limit(deps.storage, &denom)?; + + // Check if user can borrow specified amount + let mut uncollateralized_debt = false; + if uncollateralized_loan_limit.is_zero() { + if !assert_below_max_ltv_after_borrow( + &deps.as_ref(), + &env, + borrower.address(), + oracle_addr, + params_addr, + &denom, + borrow_amount, + )? { + return Err(ContractError::BorrowAmountExceedsGivenCollateral {}); + } + } else { + // Uncollateralized loan: check borrow amount plus debt does not exceed uncollateralized loan limit + uncollateralized_debt = true; + + let debt_amount_scaled = borrower.debt_amount_scaled(deps.storage, &denom)?; + + let asset_market = MARKETS.load(deps.storage, &denom)?; + let debt_amount = get_underlying_debt_amount( + debt_amount_scaled, + &asset_market, + env.block.time.seconds(), + )?; + + let debt_after_borrow = debt_amount.checked_add(borrow_amount)?; + if debt_after_borrow > uncollateralized_loan_limit { + return Err(ContractError::BorrowAmountExceedsUncollateralizedLoanLimit {}); + } + } + + let mut response = Response::new(); + + response = apply_accumulated_interests( + deps.storage, + &env, + &mut borrow_market, + rewards_collector_addr, + incentives_addr, + response, + )?; + + // Set new debt + let borrow_amount_scaled = + get_scaled_debt_amount(borrow_amount, &borrow_market, env.block.time.seconds())?; + + borrow_market.increase_debt(borrow_amount_scaled)?; + borrower.increase_debt(deps.storage, &denom, borrow_amount_scaled, uncollateralized_debt)?; + + response = update_interest_rates(&env, &mut borrow_market, response)?; + MARKETS.save(deps.storage, &denom, &borrow_market)?; + + // Send borrow amount to borrower or another recipient + let recipient_addr = if let Some(recipient) = recipient { + deps.api.addr_validate(&recipient)? + } else { + borrower.address().clone() + }; + + Ok(response + .add_message(build_send_asset_msg(&recipient_addr, &denom, borrow_amount)) + .add_attribute("action", "borrow") + .add_attribute("sender", borrower) + .add_attribute("recipient", recipient_addr) + .add_attribute("denom", denom) + .add_attribute("amount", borrow_amount) + .add_attribute("amount_scaled", borrow_amount_scaled)) +} diff --git a/contracts/red-bank/src/collateral.rs b/contracts/red-bank/src/collateral.rs new file mode 100644 index 000000000..b9aa62dfe --- /dev/null +++ b/contracts/red-bank/src/collateral.rs @@ -0,0 +1,68 @@ +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; +use mars_red_bank_types::{ + self, + address_provider::{self, MarsAddressType}, +}; + +use crate::{ + error::ContractError, + health::get_health_and_positions, + state::{COLLATERALS, CONFIG}, + user::User, +}; + +/// Update (enable / disable) collateral asset for specific user +pub fn update_asset_collateral_status( + deps: DepsMut, + env: Env, + info: MessageInfo, + denom: String, + enable: bool, +) -> Result { + let user = User(&info.sender); + + let mut collateral = + COLLATERALS.may_load(deps.storage, (user.address(), &denom))?.ok_or_else(|| { + ContractError::UserNoCollateralBalance { + user: user.into(), + denom: denom.clone(), + } + })?; + + let previously_enabled = collateral.enabled; + + collateral.enabled = enable; + COLLATERALS.save(deps.storage, (user.address(), &denom), &collateral)?; + + // if the collateral was previously enabled, but is not disabled, it is necessary to ensure the + // user is not liquidatable after disabling + if previously_enabled && !enable { + let config = CONFIG.load(deps.storage)?; + + let addresses = address_provider::helpers::query_contract_addrs( + deps.as_ref(), + &config.address_provider, + vec![MarsAddressType::Oracle, MarsAddressType::Params], + )?; + let oracle_addr = &addresses[&MarsAddressType::Oracle]; + let params_addr = &addresses[&MarsAddressType::Params]; + + let (health, _) = get_health_and_positions( + &deps.as_ref(), + &env, + user.address(), + oracle_addr, + params_addr, + )?; + + if health.is_liquidatable() { + return Err(ContractError::InvalidHealthFactorAfterDisablingCollateral {}); + } + } + + Ok(Response::new() + .add_attribute("action", "update_asset_collateral_status") + .add_attribute("user", user) + .add_attribute("denom", denom) + .add_attribute("enable", enable.to_string())) +} diff --git a/contracts/red-bank/src/config.rs b/contracts/red-bank/src/config.rs new file mode 100644 index 000000000..eede3e1c2 --- /dev/null +++ b/contracts/red-bank/src/config.rs @@ -0,0 +1,42 @@ +use cosmwasm_std::{DepsMut, MessageInfo, Response}; +use mars_owner::OwnerUpdate; +use mars_red_bank_types::red_bank::CreateOrUpdateConfig; +use mars_utils::helpers::option_string_to_addr; + +use crate::{ + error::ContractError, + state::{CONFIG, OWNER}, +}; + +pub fn update_owner( + deps: DepsMut, + info: MessageInfo, + update: OwnerUpdate, +) -> Result { + Ok(OWNER.update(deps, info, update)?) +} + +/// Update config +pub fn update_config( + deps: DepsMut, + info: MessageInfo, + new_config: CreateOrUpdateConfig, +) -> Result { + OWNER.assert_owner(deps.storage, &info.sender)?; + + let mut config = CONFIG.load(deps.storage)?; + + // Destructuring a struct’s fields into separate variables in order to force + // compile error if we add more params + let CreateOrUpdateConfig { + address_provider, + } = new_config; + + // Update config + config.address_provider = + option_string_to_addr(deps.api, address_provider, config.address_provider)?; + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new().add_attribute("action", "update_config")) +} diff --git a/contracts/red-bank/src/contract.rs b/contracts/red-bank/src/contract.rs index 7017c542a..6d9154049 100644 --- a/contracts/red-bank/src/contract.rs +++ b/contracts/red-bank/src/contract.rs @@ -1,7 +1,10 @@ use cosmwasm_std::{entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response}; use mars_red_bank_types::red_bank::{ExecuteMsg, InstantiateMsg, QueryMsg}; -use crate::{error::ContractError, execute, liquidate, query}; +use crate::{ + asset, borrow, collateral, config, deposit, error::ContractError, instantiate, liquidate, + query, repay, uncollateralized_loan, withdraw, +}; #[entry_point] pub fn instantiate( @@ -10,7 +13,7 @@ pub fn instantiate( _info: MessageInfo, msg: InstantiateMsg, ) -> Result { - execute::instantiate(deps, msg) + instantiate::instantiate(deps, msg) } #[entry_point] @@ -21,29 +24,31 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { - ExecuteMsg::UpdateOwner(update) => execute::update_owner(deps, info, update), + ExecuteMsg::UpdateOwner(update) => config::update_owner(deps, info, update), ExecuteMsg::UpdateConfig { config, - } => execute::update_config(deps, info, config), + } => config::update_config(deps, info, config), ExecuteMsg::InitAsset { denom, params, - } => execute::init_asset(deps, env, info, denom, params), + } => asset::init_asset(deps, env, info, denom, params), ExecuteMsg::UpdateAsset { denom, params, - } => execute::update_asset(deps, env, info, denom, params), + } => asset::update_asset(deps, env, info, denom, params), ExecuteMsg::UpdateUncollateralizedLoanLimit { user, denom, new_limit, } => { let user_addr = deps.api.addr_validate(&user)?; - execute::update_uncollateralized_loan_limit(deps, info, user_addr, denom, new_limit) + uncollateralized_loan::update_uncollateralized_loan_limit( + deps, info, user_addr, denom, new_limit, + ) } ExecuteMsg::Deposit {} => { let sent_coin = cw_utils::one_coin(&info)?; - execute::deposit(deps, env, info, sent_coin.denom, sent_coin.amount) + deposit::deposit(deps, env, info, sent_coin.denom, sent_coin.amount) } ExecuteMsg::Withdraw { denom, @@ -51,7 +56,7 @@ pub fn execute( recipient, } => { cw_utils::nonpayable(&info)?; - execute::withdraw(deps, env, info, denom, amount, recipient) + withdraw::withdraw(deps, env, info, denom, amount, recipient) } ExecuteMsg::Borrow { denom, @@ -59,13 +64,13 @@ pub fn execute( recipient, } => { cw_utils::nonpayable(&info)?; - execute::borrow(deps, env, info, denom, amount, recipient) + borrow::borrow(deps, env, info, denom, amount, recipient) } ExecuteMsg::Repay { on_behalf_of, } => { let sent_coin = cw_utils::one_coin(&info)?; - execute::repay(deps, env, info, on_behalf_of, sent_coin.denom, sent_coin.amount) + repay::repay(deps, env, info, on_behalf_of, sent_coin.denom, sent_coin.amount) } ExecuteMsg::Liquidate { user, @@ -90,7 +95,7 @@ pub fn execute( enable, } => { cw_utils::nonpayable(&info)?; - execute::update_asset_collateral_status(deps, env, info, denom, enable) + collateral::update_asset_collateral_status(deps, env, info, denom, enable) } } } diff --git a/contracts/red-bank/src/deposit.rs b/contracts/red-bank/src/deposit.rs new file mode 100644 index 000000000..c3cda468e --- /dev/null +++ b/contracts/red-bank/src/deposit.rs @@ -0,0 +1,94 @@ +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Uint128}; +use mars_red_bank_types::address_provider::{self, MarsAddressType}; + +use crate::{ + error::ContractError, + helpers::query_asset_params, + interest_rates::{ + apply_accumulated_interests, get_scaled_liquidity_amount, get_underlying_liquidity_amount, + update_interest_rates, + }, + state::{CONFIG, MARKETS}, + user::User, +}; + +pub fn deposit( + deps: DepsMut, + env: Env, + info: MessageInfo, + denom: String, + deposit_amount: Uint128, +) -> Result { + let mut market = MARKETS.load(deps.storage, &denom)?; + + let config = CONFIG.load(deps.storage)?; + + let addresses = address_provider::helpers::query_contract_addrs( + deps.as_ref(), + &config.address_provider, + vec![ + MarsAddressType::Incentives, + MarsAddressType::RewardsCollector, + MarsAddressType::Params, + ], + )?; + let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; + let incentives_addr = &addresses[&MarsAddressType::Incentives]; + let params_addr = &addresses[&MarsAddressType::Params]; + + let asset_params = query_asset_params(&deps.querier, params_addr, &denom)?; + + if !asset_params.red_bank.deposit_enabled { + return Err(ContractError::DepositNotEnabled { + denom, + }); + } + + let total_scaled_deposits = market.collateral_total_scaled; + let total_deposits = + get_underlying_liquidity_amount(total_scaled_deposits, &market, env.block.time.seconds())?; + if total_deposits.checked_add(deposit_amount)? > asset_params.red_bank.deposit_cap { + return Err(ContractError::DepositCapExceeded { + denom, + }); + } + + let mut response = Response::new(); + + // update indexes and interest rates + response = apply_accumulated_interests( + deps.storage, + &env, + &mut market, + rewards_collector_addr, + incentives_addr, + response, + )?; + + if market.liquidity_index.is_zero() { + return Err(ContractError::InvalidLiquidityIndex {}); + } + let deposit_amount_scaled = + get_scaled_liquidity_amount(deposit_amount, &market, env.block.time.seconds())?; + + response = User(&info.sender).increase_collateral( + deps.storage, + &market, + deposit_amount_scaled, + incentives_addr, + response, + )?; + + market.increase_collateral(deposit_amount_scaled)?; + + response = update_interest_rates(&env, &mut market, response)?; + + MARKETS.save(deps.storage, &denom, &market)?; + + Ok(response + .add_attribute("action", "deposit") + .add_attribute("sender", &info.sender) + .add_attribute("denom", denom) + .add_attribute("amount", deposit_amount) + .add_attribute("amount_scaled", deposit_amount_scaled)) +} diff --git a/contracts/red-bank/src/execute.rs b/contracts/red-bank/src/execute.rs deleted file mode 100644 index 10b157932..000000000 --- a/contracts/red-bank/src/execute.rs +++ /dev/null @@ -1,742 +0,0 @@ -use cosmwasm_std::{Addr, Decimal, DepsMut, Env, MessageInfo, Response, StdResult, Uint128}; -use mars_owner::{OwnerInit::SetInitialOwner, OwnerUpdate}; -use mars_red_bank_types::{ - address_provider::{self, MarsAddressType}, - error::MarsError, - red_bank::{ - Config, CreateOrUpdateConfig, Debt, InitOrUpdateAssetParams, InstantiateMsg, Market, - }, -}; -use mars_utils::helpers::{ - build_send_asset_msg, option_string_to_addr, validate_native_denom, zero_address, -}; - -use crate::{ - error::ContractError, - health::{ - assert_below_liq_threshold_after_withdraw, assert_below_max_ltv_after_borrow, - get_health_and_positions, - }, - helpers::query_asset_params, - interest_rates::{ - apply_accumulated_interests, get_scaled_debt_amount, get_scaled_liquidity_amount, - get_underlying_debt_amount, get_underlying_liquidity_amount, update_interest_rates, - }, - state::{COLLATERALS, CONFIG, DEBTS, MARKETS, OWNER, UNCOLLATERALIZED_LOAN_LIMITS}, - user::User, -}; - -pub const CONTRACT_NAME: &str = "crates.io:mars-red-bank"; -pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); - -pub fn instantiate(deps: DepsMut, msg: InstantiateMsg) -> Result { - cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - - // Destructuring a struct’s fields into separate variables in order to force - // compile error if we add more params - let CreateOrUpdateConfig { - address_provider, - } = msg.config; - - if address_provider.is_none() { - return Err(MarsError::InstantiateParamsUnavailable {}.into()); - }; - - let config = Config { - address_provider: option_string_to_addr(deps.api, address_provider, zero_address())?, - }; - - CONFIG.save(deps.storage, &config)?; - - OWNER.initialize( - deps.storage, - deps.api, - SetInitialOwner { - owner: msg.owner, - }, - )?; - - Ok(Response::default()) -} - -pub fn update_owner( - deps: DepsMut, - info: MessageInfo, - update: OwnerUpdate, -) -> Result { - Ok(OWNER.update(deps, info, update)?) -} - -/// Update config -pub fn update_config( - deps: DepsMut, - info: MessageInfo, - new_config: CreateOrUpdateConfig, -) -> Result { - OWNER.assert_owner(deps.storage, &info.sender)?; - - let mut config = CONFIG.load(deps.storage)?; - - // Destructuring a struct’s fields into separate variables in order to force - // compile error if we add more params - let CreateOrUpdateConfig { - address_provider, - } = new_config; - - // Update config - config.address_provider = - option_string_to_addr(deps.api, address_provider, config.address_provider)?; - - CONFIG.save(deps.storage, &config)?; - - Ok(Response::new().add_attribute("action", "update_config")) -} - -/// Initialize asset if not exist. -/// Initialization requires that all params are provided and there is no asset in state. -pub fn init_asset( - deps: DepsMut, - env: Env, - info: MessageInfo, - denom: String, - params: InitOrUpdateAssetParams, -) -> Result { - OWNER.assert_owner(deps.storage, &info.sender)?; - - validate_native_denom(&denom)?; - - if MARKETS.may_load(deps.storage, &denom)?.is_some() { - return Err(ContractError::AssetAlreadyInitialized {}); - } - - let new_market = create_market(env.block.time.seconds(), &denom, params)?; - MARKETS.save(deps.storage, &denom, &new_market)?; - - Ok(Response::new().add_attribute("action", "init_asset").add_attribute("denom", denom)) -} - -/// Initialize new market -pub fn create_market( - block_time: u64, - denom: &str, - params: InitOrUpdateAssetParams, -) -> Result { - // Destructuring a struct’s fields into separate variables in order to force - // compile error if we add more params - let InitOrUpdateAssetParams { - reserve_factor, - interest_rate_model, - } = params; - - // All fields should be available - let available = reserve_factor.is_some() && interest_rate_model.is_some(); - - if !available { - return Err(MarsError::InstantiateParamsUnavailable {}.into()); - } - - let new_market = Market { - denom: denom.to_string(), - borrow_index: Decimal::one(), - liquidity_index: Decimal::one(), - borrow_rate: Decimal::zero(), - liquidity_rate: Decimal::zero(), - reserve_factor: reserve_factor.unwrap(), - indexes_last_updated: block_time, - collateral_total_scaled: Uint128::zero(), - debt_total_scaled: Uint128::zero(), - interest_rate_model: interest_rate_model.unwrap(), - }; - - new_market.validate()?; - - Ok(new_market) -} - -/// Update asset with new params. -pub fn update_asset( - deps: DepsMut, - env: Env, - info: MessageInfo, - denom: String, - params: InitOrUpdateAssetParams, -) -> Result { - OWNER.assert_owner(deps.storage, &info.sender)?; - - let market_option = MARKETS.may_load(deps.storage, &denom)?; - match market_option { - None => Err(ContractError::AssetNotInitialized {}), - Some(mut market) => { - // Destructuring a struct’s fields into separate variables in order to force - // compile error if we add more params - let InitOrUpdateAssetParams { - reserve_factor, - interest_rate_model, - } = params; - - // If reserve factor or interest rates are updated we update indexes with - // current values before applying the change to prevent applying this - // new params to a period where they were not valid yet. Interests rates are - // recalculated after changes are applied. - let should_update_interest_rates = (reserve_factor.is_some() - && reserve_factor.unwrap() != market.reserve_factor) - || interest_rate_model.is_some(); - - let mut response = Response::new(); - - if should_update_interest_rates { - let config = CONFIG.load(deps.storage)?; - let addresses = address_provider::helpers::query_contract_addrs( - deps.as_ref(), - &config.address_provider, - vec![MarsAddressType::Incentives, MarsAddressType::RewardsCollector], - )?; - let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; - let incentives_addr = &addresses[&MarsAddressType::Incentives]; - - response = apply_accumulated_interests( - deps.storage, - &env, - &mut market, - rewards_collector_addr, - incentives_addr, - response, - )?; - } - - let mut updated_market = Market { - reserve_factor: reserve_factor.unwrap_or(market.reserve_factor), - interest_rate_model: interest_rate_model.unwrap_or(market.interest_rate_model), - ..market - }; - - updated_market.validate()?; - - if should_update_interest_rates { - response = update_interest_rates(&env, &mut updated_market, response)?; - } - MARKETS.save(deps.storage, &denom, &updated_market)?; - - Ok(response.add_attribute("action", "update_asset").add_attribute("denom", denom)) - } - } -} - -/// Update uncollateralized loan limit by a given amount in base asset -pub fn update_uncollateralized_loan_limit( - deps: DepsMut, - info: MessageInfo, - user_addr: Addr, - denom: String, - new_limit: Uint128, -) -> Result { - OWNER.assert_owner(deps.storage, &info.sender)?; - - // Check that the user has no collateralized debt - let current_limit = UNCOLLATERALIZED_LOAN_LIMITS - .may_load(deps.storage, (&user_addr, &denom))? - .unwrap_or_else(Uint128::zero); - let current_debt = DEBTS - .may_load(deps.storage, (&user_addr, &denom))? - .map(|debt| debt.amount_scaled) - .unwrap_or_else(Uint128::zero); - if current_limit.is_zero() && !current_debt.is_zero() { - return Err(ContractError::UserHasCollateralizedDebt {}); - } - if !current_limit.is_zero() && new_limit.is_zero() && !current_debt.is_zero() { - return Err(ContractError::UserHasUncollateralizedDebt {}); - } - - UNCOLLATERALIZED_LOAN_LIMITS.save(deps.storage, (&user_addr, &denom), &new_limit)?; - - DEBTS.update(deps.storage, (&user_addr, &denom), |debt_opt: Option| -> StdResult<_> { - let mut debt = debt_opt.unwrap_or(Debt { - amount_scaled: Uint128::zero(), - uncollateralized: false, - }); - // if limit == 0 then uncollateralized = false, otherwise uncollateralized = true - debt.uncollateralized = !new_limit.is_zero(); - Ok(debt) - })?; - - Ok(Response::new() - .add_attribute("action", "update_uncollateralized_loan_limit") - .add_attribute("user", user_addr) - .add_attribute("denom", denom) - .add_attribute("new_allowance", new_limit)) -} - -/// Execute deposits -pub fn deposit( - deps: DepsMut, - env: Env, - info: MessageInfo, - denom: String, - deposit_amount: Uint128, -) -> Result { - let mut market = MARKETS.load(deps.storage, &denom)?; - - let config = CONFIG.load(deps.storage)?; - - let addresses = address_provider::helpers::query_contract_addrs( - deps.as_ref(), - &config.address_provider, - vec![ - MarsAddressType::Incentives, - MarsAddressType::RewardsCollector, - MarsAddressType::Params, - ], - )?; - let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; - let incentives_addr = &addresses[&MarsAddressType::Incentives]; - let params_addr = &addresses[&MarsAddressType::Params]; - - let asset_params = query_asset_params(&deps.querier, params_addr, &denom)?; - - if !asset_params.red_bank.deposit_enabled { - return Err(ContractError::DepositNotEnabled { - denom, - }); - } - - let total_scaled_deposits = market.collateral_total_scaled; - let total_deposits = - get_underlying_liquidity_amount(total_scaled_deposits, &market, env.block.time.seconds())?; - if total_deposits.checked_add(deposit_amount)? > asset_params.red_bank.deposit_cap { - return Err(ContractError::DepositCapExceeded { - denom, - }); - } - - let mut response = Response::new(); - - // update indexes and interest rates - response = apply_accumulated_interests( - deps.storage, - &env, - &mut market, - rewards_collector_addr, - incentives_addr, - response, - )?; - - if market.liquidity_index.is_zero() { - return Err(ContractError::InvalidLiquidityIndex {}); - } - let deposit_amount_scaled = - get_scaled_liquidity_amount(deposit_amount, &market, env.block.time.seconds())?; - - response = User(&info.sender).increase_collateral( - deps.storage, - &market, - deposit_amount_scaled, - incentives_addr, - response, - )?; - - market.increase_collateral(deposit_amount_scaled)?; - - response = update_interest_rates(&env, &mut market, response)?; - - MARKETS.save(deps.storage, &denom, &market)?; - - Ok(response - .add_attribute("action", "deposit") - .add_attribute("sender", &info.sender) - .add_attribute("denom", denom) - .add_attribute("amount", deposit_amount) - .add_attribute("amount_scaled", deposit_amount_scaled)) -} - -/// Burns sent maAsset in exchange of underlying asset -pub fn withdraw( - deps: DepsMut, - env: Env, - info: MessageInfo, - denom: String, - amount: Option, - recipient: Option, -) -> Result { - let withdrawer = User(&info.sender); - - let mut market = MARKETS.load(deps.storage, &denom)?; - - let collateral = withdrawer.collateral(deps.storage, &denom)?; - let withdrawer_balance_scaled_before = collateral.amount_scaled; - - if withdrawer_balance_scaled_before.is_zero() { - return Err(ContractError::UserNoCollateralBalance { - user: withdrawer.into(), - denom, - }); - } - - let withdrawer_balance_before = get_underlying_liquidity_amount( - withdrawer_balance_scaled_before, - &market, - env.block.time.seconds(), - )?; - - let withdraw_amount = match amount { - // Check user has sufficient balance to send back - Some(amount) if amount.is_zero() || amount > withdrawer_balance_before => { - return Err(ContractError::InvalidWithdrawAmount { - denom, - }); - } - Some(amount) => amount, - // If no amount is specified, the full balance is withdrawn - None => withdrawer_balance_before, - }; - - let config = CONFIG.load(deps.storage)?; - - let addresses = address_provider::helpers::query_contract_addrs( - deps.as_ref(), - &config.address_provider, - vec![ - MarsAddressType::Oracle, - MarsAddressType::Incentives, - MarsAddressType::RewardsCollector, - MarsAddressType::Params, - ], - )?; - let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; - let incentives_addr = &addresses[&MarsAddressType::Incentives]; - let oracle_addr = &addresses[&MarsAddressType::Oracle]; - let params_addr = &addresses[&MarsAddressType::Params]; - - // if asset is used as collateral and user is borrowing we need to validate health factor after withdraw, - // otherwise no reasons to block the withdraw - if collateral.enabled - && withdrawer.is_borrowing(deps.storage) - && !assert_below_liq_threshold_after_withdraw( - &deps.as_ref(), - &env, - withdrawer.address(), - oracle_addr, - params_addr, - &denom, - withdraw_amount, - )? - { - return Err(ContractError::InvalidHealthFactorAfterWithdraw {}); - } - - let mut response = Response::new(); - - // update indexes and interest rates - response = apply_accumulated_interests( - deps.storage, - &env, - &mut market, - rewards_collector_addr, - incentives_addr, - response, - )?; - - // reduce the withdrawer's scaled collateral amount - let withdrawer_balance_after = withdrawer_balance_before.checked_sub(withdraw_amount)?; - let withdrawer_balance_scaled_after = - get_scaled_liquidity_amount(withdrawer_balance_after, &market, env.block.time.seconds())?; - - let withdraw_amount_scaled = - withdrawer_balance_scaled_before.checked_sub(withdrawer_balance_scaled_after)?; - - response = withdrawer.decrease_collateral( - deps.storage, - &market, - withdraw_amount_scaled, - incentives_addr, - response, - )?; - - market.decrease_collateral(withdraw_amount_scaled)?; - - response = update_interest_rates(&env, &mut market, response)?; - - MARKETS.save(deps.storage, &denom, &market)?; - - // send underlying asset to user or another recipient - let recipient_addr = if let Some(recipient) = recipient { - deps.api.addr_validate(&recipient)? - } else { - withdrawer.address().clone() - }; - - Ok(response - .add_message(build_send_asset_msg(&recipient_addr, &denom, withdraw_amount)) - .add_attribute("action", "withdraw") - .add_attribute("sender", withdrawer) - .add_attribute("recipient", recipient_addr) - .add_attribute("denom", denom) - .add_attribute("amount", withdraw_amount) - .add_attribute("amount_scaled", withdraw_amount_scaled)) -} - -/// Add debt for the borrower and send the borrowed funds -pub fn borrow( - deps: DepsMut, - env: Env, - info: MessageInfo, - denom: String, - borrow_amount: Uint128, - recipient: Option, -) -> Result { - let borrower = User(&info.sender); - - let config = CONFIG.load(deps.storage)?; - - let addresses = address_provider::helpers::query_contract_addrs( - deps.as_ref(), - &config.address_provider, - vec![ - MarsAddressType::Oracle, - MarsAddressType::Incentives, - MarsAddressType::RewardsCollector, - MarsAddressType::Params, - ], - )?; - let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; - let incentives_addr = &addresses[&MarsAddressType::Incentives]; - let oracle_addr = &addresses[&MarsAddressType::Oracle]; - let params_addr = &addresses[&MarsAddressType::Params]; - - let asset_params = query_asset_params(&deps.querier, params_addr, &denom)?; - - if !asset_params.red_bank.borrow_enabled { - return Err(ContractError::BorrowNotEnabled { - denom, - }); - } - - // Load market and user state - let mut borrow_market = MARKETS.load(deps.storage, &denom)?; - - let collateral_balance_before = get_underlying_liquidity_amount( - borrow_market.collateral_total_scaled, - &borrow_market, - env.block.time.seconds(), - )?; - - // Cannot borrow zero amount or more than available collateral - if borrow_amount.is_zero() || borrow_amount > collateral_balance_before { - return Err(ContractError::InvalidBorrowAmount { - denom, - }); - } - - let uncollateralized_loan_limit = borrower.uncollateralized_loan_limit(deps.storage, &denom)?; - - // Check if user can borrow specified amount - let mut uncollateralized_debt = false; - if uncollateralized_loan_limit.is_zero() { - if !assert_below_max_ltv_after_borrow( - &deps.as_ref(), - &env, - borrower.address(), - oracle_addr, - params_addr, - &denom, - borrow_amount, - )? { - return Err(ContractError::BorrowAmountExceedsGivenCollateral {}); - } - } else { - // Uncollateralized loan: check borrow amount plus debt does not exceed uncollateralized loan limit - uncollateralized_debt = true; - - let debt_amount_scaled = borrower.debt_amount_scaled(deps.storage, &denom)?; - - let asset_market = MARKETS.load(deps.storage, &denom)?; - let debt_amount = get_underlying_debt_amount( - debt_amount_scaled, - &asset_market, - env.block.time.seconds(), - )?; - - let debt_after_borrow = debt_amount.checked_add(borrow_amount)?; - if debt_after_borrow > uncollateralized_loan_limit { - return Err(ContractError::BorrowAmountExceedsUncollateralizedLoanLimit {}); - } - } - - let mut response = Response::new(); - - response = apply_accumulated_interests( - deps.storage, - &env, - &mut borrow_market, - rewards_collector_addr, - incentives_addr, - response, - )?; - - // Set new debt - let borrow_amount_scaled = - get_scaled_debt_amount(borrow_amount, &borrow_market, env.block.time.seconds())?; - - borrow_market.increase_debt(borrow_amount_scaled)?; - borrower.increase_debt(deps.storage, &denom, borrow_amount_scaled, uncollateralized_debt)?; - - response = update_interest_rates(&env, &mut borrow_market, response)?; - MARKETS.save(deps.storage, &denom, &borrow_market)?; - - // Send borrow amount to borrower or another recipient - let recipient_addr = if let Some(recipient) = recipient { - deps.api.addr_validate(&recipient)? - } else { - borrower.address().clone() - }; - - Ok(response - .add_message(build_send_asset_msg(&recipient_addr, &denom, borrow_amount)) - .add_attribute("action", "borrow") - .add_attribute("sender", borrower) - .add_attribute("recipient", recipient_addr) - .add_attribute("denom", denom) - .add_attribute("amount", borrow_amount) - .add_attribute("amount_scaled", borrow_amount_scaled)) -} - -/// Handle the repay of native tokens. Refund extra funds if they exist -pub fn repay( - deps: DepsMut, - env: Env, - info: MessageInfo, - on_behalf_of: Option, - denom: String, - repay_amount: Uint128, -) -> Result { - let user_addr: Addr; - let user = if let Some(address) = on_behalf_of { - user_addr = deps.api.addr_validate(&address)?; - let user = User(&user_addr); - // Uncollateralized loans should not have 'on behalf of' because it creates accounting complexity for them - if !user.uncollateralized_loan_limit(deps.storage, &denom)?.is_zero() { - return Err(ContractError::CannotRepayUncollateralizedLoanOnBehalfOf {}); - } - user - } else { - User(&info.sender) - }; - - // Check new debt - let debt = DEBTS - .may_load(deps.storage, (user.address(), &denom))? - .ok_or(ContractError::CannotRepayZeroDebt {})?; - - let config = CONFIG.load(deps.storage)?; - - let addresses = address_provider::helpers::query_contract_addrs( - deps.as_ref(), - &config.address_provider, - vec![MarsAddressType::Incentives, MarsAddressType::RewardsCollector], - )?; - let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; - let incentives_addr = &addresses[&MarsAddressType::Incentives]; - - let mut market = MARKETS.load(deps.storage, &denom)?; - - let mut response = Response::new(); - - response = apply_accumulated_interests( - deps.storage, - &env, - &mut market, - rewards_collector_addr, - incentives_addr, - response, - )?; - - let debt_amount_scaled_before = debt.amount_scaled; - let debt_amount_before = - get_underlying_debt_amount(debt.amount_scaled, &market, env.block.time.seconds())?; - - // If repay amount exceeds debt, refund any excess amounts - let mut refund_amount = Uint128::zero(); - let mut debt_amount_after = Uint128::zero(); - if repay_amount > debt_amount_before { - refund_amount = repay_amount - debt_amount_before; - let refund_msg = build_send_asset_msg(&info.sender, &denom, refund_amount); - response = response.add_message(refund_msg); - } else { - debt_amount_after = debt_amount_before - repay_amount; - } - - let debt_amount_scaled_after = - get_scaled_debt_amount(debt_amount_after, &market, env.block.time.seconds())?; - - let debt_amount_scaled_delta = - debt_amount_scaled_before.checked_sub(debt_amount_scaled_after)?; - - market.decrease_debt(debt_amount_scaled_delta)?; - user.decrease_debt(deps.storage, &denom, debt_amount_scaled_delta)?; - - response = update_interest_rates(&env, &mut market, response)?; - MARKETS.save(deps.storage, &denom, &market)?; - - Ok(response - .add_attribute("action", "repay") - .add_attribute("sender", &info.sender) - .add_attribute("on_behalf_of", user) - .add_attribute("denom", denom) - .add_attribute("amount", repay_amount.checked_sub(refund_amount)?) - .add_attribute("amount_scaled", debt_amount_scaled_delta)) -} - -/// Update (enable / disable) collateral asset for specific user -pub fn update_asset_collateral_status( - deps: DepsMut, - env: Env, - info: MessageInfo, - denom: String, - enable: bool, -) -> Result { - let user = User(&info.sender); - - let mut collateral = - COLLATERALS.may_load(deps.storage, (user.address(), &denom))?.ok_or_else(|| { - ContractError::UserNoCollateralBalance { - user: user.into(), - denom: denom.clone(), - } - })?; - - let previously_enabled = collateral.enabled; - - collateral.enabled = enable; - COLLATERALS.save(deps.storage, (user.address(), &denom), &collateral)?; - - // if the collateral was previously enabled, but is not disabled, it is necessary to ensure the - // user is not liquidatable after disabling - if previously_enabled && !enable { - let config = CONFIG.load(deps.storage)?; - - let addresses = address_provider::helpers::query_contract_addrs( - deps.as_ref(), - &config.address_provider, - vec![MarsAddressType::Oracle, MarsAddressType::Params], - )?; - let oracle_addr = &addresses[&MarsAddressType::Oracle]; - let params_addr = &addresses[&MarsAddressType::Params]; - - let (health, _) = get_health_and_positions( - &deps.as_ref(), - &env, - user.address(), - oracle_addr, - params_addr, - )?; - - if health.is_liquidatable() { - return Err(ContractError::InvalidHealthFactorAfterDisablingCollateral {}); - } - } - - Ok(Response::new() - .add_attribute("action", "update_asset_collateral_status") - .add_attribute("user", user) - .add_attribute("denom", denom) - .add_attribute("enable", enable.to_string())) -} diff --git a/contracts/red-bank/src/instantiate.rs b/contracts/red-bank/src/instantiate.rs new file mode 100644 index 000000000..eb79948db --- /dev/null +++ b/contracts/red-bank/src/instantiate.rs @@ -0,0 +1,46 @@ +use cosmwasm_std::{DepsMut, Response}; +use mars_owner::OwnerInit::SetInitialOwner; +use mars_red_bank_types::{ + error::MarsError, + red_bank::{Config, CreateOrUpdateConfig, InstantiateMsg}, +}; +use mars_utils::helpers::{option_string_to_addr, zero_address}; + +use crate::{ + error::ContractError, + state::{CONFIG, OWNER}, +}; + +pub const CONTRACT_NAME: &str = "crates.io:mars-red-bank"; + +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub fn instantiate(deps: DepsMut, msg: InstantiateMsg) -> Result { + cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + // Destructuring a struct’s fields into separate variables in order to force + // compile error if we add more params + let CreateOrUpdateConfig { + address_provider, + } = msg.config; + + if address_provider.is_none() { + return Err(MarsError::InstantiateParamsUnavailable {}.into()); + }; + + let config = Config { + address_provider: option_string_to_addr(deps.api, address_provider, zero_address())?, + }; + + CONFIG.save(deps.storage, &config)?; + + OWNER.initialize( + deps.storage, + deps.api, + SetInitialOwner { + owner: msg.owner, + }, + )?; + + Ok(Response::default()) +} diff --git a/contracts/red-bank/src/lib.rs b/contracts/red-bank/src/lib.rs index 165013c9a..c3e17f007 100644 --- a/contracts/red-bank/src/lib.rs +++ b/contracts/red-bank/src/lib.rs @@ -1,12 +1,20 @@ +pub mod asset; +pub mod borrow; +pub mod collateral; +pub mod config; #[cfg(not(feature = "library"))] pub mod contract; +pub mod deposit; pub mod error; -pub mod execute; pub mod health; +pub mod instantiate; pub mod interest_rates; pub mod liquidate; pub mod query; +pub mod repay; pub mod state; +pub mod uncollateralized_loan; pub mod user; +pub mod withdraw; pub mod helpers; diff --git a/contracts/red-bank/src/repay.rs b/contracts/red-bank/src/repay.rs new file mode 100644 index 000000000..6e28874a6 --- /dev/null +++ b/contracts/red-bank/src/repay.rs @@ -0,0 +1,98 @@ +use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response, Uint128}; +use mars_red_bank_types::{address_provider, address_provider::MarsAddressType}; +use mars_utils::helpers::build_send_asset_msg; + +use crate::{ + error::ContractError, + interest_rates::{ + apply_accumulated_interests, get_scaled_debt_amount, get_underlying_debt_amount, + update_interest_rates, + }, + state::{CONFIG, DEBTS, MARKETS}, + user::User, +}; + +pub fn repay( + deps: DepsMut, + env: Env, + info: MessageInfo, + on_behalf_of: Option, + denom: String, + repay_amount: Uint128, +) -> Result { + let user_addr: Addr; + let user = if let Some(address) = on_behalf_of { + user_addr = deps.api.addr_validate(&address)?; + let user = User(&user_addr); + // Uncollateralized loans should not have 'on behalf of' because it creates accounting complexity for them + if !user.uncollateralized_loan_limit(deps.storage, &denom)?.is_zero() { + return Err(ContractError::CannotRepayUncollateralizedLoanOnBehalfOf {}); + } + user + } else { + User(&info.sender) + }; + + // Check new debt + let debt = DEBTS + .may_load(deps.storage, (user.address(), &denom))? + .ok_or(ContractError::CannotRepayZeroDebt {})?; + + let config = CONFIG.load(deps.storage)?; + + let addresses = address_provider::helpers::query_contract_addrs( + deps.as_ref(), + &config.address_provider, + vec![MarsAddressType::Incentives, MarsAddressType::RewardsCollector], + )?; + let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; + let incentives_addr = &addresses[&MarsAddressType::Incentives]; + + let mut market = MARKETS.load(deps.storage, &denom)?; + + let mut response = Response::new(); + + response = apply_accumulated_interests( + deps.storage, + &env, + &mut market, + rewards_collector_addr, + incentives_addr, + response, + )?; + + let debt_amount_scaled_before = debt.amount_scaled; + let debt_amount_before = + get_underlying_debt_amount(debt.amount_scaled, &market, env.block.time.seconds())?; + + // If repay amount exceeds debt, refund any excess amounts + let mut refund_amount = Uint128::zero(); + let mut debt_amount_after = Uint128::zero(); + if repay_amount > debt_amount_before { + refund_amount = repay_amount - debt_amount_before; + let refund_msg = build_send_asset_msg(&info.sender, &denom, refund_amount); + response = response.add_message(refund_msg); + } else { + debt_amount_after = debt_amount_before - repay_amount; + } + + let debt_amount_scaled_after = + get_scaled_debt_amount(debt_amount_after, &market, env.block.time.seconds())?; + + let debt_amount_scaled_delta = + debt_amount_scaled_before.checked_sub(debt_amount_scaled_after)?; + + market.decrease_debt(debt_amount_scaled_delta)?; + user.decrease_debt(deps.storage, &denom, debt_amount_scaled_delta)?; + + response = update_interest_rates(&env, &mut market, response)?; + MARKETS.save(deps.storage, &denom, &market)?; + + Ok(response + .add_attribute("action", "repay") + .add_attribute("sender", &info.sender) + .add_attribute("on_behalf_of", user) + .add_attribute("denom", denom) + .add_attribute("amount", repay_amount.checked_sub(refund_amount)?) + .add_attribute("amount_scaled", debt_amount_scaled_delta)) +} diff --git a/contracts/red-bank/src/uncollateralized_loan.rs b/contracts/red-bank/src/uncollateralized_loan.rs new file mode 100644 index 000000000..7b4d839f9 --- /dev/null +++ b/contracts/red-bank/src/uncollateralized_loan.rs @@ -0,0 +1,51 @@ +use cosmwasm_std::{Addr, DepsMut, MessageInfo, Response, StdResult, Uint128}; +use mars_red_bank_types::red_bank::Debt; + +use crate::{ + error::ContractError, + state::{DEBTS, OWNER, UNCOLLATERALIZED_LOAN_LIMITS}, +}; + +/// Update uncollateralized loan limit by a given amount in base asset +pub fn update_uncollateralized_loan_limit( + deps: DepsMut, + info: MessageInfo, + user_addr: Addr, + denom: String, + new_limit: Uint128, +) -> Result { + OWNER.assert_owner(deps.storage, &info.sender)?; + + // Check that the user has no collateralized debt + let current_limit = UNCOLLATERALIZED_LOAN_LIMITS + .may_load(deps.storage, (&user_addr, &denom))? + .unwrap_or_else(Uint128::zero); + let current_debt = DEBTS + .may_load(deps.storage, (&user_addr, &denom))? + .map(|debt| debt.amount_scaled) + .unwrap_or_else(Uint128::zero); + if current_limit.is_zero() && !current_debt.is_zero() { + return Err(ContractError::UserHasCollateralizedDebt {}); + } + if !current_limit.is_zero() && new_limit.is_zero() && !current_debt.is_zero() { + return Err(ContractError::UserHasUncollateralizedDebt {}); + } + + UNCOLLATERALIZED_LOAN_LIMITS.save(deps.storage, (&user_addr, &denom), &new_limit)?; + + DEBTS.update(deps.storage, (&user_addr, &denom), |debt_opt: Option| -> StdResult<_> { + let mut debt = debt_opt.unwrap_or(Debt { + amount_scaled: Uint128::zero(), + uncollateralized: false, + }); + // if limit == 0 then uncollateralized = false, otherwise uncollateralized = true + debt.uncollateralized = !new_limit.is_zero(); + Ok(debt) + })?; + + Ok(Response::new() + .add_attribute("action", "update_uncollateralized_loan_limit") + .add_attribute("user", user_addr) + .add_attribute("denom", denom) + .add_attribute("new_allowance", new_limit)) +} diff --git a/contracts/red-bank/src/withdraw.rs b/contracts/red-bank/src/withdraw.rs new file mode 100644 index 000000000..48e94bb9c --- /dev/null +++ b/contracts/red-bank/src/withdraw.rs @@ -0,0 +1,139 @@ +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Uint128}; +use mars_red_bank_types::{address_provider, address_provider::MarsAddressType}; +use mars_utils::helpers::build_send_asset_msg; + +use crate::{ + error::ContractError, + health::assert_below_liq_threshold_after_withdraw, + interest_rates::{ + apply_accumulated_interests, get_scaled_liquidity_amount, get_underlying_liquidity_amount, + update_interest_rates, + }, + state::{CONFIG, MARKETS}, + user::User, +}; + +pub fn withdraw( + deps: DepsMut, + env: Env, + info: MessageInfo, + denom: String, + amount: Option, + recipient: Option, +) -> Result { + let withdrawer = User(&info.sender); + + let mut market = MARKETS.load(deps.storage, &denom)?; + + let collateral = withdrawer.collateral(deps.storage, &denom)?; + let withdrawer_balance_scaled_before = collateral.amount_scaled; + + if withdrawer_balance_scaled_before.is_zero() { + return Err(ContractError::UserNoCollateralBalance { + user: withdrawer.into(), + denom, + }); + } + + let withdrawer_balance_before = get_underlying_liquidity_amount( + withdrawer_balance_scaled_before, + &market, + env.block.time.seconds(), + )?; + + let withdraw_amount = match amount { + // Check user has sufficient balance to send back + Some(amount) if amount.is_zero() || amount > withdrawer_balance_before => { + return Err(ContractError::InvalidWithdrawAmount { + denom, + }); + } + Some(amount) => amount, + // If no amount is specified, the full balance is withdrawn + None => withdrawer_balance_before, + }; + + let config = CONFIG.load(deps.storage)?; + + let addresses = address_provider::helpers::query_contract_addrs( + deps.as_ref(), + &config.address_provider, + vec![ + MarsAddressType::Oracle, + MarsAddressType::Incentives, + MarsAddressType::RewardsCollector, + MarsAddressType::Params, + ], + )?; + let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; + let incentives_addr = &addresses[&MarsAddressType::Incentives]; + let oracle_addr = &addresses[&MarsAddressType::Oracle]; + let params_addr = &addresses[&MarsAddressType::Params]; + + // if asset is used as collateral and user is borrowing we need to validate health factor after withdraw, + // otherwise no reasons to block the withdraw + if collateral.enabled + && withdrawer.is_borrowing(deps.storage) + && !assert_below_liq_threshold_after_withdraw( + &deps.as_ref(), + &env, + withdrawer.address(), + oracle_addr, + params_addr, + &denom, + withdraw_amount, + )? + { + return Err(ContractError::InvalidHealthFactorAfterWithdraw {}); + } + + let mut response = Response::new(); + + // update indexes and interest rates + response = apply_accumulated_interests( + deps.storage, + &env, + &mut market, + rewards_collector_addr, + incentives_addr, + response, + )?; + + // reduce the withdrawer's scaled collateral amount + let withdrawer_balance_after = withdrawer_balance_before.checked_sub(withdraw_amount)?; + let withdrawer_balance_scaled_after = + get_scaled_liquidity_amount(withdrawer_balance_after, &market, env.block.time.seconds())?; + + let withdraw_amount_scaled = + withdrawer_balance_scaled_before.checked_sub(withdrawer_balance_scaled_after)?; + + response = withdrawer.decrease_collateral( + deps.storage, + &market, + withdraw_amount_scaled, + incentives_addr, + response, + )?; + + market.decrease_collateral(withdraw_amount_scaled)?; + + response = update_interest_rates(&env, &mut market, response)?; + + MARKETS.save(deps.storage, &denom, &market)?; + + // send underlying asset to user or another recipient + let recipient_addr = if let Some(recipient) = recipient { + deps.api.addr_validate(&recipient)? + } else { + withdrawer.address().clone() + }; + + Ok(response + .add_message(build_send_asset_msg(&recipient_addr, &denom, withdraw_amount)) + .add_attribute("action", "withdraw") + .add_attribute("sender", withdrawer) + .add_attribute("recipient", recipient_addr) + .add_attribute("denom", denom) + .add_attribute("amount", withdraw_amount) + .add_attribute("amount_scaled", withdraw_amount_scaled)) +} From 4cd795ca6821bf6aa0a6c374f8b4e4bb0e745380 Mon Sep 17 00:00:00 2001 From: piobab Date: Wed, 26 Jul 2023 16:30:40 +0200 Subject: [PATCH 11/43] MP-2615 rover rewards (#273) * Add account_id for rover deposit rewards. * Use user addr and account id as compound key. * Apply comments. --- Cargo.lock | 1 + contracts/incentives/src/contract.rs | 37 ++- contracts/incentives/src/helpers.rs | 10 +- contracts/incentives/src/state.rs | 12 +- .../incentives/tests/test_balance_change.rs | 192 ++++++++++++--- .../incentives/tests/test_claim_rewards.rs | 41 ++-- contracts/incentives/tests/test_whitelist.rs | 3 + contracts/red-bank/src/collateral.rs | 13 +- contracts/red-bank/src/contract.rs | 16 +- contracts/red-bank/src/deposit.rs | 2 + contracts/red-bank/src/health.rs | 17 +- contracts/red-bank/src/interest_rates.rs | 1 + contracts/red-bank/src/liquidate.rs | 5 +- contracts/red-bank/src/query.rs | 10 +- contracts/red-bank/src/state.rs | 3 +- contracts/red-bank/src/user.rs | 28 ++- contracts/red-bank/src/withdraw.rs | 4 +- contracts/red-bank/tests/helpers.rs | 8 +- contracts/red-bank/tests/test_admin.rs | 2 +- contracts/red-bank/tests/test_deposit.rs | 96 +++----- contracts/red-bank/tests/test_liquidate.rs | 21 +- contracts/red-bank/tests/test_payment.rs | 1 + contracts/red-bank/tests/test_query.rs | 5 +- contracts/red-bank/tests/test_withdraw.rs | 24 +- contracts/rewards-collector/src/contract.rs | 2 + .../rewards-collector/tests/test_withdraw.rs | 3 +- integration-tests/Cargo.toml | 1 + integration-tests/tests/helpers.rs | 15 +- integration-tests/tests/test_incentives.rs | 230 ++++++++++++++++-- integration-tests/tests/test_oracles.rs | 20 +- integration-tests/tests/test_rover_flow.rs | 10 +- integration-tests/tests/test_user_flow.rs | 4 +- packages/testing/src/incentives_querier.rs | 1 + packages/testing/src/integration/mock_env.rs | 102 ++++++-- packages/testing/src/red_bank_querier.rs | 1 + packages/types/src/address_provider.rs | 3 + packages/types/src/incentives.rs | 6 + packages/types/src/red_bank/msg.rs | 9 +- .../mars-address-provider.json | 15 +- schemas/mars-incentives/mars-incentives.json | 21 ++ schemas/mars-red-bank/mars-red-bank.json | 28 +++ .../MarsAddressProvider.types.ts | 2 +- .../mars-incentives/MarsIncentives.client.ts | 15 ++ .../MarsIncentives.react-query.ts | 4 + .../mars-incentives/MarsIncentives.types.ts | 3 + .../mars-red-bank/MarsRedBank.client.ts | 29 ++- .../mars-red-bank/MarsRedBank.react-query.ts | 10 +- .../mars-red-bank/MarsRedBank.types.ts | 7 +- 48 files changed, 862 insertions(+), 231 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f1fa53331..4b7e8dd38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1807,6 +1807,7 @@ dependencies = [ "cosmwasm-std", "cw-it", "cw-multi-test 0.16.5", + "mars-incentives", "mars-oracle-base", "mars-oracle-osmosis", "mars-osmosis", diff --git a/contracts/incentives/src/contract.rs b/contracts/incentives/src/contract.rs index 78969d0f9..a877040f5 100644 --- a/contracts/incentives/src/contract.rs +++ b/contracts/incentives/src/contract.rs @@ -104,6 +104,7 @@ pub fn execute( ), ExecuteMsg::BalanceChange { user_addr, + account_id, denom, user_amount_scaled_before, total_amount_scaled_before, @@ -112,11 +113,13 @@ pub fn execute( env, info, user_addr, + account_id, denom, user_amount_scaled_before, total_amount_scaled_before, ), ExecuteMsg::ClaimRewards { + account_id, start_after_collateral_denom, start_after_incentive_denom, limit, @@ -124,6 +127,7 @@ pub fn execute( deps, env, info, + account_id, start_after_collateral_denom, start_after_incentive_denom, limit, @@ -339,6 +343,7 @@ pub fn execute_balance_change( env: Env, info: MessageInfo, user_addr: Addr, + account_id: Option, collateral_denom: String, user_amount_scaled_before: Uint128, total_amount_scaled_before: Uint128, @@ -349,10 +354,17 @@ pub fn execute_balance_change( return Err(MarsError::Unauthorized {}.into()); } + let acc_id = account_id.clone().unwrap_or("".to_string()); + let base_event = Event::new("mars/incentives/balance_change") .add_attribute("action", "balance_change") .add_attribute("denom", collateral_denom.clone()) .add_attribute("user", user_addr.to_string()); + let base_event = if account_id.is_some() { + base_event.add_attribute("account_id", &acc_id) + } else { + base_event + }; let mut events = vec![base_event]; let incentive_states = INCENTIVE_STATES @@ -371,7 +383,7 @@ pub fn execute_balance_change( // Check if user has accumulated uncomputed rewards (which means index is not up to date) let user_asset_index_key = - USER_ASSET_INDICES.key((&user_addr, &collateral_denom, &incentive_denom)); + USER_ASSET_INDICES.key(((&user_addr, &acc_id), &collateral_denom, &incentive_denom)); let user_asset_index = user_asset_index_key.may_load(deps.storage)?.unwrap_or_else(Decimal::zero); @@ -391,6 +403,7 @@ pub fn execute_balance_change( state::increase_unclaimed_rewards( deps.storage, &user_addr, + &acc_id, &collateral_denom, &incentive_denom, accrued_rewards, @@ -415,17 +428,25 @@ pub fn execute_claim_rewards( mut deps: DepsMut, env: Env, info: MessageInfo, + account_id: Option, start_after_collateral_denom: Option, start_after_incentive_denom: Option, limit: Option, ) -> Result { - let red_bank_addr = query_red_bank_address(deps.as_ref())?; let user_addr = info.sender; + let acc_id = account_id.clone().unwrap_or("".to_string()); + + let red_bank_addr = query_red_bank_address(deps.as_ref())?; let mut response = Response::new(); let base_event = Event::new("mars/incentives/claim_rewards") .add_attribute("action", "claim_rewards") .add_attribute("user", user_addr.to_string()); + let base_event = if account_id.is_some() { + base_event.add_attribute("account_id", &acc_id) + } else { + base_event + }; let mut events = vec![base_event]; let asset_incentives = state::paginate_incentive_states( @@ -445,6 +466,7 @@ pub fn execute_claim_rewards( &env.block, &red_bank_addr, &user_addr, + &account_id, &collateral_denom, &incentive_denom, )?; @@ -452,7 +474,7 @@ pub fn execute_claim_rewards( // clear unclaimed rewards USER_UNCLAIMED_REWARDS.save( deps.storage, - (&user_addr, &collateral_denom, &incentive_denom), + ((&user_addr, &acc_id), &collateral_denom, &incentive_denom), &Uint128::zero(), )?; @@ -532,6 +554,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { )?), QueryMsg::UserUnclaimedRewards { user, + account_id, start_after_collateral_denom, start_after_incentive_denom, limit, @@ -539,6 +562,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { deps, env, user, + account_id, start_after_collateral_denom, start_after_incentive_denom, limit, @@ -635,13 +659,15 @@ pub fn query_user_unclaimed_rewards( deps: Deps, env: Env, user: String, + account_id: Option, start_after_collateral_denom: Option, start_after_incentive_denom: Option, limit: Option, -) -> StdResult> { - let red_bank_addr = query_red_bank_address(deps)?; +) -> Result, ContractError> { let user_addr = deps.api.addr_validate(&user)?; + let red_bank_addr = query_red_bank_address(deps)?; + let incentive_states = state::paginate_incentive_states( deps.storage, start_after_collateral_denom, @@ -658,6 +684,7 @@ pub fn query_user_unclaimed_rewards( &env.block, &red_bank_addr, &user_addr, + &account_id, &collateral_denom, &incentive_denom, )?; diff --git a/contracts/incentives/src/helpers.rs b/contracts/incentives/src/helpers.rs index 7183496ea..5201bd946 100644 --- a/contracts/incentives/src/helpers.rs +++ b/contracts/incentives/src/helpers.rs @@ -254,11 +254,14 @@ pub fn compute_user_unclaimed_rewards( block: &BlockInfo, red_bank_addr: &Addr, user_addr: &Addr, + account_id: &Option, collateral_denom: &str, incentive_denom: &str, ) -> StdResult { + let acc_id = account_id.clone().unwrap_or("".to_string()); + let mut unclaimed_rewards = USER_UNCLAIMED_REWARDS - .may_load(storage.to_storage(), (user_addr, collateral_denom, incentive_denom))? + .may_load(storage.to_storage(), ((user_addr, &acc_id), collateral_denom, incentive_denom))? .unwrap_or_else(Uint128::zero); // Get asset user balances and total supply @@ -266,6 +269,7 @@ pub fn compute_user_unclaimed_rewards( red_bank_addr, &red_bank::QueryMsg::UserCollateral { user: user_addr.to_string(), + account_id: account_id.clone(), denom: collateral_denom.to_string(), }, )?; @@ -292,7 +296,7 @@ pub fn compute_user_unclaimed_rewards( )?; let user_asset_index = USER_ASSET_INDICES - .may_load(storage.to_storage(), (user_addr, collateral_denom, incentive_denom))? + .may_load(storage.to_storage(), ((user_addr, &acc_id), collateral_denom, incentive_denom))? .unwrap_or_else(Decimal::zero); if user_asset_index != incentive_state.index { @@ -310,7 +314,7 @@ pub fn compute_user_unclaimed_rewards( if user_asset_index != incentive_state.index { USER_ASSET_INDICES.save( *storage, - (user_addr, collateral_denom, incentive_denom), + ((user_addr, &acc_id), collateral_denom, incentive_denom), &incentive_state.index, )? } diff --git a/contracts/incentives/src/state.rs b/contracts/incentives/src/state.rs index b8f9ede94..16d2342f9 100644 --- a/contracts/incentives/src/state.rs +++ b/contracts/incentives/src/state.rs @@ -32,12 +32,13 @@ pub const INCENTIVE_STATES: Map<(&str, &str), IncentiveState> = Map::new("incent pub const EMISSIONS: Map<(&str, &str, u64), Uint128> = Map::new("emissions"); /// A map containing the incentive index for a given user, collateral denom and incentive denom. -/// The key is (user address, collateral denom, incentive denom). -pub const USER_ASSET_INDICES: Map<(&Addr, &str, &str), Decimal> = Map::new("indices"); +/// The key is (user address with optional account id, collateral denom, incentive denom). +pub const USER_ASSET_INDICES: Map<((&Addr, &str), &str, &str), Decimal> = Map::new("indices"); /// A map containing the amount of unclaimed incentives for a given user and incentive denom. -/// The key is (user address, collateral denom, incentive denom). -pub const USER_UNCLAIMED_REWARDS: Map<(&Addr, &str, &str), Uint128> = Map::new("unclaimed_rewards"); +/// The key is (user address with optional account id, collateral denom, incentive denom). +pub const USER_UNCLAIMED_REWARDS: Map<((&Addr, &str), &str, &str), Uint128> = + Map::new("unclaimed_rewards"); /// The default limit for pagination pub const DEFAULT_LIMIT: u32 = 5; @@ -50,13 +51,14 @@ pub const MAX_LIMIT: u32 = 10; pub fn increase_unclaimed_rewards( storage: &mut dyn Storage, user_addr: &Addr, + acc_id: &str, collateral_denom: &str, incentive_denom: &str, accrued_rewards: Uint128, ) -> StdResult<()> { USER_UNCLAIMED_REWARDS.update( storage, - (user_addr, collateral_denom, incentive_denom), + ((user_addr, acc_id), collateral_denom, incentive_denom), |ur: Option| -> StdResult { Ok(ur.map_or_else(|| accrued_rewards, |r| r + accrued_rewards)) }, diff --git a/contracts/incentives/tests/test_balance_change.rs b/contracts/incentives/tests/test_balance_change.rs index 96216f174..07b49c8e8 100644 --- a/contracts/incentives/tests/test_balance_change.rs +++ b/contracts/incentives/tests/test_balance_change.rs @@ -30,6 +30,7 @@ fn balance_change_unauthorized() { mock_info("jake", &[]), // not Red Bank ExecuteMsg::BalanceChange { user_addr: Addr::unchecked("user"), + account_id: None, denom: "uosmo".to_string(), user_amount_scaled_before: Uint128::new(100000), total_amount_scaled_before: Uint128::new(100000), @@ -47,6 +48,7 @@ fn execute_balance_change_noops() { let info = mock_info("red_bank", &[]); let msg = ExecuteMsg::BalanceChange { user_addr: Addr::unchecked("user"), + account_id: None, denom: "uosmo".to_string(), user_amount_scaled_before: Uint128::new(100000), total_amount_scaled_before: Uint128::new(100000), @@ -93,6 +95,7 @@ fn balance_change_zero_emission() { }); let msg = ExecuteMsg::BalanceChange { user_addr: Addr::unchecked("user"), + account_id: None, denom: "uosmo".to_string(), user_amount_scaled_before: Uint128::new(100_000), total_amount_scaled_before: Uint128::new(100_000), @@ -106,7 +109,7 @@ fn balance_change_zero_emission() { assert_eq!( res.events[0].attributes, - vec![attr("action", "balance_change"), attr("denom", denom), attr("user", "user"),] + vec![attr("action", "balance_change"), attr("denom", denom), attr("user", "user")] ); assert_eq!( res.events[1].attributes, @@ -124,12 +127,13 @@ fn balance_change_zero_emission() { // user index is set to asset's index let user_asset_index = - USER_ASSET_INDICES.load(deps.as_ref().storage, (&user_addr, denom, "umars")).unwrap(); + USER_ASSET_INDICES.load(deps.as_ref().storage, ((&user_addr, ""), denom, "umars")).unwrap(); assert_eq!(user_asset_index, asset_incentive_index); // rewards get updated - let user_unclaimed_rewards = - USER_UNCLAIMED_REWARDS.load(deps.as_ref().storage, (&user_addr, denom, "umars")).unwrap(); + let user_unclaimed_rewards = USER_UNCLAIMED_REWARDS + .load(deps.as_ref().storage, ((&user_addr, ""), denom, "umars")) + .unwrap(); assert_eq!(user_unclaimed_rewards, expected_accrued_rewards) } @@ -167,6 +171,7 @@ fn balance_change_user_with_zero_balance() { }); let msg = ExecuteMsg::BalanceChange { user_addr: user_addr.clone(), + account_id: None, denom: "uosmo".to_string(), user_amount_scaled_before: Uint128::zero(), total_amount_scaled_before: total_supply, @@ -185,7 +190,7 @@ fn balance_change_user_with_zero_balance() { assert_eq!( res.events[0].attributes, - vec![attr("action", "balance_change"), attr("denom", denom), attr("user", "user"),] + vec![attr("action", "balance_change"), attr("denom", denom), attr("user", "user")] ); assert_eq!( res.events[1].attributes, @@ -203,12 +208,12 @@ fn balance_change_user_with_zero_balance() { // user index is set to asset's index let user_asset_index = - USER_ASSET_INDICES.load(deps.as_ref().storage, (&user_addr, denom, "umars")).unwrap(); + USER_ASSET_INDICES.load(deps.as_ref().storage, ((&user_addr, ""), denom, "umars")).unwrap(); assert_eq!(user_asset_index, expected_index); // no new rewards let user_unclaimed_rewards = USER_UNCLAIMED_REWARDS - .may_load(deps.as_ref().storage, (&user_addr, denom, "umars")) + .may_load(deps.as_ref().storage, ((&user_addr, ""), denom, "umars")) .unwrap(); assert_eq!(user_unclaimed_rewards, None) } @@ -247,6 +252,7 @@ fn with_zero_previous_balance_and_asset_with_zero_index_accumulates_rewards() { }); let msg = ExecuteMsg::BalanceChange { user_addr: user_addr.clone(), + account_id: None, denom: "uosmo".to_string(), user_amount_scaled_before: Uint128::zero(), total_amount_scaled_before: Uint128::zero(), @@ -278,9 +284,16 @@ fn with_zero_previous_balance_and_asset_with_zero_index_accumulates_rewards() { block_time: Timestamp::from_seconds(time_contract_call + 1000), ..Default::default() }); - let rewards_query = - query_user_unclaimed_rewards(deps.as_ref(), env, "user".to_string(), None, None, None) - .unwrap(); + let rewards_query = query_user_unclaimed_rewards( + deps.as_ref(), + env, + "user".to_string(), + None, + None, + None, + None, + ) + .unwrap(); // Rewards that are accrued when no one had deposit in Red Bank are distributed to the first depositor assert_eq!( vec![coin( @@ -351,9 +364,16 @@ fn set_new_asset_incentive_user_non_zero_balance() { ..Default::default() }); - let unclaimed_rewards = - query_user_unclaimed_rewards(deps.as_ref(), env, "user".to_string(), None, None, None) - .unwrap(); + let unclaimed_rewards = query_user_unclaimed_rewards( + deps.as_ref(), + env, + "user".to_string(), + None, + None, + None, + None, + ) + .unwrap(); // 100_000 s * 100 MARS/s * 1/10th of total deposit let expected_unclaimed_rewards = vec![coin(1_000_000, "umars")]; assert_eq!(unclaimed_rewards, expected_unclaimed_rewards); @@ -386,6 +406,7 @@ fn set_new_asset_incentive_user_non_zero_balance() { env, info, user_addr, + None, denom.to_string(), Uint128::new(10_000), total_supply, @@ -402,9 +423,16 @@ fn set_new_asset_incentive_user_non_zero_balance() { ..Default::default() }); - let unclaimed_rewards = - query_user_unclaimed_rewards(deps.as_ref(), env, "user".to_string(), None, None, None) - .unwrap(); + let unclaimed_rewards = query_user_unclaimed_rewards( + deps.as_ref(), + env, + "user".to_string(), + None, + None, + None, + None, + ) + .unwrap(); let expected_unclaimed_rewards = vec![coin( // 200_000 s * 100 MARS/s * 1/10th of total deposit + 2_000_000 + @@ -461,6 +489,7 @@ fn balance_change_user_non_zero_balance() { }); let msg = ExecuteMsg::BalanceChange { user_addr: user_addr.clone(), + account_id: None, denom: "uosmo".to_string(), user_amount_scaled_before: user_balance, total_amount_scaled_before: total_supply, @@ -484,7 +513,7 @@ fn balance_change_user_non_zero_balance() { .unwrap(); assert_eq!( res.events[0].attributes, - vec![attr("action", "balance_change"), attr("denom", denom), attr("user", "user"),] + vec![attr("action", "balance_change"), attr("denom", denom), attr("user", "user")] ); assert_eq!( res.events[1].attributes, @@ -504,13 +533,14 @@ fn balance_change_user_non_zero_balance() { assert_eq!(asset_incentive.last_updated, expected_time_last_updated); // user index is set to asset's index - let user_asset_index = - USER_ASSET_INDICES.load(deps.as_ref().storage, (&user_addr, denom, "umars")).unwrap(); + let user_asset_index = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_addr, ""), denom, "umars")) + .unwrap(); assert_eq!(user_asset_index, expected_asset_incentive_index); // user gets new rewards let user_unclaimed_rewards = USER_UNCLAIMED_REWARDS - .load(deps.as_ref().storage, (&user_addr, denom, "umars")) + .load(deps.as_ref().storage, ((&user_addr, ""), denom, "umars")) .unwrap(); expected_accumulated_rewards += expected_accrued_rewards; assert_eq!(user_unclaimed_rewards, expected_accumulated_rewards) @@ -527,6 +557,7 @@ fn balance_change_user_non_zero_balance() { }); let msg = ExecuteMsg::BalanceChange { user_addr: user_addr.clone(), + account_id: None, denom: "uosmo".to_string(), user_amount_scaled_before: user_balance, total_amount_scaled_before: total_supply, @@ -551,7 +582,7 @@ fn balance_change_user_non_zero_balance() { .unwrap(); assert_eq!( res.events[0].attributes, - vec![attr("action", "balance_change"), attr("denom", denom), attr("user", "user"),] + vec![attr("action", "balance_change"), attr("denom", denom), attr("user", "user")] ); assert_eq!( res.events[1].attributes, @@ -571,13 +602,14 @@ fn balance_change_user_non_zero_balance() { assert_eq!(asset_incentive.last_updated, expected_time_last_updated); // user index is set to asset's index - let user_asset_index = - USER_ASSET_INDICES.load(deps.as_ref().storage, (&user_addr, denom, "umars")).unwrap(); + let user_asset_index = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_addr, ""), denom, "umars")) + .unwrap(); assert_eq!(user_asset_index, expected_asset_incentive_index); // user gets new rewards let user_unclaimed_rewards = USER_UNCLAIMED_REWARDS - .load(deps.as_ref().storage, (&user_addr, denom, "umars")) + .load(deps.as_ref().storage, ((&user_addr, ""), denom, "umars")) .unwrap(); expected_accumulated_rewards += expected_accrued_rewards; assert_eq!(user_unclaimed_rewards, expected_accumulated_rewards) @@ -594,6 +626,7 @@ fn balance_change_user_non_zero_balance() { }); let msg = ExecuteMsg::BalanceChange { user_addr: user_addr.clone(), + account_id: None, denom: "uosmo".to_string(), user_amount_scaled_before: user_balance, total_amount_scaled_before: total_supply, @@ -602,7 +635,7 @@ fn balance_change_user_non_zero_balance() { assert_eq!( res.events[0].attributes, - vec![attr("action", "balance_change"), attr("denom", denom), attr("user", "user"),] + vec![attr("action", "balance_change"), attr("denom", denom), attr("user", "user")] ); assert_eq!( res.events[1].attributes, @@ -620,14 +653,117 @@ fn balance_change_user_non_zero_balance() { assert_eq!(asset_incentive.last_updated, expected_time_last_updated); // user index is still the same - let user_asset_index = - USER_ASSET_INDICES.load(deps.as_ref().storage, (&user_addr, denom, "umars")).unwrap(); + let user_asset_index = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_addr, ""), denom, "umars")) + .unwrap(); assert_eq!(user_asset_index, expected_asset_incentive_index); // user gets no new rewards let user_unclaimed_rewards = USER_UNCLAIMED_REWARDS - .load(deps.as_ref().storage, (&user_addr, denom, "umars")) + .load(deps.as_ref().storage, ((&user_addr, ""), denom, "umars")) .unwrap(); assert_eq!(user_unclaimed_rewards, expected_accumulated_rewards) } } + +#[test] +fn balance_change_for_credit_account_id_with_non_zero_balance() { + let env = mock_env(); + let mut deps = ths_setup_with_epoch_duration(env, 8640000); + let denom = "uosmo"; + let user_addr = Addr::unchecked("credit_manager"); + let account_id = "random_account_id"; + + let emission_per_second = Uint128::new(100); + let total_supply = Uint128::new(100_000); + + let mut expected_asset_incentive_index = Decimal::from_ratio(1_u128, 2_u128); + let mut expected_time_last_updated = 500_000_u64; + let mut expected_accumulated_rewards = Uint128::zero(); + + INCENTIVE_STATES + .save( + deps.as_mut().storage, + (denom, "umars"), + &IncentiveState { + index: expected_asset_incentive_index, + last_updated: expected_time_last_updated, + }, + ) + .unwrap(); + EMISSIONS + .save( + deps.as_mut().storage, + (denom, "umars", expected_time_last_updated), + &emission_per_second, + ) + .unwrap(); + + let info = mock_info("red_bank", &[]); + + let time_contract_call = 600_000_u64; + let user_balance = Uint128::new(10_000); + + let env = mars_testing::mock_env(MockEnvParams { + block_time: Timestamp::from_seconds(time_contract_call), + ..Default::default() + }); + let msg = ExecuteMsg::BalanceChange { + user_addr: user_addr.clone(), + account_id: Some(account_id.to_string()), + denom: "uosmo".to_string(), + user_amount_scaled_before: user_balance, + total_amount_scaled_before: total_supply, + }; + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + + expected_asset_incentive_index = compute_incentive_index( + expected_asset_incentive_index, + emission_per_second, + total_supply, + expected_time_last_updated, + time_contract_call, + ) + .unwrap(); + + let expected_accrued_rewards = + compute_user_accrued_rewards(user_balance, Decimal::zero(), expected_asset_incentive_index) + .unwrap(); + assert_eq!( + res.events[0].attributes, + vec![ + attr("action", "balance_change"), + attr("denom", denom), + attr("user", "credit_manager"), + attr("account_id", account_id) + ] + ); + assert_eq!( + res.events[1].attributes, + vec![ + attr("incentive_denom", "umars"), + attr("rewards_accrued", expected_accrued_rewards), + attr("asset_index", expected_asset_incentive_index.to_string()) + ] + ); + + // asset incentive gets updated + expected_time_last_updated = time_contract_call; + + let asset_incentive = INCENTIVE_STATES.load(deps.as_ref().storage, (denom, "umars")).unwrap(); + assert_eq!(asset_incentive.index, expected_asset_incentive_index); + assert_eq!(asset_incentive.last_updated, expected_time_last_updated); + + // user index is set to asset's index + let user_asset_index = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_addr, account_id), denom, "umars")) + .unwrap(); + assert_eq!(user_asset_index, expected_asset_incentive_index); + + // user gets new rewards + let user_unclaimed_rewards = USER_UNCLAIMED_REWARDS + .load(deps.as_ref().storage, ((&user_addr, account_id), denom, "umars")) + .unwrap(); + expected_accumulated_rewards += expected_accrued_rewards; + assert_eq!(user_unclaimed_rewards, expected_accumulated_rewards) +} diff --git a/contracts/incentives/tests/test_claim_rewards.rs b/contracts/incentives/tests/test_claim_rewards.rs index 66c887556..8320d81ec 100644 --- a/contracts/incentives/tests/test_claim_rewards.rs +++ b/contracts/incentives/tests/test_claim_rewards.rs @@ -133,13 +133,13 @@ fn execute_claim_rewards() { // user indices USER_ASSET_INDICES - .save(deps.as_mut().storage, (&user_addr, asset_denom, "umars"), &Decimal::one()) + .save(deps.as_mut().storage, ((&user_addr, ""), asset_denom, "umars"), &Decimal::one()) .unwrap(); USER_ASSET_INDICES .save( deps.as_mut().storage, - (&user_addr, zero_denom, "umars"), + ((&user_addr, ""), zero_denom, "umars"), &Decimal::from_ratio(1_u128, 2_u128), ) .unwrap(); @@ -148,7 +148,7 @@ fn execute_claim_rewards() { USER_UNCLAIMED_REWARDS .save( deps.as_mut().storage, - (&user_addr, asset_denom, "umars"), + ((&user_addr, ""), asset_denom, "umars"), &previous_unclaimed_rewards, ) .unwrap(); @@ -186,6 +186,7 @@ fn execute_claim_rewards() { ..Default::default() }); let msg = ExecuteMsg::ClaimRewards { + account_id: None, start_after_collateral_denom: None, start_after_incentive_denom: None, limit: None, @@ -203,6 +204,7 @@ fn execute_claim_rewards() { None, None, None, + None, ) .unwrap(); assert!(rewards_query_before.len() == 1); @@ -216,6 +218,7 @@ fn execute_claim_rewards() { None, None, None, + None, ) .unwrap(); assert_eq!(rewards_query[0].amount, expected_accrued_rewards); @@ -223,9 +226,16 @@ fn execute_claim_rewards() { let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); // query after execution gives 0 rewards - let rewards_query_after = - query_user_unclaimed_rewards(deps.as_ref(), env, String::from("user"), None, None, None) - .unwrap(); + let rewards_query_after = query_user_unclaimed_rewards( + deps.as_ref(), + env, + String::from("user"), + None, + None, + None, + None, + ) + .unwrap(); assert_eq!(rewards_query_after[0].amount, Uint128::zero()); // ASSERT @@ -240,7 +250,7 @@ fn execute_claim_rewards() { assert_eq!( res.events[0].attributes, - vec![attr("action", "claim_rewards"), attr("user", "user"),] + vec![attr("action", "claim_rewards"), attr("user", "user")] ); assert_eq!( res.events[1].attributes, @@ -263,23 +273,25 @@ fn execute_claim_rewards() { assert_eq!(no_user_incentive.last_updated, time_start); // user's asset and zero indices are updated - let user_asset_index = - USER_ASSET_INDICES.load(deps.as_ref().storage, (&user_addr, asset_denom, "umars")).unwrap(); + let user_asset_index = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_addr, ""), asset_denom, "umars")) + .unwrap(); assert_eq!(user_asset_index, expected_asset_incentive_index); - let user_zero_index = - USER_ASSET_INDICES.load(deps.as_ref().storage, (&user_addr, zero_denom, "umars")).unwrap(); + let user_zero_index = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_addr, ""), zero_denom, "umars")) + .unwrap(); assert_eq!(user_zero_index, Decimal::one()); // user's no_user does not get updated let user_no_user_index = USER_ASSET_INDICES - .may_load(deps.as_ref().storage, (&user_addr, no_user_denom, "umars")) + .may_load(deps.as_ref().storage, ((&user_addr, ""), no_user_denom, "umars")) .unwrap(); assert_eq!(user_no_user_index, None); // user rewards are cleared let user_unclaimed_rewards = USER_UNCLAIMED_REWARDS - .load(deps.as_ref().storage, (&user_addr, asset_denom, "umars")) + .load(deps.as_ref().storage, ((&user_addr, ""), asset_denom, "umars")) .unwrap(); assert_eq!(user_unclaimed_rewards, Uint128::zero()) } @@ -291,6 +303,7 @@ fn claim_zero_rewards() { let info = mock_info("user", &[]); let msg = ExecuteMsg::ClaimRewards { + account_id: None, start_after_collateral_denom: None, start_after_incentive_denom: None, limit: None, @@ -300,6 +313,6 @@ fn claim_zero_rewards() { assert_eq!(res.messages.len(), 0); assert_eq!( res.events[0].attributes, - vec![attr("action", "claim_rewards"), attr("user", "user"),] + vec![attr("action", "claim_rewards"), attr("user", "user")] ); } diff --git a/contracts/incentives/tests/test_whitelist.rs b/contracts/incentives/tests/test_whitelist.rs index 6424c5ab6..959e1d09b 100644 --- a/contracts/incentives/tests/test_whitelist.rs +++ b/contracts/incentives/tests/test_whitelist.rs @@ -210,6 +210,7 @@ fn incentives_updated_and_removed_when_removing_from_whitelist() { env.clone(), mock_info("red_bank", &[]), user_addr.clone(), + None, "uosmo".to_string(), Uint128::zero(), Uint128::zero(), @@ -236,6 +237,7 @@ fn incentives_updated_and_removed_when_removing_from_whitelist() { env.clone(), QueryMsg::UserUnclaimedRewards { user: user_addr.to_string(), + account_id: None, start_after_collateral_denom: None, start_after_incentive_denom: None, limit: None, @@ -255,6 +257,7 @@ fn incentives_updated_and_removed_when_removing_from_whitelist() { env, QueryMsg::UserUnclaimedRewards { user: user_addr.to_string(), + account_id: None, start_after_collateral_denom: None, start_after_incentive_denom: None, limit: None, diff --git a/contracts/red-bank/src/collateral.rs b/contracts/red-bank/src/collateral.rs index b9aa62dfe..f2ef6c7f2 100644 --- a/contracts/red-bank/src/collateral.rs +++ b/contracts/red-bank/src/collateral.rs @@ -21,18 +21,17 @@ pub fn update_asset_collateral_status( ) -> Result { let user = User(&info.sender); - let mut collateral = - COLLATERALS.may_load(deps.storage, (user.address(), &denom))?.ok_or_else(|| { - ContractError::UserNoCollateralBalance { - user: user.into(), - denom: denom.clone(), - } + let mut collateral = COLLATERALS + .may_load(deps.storage, (user.address(), "", &denom))? + .ok_or_else(|| ContractError::UserNoCollateralBalance { + user: user.into(), + denom: denom.clone(), })?; let previously_enabled = collateral.enabled; collateral.enabled = enable; - COLLATERALS.save(deps.storage, (user.address(), &denom), &collateral)?; + COLLATERALS.save(deps.storage, (user.address(), "", &denom), &collateral)?; // if the collateral was previously enabled, but is not disabled, it is necessary to ensure the // user is not liquidatable after disabling diff --git a/contracts/red-bank/src/contract.rs b/contracts/red-bank/src/contract.rs index 6d9154049..052e96c05 100644 --- a/contracts/red-bank/src/contract.rs +++ b/contracts/red-bank/src/contract.rs @@ -46,17 +46,20 @@ pub fn execute( deps, info, user_addr, denom, new_limit, ) } - ExecuteMsg::Deposit {} => { + ExecuteMsg::Deposit { + account_id, + } => { let sent_coin = cw_utils::one_coin(&info)?; - deposit::deposit(deps, env, info, sent_coin.denom, sent_coin.amount) + deposit::deposit(deps, env, info, sent_coin.denom, sent_coin.amount, account_id) } ExecuteMsg::Withdraw { denom, amount, recipient, + account_id, } => { cw_utils::nonpayable(&info)?; - withdraw::withdraw(deps, env, info, denom, amount, recipient) + withdraw::withdraw(deps, env, info, denom, amount, recipient, account_id) } ExecuteMsg::Borrow { denom, @@ -148,13 +151,17 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { let user_addr = deps.api.addr_validate(&user)?; - to_binary(&query::query_user_collateral(deps, &env.block, user_addr, denom)?) + to_binary(&query::query_user_collateral( + deps, &env.block, user_addr, account_id, denom, + )?) } QueryMsg::UserCollaterals { user, + account_id, start_after, limit, } => { @@ -163,6 +170,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result, ) -> Result { let mut market = MARKETS.load(deps.storage, &denom)?; @@ -77,6 +78,7 @@ pub fn deposit( deposit_amount_scaled, incentives_addr, response, + account_id, )?; market.increase_collateral(deposit_amount_scaled)?; diff --git a/contracts/red-bank/src/health.rs b/contracts/red-bank/src/health.rs index 1793cbc6b..cf9bc41df 100644 --- a/contracts/red-bank/src/health.rs +++ b/contracts/red-bank/src/health.rs @@ -117,7 +117,7 @@ pub fn get_user_positions_map( // Find all denoms that the user has a collateral or debt position in let collateral_denoms = COLLATERALS - .prefix(user_addr) + .prefix((user_addr, "")) .keys(deps.storage, None, None, Order::Ascending) .collect::>>()?; let debt_denoms = DEBTS @@ -138,13 +138,14 @@ pub fn get_user_positions_map( let market = MARKETS.load(deps.storage, &denom)?; let params = query_asset_params(&deps.querier, params_addr, &denom)?; - let collateral_amount = match COLLATERALS.may_load(deps.storage, (user_addr, &denom))? { - Some(collateral) if collateral.enabled => { - let amount_scaled = collateral.amount_scaled; - get_underlying_liquidity_amount(amount_scaled, &market, block_time)? - } - _ => Uint128::zero(), - }; + let collateral_amount = + match COLLATERALS.may_load(deps.storage, (user_addr, "", &denom))? { + Some(collateral) if collateral.enabled => { + let amount_scaled = collateral.amount_scaled; + get_underlying_liquidity_amount(amount_scaled, &market, block_time)? + } + _ => Uint128::zero(), + }; let (debt_amount, uncollateralized_debt) = match DEBTS.may_load(deps.storage, (user_addr, &denom))? { diff --git a/contracts/red-bank/src/interest_rates.rs b/contracts/red-bank/src/interest_rates.rs index ce01a6a4f..43359df6f 100644 --- a/contracts/red-bank/src/interest_rates.rs +++ b/contracts/red-bank/src/interest_rates.rs @@ -88,6 +88,7 @@ pub fn apply_accumulated_interests( reward_amount_scaled, incentives_addr, response, + None, )?; market.increase_collateral(reward_amount_scaled)?; } diff --git a/contracts/red-bank/src/liquidate.rs b/contracts/red-bank/src/liquidate.rs index 17ac92393..93ef31b24 100644 --- a/contracts/red-bank/src/liquidate.rs +++ b/contracts/red-bank/src/liquidate.rs @@ -49,7 +49,7 @@ pub fn liquidate( // check if the user has enabled the collateral asset as collateral let user_collateral = COLLATERALS - .may_load(deps.storage, (&liquidatee_addr, &collateral_denom))? + .may_load(deps.storage, (&liquidatee_addr, "", &collateral_denom))? .ok_or(ContractError::CannotLiquidateWhenNoCollateralBalance {})?; if !user_collateral.enabled { return Err(ContractError::CannotLiquidateWhenCollateralUnset { @@ -159,6 +159,7 @@ pub fn liquidate( collateral_amount_to_liquidate_scaled, incentives_addr, response, + None, )?; response = recipient.increase_collateral( deps.storage, @@ -166,6 +167,7 @@ pub fn liquidate( collateral_amount_received_by_liquidator_scaled, incentives_addr, response, + None, )?; if !protocol_fee.is_zero() { response = User(rewards_collector_addr).increase_collateral( @@ -174,6 +176,7 @@ pub fn liquidate( protocol_fee_scaled, incentives_addr, response, + None, )?; } diff --git a/contracts/red-bank/src/query.rs b/contracts/red-bank/src/query.rs index dbb9698c2..118e00688 100644 --- a/contracts/red-bank/src/query.rs +++ b/contracts/red-bank/src/query.rs @@ -151,12 +151,15 @@ pub fn query_user_collateral( deps: Deps, block: &BlockInfo, user_addr: Addr, + account_id: Option, denom: String, ) -> StdResult { + let acc_id = account_id.unwrap_or("".to_string()); + let Collateral { amount_scaled, enabled, - } = COLLATERALS.may_load(deps.storage, (&user_addr, &denom))?.unwrap_or_default(); + } = COLLATERALS.may_load(deps.storage, (&user_addr, &acc_id, &denom))?.unwrap_or_default(); let block_time = block.time.seconds(); let market = MARKETS.load(deps.storage, &denom)?; @@ -174,6 +177,7 @@ pub fn query_user_collaterals( deps: Deps, block: &BlockInfo, user_addr: Addr, + account_id: Option, start_after: Option, limit: Option, ) -> StdResult> { @@ -182,8 +186,10 @@ pub fn query_user_collaterals( let start = start_after.map(|denom| Bound::ExclusiveRaw(denom.into_bytes())); let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let acc_id = account_id.unwrap_or("".to_string()); + COLLATERALS - .prefix(&user_addr) + .prefix((&user_addr, &acc_id)) .range(deps.storage, start, None, Order::Ascending) .take(limit) .map(|item| { diff --git a/contracts/red-bank/src/state.rs b/contracts/red-bank/src/state.rs index 4eaf0be3b..075eb7c01 100644 --- a/contracts/red-bank/src/state.rs +++ b/contracts/red-bank/src/state.rs @@ -6,6 +6,7 @@ use mars_red_bank_types::red_bank::{Collateral, Config, Debt, Market}; pub const OWNER: Owner = Owner::new("owner"); pub const CONFIG: Item> = Item::new("config"); pub const MARKETS: Map<&str, Market> = Map::new("markets"); -pub const COLLATERALS: Map<(&Addr, &str), Collateral> = Map::new("collaterals"); +/// The key is: user address, account id (if any), collateral denom +pub const COLLATERALS: Map<(&Addr, &str, &str), Collateral> = Map::new("collaterals"); pub const DEBTS: Map<(&Addr, &str), Debt> = Map::new("debts"); pub const UNCOLLATERALIZED_LOAN_LIMITS: Map<(&Addr, &str), Uint128> = Map::new("limits"); diff --git a/contracts/red-bank/src/user.rs b/contracts/red-bank/src/user.rs index b8e7dd4b1..dbee6f8df 100644 --- a/contracts/red-bank/src/user.rs +++ b/contracts/red-bank/src/user.rs @@ -48,8 +48,14 @@ impl<'a> User<'a> { } /// Load the user's collateral - pub fn collateral(&self, store: &dyn Storage, denom: &str) -> StdResult { - COLLATERALS.load(store, (self.0, denom)) + pub fn collateral( + &self, + store: &dyn Storage, + denom: &str, + account_id: Option, + ) -> StdResult { + let acc_id = account_id.unwrap_or("".to_string()); + COLLATERALS.load(store, (self.0, &acc_id, denom)) } /// Load the user's debt @@ -102,10 +108,13 @@ impl<'a> User<'a> { amount_scaled: Uint128, incentives_addr: &Addr, response: Response, + account_id: Option, ) -> StdResult { + let acc_id = account_id.clone().unwrap_or("".to_string()); + let mut amount_scaled_before = Uint128::zero(); - COLLATERALS.update(store, (self.0, &market.denom), |opt| -> StdResult<_> { + COLLATERALS.update(store, (self.0, &acc_id, &market.denom), |opt| -> StdResult<_> { match opt { Some(mut col) => { amount_scaled_before = col.amount_scaled; @@ -123,6 +132,7 @@ impl<'a> User<'a> { incentives_addr, market, amount_scaled_before, + account_id, )?; Ok(response.add_message(msg)) @@ -141,22 +151,26 @@ impl<'a> User<'a> { amount_scaled: Uint128, incentives_addr: &Addr, response: Response, + account_id: Option, ) -> StdResult { - let mut collateral = COLLATERALS.load(store, (self.0, &market.denom))?; + let acc_id = account_id.clone().unwrap_or("".to_string()); + + let mut collateral = COLLATERALS.load(store, (self.0, &acc_id, &market.denom))?; let amount_scaled_before = collateral.amount_scaled; collateral.amount_scaled = collateral.amount_scaled.checked_sub(amount_scaled)?; if collateral.amount_scaled.is_zero() { - COLLATERALS.remove(store, (self.0, &market.denom)); + COLLATERALS.remove(store, (self.0, &acc_id, &market.denom)); } else { - COLLATERALS.save(store, (self.0, &market.denom), &collateral)?; + COLLATERALS.save(store, (self.0, &acc_id, &market.denom), &collateral)?; } let msg = self.build_incentives_balance_changed_msg( incentives_addr, market, amount_scaled_before, + account_id, )?; Ok(response.add_message(msg)) @@ -171,11 +185,13 @@ impl<'a> User<'a> { incentives_addr: &Addr, market: &Market, user_amount_scaled_before: Uint128, + account_id: Option, ) -> StdResult { Ok(WasmMsg::Execute { contract_addr: incentives_addr.into(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: self.address().clone(), + account_id, denom: market.denom.clone(), user_amount_scaled_before, total_amount_scaled_before: market.collateral_total_scaled, diff --git a/contracts/red-bank/src/withdraw.rs b/contracts/red-bank/src/withdraw.rs index 48e94bb9c..66e32774f 100644 --- a/contracts/red-bank/src/withdraw.rs +++ b/contracts/red-bank/src/withdraw.rs @@ -20,12 +20,13 @@ pub fn withdraw( denom: String, amount: Option, recipient: Option, + account_id: Option, ) -> Result { let withdrawer = User(&info.sender); let mut market = MARKETS.load(deps.storage, &denom)?; - let collateral = withdrawer.collateral(deps.storage, &denom)?; + let collateral = withdrawer.collateral(deps.storage, &denom, account_id.clone())?; let withdrawer_balance_scaled_before = collateral.amount_scaled; if withdrawer_balance_scaled_before.is_zero() { @@ -113,6 +114,7 @@ pub fn withdraw( withdraw_amount_scaled, incentives_addr, response, + account_id, )?; market.decrease_collateral(withdraw_amount_scaled)?; diff --git a/contracts/red-bank/tests/helpers.rs b/contracts/red-bank/tests/helpers.rs index 8f999fbfd..8f112540b 100644 --- a/contracts/red-bank/tests/helpers.rs +++ b/contracts/red-bank/tests/helpers.rs @@ -37,11 +37,11 @@ pub fn set_collateral( amount_scaled, enabled, }; - COLLATERALS.save(deps.storage, (user_addr, denom), &collateral).unwrap(); + COLLATERALS.save(deps.storage, (user_addr, "", denom), &collateral).unwrap(); } pub fn unset_collateral(deps: DepsMut, user_addr: &Addr, denom: &str) { - COLLATERALS.remove(deps.storage, (user_addr, denom)); + COLLATERALS.remove(deps.storage, (user_addr, "", denom)); } pub fn set_debt( @@ -65,13 +65,13 @@ pub fn has_debt_position(deps: Deps, user_addr: &Addr, denom: &str) -> bool { /// Find if a user has a collateral position in the specified asset, regardless of whether enabled pub fn has_collateral_position(deps: Deps, user_addr: &Addr, denom: &str) -> bool { - COLLATERALS.may_load(deps.storage, (user_addr, denom)).unwrap().is_some() + COLLATERALS.may_load(deps.storage, (user_addr, "", denom)).unwrap().is_some() } /// Find whether a user has a collateral position AND has it enabled in the specified asset pub fn has_collateral_enabled(deps: Deps, user_addr: &Addr, denom: &str) -> bool { COLLATERALS - .may_load(deps.storage, (user_addr, denom)) + .may_load(deps.storage, (user_addr, "", denom)) .unwrap() .map(|collateral| collateral.enabled) .unwrap_or(false) diff --git a/contracts/red-bank/tests/test_admin.rs b/contracts/red-bank/tests/test_admin.rs index 66c921311..03e3f6a7f 100644 --- a/contracts/red-bank/tests/test_admin.rs +++ b/contracts/red-bank/tests/test_admin.rs @@ -653,7 +653,7 @@ fn update_asset_new_reserve_factor_accrues_interest_rate() { let collateral = COLLATERALS .load( deps.as_ref().storage, - (&Addr::unchecked(MarsAddressType::RewardsCollector.to_string()), "somecoin"), + (&Addr::unchecked(MarsAddressType::RewardsCollector.to_string()), "", "somecoin"), ) .unwrap(); assert_eq!(collateral.amount_scaled, expected_rewards_scaled); diff --git a/contracts/red-bank/tests/test_deposit.rs b/contracts/red-bank/tests/test_deposit.rs index 7f77e71d0..d5e83d4e9 100644 --- a/contracts/red-bank/tests/test_deposit.rs +++ b/contracts/red-bank/tests/test_deposit.rs @@ -100,7 +100,9 @@ fn depositing_with_no_coin_sent() { deps.as_mut(), mock_env(), mock_info(depositor_addr.as_str(), &[]), - ExecuteMsg::Deposit {}, + ExecuteMsg::Deposit { + account_id: None, + }, ) .unwrap_err(); assert_eq!(err, PaymentError::NoFunds {}.into()); @@ -120,7 +122,9 @@ fn depositing_with_multiple_coins_sent() { deps.as_mut(), mock_env(), mock_info(depositor_addr.as_str(), &sent_coins), - ExecuteMsg::Deposit {}, + ExecuteMsg::Deposit { + account_id: None, + }, ) .unwrap_err(); assert_eq!(err, PaymentError::MultipleDenoms {}.into()); @@ -141,7 +145,9 @@ fn depositing_to_non_existent_market() { deps.as_mut(), mock_env(), mock_info(depositor_addr.as_str(), &coins(123, false_denom)), - ExecuteMsg::Deposit {}, + ExecuteMsg::Deposit { + account_id: None, + }, ) .unwrap_err(); assert_eq!(err, StdError::not_found(type_name::()).into()); @@ -177,7 +183,9 @@ fn depositing_to_disabled_market() { deps.as_mut(), mock_env(), mock_info(depositor_addr.as_str(), &coins(123, denom)), - ExecuteMsg::Deposit {}, + ExecuteMsg::Deposit { + account_id: None, + }, ) .unwrap_err(); assert_eq!( @@ -226,7 +234,9 @@ fn depositing_above_cap() { deps.as_mut(), mock_env_at_block_time(10000100), mock_info(depositor_addr.as_str(), &coins(1_000_001, denom)), - ExecuteMsg::Deposit {}, + ExecuteMsg::Deposit { + account_id: None, + }, ) .unwrap_err(); assert_eq!( @@ -241,7 +251,9 @@ fn depositing_above_cap() { deps.as_mut(), mock_env_at_block_time(10000100), mock_info(depositor_addr.as_str(), &coins(123, denom)), - ExecuteMsg::Deposit {}, + ExecuteMsg::Deposit { + account_id: None, + }, ); assert!(result.is_ok()); } @@ -272,7 +284,9 @@ fn depositing_without_existing_position() { deps.as_mut(), mock_env_at_block_time(block_time), mock_info(depositor_addr.as_str(), &coins(deposit_amount, denom)), - ExecuteMsg::Deposit {}, + ExecuteMsg::Deposit { + account_id: None, + }, ) .unwrap(); @@ -285,6 +299,7 @@ fn depositing_without_existing_position() { contract_addr: MarsAddressType::Incentives.to_string(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: depositor_addr.clone(), + account_id: None, denom: initial_market.denom.clone(), user_amount_scaled_before: Uint128::zero(), // NOTE: Protocol rewards accrued is zero, so here it's initial total supply @@ -319,7 +334,7 @@ fn depositing_without_existing_position() { // the depositor previously did not have a collateral position // a position should have been created with the correct scaled amount, and enabled by default - let collateral = COLLATERALS.load(deps.as_ref().storage, (&depositor_addr, denom)).unwrap(); + let collateral = COLLATERALS.load(deps.as_ref().storage, (&depositor_addr, "", denom)).unwrap(); assert_eq!( collateral, Collateral { @@ -359,7 +374,9 @@ fn depositing_with_existing_position() { deps.as_mut(), mock_env_at_block_time(block_time), mock_info(depositor_addr.as_str(), &coins(deposit_amount, denom)), - ExecuteMsg::Deposit {}, + ExecuteMsg::Deposit { + account_id: None, + }, ) .unwrap(); @@ -372,6 +389,7 @@ fn depositing_with_existing_position() { contract_addr: MarsAddressType::Incentives.to_string(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: depositor_addr.clone(), + account_id: None, denom: initial_market.denom.clone(), user_amount_scaled_before: collateral_amount_scaled, // NOTE: Protocol rewards accrued is zero, so here it's initial total supply @@ -384,7 +402,7 @@ fn depositing_with_existing_position() { // the depositor's scaled collateral amount should have been increased // however, the `enabled` status should not been affected - let collateral = COLLATERALS.load(deps.as_ref().storage, (&depositor_addr, denom)).unwrap(); + let collateral = COLLATERALS.load(deps.as_ref().storage, (&depositor_addr, "", denom)).unwrap(); let expected = collateral_amount_scaled + expected_mint_amount; assert_eq!( collateral, @@ -394,61 +412,3 @@ fn depositing_with_existing_position() { } ); } - -#[test] -fn depositing_on_behalf_of_cannot_enable_collateral() { - let TestSuite { - mut deps, - denom, - depositor_addr, - .. - } = setup_test(); - - deps.querier.set_oracle_price(denom, Decimal::one()); - - let on_behalf_of_addr = Addr::unchecked("jake"); - - let block_time = 10000300; - - // 'on_behalf_of_addr' deposit funds to their own account - execute( - deps.as_mut(), - mock_env_at_block_time(block_time), - mock_info(on_behalf_of_addr.as_str(), &coins(1u128, denom)), - ExecuteMsg::Deposit {}, - ) - .unwrap(); - - // 'on_behalf_of_addr' should have collateral enabled - let collateral = COLLATERALS.load(deps.as_ref().storage, (&on_behalf_of_addr, denom)).unwrap(); - assert!(collateral.enabled); - - // 'on_behalf_of_addr' disables asset as collateral - execute( - deps.as_mut(), - mock_env_at_block_time(block_time), - mock_info(on_behalf_of_addr.as_str(), &[]), - ExecuteMsg::UpdateAssetCollateralStatus { - denom: denom.to_string(), - enable: false, - }, - ) - .unwrap(); - - // verify asset is disabled as collateral for 'on_behalf_of_addr' - let collateral = COLLATERALS.load(deps.as_ref().storage, (&on_behalf_of_addr, denom)).unwrap(); - assert!(!collateral.enabled); - - // 'depositor_addr' deposits a small amount of funds to 'on_behalf_of_addr' to enable his asset as collateral - execute( - deps.as_mut(), - mock_env_at_block_time(block_time), - mock_info(depositor_addr.as_str(), &coins(1u128, denom)), - ExecuteMsg::Deposit {}, - ) - .unwrap(); - - // 'on_behalf_of_addr' doesn't have the asset enabled as collateral - let collateral = COLLATERALS.load(deps.as_ref().storage, (&on_behalf_of_addr, denom)).unwrap(); - assert!(!collateral.enabled); -} diff --git a/contracts/red-bank/tests/test_liquidate.rs b/contracts/red-bank/tests/test_liquidate.rs index 06e268562..ed9efcdf6 100644 --- a/contracts/red-bank/tests/test_liquidate.rs +++ b/contracts/red-bank/tests/test_liquidate.rs @@ -801,14 +801,18 @@ fn response_verification() { deps.as_mut(), env.clone(), mock_info(provider.as_str(), &[coin(1000000, "uusdc")]), - ExecuteMsg::Deposit {}, + ExecuteMsg::Deposit { + account_id: None, + }, ) .unwrap(); execute( deps.as_mut(), env.clone(), mock_info(provider.as_str(), &[coin(1000000, "untrn")]), - ExecuteMsg::Deposit {}, + ExecuteMsg::Deposit { + account_id: None, + }, ) .unwrap(); @@ -817,14 +821,18 @@ fn response_verification() { deps.as_mut(), env.clone(), mock_info(liquidatee.as_str(), &[coin(10000, "uosmo")]), - ExecuteMsg::Deposit {}, + ExecuteMsg::Deposit { + account_id: None, + }, ) .unwrap(); execute( deps.as_mut(), env.clone(), mock_info(liquidatee.as_str(), &[coin(900, "uatom")]), - ExecuteMsg::Deposit {}, + ExecuteMsg::Deposit { + account_id: None, + }, ) .unwrap(); execute( @@ -869,6 +877,7 @@ fn response_verification() { deps.as_ref(), QueryMsg::UserCollateral { user: liquidatee.to_string(), + account_id: None, denom: "uosmo".to_string(), }, ); @@ -980,6 +989,7 @@ fn expected_messages( contract_addr: MarsAddressType::Incentives.to_string(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: user_addr.clone(), + account_id: None, denom: collateral_market.denom.clone(), user_amount_scaled_before: user_collateral_scaled, total_amount_scaled_before: collateral_market.collateral_total_scaled, @@ -991,6 +1001,7 @@ fn expected_messages( contract_addr: MarsAddressType::Incentives.to_string(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: recipient_addr.clone(), + account_id: None, denom: collateral_market.denom.clone(), user_amount_scaled_before: recipient_collateral_scaled, total_amount_scaled_before: collateral_market.collateral_total_scaled, @@ -1002,6 +1013,7 @@ fn expected_messages( contract_addr: MarsAddressType::Incentives.to_string(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: Addr::unchecked(MarsAddressType::RewardsCollector.to_string()), + account_id: None, denom: collateral_market.denom.clone(), user_amount_scaled_before: Uint128::zero(), total_amount_scaled_before: collateral_market.collateral_total_scaled, @@ -1013,6 +1025,7 @@ fn expected_messages( contract_addr: MarsAddressType::Incentives.to_string(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: Addr::unchecked(MarsAddressType::RewardsCollector.to_string()), + account_id: None, denom: debt_market.denom.clone(), user_amount_scaled_before: Uint128::zero(), total_amount_scaled_before: debt_market.collateral_total_scaled, diff --git a/contracts/red-bank/tests/test_payment.rs b/contracts/red-bank/tests/test_payment.rs index 588997c7a..b3ef8dde7 100644 --- a/contracts/red-bank/tests/test_payment.rs +++ b/contracts/red-bank/tests/test_payment.rs @@ -30,6 +30,7 @@ fn rejecting_unexpected_payments() { denom: "".into(), amount: None, recipient: None, + account_id: None, }, ) .unwrap_err(); diff --git a/contracts/red-bank/tests/test_query.rs b/contracts/red-bank/tests/test_query.rs index 8539dccaf..80bd3b58d 100644 --- a/contracts/red-bank/tests/test_query.rs +++ b/contracts/red-bank/tests/test_query.rs @@ -31,7 +31,8 @@ fn query_collateral() { // Assert markets correctly return collateral status let collaterals = - query_user_collaterals(deps.as_ref(), &env.block, user_addr.clone(), None, None).unwrap(); + query_user_collaterals(deps.as_ref(), &env.block, user_addr.clone(), None, None, None) + .unwrap(); assert_eq!( collaterals, vec![UserCollateralResponse { @@ -47,7 +48,7 @@ fn query_collateral() { // Assert markets correctly return collateral status let collaterals = - query_user_collaterals(deps.as_ref(), &env.block, user_addr, None, None).unwrap(); + query_user_collaterals(deps.as_ref(), &env.block, user_addr, None, None, None).unwrap(); assert_eq!( collaterals, vec![ diff --git a/contracts/red-bank/tests/test_withdraw.rs b/contracts/red-bank/tests/test_withdraw.rs index 1020d6698..7f88ba5cf 100644 --- a/contracts/red-bank/tests/test_withdraw.rs +++ b/contracts/red-bank/tests/test_withdraw.rs @@ -85,6 +85,7 @@ fn withdrawing_more_than_balance() { denom: denom.to_string(), amount: Some(Uint128::from(2000u128)), recipient: None, + account_id: None, }, ) .unwrap_err(); @@ -128,6 +129,7 @@ fn withdrawing_partially() { denom: denom.to_string(), amount: Some(withdraw_amount), recipient: None, + account_id: None, }, ) .unwrap(); @@ -181,6 +183,7 @@ fn withdrawing_partially() { contract_addr: MarsAddressType::Incentives.to_string(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: Addr::unchecked(MarsAddressType::RewardsCollector.to_string()), + account_id: None, denom: denom.to_string(), user_amount_scaled_before: Uint128::zero(), total_amount_scaled_before: initial_market.collateral_total_scaled, @@ -192,6 +195,7 @@ fn withdrawing_partially() { contract_addr: MarsAddressType::Incentives.to_string(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: withdrawer_addr.clone(), + account_id: None, denom: denom.to_string(), user_amount_scaled_before: initial_deposit_amount_scaled, total_amount_scaled_before: initial_market.collateral_total_scaled @@ -229,12 +233,13 @@ fn withdrawing_partially() { assert_eq!(market.collateral_total_scaled, expected_total_collateral_amount_scaled); // the user's collateral scaled amount should have been decreased - let collateral = COLLATERALS.load(deps.as_ref().storage, (&withdrawer_addr, denom)).unwrap(); + let collateral = + COLLATERALS.load(deps.as_ref().storage, (&withdrawer_addr, "", denom)).unwrap(); assert_eq!(collateral.amount_scaled, expected_withdraw_amount_scaled_remaining); // the reward collector's collateral scaled amount should have been increased let rewards_addr = Addr::unchecked(MarsAddressType::RewardsCollector.to_string()); - let collateral = COLLATERALS.load(deps.as_ref().storage, (&rewards_addr, denom)).unwrap(); + let collateral = COLLATERALS.load(deps.as_ref().storage, (&rewards_addr, "", denom)).unwrap(); assert_eq!(collateral.amount_scaled, expected_rewards_amount_scaled); } @@ -261,6 +266,7 @@ fn withdrawing_completely() { denom: denom.to_string(), amount: None, recipient: None, + account_id: None, }, ) .unwrap(); @@ -297,6 +303,7 @@ fn withdrawing_completely() { contract_addr: MarsAddressType::Incentives.to_string(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: Addr::unchecked(MarsAddressType::RewardsCollector.to_string()), + account_id: None, denom: denom.to_string(), user_amount_scaled_before: Uint128::zero(), total_amount_scaled_before: initial_market.collateral_total_scaled, @@ -308,6 +315,7 @@ fn withdrawing_completely() { contract_addr: MarsAddressType::Incentives.to_string(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: withdrawer_addr.clone(), + account_id: None, denom: denom.to_string(), user_amount_scaled_before: withdrawer_balance_scaled, total_amount_scaled_before: initial_market.collateral_total_scaled @@ -368,6 +376,7 @@ fn withdrawing_to_another_user() { denom: denom.to_string(), amount: None, recipient: Some(recipient_addr.to_string()), + account_id: None, }, ) .unwrap(); @@ -405,6 +414,7 @@ fn withdrawing_to_another_user() { contract_addr: MarsAddressType::Incentives.to_string(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: Addr::unchecked(MarsAddressType::RewardsCollector.to_string()), + account_id: None, denom: denom.to_string(), user_amount_scaled_before: Uint128::zero(), total_amount_scaled_before: initial_market.collateral_total_scaled, @@ -416,6 +426,7 @@ fn withdrawing_to_another_user() { contract_addr: MarsAddressType::Incentives.to_string(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: withdrawer_addr.clone(), + account_id: None, denom: denom.to_string(), user_amount_scaled_before: withdrawer_balance_scaled, total_amount_scaled_before: initial_market.collateral_total_scaled @@ -561,7 +572,9 @@ fn setup_health_check_test() -> HealthCheckTestSuite { denoms.iter().zip(collaterals.iter()).for_each(|(denom, collateral)| { if !collateral.amount_scaled.is_zero() { - COLLATERALS.save(deps.as_mut().storage, (&withdrawer_addr, denom), collateral).unwrap(); + COLLATERALS + .save(deps.as_mut().storage, (&withdrawer_addr, "", denom), collateral) + .unwrap(); } }); @@ -661,6 +674,7 @@ fn withdrawing_if_health_factor_not_met() { denom: denoms[2].to_string(), amount: Some(withdraw_amount), recipient: None, + account_id: None, }, ) .unwrap_err(); @@ -697,6 +711,7 @@ fn withdrawing_if_health_factor_met() { denom: denoms[2].to_string(), amount: Some(withdraw_amount), recipient: None, + account_id: None, }, ) .unwrap(); @@ -711,6 +726,7 @@ fn withdrawing_if_health_factor_met() { contract_addr: MarsAddressType::Incentives.to_string(), msg: to_binary(&incentives::ExecuteMsg::BalanceChange { user_addr: withdrawer_addr.clone(), + account_id: None, denom: denoms[2].to_string(), user_amount_scaled_before: collaterals[2].amount_scaled, // NOTE: Protocol rewards accrued is zero, so here it's initial total supply @@ -733,7 +749,7 @@ fn withdrawing_if_health_factor_met() { let expected_collateral_total_amount_scaled_after = markets[2].collateral_total_scaled - expected_withdraw_amount_scaled; - let col = COLLATERALS.load(deps.as_ref().storage, (&withdrawer_addr, denoms[2])).unwrap(); + let col = COLLATERALS.load(deps.as_ref().storage, (&withdrawer_addr, "", denoms[2])).unwrap(); assert_eq!(col.amount_scaled, expected_withdrawer_balance_after); let market = MARKETS.load(deps.as_ref().storage, denoms[2]).unwrap(); diff --git a/contracts/rewards-collector/src/contract.rs b/contracts/rewards-collector/src/contract.rs index d58da81e3..2f08d0feb 100644 --- a/contracts/rewards-collector/src/contract.rs +++ b/contracts/rewards-collector/src/contract.rs @@ -97,6 +97,7 @@ impl<'a> Collector<'a> { denom: denom.clone(), amount, recipient: None, + account_id: None, })?, funds: vec![], }); @@ -126,6 +127,7 @@ impl<'a> Collector<'a> { let claim_msg = CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: incentives_addr.to_string(), msg: to_binary(&incentives::ExecuteMsg::ClaimRewards { + account_id: None, start_after_collateral_denom, start_after_incentive_denom, limit, diff --git a/contracts/rewards-collector/tests/test_withdraw.rs b/contracts/rewards-collector/tests/test_withdraw.rs index 88ab1661e..c19297e1f 100644 --- a/contracts/rewards-collector/tests/test_withdraw.rs +++ b/contracts/rewards-collector/tests/test_withdraw.rs @@ -29,7 +29,8 @@ fn withdrawing_from_red_bank() { msg: to_binary(&mars_red_bank_types::red_bank::ExecuteMsg::Withdraw { denom: "uatom".to_string(), amount: Some(Uint128::new(42069)), - recipient: None + recipient: None, + account_id: None }) .unwrap(), funds: vec![] diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 3524937c8..34d0a7189 100755 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -27,6 +27,7 @@ anyhow = { workspace = true } cosmwasm-std = { workspace = true } cw-multi-test = { workspace = true } cw-it = { workspace = true, features = ["osmosis-test-tube"] } +mars-incentives = { workspace = true } mars-oracle-osmosis = { workspace = true } mars-oracle-base = { workspace = true } mars-osmosis = { workspace = true } diff --git a/integration-tests/tests/helpers.rs b/integration-tests/tests/helpers.rs index 868b8d380..60b54f10b 100644 --- a/integration-tests/tests/helpers.rs +++ b/integration-tests/tests/helpers.rs @@ -4,7 +4,6 @@ use anyhow::Result as AnyResult; use cosmwasm_std::{Coin, Decimal, Fraction, Uint128}; use cw_multi_test::AppResponse; use mars_params::types::asset::{AssetParams, CmSettings, LiquidationBonus, RedBankSettings}; -use mars_red_bank::error::ContractError; use mars_red_bank_types::red_bank::{ InitOrUpdateAssetParams, InterestRateModel, UserHealthStatus, UserPositionResponse, }; @@ -218,11 +217,21 @@ pub fn swap( .unwrap() } -pub fn assert_err(res: AnyResult, err: ContractError) { +pub fn assert_red_bank_err(res: AnyResult, err: mars_red_bank::error::ContractError) { match res { Ok(_) => panic!("Result was not an error"), Err(generic_err) => { - let contract_err: ContractError = generic_err.downcast().unwrap(); + let contract_err: mars_red_bank::error::ContractError = generic_err.downcast().unwrap(); + assert_eq!(contract_err, err); + } + } +} + +pub fn assert_incentives_err(res: AnyResult, err: mars_incentives::ContractError) { + match res { + Ok(_) => panic!("Result was not an error"), + Err(generic_err) => { + let contract_err: mars_incentives::ContractError = generic_err.downcast().unwrap(); assert_eq!(contract_err, err); } } diff --git a/integration-tests/tests/test_incentives.rs b/integration-tests/tests/test_incentives.rs index fe449fdde..793b364db 100644 --- a/integration-tests/tests/test_incentives.rs +++ b/integration-tests/tests/test_incentives.rs @@ -46,12 +46,12 @@ fn rewards_claim() { let user_collateral = red_bank.query_user_collateral(&mut mock_env, &user, "uusdc"); assert_eq!(user_collateral.amount.u128(), funded_amt); - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::zero()); mock_env.increment_by_time(86400); // 24 hours - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::new(864000)); incentives.claim_rewards(&mut mock_env, &user).unwrap(); @@ -61,10 +61,185 @@ fn rewards_claim() { let mars_balance = mock_env.query_balance(&incentives.contract_addr, "umars").unwrap(); assert_eq!(mars_balance.amount, Uint128::from(ONE_WEEK_IN_SEC * 10 - 864000)); - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::zero()); } +// Credit accounts can deposit / withdraw from Red Bank and accure rewards in incentives contract. +#[test] +fn rewards_claim_for_credit_account() { + let owner = Addr::unchecked("owner"); + let mut mock_env = MockEnvBuilder::new(None, owner).build(); + + let red_bank = mock_env.red_bank.clone(); + let params = mock_env.params.clone(); + + let (market_params, asset_params) = default_asset_params("uusdc"); + red_bank.init_asset(&mut mock_env, "uusdc", market_params); + params.init_params(&mut mock_env, asset_params); + + let incentives = mock_env.incentives.clone(); + incentives.whitelist_incentive_denoms(&mut mock_env, &[("umars", 3)]); + incentives.init_asset_incentive_from_current_block( + &mut mock_env, + "uusdc", + "umars", + 10, + ONE_WEEK_IN_SEC, + ); + + // setup credit accounts + let credit_manager = mock_env.credit_manager.clone(); + let funded_amt = 10_000_000_000u128; + mock_env.fund_account(&credit_manager, &[coin(funded_amt, "uusdc")]); + + let acc_id_1 = "101".to_string(); + let acc_id_2 = "205".to_string(); + let funded_amt_acc_id_1 = 600_000_000u128; // 60% of total deposited amount + let funded_amt_acc_id_2 = 400_000_000u128; // 40% of total deposited amount + + // credit accounts deposit to Red Bank + red_bank + .deposit_with_acc_id( + &mut mock_env, + &credit_manager, + coin(funded_amt_acc_id_1, "uusdc"), + Some(acc_id_1.clone()), + ) + .unwrap(); + red_bank + .deposit_with_acc_id( + &mut mock_env, + &credit_manager, + coin(funded_amt_acc_id_2, "uusdc"), + Some(acc_id_2.clone()), + ) + .unwrap(); + let user_collateral = red_bank.query_user_collateral(&mut mock_env, &credit_manager, "uusdc"); + assert_eq!(user_collateral.amount.u128(), 0); + let acc_id_1_collateral = red_bank.query_user_collateral_with_acc_id( + &mut mock_env, + &credit_manager, + Some(acc_id_1.clone()), + "uusdc", + ); + assert_eq!(acc_id_1_collateral.amount.u128(), funded_amt_acc_id_1); + let acc_id_2_collateral = red_bank.query_user_collateral_with_acc_id( + &mut mock_env, + &credit_manager, + Some(acc_id_2.clone()), + "uusdc", + ); + assert_eq!(acc_id_2_collateral.amount.u128(), funded_amt_acc_id_2); + + // no rewards in the deposit block + let rewards_balance_acc_id_1 = incentives + .query_unclaimed_rewards_with_acc_id(&mut mock_env, &credit_manager, Some(acc_id_1.clone())) + .unwrap(); + assert_eq!(rewards_balance_acc_id_1[0].amount, Uint128::zero()); + let rewards_balance_acc_id_2 = incentives + .query_unclaimed_rewards_with_acc_id(&mut mock_env, &credit_manager, Some(acc_id_2.clone())) + .unwrap(); + assert_eq!(rewards_balance_acc_id_2[0].amount, Uint128::zero()); + + // move 24 hours + mock_env.increment_by_time(86400); + + // credit accounts should accure rewards proportionally + let rewards_balance_acc_id_1 = incentives + .query_unclaimed_rewards_with_acc_id(&mut mock_env, &credit_manager, Some(acc_id_1.clone())) + .unwrap(); + assert_eq!(rewards_balance_acc_id_1[0].amount, Uint128::new(518400)); // 60% * 864000 + let rewards_balance_acc_id_2 = incentives + .query_unclaimed_rewards_with_acc_id(&mut mock_env, &credit_manager, Some(acc_id_2.clone())) + .unwrap(); + assert_eq!(rewards_balance_acc_id_2[0].amount, Uint128::new(345600)); // 40% * 864000 + + // claiming credit manager rewards without account id should fail + incentives.claim_rewards(&mut mock_env, &credit_manager).unwrap_err(); + // query credit manager rewards without account id should return zero + let rewards_balance = + incentives.query_unclaimed_rewards(&mut mock_env, &credit_manager).unwrap(); + assert_eq!(rewards_balance[0].amount, Uint128::zero()); + + // claim rewards for credit accounts + incentives + .claim_rewards_with_acc_id(&mut mock_env, &credit_manager, Some(acc_id_1.clone())) + .unwrap(); + let rewards_balance_acc_id_1 = incentives + .query_unclaimed_rewards_with_acc_id(&mut mock_env, &credit_manager, Some(acc_id_1.clone())) + .unwrap(); + assert_eq!(rewards_balance_acc_id_1[0].amount, Uint128::zero()); + incentives + .claim_rewards_with_acc_id(&mut mock_env, &credit_manager, Some(acc_id_2.clone())) + .unwrap(); + let rewards_balance_acc_id_2 = incentives + .query_unclaimed_rewards_with_acc_id(&mut mock_env, &credit_manager, Some(acc_id_2.clone())) + .unwrap(); + assert_eq!(rewards_balance_acc_id_2[0].amount, Uint128::zero()); + + // credit accounts withdraw from Red Bank + let withdraw_amt_acc_id_1 = 300_000_000u128; + let withdraw_amt_acc_id_2 = 100_000_000u128; + red_bank + .withdraw_with_acc_id( + &mut mock_env, + &credit_manager, + "uusdc", + Some(Uint128::from(withdraw_amt_acc_id_1)), + Some(acc_id_1.clone()), + ) + .unwrap(); + red_bank + .withdraw_with_acc_id( + &mut mock_env, + &credit_manager, + "uusdc", + Some(Uint128::from(withdraw_amt_acc_id_2)), + Some(acc_id_2.clone()), + ) + .unwrap(); + let user_collateral = red_bank.query_user_collateral(&mut mock_env, &credit_manager, "uusdc"); + assert_eq!(user_collateral.amount.u128(), 0); + let acc_id_1_collateral = red_bank.query_user_collateral_with_acc_id( + &mut mock_env, + &credit_manager, + Some(acc_id_1.clone()), + "uusdc", + ); + assert_eq!(acc_id_1_collateral.amount.u128(), funded_amt_acc_id_1 - withdraw_amt_acc_id_1); + let acc_id_2_collateral = red_bank.query_user_collateral_with_acc_id( + &mut mock_env, + &credit_manager, + Some(acc_id_2.clone()), + "uusdc", + ); + assert_eq!(acc_id_2_collateral.amount.u128(), funded_amt_acc_id_2 - withdraw_amt_acc_id_2); + + // move 24 hours + mock_env.increment_by_time(86400); + + // credit accounts should accure rewards proportionally + let rewards_balance_acc_id_1 = incentives + .query_unclaimed_rewards_with_acc_id(&mut mock_env, &credit_manager, Some(acc_id_1.clone())) + .unwrap(); + assert_eq!(rewards_balance_acc_id_1[0].amount, Uint128::new(432000)); // 50% * 864000 + let rewards_balance_acc_id_2 = incentives + .query_unclaimed_rewards_with_acc_id(&mut mock_env, &credit_manager, Some(acc_id_2.clone())) + .unwrap(); + assert_eq!(rewards_balance_acc_id_2[0].amount, Uint128::new(432000)); // 50% * 864000 + + // claim rewards for credit accounts + incentives.claim_rewards_with_acc_id(&mut mock_env, &credit_manager, Some(acc_id_1)).unwrap(); + incentives.claim_rewards_with_acc_id(&mut mock_env, &credit_manager, Some(acc_id_2)).unwrap(); + + // check balances for umars in credit manager and incentives contracts + let balance = mock_env.query_balance(&credit_manager, "umars").unwrap(); + assert_eq!(balance.amount, Uint128::new(864000 + 864000)); + let mars_balance = mock_env.query_balance(&incentives.contract_addr, "umars").unwrap(); + assert_eq!(mars_balance.amount, Uint128::from(ONE_WEEK_IN_SEC * 10 - 864000 - 864000)); +} + // User A deposited usdc in the redbank when incentives were 5 emissions per second // Then claimed rewards after one day // Then user A later deposits osmo in the red bank when incentives were 10 emissions per second without withdrawing usdc @@ -112,12 +287,12 @@ fn emissions_rates() { let user_collateral = red_bank.query_user_collateral(&mut mock_env, &user, "uusdc"); assert_eq!(user_collateral.amount.u128(), funded_amt); - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::zero()); mock_env.increment_by_time(86400); // 24 hours - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::new(432000)); // 86400*5 incentives.claim_rewards(&mut mock_env, &user).unwrap(); @@ -125,7 +300,7 @@ fn emissions_rates() { let balance = mock_env.query_balance(&user, "umars").unwrap(); assert_eq!(balance.amount, Uint128::new(432000)); - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::zero()); incentives.init_asset_incentive_from_current_block( @@ -144,12 +319,12 @@ fn emissions_rates() { let user_collateral = red_bank.query_user_collateral(&mut mock_env, &user, "uosmo"); assert_eq!(user_collateral.amount.u128(), funded_amt); - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::zero()); mock_env.increment_by_time(86400); // 24 hours - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::new(1296000)); // 432000 + (86400*10) incentives.claim_rewards(&mut mock_env, &user).unwrap(); @@ -157,7 +332,7 @@ fn emissions_rates() { let balance = mock_env.query_balance(&user, "umars").unwrap(); assert_eq!(balance.amount, Uint128::new(1728000)); // 1296000 + 432000 - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::zero()); } @@ -206,12 +381,12 @@ fn no_incentives_accrued_after_withdraw() { let user_collateral = red_bank.query_user_collateral(&mut mock_env, &user, "uusdc"); assert_eq!(user_collateral.amount.u128(), funded_amt); - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::zero()); mock_env.increment_by_time(86400); // 24 hours - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::new(432000)); // 86400 * 5 incentives.claim_rewards(&mut mock_env, &user).unwrap(); @@ -219,7 +394,7 @@ fn no_incentives_accrued_after_withdraw() { let balance = mock_env.query_balance(&user, "umars").unwrap(); assert_eq!(balance.amount, Uint128::new(432000)); - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::zero()); red_bank.withdraw(&mut mock_env, &user, "uusdc", None).unwrap(); @@ -230,12 +405,12 @@ fn no_incentives_accrued_after_withdraw() { let user_collateral = red_bank.query_user_collateral(&mut mock_env, &user, "uosmo"); assert_eq!(user_collateral.amount, Uint128::zero()); - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::zero()); mock_env.increment_by_time(86400); // 24 hours - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::zero()); } @@ -315,12 +490,12 @@ fn multiple_assets() { let user_collateral = red_bank.query_user_collateral(&mut mock_env, &user, "uosmo"); assert_eq!(user_collateral.amount.u128(), funded_amt); - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::zero()); mock_env.increment_by_time(86400); // 24 hours - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::new(1555200)); } @@ -377,18 +552,18 @@ fn multiple_users() { let user_collateral = red_bank.query_user_collateral(&mut mock_env, &user_b, "uusdc"); assert_eq!(user_collateral.amount.u128(), funded_amt_two); - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user_a); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user_a).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::zero()); - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user_b); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user_b).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::zero()); mock_env.increment_by_time(86400); // 24 hours - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user_a); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user_a).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::new(144000)); // (86400*5) * (1/3) - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user_b); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user_b).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::new(288000)); // (86400*5)/2 * (2/3) // User A withdraws, user B holds @@ -397,10 +572,10 @@ fn multiple_users() { mock_env.increment_by_time(86400); // 24 hours - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user_a); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user_a).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::new(144000)); // stays the same - let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user_b); + let rewards_balance = incentives.query_unclaimed_rewards(&mut mock_env, &user_b).unwrap(); assert_eq!(rewards_balance[0].amount, Uint128::new(720000)); // 288000 + (86400*5) } @@ -514,15 +689,18 @@ fn rewards_distributed_among_users_and_rewards_collector() { assert_eq!(uosmo_collateral_rc.amount, Uint128::zero()); // rewards-collector accrue rewards - let rewards_balance_rc = - incentives.query_unclaimed_rewards(&mut mock_env, &rewards_collector.contract_addr); + let rewards_balance_rc = incentives + .query_unclaimed_rewards(&mut mock_env, &rewards_collector.contract_addr) + .unwrap(); assert!(!rewards_balance_rc.is_empty()); println!("rewards_balance_rc: {:?}", rewards_balance_rc); // sum of unclaimed rewards should be equal to total umars available for finished incentive - let rewards_balance_user_a = incentives.query_unclaimed_rewards(&mut mock_env, &user_a); + let rewards_balance_user_a = + incentives.query_unclaimed_rewards(&mut mock_env, &user_a).unwrap(); println!("rewards_balance_user_a: {:?}", rewards_balance_user_a); - let rewards_balance_user_b = incentives.query_unclaimed_rewards(&mut mock_env, &user_b); + let rewards_balance_user_b = + incentives.query_unclaimed_rewards(&mut mock_env, &user_b).unwrap(); println!("rewards_balance_user_b: {:?}", rewards_balance_user_b); let total_claimed_rewards = rewards_balance_rc[0].amount + rewards_balance_user_a[0].amount diff --git a/integration-tests/tests/test_oracles.rs b/integration-tests/tests/test_oracles.rs index f11355abc..298a411af 100644 --- a/integration-tests/tests/test_oracles.rs +++ b/integration-tests/tests/test_oracles.rs @@ -949,7 +949,15 @@ fn redbank_should_fail_if_no_price() { ) .unwrap(); - wasm.execute(&red_bank_addr, &Deposit {}, &[coin(1_000_000, "uatom")], depositor).unwrap(); + wasm.execute( + &red_bank_addr, + &Deposit { + account_id: None, + }, + &[coin(1_000_000, "uatom")], + depositor, + ) + .unwrap(); // execute msg should fail since it is attempting to query an asset from the oracle contract that doesn't have an LP pool set up wasm.execute( @@ -1003,7 +1011,15 @@ fn redbank_quering_oracle_successfully() { ) .unwrap(); - wasm.execute(&red_bank_addr, &Deposit {}, &[coin(1_000_000, "uatom")], depositor).unwrap(); + wasm.execute( + &red_bank_addr, + &Deposit { + account_id: None, + }, + &[coin(1_000_000, "uatom")], + depositor, + ) + .unwrap(); wasm.execute( &red_bank_addr, diff --git a/integration-tests/tests/test_rover_flow.rs b/integration-tests/tests/test_rover_flow.rs index e8a61fde1..0abc8c868 100644 --- a/integration-tests/tests/test_rover_flow.rs +++ b/integration-tests/tests/test_rover_flow.rs @@ -3,7 +3,7 @@ use mars_red_bank::error::ContractError; use mars_red_bank_types::red_bank::UserHealthStatus; use mars_testing::integration::mock_env::MockEnvBuilder; -use crate::helpers::{assert_err, default_asset_params}; +use crate::helpers::{assert_red_bank_err, default_asset_params}; mod helpers; @@ -50,7 +50,7 @@ fn rover_flow() { // rover can't borrow above the credit line let res_err = red_bank.borrow(&mut mock_env, &rover, "uusdc", rover_uusdc_limit + 1u128); - assert_err(res_err, ContractError::BorrowAmountExceedsUncollateralizedLoanLimit {}); + assert_red_bank_err(res_err, ContractError::BorrowAmountExceedsUncollateralizedLoanLimit {}); // rover borrows the entire line of credit let balance = mock_env.query_balance(&rover, "uusdc").unwrap(); @@ -76,7 +76,7 @@ fn rover_flow() { // can't borrow above the credit line let res_err = red_bank.borrow(&mut mock_env, &rover, "uusdc", 1u128); - assert_err(res_err, ContractError::BorrowAmountExceedsUncollateralizedLoanLimit {}); + assert_red_bank_err(res_err, ContractError::BorrowAmountExceedsUncollateralizedLoanLimit {}); // rover should be healthy (NotBorrowing because uncollateralized debt is not included in HF calculation) let position = red_bank.query_user_position(&mut mock_env, &rover); @@ -90,7 +90,7 @@ fn rover_flow() { "uusdc", Uint128::zero(), ); - assert_err(res_err, ContractError::UserHasUncollateralizedDebt {}); + assert_red_bank_err(res_err, ContractError::UserHasUncollateralizedDebt {}); let debt = red_bank.query_user_debt(&mut mock_env, &rover, "uusdc"); assert!(debt.uncollateralized); assert_eq!(debt.amount.u128(), rover_uusdc_limit); @@ -129,5 +129,5 @@ fn rover_flow() { "uusdc", Uint128::from(rover_uusdc_limit), ); - assert_err(res_err, ContractError::UserHasCollateralizedDebt {}); + assert_red_bank_err(res_err, ContractError::UserHasCollateralizedDebt {}); } diff --git a/integration-tests/tests/test_user_flow.rs b/integration-tests/tests/test_user_flow.rs index 52ecfbe27..8f3813b76 100644 --- a/integration-tests/tests/test_user_flow.rs +++ b/integration-tests/tests/test_user_flow.rs @@ -5,7 +5,7 @@ use mars_params::types::asset::LiquidationBonus; use mars_red_bank::error::ContractError; use mars_testing::integration::mock_env::{MockEnv, MockEnvBuilder, RedBank}; -use crate::helpers::{assert_err, default_asset_params, default_asset_params_with}; +use crate::helpers::{assert_red_bank_err, default_asset_params, default_asset_params_with}; mod helpers; @@ -343,7 +343,7 @@ fn internally_tracked_balances_used_for_borrow() { mock_env.fund_account(&borrower2, &[coin(funded_osmo, "uosmo")]); red_bank.deposit(&mut mock_env, &borrower2, coin(funded_osmo, "uosmo")).unwrap(); let res = red_bank.borrow(&mut mock_env, &borrower2, "uatom", donated_atom); - assert_err( + assert_red_bank_err( res, ContractError::InvalidBorrowAmount { denom: "uatom".to_string(), diff --git a/packages/testing/src/incentives_querier.rs b/packages/testing/src/incentives_querier.rs index 55421b3c3..1d2b76c8d 100644 --- a/packages/testing/src/incentives_querier.rs +++ b/packages/testing/src/incentives_querier.rs @@ -32,6 +32,7 @@ impl IncentivesQuerier { let ret: ContractResult = match query { QueryMsg::UserUnclaimedRewards { user: _, + account_id: _, start_after_collateral_denom: _, start_after_incentive_denom: _, limit: _, diff --git a/packages/testing/src/integration/mock_env.rs b/packages/testing/src/integration/mock_env.rs index 42a5de50b..34ccb5870 100644 --- a/packages/testing/src/integration/mock_env.rs +++ b/packages/testing/src/integration/mock_env.rs @@ -33,6 +33,7 @@ pub struct MockEnv { pub red_bank: RedBank, pub rewards_collector: RewardsCollector, pub params: Params, + pub credit_manager: Addr, } #[derive(Clone)] @@ -176,10 +177,20 @@ impl Incentives { } pub fn claim_rewards(&self, env: &mut MockEnv, sender: &Addr) -> AnyResult { + self.claim_rewards_with_acc_id(env, sender, None) + } + + pub fn claim_rewards_with_acc_id( + &self, + env: &mut MockEnv, + sender: &Addr, + account_id: Option, + ) -> AnyResult { env.app.execute_contract( sender.clone(), self.contract_addr.clone(), &incentives::ExecuteMsg::ClaimRewards { + account_id, start_after_collateral_denom: None, start_after_incentive_denom: None, limit: None, @@ -188,19 +199,26 @@ impl Incentives { ) } - pub fn query_unclaimed_rewards(&self, env: &mut MockEnv, user: &Addr) -> Vec { - env.app - .wrap() - .query_wasm_smart( - self.contract_addr.clone(), - &incentives::QueryMsg::UserUnclaimedRewards { - user: user.to_string(), - start_after_collateral_denom: None, - start_after_incentive_denom: None, - limit: None, - }, - ) - .unwrap() + pub fn query_unclaimed_rewards(&self, env: &mut MockEnv, user: &Addr) -> StdResult> { + self.query_unclaimed_rewards_with_acc_id(env, user, None) + } + + pub fn query_unclaimed_rewards_with_acc_id( + &self, + env: &mut MockEnv, + user: &Addr, + account_id: Option, + ) -> StdResult> { + env.app.wrap().query_wasm_smart( + self.contract_addr.clone(), + &incentives::QueryMsg::UserUnclaimedRewards { + account_id, + user: user.to_string(), + start_after_collateral_denom: None, + start_after_incentive_denom: None, + limit: None, + }, + ) } } @@ -271,10 +289,22 @@ impl RedBank { } pub fn deposit(&self, env: &mut MockEnv, sender: &Addr, coin: Coin) -> AnyResult { + self.deposit_with_acc_id(env, sender, coin, None) + } + + pub fn deposit_with_acc_id( + &self, + env: &mut MockEnv, + sender: &Addr, + coin: Coin, + account_id: Option, + ) -> AnyResult { env.app.execute_contract( sender.clone(), self.contract_addr.clone(), - &red_bank::ExecuteMsg::Deposit {}, + &red_bank::ExecuteMsg::Deposit { + account_id, + }, &[coin], ) } @@ -315,6 +345,17 @@ impl RedBank { sender: &Addr, denom: &str, amount: Option, + ) -> AnyResult { + self.withdraw_with_acc_id(env, sender, denom, amount, None) + } + + pub fn withdraw_with_acc_id( + &self, + env: &mut MockEnv, + sender: &Addr, + denom: &str, + amount: Option, + account_id: Option, ) -> AnyResult { env.app.execute_contract( sender.clone(), @@ -323,6 +364,7 @@ impl RedBank { denom: denom.to_string(), amount, recipient: None, + account_id, }, &[], ) @@ -452,6 +494,16 @@ impl RedBank { env: &mut MockEnv, user: &Addr, denom: &str, + ) -> UserCollateralResponse { + self.query_user_collateral_with_acc_id(env, user, None, denom) + } + + pub fn query_user_collateral_with_acc_id( + &self, + env: &mut MockEnv, + user: &Addr, + account_id: Option, + denom: &str, ) -> UserCollateralResponse { env.app .wrap() @@ -459,6 +511,7 @@ impl RedBank { self.contract_addr.clone(), &red_bank::QueryMsg::UserCollateral { user: user.to_string(), + account_id, denom: denom.to_string(), }, ) @@ -469,6 +522,15 @@ impl RedBank { &self, env: &mut MockEnv, user: &Addr, + ) -> HashMap { + self.query_user_collaterals_with_acc_id(env, user, None) + } + + pub fn query_user_collaterals_with_acc_id( + &self, + env: &mut MockEnv, + user: &Addr, + account_id: Option, ) -> HashMap { let res: Vec = env .app @@ -477,6 +539,7 @@ impl RedBank { self.contract_addr.clone(), &red_bank::QueryMsg::UserCollaterals { user: user.to_string(), + account_id, start_after: None, limit: Some(100), }, @@ -616,6 +679,8 @@ pub struct MockEnvBuilder { slippage_tolerance: Decimal, pyth_contract_addr: String, + + credit_manager_contract_addr: String, } impl MockEnvBuilder { @@ -635,6 +700,8 @@ impl MockEnvBuilder { slippage_tolerance: Decimal::percent(5), pyth_contract_addr: "osmo1svg55quy7jjee6dn0qx85qxxvx5cafkkw4tmqpcjr9dx99l0zrhs4usft5" .to_string(), // correct bech32 addr to pass validation + credit_manager_contract_addr: + "osmo1q7khj532p2fyvmnu83tul6xddl6yl0d0kmrzdz2pfel3lkxem92sw6zqrl".to_string(), } } @@ -708,6 +775,12 @@ impl MockEnvBuilder { &rewards_collector_addr, ); self.update_address_provider(&address_provider_addr, MarsAddressType::Params, ¶ms_addr); + let cm_addr = Addr::unchecked(&self.credit_manager_contract_addr); + self.update_address_provider( + &address_provider_addr, + MarsAddressType::CreditManager, + &cm_addr, + ); MockEnv { app: take(&mut self.app), @@ -730,6 +803,7 @@ impl MockEnvBuilder { params: Params { contract_addr: params_addr, }, + credit_manager: cm_addr, } } diff --git a/packages/testing/src/red_bank_querier.rs b/packages/testing/src/red_bank_querier.rs index b4984d268..f31a9c8c8 100644 --- a/packages/testing/src/red_bank_querier.rs +++ b/packages/testing/src/red_bank_querier.rs @@ -23,6 +23,7 @@ impl RedBankQuerier { }, QueryMsg::UserCollateral { user, + account_id: _, denom, } => match self.users_denoms_collaterals.get(&(user.clone(), denom)) { Some(collateral) => to_binary(&collateral).into(), diff --git a/packages/types/src/address_provider.rs b/packages/types/src/address_provider.rs index bbcb134a5..c212ec890 100644 --- a/packages/types/src/address_provider.rs +++ b/packages/types/src/address_provider.rs @@ -13,6 +13,7 @@ pub enum MarsAddressType { RedBank, RewardsCollector, Params, + CreditManager, /// Protocol admin is an ICS-27 interchain account controlled by Mars Hub's x/gov module. /// This account will take the owner and admin roles of red-bank contracts. /// @@ -39,6 +40,7 @@ pub enum MarsAddressType { impl fmt::Display for MarsAddressType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = match self { + MarsAddressType::CreditManager => "credit_manager", MarsAddressType::FeeCollector => "fee_collector", MarsAddressType::Incentives => "incentives", MarsAddressType::Oracle => "oracle", @@ -58,6 +60,7 @@ impl FromStr for MarsAddressType { fn from_str(s: &str) -> Result { match s { + "credit_manager" => Ok(MarsAddressType::CreditManager), "fee_collector" => Ok(MarsAddressType::FeeCollector), "incentives" => Ok(MarsAddressType::Incentives), "oracle" => Ok(MarsAddressType::Oracle), diff --git a/packages/types/src/incentives.rs b/packages/types/src/incentives.rs index 8aceee19b..41b4ad52a 100644 --- a/packages/types/src/incentives.rs +++ b/packages/types/src/incentives.rs @@ -125,6 +125,8 @@ pub enum ExecuteMsg { /// User address. Address is trusted as it must be validated by the Red Bank /// contract before calling this method user_addr: Addr, + /// Credit account id (Rover) + account_id: Option, /// Denom of the asset of which deposited balance is changed denom: String, /// The user's scaled collateral amount up to the instant before the change @@ -136,6 +138,8 @@ pub enum ExecuteMsg { /// Claim rewards. MARS rewards accrued by the user will be staked into xMARS before /// being sent. ClaimRewards { + /// Credit account id (Rover) + account_id: Option, /// Start pagination after this collateral denom start_after_collateral_denom: Option, /// Start pagination after this incentive denom. If supplied you must also supply @@ -229,6 +233,8 @@ pub enum QueryMsg { UserUnclaimedRewards { /// The user address for which to query unclaimed rewards user: String, + /// Credit account id (Rover) + account_id: Option, /// Start pagination after this collateral denom start_after_collateral_denom: Option, /// Start pagination after this incentive denom. If supplied you must also supply diff --git a/packages/types/src/red_bank/msg.rs b/packages/types/src/red_bank/msg.rs index 12359b4c3..a01d99d61 100644 --- a/packages/types/src/red_bank/msg.rs +++ b/packages/types/src/red_bank/msg.rs @@ -53,7 +53,10 @@ pub enum ExecuteMsg { /// Deposit native coins. Deposited coins must be sent in the transaction /// this call is made - Deposit {}, + Deposit { + /// Credit account id (Rover) + account_id: Option, + }, /// Withdraw native coins Withdraw { @@ -63,6 +66,8 @@ pub enum ExecuteMsg { amount: Option, /// The address where the withdrawn amount is sent recipient: Option, + /// Credit account id (Rover) + account_id: Option, }, /// Borrow native coins. If borrow allowed, amount is added to caller's debt @@ -174,6 +179,7 @@ pub enum QueryMsg { #[returns(crate::red_bank::UserCollateralResponse)] UserCollateral { user: String, + account_id: Option, denom: String, }, @@ -181,6 +187,7 @@ pub enum QueryMsg { #[returns(Vec)] UserCollaterals { user: String, + account_id: Option, start_after: Option, limit: Option, }, diff --git a/schemas/mars-address-provider/mars-address-provider.json b/schemas/mars-address-provider/mars-address-provider.json index ea4a34e9c..3d9aaad94 100644 --- a/schemas/mars-address-provider/mars-address-provider.json +++ b/schemas/mars-address-provider/mars-address-provider.json @@ -77,7 +77,8 @@ "oracle", "red_bank", "rewards_collector", - "params" + "params", + "credit_manager" ] }, { @@ -280,7 +281,8 @@ "oracle", "red_bank", "rewards_collector", - "params" + "params", + "credit_manager" ] }, { @@ -351,7 +353,8 @@ "oracle", "red_bank", "rewards_collector", - "params" + "params", + "credit_manager" ] }, { @@ -425,7 +428,8 @@ "oracle", "red_bank", "rewards_collector", - "params" + "params", + "credit_manager" ] }, { @@ -499,7 +503,8 @@ "oracle", "red_bank", "rewards_collector", - "params" + "params", + "credit_manager" ] }, { diff --git a/schemas/mars-incentives/mars-incentives.json b/schemas/mars-incentives/mars-incentives.json index 7bbb22178..0b12eb446 100644 --- a/schemas/mars-incentives/mars-incentives.json +++ b/schemas/mars-incentives/mars-incentives.json @@ -141,6 +141,13 @@ "user_amount_scaled_before" ], "properties": { + "account_id": { + "description": "Credit account id (Rover)", + "type": [ + "string", + "null" + ] + }, "denom": { "description": "Denom of the asset of which deposited balance is changed", "type": "string" @@ -185,6 +192,13 @@ "claim_rewards": { "type": "object", "properties": { + "account_id": { + "description": "Credit account id (Rover)", + "type": [ + "string", + "null" + ] + }, "limit": { "description": "The maximum number of results to return. If not set, 5 is used. If larger than 10, 10 is used.", "type": [ @@ -571,6 +585,13 @@ "user" ], "properties": { + "account_id": { + "description": "Credit account id (Rover)", + "type": [ + "string", + "null" + ] + }, "limit": { "description": "The maximum number of results to return. If not set, 5 is used. If larger than 10, 10 is used.", "type": [ diff --git a/schemas/mars-red-bank/mars-red-bank.json b/schemas/mars-red-bank/mars-red-bank.json index a42ba09b7..19c489f72 100644 --- a/schemas/mars-red-bank/mars-red-bank.json +++ b/schemas/mars-red-bank/mars-red-bank.json @@ -189,6 +189,15 @@ "properties": { "deposit": { "type": "object", + "properties": { + "account_id": { + "description": "Credit account id (Rover)", + "type": [ + "string", + "null" + ] + } + }, "additionalProperties": false } }, @@ -207,6 +216,13 @@ "denom" ], "properties": { + "account_id": { + "description": "Credit account id (Rover)", + "type": [ + "string", + "null" + ] + }, "amount": { "description": "Amount to be withdrawn. If None is specified, the full amount will be withdrawn.", "anyOf": [ @@ -740,6 +756,12 @@ "user" ], "properties": { + "account_id": { + "type": [ + "string", + "null" + ] + }, "denom": { "type": "string" }, @@ -765,6 +787,12 @@ "user" ], "properties": { + "account_id": { + "type": [ + "string", + "null" + ] + }, "limit": { "type": [ "integer", diff --git a/scripts/types/generated/mars-address-provider/MarsAddressProvider.types.ts b/scripts/types/generated/mars-address-provider/MarsAddressProvider.types.ts index e568853d7..695663ba9 100644 --- a/scripts/types/generated/mars-address-provider/MarsAddressProvider.types.ts +++ b/scripts/types/generated/mars-address-provider/MarsAddressProvider.types.ts @@ -20,7 +20,7 @@ export type ExecuteMsg = update_owner: OwnerUpdate } export type MarsAddressType = - | ('incentives' | 'oracle' | 'red_bank' | 'rewards_collector' | 'params') + | ('incentives' | 'oracle' | 'red_bank' | 'rewards_collector' | 'params' | 'credit_manager') | 'protocol_admin' | 'fee_collector' | 'safety_fund' diff --git a/scripts/types/generated/mars-incentives/MarsIncentives.client.ts b/scripts/types/generated/mars-incentives/MarsIncentives.client.ts index 566f4bbc2..17a229869 100644 --- a/scripts/types/generated/mars-incentives/MarsIncentives.client.ts +++ b/scripts/types/generated/mars-incentives/MarsIncentives.client.ts @@ -72,11 +72,13 @@ export interface MarsIncentivesReadOnlyInterface { startAfterTimestamp?: number }) => Promise userUnclaimedRewards: ({ + accountId, limit, startAfterCollateralDenom, startAfterIncentiveDenom, user, }: { + accountId?: string limit?: number startAfterCollateralDenom?: string startAfterIncentiveDenom?: string @@ -186,11 +188,13 @@ export class MarsIncentivesQueryClient implements MarsIncentivesReadOnlyInterfac }) } userUnclaimedRewards = async ({ + accountId, limit, startAfterCollateralDenom, startAfterIncentiveDenom, user, }: { + accountId?: string limit?: number startAfterCollateralDenom?: string startAfterIncentiveDenom?: string @@ -198,6 +202,7 @@ export class MarsIncentivesQueryClient implements MarsIncentivesReadOnlyInterfac }): Promise => { return this.client.queryContractSmart(this.contractAddress, { user_unclaimed_rewards: { + account_id: accountId, limit, start_after_collateral_denom: startAfterCollateralDenom, start_after_incentive_denom: startAfterIncentiveDenom, @@ -246,11 +251,13 @@ export interface MarsIncentivesInterface extends MarsIncentivesReadOnlyInterface ) => Promise balanceChange: ( { + accountId, denom, totalAmountScaledBefore, userAddr, userAmountScaledBefore, }: { + accountId?: string denom: string totalAmountScaledBefore: Uint128 userAddr: Addr @@ -262,10 +269,12 @@ export interface MarsIncentivesInterface extends MarsIncentivesReadOnlyInterface ) => Promise claimRewards: ( { + accountId, limit, startAfterCollateralDenom, startAfterIncentiveDenom, }: { + accountId?: string limit?: number startAfterCollateralDenom?: string startAfterIncentiveDenom?: string @@ -377,11 +386,13 @@ export class MarsIncentivesClient } balanceChange = async ( { + accountId, denom, totalAmountScaledBefore, userAddr, userAmountScaledBefore, }: { + accountId?: string denom: string totalAmountScaledBefore: Uint128 userAddr: Addr @@ -396,6 +407,7 @@ export class MarsIncentivesClient this.contractAddress, { balance_change: { + account_id: accountId, denom, total_amount_scaled_before: totalAmountScaledBefore, user_addr: userAddr, @@ -409,10 +421,12 @@ export class MarsIncentivesClient } claimRewards = async ( { + accountId, limit, startAfterCollateralDenom, startAfterIncentiveDenom, }: { + accountId?: string limit?: number startAfterCollateralDenom?: string startAfterIncentiveDenom?: string @@ -426,6 +440,7 @@ export class MarsIncentivesClient this.contractAddress, { claim_rewards: { + account_id: accountId, limit, start_after_collateral_denom: startAfterCollateralDenom, start_after_incentive_denom: startAfterIncentiveDenom, diff --git a/scripts/types/generated/mars-incentives/MarsIncentives.react-query.ts b/scripts/types/generated/mars-incentives/MarsIncentives.react-query.ts index 9386101c4..3014ffed2 100644 --- a/scripts/types/generated/mars-incentives/MarsIncentives.react-query.ts +++ b/scripts/types/generated/mars-incentives/MarsIncentives.react-query.ts @@ -94,6 +94,7 @@ export function useMarsIncentivesWhitelistQuery({ export interface MarsIncentivesUserUnclaimedRewardsQuery extends MarsIncentivesReactQuery { args: { + accountId?: string limit?: number startAfterCollateralDenom?: string startAfterIncentiveDenom?: string @@ -110,6 +111,7 @@ export function useMarsIncentivesUserUnclaimedRewardsQuery( () => client ? client.userUnclaimedRewards({ + accountId: args.accountId, limit: args.limit, startAfterCollateralDenom: args.startAfterCollateralDenom, startAfterIncentiveDenom: args.startAfterIncentiveDenom, @@ -304,6 +306,7 @@ export function useMarsIncentivesUpdateConfigMutation( export interface MarsIncentivesClaimRewardsMutation { client: MarsIncentivesClient msg: { + accountId?: string limit?: number startAfterCollateralDenom?: string startAfterIncentiveDenom?: string @@ -329,6 +332,7 @@ export function useMarsIncentivesClaimRewardsMutation( export interface MarsIncentivesBalanceChangeMutation { client: MarsIncentivesClient msg: { + accountId?: string denom: string totalAmountScaledBefore: Uint128 userAddr: Addr diff --git a/scripts/types/generated/mars-incentives/MarsIncentives.types.ts b/scripts/types/generated/mars-incentives/MarsIncentives.types.ts index f3b2ca4de..1098c5bb7 100644 --- a/scripts/types/generated/mars-incentives/MarsIncentives.types.ts +++ b/scripts/types/generated/mars-incentives/MarsIncentives.types.ts @@ -29,6 +29,7 @@ export type ExecuteMsg = } | { balance_change: { + account_id?: string | null denom: string total_amount_scaled_before: Uint128 user_addr: Addr @@ -37,6 +38,7 @@ export type ExecuteMsg = } | { claim_rewards: { + account_id?: string | null limit?: number | null start_after_collateral_denom?: string | null start_after_incentive_denom?: string | null @@ -111,6 +113,7 @@ export type QueryMsg = } | { user_unclaimed_rewards: { + account_id?: string | null limit?: number | null start_after_collateral_denom?: string | null start_after_incentive_denom?: string | null diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts index e603cb6ef..4b84c7f1e 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts @@ -67,17 +67,21 @@ export interface MarsRedBankReadOnlyInterface { user: string }) => Promise userCollateral: ({ + accountId, denom, user, }: { + accountId?: string denom: string user: string }) => Promise userCollaterals: ({ + accountId, limit, startAfter, user, }: { + accountId?: string limit?: number startAfter?: string user: string @@ -212,30 +216,36 @@ export class MarsRedBankQueryClient implements MarsRedBankReadOnlyInterface { }) } userCollateral = async ({ + accountId, denom, user, }: { + accountId?: string denom: string user: string }): Promise => { return this.client.queryContractSmart(this.contractAddress, { user_collateral: { + account_id: accountId, denom, user, }, }) } userCollaterals = async ({ + accountId, limit, startAfter, user, }: { + accountId?: string limit?: number startAfter?: string user: string }): Promise => { return this.client.queryContractSmart(this.contractAddress, { user_collaterals: { + account_id: accountId, limit, start_after: startAfter, user, @@ -364,16 +374,23 @@ export interface MarsRedBankInterface extends MarsRedBankReadOnlyInterface { _funds?: Coin[], ) => Promise deposit: ( + { + accountId, + }: { + accountId?: string + }, fee?: number | StdFee | 'auto', memo?: string, _funds?: Coin[], ) => Promise withdraw: ( { + accountId, amount, denom, recipient, }: { + accountId?: string amount?: Uint128 denom: string recipient?: string @@ -578,6 +595,11 @@ export class MarsRedBankClient extends MarsRedBankQueryClient implements MarsRed ) } deposit = async ( + { + accountId, + }: { + accountId?: string + }, fee: number | StdFee | 'auto' = 'auto', memo?: string, _funds?: Coin[], @@ -586,7 +608,9 @@ export class MarsRedBankClient extends MarsRedBankQueryClient implements MarsRed this.sender, this.contractAddress, { - deposit: {}, + deposit: { + account_id: accountId, + }, }, fee, memo, @@ -595,10 +619,12 @@ export class MarsRedBankClient extends MarsRedBankQueryClient implements MarsRed } withdraw = async ( { + accountId, amount, denom, recipient, }: { + accountId?: string amount?: Uint128 denom: string recipient?: string @@ -612,6 +638,7 @@ export class MarsRedBankClient extends MarsRedBankQueryClient implements MarsRed this.contractAddress, { withdraw: { + account_id: accountId, amount, denom, recipient, diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts index 8ba59b9b5..9047d2f7e 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts @@ -245,6 +245,7 @@ export function useMarsRedBankUserPositionQuery({ export interface MarsRedBankUserCollateralsQuery extends MarsRedBankReactQuery { args: { + accountId?: string limit?: number startAfter?: string user: string @@ -260,6 +261,7 @@ export function useMarsRedBankUserCollateralsQuery client ? client.userCollaterals({ + accountId: args.accountId, limit: args.limit, startAfter: args.startAfter, user: args.user, @@ -271,6 +273,7 @@ export function useMarsRedBankUserCollateralsQuery extends MarsRedBankReactQuery { args: { + accountId?: string denom: string user: string } @@ -285,6 +288,7 @@ export function useMarsRedBankUserCollateralQuery client ? client.userCollateral({ + accountId: args.accountId, denom: args.denom, user: args.user, }) @@ -536,6 +540,7 @@ export function useMarsRedBankBorrowMutation( export interface MarsRedBankWithdrawMutation { client: MarsRedBankClient msg: { + accountId?: string amount?: Uint128 denom: string recipient?: string @@ -559,6 +564,9 @@ export function useMarsRedBankWithdrawMutation( } export interface MarsRedBankDepositMutation { client: MarsRedBankClient + msg: { + accountId?: string + } args?: { fee?: number | StdFee | 'auto' memo?: string @@ -572,7 +580,7 @@ export function useMarsRedBankDepositMutation( >, ) { return useMutation( - ({ client, args: { fee, memo, funds } = {} }) => client.deposit(fee, memo, funds), + ({ client, msg, args: { fee, memo, funds } = {} }) => client.deposit(msg, fee, memo, funds), options, ) } diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts index 54fc21ed7..40c86bf51 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts @@ -41,10 +41,13 @@ export type ExecuteMsg = } } | { - deposit: {} + deposit: { + account_id?: string | null + } } | { withdraw: { + account_id?: string | null amount?: Uint128 | null denom: string recipient?: string | null @@ -145,12 +148,14 @@ export type QueryMsg = } | { user_collateral: { + account_id?: string | null denom: string user: string } } | { user_collaterals: { + account_id?: string | null limit?: number | null start_after?: string | null user: string From e15f31c55d7f0fa9d7d0d7f845b61216cfb135f8 Mon Sep 17 00:00:00 2001 From: piobab Date: Fri, 28 Jul 2023 12:09:15 +0200 Subject: [PATCH 12/43] Migrate to PoolManager for Pool and SpotPrice. Read StableSwap pool. (#279) --- Cargo.lock | 9 +- Cargo.toml | 2 +- contracts/oracle/osmosis/src/helpers.rs | 51 +++++- contracts/oracle/osmosis/src/price_source.rs | 13 +- contracts/oracle/osmosis/tests/helpers.rs | 75 ++++++-- .../oracle/osmosis/tests/test_query_price.rs | 10 +- .../osmosis/tests/test_set_price_source.rs | 9 + .../osmosis/tests/helpers.rs | 14 +- contracts/swapper/osmosis/src/route.rs | 7 +- packages/chains/osmosis/Cargo.toml | 1 + packages/chains/osmosis/src/helpers.rs | 164 ++++++++++++++---- packages/chains/osmosis/src/lib.rs | 4 + packages/testing/src/mars_mock_querier.rs | 5 +- packages/testing/src/osmosis_querier.rs | 9 +- 14 files changed, 283 insertions(+), 90 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8212e3e39..4e09fde35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1859,6 +1859,7 @@ version = "1.2.0" dependencies = [ "cosmwasm-std", "osmosis-std", + "prost 0.11.9", "serde", ] @@ -2256,9 +2257,9 @@ dependencies = [ [[package]] name = "osmosis-test-tube" -version = "16.0.1" +version = "16.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527375c01396e7e4de4ccc18a0141aeb6b342dc089d30c57282025f3a8753e72" +checksum = "4929047d1dcec5d7d02fd0a00ecdfca78918d3a33bffc193bf57b3eeb4d407ab" dependencies = [ "base64 0.13.1", "bindgen", @@ -3016,9 +3017,9 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e168eaaf71e8f9bd6037feb05190485708e019f4fd87d161b3c0a0d37daf85e5" +checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index da170bbbf..2c4861dcd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,7 +62,7 @@ neutron-sdk = "0.6.0" # dev-dependencies cw-multi-test = "0.16.5" cw-it = "0.1.0" -osmosis-test-tube = "16.0.0" +osmosis-test-tube = "=16.1.1" test-case = "3.0.0" proptest = "1.1.0" diff --git a/contracts/oracle/osmosis/src/helpers.rs b/contracts/oracle/osmosis/src/helpers.rs index 342c9e2ce..4ffc2a1ec 100644 --- a/contracts/oracle/osmosis/src/helpers.rs +++ b/contracts/oracle/osmosis/src/helpers.rs @@ -1,28 +1,63 @@ use mars_oracle_base::{ContractError, ContractResult}; -use mars_osmosis::helpers::{has_denom, Pool}; +use mars_osmosis::{ + helpers::{CommonPoolData, Pool}, + BalancerPool, +}; use crate::DowntimeDetector; /// 48 hours in seconds const TWO_DAYS_IN_SECONDS: u64 = 172800u64; -/// Assert the Osmosis pool indicated by `pool_id` is of XYK type and assets are OSMO and `denom` +/// Assert the Osmosis pool indicated by `pool_id` is of Balancer XYK or StableSwap and assets are OSMO and `denom` pub fn assert_osmosis_pool_assets( pool: &Pool, denom: &str, base_denom: &str, ) -> ContractResult<()> { - assert_osmosis_xyk_pool(pool)?; + match pool { + Pool::Balancer(balancer_pool) => { + assert_osmosis_xyk_pool(balancer_pool)?; + } + Pool::StableSwap(_) => {} + }; + + assert_osmosis_pool_contains_two_assets(pool, denom, base_denom)?; + + Ok(()) +} + +/// Assert the Osmosis pool indicated by `pool_id` is Balancer XYK type +pub fn assert_osmosis_xyk_lp_pool(pool: &Pool) -> ContractResult<()> { + match pool { + Pool::Balancer(balancer_pool) => assert_osmosis_xyk_pool(balancer_pool)?, + Pool::StableSwap(stable_swap_pool) => { + return Err(ContractError::InvalidPriceSource { + reason: format!("StableSwap pool not supported. Pool id {}", stable_swap_pool.id), + }); + } + }; + + Ok(()) +} + +fn assert_osmosis_pool_contains_two_assets( + pool: &Pool, + denom: &str, + base_denom: &str, +) -> ContractResult<()> { + let pool_id = pool.get_pool_id(); + let pool_denoms = pool.get_pool_denoms(); - if !has_denom(base_denom, &pool.pool_assets) { + if !pool_denoms.contains(&base_denom.to_string()) { return Err(ContractError::InvalidPriceSource { - reason: format!("pool {} does not contain the base denom {}", pool.id, base_denom), + reason: format!("pool {} does not contain the base denom {}", pool_id, base_denom), }); } - if !has_denom(denom, &pool.pool_assets) { + if !pool_denoms.contains(&denom.to_string()) { return Err(ContractError::InvalidPriceSource { - reason: format!("pool {} does not contain {}", pool.id, denom), + reason: format!("pool {} does not contain {}", pool_id, denom), }); } @@ -30,7 +65,7 @@ pub fn assert_osmosis_pool_assets( } /// Assert the Osmosis pool indicated by `pool_id` is of XYK type -pub fn assert_osmosis_xyk_pool(pool: &Pool) -> ContractResult<()> { +pub fn assert_osmosis_xyk_pool(pool: &BalancerPool) -> ContractResult<()> { if pool.pool_assets.len() != 2 { return Err(ContractError::InvalidPriceSource { reason: format!( diff --git a/contracts/oracle/osmosis/src/price_source.rs b/contracts/oracle/osmosis/src/price_source.rs index f1c1ed599..22f0eba1a 100644 --- a/contracts/oracle/osmosis/src/price_source.rs +++ b/contracts/oracle/osmosis/src/price_source.rs @@ -3,7 +3,8 @@ use std::{cmp::min, fmt}; use cosmwasm_std::{Addr, Decimal, Decimal256, Deps, Empty, Env, Isqrt, Uint128, Uint256}; use cw_storage_plus::Map; use mars_oracle_base::{ - ContractError::InvalidPrice, ContractResult, PriceSourceChecked, PriceSourceUnchecked, + ContractError::{self, InvalidPrice}, + ContractResult, PriceSourceChecked, PriceSourceUnchecked, }; use mars_osmosis::helpers::{ query_arithmetic_twap_price, query_geometric_twap_price, query_pool, query_spot_price, @@ -369,7 +370,7 @@ impl PriceSourceUnchecked for OsmosisPriceSour pool_id, } => { let pool = query_pool(&deps.querier, *pool_id)?; - helpers::assert_osmosis_xyk_pool(&pool)?; + helpers::assert_osmosis_xyk_lp_pool(&pool)?; Ok(OsmosisPriceSourceChecked::XykLiquidityToken { pool_id: *pool_id, }) @@ -593,6 +594,14 @@ impl OsmosisPriceSourceChecked { ) -> ContractResult { // XYK pool asserted during price source creation let pool = query_pool(&deps.querier, pool_id)?; + let pool = match pool { + Pool::Balancer(pool) => pool, + Pool::StableSwap(pool) => { + return Err(ContractError::InvalidPrice { + reason: format!("StableSwap pool not supported. Pool id {}", pool.id), + }) + } + }; let coin0 = Pool::unwrap_coin(&pool.pool_assets[0].token)?; let coin1 = Pool::unwrap_coin(&pool.pool_assets[1].token)?; diff --git a/contracts/oracle/osmosis/tests/helpers.rs b/contracts/oracle/osmosis/tests/helpers.rs index 80b683e54..82bb4ea76 100644 --- a/contracts/oracle/osmosis/tests/helpers.rs +++ b/contracts/oracle/osmosis/tests/helpers.rs @@ -9,10 +9,10 @@ use cosmwasm_std::{ }; use mars_oracle_base::ContractError; use mars_oracle_osmosis::{contract::entry, msg::ExecuteMsg, OsmosisPriceSourceUnchecked}; -use mars_osmosis::helpers::{Pool, QueryPoolResponse}; +use mars_osmosis::{BalancerPool, StableSwapPool}; use mars_red_bank_types::oracle::msg::{InstantiateMsg, QueryMsg}; use mars_testing::{mock_info, MarsMockQuerier}; -use osmosis_std::types::osmosis::gamm::v1beta1::PoolAsset; +use osmosis_std::types::osmosis::{gamm::v1beta1::PoolAsset, poolmanager::v1beta1::PoolResponse}; use pyth_sdk_cw::PriceIdentifier; pub fn setup_test_with_pools() -> OwnedDeps { @@ -22,25 +22,40 @@ pub fn setup_test_with_pools() -> OwnedDeps OwnedDeps OwnedDeps OwnedDeps OwnedDeps { deps } -pub fn prepare_query_pool_response( +pub fn prepare_query_balancer_pool_response( pool_id: u64, assets: &[Coin], weights: &[u64], shares: &Coin, -) -> QueryPoolResponse { - let pool = Pool { +) -> PoolResponse { + let pool = BalancerPool { address: "address".to_string(), - id: pool_id.to_string(), + id: pool_id, pool_params: None, future_pool_governor: "future_pool_governor".to_string(), total_shares: Some(osmosis_std::types::cosmos::base::v1beta1::Coin { @@ -131,8 +151,8 @@ pub fn prepare_query_pool_response( pool_assets: prepare_pool_assets(assets, weights), total_weight: "".to_string(), }; - QueryPoolResponse { - pool, + PoolResponse { + pool: Some(pool.to_any()), } } @@ -155,6 +175,33 @@ fn prepare_pool_assets(coins: &[Coin], weights: &[u64]) -> Vec { .collect() } +pub fn prepare_query_stable_swap_pool_response(pool_id: u64, assets: &[Coin]) -> PoolResponse { + let pool_liquidity: Vec<_> = assets + .iter() + .map(|coin| osmosis_std::types::cosmos::base::v1beta1::Coin { + denom: coin.denom.clone(), + amount: coin.amount.to_string(), + }) + .collect(); + + let pool = StableSwapPool { + address: "osmo15v4mn84s9flhzpstkf9ql2mu0rnxh42pm8zhq47kh2fzs5zlwjsqaterkr".to_string(), + id: pool_id, + pool_params: None, + future_pool_governor: "".to_string(), + total_shares: Some(osmosis_std::types::cosmos::base::v1beta1::Coin { + denom: format!("gamm/pool/{pool_id}"), + amount: 4497913440357232330148u128.to_string(), + }), + pool_liquidity, + scaling_factors: vec![100000u64, 113890u64], + scaling_factor_controller: "osmo1k8c2m5cn322akk5wy8lpt87dd2f4yh9afcd7af".to_string(), + }; + PoolResponse { + pool: Some(pool.to_any()), + } +} + pub fn set_pyth_price_source(deps: DepsMut, denom: &str, price_id: PriceIdentifier) { set_price_source( deps, diff --git a/contracts/oracle/osmosis/tests/test_query_price.rs b/contracts/oracle/osmosis/tests/test_query_price.rs index 0b330b91d..dc5a1c611 100644 --- a/contracts/oracle/osmosis/tests/test_query_price.rs +++ b/contracts/oracle/osmosis/tests/test_query_price.rs @@ -18,7 +18,7 @@ use osmosis_std::types::osmosis::{ }; use pyth_sdk_cw::{Price, PriceFeed, PriceFeedResponse, PriceIdentifier}; -use crate::helpers::prepare_query_pool_response; +use crate::helpers::prepare_query_balancer_pool_response; mod helpers; @@ -804,7 +804,7 @@ fn querying_xyk_lp_price() { let assets = vec![coin(1, "uatom"), coin(1, "uosmo")]; deps.querier.set_query_pool_response( 10001, - prepare_query_pool_response( + prepare_query_balancer_pool_response( 10001, &assets, &[5000u64, 5000u64], @@ -815,7 +815,7 @@ fn querying_xyk_lp_price() { let assets = vec![coin(1, "umars"), coin(1, "uosmo")]; deps.querier.set_query_pool_response( 10002, - prepare_query_pool_response( + prepare_query_balancer_pool_response( 10002, &assets, &[5000u64, 5000u64], @@ -826,7 +826,7 @@ fn querying_xyk_lp_price() { let assets = vec![coin(10000, "uatom"), coin(885000, "umars")]; deps.querier.set_query_pool_response( 10003, - prepare_query_pool_response( + prepare_query_balancer_pool_response( 10003, &assets, &[5000u64, 5000u64], @@ -889,7 +889,7 @@ fn querying_xyk_lp_price() { let assets = vec![coin(6389, "uatom"), coin(1385000, "umars")]; deps.querier.set_query_pool_response( 10003, - prepare_query_pool_response( + prepare_query_balancer_pool_response( 10003, &assets, &[5000u64, 5000u64], diff --git a/contracts/oracle/osmosis/tests/test_set_price_source.rs b/contracts/oracle/osmosis/tests/test_set_price_source.rs index f7f23c349..d56fa5c91 100644 --- a/contracts/oracle/osmosis/tests/test_set_price_source.rs +++ b/contracts/oracle/osmosis/tests/test_set_price_source.rs @@ -926,6 +926,15 @@ fn setting_price_source_xyk_lp() { } ); + // attempting to use StableSwap pool + let err = set_price_source_xyk_lp("atom_mars_lp", 5555).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "StableSwap pool not supported. Pool id 5555".to_string() + } + ); + // properly set xyk lp price source let res = set_price_source_xyk_lp("uosmo_umars_lp", 89).unwrap(); assert_eq!(res.messages.len(), 0); diff --git a/contracts/rewards-collector/osmosis/tests/helpers.rs b/contracts/rewards-collector/osmosis/tests/helpers.rs index a7872d802..6760a31b3 100644 --- a/contracts/rewards-collector/osmosis/tests/helpers.rs +++ b/contracts/rewards-collector/osmosis/tests/helpers.rs @@ -5,11 +5,11 @@ use cosmwasm_std::{ testing::{mock_env, MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR}, Coin, Decimal, Deps, OwnedDeps, }; -use mars_osmosis::helpers::{Pool, QueryPoolResponse}; +use mars_osmosis::BalancerPool; use mars_red_bank_types::rewards_collector::{Config, InstantiateMsg, QueryMsg}; use mars_rewards_collector_osmosis::entry; use mars_testing::{mock_info, MarsMockQuerier}; -use osmosis_std::types::osmosis::gamm::v1beta1::PoolAsset; +use osmosis_std::types::osmosis::{gamm::v1beta1::PoolAsset, poolmanager::v1beta1::PoolResponse}; pub fn mock_instantiate_msg() -> InstantiateMsg { InstantiateMsg { @@ -93,10 +93,10 @@ fn prepare_query_pool_response( assets: &[Coin], weights: &[u64], shares: &Coin, -) -> QueryPoolResponse { - let pool = Pool { +) -> PoolResponse { + let pool = BalancerPool { address: "address".to_string(), - id: pool_id.to_string(), + id: pool_id, pool_params: None, future_pool_governor: "future_pool_governor".to_string(), total_shares: Some(osmosis_std::types::cosmos::base::v1beta1::Coin { @@ -106,8 +106,8 @@ fn prepare_query_pool_response( pool_assets: prepare_pool_assets(assets, weights), total_weight: "".to_string(), }; - QueryPoolResponse { - pool, + PoolResponse { + pool: Some(pool.to_any()), } } diff --git a/contracts/swapper/osmosis/src/route.rs b/contracts/swapper/osmosis/src/route.rs index 81486db4a..698eba5b5 100644 --- a/contracts/swapper/osmosis/src/route.rs +++ b/contracts/swapper/osmosis/src/route.rs @@ -2,7 +2,7 @@ use std::fmt; use cosmwasm_schema::cw_serde; use cosmwasm_std::{BlockInfo, CosmosMsg, Decimal, Empty, Env, Fraction, QuerierWrapper, Uint128}; -use mars_osmosis::helpers::{has_denom, query_arithmetic_twap_price, query_pool}; +use mars_osmosis::helpers::{query_arithmetic_twap_price, query_pool, CommonPoolData}; use mars_red_bank_types::swapper::EstimateExactInSwapResponse; use mars_swapper_base::{ContractError, ContractResult, Route}; use osmosis_std::types::osmosis::gamm::v1beta1::MsgSwapExactAmountIn; @@ -52,8 +52,9 @@ impl Route for OsmosisRoute { let mut seen_denoms = hashset(&[denom_in]); for (i, step) in steps.iter().enumerate() { let pool = query_pool(querier, step.pool_id)?; + let pool_denoms = pool.get_pool_denoms(); - if !has_denom(prev_denom_out, &pool.pool_assets) { + if !pool_denoms.contains(&prev_denom_out.to_string()) { return Err(ContractError::InvalidRoute { reason: format!( "step {}: pool {} does not contain input denom {}", @@ -64,7 +65,7 @@ impl Route for OsmosisRoute { }); } - if !has_denom(&step.token_out_denom, &pool.pool_assets) { + if !pool_denoms.contains(&step.token_out_denom) { return Err(ContractError::InvalidRoute { reason: format!( "step {}: pool {} does not contain output denom {}", diff --git a/packages/chains/osmosis/Cargo.toml b/packages/chains/osmosis/Cargo.toml index d2af81999..7f44f9934 100755 --- a/packages/chains/osmosis/Cargo.toml +++ b/packages/chains/osmosis/Cargo.toml @@ -21,3 +21,4 @@ backtraces = ["cosmwasm-std/backtraces"] cosmwasm-std = { workspace = true } osmosis-std = { workspace = true } serde = { workspace = true } +prost = { workspace = true } diff --git a/packages/chains/osmosis/src/helpers.rs b/packages/chains/osmosis/src/helpers.rs index 1503569fd..6cb9e204d 100644 --- a/packages/chains/osmosis/src/helpers.rs +++ b/packages/chains/osmosis/src/helpers.rs @@ -3,9 +3,6 @@ use std::str::FromStr; use cosmwasm_std::{ coin, Decimal, Empty, QuerierWrapper, QueryRequest, StdError, StdResult, Uint128, }; -/// FIXME: migrate to Spot queries from PoolManager once whitelisted in https://github.com/osmosis-labs/osmosis/blob/main/wasmbinding/stargate_whitelist.go#L127 -#[allow(deprecated)] -use osmosis_std::types::osmosis::gamm::v1beta1::QueryPoolRequest as PoolRequest; use osmosis_std::{ shim::{Duration, Timestamp}, types::{ @@ -13,26 +10,67 @@ use osmosis_std::{ osmosis::{ downtimedetector::v1beta1::DowntimedetectorQuerier, gamm::{ - v1beta1::{PoolAsset, PoolParams}, - v2::GammQuerier, + poolmodels::stableswap::v1beta1::Pool as StableSwapPool, + v1beta1::Pool as BalancerPool, }, + poolmanager::v1beta1::{PoolRequest, PoolResponse, PoolmanagerQuerier}, twap::v1beta1::TwapQuerier, }, }, }; -use serde::{Deserialize, Serialize}; - -// NOTE: Use custom Pool (`id` type as String) due to problem with json (de)serialization discrepancy between go and rust side. -// https://github.com/osmosis-labs/osmosis-rust/issues/42 -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct Pool { - pub id: String, - pub address: String, - pub pool_params: Option, - pub future_pool_governor: String, - pub pool_assets: Vec, - pub total_shares: Option, - pub total_weight: String, +use prost::Message; + +// Get denoms from different type of the pool +pub trait CommonPoolData { + fn get_pool_id(&self) -> u64; + fn get_pool_denoms(&self) -> Vec; +} + +#[derive(Debug, PartialEq)] +pub enum Pool { + Balancer(BalancerPool), + StableSwap(StableSwapPool), +} + +impl CommonPoolData for Pool { + fn get_pool_id(&self) -> u64 { + match self { + Pool::Balancer(pool) => pool.id, + Pool::StableSwap(pool) => pool.id, + } + } + + fn get_pool_denoms(&self) -> Vec { + match self { + Pool::Balancer(pool) => pool + .pool_assets + .iter() + .flat_map(|asset| &asset.token) + .map(|token| token.denom.clone()) + .collect(), + Pool::StableSwap(pool) => { + pool.pool_liquidity.iter().map(|pl| pl.denom.clone()).collect() + } + } + } +} + +impl TryFrom for Pool { + type Error = StdError; + + fn try_from(value: osmosis_std::shim::Any) -> Result { + if let Ok(pool) = BalancerPool::decode(value.value.as_slice()) { + return Ok(Pool::Balancer(pool)); + } + if let Ok(pool) = StableSwapPool::decode(value.value.as_slice()) { + return Ok(Pool::StableSwap(pool)); + } + + Err(StdError::parse_err( + "Pool", + "Unsupported pool: must be either `Balancer` or `StableSwap`.", + )) + } } impl Pool { @@ -48,39 +86,24 @@ impl Pool { } } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct QueryPoolResponse { - pub pool: Pool, -} - /// Query an Osmosis pool's coin depths and the supply of of liquidity token -/// -/// FIXME: migrate to Spot queries from PoolManager once whitelisted in https://github.com/osmosis-labs/osmosis/blob/main/wasmbinding/stargate_whitelist.go#L127 -#[allow(deprecated)] pub fn query_pool(querier: &QuerierWrapper, pool_id: u64) -> StdResult { let req: QueryRequest = PoolRequest { pool_id, } .into(); - let res: QueryPoolResponse = querier.query(&req)?; - Ok(res.pool) -} - -pub fn has_denom(denom: &str, pool_assets: &[PoolAsset]) -> bool { - pool_assets.iter().flat_map(|asset| &asset.token).any(|coin| coin.denom == denom) + let res: PoolResponse = querier.query(&req)?; + res.pool.ok_or_else(|| StdError::not_found("pool"))?.try_into() // convert `Any` to `Pool` } /// Query the spot price of a coin, denominated in OSMO -/// -/// FIXME: migrate to Spot queries from PoolManager once whitelisted in https://github.com/osmosis-labs/osmosis/blob/main/wasmbinding/stargate_whitelist.go#L127 -#[allow(deprecated)] pub fn query_spot_price( querier: &QuerierWrapper, pool_id: u64, base_denom: &str, quote_denom: &str, ) -> StdResult { - let spot_price_res = GammQuerier::new(querier).spot_price( + let spot_price_res = PoolmanagerQuerier::new(querier).spot_price( pool_id, base_denom.to_string(), quote_denom.to_string(), @@ -154,12 +177,14 @@ pub fn recovered_since_downtime_of_length( #[cfg(test)] mod tests { + use osmosis_std::types::osmosis::gamm::v1beta1::PoolAsset; + use super::*; #[test] fn unwrapping_coin() { - let pool = Pool { - id: "1111".to_string(), + let pool = BalancerPool { + id: 1111, address: "".to_string(), pool_params: None, future_pool_governor: "".to_string(), @@ -191,4 +216,67 @@ mod tests { let res = Pool::unwrap_coin(&pool.pool_assets[1].token).unwrap(); assert_eq!(res, coin(430, "denom_2")); } + + #[test] + fn common_data_for_balancer_pool() { + let balancer_pool = BalancerPool { + id: 1111, + address: "".to_string(), + pool_params: None, + future_pool_governor: "".to_string(), + pool_assets: vec![ + PoolAsset { + token: Some(Coin { + denom: "denom_1".to_string(), + amount: "123".to_string(), + }), + weight: "500".to_string(), + }, + PoolAsset { + token: Some(Coin { + denom: "denom_2".to_string(), + amount: "430".to_string(), + }), + weight: "500".to_string(), + }, + ], + total_shares: None, + total_weight: "".to_string(), + }; + + let any_pool = balancer_pool.to_any(); + let pool: Pool = any_pool.try_into().unwrap(); + + assert_eq!(balancer_pool.id, pool.get_pool_id()); + assert_eq!(vec!["denom_1".to_string(), "denom_2".to_string()], pool.get_pool_denoms()) + } + + #[test] + fn common_data_for_stable_swap_pool() { + let stable_swap_pool = StableSwapPool { + address: "".to_string(), + id: 4444, + pool_params: None, + future_pool_governor: "".to_string(), + total_shares: None, + pool_liquidity: vec![ + Coin { + denom: "denom_1".to_string(), + amount: "123".to_string(), + }, + Coin { + denom: "denom_2".to_string(), + amount: "430".to_string(), + }, + ], + scaling_factors: vec![], + scaling_factor_controller: "".to_string(), + }; + + let any_pool = stable_swap_pool.to_any(); + let pool: Pool = any_pool.try_into().unwrap(); + + assert_eq!(stable_swap_pool.id, pool.get_pool_id()); + assert_eq!(vec!["denom_1".to_string(), "denom_2".to_string()], pool.get_pool_denoms()) + } } diff --git a/packages/chains/osmosis/src/lib.rs b/packages/chains/osmosis/src/lib.rs index 1630fabcd..cfa498093 100644 --- a/packages/chains/osmosis/src/lib.rs +++ b/packages/chains/osmosis/src/lib.rs @@ -1 +1,5 @@ pub mod helpers; + +pub use osmosis_std::types::osmosis::gamm::{ + poolmodels::stableswap::v1beta1::Pool as StableSwapPool, v1beta1::Pool as BalancerPool, +}; diff --git a/packages/testing/src/mars_mock_querier.rs b/packages/testing/src/mars_mock_querier.rs index ea89017ea..a45dd6108 100644 --- a/packages/testing/src/mars_mock_querier.rs +++ b/packages/testing/src/mars_mock_querier.rs @@ -9,12 +9,11 @@ use mars_oracle_osmosis::{ stride::{Price, RedemptionRateResponse}, DowntimeDetector, }; -use mars_osmosis::helpers::QueryPoolResponse; use mars_params::types::asset::AssetParams; use mars_red_bank_types::{address_provider, incentives, oracle, red_bank}; use osmosis_std::types::osmosis::{ downtimedetector::v1beta1::RecoveredSinceDowntimeOfLengthResponse, - poolmanager::v1beta1::SpotPriceResponse, + poolmanager::v1beta1::{PoolResponse, SpotPriceResponse}, twap::v1beta1::{ArithmeticTwapToNowResponse, GeometricTwapToNowResponse}, }; use pyth_sdk_cw::{PriceFeedResponse, PriceIdentifier}; @@ -97,7 +96,7 @@ impl MarsMockQuerier { ); } - pub fn set_query_pool_response(&mut self, pool_id: u64, pool_response: QueryPoolResponse) { + pub fn set_query_pool_response(&mut self, pool_id: u64, pool_response: PoolResponse) { self.osmosis_querier.pools.insert(pool_id, pool_response); } diff --git a/packages/testing/src/osmosis_querier.rs b/packages/testing/src/osmosis_querier.rs index 539ba769a..9773e8772 100644 --- a/packages/testing/src/osmosis_querier.rs +++ b/packages/testing/src/osmosis_querier.rs @@ -1,12 +1,11 @@ use std::collections::HashMap; use cosmwasm_std::{to_binary, Binary, ContractResult, QuerierResult, SystemError}; -use mars_osmosis::helpers::QueryPoolResponse; use osmosis_std::types::osmosis::{ downtimedetector::v1beta1::{ RecoveredSinceDowntimeOfLengthRequest, RecoveredSinceDowntimeOfLengthResponse, }, - poolmanager::v1beta1::{PoolRequest, SpotPriceRequest, SpotPriceResponse}, + poolmanager::v1beta1::{PoolRequest, PoolResponse, SpotPriceRequest, SpotPriceResponse}, twap::v1beta1::{ ArithmeticTwapToNowRequest, ArithmeticTwapToNowResponse, GeometricTwapToNowRequest, GeometricTwapToNowResponse, @@ -23,7 +22,7 @@ pub struct PriceKey { #[derive(Clone, Default)] pub struct OsmosisQuerier { - pub pools: HashMap, + pub pools: HashMap, pub spot_prices: HashMap, pub arithmetic_twap_prices: HashMap, @@ -34,7 +33,7 @@ pub struct OsmosisQuerier { impl OsmosisQuerier { pub fn handle_stargate_query(&self, path: &str, data: &Binary) -> Result { - if path == "/osmosis.gamm.v1beta1.Query/Pool" { + if path == "/osmosis.poolmanager.v1beta1.Query/Pool" { let parse_osmosis_query: Result = Message::decode(data.as_slice()); if let Ok(osmosis_query) = parse_osmosis_query { @@ -42,7 +41,7 @@ impl OsmosisQuerier { } } - if path == "/osmosis.gamm.v2.Query/SpotPrice" { + if path == "/osmosis.poolmanager.v1beta1.Query/SpotPrice" { let parse_osmosis_query: Result = Message::decode(data.as_slice()); if let Ok(osmosis_query) = parse_osmosis_query { From 2388b76e556ba94f66c2420686605d361c7749e3 Mon Sep 17 00:00:00 2001 From: larry <26318510+larry0x@users.noreply.github.com> Date: Wed, 2 Aug 2023 12:52:44 +0100 Subject: [PATCH 13/43] Shared deposit cap (#280) * add credit manager to address provider * extract interest rate logics to a separate library * query to return `None` on nonexistent market * add `total_deposit` query to mars-params * check total deposit when user deposits * add debt to mock querier * mock querier to return `Option` * add test for computing total deposit * add total deposit to mock params querier * Move deposit_cap from RedBank struct in params contract. --------- Co-authored-by: piobab --- Cargo.lock | 101 ++++---- Cargo.toml | 4 +- contracts/params/Cargo.toml | 4 + contracts/params/src/contract.rs | 14 +- contracts/params/src/msg.rs | 11 +- contracts/params/src/query.rs | 84 ++++++- contracts/params/src/state.rs | 1 + contracts/params/src/types/asset.rs | 4 +- contracts/params/tests/helpers/generator.rs | 2 +- contracts/params/tests/helpers/mock_env.rs | 1 + contracts/params/tests/test_deposit_cap.rs | 67 +++++ contracts/red-bank/Cargo.toml | 2 + contracts/red-bank/src/borrow.rs | 8 +- contracts/red-bank/src/deposit.rs | 14 +- contracts/red-bank/src/health.rs | 2 +- contracts/red-bank/src/helpers.rs | 15 +- contracts/red-bank/src/interest_rates.rs | 232 +----------------- contracts/red-bank/src/liquidate.rs | 9 +- contracts/red-bank/src/query.rs | 16 +- contracts/red-bank/src/repay.rs | 6 +- contracts/red-bank/src/withdraw.rs | 6 +- contracts/red-bank/tests/helpers.rs | 10 +- contracts/red-bank/tests/test_admin.rs | 2 +- contracts/red-bank/tests/test_borrow.rs | 9 +- contracts/red-bank/tests/test_deposit.rs | 83 ++++--- contracts/red-bank/tests/test_liquidate.rs | 8 +- contracts/red-bank/tests/test_misc.rs | 8 +- contracts/red-bank/tests/test_query.rs | 2 +- contracts/red-bank/tests/test_withdraw.rs | 8 +- integration-tests/tests/helpers.rs | 4 +- integration-tests/tests/test_oracles.rs | 13 + .../tests/test_from_coins_to_positions.rs | 4 +- .../health/tests/test_health_from_coins.rs | 6 +- packages/interest-rate/Cargo.toml | 25 ++ packages/interest-rate/src/lib.rs | 222 +++++++++++++++++ packages/testing/src/integration/mock_env.rs | 5 +- packages/testing/src/mars_mock_querier.rs | 16 ++ packages/testing/src/params_querier.rs | 13 +- packages/testing/src/red_bank_querier.rs | 18 +- schemas/mars-params/mars-params.json | 74 +++++- scripts/deploy/base/deployer.ts | 3 +- scripts/deploy/neutron/config_mainnet.ts | 6 +- scripts/deploy/neutron/config_testnet.ts | 6 +- .../deploy/neutron/config_testnet_multisig.ts | 6 +- scripts/deploy/osmosis/config.ts | 12 +- scripts/deploy/osmosis/mainnetConfig.ts | 6 +- scripts/deploy/osmosis/testnetConfig.ts | 6 +- scripts/types/config.ts | 2 + .../mars-params/MarsParams.client.ts | 9 + .../mars-params/MarsParams.react-query.ts | 25 ++ .../generated/mars-params/MarsParams.types.ts | 9 +- 51 files changed, 797 insertions(+), 426 deletions(-) create mode 100644 contracts/params/tests/test_deposit_cap.rs create mode 100644 packages/interest-rate/Cargo.toml create mode 100644 packages/interest-rate/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 90a254dd8..288a878e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -293,7 +293,7 @@ checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -342,9 +342,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.20.0" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" [[package]] name = "base64ct" @@ -736,7 +736,7 @@ dependencies = [ "hex", "schemars", "serde", - "serde-json-wasm 0.5.1", + "serde-json-wasm", "sha2 0.10.7", "thiserror", ] @@ -1164,9 +1164,9 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" dependencies = [ "errno-dragonfly", "libc", @@ -1296,7 +1296,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -1711,9 +1711,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" [[package]] name = "log" @@ -1789,6 +1789,16 @@ dependencies = [ "serde", ] +[[package]] +name = "mars-interest-rate" +version = "1.2.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "mars-red-bank-types", + "mars-utils", +] + [[package]] name = "mars-liquidation" version = "1.0.0" @@ -1886,10 +1896,14 @@ dependencies = [ "cw-multi-test", "cw-storage-plus 1.1.0", "cw2 1.1.0", + "mars-interest-rate", "mars-owner", + "mars-red-bank-types", + "mars-testing", "mars-utils", "schemars", "serde", + "test-case", "thiserror", ] @@ -1905,12 +1919,14 @@ dependencies = [ "cw-utils 1.0.1", "cw2 1.1.0", "mars-health", + "mars-interest-rate", "mars-liquidation", "mars-owner", "mars-params", "mars-red-bank-types", "mars-testing", "mars-utils", + "test-case", "thiserror", ] @@ -2116,11 +2132,11 @@ dependencies = [ [[package]] name = "neutron-sdk" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7cc760801f3ed881155431c6a0102c1a8df178430af341c9f8008951ceb0721" +checksum = "adfc6f92cae61b5af9014c09b7bac25ac95b7442be38441a7103377a8edfd37c" dependencies = [ - "base64 0.20.0", + "base64 0.21.2", "bech32", "cosmos-sdk-proto 0.16.0", "cosmwasm-schema", @@ -2130,7 +2146,7 @@ dependencies = [ "protobuf 3.2.0", "schemars", "serde", - "serde-json-wasm 0.4.1", + "serde-json-wasm", "serde_json", "thiserror", ] @@ -2337,9 +2353,9 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pest" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d2d1d55045829d65aad9d389139882ad623b33b904e7c9f1b10c5b8927298e5" +checksum = "1acb4a4365a13f749a93f1a094a7805e5cfa0955373a9de860d962eaa3a5fe5a" dependencies = [ "thiserror", "ucd-trie", @@ -2347,9 +2363,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f94bca7e7a599d89dea5dfa309e217e7906c3c007fb9c3299c40b10d6a315d3" +checksum = "666d00490d4ac815001da55838c500eafb0320019bbaa44444137c48b443a853" dependencies = [ "pest", "pest_generator", @@ -2357,22 +2373,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d490fe7e8556575ff6911e45567ab95e71617f43781e5c05490dc8d75c965c" +checksum = "68ca01446f50dbda87c1786af8770d535423fa8a53aec03b8f4e3d7eb10e0929" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] name = "pest_meta" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2674c66ebb4b4d9036012091b537aae5878970d6999f81a265034d85b136b341" +checksum = "56af0a30af74d0445c0bf6d9d051c979b516a1a5af790d251daee76005420a48" dependencies = [ "once_cell", "pest", @@ -2396,7 +2412,7 @@ checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -2682,9 +2698,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294" dependencies = [ "aho-corasick", "memchr", @@ -2941,9 +2957,9 @@ checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" [[package]] name = "serde" -version = "1.0.176" +version = "1.0.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76dc28c9523c5d70816e393136b86d48909cfb27cecaa902d338c19ed47164dc" +checksum = "0ea67f183f058fe88a4e3ec6e2788e003840893b91bac4559cabedd00863b3ed" dependencies = [ "serde_derive", ] @@ -2957,15 +2973,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde-json-wasm" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479b4dbc401ca13ee8ce902851b834893251404c4f3c65370a49e047a6be09a5" -dependencies = [ - "serde", -] - [[package]] name = "serde-json-wasm" version = "0.5.1" @@ -2986,13 +2993,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.176" +version = "1.0.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e7b8c5dc823e3b90651ff1d3808419cd14e5ad76de04feaf37da114e7a306f" +checksum = "24e744d7782b686ab3b73267ef05697159cc0e5abbed3f47f9933165e5219036" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -3025,7 +3032,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -3207,9 +3214,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.27" +version = "2.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" dependencies = [ "proc-macro2", "quote", @@ -3426,7 +3433,7 @@ checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -3487,7 +3494,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] [[package]] @@ -3691,7 +3698,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", "wasm-bindgen-shared", ] @@ -3713,7 +3720,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3887,5 +3894,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.28", ] diff --git a/Cargo.toml b/Cargo.toml index 2783ad574..83cd8ee78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "contracts/rewards-collector/*", "packages/chains/*", "packages/health", + "packages/interest-rate", "packages/liquidation", "packages/testing", "packages/types", @@ -68,9 +69,9 @@ proptest = "1.1.0" # packages mars-health = { path = "./packages/health" } +mars-interest-rate = { path = "./packages/interest-rate" } mars-liquidation = { path = "./packages/liquidation" } mars-osmosis = { path = "./packages/chains/osmosis" } -mars-params = { path = "./contracts/params" } mars-red-bank-types = { path = "./packages/types" } mars-testing = { path = "./packages/testing" } mars-utils = { path = "./packages/utils" } @@ -81,6 +82,7 @@ mars-incentives = { path = "./contracts/incentives" } mars-oracle-base = { path = "./contracts/oracle/base" } mars-oracle-osmosis = { path = "./contracts/oracle/osmosis" } mars-oracle-wasm = { path = "./contracts/oracle/wasm" } +mars-params = { path = "./contracts/params" } mars-red-bank = { path = "./contracts/red-bank" } mars-rewards-collector-base = { path = "./contracts/rewards-collector/base" } mars-rewards-collector-osmosis = { path = "./contracts/rewards-collector/osmosis" } diff --git a/contracts/params/Cargo.toml b/contracts/params/Cargo.toml index 1f449d6fe..88b963499 100644 --- a/contracts/params/Cargo.toml +++ b/contracts/params/Cargo.toml @@ -24,7 +24,9 @@ cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw2 = { workspace = true } cw-storage-plus = { workspace = true } +mars-interest-rate = { workspace = true } mars-owner = { workspace = true } +mars-red-bank-types = { workspace = true } mars-utils = { workspace = true } schemars = { workspace = true } serde = { workspace = true } @@ -33,3 +35,5 @@ thiserror = { workspace = true } [dev-dependencies] anyhow = { workspace = true } cw-multi-test = { workspace = true } +mars-testing = { workspace = true } +test-case = { workspace = true } diff --git a/contracts/params/src/contract.rs b/contracts/params/src/contract.rs index 71ffd2b0c..ab9b04e64 100644 --- a/contracts/params/src/contract.rs +++ b/contracts/params/src/contract.rs @@ -12,8 +12,10 @@ use crate::{ CmEmergencyUpdate, EmergencyUpdate, ExecuteMsg, InstantiateMsg, QueryMsg, RedBankEmergencyUpdate, }, - query::{query_all_asset_params, query_all_vault_configs, query_vault_config}, - state::{ASSET_PARAMS, OWNER, TARGET_HEALTH_FACTOR}, + query::{ + query_all_asset_params, query_all_vault_configs, query_total_deposit, query_vault_config, + }, + state::{ADDRESS_PROVIDER, ASSET_PARAMS, OWNER, TARGET_HEALTH_FACTOR}, }; const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); @@ -36,6 +38,9 @@ pub fn instantiate( }, )?; + let address_provider_addr = deps.api.addr_validate(&msg.address_provider)?; + ADDRESS_PROVIDER.save(deps.storage, &address_provider_addr)?; + assert_thf(msg.target_health_factor)?; TARGET_HEALTH_FACTOR.save(deps.storage, &msg.target_health_factor)?; @@ -72,7 +77,7 @@ pub fn execute( } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> ContractResult { +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> ContractResult { let res = match msg { QueryMsg::Owner {} => to_binary(&OWNER.query(deps.storage)?), QueryMsg::AssetParams { @@ -90,6 +95,9 @@ pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> ContractResult { limit, } => to_binary(&query_all_vault_configs(deps, start_after, limit)?), QueryMsg::TargetHealthFactor {} => to_binary(&TARGET_HEALTH_FACTOR.load(deps.storage)?), + QueryMsg::TotalDeposit { + denom, + } => to_binary(&query_total_deposit(deps, &env, denom)?), }; res.map_err(Into::into) } diff --git a/contracts/params/src/msg.rs b/contracts/params/src/msg.rs index 836691097..7f192d08d 100644 --- a/contracts/params/src/msg.rs +++ b/contracts/params/src/msg.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::Decimal; +use cosmwasm_std::{Coin, Decimal}; use mars_owner::OwnerUpdate; use crate::types::{asset::AssetParamsUnchecked, vault::VaultConfigUnchecked}; @@ -8,6 +8,8 @@ use crate::types::{asset::AssetParamsUnchecked, vault::VaultConfigUnchecked}; pub struct InstantiateMsg { /// Contract's owner pub owner: String, + /// Address of the address provider contract + pub address_provider: String, /// Determines the ideal HF a position should be left at immediately after the position has been liquidated. pub target_health_factor: Decimal, } @@ -52,6 +54,13 @@ pub enum QueryMsg { #[returns(Decimal)] TargetHealthFactor {}, + + /// Compute the total amount deposited of the given asset across Red Bank + /// and Credit Manager. + #[returns(Coin)] + TotalDeposit { + denom: String, + }, } #[cw_serde] diff --git a/contracts/params/src/query.rs b/contracts/params/src/query.rs index b9ec3b109..d7a5b8256 100644 --- a/contracts/params/src/query.rs +++ b/contracts/params/src/query.rs @@ -1,8 +1,13 @@ -use cosmwasm_std::{Addr, Deps, Order, StdResult}; +use cosmwasm_std::{Addr, Coin, Deps, Env, Order, StdResult, Uint128}; use cw_storage_plus::Bound; +use mars_interest_rate::get_underlying_liquidity_amount; +use mars_red_bank_types::{ + address_provider::{self, MarsAddressType}, + red_bank::{self, Market, UserDebtResponse}, +}; use crate::{ - state::{ASSET_PARAMS, VAULT_CONFIGS}, + state::{ADDRESS_PROVIDER, ASSET_PARAMS, VAULT_CONFIGS}, types::{asset::AssetParams, vault::VaultConfig}, }; @@ -49,3 +54,78 @@ pub fn query_all_vault_configs( .map(|res| Ok(res?.1)) .collect() } + +/// Query and compute the total deposited amount of the given asset across Red +/// Bank (RB) and Credit Manager (CM). +/// +/// Specifically, the amount is defined as: +/// rb_deposit + cm_deposit - cm_debt_owed_to_rb +/// +/// Note: +/// +/// 1. We subtract the amount of debt that CM owes to RB to avoid double- +/// counting. +/// +/// 2. We only consider spot asset holdings, meaning we don't unwrap DEX LP +/// tokens or vault tokens to the underlying assets. After some discussions +/// we have concluded the latter is not feasible. +/// +/// For example, when computing the deposited amount of ATOM, we only include +/// ATOM deposited in RB and CM; we don't include the ATOM-OSMO LP token, or +/// the ATOM-OSMO farming vault. +pub fn query_total_deposit(deps: Deps, env: &Env, denom: String) -> StdResult { + let current_timestamp = env.block.time.seconds(); + + // query contract addresses + let address_provider_addr = ADDRESS_PROVIDER.load(deps.storage)?; + let addresses = address_provider::helpers::query_contract_addrs( + deps, + &address_provider_addr, + vec![MarsAddressType::RedBank, MarsAddressType::CreditManager], + )?; + let credit_manager_addr = &addresses[&MarsAddressType::CreditManager]; + let red_bank_addr = &addresses[&MarsAddressType::RedBank]; + + // amount of this asset deposited into Red Bank + // if the market doesn't exist on RB, we default to zero + let rb_deposit = deps + .querier + .query_wasm_smart::>( + red_bank_addr, + &red_bank::QueryMsg::Market { + denom: denom.clone(), + }, + )? + .map(|market| { + get_underlying_liquidity_amount( + market.collateral_total_scaled, + &market, + current_timestamp, + ) + }) + .transpose()? + .unwrap_or_else(Uint128::zero); + + // amount of debt in this asset the Credit Manager owes to Red Bank + // this query returns zero if no debt is owed + let cm_debt = deps + .querier + .query_wasm_smart::( + red_bank_addr, + &red_bank::QueryMsg::UserDebt { + user: credit_manager_addr.into(), + denom: denom.clone(), + }, + )? + .amount; + + // amount of this asset deposited into Credit Manager + // this is simply the coin balance of the CM contract + // note that this way, we don't include LP tokens or vault positions + let cm_deposit = deps.querier.query_balance(credit_manager_addr, &denom)?.amount; + + Ok(Coin { + denom, + amount: rb_deposit.checked_add(cm_deposit)?.checked_sub(cm_debt)?, + }) +} diff --git a/contracts/params/src/state.rs b/contracts/params/src/state.rs index efa0169a6..0259a0377 100644 --- a/contracts/params/src/state.rs +++ b/contracts/params/src/state.rs @@ -5,6 +5,7 @@ use mars_owner::Owner; use crate::types::{asset::AssetParams, vault::VaultConfig}; pub const OWNER: Owner = Owner::new("owner"); +pub const ADDRESS_PROVIDER: Item = Item::new("address_provider"); pub const ASSET_PARAMS: Map<&str, AssetParams> = Map::new("asset_params"); pub const VAULT_CONFIGS: Map<&Addr, VaultConfig> = Map::new("vault_configs"); 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 bd606a4e1..2021c317e 100644 --- a/contracts/params/src/types/asset.rs +++ b/contracts/params/src/types/asset.rs @@ -21,7 +21,6 @@ pub struct CmSettings { pub struct RedBankSettings { pub deposit_enabled: bool, pub borrow_enabled: bool, - pub deposit_cap: Uint128, } /// The LB will depend on the Health Factor and a couple other parameters as follows: @@ -122,6 +121,7 @@ pub struct AssetParamsBase { pub liquidation_threshold: Decimal, pub liquidation_bonus: LiquidationBonus, pub protocol_liquidation_fee: Decimal, + pub deposit_cap: Uint128, } pub type AssetParams = AssetParamsBase; @@ -140,6 +140,7 @@ impl From for AssetParamsUnchecked { liquidation_threshold: p.liquidation_threshold, liquidation_bonus: p.liquidation_bonus, protocol_liquidation_fee: p.protocol_liquidation_fee, + deposit_cap: p.deposit_cap, } } } @@ -174,6 +175,7 @@ impl AssetParamsUnchecked { liquidation_threshold: self.liquidation_threshold, liquidation_bonus: self.liquidation_bonus.clone(), protocol_liquidation_fee: self.protocol_liquidation_fee, + deposit_cap: self.deposit_cap, }) } } diff --git a/contracts/params/tests/helpers/generator.rs b/contracts/params/tests/helpers/generator.rs index e5ee7dbc1..267b0ab36 100644 --- a/contracts/params/tests/helpers/generator.rs +++ b/contracts/params/tests/helpers/generator.rs @@ -16,7 +16,6 @@ pub fn default_asset_params(denom: &str) -> AssetParamsUnchecked { 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(), @@ -27,6 +26,7 @@ pub fn default_asset_params(denom: &str) -> AssetParamsUnchecked { max_lb: Decimal::percent(8), }, protocol_liquidation_fee: Decimal::percent(2), + deposit_cap: Uint128::new(1_000_000_000), } } diff --git a/contracts/params/tests/helpers/mock_env.rs b/contracts/params/tests/helpers/mock_env.rs index e5b291d2f..729a9d331 100644 --- a/contracts/params/tests/helpers/mock_env.rs +++ b/contracts/params/tests/helpers/mock_env.rs @@ -187,6 +187,7 @@ impl MockEnvBuilder { Addr::unchecked("owner"), &InstantiateMsg { owner: "owner".to_string(), + address_provider: "address_provider".to_string(), target_health_factor: self.get_target_health_factor(), }, &[], diff --git a/contracts/params/tests/test_deposit_cap.rs b/contracts/params/tests/test_deposit_cap.rs new file mode 100644 index 000000000..302f6192a --- /dev/null +++ b/contracts/params/tests/test_deposit_cap.rs @@ -0,0 +1,67 @@ +use std::str::FromStr; + +use cosmwasm_std::{coins, Addr, Decimal, Uint128}; +use mars_interest_rate::get_underlying_liquidity_amount; +use mars_params::{query::query_total_deposit, state::ADDRESS_PROVIDER}; +use mars_red_bank_types::red_bank::{Market, UserDebtResponse}; +use mars_testing::{mock_dependencies, mock_env_at_block_time}; +use test_case::test_case; + +const CREDIT_MANAGER: &str = "credit_manager"; +const MOCK_DENOM: &str = "utoken"; +const TIMESTAMP: u64 = 1690573960; + +#[test_case( + Market { + denom: MOCK_DENOM.into(), + collateral_total_scaled: Uint128::zero(), + liquidity_index: Decimal::one(), + indexes_last_updated: TIMESTAMP, + ..Default::default() + }, + UserDebtResponse { + denom: MOCK_DENOM.into(), + amount_scaled: Uint128::zero(), + amount: Uint128::zero(), + uncollateralized: true, + }, + Uint128::zero(); + "zero liquidity, zero debt, zero balance" +)] +#[test_case( + Market { + denom: MOCK_DENOM.into(), + collateral_total_scaled: Uint128::new(6023580722925709342), + liquidity_index: Decimal::from_str("1.010435027113017045").unwrap(), + indexes_last_updated: 1690573862, + ..Default::default() + }, + UserDebtResponse { + denom: MOCK_DENOM.into(), + amount_scaled: Uint128::new(442125932248737808), + amount: Uint128::new(459180188271), + uncollateralized: true, + }, + Uint128::new(1751191642); + "real data queried from mainnet" +)] +fn querying_total_deposit(rb_market: Market, rb_debt: UserDebtResponse, cm_balance: Uint128) { + let mut deps = mock_dependencies(&[]); + let env = mock_env_at_block_time(TIMESTAMP); + + // setup + deps.querier.set_redbank_market(rb_market.clone()); + deps.querier.set_red_bank_user_debt(CREDIT_MANAGER, rb_debt.clone()); + deps.querier.update_balances(CREDIT_MANAGER, coins(cm_balance.u128(), MOCK_DENOM)); + ADDRESS_PROVIDER.save(deps.as_mut().storage, &Addr::unchecked("address_provider")).unwrap(); + + // compute the correct, expected total deposit + let rb_deposit = + get_underlying_liquidity_amount(rb_market.collateral_total_scaled, &rb_market, TIMESTAMP) + .unwrap(); + let exp_total_deposit = rb_deposit + cm_balance - rb_debt.amount; + + // query total deposit + let total_deposit = query_total_deposit(deps.as_ref(), &env, MOCK_DENOM.into()).unwrap(); + assert_eq!(total_deposit.amount, exp_total_deposit); +} diff --git a/contracts/red-bank/Cargo.toml b/contracts/red-bank/Cargo.toml index d9c406421..4352b9f90 100644 --- a/contracts/red-bank/Cargo.toml +++ b/contracts/red-bank/Cargo.toml @@ -27,6 +27,7 @@ cw2 = { workspace = true } cw-storage-plus = { workspace = true } cw-utils = { workspace = true } mars-health = { workspace = true } +mars-interest-rate = { workspace = true } mars-liquidation = { workspace = true } mars-owner = { workspace = true } mars-params = { workspace = true } @@ -39,3 +40,4 @@ anyhow = { workspace = true } cosmwasm-schema = { workspace = true } cw-multi-test = { workspace = true } mars-testing = { workspace = true } +test-case = { workspace = true } diff --git a/contracts/red-bank/src/borrow.rs b/contracts/red-bank/src/borrow.rs index 04c89cf29..527f2002f 100644 --- a/contracts/red-bank/src/borrow.rs +++ b/contracts/red-bank/src/borrow.rs @@ -1,4 +1,7 @@ use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Uint128}; +use mars_interest_rate::{ + get_scaled_debt_amount, get_underlying_debt_amount, get_underlying_liquidity_amount, +}; use mars_red_bank_types::{address_provider, address_provider::MarsAddressType}; use mars_utils::helpers::build_send_asset_msg; @@ -6,10 +9,7 @@ use crate::{ error::ContractError, health::assert_below_max_ltv_after_borrow, helpers::query_asset_params, - interest_rates::{ - apply_accumulated_interests, get_scaled_debt_amount, get_underlying_debt_amount, - get_underlying_liquidity_amount, update_interest_rates, - }, + interest_rates::{apply_accumulated_interests, update_interest_rates}, state::{CONFIG, MARKETS}, user::User, }; diff --git a/contracts/red-bank/src/deposit.rs b/contracts/red-bank/src/deposit.rs index 96d0cbb21..711f4e0ef 100644 --- a/contracts/red-bank/src/deposit.rs +++ b/contracts/red-bank/src/deposit.rs @@ -1,13 +1,11 @@ use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Uint128}; +use mars_interest_rate::get_scaled_liquidity_amount; use mars_red_bank_types::address_provider::{self, MarsAddressType}; use crate::{ error::ContractError, - helpers::query_asset_params, - interest_rates::{ - apply_accumulated_interests, get_scaled_liquidity_amount, get_underlying_liquidity_amount, - update_interest_rates, - }, + helpers::{query_asset_params, query_total_deposit}, + interest_rates::{apply_accumulated_interests, update_interest_rates}, state::{CONFIG, MARKETS}, user::User, }; @@ -45,10 +43,8 @@ pub fn deposit( }); } - let total_scaled_deposits = market.collateral_total_scaled; - let total_deposits = - get_underlying_liquidity_amount(total_scaled_deposits, &market, env.block.time.seconds())?; - if total_deposits.checked_add(deposit_amount)? > asset_params.red_bank.deposit_cap { + let total_deposits = query_total_deposit(&deps.querier, params_addr, &denom)?; + if total_deposits.amount.checked_add(deposit_amount)? > asset_params.deposit_cap { return Err(ContractError::DepositCapExceeded { denom, }); diff --git a/contracts/red-bank/src/health.rs b/contracts/red-bank/src/health.rs index cf9bc41df..ae8b3b775 100644 --- a/contracts/red-bank/src/health.rs +++ b/contracts/red-bank/src/health.rs @@ -2,12 +2,12 @@ use std::collections::{HashMap, HashSet}; use cosmwasm_std::{Addr, Deps, Env, Order, StdError, StdResult, Uint128}; use mars_health::health::{Health, Position as HealthPosition}; +use mars_interest_rate::{get_underlying_debt_amount, get_underlying_liquidity_amount}; use mars_red_bank_types::{oracle, red_bank::Position}; use crate::{ error::ContractError, helpers::query_asset_params, - interest_rates::{get_underlying_debt_amount, get_underlying_liquidity_amount}, state::{COLLATERALS, DEBTS, MARKETS}, }; diff --git a/contracts/red-bank/src/helpers.rs b/contracts/red-bank/src/helpers.rs index b6de4095e..79fa686ff 100644 --- a/contracts/red-bank/src/helpers.rs +++ b/contracts/red-bank/src/helpers.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Decimal, QuerierWrapper, StdResult}; +use cosmwasm_std::{Coin, Decimal, QuerierWrapper, StdResult}; use mars_params::{msg::QueryMsg, types::asset::AssetParams}; pub fn query_asset_params( @@ -20,3 +20,16 @@ pub fn query_target_health_factor( ) -> StdResult { querier.query_wasm_smart(params.into(), &QueryMsg::TargetHealthFactor {}) } + +pub fn query_total_deposit( + querier: &QuerierWrapper, + params: impl Into, + denom: impl Into, +) -> StdResult { + querier.query_wasm_smart( + params.into(), + &QueryMsg::TotalDeposit { + denom: denom.into(), + }, + ) +} diff --git a/contracts/red-bank/src/interest_rates.rs b/contracts/red-bank/src/interest_rates.rs index 43359df6f..d25f83236 100644 --- a/contracts/red-bank/src/interest_rates.rs +++ b/contracts/red-bank/src/interest_rates.rs @@ -1,16 +1,14 @@ use std::str; -use cosmwasm_std::{Addr, Decimal, Env, Event, Response, StdError, StdResult, Storage, Uint128}; +use cosmwasm_std::{Addr, Decimal, Env, Event, Response, StdResult, Storage, Uint128}; +use mars_interest_rate::{ + calculate_applied_linear_interest_rate, compute_scaled_amount, compute_underlying_amount, + get_underlying_debt_amount, get_underlying_liquidity_amount, ScalingOperation, +}; use mars_red_bank_types::red_bank::Market; -use mars_utils::math; use crate::{error::ContractError, user::User}; -/// Scaling factor used to keep more precision during division / multiplication by index. -pub const SCALING_FACTOR: Uint128 = Uint128::new(1_000_000); - -const SECONDS_PER_YEAR: u64 = 31536000u64; - /// Calculates accumulated interest for the time between last time market index was updated /// and current block. /// Applies desired side effects: @@ -96,181 +94,6 @@ pub fn apply_accumulated_interests( Ok(response) } -pub fn calculate_applied_linear_interest_rate( - index: Decimal, - rate: Decimal, - time_elapsed: u64, -) -> StdResult { - let rate_factor = rate.checked_mul(Decimal::from_ratio( - Uint128::from(time_elapsed), - Uint128::from(SECONDS_PER_YEAR), - ))?; - index.checked_mul(Decimal::one() + rate_factor).map_err(StdError::from) -} - -/// Get scaled liquidity amount from an underlying amount, a Market and timestamp in seconds -/// Liquidity amounts are always truncated to make sure rounding errors accumulate in favor of -/// the protocol -/// NOTE: Calling this function when interests for the market are up to date with the current block -/// and index is not, will use the wrong interest rate to update the index. -/// NOTE: This function should not be used when calculating how much scaled amount is getting -/// burned from given underlying withdraw amount. In that case, all math should be done in underlying -/// amounts then get scaled back again -pub fn get_scaled_liquidity_amount( - amount: Uint128, - market: &Market, - timestamp: u64, -) -> StdResult { - compute_scaled_amount( - amount, - get_updated_liquidity_index(market, timestamp)?, - ScalingOperation::Truncate, - ) -} - -/// Get underlying liquidity amount from a scaled amount, a Market and timestamp in seconds -/// Liquidity amounts are always truncated to make sure rounding errors accumulate in favor of -/// the protocol -/// NOTE: Calling this function when interests for the market are up to date with the current block -/// and index is not, will use the wrong interest rate to update the index. -pub fn get_underlying_liquidity_amount( - amount_scaled: Uint128, - market: &Market, - timestamp: u64, -) -> StdResult { - compute_underlying_amount( - amount_scaled, - get_updated_liquidity_index(market, timestamp)?, - ScalingOperation::Truncate, - ) -} - -/// Get scaled borrow amount from an underlying amount, a Market and timestamp in seconds -/// Debt amounts are always ceiled to make sure rounding errors accumulate in favor of -/// the protocol -/// NOTE: Calling this function when interests for the market are up to date with the current block -/// and index is not, will use the wrong interest rate to update the index. -/// NOTE: This function should not be used when calculating how much scaled amount is getting -/// repaid from a sent underlying amount. In that case, all math should be done in underlying -/// amounts then get scaled back again -pub fn get_scaled_debt_amount( - amount: Uint128, - market: &Market, - timestamp: u64, -) -> StdResult { - compute_scaled_amount( - amount, - get_updated_borrow_index(market, timestamp)?, - ScalingOperation::Ceil, - ) -} - -/// Get underlying borrow amount from a scaled amount, a Market and timestamp in seconds -/// Debt amounts are always ceiled so as for rounding errors to accumulate in favor of -/// the protocol -/// NOTE: Calling this function when interests for the market are up to date with the current block -/// and index is not, will use the wrong interest rate to update the index. -pub fn get_underlying_debt_amount( - amount_scaled: Uint128, - market: &Market, - timestamp: u64, -) -> StdResult { - compute_underlying_amount( - amount_scaled, - get_updated_borrow_index(market, timestamp)?, - ScalingOperation::Ceil, - ) -} - -pub enum ScalingOperation { - Truncate, - Ceil, -} - -/// Scales the amount dividing by an index in order to compute interest rates. Before dividing, -/// the value is multiplied by SCALING_FACTOR for greater precision. -/// Example: -/// Current index is 10. We deposit 6.123456 OSMO (6123456 uosmo). Scaled amount will be -/// 6123456 / 10 = 612345 so we loose some precision. In order to avoid this situation -/// we scale the amount by SCALING_FACTOR. -pub fn compute_scaled_amount( - amount: Uint128, - index: Decimal, - scaling_operation: ScalingOperation, -) -> StdResult { - // Scale by SCALING_FACTOR to have better precision - let scaled_amount = amount.checked_mul(SCALING_FACTOR)?; - match scaling_operation { - ScalingOperation::Truncate => math::divide_uint128_by_decimal(scaled_amount, index), - ScalingOperation::Ceil => math::divide_uint128_by_decimal_and_ceil(scaled_amount, index), - } -} - -/// Descales the amount introduced by `get_scaled_amount`, returning the underlying amount. -/// As interest rate is accumulated the index used to descale the amount should be bigger than the one used to scale it. -pub fn compute_underlying_amount( - scaled_amount: Uint128, - index: Decimal, - scaling_operation: ScalingOperation, -) -> StdResult { - // Multiply scaled amount by decimal (index) - let before_scaling_factor = scaled_amount * index; - - // Descale by SCALING_FACTOR which is introduced when scaling the amount - match scaling_operation { - ScalingOperation::Truncate => Ok(before_scaling_factor.checked_div(SCALING_FACTOR)?), - ScalingOperation::Ceil => { - math::uint128_checked_div_with_ceil(before_scaling_factor, SCALING_FACTOR) - } - } -} - -/// Return applied interest rate for borrow index according to passed blocks -/// NOTE: Calling this function when interests for the market are up to date with the current block -/// and index is not, will use the wrong interest rate to update the index. -pub fn get_updated_borrow_index(market: &Market, timestamp: u64) -> StdResult { - if market.indexes_last_updated < timestamp { - let time_elapsed = timestamp - market.indexes_last_updated; - - if !market.borrow_rate.is_zero() { - let updated_index = calculate_applied_linear_interest_rate( - market.borrow_index, - market.borrow_rate, - time_elapsed, - ); - return updated_index; - } - } - - Ok(market.borrow_index) -} - -/// Return applied interest rate for liquidity index according to passed blocks -/// NOTE: Calling this function when interests for the market are up to date with the current block -/// and index is not, will use the wrong interest rate to update the index. -pub fn get_updated_liquidity_index(market: &Market, timestamp: u64) -> StdResult { - if market.indexes_last_updated > timestamp { - return Err(StdError::generic_err( - format!("Cannot compute updated liquidity index for a timestamp: {} smaller than last updated timestamp for market: {}", timestamp, market.indexes_last_updated) - )); - } - - if market.indexes_last_updated < timestamp { - let time_elapsed = timestamp - market.indexes_last_updated; - - if !market.liquidity_rate.is_zero() { - let updated_index = calculate_applied_linear_interest_rate( - market.liquidity_index, - market.liquidity_rate, - time_elapsed, - ); - return updated_index; - } - } - - Ok(market.liquidity_index) -} - /// Update interest rates for current liquidity and debt levels /// Note it does not save the market to the store (that is left to the caller) /// Returns response with appended interest rates updated event @@ -307,48 +130,3 @@ pub fn build_interests_updated_event(denom: &str, market: &Market) -> Event { .add_attribute("borrow_rate", market.borrow_rate.to_string()) .add_attribute("liquidity_rate", market.liquidity_rate.to_string()) } - -#[cfg(test)] -mod tests { - use cosmwasm_std::{Decimal, Uint128}; - use mars_red_bank_types::red_bank::Market; - - use crate::interest_rates::{ - calculate_applied_linear_interest_rate, get_scaled_debt_amount, - get_scaled_liquidity_amount, get_underlying_debt_amount, get_underlying_liquidity_amount, - }; - - #[test] - fn accumulated_index_calculation() { - let index = Decimal::from_ratio(1u128, 10u128); - let rate = Decimal::from_ratio(2u128, 10u128); - let time_elapsed = 15768000; // half a year - let accumulated = - calculate_applied_linear_interest_rate(index, rate, time_elapsed).unwrap(); - - assert_eq!(accumulated, Decimal::from_ratio(11u128, 100u128)); - } - - #[test] - fn liquidity_and_debt_rounding() { - let start = Uint128::from(100_000_000_000_u128); - let market = Market { - liquidity_index: Decimal::from_ratio(3_u128, 1_u128), - borrow_index: Decimal::from_ratio(3_u128, 1_u128), - indexes_last_updated: 1, - ..Default::default() - }; - - let scaled_amount_liquidity = get_scaled_liquidity_amount(start, &market, 1).unwrap(); - let scaled_amount_debt = get_scaled_debt_amount(start, &market, 1).unwrap(); - assert_eq!(Uint128::from(33_333_333_333_333_333_u128), scaled_amount_liquidity); - assert_eq!(Uint128::from(33_333_333_333_333_334_u128), scaled_amount_debt); - - let back_to_underlying_liquidity = - get_underlying_liquidity_amount(scaled_amount_liquidity, &market, 1).unwrap(); - let back_to_underlying_debt = - get_underlying_debt_amount(scaled_amount_debt, &market, 1).unwrap(); - assert_eq!(Uint128::from(99_999_999_999_u128), back_to_underlying_liquidity); - assert_eq!(Uint128::from(100_000_000_001_u128), back_to_underlying_debt); - } -} diff --git a/contracts/red-bank/src/liquidate.rs b/contracts/red-bank/src/liquidate.rs index 93ef31b24..5d4a8a523 100644 --- a/contracts/red-bank/src/liquidate.rs +++ b/contracts/red-bank/src/liquidate.rs @@ -1,4 +1,8 @@ use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response, Uint128}; +use mars_interest_rate::{ + get_scaled_debt_amount, get_scaled_liquidity_amount, get_underlying_debt_amount, + get_underlying_liquidity_amount, +}; use mars_liquidation::liquidation::calculate_liquidation_amounts; use mars_red_bank_types::address_provider::{self, MarsAddressType}; use mars_utils::helpers::{build_send_asset_msg, option_string_to_addr}; @@ -7,10 +11,7 @@ use crate::{ error::ContractError, health::get_health_and_positions, helpers::{query_asset_params, query_target_health_factor}, - interest_rates::{ - apply_accumulated_interests, get_scaled_debt_amount, get_scaled_liquidity_amount, - get_underlying_debt_amount, get_underlying_liquidity_amount, update_interest_rates, - }, + interest_rates::{apply_accumulated_interests, update_interest_rates}, state::{COLLATERALS, CONFIG, DEBTS, MARKETS}, user::User, }; diff --git a/contracts/red-bank/src/query.rs b/contracts/red-bank/src/query.rs index 118e00688..e9a74ab16 100644 --- a/contracts/red-bank/src/query.rs +++ b/contracts/red-bank/src/query.rs @@ -1,5 +1,9 @@ -use cosmwasm_std::{Addr, BlockInfo, Deps, Env, Order, StdError, StdResult, Uint128}; +use cosmwasm_std::{Addr, BlockInfo, Deps, Env, Order, StdResult, Uint128}; use cw_storage_plus::Bound; +use mars_interest_rate::{ + get_scaled_debt_amount, get_scaled_liquidity_amount, get_underlying_debt_amount, + get_underlying_liquidity_amount, +}; use mars_red_bank_types::{ address_provider::{self, MarsAddressType}, red_bank::{ @@ -11,10 +15,6 @@ use mars_red_bank_types::{ use crate::{ error::ContractError, health, - interest_rates::{ - get_scaled_debt_amount, get_scaled_liquidity_amount, get_underlying_debt_amount, - get_underlying_liquidity_amount, - }, state::{COLLATERALS, CONFIG, DEBTS, MARKETS, OWNER, UNCOLLATERALIZED_LOAN_LIMITS}, }; @@ -31,10 +31,8 @@ pub fn query_config(deps: Deps) -> StdResult { }) } -pub fn query_market(deps: Deps, denom: String) -> StdResult { - MARKETS - .load(deps.storage, &denom) - .map_err(|_| StdError::generic_err(format!("failed to load market for: {denom}"))) +pub fn query_market(deps: Deps, denom: String) -> StdResult> { + MARKETS.may_load(deps.storage, &denom) } pub fn query_markets( diff --git a/contracts/red-bank/src/repay.rs b/contracts/red-bank/src/repay.rs index 6e28874a6..b9c0fcc8c 100644 --- a/contracts/red-bank/src/repay.rs +++ b/contracts/red-bank/src/repay.rs @@ -1,13 +1,11 @@ use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response, Uint128}; +use mars_interest_rate::{get_scaled_debt_amount, get_underlying_debt_amount}; use mars_red_bank_types::{address_provider, address_provider::MarsAddressType}; use mars_utils::helpers::build_send_asset_msg; use crate::{ error::ContractError, - interest_rates::{ - apply_accumulated_interests, get_scaled_debt_amount, get_underlying_debt_amount, - update_interest_rates, - }, + interest_rates::{apply_accumulated_interests, update_interest_rates}, state::{CONFIG, DEBTS, MARKETS}, user::User, }; diff --git a/contracts/red-bank/src/withdraw.rs b/contracts/red-bank/src/withdraw.rs index 66e32774f..a9da5ce72 100644 --- a/contracts/red-bank/src/withdraw.rs +++ b/contracts/red-bank/src/withdraw.rs @@ -1,14 +1,12 @@ use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Uint128}; +use mars_interest_rate::{get_scaled_liquidity_amount, get_underlying_liquidity_amount}; use mars_red_bank_types::{address_provider, address_provider::MarsAddressType}; use mars_utils::helpers::build_send_asset_msg; use crate::{ error::ContractError, health::assert_below_liq_threshold_after_withdraw, - interest_rates::{ - apply_accumulated_interests, get_scaled_liquidity_amount, get_underlying_liquidity_amount, - update_interest_rates, - }, + interest_rates::{apply_accumulated_interests, update_interest_rates}, state::{CONFIG, MARKETS}, user::User, }; diff --git a/contracts/red-bank/tests/helpers.rs b/contracts/red-bank/tests/helpers.rs index 8f112540b..944b10908 100644 --- a/contracts/red-bank/tests/helpers.rs +++ b/contracts/red-bank/tests/helpers.rs @@ -10,14 +10,14 @@ use cosmwasm_std::{ Addr, Coin, Decimal, Deps, DepsMut, Event, OwnedDeps, Uint128, }; use cw_multi_test::AppResponse; +use mars_interest_rate::{ + calculate_applied_linear_interest_rate, compute_scaled_amount, compute_underlying_amount, + ScalingOperation, +}; use mars_params::types::asset::{AssetParams, CmSettings, LiquidationBonus, RedBankSettings}; use mars_red_bank::{ contract::{instantiate, query}, error::ContractError, - interest_rates::{ - calculate_applied_linear_interest_rate, compute_scaled_amount, compute_underlying_amount, - ScalingOperation, - }, state::{COLLATERALS, DEBTS, MARKETS}, }; use mars_red_bank_types::red_bank::{ @@ -122,7 +122,6 @@ pub fn th_default_asset_params() -> AssetParams { red_bank: RedBankSettings { deposit_enabled: true, borrow_enabled: true, - deposit_cap: Uint128::MAX, }, max_loan_to_value: Decimal::zero(), liquidation_threshold: Decimal::one(), @@ -133,6 +132,7 @@ pub fn th_default_asset_params() -> AssetParams { max_lb: Decimal::percent(5u64), }, protocol_liquidation_fee: Decimal::percent(2u64), + deposit_cap: Uint128::MAX, } } diff --git a/contracts/red-bank/tests/test_admin.rs b/contracts/red-bank/tests/test_admin.rs index 03e3f6a7f..ae6a9b162 100644 --- a/contracts/red-bank/tests/test_admin.rs +++ b/contracts/red-bank/tests/test_admin.rs @@ -1,9 +1,9 @@ use cosmwasm_std::{attr, coin, from_binary, testing::mock_info, Addr, Decimal, Event, Uint128}; +use mars_interest_rate::{compute_scaled_amount, compute_underlying_amount, ScalingOperation}; use mars_owner::OwnerError::NotOwner; use mars_red_bank::{ contract::{execute, instantiate, query}, error::ContractError, - interest_rates::{compute_scaled_amount, compute_underlying_amount, ScalingOperation}, state::{COLLATERALS, MARKETS}, }; use mars_red_bank_types::{ diff --git a/contracts/red-bank/tests/test_borrow.rs b/contracts/red-bank/tests/test_borrow.rs index 20d71a1e2..98314002e 100644 --- a/contracts/red-bank/tests/test_borrow.rs +++ b/contracts/red-bank/tests/test_borrow.rs @@ -6,14 +6,14 @@ use helpers::{ has_collateral_position, has_debt_position, set_collateral, th_build_interests_updated_event, th_get_expected_indices_and_rates, th_init_market, th_setup, TestUtilizationDeltaInfo, }; +use mars_interest_rate::{ + calculate_applied_linear_interest_rate, compute_scaled_amount, compute_underlying_amount, + ScalingOperation, SCALING_FACTOR, +}; use mars_params::types::asset::{AssetParams, CmSettings, RedBankSettings}; use mars_red_bank::{ contract::execute, error::ContractError, - interest_rates::{ - calculate_applied_linear_interest_rate, compute_scaled_amount, compute_underlying_amount, - ScalingOperation, SCALING_FACTOR, - }, state::{DEBTS, MARKETS, UNCOLLATERALIZED_LOAN_LIMITS}, }; use mars_red_bank_types::red_bank::{ExecuteMsg, Market}; @@ -1008,7 +1008,6 @@ fn cannot_borrow_if_market_not_enabled() { red_bank: RedBankSettings { deposit_enabled: false, borrow_enabled: false, - deposit_cap: Default::default(), }, ..th_default_asset_params() }, diff --git a/contracts/red-bank/tests/test_deposit.rs b/contracts/red-bank/tests/test_deposit.rs index d5e83d4e9..a10f508af 100644 --- a/contracts/red-bank/tests/test_deposit.rs +++ b/contracts/red-bank/tests/test_deposit.rs @@ -3,17 +3,19 @@ use std::any::type_name; use cosmwasm_std::{ attr, coin, coins, testing::{mock_env, mock_info, MockApi, MockStorage}, - to_binary, Addr, Decimal, OwnedDeps, StdError, StdResult, SubMsg, Uint128, WasmMsg, + to_binary, Addr, Decimal, OwnedDeps, StdError, SubMsg, Uint128, WasmMsg, }; use cw_utils::PaymentError; use helpers::{ set_collateral, th_build_interests_updated_event, th_get_expected_indices_and_rates, th_setup, }; +use mars_interest_rate::{ + compute_scaled_amount, get_underlying_liquidity_amount, ScalingOperation, SCALING_FACTOR, +}; use mars_params::types::asset::{AssetParams, CmSettings, LiquidationBonus, RedBankSettings}; use mars_red_bank::{ contract::execute, error::ContractError, - interest_rates::{compute_scaled_amount, ScalingOperation, SCALING_FACTOR}, state::{COLLATERALS, MARKETS}, }; use mars_red_bank_types::{ @@ -22,6 +24,7 @@ use mars_red_bank_types::{ red_bank::{Collateral, ExecuteMsg, Market}, }; use mars_testing::{mock_env_at_block_time, MarsMockQuerier}; +use test_case::test_case; use crate::helpers::th_default_asset_params; @@ -74,12 +77,22 @@ fn setup_test() -> TestSuite { red_bank: RedBankSettings { deposit_enabled: true, borrow_enabled: true, - deposit_cap: Uint128::new(12_000_000), }, protocol_liquidation_fee: Decimal::percent(2u64), + deposit_cap: Uint128::new(12_000_000), }, ); + deps.querier.set_total_deposit( + denom, + get_underlying_liquidity_amount( + market.collateral_total_scaled, + &market, + market.indexes_last_updated, + ) + .unwrap(), + ); + TestSuite { deps, denom, @@ -173,7 +186,6 @@ fn depositing_to_disabled_market() { red_bank: RedBankSettings { deposit_enabled: false, borrow_enabled: true, - deposit_cap: Default::default(), }, ..th_default_asset_params() }, @@ -196,23 +208,29 @@ fn depositing_to_disabled_market() { ); } -#[test] -fn depositing_above_cap() { +// note: the initial deposit amount set in the TestSuite is 11_000_000 uosmo +#[test_case( + 1_000_001, + 12_000_000, + false; + "deposit cap exceeded, should fail" +)] +#[test_case( + 999_999, + 12_000_000, + true; + "deposit cap not exceeded, should work" +)] +fn depositing_above_cap(amount_to_deposit: u128, deposit_cap: u128, exp_ok: bool) { let TestSuite { mut deps, denom, depositor_addr, + initial_market, .. } = setup_test(); - // set a deposit cap - MARKETS - .update(deps.as_mut().storage, denom, |opt| -> StdResult<_> { - let mut market = opt.unwrap(); - market.collateral_total_scaled = Uint128::new(9_000_000) * SCALING_FACTOR; - Ok(market) - }) - .unwrap(); + // set deposit cap deps.querier.set_redbank_params( denom, AssetParams { @@ -223,39 +241,32 @@ fn depositing_above_cap() { red_bank: RedBankSettings { deposit_enabled: true, borrow_enabled: true, - deposit_cap: Uint128::new(10_000_000), }, + deposit_cap: Uint128::new(deposit_cap), ..th_default_asset_params() }, ); - // try deposit with a big amount, should fail - let err = execute( + // try deposit with the given amount + let res = execute( deps.as_mut(), - mock_env_at_block_time(10000100), - mock_info(depositor_addr.as_str(), &coins(1_000_001, denom)), + mock_env_at_block_time(initial_market.indexes_last_updated), + mock_info(depositor_addr.as_str(), &coins(amount_to_deposit, denom)), ExecuteMsg::Deposit { account_id: None, }, - ) - .unwrap_err(); - assert_eq!( - err, - ContractError::DepositCapExceeded { - denom: denom.to_string() - } ); - // deposit a smaller amount, should work - let result = execute( - deps.as_mut(), - mock_env_at_block_time(10000100), - mock_info(depositor_addr.as_str(), &coins(123, denom)), - ExecuteMsg::Deposit { - account_id: None, - }, - ); - assert!(result.is_ok()); + if exp_ok { + assert!(res.is_ok()); + } else { + assert_eq!( + res, + Err(ContractError::DepositCapExceeded { + denom: denom.to_string(), + }), + ); + } } #[test] diff --git a/contracts/red-bank/tests/test_liquidate.rs b/contracts/red-bank/tests/test_liquidate.rs index ed9efcdf6..a8c052e8f 100644 --- a/contracts/red-bank/tests/test_liquidate.rs +++ b/contracts/red-bank/tests/test_liquidate.rs @@ -796,6 +796,12 @@ fn response_verification() { deps.querier.set_oracle_price("uusdc", Decimal::from_ratio(68u128, 10u128)); deps.querier.set_oracle_price("untrn", Decimal::from_ratio(55u128, 10u128)); + // no deposit yet, initialize total deposit to zero + deps.querier.set_total_deposit("uosmo", Uint128::zero()); + deps.querier.set_total_deposit("uatom", Uint128::zero()); + deps.querier.set_total_deposit("uusdc", Uint128::zero()); + deps.querier.set_total_deposit("untrn", Uint128::zero()); + // provider deposits collaterals execute( deps.as_mut(), @@ -1155,7 +1161,6 @@ fn default_asset_params_with( red_bank: RedBankSettings { deposit_enabled: true, borrow_enabled: true, - deposit_cap: Uint128::MAX, }, max_loan_to_value, liquidation_threshold, @@ -1166,6 +1171,7 @@ fn default_asset_params_with( max_lb: Decimal::percent(10), }, protocol_liquidation_fee: Decimal::percent(2), + deposit_cap: Uint128::MAX, }; (market_params, asset_params) } diff --git a/contracts/red-bank/tests/test_misc.rs b/contracts/red-bank/tests/test_misc.rs index c15b24aa7..d710b694c 100644 --- a/contracts/red-bank/tests/test_misc.rs +++ b/contracts/red-bank/tests/test_misc.rs @@ -6,16 +6,16 @@ use helpers::{ th_build_interests_updated_event, th_get_expected_indices_and_rates, th_init_market, th_setup, TestUtilizationDeltaInfo, }; +use mars_interest_rate::{ + compute_scaled_amount, compute_underlying_amount, get_scaled_debt_amount, + get_updated_liquidity_index, ScalingOperation, SCALING_FACTOR, +}; use mars_owner::OwnerError::NotOwner; use mars_params::types::asset::AssetParams; use mars_red_bank::{ contract::execute, error::ContractError, health, - interest_rates::{ - compute_scaled_amount, compute_underlying_amount, get_scaled_debt_amount, - get_updated_liquidity_index, ScalingOperation, SCALING_FACTOR, - }, state::{DEBTS, MARKETS, UNCOLLATERALIZED_LOAN_LIMITS}, }; use mars_red_bank_types::red_bank::{Debt, ExecuteMsg, Market}; diff --git a/contracts/red-bank/tests/test_query.rs b/contracts/red-bank/tests/test_query.rs index 80bd3b58d..6547db7c2 100644 --- a/contracts/red-bank/tests/test_query.rs +++ b/contracts/red-bank/tests/test_query.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{testing::mock_env, Addr, Decimal, Uint128}; use helpers::{set_collateral, th_init_market, th_setup}; +use mars_interest_rate::{get_scaled_debt_amount, get_underlying_debt_amount, SCALING_FACTOR}; use mars_red_bank::{ - interest_rates::{get_scaled_debt_amount, get_underlying_debt_amount, SCALING_FACTOR}, query::{query_user_collaterals, query_user_debt, query_user_debts}, state::DEBTS, }; diff --git a/contracts/red-bank/tests/test_withdraw.rs b/contracts/red-bank/tests/test_withdraw.rs index 7f88ba5cf..235fea0ea 100644 --- a/contracts/red-bank/tests/test_withdraw.rs +++ b/contracts/red-bank/tests/test_withdraw.rs @@ -7,14 +7,14 @@ use helpers::{ has_collateral_position, set_collateral, th_build_interests_updated_event, th_get_expected_indices_and_rates, th_setup, TestUtilizationDeltaInfo, }; +use mars_interest_rate::{ + compute_scaled_amount, compute_underlying_amount, get_scaled_liquidity_amount, + get_updated_borrow_index, get_updated_liquidity_index, ScalingOperation, SCALING_FACTOR, +}; use mars_params::types::asset::AssetParams; use mars_red_bank::{ contract::execute, error::ContractError, - interest_rates::{ - compute_scaled_amount, compute_underlying_amount, get_scaled_liquidity_amount, - get_updated_borrow_index, get_updated_liquidity_index, ScalingOperation, SCALING_FACTOR, - }, state::{COLLATERALS, DEBTS, MARKETS}, }; use mars_red_bank_types::{ diff --git a/integration-tests/tests/helpers.rs b/integration-tests/tests/helpers.rs index 60b54f10b..a8a3c51bb 100644 --- a/integration-tests/tests/helpers.rs +++ b/integration-tests/tests/helpers.rs @@ -32,7 +32,6 @@ pub fn default_asset_params(denom: &str) -> (InitOrUpdateAssetParams, AssetParam red_bank: RedBankSettings { deposit_enabled: true, borrow_enabled: true, - deposit_cap: Uint128::MAX, }, max_loan_to_value: Decimal::percent(60), liquidation_threshold: Decimal::percent(80), @@ -43,6 +42,7 @@ pub fn default_asset_params(denom: &str) -> (InitOrUpdateAssetParams, AssetParam max_lb: Decimal::percent(5u64), }, protocol_liquidation_fee: Decimal::percent(2u64), + deposit_cap: Uint128::MAX, }; (market_params, asset_params) } @@ -71,12 +71,12 @@ pub fn default_asset_params_with( red_bank: RedBankSettings { deposit_enabled: true, borrow_enabled: true, - deposit_cap: Uint128::MAX, }, max_loan_to_value, liquidation_threshold, liquidation_bonus, protocol_liquidation_fee: Decimal::percent(2u64), + deposit_cap: Uint128::MAX, }; (market_params, asset_params) } diff --git a/integration-tests/tests/test_oracles.rs b/integration-tests/tests/test_oracles.rs index 5b334e9bc..2fc5b0dcf 100644 --- a/integration-tests/tests/test_oracles.rs +++ b/integration-tests/tests/test_oracles.rs @@ -1104,6 +1104,7 @@ fn setup_redbank(wasm: &Wasm, signer: &SigningAccount) -> (Strin OSMOSIS_PARAMS_CONTRACT_NAME, &mars_params::msg::InstantiateMsg { owner: (signer.address()), + address_provider: addr_provider_addr.clone(), target_health_factor: Decimal::from_str("1.05").unwrap(), }, ); @@ -1163,6 +1164,18 @@ fn setup_redbank(wasm: &Wasm, signer: &SigningAccount) -> (Strin ) .unwrap(); + // We can simulate credit manager contract balance with own params address (used by params contract for deposit caps logic) + wasm.execute( + &addr_provider_addr, + &SetAddress { + address_type: MarsAddressType::CreditManager, + address: params_addr.clone(), + }, + &[], + signer, + ) + .unwrap(); + let (market_params, asset_params) = default_asset_params("uosmo"); wasm.execute( diff --git a/packages/health/tests/test_from_coins_to_positions.rs b/packages/health/tests/test_from_coins_to_positions.rs index 48508bac2..9aa85fe0a 100644 --- a/packages/health/tests/test_from_coins_to_positions.rs +++ b/packages/health/tests/test_from_coins_to_positions.rs @@ -136,7 +136,6 @@ fn mock_setup() -> MarsMockQuerier { red_bank: RedBankSettings { deposit_enabled: false, borrow_enabled: false, - deposit_cap: Default::default(), }, max_loan_to_value: Decimal::from_atomics(50u128, 2).unwrap(), liquidation_threshold: Decimal::from_atomics(55u128, 2).unwrap(), @@ -147,6 +146,7 @@ fn mock_setup() -> MarsMockQuerier { max_lb: Decimal::percent(5u64), }, protocol_liquidation_fee: Decimal::zero(), + deposit_cap: Default::default(), }, ); let atom_market = Market { @@ -165,7 +165,6 @@ fn mock_setup() -> MarsMockQuerier { red_bank: RedBankSettings { deposit_enabled: false, borrow_enabled: false, - deposit_cap: Default::default(), }, max_loan_to_value: Decimal::from_atomics(70u128, 2).unwrap(), liquidation_threshold: Decimal::from_atomics(75u128, 2).unwrap(), @@ -176,6 +175,7 @@ fn mock_setup() -> MarsMockQuerier { max_lb: Decimal::percent(5u64), }, protocol_liquidation_fee: Decimal::zero(), + deposit_cap: Default::default(), }, ); diff --git a/packages/health/tests/test_health_from_coins.rs b/packages/health/tests/test_health_from_coins.rs index 20da4adb5..234cedcea 100644 --- a/packages/health/tests/test_health_from_coins.rs +++ b/packages/health/tests/test_health_from_coins.rs @@ -30,7 +30,6 @@ fn health_success_from_coins() { red_bank: RedBankSettings { deposit_enabled: true, borrow_enabled: true, - deposit_cap: Uint128::MAX, }, max_loan_to_value: Decimal::from_atomics(50u128, 2).unwrap(), liquidation_threshold: Decimal::from_atomics(55u128, 2).unwrap(), @@ -41,6 +40,7 @@ fn health_success_from_coins() { max_lb: Decimal::percent(5u64), }, protocol_liquidation_fee: Decimal::zero(), + deposit_cap: Uint128::MAX, }, ); let atom_market = Market { @@ -59,7 +59,6 @@ fn health_success_from_coins() { red_bank: RedBankSettings { deposit_enabled: true, borrow_enabled: true, - deposit_cap: Uint128::MAX, }, max_loan_to_value: Decimal::from_atomics(70u128, 2).unwrap(), liquidation_threshold: Decimal::from_atomics(75u128, 2).unwrap(), @@ -70,6 +69,7 @@ fn health_success_from_coins() { max_lb: Decimal::percent(5u64), }, protocol_liquidation_fee: Decimal::zero(), + deposit_cap: Uint128::MAX, }, ); @@ -127,7 +127,6 @@ fn health_error_from_coins() { red_bank: RedBankSettings { deposit_enabled: false, borrow_enabled: false, - deposit_cap: Default::default(), }, max_loan_to_value: Decimal::from_atomics(50u128, 2).unwrap(), liquidation_threshold: Decimal::from_atomics(55u128, 2).unwrap(), @@ -138,6 +137,7 @@ fn health_error_from_coins() { max_lb: Decimal::percent(5u64), }, protocol_liquidation_fee: Decimal::zero(), + deposit_cap: Default::default(), }, ); diff --git a/packages/interest-rate/Cargo.toml b/packages/interest-rate/Cargo.toml new file mode 100644 index 000000000..905f8f5d8 --- /dev/null +++ b/packages/interest-rate/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "mars-interest-rate" +description = "Computations related to interest rates" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +keywords = { workspace = true } + +[lib] +doctest = false + +[features] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +mars-red-bank-types = { workspace = true } +mars-utils = { workspace = true } diff --git a/packages/interest-rate/src/lib.rs b/packages/interest-rate/src/lib.rs new file mode 100644 index 000000000..8f8a37591 --- /dev/null +++ b/packages/interest-rate/src/lib.rs @@ -0,0 +1,222 @@ +use cosmwasm_std::{Decimal, StdError, StdResult, Uint128}; +use mars_red_bank_types::red_bank::Market; +use mars_utils::math; + +/// Scaling factor used to keep more precision during division / multiplication by index. +pub const SCALING_FACTOR: Uint128 = Uint128::new(1_000_000); + +const SECONDS_PER_YEAR: u64 = 31536000u64; + +pub fn calculate_applied_linear_interest_rate( + index: Decimal, + rate: Decimal, + time_elapsed: u64, +) -> StdResult { + let rate_factor = rate.checked_mul(Decimal::from_ratio( + Uint128::from(time_elapsed), + Uint128::from(SECONDS_PER_YEAR), + ))?; + index.checked_mul(Decimal::one() + rate_factor).map_err(StdError::from) +} + +/// Get scaled liquidity amount from an underlying amount, a Market and timestamp in seconds +/// Liquidity amounts are always truncated to make sure rounding errors accumulate in favor of +/// the protocol +/// NOTE: Calling this function when interests for the market are up to date with the current block +/// and index is not, will use the wrong interest rate to update the index. +/// NOTE: This function should not be used when calculating how much scaled amount is getting +/// burned from given underlying withdraw amount. In that case, all math should be done in underlying +/// amounts then get scaled back again +pub fn get_scaled_liquidity_amount( + amount: Uint128, + market: &Market, + timestamp: u64, +) -> StdResult { + compute_scaled_amount( + amount, + get_updated_liquidity_index(market, timestamp)?, + ScalingOperation::Truncate, + ) +} + +/// Get underlying liquidity amount from a scaled amount, a Market and timestamp in seconds +/// Liquidity amounts are always truncated to make sure rounding errors accumulate in favor of +/// the protocol +/// NOTE: Calling this function when interests for the market are up to date with the current block +/// and index is not, will use the wrong interest rate to update the index. +pub fn get_underlying_liquidity_amount( + amount_scaled: Uint128, + market: &Market, + timestamp: u64, +) -> StdResult { + compute_underlying_amount( + amount_scaled, + get_updated_liquidity_index(market, timestamp)?, + ScalingOperation::Truncate, + ) +} + +/// Get scaled borrow amount from an underlying amount, a Market and timestamp in seconds +/// Debt amounts are always ceiled to make sure rounding errors accumulate in favor of +/// the protocol +/// NOTE: Calling this function when interests for the market are up to date with the current block +/// and index is not, will use the wrong interest rate to update the index. +/// NOTE: This function should not be used when calculating how much scaled amount is getting +/// repaid from a sent underlying amount. In that case, all math should be done in underlying +/// amounts then get scaled back again +pub fn get_scaled_debt_amount( + amount: Uint128, + market: &Market, + timestamp: u64, +) -> StdResult { + compute_scaled_amount( + amount, + get_updated_borrow_index(market, timestamp)?, + ScalingOperation::Ceil, + ) +} + +/// Get underlying borrow amount from a scaled amount, a Market and timestamp in seconds +/// Debt amounts are always ceiled so as for rounding errors to accumulate in favor of +/// the protocol +/// NOTE: Calling this function when interests for the market are up to date with the current block +/// and index is not, will use the wrong interest rate to update the index. +pub fn get_underlying_debt_amount( + amount_scaled: Uint128, + market: &Market, + timestamp: u64, +) -> StdResult { + compute_underlying_amount( + amount_scaled, + get_updated_borrow_index(market, timestamp)?, + ScalingOperation::Ceil, + ) +} + +pub enum ScalingOperation { + Truncate, + Ceil, +} + +/// Scales the amount dividing by an index in order to compute interest rates. Before dividing, +/// the value is multiplied by SCALING_FACTOR for greater precision. +/// Example: +/// Current index is 10. We deposit 6.123456 OSMO (6123456 uosmo). Scaled amount will be +/// 6123456 / 10 = 612345 so we loose some precision. In order to avoid this situation +/// we scale the amount by SCALING_FACTOR. +pub fn compute_scaled_amount( + amount: Uint128, + index: Decimal, + scaling_operation: ScalingOperation, +) -> StdResult { + // Scale by SCALING_FACTOR to have better precision + let scaled_amount = amount.checked_mul(SCALING_FACTOR)?; + match scaling_operation { + ScalingOperation::Truncate => math::divide_uint128_by_decimal(scaled_amount, index), + ScalingOperation::Ceil => math::divide_uint128_by_decimal_and_ceil(scaled_amount, index), + } +} + +/// Descales the amount introduced by `get_scaled_amount`, returning the underlying amount. +/// As interest rate is accumulated the index used to descale the amount should be bigger than the one used to scale it. +pub fn compute_underlying_amount( + scaled_amount: Uint128, + index: Decimal, + scaling_operation: ScalingOperation, +) -> StdResult { + // Multiply scaled amount by decimal (index) + let before_scaling_factor = scaled_amount * index; + + // Descale by SCALING_FACTOR which is introduced when scaling the amount + match scaling_operation { + ScalingOperation::Truncate => Ok(before_scaling_factor.checked_div(SCALING_FACTOR)?), + ScalingOperation::Ceil => { + math::uint128_checked_div_with_ceil(before_scaling_factor, SCALING_FACTOR) + } + } +} + +/// Return applied interest rate for borrow index according to passed blocks +/// NOTE: Calling this function when interests for the market are up to date with the current block +/// and index is not, will use the wrong interest rate to update the index. +pub fn get_updated_borrow_index(market: &Market, timestamp: u64) -> StdResult { + if market.indexes_last_updated < timestamp { + let time_elapsed = timestamp - market.indexes_last_updated; + + if !market.borrow_rate.is_zero() { + let updated_index = calculate_applied_linear_interest_rate( + market.borrow_index, + market.borrow_rate, + time_elapsed, + ); + return updated_index; + } + } + + Ok(market.borrow_index) +} + +/// Return applied interest rate for liquidity index according to passed blocks +/// NOTE: Calling this function when interests for the market are up to date with the current block +/// and index is not, will use the wrong interest rate to update the index. +pub fn get_updated_liquidity_index(market: &Market, timestamp: u64) -> StdResult { + if market.indexes_last_updated > timestamp { + return Err(StdError::generic_err( + format!("Cannot compute updated liquidity index for a timestamp: {} smaller than last updated timestamp for market: {}", timestamp, market.indexes_last_updated) + )); + } + + if market.indexes_last_updated < timestamp { + let time_elapsed = timestamp - market.indexes_last_updated; + + if !market.liquidity_rate.is_zero() { + let updated_index = calculate_applied_linear_interest_rate( + market.liquidity_index, + market.liquidity_rate, + time_elapsed, + ); + return updated_index; + } + } + + Ok(market.liquidity_index) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn accumulated_index_calculation() { + let index = Decimal::from_ratio(1u128, 10u128); + let rate = Decimal::from_ratio(2u128, 10u128); + let time_elapsed = 15768000; // half a year + let accumulated = + calculate_applied_linear_interest_rate(index, rate, time_elapsed).unwrap(); + + assert_eq!(accumulated, Decimal::from_ratio(11u128, 100u128)); + } + + #[test] + fn liquidity_and_debt_rounding() { + let start = Uint128::from(100_000_000_000_u128); + let market = Market { + liquidity_index: Decimal::from_ratio(3_u128, 1_u128), + borrow_index: Decimal::from_ratio(3_u128, 1_u128), + indexes_last_updated: 1, + ..Default::default() + }; + + let scaled_amount_liquidity = get_scaled_liquidity_amount(start, &market, 1).unwrap(); + let scaled_amount_debt = get_scaled_debt_amount(start, &market, 1).unwrap(); + assert_eq!(Uint128::from(33_333_333_333_333_333_u128), scaled_amount_liquidity); + assert_eq!(Uint128::from(33_333_333_333_333_334_u128), scaled_amount_debt); + + let back_to_underlying_liquidity = + get_underlying_liquidity_amount(scaled_amount_liquidity, &market, 1).unwrap(); + let back_to_underlying_debt = + get_underlying_debt_amount(scaled_amount_debt, &market, 1).unwrap(); + assert_eq!(Uint128::from(99_999_999_999_u128), back_to_underlying_liquidity); + assert_eq!(Uint128::from(100_000_000_001_u128), back_to_underlying_debt); + } +} diff --git a/packages/testing/src/integration/mock_env.rs b/packages/testing/src/integration/mock_env.rs index 9a8de4afa..2eeeaf24b 100644 --- a/packages/testing/src/integration/mock_env.rs +++ b/packages/testing/src/integration/mock_env.rs @@ -756,7 +756,7 @@ impl MockEnvBuilder { let oracle_addr = self.deploy_oracle_osmosis(); let red_bank_addr = self.deploy_red_bank(&address_provider_addr); let rewards_collector_addr = self.deploy_rewards_collector_osmosis(&address_provider_addr); - let params_addr = self.deploy_params_osmosis(); + let params_addr = self.deploy_params_osmosis(&address_provider_addr); self.update_address_provider( &address_provider_addr, @@ -909,7 +909,7 @@ impl MockEnvBuilder { .unwrap() } - fn deploy_params_osmosis(&mut self) -> Addr { + fn deploy_params_osmosis(&mut self, address_provider_addr: &Addr) -> Addr { let code_id = self.app.store_code(mock_params_osmosis_contract()); self.app @@ -918,6 +918,7 @@ impl MockEnvBuilder { self.owner.clone(), &mars_params::msg::InstantiateMsg { owner: self.owner.to_string(), + address_provider: address_provider_addr.to_string(), target_health_factor: self.target_health_factor, }, &[], diff --git a/packages/testing/src/mars_mock_querier.rs b/packages/testing/src/mars_mock_querier.rs index a45dd6108..c0255c7bb 100644 --- a/packages/testing/src/mars_mock_querier.rs +++ b/packages/testing/src/mars_mock_querier.rs @@ -76,6 +76,10 @@ impl MarsMockQuerier { self.base.update_balance(contract_addr.to_string(), contract_balances.to_vec()); } + pub fn update_balances(&mut self, addr: impl Into, balance: Vec) { + self.base.update_balance(addr, balance); + } + pub fn set_oracle_price(&mut self, denom: &str, price: Decimal) { self.oracle_querier.prices.insert(denom.to_string(), price); } @@ -185,6 +189,14 @@ impl MarsMockQuerier { .insert((user.into(), collateral.denom.clone()), collateral); } + pub fn set_red_bank_user_debt( + &mut self, + user: impl Into, + debt: red_bank::UserDebtResponse, + ) { + self.redbank_querier.users_denoms_debts.insert((user.into(), debt.denom.clone()), debt); + } + pub fn set_redbank_user_position( &mut self, user_address: String, @@ -201,6 +213,10 @@ impl MarsMockQuerier { self.params_querier.target_health_factor = thf; } + pub fn set_total_deposit(&mut self, denom: impl Into, amount: impl Into) { + self.params_querier.total_deposits.insert(denom.into(), amount.into()); + } + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { match &request { QueryRequest::Wasm(WasmQuery::Smart { diff --git a/packages/testing/src/params_querier.rs b/packages/testing/src/params_querier.rs index 21f18e09e..f3598e2ba 100644 --- a/packages/testing/src/params_querier.rs +++ b/packages/testing/src/params_querier.rs @@ -1,12 +1,13 @@ use std::collections::HashMap; -use cosmwasm_std::{to_binary, Binary, ContractResult, Decimal, QuerierResult}; +use cosmwasm_std::{to_binary, Binary, Coin, ContractResult, Decimal, QuerierResult, Uint128}; use mars_params::{msg::QueryMsg, types::asset::AssetParams}; #[derive(Default)] pub struct ParamsQuerier { pub target_health_factor: Decimal, pub params: HashMap, + pub total_deposits: HashMap, } impl ParamsQuerier { @@ -19,6 +20,16 @@ impl ParamsQuerier { Some(params) => to_binary(¶ms).into(), None => Err(format!("[mock]: could not find the params for {denom}")).into(), }, + QueryMsg::TotalDeposit { + denom, + } => match self.total_deposits.get(&denom) { + Some(amount) => to_binary(&Coin { + denom, + amount: *amount, + }) + .into(), + None => Err(format!("[mock]: could not find total deposit for {denom}")).into(), + }, _ => Err("[mock]: Unsupported params query".to_string()).into(), }; Ok(ret).into() diff --git a/packages/testing/src/red_bank_querier.rs b/packages/testing/src/red_bank_querier.rs index f31a9c8c8..6c5a1fca7 100644 --- a/packages/testing/src/red_bank_querier.rs +++ b/packages/testing/src/red_bank_querier.rs @@ -2,13 +2,14 @@ use std::collections::HashMap; use cosmwasm_std::{to_binary, Binary, ContractResult, QuerierResult}; use mars_red_bank_types::red_bank::{ - Market, QueryMsg, UserCollateralResponse, UserPositionResponse, + Market, QueryMsg, UserCollateralResponse, UserDebtResponse, UserPositionResponse, }; #[derive(Default)] pub struct RedBankQuerier { pub markets: HashMap, pub users_denoms_collaterals: HashMap<(String, String), UserCollateralResponse>, + pub users_denoms_debts: HashMap<(String, String), UserDebtResponse>, pub users_positions: HashMap, } @@ -17,10 +18,10 @@ impl RedBankQuerier { let ret: ContractResult = match query { QueryMsg::Market { denom, - } => match self.markets.get(&denom) { - Some(market) => to_binary(&market).into(), - None => Err(format!("[mock]: could not find the market for {denom}")).into(), - }, + } => { + let maybe_market = self.markets.get(&denom); + to_binary(&maybe_market).into() + } QueryMsg::UserCollateral { user, account_id: _, @@ -29,6 +30,13 @@ impl RedBankQuerier { Some(collateral) => to_binary(&collateral).into(), None => Err(format!("[mock]: could not find the collateral for {user}")).into(), }, + QueryMsg::UserDebt { + user, + denom, + } => match self.users_denoms_debts.get(&(user.clone(), denom)) { + Some(debt) => to_binary(&debt).into(), + None => Err(format!("[mock]: could not find the debt for {user}")).into(), + }, QueryMsg::UserPosition { user, } => match self.users_positions.get(&user) { diff --git a/schemas/mars-params/mars-params.json b/schemas/mars-params/mars-params.json index 873370b7f..b74a5a050 100644 --- a/schemas/mars-params/mars-params.json +++ b/schemas/mars-params/mars-params.json @@ -7,10 +7,15 @@ "title": "InstantiateMsg", "type": "object", "required": [ + "address_provider", "owner", "target_health_factor" ], "properties": { + "address_provider": { + "description": "Address of the address provider contract", + "type": "string" + }, "owner": { "description": "Contract's owner", "type": "string" @@ -103,6 +108,7 @@ "required": [ "credit_manager", "denom", + "deposit_cap", "liquidation_bonus", "liquidation_threshold", "max_loan_to_value", @@ -116,6 +122,9 @@ "denom": { "type": "string" }, + "deposit_cap": { + "$ref": "#/definitions/Uint128" + }, "liquidation_bonus": { "$ref": "#/definitions/LiquidationBonus" }, @@ -479,16 +488,12 @@ "type": "object", "required": [ "borrow_enabled", - "deposit_cap", "deposit_enabled" ], "properties": { "borrow_enabled": { "type": "boolean" }, - "deposit_cap": { - "$ref": "#/definitions/Uint128" - }, "deposit_enabled": { "type": "boolean" } @@ -694,6 +699,28 @@ } }, "additionalProperties": false + }, + { + "description": "Compute the total amount deposited of the given asset across Red Bank and Credit Manager.", + "type": "object", + "required": [ + "total_deposit" + ], + "properties": { + "total_deposit": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, @@ -717,6 +744,7 @@ "required": [ "credit_manager", "denom", + "deposit_cap", "liquidation_bonus", "liquidation_threshold", "max_loan_to_value", @@ -730,6 +758,9 @@ "denom": { "type": "string" }, + "deposit_cap": { + "$ref": "#/definitions/Uint128" + }, "liquidation_bonus": { "$ref": "#/definitions/LiquidationBonus" }, @@ -893,16 +924,12 @@ "type": "object", "required": [ "borrow_enabled", - "deposit_cap", "deposit_enabled" ], "properties": { "borrow_enabled": { "type": "boolean" }, - "deposit_cap": { - "$ref": "#/definitions/Uint128" - }, "deposit_enabled": { "type": "boolean" } @@ -1067,6 +1094,7 @@ "required": [ "credit_manager", "denom", + "deposit_cap", "liquidation_bonus", "liquidation_threshold", "max_loan_to_value", @@ -1080,6 +1108,9 @@ "denom": { "type": "string" }, + "deposit_cap": { + "$ref": "#/definitions/Uint128" + }, "liquidation_bonus": { "$ref": "#/definitions/LiquidationBonus" }, @@ -1247,16 +1278,12 @@ "type": "object", "required": [ "borrow_enabled", - "deposit_cap", "deposit_enabled" ], "properties": { "borrow_enabled": { "type": "boolean" }, - "deposit_cap": { - "$ref": "#/definitions/Uint128" - }, "deposit_enabled": { "type": "boolean" } @@ -1312,6 +1339,29 @@ "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" }, + "total_deposit": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Coin", + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + }, + "definitions": { + "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" + } + } + }, "vault_config": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "VaultConfigBase_for_Addr", diff --git a/scripts/deploy/base/deployer.ts b/scripts/deploy/base/deployer.ts index 0d5409ca4..a5c7ea102 100644 --- a/scripts/deploy/base/deployer.ts +++ b/scripts/deploy/base/deployer.ts @@ -176,6 +176,7 @@ export class Deployer { async instantiateParams() { const msg: ParamsInstantiateMsg = { owner: this.deployerAddress, + address_provider: this.storage.addresses['address-provider']!, target_health_factor: this.config.targetHealthFactor, } await this.instantiate('params', this.storage.codeIds.params!, msg) @@ -204,8 +205,8 @@ export class Deployer { red_bank: { borrow_enabled: assetConfig.red_bank.borrow_enabled, deposit_enabled: assetConfig.red_bank.borrow_enabled, - deposit_cap: assetConfig.red_bank.deposit_cap, }, + deposit_cap: assetConfig.deposit_cap, }, }, }, diff --git a/scripts/deploy/neutron/config_mainnet.ts b/scripts/deploy/neutron/config_mainnet.ts index 336137906..2189aaf6b 100644 --- a/scripts/deploy/neutron/config_mainnet.ts +++ b/scripts/deploy/neutron/config_mainnet.ts @@ -289,10 +289,10 @@ export const ntrnAsset: AssetConfig = { whitelisted: false, }, red_bank: { - deposit_cap: '5000000000000', borrow_enabled: true, deposit_enabled: true, }, + deposit_cap: '5000000000000', } export const atomAsset: AssetConfig = { @@ -319,10 +319,10 @@ export const atomAsset: AssetConfig = { whitelisted: false, }, red_bank: { - deposit_cap: '150000000000', borrow_enabled: true, deposit_enabled: true, }, + deposit_cap: '150000000000', } export const axlUSDCAsset: AssetConfig = { @@ -349,10 +349,10 @@ export const axlUSDCAsset: AssetConfig = { whitelisted: false, }, red_bank: { - deposit_cap: '500000000000', borrow_enabled: true, deposit_enabled: true, }, + deposit_cap: '500000000000', } export const neutronMainnetConfig: DeploymentConfig = { diff --git a/scripts/deploy/neutron/config_testnet.ts b/scripts/deploy/neutron/config_testnet.ts index 904c98c0f..e15dcd429 100644 --- a/scripts/deploy/neutron/config_testnet.ts +++ b/scripts/deploy/neutron/config_testnet.ts @@ -248,10 +248,10 @@ export const ntrnAsset: AssetConfig = { whitelisted: false, }, red_bank: { - deposit_cap: '5000000000000', borrow_enabled: true, deposit_enabled: true, }, + deposit_cap: '5000000000000', } export const atomAsset: AssetConfig = { @@ -278,10 +278,10 @@ export const atomAsset: AssetConfig = { whitelisted: false, }, red_bank: { - deposit_cap: '150000000000', borrow_enabled: true, deposit_enabled: true, }, + deposit_cap: '150000000000', } export const axlUSDCAsset: AssetConfig = { @@ -308,10 +308,10 @@ export const axlUSDCAsset: AssetConfig = { whitelisted: false, }, red_bank: { - deposit_cap: '500000000000', borrow_enabled: true, deposit_enabled: true, }, + deposit_cap: '500000000000', } export const neutronTestnetConfig: DeploymentConfig = { diff --git a/scripts/deploy/neutron/config_testnet_multisig.ts b/scripts/deploy/neutron/config_testnet_multisig.ts index 304dd32e1..15a0a2964 100644 --- a/scripts/deploy/neutron/config_testnet_multisig.ts +++ b/scripts/deploy/neutron/config_testnet_multisig.ts @@ -285,10 +285,10 @@ export const ntrnAsset: AssetConfig = { whitelisted: false, }, red_bank: { - deposit_cap: '5000000000000', borrow_enabled: true, deposit_enabled: true, }, + deposit_cap: '5000000000000', } export const atomAsset: AssetConfig = { @@ -315,10 +315,10 @@ export const atomAsset: AssetConfig = { whitelisted: false, }, red_bank: { - deposit_cap: '150000000000', borrow_enabled: true, deposit_enabled: true, }, + deposit_cap: '150000000000', } export const axlUSDCAsset: AssetConfig = { @@ -345,10 +345,10 @@ export const axlUSDCAsset: AssetConfig = { whitelisted: false, }, red_bank: { - deposit_cap: '500000000000', borrow_enabled: true, deposit_enabled: true, }, + deposit_cap: '500000000000', } export const neutronTetstnetMultisigConfig: DeploymentConfig = { diff --git a/scripts/deploy/osmosis/config.ts b/scripts/deploy/osmosis/config.ts index 522b49b12..9dc457a75 100644 --- a/scripts/deploy/osmosis/config.ts +++ b/scripts/deploy/osmosis/config.ts @@ -47,9 +47,9 @@ export const osmoAsset: AssetConfig = { }, red_bank: { borrow_enabled: true, - deposit_cap: '2500000000000', deposit_enabled: true, }, + deposit_cap: '2500000000000', } export const atomAsset: AssetConfig = { @@ -76,9 +76,9 @@ export const atomAsset: AssetConfig = { }, red_bank: { borrow_enabled: true, - deposit_cap: '100000000000', deposit_enabled: true, }, + deposit_cap: '100000000000', } export const atomAssetTest: AssetConfig = { @@ -105,9 +105,9 @@ export const atomAssetTest: AssetConfig = { }, red_bank: { borrow_enabled: true, - deposit_cap: '100000000000', deposit_enabled: true, }, + deposit_cap: '100000000000', } export const axlUSDCAsset: AssetConfig = { @@ -134,9 +134,9 @@ export const axlUSDCAsset: AssetConfig = { }, red_bank: { borrow_enabled: true, - deposit_cap: '500000000000', deposit_enabled: true, }, + deposit_cap: '500000000000', } export const axlUSDCAssetTest: AssetConfig = { @@ -163,9 +163,9 @@ export const axlUSDCAssetTest: AssetConfig = { }, red_bank: { borrow_enabled: true, - deposit_cap: '500000000000', deposit_enabled: true, }, + deposit_cap: '500000000000', } export const marsAssetTest: AssetConfig = { @@ -192,9 +192,9 @@ export const marsAssetTest: AssetConfig = { }, red_bank: { borrow_enabled: true, - deposit_cap: '500000000000', deposit_enabled: true, }, + deposit_cap: '500000000000', } // export const osmoOracle: OracleConfig = { diff --git a/scripts/deploy/osmosis/mainnetConfig.ts b/scripts/deploy/osmosis/mainnetConfig.ts index ea094b00a..fecc8d4c2 100644 --- a/scripts/deploy/osmosis/mainnetConfig.ts +++ b/scripts/deploy/osmosis/mainnetConfig.ts @@ -30,9 +30,9 @@ export const osmoAsset: AssetConfig = { max_loan_to_value: '0.59', red_bank: { borrow_enabled: true, - deposit_cap: '2500000000000', deposit_enabled: true, }, + deposit_cap: '2500000000000', } export const atomAsset: AssetConfig = { @@ -52,9 +52,9 @@ export const atomAsset: AssetConfig = { max_loan_to_value: '0.68', red_bank: { borrow_enabled: true, - deposit_cap: '100000000000', deposit_enabled: true, }, + deposit_cap: '100000000000', } export const axlUSDCAsset: AssetConfig = { @@ -74,9 +74,9 @@ export const axlUSDCAsset: AssetConfig = { max_loan_to_value: '0.74', red_bank: { borrow_enabled: true, - deposit_cap: '500000000000', deposit_enabled: true, }, + deposit_cap: '500000000000', } export const atomOracle: OracleConfig = { diff --git a/scripts/deploy/osmosis/testnetConfig.ts b/scripts/deploy/osmosis/testnetConfig.ts index dc4e4aa73..947a1fb11 100644 --- a/scripts/deploy/osmosis/testnetConfig.ts +++ b/scripts/deploy/osmosis/testnetConfig.ts @@ -30,9 +30,9 @@ export const osmoAsset: AssetConfig = { max_loan_to_value: '0.59', red_bank: { borrow_enabled: true, - deposit_cap: '2500000000000', deposit_enabled: true, }, + deposit_cap: '2500000000000', } export const atomAsset: AssetConfig = { @@ -52,9 +52,9 @@ export const atomAsset: AssetConfig = { max_loan_to_value: '0.68', red_bank: { borrow_enabled: true, - deposit_cap: '100000000000', deposit_enabled: true, }, + deposit_cap: '100000000000', } export const USDCAsset: AssetConfig = { @@ -74,9 +74,9 @@ export const USDCAsset: AssetConfig = { max_loan_to_value: '0.74', red_bank: { borrow_enabled: true, - deposit_cap: '500000000000', deposit_enabled: true, }, + deposit_cap: '500000000000', } export const usdcOsmoVault: VaultConfig = { diff --git a/scripts/types/config.ts b/scripts/types/config.ts index 5b8515bcc..5243ab649 100644 --- a/scripts/types/config.ts +++ b/scripts/types/config.ts @@ -14,6 +14,7 @@ import { RedBankSettings, } from './generated/mars-params/MarsParams.types' import { NeutronIbcConfig } from './generated/mars-rewards-collector-base/MarsRewardsCollectorBase.types' +import { Uint128 } from './generated/mars-red-bank/MarsRedBank.types' type SwapRoute = { denom_in: string @@ -79,6 +80,7 @@ export interface AssetConfig { max_loan_to_value: Decimal protocol_liquidation_fee: Decimal red_bank: RedBankSettings + deposit_cap: Uint128 } export interface VaultConfig { addr: string diff --git a/scripts/types/generated/mars-params/MarsParams.client.ts b/scripts/types/generated/mars-params/MarsParams.client.ts index ef6a19f05..610c71efe 100644 --- a/scripts/types/generated/mars-params/MarsParams.client.ts +++ b/scripts/types/generated/mars-params/MarsParams.client.ts @@ -57,6 +57,7 @@ export interface MarsParamsReadOnlyInterface { startAfter?: string }) => Promise targetHealthFactor: () => Promise + totalDeposit: ({ denom }: { denom: string }) => Promise } export class MarsParamsQueryClient implements MarsParamsReadOnlyInterface { client: CosmWasmClient @@ -71,6 +72,7 @@ export class MarsParamsQueryClient implements MarsParamsReadOnlyInterface { this.vaultConfig = this.vaultConfig.bind(this) this.allVaultConfigs = this.allVaultConfigs.bind(this) this.targetHealthFactor = this.targetHealthFactor.bind(this) + this.totalDeposit = this.totalDeposit.bind(this) } owner = async (): Promise => { @@ -125,6 +127,13 @@ export class MarsParamsQueryClient implements MarsParamsReadOnlyInterface { target_health_factor: {}, }) } + totalDeposit = async ({ denom }: { denom: string }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + total_deposit: { + denom, + }, + }) + } } export interface MarsParamsInterface extends MarsParamsReadOnlyInterface { contractAddress: string diff --git a/scripts/types/generated/mars-params/MarsParams.react-query.ts b/scripts/types/generated/mars-params/MarsParams.react-query.ts index 9b4c1121e..beae94e83 100644 --- a/scripts/types/generated/mars-params/MarsParams.react-query.ts +++ b/scripts/types/generated/mars-params/MarsParams.react-query.ts @@ -65,6 +65,10 @@ export const marsParamsQueryKeys = { [ { ...marsParamsQueryKeys.address(contractAddress)[0], method: 'target_health_factor', args }, ] as const, + totalDeposit: (contractAddress: string | undefined, args?: Record) => + [ + { ...marsParamsQueryKeys.address(contractAddress)[0], method: 'total_deposit', args }, + ] as const, } export interface MarsParamsReactQuery { client: MarsParamsQueryClient | undefined @@ -75,6 +79,27 @@ export interface MarsParamsReactQuery { initialData?: undefined } } +export interface MarsParamsTotalDepositQuery extends MarsParamsReactQuery { + args: { + denom: string + } +} +export function useMarsParamsTotalDepositQuery({ + client, + args, + options, +}: MarsParamsTotalDepositQuery) { + return useQuery( + marsParamsQueryKeys.totalDeposit(client?.contractAddress, args), + () => + client + ? client.totalDeposit({ + denom: args.denom, + }) + : Promise.reject(new Error('Invalid client')), + { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, + ) +} export interface MarsParamsTargetHealthFactorQuery extends MarsParamsReactQuery {} export function useMarsParamsTargetHealthFactorQuery({ diff --git a/scripts/types/generated/mars-params/MarsParams.types.ts b/scripts/types/generated/mars-params/MarsParams.types.ts index 6c2e27625..11eb13940 100644 --- a/scripts/types/generated/mars-params/MarsParams.types.ts +++ b/scripts/types/generated/mars-params/MarsParams.types.ts @@ -7,6 +7,7 @@ export type Decimal = string export interface InstantiateMsg { + address_provider: string owner: string target_health_factor: Decimal } @@ -86,6 +87,7 @@ export type RedBankEmergencyUpdate = { export interface AssetParamsBaseForString { credit_manager: CmSettingsForString denom: string + deposit_cap: Uint128 liquidation_bonus: LiquidationBonus liquidation_threshold: Decimal max_loan_to_value: Decimal @@ -109,7 +111,6 @@ export interface LiquidationBonus { } export interface RedBankSettings { borrow_enabled: boolean - deposit_cap: Uint128 deposit_enabled: boolean } export interface VaultConfigBaseForString { @@ -154,6 +155,11 @@ export type QueryMsg = | { target_health_factor: {} } + | { + total_deposit: { + denom: string + } + } export type HlsAssetTypeForAddr = | { coin: { @@ -170,6 +176,7 @@ export type ArrayOfAssetParamsBaseForAddr = AssetParamsBaseForAddr[] export interface AssetParamsBaseForAddr { credit_manager: CmSettingsForAddr denom: string + deposit_cap: Uint128 liquidation_bonus: LiquidationBonus liquidation_threshold: Decimal max_loan_to_value: Decimal From 8117c61191aa81263b958fc9c568d0a43d14e7db Mon Sep 17 00:00:00 2001 From: piobab Date: Thu, 3 Aug 2023 09:19:50 +0200 Subject: [PATCH 14/43] Remove custom math. (#283) --- Cargo.toml | 2 +- contracts/red-bank/src/error.rs | 7 +- contracts/red-bank/src/health.rs | 2 +- contracts/red-bank/src/interest_rates.rs | 4 +- contracts/red-bank/src/query.rs | 24 +- contracts/red-bank/tests/test_borrow.rs | 5 +- contracts/red-bank/tests/test_misc.rs | 9 +- contracts/red-bank/tests/test_withdraw.rs | 12 +- packages/interest-rate/src/lib.rs | 22 +- packages/types/src/error.rs | 16 +- .../types/src/red_bank/interest_rate_model.rs | 82 ++---- packages/utils/src/lib.rs | 1 - packages/utils/src/math.rs | 261 ------------------ 13 files changed, 85 insertions(+), 362 deletions(-) delete mode 100644 packages/utils/src/math.rs diff --git a/Cargo.toml b/Cargo.toml index 83cd8ee78..e69bc3b5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ keywords = [ anyhow = "1.0.71" bech32 = "0.9.1" cosmwasm-schema = "1.2.6" -cosmwasm-std = "1.2.6" +cosmwasm-std = "1.3.1" cw2 = "1.1.0" cw-storage-plus = "1.0.1" cw-utils = "1.0.1" diff --git a/contracts/red-bank/src/error.rs b/contracts/red-bank/src/error.rs index 8f07ce8b1..54b1972ac 100644 --- a/contracts/red-bank/src/error.rs +++ b/contracts/red-bank/src/error.rs @@ -1,4 +1,6 @@ -use cosmwasm_std::{CheckedFromRatioError, CheckedMultiplyFractionError, OverflowError, StdError}; +use cosmwasm_std::{ + CheckedFromRatioError, CheckedMultiplyFractionError, DivideByZeroError, OverflowError, StdError, +}; use cw_utils::PaymentError; use mars_health::error::HealthError; use mars_liquidation::error::LiquidationError; @@ -33,6 +35,9 @@ pub enum ContractError { #[error("{0}")] CheckedMultiplyFraction(#[from] CheckedMultiplyFractionError), + #[error("{0}")] + DivideByZero(#[from] DivideByZeroError), + #[error("{0}")] Health(#[from] HealthError), diff --git a/contracts/red-bank/src/health.rs b/contracts/red-bank/src/health.rs index ae8b3b775..aaced7409 100644 --- a/contracts/red-bank/src/health.rs +++ b/contracts/red-bank/src/health.rs @@ -112,7 +112,7 @@ pub fn get_user_positions_map( user_addr: &Addr, oracle_addr: &Addr, params_addr: &Addr, -) -> StdResult> { +) -> Result, ContractError> { let block_time = env.block.time.seconds(); // Find all denoms that the user has a collateral or debt position in diff --git a/contracts/red-bank/src/interest_rates.rs b/contracts/red-bank/src/interest_rates.rs index d25f83236..d0efc4c80 100644 --- a/contracts/red-bank/src/interest_rates.rs +++ b/contracts/red-bank/src/interest_rates.rs @@ -1,6 +1,6 @@ use std::str; -use cosmwasm_std::{Addr, Decimal, Env, Event, Response, StdResult, Storage, Uint128}; +use cosmwasm_std::{Addr, Decimal, Env, Event, Response, Storage, Uint128}; use mars_interest_rate::{ calculate_applied_linear_interest_rate, compute_scaled_amount, compute_underlying_amount, get_underlying_debt_amount, get_underlying_liquidity_amount, ScalingOperation, @@ -26,7 +26,7 @@ pub fn apply_accumulated_interests( rewards_collector_addr: &Addr, incentives_addr: &Addr, mut response: Response, -) -> StdResult { +) -> Result { let current_timestamp = env.block.time.seconds(); let previous_borrow_index = market.borrow_index; diff --git a/contracts/red-bank/src/query.rs b/contracts/red-bank/src/query.rs index e9a74ab16..44abb98ea 100644 --- a/contracts/red-bank/src/query.rs +++ b/contracts/red-bank/src/query.rs @@ -93,7 +93,7 @@ pub fn query_user_debt( block: &BlockInfo, user_addr: Addr, denom: String, -) -> StdResult { +) -> Result { let Debt { amount_scaled, uncollateralized, @@ -117,7 +117,7 @@ pub fn query_user_debts( user_addr: Addr, start_after: Option, limit: Option, -) -> StdResult> { +) -> Result, ContractError> { let block_time = block.time.seconds(); let start = start_after.map(|denom| Bound::ExclusiveRaw(denom.into_bytes())); @@ -151,7 +151,7 @@ pub fn query_user_collateral( user_addr: Addr, account_id: Option, denom: String, -) -> StdResult { +) -> Result { let acc_id = account_id.unwrap_or("".to_string()); let Collateral { @@ -178,7 +178,7 @@ pub fn query_user_collaterals( account_id: Option, start_after: Option, limit: Option, -) -> StdResult> { +) -> Result, ContractError> { let block_time = block.time.seconds(); let start = start_after.map(|denom| Bound::ExclusiveRaw(denom.into_bytes())); @@ -213,9 +213,9 @@ pub fn query_scaled_liquidity_amount( env: Env, denom: String, amount: Uint128, -) -> StdResult { +) -> Result { let market = MARKETS.load(deps.storage, &denom)?; - get_scaled_liquidity_amount(amount, &market, env.block.time.seconds()) + Ok(get_scaled_liquidity_amount(amount, &market, env.block.time.seconds())?) } pub fn query_scaled_debt_amount( @@ -223,9 +223,9 @@ pub fn query_scaled_debt_amount( env: Env, denom: String, amount: Uint128, -) -> StdResult { +) -> Result { let market = MARKETS.load(deps.storage, &denom)?; - get_scaled_debt_amount(amount, &market, env.block.time.seconds()) + Ok(get_scaled_debt_amount(amount, &market, env.block.time.seconds())?) } pub fn query_underlying_liquidity_amount( @@ -233,9 +233,9 @@ pub fn query_underlying_liquidity_amount( env: Env, denom: String, amount_scaled: Uint128, -) -> StdResult { +) -> Result { let market = MARKETS.load(deps.storage, &denom)?; - get_underlying_liquidity_amount(amount_scaled, &market, env.block.time.seconds()) + Ok(get_underlying_liquidity_amount(amount_scaled, &market, env.block.time.seconds())?) } pub fn query_underlying_debt_amount( @@ -243,9 +243,9 @@ pub fn query_underlying_debt_amount( env: Env, denom: String, amount_scaled: Uint128, -) -> StdResult { +) -> Result { let market = MARKETS.load(deps.storage, &denom)?; - get_underlying_debt_amount(amount_scaled, &market, env.block.time.seconds()) + Ok(get_underlying_debt_amount(amount_scaled, &market, env.block.time.seconds())?) } pub fn query_user_position( diff --git a/contracts/red-bank/tests/test_borrow.rs b/contracts/red-bank/tests/test_borrow.rs index 98314002e..7daa4ba90 100644 --- a/contracts/red-bank/tests/test_borrow.rs +++ b/contracts/red-bank/tests/test_borrow.rs @@ -18,7 +18,6 @@ use mars_red_bank::{ }; use mars_red_bank_types::red_bank::{ExecuteMsg, Market}; use mars_testing::{mock_env, mock_env_at_block_time, MockEnvParams}; -use mars_utils::math; use crate::helpers::th_default_asset_params; @@ -965,10 +964,10 @@ fn borrow_collateral_check() { .unwrap() * exchange_rate_3); let exceeding_borrow_amount = - math::divide_uint128_by_decimal(max_borrow_allowed_in_base_asset, exchange_rate_2).unwrap() + max_borrow_allowed_in_base_asset.checked_div_floor(exchange_rate_2).unwrap() + Uint128::from(100_u64); let permissible_borrow_amount = - math::divide_uint128_by_decimal(max_borrow_allowed_in_base_asset, exchange_rate_2).unwrap() + max_borrow_allowed_in_base_asset.checked_div_floor(exchange_rate_2).unwrap() - Uint128::from(100_u64); // borrow above the allowed amount given current collateral, should fail diff --git a/contracts/red-bank/tests/test_misc.rs b/contracts/red-bank/tests/test_misc.rs index d710b694c..f1c6a361d 100644 --- a/contracts/red-bank/tests/test_misc.rs +++ b/contracts/red-bank/tests/test_misc.rs @@ -20,7 +20,6 @@ use mars_red_bank::{ }; use mars_red_bank_types::red_bank::{Debt, ExecuteMsg, Market}; use mars_testing::{mock_env, mock_env_at_block_time, MockEnvParams}; -use mars_utils::math; use crate::helpers::th_default_asset_params; @@ -338,11 +337,9 @@ fn update_asset_collateral() { * token_2_exchange_rate; let weighted_liquidation_threshold_in_base_asset = token_1_weighted_lt_in_base_asset + token_2_weighted_lt_in_base_asset; - let max_debt_for_valid_hf = math::divide_uint128_by_decimal( - weighted_liquidation_threshold_in_base_asset, - token_3_exchange_rate, - ) - .unwrap(); + let max_debt_for_valid_hf = weighted_liquidation_threshold_in_base_asset + .checked_div_floor(token_3_exchange_rate) + .unwrap(); let token_3_debt_scaled = get_scaled_debt_amount( max_debt_for_valid_hf, &market_3_initial, diff --git a/contracts/red-bank/tests/test_withdraw.rs b/contracts/red-bank/tests/test_withdraw.rs index 235fea0ea..4496a9c86 100644 --- a/contracts/red-bank/tests/test_withdraw.rs +++ b/contracts/red-bank/tests/test_withdraw.rs @@ -23,7 +23,6 @@ use mars_red_bank_types::{ red_bank::{Collateral, Debt, ExecuteMsg, Market}, }; use mars_testing::{mock_env_at_block_time, MarsMockQuerier}; -use mars_utils::math; use crate::helpers::th_default_asset_params; @@ -637,13 +636,12 @@ fn how_much_to_withdraw(suite: &HealthCheckTestSuite, block_time: u64) -> Uint12 * prices[1]; // How much to withdraw in base asset to have health factor equal to one - let how_much_to_withdraw_in_base_asset = math::divide_uint128_by_decimal( - weighted_liquidation_threshold_in_base_asset - total_collateralized_debt_in_base_asset, - asset_params[2].liquidation_threshold, - ) - .unwrap(); + let how_much_to_withdraw_in_base_asset = (weighted_liquidation_threshold_in_base_asset + - total_collateralized_debt_in_base_asset) + .checked_div_floor(asset_params[2].liquidation_threshold) + .unwrap(); - math::divide_uint128_by_decimal(how_much_to_withdraw_in_base_asset, prices[2]).unwrap() + how_much_to_withdraw_in_base_asset.checked_div_floor(prices[2]).unwrap() } #[test] diff --git a/packages/interest-rate/src/lib.rs b/packages/interest-rate/src/lib.rs index 8f8a37591..e8631da1c 100644 --- a/packages/interest-rate/src/lib.rs +++ b/packages/interest-rate/src/lib.rs @@ -1,6 +1,5 @@ use cosmwasm_std::{Decimal, StdError, StdResult, Uint128}; -use mars_red_bank_types::red_bank::Market; -use mars_utils::math; +use mars_red_bank_types::{error::MarsError, red_bank::Market}; /// Scaling factor used to keep more precision during division / multiplication by index. pub const SCALING_FACTOR: Uint128 = Uint128::new(1_000_000); @@ -31,7 +30,7 @@ pub fn get_scaled_liquidity_amount( amount: Uint128, market: &Market, timestamp: u64, -) -> StdResult { +) -> Result { compute_scaled_amount( amount, get_updated_liquidity_index(market, timestamp)?, @@ -48,7 +47,7 @@ pub fn get_underlying_liquidity_amount( amount_scaled: Uint128, market: &Market, timestamp: u64, -) -> StdResult { +) -> Result { compute_underlying_amount( amount_scaled, get_updated_liquidity_index(market, timestamp)?, @@ -68,7 +67,7 @@ pub fn get_scaled_debt_amount( amount: Uint128, market: &Market, timestamp: u64, -) -> StdResult { +) -> Result { compute_scaled_amount( amount, get_updated_borrow_index(market, timestamp)?, @@ -85,7 +84,7 @@ pub fn get_underlying_debt_amount( amount_scaled: Uint128, market: &Market, timestamp: u64, -) -> StdResult { +) -> Result { compute_underlying_amount( amount_scaled, get_updated_borrow_index(market, timestamp)?, @@ -108,12 +107,12 @@ pub fn compute_scaled_amount( amount: Uint128, index: Decimal, scaling_operation: ScalingOperation, -) -> StdResult { +) -> Result { // Scale by SCALING_FACTOR to have better precision let scaled_amount = amount.checked_mul(SCALING_FACTOR)?; match scaling_operation { - ScalingOperation::Truncate => math::divide_uint128_by_decimal(scaled_amount, index), - ScalingOperation::Ceil => math::divide_uint128_by_decimal_and_ceil(scaled_amount, index), + ScalingOperation::Truncate => Ok(scaled_amount.checked_div_floor(index)?), + ScalingOperation::Ceil => Ok(scaled_amount.checked_div_ceil(index)?), } } @@ -123,7 +122,7 @@ pub fn compute_underlying_amount( scaled_amount: Uint128, index: Decimal, scaling_operation: ScalingOperation, -) -> StdResult { +) -> Result { // Multiply scaled amount by decimal (index) let before_scaling_factor = scaled_amount * index; @@ -131,7 +130,8 @@ pub fn compute_underlying_amount( match scaling_operation { ScalingOperation::Truncate => Ok(before_scaling_factor.checked_div(SCALING_FACTOR)?), ScalingOperation::Ceil => { - math::uint128_checked_div_with_ceil(before_scaling_factor, SCALING_FACTOR) + let scaling_factor_dec = Decimal::from_ratio(SCALING_FACTOR, Uint128::one()); + Ok(before_scaling_factor.checked_div_ceil(scaling_factor_dec)?) } } } diff --git a/packages/types/src/error.rs b/packages/types/src/error.rs index 0a2c667db..e9cb63111 100644 --- a/packages/types/src/error.rs +++ b/packages/types/src/error.rs @@ -1,4 +1,6 @@ -use cosmwasm_std::StdError; +use cosmwasm_std::{ + CheckedFromRatioError, CheckedMultiplyFractionError, DivideByZeroError, OverflowError, StdError, +}; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -22,6 +24,18 @@ pub enum MarsError { Deserialize { target_type: String, }, + + #[error("{0}")] + Overflow(#[from] OverflowError), + + #[error("{0}")] + DivideByZero(#[from] DivideByZeroError), + + #[error("{0}")] + CheckedFromRatio(#[from] CheckedFromRatioError), + + #[error("{0}")] + CheckedMultiplyFraction(#[from] CheckedMultiplyFractionError), } impl From for StdError { diff --git a/packages/types/src/red_bank/interest_rate_model.rs b/packages/types/src/red_bank/interest_rate_model.rs index 9e2af3129..d94e1c7e2 100644 --- a/packages/types/src/red_bank/interest_rate_model.rs +++ b/packages/types/src/red_bank/interest_rate_model.rs @@ -1,6 +1,8 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Decimal, StdError, StdResult}; -use mars_utils::{error::ValidationError, helpers::decimal_param_le_one, math}; +use cosmwasm_std::Decimal; +use mars_utils::{error::ValidationError, helpers::decimal_param_le_one}; + +use crate::error::MarsError; #[cw_serde] #[derive(Eq, Default)] @@ -21,28 +23,26 @@ impl InterestRateModel { Ok(()) } - pub fn get_borrow_rate(&self, current_utilization_rate: Decimal) -> StdResult { + pub fn get_borrow_rate(&self, current_utilization_rate: Decimal) -> Result { let new_borrow_rate = if current_utilization_rate <= self.optimal_utilization_rate { if current_utilization_rate.is_zero() { - // prevent division by zero when optimal_utilization_rate is zero + // prevent division by zero when current_utilization_rate is zero self.base } else { // The borrow interest rates increase slowly with utilization self.base - + self.slope_1.checked_mul(math::divide_decimal_by_decimal( - current_utilization_rate, - self.optimal_utilization_rate, - )?)? + + self.slope_1.checked_mul( + current_utilization_rate.checked_div(self.optimal_utilization_rate)?, + )? } } else { // The borrow interest rates increase sharply with utilization self.base + self.slope_1 - + math::divide_decimal_by_decimal( - self.slope_2 - .checked_mul(current_utilization_rate - self.optimal_utilization_rate)?, - Decimal::one() - self.optimal_utilization_rate, - )? + + self + .slope_2 + .checked_mul(current_utilization_rate - self.optimal_utilization_rate)? + .checked_div(Decimal::one() - self.optimal_utilization_rate)? }; Ok(new_borrow_rate) } @@ -52,12 +52,11 @@ impl InterestRateModel { borrow_rate: Decimal, current_utilization_rate: Decimal, reserve_factor: Decimal, - ) -> StdResult { - borrow_rate + ) -> Result { + Ok(borrow_rate .checked_mul(current_utilization_rate)? // This operation should not underflow as reserve_factor is checked to be <= 1 - .checked_mul(Decimal::one() - reserve_factor) - .map_err(StdError::from) + .checked_mul(Decimal::one() - reserve_factor)?) } } @@ -91,21 +90,13 @@ mod tests { market.update_interest_rates(utilization_rate).unwrap(); - let expected_borrow_rate = model.base - + math::divide_decimal_by_decimal( - model.slope_1.checked_mul(utilization_rate).unwrap(), - model.optimal_utilization_rate, - ) - .unwrap(); + let expected_borrow_rate = + model.base + model.slope_1 * utilization_rate / model.optimal_utilization_rate; assert_eq!(market.borrow_rate, expected_borrow_rate); assert_eq!( market.liquidity_rate, - expected_borrow_rate - .checked_mul(utilization_rate) - .unwrap() - .checked_mul(Decimal::one() - reserve_factor) - .unwrap() + expected_borrow_rate * utilization_rate * (Decimal::one() - reserve_factor) ); } @@ -124,11 +115,7 @@ mod tests { let new_borrow_rate = model.get_borrow_rate(current_utilization_rate).unwrap(); let expected_borrow_rate = model.base - + math::divide_decimal_by_decimal( - model.slope_1.checked_mul(current_utilization_rate).unwrap(), - model.optimal_utilization_rate, - ) - .unwrap(); + + model.slope_1 * current_utilization_rate / model.optimal_utilization_rate; assert_eq!(new_borrow_rate, expected_borrow_rate); } @@ -139,11 +126,7 @@ mod tests { let new_borrow_rate = model.get_borrow_rate(current_utilization_rate).unwrap(); let expected_borrow_rate = model.base - + math::divide_decimal_by_decimal( - model.slope_1.checked_mul(current_utilization_rate).unwrap(), - model.optimal_utilization_rate, - ) - .unwrap(); + + model.slope_1 * current_utilization_rate / model.optimal_utilization_rate; assert_eq!(new_borrow_rate, expected_borrow_rate); } @@ -155,14 +138,8 @@ mod tests { let expected_borrow_rate = model.base + model.slope_1 - + math::divide_decimal_by_decimal( - model - .slope_2 - .checked_mul(current_utilization_rate - model.optimal_utilization_rate) - .unwrap(), - Decimal::one() - model.optimal_utilization_rate, - ) - .unwrap(); + + model.slope_2 * (current_utilization_rate - model.optimal_utilization_rate) + / (Decimal::one() - model.optimal_utilization_rate); assert_eq!(new_borrow_rate, expected_borrow_rate); } @@ -179,9 +156,7 @@ mod tests { let current_utilization_rate = Decimal::percent(100); let new_borrow_rate = model.get_borrow_rate(current_utilization_rate).unwrap(); - let expected_borrow_rate = Decimal::percent(7); - - assert_eq!(new_borrow_rate, expected_borrow_rate); + assert_eq!(new_borrow_rate, Decimal::percent(7)); } // current utilization rate == 0% and optimal utilization rate == 0% @@ -196,9 +171,7 @@ mod tests { let current_utilization_rate = Decimal::percent(0); let new_borrow_rate = model.get_borrow_rate(current_utilization_rate).unwrap(); - let expected_borrow_rate = Decimal::percent(2); - - assert_eq!(new_borrow_rate, expected_borrow_rate); + assert_eq!(new_borrow_rate, Decimal::percent(2)); } // current utilization rate == 20% and optimal utilization rate == 0% @@ -213,9 +186,8 @@ mod tests { let current_utilization_rate = Decimal::percent(20); let new_borrow_rate = model.get_borrow_rate(current_utilization_rate).unwrap(); - let expected_borrow_rate = model.base - + model.slope_1 - + model.slope_2.checked_mul(current_utilization_rate).unwrap(); + let expected_borrow_rate = + model.base + model.slope_1 + model.slope_2 * current_utilization_rate; assert_eq!(new_borrow_rate, expected_borrow_rate); } diff --git a/packages/utils/src/lib.rs b/packages/utils/src/lib.rs index cc73f475f..17d5321cc 100644 --- a/packages/utils/src/lib.rs +++ b/packages/utils/src/lib.rs @@ -1,3 +1,2 @@ pub mod error; pub mod helpers; -pub mod math; diff --git a/packages/utils/src/math.rs b/packages/utils/src/math.rs deleted file mode 100644 index 19c9d64ad..000000000 --- a/packages/utils/src/math.rs +++ /dev/null @@ -1,261 +0,0 @@ -use std::convert::TryInto; - -use cosmwasm_std::{ - CheckedFromRatioError, Decimal, Fraction, OverflowError, OverflowOperation, StdError, - StdResult, Uint128, Uint256, -}; - -pub fn uint128_checked_div_with_ceil( - numerator: Uint128, - denominator: Uint128, -) -> StdResult { - let mut result = numerator.checked_div(denominator)?; - - if !numerator.checked_rem(denominator)?.is_zero() { - result += Uint128::from(1_u128); - } - - Ok(result) -} - -/// Divide 'a' by 'b'. -pub fn divide_decimal_by_decimal(a: Decimal, b: Decimal) -> StdResult { - Decimal::checked_from_ratio(a.numerator(), b.numerator()).map_err(|e| match e { - CheckedFromRatioError::Overflow => StdError::overflow(OverflowError { - operation: OverflowOperation::Mul, - operand1: a.numerator().to_string(), - operand2: a.denominator().to_string(), - }), - CheckedFromRatioError::DivideByZero => { - StdError::divide_by_zero(cosmwasm_std::DivideByZeroError { - operand: b.to_string(), - }) - } - }) -} - -/// Divide Uint128 by Decimal. -/// (Uint128 / numerator / denominator) is equal to (Uint128 * denominator / numerator). -pub fn divide_uint128_by_decimal(a: Uint128, b: Decimal) -> StdResult { - // (Uint128 / numerator / denominator) is equal to (Uint128 * denominator / numerator). - let numerator_u256 = a.full_mul(b.denominator()); - let denominator_u256 = Uint256::from(b.numerator()); - - let result_u256 = numerator_u256 / denominator_u256; - - let result = result_u256.try_into()?; - Ok(result) -} - -/// Divide Uint128 by Decimal, rounding up to the nearest integer. -pub fn divide_uint128_by_decimal_and_ceil(a: Uint128, b: Decimal) -> StdResult { - // (Uint128 / numerator / denominator) is equal to (Uint128 * denominator / numerator). - let numerator_u256 = a.full_mul(b.denominator()); - let denominator_u256 = Uint256::from(b.numerator()); - - let mut result_u256 = numerator_u256 / denominator_u256; - - if numerator_u256.checked_rem(denominator_u256)? > Uint256::zero() { - result_u256 += Uint256::from(1_u32); - } - - let result = result_u256.try_into()?; - Ok(result) -} - -/// Multiply Uint128 by Decimal, rounding up to the nearest integer. -pub fn multiply_uint128_by_decimal_and_ceil(a: Uint128, b: Decimal) -> StdResult { - let numerator_u256 = a.full_mul(b.numerator()); - let denominator_u256 = Uint256::from(b.denominator()); - - let mut result_u256 = numerator_u256 / denominator_u256; - - if numerator_u256.checked_rem(denominator_u256)? > Uint256::zero() { - result_u256 += Uint256::from(1_u32); - } - - let result = result_u256.try_into()?; - Ok(result) -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use cosmwasm_std::{ConversionOverflowError, OverflowOperation}; - - use super::*; - - const DECIMAL_FRACTIONAL: Uint128 = Uint128::new(1_000_000_000_000_000_000u128); // 1*10**18 - const DECIMAL_FRACTIONAL_SQUARED: Uint128 = - Uint128::new(1_000_000_000_000_000_000_000_000_000_000_000_000u128); // (1*10**18)**2 = 1*10**36 - - #[test] - fn test_uint128_checked_div_with_ceil() { - let a = Uint128::new(120u128); - let b = Uint128::zero(); - uint128_checked_div_with_ceil(a, b).unwrap_err(); - - let a = Uint128::new(120u128); - let b = Uint128::new(60_u128); - let c = uint128_checked_div_with_ceil(a, b).unwrap(); - assert_eq!(c, Uint128::new(2u128)); - - let a = Uint128::new(120u128); - let b = Uint128::new(119_u128); - let c = uint128_checked_div_with_ceil(a, b).unwrap(); - assert_eq!(c, Uint128::new(2u128)); - - let a = Uint128::new(120u128); - let b = Uint128::new(120_u128); - let c = uint128_checked_div_with_ceil(a, b).unwrap(); - assert_eq!(c, Uint128::new(1u128)); - - let a = Uint128::new(120u128); - let b = Uint128::new(121_u128); - let c = uint128_checked_div_with_ceil(a, b).unwrap(); - assert_eq!(c, Uint128::new(1u128)); - - let a = Uint128::zero(); - let b = Uint128::new(121_u128); - let c = uint128_checked_div_with_ceil(a, b).unwrap(); - assert_eq!(c, Uint128::zero()); - } - - #[test] - fn checked_decimal_division() { - let a = Decimal::from_ratio(99988u128, 100u128); - let b = Decimal::from_ratio(24997u128, 100u128); - let c = divide_decimal_by_decimal(a, b).unwrap(); - assert_eq!(c, Decimal::from_str("4.0").unwrap()); - - let a = Decimal::from_ratio(123456789u128, 1000000u128); - let b = Decimal::from_ratio(33u128, 1u128); - let c = divide_decimal_by_decimal(a, b).unwrap(); - assert_eq!(c, Decimal::from_str("3.741114818181818181").unwrap()); - - let a = Decimal::MAX; - let b = Decimal::MAX; - let c = divide_decimal_by_decimal(a, b).unwrap(); - assert_eq!(c, Decimal::one()); - - // Note: DivideByZeroError is not public so we just check if dividing by zero returns error - let a = Decimal::one(); - let b = Decimal::zero(); - divide_decimal_by_decimal(a, b).unwrap_err(); - - let a = Decimal::MAX; - let b = Decimal::from_ratio(1u128, DECIMAL_FRACTIONAL); - let res_error = divide_decimal_by_decimal(a, b).unwrap_err(); - assert_eq!( - res_error, - OverflowError::new(OverflowOperation::Mul, Uint128::MAX, DECIMAL_FRACTIONAL).into() - ); - } - - #[test] - fn test_divide_uint128_by_decimal() { - let a = Uint128::new(120u128); - let b = Decimal::from_ratio(120u128, 15u128); - let c = divide_uint128_by_decimal(a, b).unwrap(); - assert_eq!(c, Uint128::new(15u128)); - - let a = Uint128::new(DECIMAL_FRACTIONAL.u128()); - let b = Decimal::from_ratio(DECIMAL_FRACTIONAL.u128(), 1u128); - let c = divide_uint128_by_decimal(a, b).unwrap(); - assert_eq!(c, Uint128::new(1u128)); - - let a = Uint128::new(DECIMAL_FRACTIONAL.u128()); - let b = Decimal::from_ratio(1u128, DECIMAL_FRACTIONAL.u128()); - let c = divide_uint128_by_decimal(a, b).unwrap(); - assert_eq!(c, Uint128::new(DECIMAL_FRACTIONAL_SQUARED.u128())); - - let a = Uint128::MAX; - let b = Decimal::one(); - let c = divide_uint128_by_decimal(a, b).unwrap(); - assert_eq!(c, Uint128::MAX); - - let a = Uint128::new(1_000_000_000_000_000_000); - let b = Decimal::from_ratio(1u128, DECIMAL_FRACTIONAL); - let c = divide_uint128_by_decimal(a, b).unwrap(); - assert_eq!(c, Uint128::new(1_000_000_000_000_000_000_000_000_000_000_000_000)); - - // Division is truncated - let a = Uint128::new(100); - let b = Decimal::from_ratio(3u128, 1u128); - let c = divide_uint128_by_decimal(a, b).unwrap(); - assert_eq!(c, Uint128::new(33)); - - let a = Uint128::new(75); - let b = Decimal::from_ratio(100u128, 1u128); - let c = divide_uint128_by_decimal(a, b).unwrap(); - assert_eq!(c, Uint128::new(0)); - - // Overflow - let a = Uint128::MAX; - let b = Decimal::from_ratio(1_u128, 10_u128); - let res_error = divide_uint128_by_decimal(a, b).unwrap_err(); - assert_eq!( - res_error, - ConversionOverflowError::new( - "Uint256", - "Uint128", - "3402823669209384634633746074317682114550" - ) - .into() - ); - } - - #[test] - fn test_divide_uint128_by_decimal_and_ceil() { - let a = Uint128::new(120u128); - let b = Decimal::from_ratio(120u128, 15u128); - let c = divide_uint128_by_decimal_and_ceil(a, b).unwrap(); - assert_eq!(c, Uint128::new(15u128)); - - let a = Uint128::new(DECIMAL_FRACTIONAL.u128()); - let b = Decimal::from_ratio(DECIMAL_FRACTIONAL.u128(), 1u128); - let c = divide_uint128_by_decimal_and_ceil(a, b).unwrap(); - assert_eq!(c, Uint128::new(1u128)); - - let a = Uint128::new(DECIMAL_FRACTIONAL.u128()); - let b = Decimal::from_ratio(1u128, DECIMAL_FRACTIONAL.u128()); - let c = divide_uint128_by_decimal_and_ceil(a, b).unwrap(); - assert_eq!(c, Uint128::new(DECIMAL_FRACTIONAL_SQUARED.u128())); - - let a = Uint128::MAX; - let b = Decimal::one(); - let c = divide_uint128_by_decimal_and_ceil(a, b).unwrap(); - assert_eq!(c, Uint128::MAX); - - let a = Uint128::new(1_000_000_000_000_000_000); - let b = Decimal::from_ratio(1u128, DECIMAL_FRACTIONAL); - let c = divide_uint128_by_decimal_and_ceil(a, b).unwrap(); - assert_eq!(c, Uint128::new(1_000_000_000_000_000_000_000_000_000_000_000_000)); - - // Division is rounded up - let a = Uint128::new(100); - let b = Decimal::from_ratio(3u128, 1u128); - let c = divide_uint128_by_decimal_and_ceil(a, b).unwrap(); - assert_eq!(c, Uint128::new(34)); - - let a = Uint128::new(75); - let b = Decimal::from_ratio(100u128, 1u128); - let c = divide_uint128_by_decimal_and_ceil(a, b).unwrap(); - assert_eq!(c, Uint128::new(1)); - - // Overflow - let a = Uint128::MAX; - let b = Decimal::from_ratio(1_u128, 10_u128); - let res_error = divide_uint128_by_decimal_and_ceil(a, b).unwrap_err(); - assert_eq!( - res_error, - ConversionOverflowError::new( - "Uint256", - "Uint128", - "3402823669209384634633746074317682114550" - ) - .into() - ); - } -} From f90c643761b1155cbe00a174c05550dda2c9a58a Mon Sep 17 00:00:00 2001 From: piobab Date: Thu, 3 Aug 2023 21:57:17 +0200 Subject: [PATCH 15/43] New deployment config (#278) * create new deployment * Fix route pool_id serde. * Initialize red-bank markets. * Change uusd -> uosmo * Update params owner. * Set uosmo price at first. * Remove ion. --------- Co-authored-by: brimigs --- contracts/swapper/osmosis/src/route.rs | 23 +++- .../osmosis/tests/test_enumerate_routes.rs | 3 +- .../mars-swapper-osmosis.json | 4 +- scripts/deploy/addresses/osmo-test-5.json | 14 +-- scripts/deploy/base/deployer.ts | 79 ++++++++++-- scripts/deploy/base/index.ts | 5 +- scripts/deploy/base/storage.ts | 7 +- scripts/deploy/neutron/config_mainnet.ts | 42 +++---- scripts/deploy/neutron/config_testnet.ts | 42 +++---- .../deploy/neutron/config_testnet_multisig.ts | 42 +++---- scripts/deploy/osmosis/config.ts | 86 ++++++------- scripts/deploy/osmosis/mainnetConfig.ts | 23 +++- scripts/deploy/osmosis/testnetConfig.ts | 119 +++++++++++++----- scripts/types/config.ts | 7 ++ .../MarsSwapperOsmosis.types.ts | 1 - scripts/types/storageItems.ts | 1 + 16 files changed, 335 insertions(+), 163 deletions(-) diff --git a/contracts/swapper/osmosis/src/route.rs b/contracts/swapper/osmosis/src/route.rs index 698eba5b5..85a12f706 100644 --- a/contracts/swapper/osmosis/src/route.rs +++ b/contracts/swapper/osmosis/src/route.rs @@ -6,7 +6,7 @@ use mars_osmosis::helpers::{query_arithmetic_twap_price, query_pool, CommonPoolD use mars_red_bank_types::swapper::EstimateExactInSwapResponse; use mars_swapper_base::{ContractError, ContractResult, Route}; use osmosis_std::types::osmosis::gamm::v1beta1::MsgSwapExactAmountIn; -pub use osmosis_std::types::osmosis::poolmanager::v1beta1::SwapAmountInRoute; +pub use osmosis_std::types::osmosis::poolmanager::v1beta1::SwapAmountInRoute as OsmosisSwapAmountInRoute; use crate::helpers::hashset; @@ -16,6 +16,17 @@ const TWAP_WINDOW_SIZE_SECONDS: u64 = 600u64; #[cw_serde] pub struct OsmosisRoute(pub Vec); +/// SwapAmountInRoute instead of using `osmosis_std::types::osmosis::poolmanager::v1beta1::SwapAmountInRoute` +/// to keep consistency for pool_id representation as u64. +/// +/// SwapAmountInRoute from osmosis package uses as_str serializer/deserializer, so it expects pool_id +/// as a String, but JSON schema doesn't correctly represent it. +#[cw_serde] +pub struct SwapAmountInRoute { + pub pool_id: u64, + pub token_out_denom: String, +} + impl fmt::Display for OsmosisRoute { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let s = self @@ -118,9 +129,17 @@ impl Route for OsmosisRoute { let out_amount = query_out_amount(querier, &env.block, coin_in, steps)?; let min_out_amount = (Decimal::one() - slippage) * out_amount; + let routes: Vec<_> = steps + .iter() + .map(|step| OsmosisSwapAmountInRoute { + pool_id: step.pool_id, + token_out_denom: step.token_out_denom.clone(), + }) + .collect(); + let swap_msg: CosmosMsg = MsgSwapExactAmountIn { sender: env.contract.address.to_string(), - routes: steps.to_vec(), + routes, token_in: Some(osmosis_std::types::cosmos::base::v1beta1::Coin { denom: coin_in.denom.clone(), amount: coin_in.amount.to_string(), diff --git a/contracts/swapper/osmosis/tests/test_enumerate_routes.rs b/contracts/swapper/osmosis/tests/test_enumerate_routes.rs index 9a6358bc1..c9a679a99 100644 --- a/contracts/swapper/osmosis/tests/test_enumerate_routes.rs +++ b/contracts/swapper/osmosis/tests/test_enumerate_routes.rs @@ -5,8 +5,7 @@ use std::collections::HashMap; use cosmwasm_std::coin; use cw_it::osmosis_test_tube::{Gamm, Module, OsmosisTestApp, SigningAccount, Wasm}; use mars_red_bank_types::swapper::{ExecuteMsg, QueryMsg, RouteResponse}; -use mars_swapper_osmosis::route::OsmosisRoute; -use osmosis_std::types::osmosis::poolmanager::v1beta1::SwapAmountInRoute; +use mars_swapper_osmosis::route::{OsmosisRoute, SwapAmountInRoute}; use crate::helpers::instantiate_contract; diff --git a/schemas/mars-swapper-osmosis/mars-swapper-osmosis.json b/schemas/mars-swapper-osmosis/mars-swapper-osmosis.json index 5fd1ac9ee..512d773a2 100644 --- a/schemas/mars-swapper-osmosis/mars-swapper-osmosis.json +++ b/schemas/mars-swapper-osmosis/mars-swapper-osmosis.json @@ -232,6 +232,7 @@ ] }, "SwapAmountInRoute": { + "description": "SwapAmountInRoute instead of using `osmosis_std::types::osmosis::poolmanager::v1beta1::SwapAmountInRoute` to keep consistency for pool_id representation as u64.\n\nSwapAmountInRoute from osmosis package uses as_str serializer/deserializer, so it expects pool_id as a String, but JSON schema doesn't correctly represent it.", "type": "object", "required": [ "pool_id", @@ -246,7 +247,8 @@ "token_out_denom": { "type": "string" } - } + }, + "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); ```", diff --git a/scripts/deploy/addresses/osmo-test-5.json b/scripts/deploy/addresses/osmo-test-5.json index cf6020bfa..561d4776c 100644 --- a/scripts/deploy/addresses/osmo-test-5.json +++ b/scripts/deploy/addresses/osmo-test-5.json @@ -1,9 +1,9 @@ { - "address-provider": "osmo1xlf93me2979mvgj0gmluzuw22zxwd9rt2jaafzymw7d8kghkhwsqlwgh2r", - "red-bank": "osmo1r9ks824qewvpa9sqgzs3w2ylxx582c6d0hrnzqf3csufchqneydq60hgrv", - "incentives": "osmo1uq2y7h5sw8xtzhluhh7m9l3h9jdv5zjz7jhvavc3sgq0ps8ggq9s39qgcf", - "oracle": "osmo1vae2gsgeqw8q2x5ycvcrspsuqrnydveqgca03v8k9p4uvl2fgdlqp5r8fc", - "rewards-collector": "osmo1uw8g8mxlfk4at27xq26vqzttddtae7fgup4sd59ndrh0ymk99prsezh896", - "swapper": "osmo1q97xatqr3c0zrlgck60yd6f6nqvvtcg5772pngthzrkmdxkc6kqqdj56v2", - "params": "osmo1et0qv7acfv0wv3wlqmjtyflw5dectct24nuwjeqdkfzm9fznfunste0hnc" + "address-provider": "osmo1sm42690a2836cy0ufzaffvsc5e29xagm267ef7jm7acn82f7f4nsh320uv", + "red-bank": "osmo1hs4sm0fah9rk4mz8e56v4n76g0q9fffdkkjm3f8tjagkdx78pqcq75pk0a", + "incentives": "osmo1nu0k6g294jela67vyth6nwr3l42gutq2m07pg9927f7v7tuv0d4sre9fr7", + "oracle": "osmo1dxu93scjdnx42txdp9d4hm3snffvnzmkp4jpc9sml8xlu3ncgamsl2lx58", + "rewards-collector": "osmo1q8gsh9ugl68yx03as3se8w7yegzcpdggy56zxt0mxsjxjvsggyqqedj03f", + "swapper": "osmo1ee9cq8dcknmw43znznx6vuupx5ku0tt505agccgaz5gn48mhe45s3kwwfm", + "params": "osmo1h334tvddn82m4apm08rm9k6kt32ws7vy0c4n30ngrvu6h6yxh8eq9l9jfh" } diff --git a/scripts/deploy/base/deployer.ts b/scripts/deploy/base/deployer.ts index a5c7ea102..75cb7294b 100644 --- a/scripts/deploy/base/deployer.ts +++ b/scripts/deploy/base/deployer.ts @@ -21,9 +21,13 @@ import { InstantiateMsg as ParamsInstantiateMsg } from '../../types/generated/ma import { ExecuteMsg as ParamsExecuteMsg } from '../../types/generated/mars-params/MarsParams.types' import { InstantiateMsg as RedBankInstantiateMsg, + ExecuteMsg as RedBankExecuteMsg, QueryMsg as RedBankQueryMsg, } from '../../types/generated/mars-red-bank/MarsRedBank.types' -import { InstantiateMsg as AddressProviderInstantiateMsg } from '../../types/generated/mars-address-provider/MarsAddressProvider.types' +import { + AddressResponseItem, + InstantiateMsg as AddressProviderInstantiateMsg, +} from '../../types/generated/mars-address-provider/MarsAddressProvider.types' import { InstantiateMsg as IncentivesInstantiateMsg } from '../../types/generated/mars-incentives/MarsIncentives.types' import { InstantiateMsg as RewardsInstantiateMsg } from '../../types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.types' import { @@ -217,6 +221,40 @@ export class Deployer { printYellow(`${assetConfig.symbol} updated.`) } + async initializeMarket(assetConfig: AssetConfig) { + if (this.storage.execute.marketsUpdated.includes(assetConfig.denom)) { + printBlue(`${assetConfig.symbol} already initialized in red-bank contract`) + return + } + printBlue(`Initializing ${assetConfig.symbol}...`) + + const msg: RedBankExecuteMsg = { + init_asset: { + denom: assetConfig.denom, + params: { + reserve_factor: assetConfig.reserve_factor, + interest_rate_model: { + optimal_utilization_rate: assetConfig.interest_rate_model.optimal_utilization_rate, + base: assetConfig.interest_rate_model.base, + slope_1: assetConfig.interest_rate_model.slope_1, + slope_2: assetConfig.interest_rate_model.slope_2, + }, + }, + }, + } + + await this.client.execute( + this.deployerAddress, + this.storage.addresses['red-bank']!, + msg, + 'auto', + ) + + printYellow(`${assetConfig.symbol} initialized`) + + this.storage.execute.marketsUpdated.push(assetConfig.denom) + } + async updateVaultConfig(vaultConfig: VaultConfig) { if (this.storage.execute.vaultsUpdated.includes(vaultConfig.addr)) { printBlue(`${vaultConfig.symbol} already updated in Params contract`) @@ -273,38 +311,42 @@ export class Deployer { async updateAddressProvider() { printBlue('Updating addresses in Address Provider...') - const addressesToSet = [ + const addressesToSet: AddressResponseItem[] = [ { + address: this.storage.addresses['rewards-collector']!, address_type: 'rewards_collector', - address: this.storage.addresses['rewards-collector'], }, { + address: this.storage.addresses.incentives!, address_type: 'incentives', - address: this.storage.addresses.incentives, }, { + address: this.storage.addresses.oracle!, address_type: 'oracle', - address: this.storage.addresses.oracle, }, { + address: this.storage.addresses['red-bank']!, address_type: 'red_bank', - address: this.storage.addresses['red-bank'], }, { - address_type: 'fee_collector', address: this.config.feeCollectorAddr, + address_type: 'fee_collector', }, { - address_type: 'safety_fund', address: this.config.safetyFundAddr, + address_type: 'safety_fund', }, { - address_type: 'protocol_admin', address: this.config.protocolAdminAddr, + address_type: 'protocol_admin', }, { + address: this.storage.addresses.swapper!, address_type: 'swapper', - address: this.storage.addresses.swapper, + }, + { + address: this.storage.addresses.params!, + address_type: 'params', }, ] @@ -635,6 +677,23 @@ export class Deployer { assert.equal(swapperConfig.proposed, this.config.multisigAddr) } + async updateParamsContractOwner() { + const msg = { + update_owner: { + propose_new_owner: { + proposed: this.storage.owner, + }, + }, + } + await this.client.execute(this.deployerAddress, this.storage.addresses.params!, msg, 'auto') + printYellow('Owner updated to Mutlisig for Params') + const paramsConfig = (await this.client.queryContractSmart(this.storage.addresses.params!, { + owner: {}, + })) as { proposed: string } + + assert.equal(paramsConfig.proposed, this.config.multisigAddr) + } + async updateAddressProviderContractOwner() { const msg = { update_owner: { diff --git a/scripts/deploy/base/index.ts b/scripts/deploy/base/index.ts index fc3fa13ba..5dedb8547 100644 --- a/scripts/deploy/base/index.ts +++ b/scripts/deploy/base/index.ts @@ -19,6 +19,7 @@ export const taskRunner = async (config: DeploymentConfig) => { `mars_rewards_collector_${config.rewardsCollectorName}.wasm`, ) await deployer.upload('swapper', `mars_swapper_${config.swapperDexName}.wasm`) + await deployer.upload('params', `mars_params.wasm`) // Instantiate contracts deployer.setOwnerAddr() @@ -32,9 +33,10 @@ export const taskRunner = async (config: DeploymentConfig) => { await deployer.saveDeploymentAddrsToFile() // setup - await deployer.updateAddressProvider() + await deployer.updateAddressProvider() // CreditManager address in address-provider should be set once known for (const asset of config.assets) { await deployer.updateAssetParams(asset) + await deployer.initializeMarket(asset) } for (const vault of config.vaults) { await deployer.updateVaultConfig(vault) @@ -59,6 +61,7 @@ export const taskRunner = async (config: DeploymentConfig) => { await deployer.updateOracleContractOwner() await deployer.updateRewardsContractOwner() await deployer.updateSwapperContractOwner() + await deployer.updateParamsContractOwner() await deployer.updateAddressProviderContractOwner() printGreen('It is confirmed that all contracts have transferred ownership to the Multisig') } else { diff --git a/scripts/deploy/base/storage.ts b/scripts/deploy/base/storage.ts index 6f429224e..768ae3ee0 100644 --- a/scripts/deploy/base/storage.ts +++ b/scripts/deploy/base/storage.ts @@ -28,7 +28,12 @@ export class Storage implements StorageItems { return new this(chainId, { addresses: {}, codeIds: {}, - execute: { assetsUpdated: [], vaultsUpdated: [], addressProviderUpdated: {} }, + execute: { + assetsUpdated: [], + marketsUpdated: [], + vaultsUpdated: [], + addressProviderUpdated: {}, + }, }) } } diff --git a/scripts/deploy/neutron/config_mainnet.ts b/scripts/deploy/neutron/config_mainnet.ts index 2189aaf6b..f7ba6a1e3 100644 --- a/scripts/deploy/neutron/config_mainnet.ts +++ b/scripts/deploy/neutron/config_mainnet.ts @@ -277,13 +277,6 @@ export const ntrnAsset: AssetConfig = { }, protocol_liquidation_fee: '0.5', // liquidation_bonus: '0.15', - // reserve_factor: '0.1', - // interest_rate_model: { - // optimal_utilization_rate: '0.6', - // base: '0', - // slope_1: '0.15', - // slope_2: '3', - // }, symbol: 'NTRN', credit_manager: { whitelisted: false, @@ -293,6 +286,13 @@ export const ntrnAsset: AssetConfig = { deposit_enabled: true, }, deposit_cap: '5000000000000', + reserve_factor: '0.1', + interest_rate_model: { + optimal_utilization_rate: '0.6', + base: '0', + slope_1: '0.15', + slope_2: '3', + }, } export const atomAsset: AssetConfig = { @@ -307,13 +307,6 @@ export const atomAsset: AssetConfig = { }, protocol_liquidation_fee: '0.5', // liquidation_bonus: '0.1', - // reserve_factor: '0.1', - // interest_rate_model: { - // optimal_utilization_rate: '0.7', - // base: '0', - // slope_1: '0.2', - // slope_2: '3', - // }, symbol: 'ATOM', credit_manager: { whitelisted: false, @@ -323,6 +316,13 @@ export const atomAsset: AssetConfig = { deposit_enabled: true, }, deposit_cap: '150000000000', + reserve_factor: '0.1', + interest_rate_model: { + optimal_utilization_rate: '0.7', + base: '0', + slope_1: '0.2', + slope_2: '3', + }, } export const axlUSDCAsset: AssetConfig = { @@ -337,13 +337,6 @@ export const axlUSDCAsset: AssetConfig = { }, protocol_liquidation_fee: '0.5', // liquidation_bonus: '0.1', - // reserve_factor: '0.1', - // interest_rate_model: { - // optimal_utilization_rate: '0.8', - // base: '0', - // slope_1: '0.125', - // slope_2: '2', - // }, symbol: 'axlUSDC', credit_manager: { whitelisted: false, @@ -353,6 +346,13 @@ export const axlUSDCAsset: AssetConfig = { deposit_enabled: true, }, deposit_cap: '500000000000', + reserve_factor: '0.1', + interest_rate_model: { + optimal_utilization_rate: '0.8', + base: '0', + slope_1: '0.125', + slope_2: '2', + }, } export const neutronMainnetConfig: DeploymentConfig = { diff --git a/scripts/deploy/neutron/config_testnet.ts b/scripts/deploy/neutron/config_testnet.ts index e15dcd429..612b31caa 100644 --- a/scripts/deploy/neutron/config_testnet.ts +++ b/scripts/deploy/neutron/config_testnet.ts @@ -236,13 +236,6 @@ export const ntrnAsset: AssetConfig = { }, protocol_liquidation_fee: '0.5', // liquidation_bonus: '0.15', - // reserve_factor: '0.1', - // interest_rate_model: { - // optimal_utilization_rate: '0.6', - // base: '0', - // slope_1: '0.15', - // slope_2: '3', - // }, symbol: 'NTRN', credit_manager: { whitelisted: false, @@ -252,6 +245,13 @@ export const ntrnAsset: AssetConfig = { deposit_enabled: true, }, deposit_cap: '5000000000000', + reserve_factor: '0.1', + interest_rate_model: { + optimal_utilization_rate: '0.6', + base: '0', + slope_1: '0.15', + slope_2: '3', + }, } export const atomAsset: AssetConfig = { @@ -266,13 +266,6 @@ export const atomAsset: AssetConfig = { }, protocol_liquidation_fee: '0.5', // liquidation_bonus: '0.1', - // reserve_factor: '0.1', - // interest_rate_model: { - // optimal_utilization_rate: '0.7', - // base: '0', - // slope_1: '0.2', - // slope_2: '3', - // }, symbol: 'ATOM', credit_manager: { whitelisted: false, @@ -282,6 +275,13 @@ export const atomAsset: AssetConfig = { deposit_enabled: true, }, deposit_cap: '150000000000', + reserve_factor: '0.1', + interest_rate_model: { + optimal_utilization_rate: '0.7', + base: '0', + slope_1: '0.2', + slope_2: '3', + }, } export const axlUSDCAsset: AssetConfig = { @@ -296,13 +296,6 @@ export const axlUSDCAsset: AssetConfig = { }, protocol_liquidation_fee: '0.5', // liquidation_bonus: '0.1', - // reserve_factor: '0.1', - // interest_rate_model: { - // optimal_utilization_rate: '0.8', - // base: '0', - // slope_1: '0.125', - // slope_2: '2', - // }, symbol: 'axlUSDC', credit_manager: { whitelisted: false, @@ -312,6 +305,13 @@ export const axlUSDCAsset: AssetConfig = { deposit_enabled: true, }, deposit_cap: '500000000000', + reserve_factor: '0.1', + interest_rate_model: { + optimal_utilization_rate: '0.8', + base: '0', + slope_1: '0.125', + slope_2: '2', + }, } export const neutronTestnetConfig: DeploymentConfig = { diff --git a/scripts/deploy/neutron/config_testnet_multisig.ts b/scripts/deploy/neutron/config_testnet_multisig.ts index 15a0a2964..552e75eb0 100644 --- a/scripts/deploy/neutron/config_testnet_multisig.ts +++ b/scripts/deploy/neutron/config_testnet_multisig.ts @@ -273,13 +273,6 @@ export const ntrnAsset: AssetConfig = { }, protocol_liquidation_fee: '0.5', // liquidation_bonus: '0.15', - // reserve_factor: '0.1', - // interest_rate_model: { - // optimal_utilization_rate: '0.6', - // base: '0', - // slope_1: '0.15', - // slope_2: '3', - // }, symbol: 'NTRN', credit_manager: { whitelisted: false, @@ -289,6 +282,13 @@ export const ntrnAsset: AssetConfig = { deposit_enabled: true, }, deposit_cap: '5000000000000', + reserve_factor: '0.1', + interest_rate_model: { + optimal_utilization_rate: '0.6', + base: '0', + slope_1: '0.15', + slope_2: '3', + }, } export const atomAsset: AssetConfig = { @@ -303,13 +303,6 @@ export const atomAsset: AssetConfig = { }, protocol_liquidation_fee: '0.5', // liquidation_bonus: '0.1', - // reserve_factor: '0.1', - // interest_rate_model: { - // optimal_utilization_rate: '0.7', - // base: '0', - // slope_1: '0.2', - // slope_2: '3', - // }, symbol: 'ATOM', credit_manager: { whitelisted: false, @@ -319,6 +312,13 @@ export const atomAsset: AssetConfig = { deposit_enabled: true, }, deposit_cap: '150000000000', + reserve_factor: '0.1', + interest_rate_model: { + optimal_utilization_rate: '0.7', + base: '0', + slope_1: '0.2', + slope_2: '3', + }, } export const axlUSDCAsset: AssetConfig = { @@ -333,13 +333,6 @@ export const axlUSDCAsset: AssetConfig = { }, protocol_liquidation_fee: '0.5', // liquidation_bonus: '0.1', - // reserve_factor: '0.1', - // interest_rate_model: { - // optimal_utilization_rate: '0.8', - // base: '0', - // slope_1: '0.125', - // slope_2: '2', - // }, symbol: 'axlUSDC', credit_manager: { whitelisted: false, @@ -349,6 +342,13 @@ export const axlUSDCAsset: AssetConfig = { deposit_enabled: true, }, deposit_cap: '500000000000', + reserve_factor: '0.1', + interest_rate_model: { + optimal_utilization_rate: '0.8', + base: '0', + slope_1: '0.125', + slope_2: '2', + }, } export const neutronTetstnetMultisigConfig: DeploymentConfig = { diff --git a/scripts/deploy/osmosis/config.ts b/scripts/deploy/osmosis/config.ts index 9dc457a75..3a59e3047 100644 --- a/scripts/deploy/osmosis/config.ts +++ b/scripts/deploy/osmosis/config.ts @@ -34,13 +34,6 @@ export const osmoAsset: AssetConfig = { starting_lb: '0', }, protocol_liquidation_fee: '0.5', - // reserve_factor: '0.2', - // interest_rate_model: { - // optimal_utilization_rate: '0.6', - // base: '0', - // slope_1: '0.15', - // slope_2: '3', - // }, symbol: 'OSMO', credit_manager: { whitelisted: true, @@ -50,6 +43,13 @@ export const osmoAsset: AssetConfig = { deposit_enabled: true, }, deposit_cap: '2500000000000', + reserve_factor: '0.2', + interest_rate_model: { + optimal_utilization_rate: '0.6', + base: '0', + slope_1: '0.15', + slope_2: '3', + }, } export const atomAsset: AssetConfig = { @@ -63,13 +63,6 @@ export const atomAsset: AssetConfig = { starting_lb: '0', }, protocol_liquidation_fee: '0.5', - // reserve_factor: '0.2', - // interest_rate_model: { - // optimal_utilization_rate: '0.6', - // base: '0', - // slope_1: '0.15', - // slope_2: '3', - // }, symbol: 'ATOM', credit_manager: { whitelisted: true, @@ -79,6 +72,13 @@ export const atomAsset: AssetConfig = { deposit_enabled: true, }, deposit_cap: '100000000000', + reserve_factor: '0.2', + interest_rate_model: { + optimal_utilization_rate: '0.6', + base: '0', + slope_1: '0.15', + slope_2: '3', + }, } export const atomAssetTest: AssetConfig = { @@ -92,13 +92,6 @@ export const atomAssetTest: AssetConfig = { starting_lb: '0', }, protocol_liquidation_fee: '0.5', - // reserve_factor: '0.2', - // interest_rate_model: { - // optimal_utilization_rate: '0.6', - // base: '0', - // slope_1: '0.15', - // slope_2: '3', - // }, symbol: 'ATOM', credit_manager: { whitelisted: true, @@ -108,6 +101,13 @@ export const atomAssetTest: AssetConfig = { deposit_enabled: true, }, deposit_cap: '100000000000', + reserve_factor: '0.2', + interest_rate_model: { + optimal_utilization_rate: '0.6', + base: '0', + slope_1: '0.15', + slope_2: '3', + }, } export const axlUSDCAsset: AssetConfig = { @@ -121,13 +121,6 @@ export const axlUSDCAsset: AssetConfig = { starting_lb: '0', }, protocol_liquidation_fee: '0.5', - // reserve_factor: '0.2', - // interest_rate_model: { - // optimal_utilization_rate: '0.8', - // base: '0', - // slope_1: '0.2', - // slope_2: '2', - // }, symbol: 'axlUSDC', credit_manager: { whitelisted: true, @@ -137,6 +130,13 @@ export const axlUSDCAsset: AssetConfig = { deposit_enabled: true, }, deposit_cap: '500000000000', + reserve_factor: '0.2', + interest_rate_model: { + optimal_utilization_rate: '0.8', + base: '0', + slope_1: '0.2', + slope_2: '2', + }, } export const axlUSDCAssetTest: AssetConfig = { @@ -150,13 +150,6 @@ export const axlUSDCAssetTest: AssetConfig = { starting_lb: '0', }, protocol_liquidation_fee: '0.5', - // reserve_factor: '0.2', - // interest_rate_model: { - // optimal_utilization_rate: '0.8', - // base: '0', - // slope_1: '0.2', - // slope_2: '2', - // }, symbol: 'axlUSDC', credit_manager: { whitelisted: true, @@ -166,6 +159,13 @@ export const axlUSDCAssetTest: AssetConfig = { deposit_enabled: true, }, deposit_cap: '500000000000', + reserve_factor: '0.2', + interest_rate_model: { + optimal_utilization_rate: '0.8', + base: '0', + slope_1: '0.2', + slope_2: '2', + }, } export const marsAssetTest: AssetConfig = { @@ -179,13 +179,6 @@ export const marsAssetTest: AssetConfig = { starting_lb: '0', }, protocol_liquidation_fee: '0.5', - // reserve_factor: '0.2', - // interest_rate_model: { - // optimal_utilization_rate: '0.8', - // base: '0', - // slope_1: '0.2', - // slope_2: '2', - // }, symbol: 'mars', credit_manager: { whitelisted: true, @@ -195,6 +188,13 @@ export const marsAssetTest: AssetConfig = { deposit_enabled: true, }, deposit_cap: '500000000000', + reserve_factor: '0.2', + interest_rate_model: { + optimal_utilization_rate: '0.8', + base: '0', + slope_1: '0.2', + slope_2: '2', + }, } // export const osmoOracle: OracleConfig = { @@ -428,7 +428,7 @@ export const osmosisLocalConfig: DeploymentConfig = { assets: [osmoAsset, atomAsset, axlUSDCAsset], vaults: [], oracleConfigs: [atomOracle, axlUSDCOracle], - incentiveEpochDuration: 86400, + incentiveEpochDuration: 604800, // 1 week maxWhitelistedIncentiveDenoms: 10, targetHealthFactor: '1.2', } diff --git a/scripts/deploy/osmosis/mainnetConfig.ts b/scripts/deploy/osmosis/mainnetConfig.ts index fecc8d4c2..8085df2cd 100644 --- a/scripts/deploy/osmosis/mainnetConfig.ts +++ b/scripts/deploy/osmosis/mainnetConfig.ts @@ -33,6 +33,13 @@ export const osmoAsset: AssetConfig = { deposit_enabled: true, }, deposit_cap: '2500000000000', + reserve_factor: '0.2', + interest_rate_model: { + optimal_utilization_rate: '0.8', + base: '0', + slope_1: '0.2', + slope_2: '2', + }, } export const atomAsset: AssetConfig = { @@ -55,6 +62,13 @@ export const atomAsset: AssetConfig = { deposit_enabled: true, }, deposit_cap: '100000000000', + reserve_factor: '0.2', + interest_rate_model: { + optimal_utilization_rate: '0.8', + base: '0', + slope_1: '0.2', + slope_2: '2', + }, } export const axlUSDCAsset: AssetConfig = { @@ -77,6 +91,13 @@ export const axlUSDCAsset: AssetConfig = { deposit_enabled: true, }, deposit_cap: '500000000000', + reserve_factor: '0.2', + interest_rate_model: { + optimal_utilization_rate: '0.8', + base: '0', + slope_1: '0.2', + slope_2: '2', + }, } export const atomOracle: OracleConfig = { @@ -165,6 +186,6 @@ export const osmosisMainnet: DeploymentConfig = { vaults: [], oracleConfigs: [atomOracle, axlUSDCOracle], targetHealthFactor: '1.2', - incentiveEpochDuration: 86400, + incentiveEpochDuration: 604800, // 1 week maxWhitelistedIncentiveDenoms: 10, } diff --git a/scripts/deploy/osmosis/testnetConfig.ts b/scripts/deploy/osmosis/testnetConfig.ts index 947a1fb11..35e554703 100644 --- a/scripts/deploy/osmosis/testnetConfig.ts +++ b/scripts/deploy/osmosis/testnetConfig.ts @@ -1,12 +1,13 @@ import { DeploymentConfig, AssetConfig, OracleConfig, VaultConfig } from '../../types/config' // assets based off of OSMO-TEST-5: https://docs.osmosis.zone/osmosis-core/asset-info/ -const osmo = 'uosmo' +const uosmo = 'uosmo' +const aUSDC = 'ibc/6F34E1BD664C36CE49ACC28E60D62559A5F96C4F9A6CCE4FC5A67B2852E24CFE' // axelar USDC const atom = 'ibc/A8C2D23A1E6F95DA4E48BA349667E322BD7A6C996D8A4AAE8BA72E190F3D1477' -const nUSDC = 'ibc/40F1B2458AEDA66431F9D44F48413240B8D28C072463E2BF53655728683583E3' // noble const mars = 'ibc/2E7368A14AC9AB7870F32CFEA687551C5064FA861868EDF7437BC877358A81F9' +const usdcOsmo = 'gamm/pool/5' +const atomOsmo = 'gamm/pool/12' -const pythContractAddr = 'UPDATE' const protocolAdminAddr = 'osmo14w4x949nwcrqgfe53pxs3k7x53p0gvlrq34l5n' // note the following addresses are all 'mars' bech32 prefix @@ -18,7 +19,7 @@ export const osmoAsset: AssetConfig = { whitelisted: true, }, symbol: 'OSMO', - denom: osmo, + denom: uosmo, liquidation_bonus: { max_lb: '0.05', min_lb: '0', @@ -33,6 +34,13 @@ export const osmoAsset: AssetConfig = { deposit_enabled: true, }, deposit_cap: '2500000000000', + reserve_factor: '0.2', + interest_rate_model: { + optimal_utilization_rate: '0.8', + base: '0', + slope_1: '0.2', + slope_2: '2', + }, } export const atomAsset: AssetConfig = { @@ -55,14 +63,21 @@ export const atomAsset: AssetConfig = { deposit_enabled: true, }, deposit_cap: '100000000000', + reserve_factor: '0.2', + interest_rate_model: { + optimal_utilization_rate: '0.8', + base: '0', + slope_1: '0.2', + slope_2: '2', + }, } export const USDCAsset: AssetConfig = { credit_manager: { whitelisted: true, }, - symbol: 'nUSDC', - denom: nUSDC, + symbol: 'aUSDC', + denom: aUSDC, liquidation_bonus: { max_lb: '0.05', min_lb: '0', @@ -77,13 +92,20 @@ export const USDCAsset: AssetConfig = { deposit_enabled: true, }, deposit_cap: '500000000000', + reserve_factor: '0.2', + interest_rate_model: { + optimal_utilization_rate: '0.8', + base: '0', + slope_1: '0.2', + slope_2: '2', + }, } export const usdcOsmoVault: VaultConfig = { - addr: 'osmo1fmq9hw224fgz8lk48wyd0gfg028kvvzggt6c3zvnaqkw23x68cws5nd5em', + addr: 'osmo1l3q4mrhkzjyernjhg8lz2t52ddw589y5qc0z7y8y28h6y5wcl46sg9n28j', symbol: 'usdcOsmoVault', deposit_cap: { - denom: nUSDC, + denom: aUSDC, amount: '1000000000', }, liquidation_threshold: '0.65', @@ -91,38 +113,73 @@ export const usdcOsmoVault: VaultConfig = { whitelisted: true, } +export const atomOsmoVault: VaultConfig = { + addr: 'osmo1m45ap4rq4m2mfjkcqu9ks9mxmyx2hvx0cdca9sjmrg46q7lghzqqhxxup5', + symbol: 'atomOsmoVault', + deposit_cap: { + denom: aUSDC, + amount: '1000000000', + }, + liquidation_threshold: '0.65', + max_loan_to_value: '0.63', + whitelisted: true, +} + +export const osmoOracle: OracleConfig = { + denom: uosmo, + price_source: { + fixed: { + price: '1', + }, + }, +} + export const atomOracle: OracleConfig = { denom: atom, price_source: { - pyth: { - contract_addr: pythContractAddr, - price_feed_id: 'UPDATE', - max_staleness: 60, - denom_decimals: 6, - max_confidence: '5', - max_deviation: '4', + geometric_twap: { + downtime_detector: { downtime: 'duration30m', recovery: 7200 }, + window_size: 1800, + pool_id: 12, }, }, } - export const USDCOracle: OracleConfig = { - denom: nUSDC, + denom: aUSDC, price_source: { staked_geometric_twap: { - transitive_denom: osmo, - pool_id: 6, + transitive_denom: uosmo, + pool_id: 5, window_size: 1800, downtime_detector: { downtime: 'duration30m', recovery: 7200 }, }, }, } +export const usdcOsmoOracle: OracleConfig = { + denom: usdcOsmo, + price_source: { + xyk_liquidity_token: { + pool_id: 5, + }, + }, +} + +export const atomOsmoOracle: OracleConfig = { + denom: atomOsmo, + price_source: { + xyk_liquidity_token: { + pool_id: 12, + }, + }, +} + export const osmosisTestnetConfig: DeploymentConfig = { oracleName: 'osmosis', - oracleBaseDenom: 'uusd', + oracleBaseDenom: 'uosmo', rewardsCollectorName: 'osmosis', atomDenom: atom, - baseAssetDenom: osmo, + baseAssetDenom: uosmo, gasPrice: '0.1uosmo', chainId: 'osmo-test-5', chainPrefix: 'osmo', @@ -138,19 +195,19 @@ export const osmosisTestnetConfig: DeploymentConfig = { runTests: false, mainnet: false, feeCollectorDenom: mars, - safetyFundDenom: nUSDC, + safetyFundDenom: aUSDC, swapRoutes: [ - { denom_in: atom, denom_out: osmo, route: [{ pool_id: 12, token_out_denom: osmo }] }, + { denom_in: atom, denom_out: uosmo, route: [{ pool_id: 12, token_out_denom: uosmo }] }, ], safetyFundAddr: safetyFundAddr, protocolAdminAddr: protocolAdminAddr, feeCollectorAddr: feeCollectorAddr, swapperDexName: 'osmosis', assets: [osmoAsset, atomAsset, USDCAsset], - vaults: [usdcOsmoVault], - oracleConfigs: [atomOracle, USDCOracle], + vaults: [usdcOsmoVault, atomOsmoVault], + oracleConfigs: [osmoOracle, atomOracle, USDCOracle, atomOsmoOracle, usdcOsmoOracle], targetHealthFactor: '1.2', - incentiveEpochDuration: 86400, + incentiveEpochDuration: 604800, // 1 week maxWhitelistedIncentiveDenoms: 10, } @@ -176,18 +233,18 @@ export const osmosisTestMultisig: DeploymentConfig = { runTests: false, mainnet: false, feeCollectorDenom: mars, - safetyFundDenom: nUSDC, + safetyFundDenom: aUSDC, swapRoutes: [ - { denom_in: atom, denom_out: 'uosmo', route: [{ pool_id: 1, token_out_denom: 'uosmo' }] }, + { denom_in: atom, denom_out: 'uosmo', route: [{ pool_id: 12, token_out_denom: 'uosmo' }] }, ], safetyFundAddr: safetyFundAddr, protocolAdminAddr: protocolAdminAddr, feeCollectorAddr: feeCollectorAddr, swapperDexName: 'osmosis', assets: [osmoAsset, atomAsset, USDCAsset], - vaults: [usdcOsmoVault], - oracleConfigs: [atomOracle, USDCOracle], + vaults: [usdcOsmoVault, atomOsmoVault], + oracleConfigs: [osmoOracle, atomOracle, USDCOracle, atomOsmoOracle, usdcOsmoOracle], targetHealthFactor: '1.2', - incentiveEpochDuration: 86400, + incentiveEpochDuration: 604800, // 1 week maxWhitelistedIncentiveDenoms: 10, } diff --git a/scripts/types/config.ts b/scripts/types/config.ts index 5243ab649..413242d33 100644 --- a/scripts/types/config.ts +++ b/scripts/types/config.ts @@ -81,6 +81,13 @@ export interface AssetConfig { protocol_liquidation_fee: Decimal red_bank: RedBankSettings deposit_cap: Uint128 + reserve_factor: string + interest_rate_model: { + optimal_utilization_rate: string + base: string + slope_1: string + slope_2: string + } } export interface VaultConfig { addr: string diff --git a/scripts/types/generated/mars-swapper-osmosis/MarsSwapperOsmosis.types.ts b/scripts/types/generated/mars-swapper-osmosis/MarsSwapperOsmosis.types.ts index 0046c3fb7..0df6ac650 100644 --- a/scripts/types/generated/mars-swapper-osmosis/MarsSwapperOsmosis.types.ts +++ b/scripts/types/generated/mars-swapper-osmosis/MarsSwapperOsmosis.types.ts @@ -55,7 +55,6 @@ export type Addr = string export interface SwapAmountInRoute { pool_id: number token_out_denom: string - [k: string]: unknown } export interface Coin { amount: Uint128 diff --git a/scripts/types/storageItems.ts b/scripts/types/storageItems.ts index f5a134753..2ec983b9d 100644 --- a/scripts/types/storageItems.ts +++ b/scripts/types/storageItems.ts @@ -21,6 +21,7 @@ export interface StorageItems { execute: { addressProviderUpdated: Record assetsUpdated: string[] + marketsUpdated: string[] vaultsUpdated: string[] oraclePriceSet?: boolean smokeTest?: boolean From 156a1399300d586b0f243a8111429263066d2dd3 Mon Sep 17 00:00:00 2001 From: brimigs <85972460+brimigs@users.noreply.github.com> Date: Thu, 3 Aug 2023 22:17:32 +0100 Subject: [PATCH 16/43] update price source for liquidation to action kind (#275) * update price source * Fix clippy. Update schema. add mock pyth contract * Cleanup. * Additional tests to cover Liquidation pricing. --------- Co-authored-by: Piotr Babel --- Cargo.lock | 12 ++ Cargo.toml | 3 + contracts/mock-pyth/Cargo.toml | 25 ++++ contracts/mock-pyth/src/contract.rs | 51 +++++++++ contracts/mock-pyth/src/lib.rs | 1 + .../tests/test_query_price_for_pyth.rs | 46 +++++++- contracts/red-bank/Cargo.toml | 1 + contracts/red-bank/src/collateral.rs | 1 + contracts/red-bank/src/contract.rs | 8 +- contracts/red-bank/src/health.rs | 18 ++- contracts/red-bank/src/liquidate.rs | 10 +- contracts/red-bank/src/query.rs | 11 +- contracts/red-bank/tests/test_liquidate.rs | 31 +++++ contracts/red-bank/tests/test_misc.rs | 1 + packages/health/src/query.rs | 14 +++ packages/testing/Cargo.toml | 2 + .../testing/src/integration/mock_contracts.rs | 9 ++ packages/testing/src/integration/mock_env.rs | 69 ++++++++++- packages/types/src/oracle/msg.rs | 16 +++ packages/types/src/red_bank/msg.rs | 6 + schemas/mars-red-bank/mars-red-bank.json | 107 ++++++++++++++++++ .../mars-red-bank/MarsRedBank.client.ts | 13 +++ .../mars-red-bank/MarsRedBank.react-query.ts | 33 ++++++ .../mars-red-bank/MarsRedBank.types.ts | 5 + 24 files changed, 477 insertions(+), 16 deletions(-) create mode 100644 contracts/mock-pyth/Cargo.toml create mode 100644 contracts/mock-pyth/src/contract.rs create mode 100644 contracts/mock-pyth/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 288a878e0..b938e3260 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1809,6 +1809,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "mars-mock-pyth" +version = "1.2.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "pyth-sdk-cw", +] + [[package]] name = "mars-oracle-base" version = "1.2.0" @@ -1926,6 +1935,7 @@ dependencies = [ "mars-red-bank-types", "mars-testing", "mars-utils", + "pyth-sdk-cw", "test-case", "thiserror", ] @@ -2065,11 +2075,13 @@ version = "1.2.0" dependencies = [ "anyhow", "astroport", + "cosmwasm-schema", "cosmwasm-std", "cw-it", "cw-multi-test", "mars-address-provider", "mars-incentives", + "mars-mock-pyth", "mars-oracle-osmosis", "mars-oracle-wasm", "mars-osmosis", diff --git a/Cargo.toml b/Cargo.toml index e69bc3b5d..41b21ba77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "contracts/address-provider", "contracts/incentives", + "contracts/mock-pyth", "contracts/oracle/*", "contracts/params", "contracts/swapper/*", @@ -59,6 +60,7 @@ cw-paginate = "0.2.1" astroport = "2.8.0" strum = "0.24.1" neutron-sdk = "0.6.0" +serde_json = "1.0" # dev-dependencies cw-multi-test = "0.16.5" @@ -79,6 +81,7 @@ mars-utils = { path = "./packages/utils" } # contracts mars-address-provider = { path = "./contracts/address-provider" } mars-incentives = { path = "./contracts/incentives" } +mars-mock-pyth = { path = "./contracts/mock-pyth" } mars-oracle-base = { path = "./contracts/oracle/base" } mars-oracle-osmosis = { path = "./contracts/oracle/osmosis" } mars-oracle-wasm = { path = "./contracts/oracle/wasm" } diff --git a/contracts/mock-pyth/Cargo.toml b/contracts/mock-pyth/Cargo.toml new file mode 100644 index 000000000..85b62884f --- /dev/null +++ b/contracts/mock-pyth/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "mars-mock-pyth" +description = "Mocked version of the Pyth oracle contract" +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 } +pyth-sdk-cw = { workspace = true } diff --git a/contracts/mock-pyth/src/contract.rs b/contracts/mock-pyth/src/contract.rs new file mode 100644 index 000000000..2817dbc60 --- /dev/null +++ b/contracts/mock-pyth/src/contract.rs @@ -0,0 +1,51 @@ +use cosmwasm_std::{ + entry_point, to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult, +}; +use pyth_sdk_cw::{Price, PriceFeed, PriceFeedResponse, PriceIdentifier, QueryMsg}; + +#[entry_point] +pub fn instantiate( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: Empty, +) -> StdResult { + Ok(Response::default()) +} + +#[entry_point] +pub fn execute(_deps: DepsMut, _env: Env, _info: MessageInfo, _msg: Empty) -> StdResult { + Ok(Response::default()) +} + +#[entry_point] +pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::PriceFeed { + id, + } => to_binary(&mocked_price_feed(deps, id)?), + _ => panic!("Unsupported query!"), + } +} + +fn mocked_price_feed(_deps: Deps, id: PriceIdentifier) -> StdResult { + let price_feed_response = PriceFeedResponse { + price_feed: PriceFeed::new( + id, + Price { + price: 680000, + conf: 510000, + expo: -5, + publish_time: 1571797419, + }, + Price { + price: 681000, + conf: 400000, + expo: -5, + publish_time: 1571797419, + }, + ), + }; + + Ok(price_feed_response) +} diff --git a/contracts/mock-pyth/src/lib.rs b/contracts/mock-pyth/src/lib.rs new file mode 100644 index 000000000..2943dbb50 --- /dev/null +++ b/contracts/mock-pyth/src/lib.rs @@ -0,0 +1 @@ +pub mod contract; diff --git a/contracts/oracle/osmosis/tests/test_query_price_for_pyth.rs b/contracts/oracle/osmosis/tests/test_query_price_for_pyth.rs index 741fcb9e3..3effd960e 100644 --- a/contracts/oracle/osmosis/tests/test_query_price_for_pyth.rs +++ b/contracts/oracle/osmosis/tests/test_query_price_for_pyth.rs @@ -303,7 +303,7 @@ fn querying_liquidation_pyth_price_if_signed() { } #[test] -fn querying_default_pyth_price_if_confidence_exceeded() { +fn querying_pyth_price_if_confidence_exceeded() { let mut deps = helpers::setup_test_for_pyth(); let price_id = PriceIdentifier::from_hex( @@ -347,6 +347,7 @@ fn querying_default_pyth_price_if_confidence_exceeded() { }, ); + // should fail for Default pricing let res_err = entry::query( deps.as_ref(), mock_env_at_block_time(publish_time), @@ -362,10 +363,23 @@ fn querying_default_pyth_price_if_confidence_exceeded() { reason: "price confidence deviation 0.051 exceeds max allowed 0.05".to_string() } ); + + // should succeed for Liquidation pricing + let res = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Liquidation), + }, + ) + .unwrap(); + let res: PriceResponse = from_binary(&res).unwrap(); + assert_eq!(res.price, Decimal::from_ratio(101u128, 1u128)); } #[test] -fn querying_default_pyth_price_if_deviation_exceeded() { +fn querying_pyth_price_if_deviation_exceeded() { let mut deps = helpers::setup_test_for_pyth(); let price_id = PriceIdentifier::from_hex( @@ -411,6 +425,7 @@ fn querying_default_pyth_price_if_deviation_exceeded() { }, ); + // should fail for Default pricing let res_err = entry::query( deps.as_ref(), mock_env_at_block_time(publish_time), @@ -427,6 +442,19 @@ fn querying_default_pyth_price_if_deviation_exceeded() { } ); + // should succeed for Liquidation pricing + let res = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Liquidation), + }, + ) + .unwrap(); + let res: PriceResponse = from_binary(&res).unwrap(); + assert_eq!(res.price, Decimal::from_ratio(1061u128, 10u128)); + // ema_price > price deps.querier.set_pyth_price( price_id, @@ -449,6 +477,7 @@ fn querying_default_pyth_price_if_deviation_exceeded() { }, ); + // should fail for Default pricing let res_err = entry::query( deps.as_ref(), mock_env_at_block_time(publish_time), @@ -464,6 +493,19 @@ fn querying_default_pyth_price_if_deviation_exceeded() { reason: "price deviation 0.060001 exceeds max allowed 0.06".to_string() } ); + + // should succeed for Liquidation pricing + let res = entry::query( + deps.as_ref(), + mock_env_at_block_time(publish_time), + QueryMsg::Price { + denom: "uatom".to_string(), + kind: Some(ActionKind::Liquidation), + }, + ) + .unwrap(); + let res: PriceResponse = from_binary(&res).unwrap(); + assert_eq!(res.price, Decimal::from_ratio(939999u128, 10000u128)); } #[test] diff --git a/contracts/red-bank/Cargo.toml b/contracts/red-bank/Cargo.toml index 4352b9f90..90c2a0e4c 100644 --- a/contracts/red-bank/Cargo.toml +++ b/contracts/red-bank/Cargo.toml @@ -34,6 +34,7 @@ mars-params = { workspace = true } mars-red-bank-types = { workspace = true } mars-utils = { workspace = true } thiserror = { workspace = true } +pyth-sdk-cw = { workspace = true } [dev-dependencies] anyhow = { workspace = true } diff --git a/contracts/red-bank/src/collateral.rs b/contracts/red-bank/src/collateral.rs index f2ef6c7f2..4f7577182 100644 --- a/contracts/red-bank/src/collateral.rs +++ b/contracts/red-bank/src/collateral.rs @@ -52,6 +52,7 @@ pub fn update_asset_collateral_status( user.address(), oracle_addr, params_addr, + false, )?; if health.is_liquidatable() { diff --git a/contracts/red-bank/src/contract.rs b/contracts/red-bank/src/contract.rs index 052e96c05..9904d3034 100644 --- a/contracts/red-bank/src/contract.rs +++ b/contracts/red-bank/src/contract.rs @@ -179,7 +179,13 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { let user_addr = deps.api.addr_validate(&user)?; - to_binary(&query::query_user_position(deps, env, user_addr)?) + to_binary(&query::query_user_position(deps, env, user_addr, false)?) + } + QueryMsg::UserPositionLiquidationPricing { + user, + } => { + let user_addr = deps.api.addr_validate(&user)?; + to_binary(&query::query_user_position(deps, env, user_addr, true)?) } QueryMsg::ScaledLiquidityAmount { denom, diff --git a/contracts/red-bank/src/health.rs b/contracts/red-bank/src/health.rs index aaced7409..7909905a0 100644 --- a/contracts/red-bank/src/health.rs +++ b/contracts/red-bank/src/health.rs @@ -18,8 +18,10 @@ pub fn get_health_and_positions( user_addr: &Addr, oracle_addr: &Addr, params_addr: &Addr, + is_liquidation: bool, ) -> Result<(Health, HashMap), ContractError> { - let positions = get_user_positions_map(deps, env, user_addr, oracle_addr, params_addr)?; + let positions = + get_user_positions_map(deps, env, user_addr, oracle_addr, params_addr, is_liquidation)?; let health = compute_position_health(&positions)?; Ok((health, positions)) @@ -35,8 +37,8 @@ pub fn assert_below_liq_threshold_after_withdraw( denom: &str, withdraw_amount: Uint128, ) -> Result { - let mut positions = get_user_positions_map(deps, env, user_addr, oracle_addr, params_addr)?; - + let mut positions = + get_user_positions_map(deps, env, user_addr, oracle_addr, params_addr, false)?; // Update position to compute health factor after withdraw match positions.get_mut(denom) { Some(p) => { @@ -59,7 +61,8 @@ pub fn assert_below_max_ltv_after_borrow( denom: &str, borrow_amount: Uint128, ) -> Result { - let mut positions = get_user_positions_map(deps, env, user_addr, oracle_addr, params_addr)?; + let mut positions = + get_user_positions_map(deps, env, user_addr, oracle_addr, params_addr, false)?; // Update position to compute health factor after borrow positions @@ -112,6 +115,7 @@ pub fn get_user_positions_map( user_addr: &Addr, oracle_addr: &Addr, params_addr: &Addr, + is_liquidation: bool, ) -> Result, ContractError> { let block_time = env.block.time.seconds(); @@ -157,7 +161,11 @@ pub fn get_user_positions_map( None => (Uint128::zero(), false), }; - let asset_price = oracle::helpers::query_price(&deps.querier, oracle_addr, &denom)?; + let asset_price = if is_liquidation { + oracle::helpers::query_price_for_liquidate(&deps.querier, oracle_addr, &denom)? + } else { + oracle::helpers::query_price(&deps.querier, oracle_addr, &denom)? + }; let position = Position { denom: denom.clone(), diff --git a/contracts/red-bank/src/liquidate.rs b/contracts/red-bank/src/liquidate.rs index 5d4a8a523..a314812e4 100644 --- a/contracts/red-bank/src/liquidate.rs +++ b/contracts/red-bank/src/liquidate.rs @@ -84,8 +84,14 @@ pub fn liquidate( let oracle_addr = &addresses[&MarsAddressType::Oracle]; let params_addr = &addresses[&MarsAddressType::Params]; - let (health, assets_positions) = - get_health_and_positions(&deps.as_ref(), &env, &liquidatee_addr, oracle_addr, params_addr)?; + let (health, assets_positions) = get_health_and_positions( + &deps.as_ref(), + &env, + &liquidatee_addr, + oracle_addr, + params_addr, + true, + )?; if !health.is_liquidatable() { return Err(ContractError::CannotLiquidateHealthyPosition {}); diff --git a/contracts/red-bank/src/query.rs b/contracts/red-bank/src/query.rs index 44abb98ea..1bca591e3 100644 --- a/contracts/red-bank/src/query.rs +++ b/contracts/red-bank/src/query.rs @@ -252,6 +252,7 @@ pub fn query_user_position( deps: Deps, env: Env, user_addr: Addr, + liquidation_pricing: bool, ) -> Result { let config = CONFIG.load(deps.storage)?; @@ -263,8 +264,14 @@ pub fn query_user_position( let oracle_addr = &addresses[&MarsAddressType::Oracle]; let params_addr = &addresses[&MarsAddressType::Params]; - let positions = - health::get_user_positions_map(&deps, &env, &user_addr, oracle_addr, params_addr)?; + let positions = health::get_user_positions_map( + &deps, + &env, + &user_addr, + oracle_addr, + params_addr, + liquidation_pricing, + )?; let health = health::compute_position_health(&positions)?; let health_status = if let (Some(max_ltv_hf), Some(liq_threshold_hf)) = diff --git a/contracts/red-bank/tests/test_liquidate.rs b/contracts/red-bank/tests/test_liquidate.rs index a8c052e8f..ada8106d7 100644 --- a/contracts/red-bank/tests/test_liquidate.rs +++ b/contracts/red-bank/tests/test_liquidate.rs @@ -972,6 +972,37 @@ fn response_verification() { assert_eq!(res.messages, expected_msgs); } +#[test] +fn liquidation_uses_correct_price_kind() { + let mut mock_env = MockEnvBuilder::new(None, Addr::unchecked("owner")) + .target_health_factor(Decimal::from_ratio(12u128, 10u128)) + .build(); + + let red_bank = mock_env.red_bank.clone(); + let oracle = mock_env.oracle.clone(); + let pyth = mock_env.pyth.clone(); + + let (_funded_amt, provider, liquidatee, liquidator) = setup_env(&mut mock_env); + + // change price to be able to liquidate + oracle.set_price_source_fixed(&mut mock_env, "usd", Decimal::from_str("1000000").unwrap()); + oracle.set_price_source_pyth( + &mut mock_env, + "uusdc", + pyth.to_string(), + Decimal::percent(10u64), + Decimal::percent(15u64), + ); + + // liquidation should succeed because it uses simpler pricing for Pyth + red_bank + .liquidate(&mut mock_env, &liquidator, &liquidatee, "uosmo", &[coin(120, "uusdc")]) + .unwrap(); + + // confidence is higher than max_confidence so borrow will fail + red_bank.borrow(&mut mock_env, &provider, "uusdc", 300).unwrap_err(); +} + // recipient - can be liquidator or another address which can receive collateral fn expected_messages( user_addr: &Addr, diff --git a/contracts/red-bank/tests/test_misc.rs b/contracts/red-bank/tests/test_misc.rs index f1c6a361d..870ee7360 100644 --- a/contracts/red-bank/tests/test_misc.rs +++ b/contracts/red-bank/tests/test_misc.rs @@ -360,6 +360,7 @@ fn update_asset_collateral() { &user_addr, &Addr::unchecked("oracle"), &Addr::unchecked("params"), + false, ) .unwrap(); let health = health::compute_position_health(&positions).unwrap(); diff --git a/packages/health/src/query.rs b/packages/health/src/query.rs index da649ead8..29bf5ae41 100644 --- a/packages/health/src/query.rs +++ b/packages/health/src/query.rs @@ -39,4 +39,18 @@ impl<'a> MarsQuerier<'a> { )?; Ok(price) } + + pub fn query_price_for_liquidate(&self, denom: &str) -> StdResult { + let PriceResponse { + price, + .. + } = self.querier.query_wasm_smart( + self.oracle_addr, + &oracle::QueryMsg::Price { + denom: denom.to_string(), + kind: Some(ActionKind::Liquidation), + }, + )?; + Ok(price) + } } diff --git a/packages/testing/Cargo.toml b/packages/testing/Cargo.toml index 146b1c350..1e2f5767f 100644 --- a/packages/testing/Cargo.toml +++ b/packages/testing/Cargo.toml @@ -25,9 +25,11 @@ backtraces = ["cosmwasm-std/backtraces", "osmosis-std/backtraces"] anyhow = { workspace = true } astroport = { workspace = true, optional = true } cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } osmosis-std = { workspace = true } mars-address-provider = { workspace = true } mars-incentives = { workspace = true } +mars-mock-pyth = { workspace = true } mars-oracle-osmosis = { workspace = true } mars-oracle-wasm = { workspace = true } mars-osmosis = { workspace = true } diff --git a/packages/testing/src/integration/mock_contracts.rs b/packages/testing/src/integration/mock_contracts.rs index f18e124d9..826fb98a3 100644 --- a/packages/testing/src/integration/mock_contracts.rs +++ b/packages/testing/src/integration/mock_contracts.rs @@ -58,3 +58,12 @@ pub fn mock_params_osmosis_contract() -> Box> { ); Box::new(contract) } + +pub fn mock_pyth_contract() -> Box> { + let contract = ContractWrapper::new( + mars_mock_pyth::contract::execute, + mars_mock_pyth::contract::instantiate, + mars_mock_pyth::contract::query, + ); + Box::new(contract) +} diff --git a/packages/testing/src/integration/mock_env.rs b/packages/testing/src/integration/mock_env.rs index 2eeeaf24b..8f160abb1 100644 --- a/packages/testing/src/integration/mock_env.rs +++ b/packages/testing/src/integration/mock_env.rs @@ -1,6 +1,6 @@ #![allow(dead_code)] -use std::{collections::HashMap, mem::take, str::FromStr}; +use std::{collections::HashMap, default::Default, mem::take, str::FromStr}; use anyhow::Result as AnyResult; use cosmwasm_std::{coin, Addr, Coin, Decimal, Empty, StdResult, Uint128}; @@ -10,7 +10,11 @@ use mars_params::{msg::AssetParamsUpdate, types::asset::AssetParams}; use mars_red_bank_types::{ address_provider::{self, MarsAddressType}, incentives, - oracle::{self, PriceResponse}, + oracle::{ + self, + ActionKind::{Default as ActionDefault, Liquidation}, + PriceResponse, + }, red_bank::{ self, CreateOrUpdateConfig, InitOrUpdateAssetParams, Market, UncollateralizedLoanLimitResponse, UserCollateralResponse, UserDebtResponse, @@ -18,10 +22,12 @@ use mars_red_bank_types::{ }, rewards_collector, }; +use pyth_sdk_cw::PriceIdentifier; use crate::integration::mock_contracts::{ mock_address_provider_contract, mock_incentives_contract, mock_oracle_osmosis_contract, - mock_params_osmosis_contract, mock_red_bank_contract, mock_rewards_collector_osmosis_contract, + mock_params_osmosis_contract, mock_pyth_contract, mock_red_bank_contract, + mock_rewards_collector_osmosis_contract, }; pub struct MockEnv { @@ -34,6 +40,7 @@ pub struct MockEnv { pub rewards_collector: RewardsCollector, pub params: Params, pub credit_manager: Addr, + pub pyth: Addr, } #[derive(Clone)] @@ -239,6 +246,37 @@ impl Oracle { .unwrap(); } + pub fn set_price_source_pyth( + &self, + env: &mut MockEnv, + denom: &str, + pyth_addr: String, + max_confidence: Decimal, + max_deviation: Decimal, + ) { + env.app + .execute_contract( + env.owner.clone(), + self.contract_addr.clone(), + &oracle::ExecuteMsg::<_, Empty>::SetPriceSource { + denom: denom.to_string(), + price_source: OsmosisPriceSourceUnchecked::Pyth { + contract_addr: pyth_addr, + price_feed_id: PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(), + max_staleness: 30u64, + max_confidence, + max_deviation, + denom_decimals: 6u8, + }, + }, + &[], + ) + .unwrap(); + } + pub fn query_price(&self, env: &mut MockEnv, denom: &str) -> PriceResponse { env.app .wrap() @@ -246,7 +284,20 @@ impl Oracle { self.contract_addr.clone(), &oracle::QueryMsg::Price { denom: denom.to_string(), - kind: None, + kind: Some(ActionDefault), + }, + ) + .unwrap() + } + + pub fn query_price_for_liquidate(&self, env: &mut MockEnv, denom: &str) -> PriceResponse { + env.app + .wrap() + .query_wasm_smart( + self.contract_addr.clone(), + &oracle::QueryMsg::Price { + denom: denom.to_string(), + kind: Some(Liquidation), }, ) .unwrap() @@ -757,6 +808,7 @@ impl MockEnvBuilder { let red_bank_addr = self.deploy_red_bank(&address_provider_addr); let rewards_collector_addr = self.deploy_rewards_collector_osmosis(&address_provider_addr); let params_addr = self.deploy_params_osmosis(&address_provider_addr); + let pyth_addr = self.deploy_mock_pyth(); self.update_address_provider( &address_provider_addr, @@ -804,6 +856,7 @@ impl MockEnvBuilder { contract_addr: params_addr, }, credit_manager: cm_addr, + pyth: pyth_addr, } } @@ -928,6 +981,14 @@ impl MockEnvBuilder { .unwrap() } + pub fn deploy_mock_pyth(&mut self) -> Addr { + let code_id = self.app.store_code(mock_pyth_contract()); + + self.app + .instantiate_contract(code_id, self.owner.clone(), &Empty {}, &[], "mock-pyth", None) + .unwrap() + } + fn update_address_provider( &mut self, address_provider_addr: &Addr, diff --git a/packages/types/src/oracle/msg.rs b/packages/types/src/oracle/msg.rs index 393e1dc8a..572aae9bc 100644 --- a/packages/types/src/oracle/msg.rs +++ b/packages/types/src/oracle/msg.rs @@ -116,6 +116,7 @@ pub mod helpers { use cosmwasm_std::{Decimal, QuerierWrapper, StdError, StdResult}; use super::{ActionKind, PriceResponse, QueryMsg}; + use crate::oracle::ActionKind::Liquidation; pub fn query_price( querier: &QuerierWrapper, @@ -139,4 +140,19 @@ pub mod helpers { })?; Ok(res.price) } + + pub fn query_price_for_liquidate( + querier: &QuerierWrapper, + oracle: impl Into, + denom: impl Into, + ) -> StdResult { + let res: PriceResponse = querier.query_wasm_smart( + oracle.into(), + &QueryMsg::Price { + denom: denom.into(), + kind: Some(Liquidation), + }, + )?; + Ok(res.price) + } } diff --git a/packages/types/src/red_bank/msg.rs b/packages/types/src/red_bank/msg.rs index a01d99d61..b206e4b1a 100644 --- a/packages/types/src/red_bank/msg.rs +++ b/packages/types/src/red_bank/msg.rs @@ -198,6 +198,12 @@ pub enum QueryMsg { user: String, }, + /// Get user position for liquidation + #[returns(crate::red_bank::UserPositionResponse)] + UserPositionLiquidationPricing { + user: String, + }, + /// Get liquidity scaled amount for a given underlying asset amount. /// (i.e: how much scaled collateral is added if the given amount is deposited) #[returns(Uint128)] diff --git a/schemas/mars-red-bank/mars-red-bank.json b/schemas/mars-red-bank/mars-red-bank.json index a9e61ef47..084a66da0 100644 --- a/schemas/mars-red-bank/mars-red-bank.json +++ b/schemas/mars-red-bank/mars-red-bank.json @@ -838,6 +838,28 @@ }, "additionalProperties": false }, + { + "description": "Get user position for liquidation", + "type": "object", + "required": [ + "user_position_liquidation_pricing" + ], + "properties": { + "user_position_liquidation_pricing": { + "type": "object", + "required": [ + "user" + ], + "properties": { + "user": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Get liquidity scaled amount for a given underlying asset amount. (i.e: how much scaled collateral is added if the given amount is deposited)", "type": "object", @@ -1646,6 +1668,91 @@ ] } } + }, + "user_position_liquidation_pricing": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "UserPositionResponse", + "type": "object", + "required": [ + "health_status", + "total_collateralized_debt", + "total_enabled_collateral", + "weighted_liquidation_threshold_collateral", + "weighted_max_ltv_collateral" + ], + "properties": { + "health_status": { + "$ref": "#/definitions/UserHealthStatus" + }, + "total_collateralized_debt": { + "description": "Total value of all collateralized debts. If the user has an uncollateralized loan limit in an asset, the debt in this asset will not be included in this value.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "total_enabled_collateral": { + "description": "Total value of all enabled collateral assets. If an asset is disabled as collateral, it will not be included in this value.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "weighted_liquidation_threshold_collateral": { + "$ref": "#/definitions/Uint128" + }, + "weighted_max_ltv_collateral": { + "$ref": "#/definitions/Uint128" + } + }, + "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" + }, + "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" + }, + "UserHealthStatus": { + "oneOf": [ + { + "type": "string", + "enum": [ + "not_borrowing" + ] + }, + { + "type": "object", + "required": [ + "borrowing" + ], + "properties": { + "borrowing": { + "type": "object", + "required": [ + "liq_threshold_hf", + "max_ltv_hf" + ], + "properties": { + "liq_threshold_hf": { + "$ref": "#/definitions/Decimal" + }, + "max_ltv_hf": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + } + } } } } diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts index 4b84c7f1e..fe9cb5288 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts @@ -87,6 +87,7 @@ export interface MarsRedBankReadOnlyInterface { user: string }) => Promise userPosition: ({ user }: { user: string }) => Promise + userPositionLiquidationPricing: ({ user }: { user: string }) => Promise scaledLiquidityAmount: ({ amount, denom }: { amount: Uint128; denom: string }) => Promise scaledDebtAmount: ({ amount, denom }: { amount: Uint128; denom: string }) => Promise underlyingLiquidityAmount: ({ @@ -121,6 +122,7 @@ export class MarsRedBankQueryClient implements MarsRedBankReadOnlyInterface { this.userCollateral = this.userCollateral.bind(this) this.userCollaterals = this.userCollaterals.bind(this) this.userPosition = this.userPosition.bind(this) + this.userPositionLiquidationPricing = this.userPositionLiquidationPricing.bind(this) this.scaledLiquidityAmount = this.scaledLiquidityAmount.bind(this) this.scaledDebtAmount = this.scaledDebtAmount.bind(this) this.underlyingLiquidityAmount = this.underlyingLiquidityAmount.bind(this) @@ -259,6 +261,17 @@ export class MarsRedBankQueryClient implements MarsRedBankReadOnlyInterface { }, }) } + userPositionLiquidationPricing = async ({ + user, + }: { + user: string + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + user_position_liquidation_pricing: { + user, + }, + }) + } scaledLiquidityAmount = async ({ amount, denom, diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts index 9047d2f7e..981852c24 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts @@ -83,6 +83,17 @@ export const marsRedBankQueryKeys = { [ { ...marsRedBankQueryKeys.address(contractAddress)[0], method: 'user_position', args }, ] as const, + userPositionLiquidationPricing: ( + contractAddress: string | undefined, + args?: Record, + ) => + [ + { + ...marsRedBankQueryKeys.address(contractAddress)[0], + method: 'user_position_liquidation_pricing', + args, + }, + ] as const, scaledLiquidityAmount: (contractAddress: string | undefined, args?: Record) => [ { @@ -220,6 +231,28 @@ export function useMarsRedBankScaledLiquidityAmountQuery({ { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, ) } +export interface MarsRedBankUserPositionLiquidationPricingQuery + extends MarsRedBankReactQuery { + args: { + user: string + } +} +export function useMarsRedBankUserPositionLiquidationPricingQuery({ + client, + args, + options, +}: MarsRedBankUserPositionLiquidationPricingQuery) { + return useQuery( + marsRedBankQueryKeys.userPositionLiquidationPricing(client?.contractAddress, args), + () => + client + ? client.userPositionLiquidationPricing({ + user: args.user, + }) + : Promise.reject(new Error('Invalid client')), + { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, + ) +} export interface MarsRedBankUserPositionQuery extends MarsRedBankReactQuery { args: { diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts index 40c86bf51..21b483d57 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts @@ -166,6 +166,11 @@ export type QueryMsg = user: string } } + | { + user_position_liquidation_pricing: { + user: string + } + } | { scaled_liquidity_amount: { amount: Uint128 From 46771da3f37c85b67b67f693d146c8bc559535f6 Mon Sep 17 00:00:00 2001 From: pacmanifold <105084485+pacmanifold@users.noreply.github.com> Date: Fri, 4 Aug 2023 13:34:36 +0200 Subject: [PATCH 17/43] docs: add README to swapper (#292) --- contracts/swapper/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 contracts/swapper/README.md diff --git a/contracts/swapper/README.md b/contracts/swapper/README.md new file mode 100644 index 000000000..674764ff9 --- /dev/null +++ b/contracts/swapper/README.md @@ -0,0 +1,9 @@ +# Mars Swapper + +`mars-swapper-base` contains chain-agnostic logics for the swapper contract. Each chain specific implementation should implement its own route struct that should implement the `mars_swapper_base::Route` trait and then use the `mars_swapper_base::SwapBase` to implement the entry point functions. See `./osmosis/src/contract.rs` for an example. Each chain specific swapper will thus implement the same API. + +The swapper contracts should NEVER hold any funds and any funds sent to the contract except as part of executing the `SwapExactIn` message can be stolen by an attacker. See [Oak Audit 2023-08-01](https://github.com/oak-security/audit-reports/blob/master/Mars/2023-08-01%20Audit%20Report%20-%20Mars%20Red%20Bank%20Updates%20v1.0.pdf) issue 14. + +## Mars Swapper Mock + +Mock swapper contains a mock swapper contract to be used for testing purposes only. It only implements `ExecuteMsg::SwapExactIn` and `QueryMsg::EstimateExactInSwap`. When calling `ExecuteMsg::SwapExactIn` `denom_out` must be `uosmo` and the resulting amount will always be `1337uosmo`. The contract MUST be prefunded with this amount. From 496062c4a677c51c191ccd36ffe54e228d221750 Mon Sep 17 00:00:00 2001 From: piobab Date: Fri, 4 Aug 2023 16:04:25 +0200 Subject: [PATCH 18/43] Assert hf improve after liq (#294) * Assert liq HF improve after liquidation. * Fix comparison. New HF should be bigger. --- contracts/red-bank/src/error.rs | 8 ++ contracts/red-bank/src/liquidate.rs | 38 +++++++++- contracts/red-bank/tests/test_liquidate.rs | 88 ++++------------------ 3 files changed, 58 insertions(+), 76 deletions(-) diff --git a/contracts/red-bank/src/error.rs b/contracts/red-bank/src/error.rs index 54b1972ac..9e30ea748 100644 --- a/contracts/red-bank/src/error.rs +++ b/contracts/red-bank/src/error.rs @@ -155,4 +155,12 @@ pub enum ContractError { #[error("Cannot repay uncollateralized loan on behalf of another user")] CannotRepayUncollateralizedLoanOnBehalfOf {}, + + #[error( + "Liquidation did not result in improved health factor: before: {prev_hf:?}, after: {new_hf:?}" + )] + HealthNotImproved { + prev_hf: String, + new_hf: String, + }, } diff --git a/contracts/red-bank/src/liquidate.rs b/contracts/red-bank/src/liquidate.rs index a314812e4..b29f80a94 100644 --- a/contracts/red-bank/src/liquidate.rs +++ b/contracts/red-bank/src/liquidate.rs @@ -1,4 +1,5 @@ -use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response, Uint128}; +use cosmwasm_std::{Addr, Deps, DepsMut, Env, MessageInfo, Response, Uint128}; +use mars_health::health::Health; use mars_interest_rate::{ get_scaled_debt_amount, get_scaled_liquidity_amount, get_underlying_debt_amount, get_underlying_liquidity_amount, @@ -215,7 +216,17 @@ pub fn liquidate( response = update_interest_rates(&env, &mut debt_market_after, response)?; MARKETS.save(deps.storage, &debt_denom, &debt_market_after)?; - // 7. Build response + // 7. Assert improvement for liquidation HF + assert_liq_threshold( + &deps.as_ref(), + &env, + &liquidatee_addr, + oracle_addr, + params_addr, + &health, + )?; + + // 8. Build response // refund sent amount in excess of actual debt amount to liquidate if !refund_amount.is_zero() { response = @@ -234,3 +245,26 @@ pub fn liquidate( .add_attribute("debt_amount", debt_amount_to_repay) .add_attribute("debt_amount_scaled", debt_amount_scaled_delta)) } + +fn assert_liq_threshold( + deps: &Deps, + env: &Env, + user_addr: &Addr, + oracle_addr: &Addr, + params_addr: &Addr, + prev_health: &Health, +) -> Result<(), ContractError> { + let (new_health, _) = + get_health_and_positions(deps, env, user_addr, oracle_addr, params_addr, true)?; + + // liquidation_health_factor = None only if debt = 0 but liquidation is not possible + match (prev_health.liquidation_health_factor, new_health.liquidation_health_factor) { + (Some(prev_liq_hf), Some(new_liq_hf)) if prev_liq_hf >= new_liq_hf => { + Err(ContractError::HealthNotImproved { + prev_hf: prev_liq_hf.to_string(), + new_hf: new_liq_hf.to_string(), + }) + } + _ => Ok(()), + } +} diff --git a/contracts/red-bank/tests/test_liquidate.rs b/contracts/red-bank/tests/test_liquidate.rs index ada8106d7..6dd854671 100644 --- a/contracts/red-bank/tests/test_liquidate.rs +++ b/contracts/red-bank/tests/test_liquidate.rs @@ -553,7 +553,6 @@ fn same_asset_for_debt_and_collateral_with_refund() { let red_bank = mock_env.red_bank.clone(); let params = mock_env.params.clone(); let oracle = mock_env.oracle.clone(); - let rewards_collector = mock_env.rewards_collector.clone(); let funded_amt = 1_000_000_000_000u128; let provider = Addr::unchecked("provider"); // provides collateral to be borrowed by others @@ -588,81 +587,22 @@ fn same_asset_for_debt_and_collateral_with_refund() { // change price to be able to liquidate oracle.set_price_source_fixed(&mut mock_env, "uatom", Decimal::from_ratio(2u128, 1u128)); - // liquidatee should be liquidatable - let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); - let prev_liq_threshold_hf = liq_threshold_hf(&liquidatee_position); - // liquidate user let osmo_repay_amt = 1000; - red_bank - .liquidate( - &mut mock_env, - &liquidator, - &liquidatee, - "uosmo", - &[coin(osmo_repay_amt, "uosmo")], - ) - .unwrap(); - - // check provider positions - let provider_collaterals = red_bank.query_user_collaterals(&mut mock_env, &provider); - assert_eq!(provider_collaterals.len(), 1); - assert_eq!(provider_collaterals.get("uosmo").unwrap().amount.u128(), 1000000); - let provider_debts = red_bank.query_user_debts(&mut mock_env, &provider); - assert_eq!(provider_debts.len(), 0); - - // check liquidatee positions - let liquidatee_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidatee); - assert_eq!(liquidatee_collaterals.len(), 2); - assert_eq!(liquidatee_collaterals.get("uosmo").unwrap().amount.u128(), 1); - assert_eq!(liquidatee_collaterals.get("uatom").unwrap().amount.u128(), 1000); - let liquidatee_debts = red_bank.query_user_debts(&mut mock_env, &liquidatee); - assert_eq!(liquidatee_debts.len(), 1); - assert_eq!(liquidatee_debts.get("uosmo").unwrap().amount.u128(), 2020); - - // check liquidator positions - let liquidator_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidator); - assert_eq!(liquidator_collaterals.len(), 1); - assert_eq!(liquidator_collaterals.get("uosmo").unwrap().amount.u128(), 998); - let liquidator_debts = red_bank.query_user_debts(&mut mock_env, &liquidator); - assert_eq!(liquidator_debts.len(), 0); - - // check rewards-collector positions (protocol fee) - let rc_collaterals = - red_bank.query_user_collaterals(&mut mock_env, &rewards_collector.contract_addr); - assert_eq!(rc_collaterals.len(), 1); - assert_eq!(rc_collaterals.get("uosmo").unwrap().amount.u128(), 1); - let rc_debts = red_bank.query_user_debts(&mut mock_env, &rewards_collector.contract_addr); - assert_eq!(rc_debts.len(), 0); - - let (merged_collaterals, merged_debts, merged_balances) = merge_collaterals_and_debts( - &[&provider_collaterals, &liquidatee_collaterals, &liquidator_collaterals, &rc_collaterals], - &[&provider_debts, &liquidatee_debts, &liquidator_debts, &rc_debts], + let error_res = red_bank.liquidate( + &mut mock_env, + &liquidator, + &liquidatee, + "uosmo", + &[coin(osmo_repay_amt, "uosmo")], + ); + assert_err( + error_res, + ContractError::HealthNotImproved { + prev_hf: "0.66".to_string(), + new_hf: "0.594059405940594059".to_string(), + }, ); - - // check if users collaterals and debts are equal to markets scaled amounts - let markets = red_bank.query_markets(&mut mock_env); - assert_eq!(markets.len(), 2); - let osmo_market = markets.get("uosmo").unwrap(); - let atom_market = markets.get("uatom").unwrap(); - assert_eq!(merged_collaterals.get_or_default("uosmo"), osmo_market.collateral_total_scaled); - assert_eq!(merged_debts.get_or_default("uosmo"), osmo_market.debt_total_scaled); - assert_eq!(merged_collaterals.get_or_default("uatom"), atom_market.collateral_total_scaled); - assert_eq!(merged_debts.get_or_default("uatom"), atom_market.debt_total_scaled); - - // check red bank underlying balances - let balances = mock_env.query_all_balances(&red_bank.contract_addr); - assert_eq!(merged_balances.get("uosmo"), balances.get("uosmo")); - assert_eq!(merged_balances.get("uatom"), balances.get("uatom")); - - // check liquidator account balance - let omso_liquidator_balance = mock_env.query_balance(&liquidator, "uosmo").unwrap(); - assert_eq!(omso_liquidator_balance.amount.u128(), funded_amt - osmo_repay_amt + 20); // 20 refunded - - // liquidatee hf should improve - let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); - let liq_threshold_hf = liq_threshold_hf(&liquidatee_position); - assert!(liq_threshold_hf < prev_liq_threshold_hf); // FIXME: is it ok? we should block liquidation? } #[test] @@ -896,7 +836,7 @@ fn response_verification() { ); let debt_to_repay = 2883_u128; - let block_time = 200_000_000; + let block_time = 500_000; let env = mock_env_at_block_time(block_time); let info = mock_info(liquidator.as_str(), &[coin(debt_to_repay, "uusdc")]); let res = execute( From 18e9c565df15c2a5ea5fa92da561b0c7cb570294 Mon Sep 17 00:00:00 2001 From: larry <26318510+larry0x@users.noreply.github.com> Date: Sat, 5 Aug 2023 14:47:43 +0100 Subject: [PATCH 19/43] Include deposit cap in the `total_deposit` query (#295) * include deposit cap in total deposit query * update schema and typescript types --- contracts/params/src/msg.rs | 11 ++++++-- contracts/params/src/query.rs | 20 ++++++++++++--- contracts/params/tests/test_deposit_cap.rs | 25 ++++++++++++++++--- schemas/mars-params/mars-params.json | 7 +++++- .../mars-params/MarsParams.client.ts | 5 ++-- .../mars-params/MarsParams.react-query.ts | 8 +++--- .../generated/mars-params/MarsParams.types.ts | 5 ++++ 7 files changed, 66 insertions(+), 15 deletions(-) diff --git a/contracts/params/src/msg.rs b/contracts/params/src/msg.rs index 7f192d08d..d4fbd3177 100644 --- a/contracts/params/src/msg.rs +++ b/contracts/params/src/msg.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Coin, Decimal}; +use cosmwasm_std::{Decimal, Uint128}; use mars_owner::OwnerUpdate; use crate::types::{asset::AssetParamsUnchecked, vault::VaultConfigUnchecked}; @@ -57,12 +57,19 @@ pub enum QueryMsg { /// Compute the total amount deposited of the given asset across Red Bank /// and Credit Manager. - #[returns(Coin)] + #[returns(TotalDepositResponse)] TotalDeposit { denom: String, }, } +#[cw_serde] +pub struct TotalDepositResponse { + pub denom: String, + pub cap: Uint128, + pub amount: Uint128, +} + #[cw_serde] pub enum AssetParamsUpdate { AddOrUpdate { diff --git a/contracts/params/src/query.rs b/contracts/params/src/query.rs index d7a5b8256..c5e53bd0d 100644 --- a/contracts/params/src/query.rs +++ b/contracts/params/src/query.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Addr, Coin, Deps, Env, Order, StdResult, Uint128}; +use cosmwasm_std::{Addr, Deps, Env, Order, StdResult, Uint128}; use cw_storage_plus::Bound; use mars_interest_rate::get_underlying_liquidity_amount; use mars_red_bank_types::{ @@ -7,6 +7,7 @@ use mars_red_bank_types::{ }; use crate::{ + msg::TotalDepositResponse, state::{ADDRESS_PROVIDER, ASSET_PARAMS, VAULT_CONFIGS}, types::{asset::AssetParams, vault::VaultConfig}, }; @@ -73,7 +74,11 @@ pub fn query_all_vault_configs( /// For example, when computing the deposited amount of ATOM, we only include /// ATOM deposited in RB and CM; we don't include the ATOM-OSMO LP token, or /// the ATOM-OSMO farming vault. -pub fn query_total_deposit(deps: Deps, env: &Env, denom: String) -> StdResult { +pub fn query_total_deposit( + deps: Deps, + env: &Env, + denom: String, +) -> StdResult { let current_timestamp = env.block.time.seconds(); // query contract addresses @@ -124,8 +129,15 @@ pub fn query_total_deposit(deps: Deps, env: &Env, denom: String) -> StdResult Promise targetHealthFactor: () => Promise - totalDeposit: ({ denom }: { denom: string }) => Promise + totalDeposit: ({ denom }: { denom: string }) => Promise } export class MarsParamsQueryClient implements MarsParamsReadOnlyInterface { client: CosmWasmClient @@ -127,7 +128,7 @@ export class MarsParamsQueryClient implements MarsParamsReadOnlyInterface { target_health_factor: {}, }) } - totalDeposit = async ({ denom }: { denom: string }): Promise => { + totalDeposit = async ({ denom }: { denom: string }): Promise => { return this.client.queryContractSmart(this.contractAddress, { total_deposit: { denom, diff --git a/scripts/types/generated/mars-params/MarsParams.react-query.ts b/scripts/types/generated/mars-params/MarsParams.react-query.ts index beae94e83..54b185c5b 100644 --- a/scripts/types/generated/mars-params/MarsParams.react-query.ts +++ b/scripts/types/generated/mars-params/MarsParams.react-query.ts @@ -37,6 +37,7 @@ import { ArrayOfVaultConfigBaseForAddr, VaultConfigBaseForAddr, OwnerResponse, + TotalDepositResponse, } from './MarsParams.types' import { MarsParamsQueryClient, MarsParamsClient } from './MarsParams.client' export const marsParamsQueryKeys = { @@ -79,17 +80,18 @@ export interface MarsParamsReactQuery { initialData?: undefined } } -export interface MarsParamsTotalDepositQuery extends MarsParamsReactQuery { +export interface MarsParamsTotalDepositQuery + extends MarsParamsReactQuery { args: { denom: string } } -export function useMarsParamsTotalDepositQuery({ +export function useMarsParamsTotalDepositQuery({ client, args, options, }: MarsParamsTotalDepositQuery) { - return useQuery( + return useQuery( marsParamsQueryKeys.totalDeposit(client?.contractAddress, args), () => client diff --git a/scripts/types/generated/mars-params/MarsParams.types.ts b/scripts/types/generated/mars-params/MarsParams.types.ts index 11eb13940..d27e9d118 100644 --- a/scripts/types/generated/mars-params/MarsParams.types.ts +++ b/scripts/types/generated/mars-params/MarsParams.types.ts @@ -208,3 +208,8 @@ export interface OwnerResponse { owner?: string | null proposed?: string | null } +export interface TotalDepositResponse { + amount: Uint128 + cap: Uint128 + denom: string +} From 7d698f8145b7cce38bc7b785b4ae0e848e15633c Mon Sep 17 00:00:00 2001 From: piobab Date: Sun, 6 Aug 2023 10:32:06 +0200 Subject: [PATCH 20/43] Add withdraw msg for protocol fees in credit manager. (#296) --- .../rewards-collector/base/src/contract.rs | 41 ++++++ contracts/rewards-collector/base/src/error.rs | 3 + .../osmosis/tests/test_withdraw.rs | 81 +++++++++++- packages/types/src/rewards_collector.rs | 44 +++++++ .../mars-rewards-collector-base.json | 124 ++++++++++++++++++ .../MarsRewardsCollectorBase.client.ts | 42 ++++++ .../MarsRewardsCollectorBase.react-query.ts | 35 +++++ .../MarsRewardsCollectorBase.types.ts | 28 ++++ 8 files changed, 396 insertions(+), 2 deletions(-) diff --git a/contracts/rewards-collector/base/src/contract.rs b/contracts/rewards-collector/base/src/contract.rs index 3b1c4d509..7615ab34a 100644 --- a/contracts/rewards-collector/base/src/contract.rs +++ b/contracts/rewards-collector/base/src/contract.rs @@ -8,6 +8,7 @@ use mars_red_bank_types::{ address_provider::{self, AddressResponseItem, MarsAddressType}, incentives, red_bank, rewards_collector::{ + credit_manager::{self, Action}, Config, ConfigResponse, ExecuteMsg, InstantiateMsg, QueryMsg, UpdateConfig, }, }; @@ -85,6 +86,10 @@ where denom, amount, } => self.withdraw_from_red_bank(deps, denom, amount), + ExecuteMsg::WithdrawFromCreditManager { + account_id, + actions, + } => self.withdraw_from_credit_manager(deps, account_id, actions), ExecuteMsg::DistributeRewards { denom, amount, @@ -194,6 +199,42 @@ where .add_attribute("amount", stringify_option_amount(amount))) } + pub fn withdraw_from_credit_manager( + &self, + deps: DepsMut, + account_id: String, + actions: Vec, + ) -> ContractResult> { + let cfg = self.config.load(deps.storage)?; + + let valid_actions = actions.iter().all(|action| { + matches!(action, Action::Withdraw(..) | Action::WithdrawLiquidity { .. }) + }); + if !valid_actions { + return Err(ContractError::InvalidActionsForCreditManager {}); + } + + let cm_addr = address_provider::helpers::query_contract_addr( + deps.as_ref(), + &cfg.address_provider, + MarsAddressType::CreditManager, + )?; + + let withdraw_from_cm_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cm_addr.to_string(), + msg: to_binary(&credit_manager::ExecuteMsg::UpdateCreditAccount { + account_id: account_id.clone(), + actions, + })?, + funds: vec![], + }); + + Ok(Response::new() + .add_message(withdraw_from_cm_msg) + .add_attribute("action", "withdraw_from_credit_manager") + .add_attribute("account_id", account_id)) + } + pub fn claim_incentive_rewards( &self, deps: DepsMut, diff --git a/contracts/rewards-collector/base/src/error.rs b/contracts/rewards-collector/base/src/error.rs index 83ab8d732..6744b846a 100644 --- a/contracts/rewards-collector/base/src/error.rs +++ b/contracts/rewards-collector/base/src/error.rs @@ -39,6 +39,9 @@ pub enum ContractError { InvalidRoute { reason: String, }, + + #[error("Invalid actions. Only Withdraw and WithdrawLiquidity is possible to pass for CreditManager")] + InvalidActionsForCreditManager {}, } pub type ContractResult = Result; diff --git a/contracts/rewards-collector/osmosis/tests/test_withdraw.rs b/contracts/rewards-collector/osmosis/tests/test_withdraw.rs index a2e5645aa..48d4b3662 100644 --- a/contracts/rewards-collector/osmosis/tests/test_withdraw.rs +++ b/contracts/rewards-collector/osmosis/tests/test_withdraw.rs @@ -1,5 +1,9 @@ -use cosmwasm_std::{testing::mock_env, to_binary, CosmosMsg, SubMsg, Uint128, WasmMsg}; -use mars_red_bank_types::rewards_collector::ExecuteMsg; +use cosmwasm_std::{coin, testing::mock_env, to_binary, CosmosMsg, SubMsg, Uint128, WasmMsg}; +use mars_red_bank_types::rewards_collector::{ + credit_manager::{self, Action, ActionAmount, ActionCoin}, + ExecuteMsg, +}; +use mars_rewards_collector_base::ContractError; use mars_rewards_collector_osmosis::entry::execute; use mars_testing::mock_info; @@ -37,3 +41,76 @@ fn withdrawing_from_red_bank() { })) ) } + +#[test] +fn withdrawing_from_cm_if_action_not_allowed() { + let mut deps = helpers::setup_test(); + + // anyone can execute a withdrawal + let error_res = execute( + deps.as_mut(), + mock_env(), + mock_info("jake"), + ExecuteMsg::WithdrawFromCreditManager { + account_id: "random_id".to_string(), + actions: vec![ + Action::Withdraw(coin(100u128, "uatom")), + Action::Unknown {}, + Action::WithdrawLiquidity { + lp_token: ActionCoin { + denom: "gamm/pool/1".to_string(), + amount: ActionAmount::AccountBalance, + }, + minimum_receive: vec![], + }, + ], + }, + ) + .unwrap_err(); + assert_eq!(error_res, ContractError::InvalidActionsForCreditManager {}); +} + +#[test] +fn withdrawing_from_cm_successfully() { + let mut deps = helpers::setup_test(); + + let account_id = "random_id".to_string(); + let actions = vec![ + Action::Withdraw(coin(100u128, "uusdc")), + Action::WithdrawLiquidity { + lp_token: ActionCoin { + denom: "gamm/pool/1".to_string(), + amount: ActionAmount::AccountBalance, + }, + minimum_receive: vec![], + }, + Action::Withdraw(coin(120u128, "uatom")), + Action::Withdraw(coin(140u128, "uosmo")), + ]; + + // anyone can execute a withdrawal + let res = execute( + deps.as_mut(), + mock_env(), + mock_info("jake"), + ExecuteMsg::WithdrawFromCreditManager { + account_id: account_id.clone(), + actions: actions.clone(), + }, + ) + .unwrap(); + + assert_eq!(res.messages.len(), 1); + assert_eq!( + res.messages[0], + SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "credit_manager".to_string(), + msg: to_binary(&credit_manager::ExecuteMsg::UpdateCreditAccount { + account_id, + actions + }) + .unwrap(), + funds: vec![] + })) + ) +} diff --git a/packages/types/src/rewards_collector.rs b/packages/types/src/rewards_collector.rs index 748e51c0a..149490eb5 100644 --- a/packages/types/src/rewards_collector.rs +++ b/packages/types/src/rewards_collector.rs @@ -6,6 +6,8 @@ use mars_utils::{ helpers::{decimal_param_le_one, integer_param_gt_zero, validate_native_denom}, }; +use self::credit_manager::Action; + const MAX_SLIPPAGE_TOLERANCE_PERCENTAGE: u64 = 50; #[cw_serde] @@ -130,6 +132,12 @@ pub enum ExecuteMsg { amount: Option, }, + /// Withdraw coins from the credit manager + WithdrawFromCreditManager { + account_id: String, + actions: Vec, + }, + /// Distribute the accrued protocol income between the safety fund and the fee modules on mars hub, /// according to the split set in config. /// Callable by any address. @@ -191,3 +199,39 @@ pub enum QueryMsg { #[returns(ConfigResponse)] Config {}, } + +// TODO: rover is private repo for now so can't use it as a dependency. Use rover types once repo is public. +pub mod credit_manager { + use cosmwasm_schema::cw_serde; + use cosmwasm_std::{Coin, Uint128}; + + #[cw_serde] + pub enum ExecuteMsg { + UpdateCreditAccount { + account_id: String, + actions: Vec, + }, + } + + #[cw_serde] + pub enum Action { + Withdraw(Coin), + WithdrawLiquidity { + lp_token: ActionCoin, + minimum_receive: Vec, + }, + Unknown {}, // Used to simulate allowance only for: Withdraw and WithdrawLiquidity + } + + #[cw_serde] + pub struct ActionCoin { + pub denom: String, + pub amount: ActionAmount, + } + + #[cw_serde] + pub enum ActionAmount { + Exact(Uint128), + AccountBalance, + } +} diff --git a/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json b/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json index e0b95cadc..30e68fddc 100644 --- a/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json +++ b/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json @@ -195,6 +195,35 @@ }, "additionalProperties": false }, + { + "description": "Withdraw coins from the credit manager", + "type": "object", + "required": [ + "withdraw_from_credit_manager" + ], + "properties": { + "withdraw_from_credit_manager": { + "type": "object", + "required": [ + "account_id", + "actions" + ], + "properties": { + "account_id": { + "type": "string" + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/definitions/Action" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Distribute the accrued protocol income between the safety fund and the fee modules on mars hub, according to the split set in config. Callable by any address.", "type": "object", @@ -300,6 +329,101 @@ } ], "definitions": { + "Action": { + "oneOf": [ + { + "type": "object", + "required": [ + "withdraw" + ], + "properties": { + "withdraw": { + "$ref": "#/definitions/Coin" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "withdraw_liquidity" + ], + "properties": { + "withdraw_liquidity": { + "type": "object", + "required": [ + "lp_token", + "minimum_receive" + ], + "properties": { + "lp_token": { + "$ref": "#/definitions/ActionCoin" + }, + "minimum_receive": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "unknown" + ], + "properties": { + "unknown": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "ActionAmount": { + "oneOf": [ + { + "type": "string", + "enum": [ + "account_balance" + ] + }, + { + "type": "object", + "required": [ + "exact" + ], + "properties": { + "exact": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + ] + }, + "ActionCoin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/ActionAmount" + }, + "denom": { + "type": "string" + } + }, + "additionalProperties": false + }, "Coin": { "type": "object", "required": [ diff --git a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.client.ts b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.client.ts index 44808d0c6..b52768c4a 100644 --- a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.client.ts +++ b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.client.ts @@ -15,7 +15,10 @@ import { Coin, ExecuteMsg, OwnerUpdate, + Action, + ActionAmount, UpdateConfig, + ActionCoin, QueryMsg, ConfigResponse, } from './MarsRewardsCollectorBase.types' @@ -73,6 +76,18 @@ export interface MarsRewardsCollectorBaseInterface memo?: string, _funds?: Coin[], ) => Promise + withdrawFromCreditManager: ( + { + accountId, + actions, + }: { + accountId: string + actions: Action[] + }, + fee?: number | StdFee | 'auto', + memo?: string, + _funds?: Coin[], + ) => Promise distributeRewards: ( { amount, @@ -128,6 +143,7 @@ export class MarsRewardsCollectorBaseClient this.updateOwner = this.updateOwner.bind(this) this.updateConfig = this.updateConfig.bind(this) this.withdrawFromRedBank = this.withdrawFromRedBank.bind(this) + this.withdrawFromCreditManager = this.withdrawFromCreditManager.bind(this) this.distributeRewards = this.distributeRewards.bind(this) this.swapAsset = this.swapAsset.bind(this) this.claimIncentiveRewards = this.claimIncentiveRewards.bind(this) @@ -199,6 +215,32 @@ export class MarsRewardsCollectorBaseClient _funds, ) } + withdrawFromCreditManager = async ( + { + accountId, + actions, + }: { + accountId: string + actions: Action[] + }, + fee: number | StdFee | 'auto' = 'auto', + memo?: string, + _funds?: Coin[], + ): Promise => { + return await this.client.execute( + this.sender, + this.contractAddress, + { + withdraw_from_credit_manager: { + account_id: accountId, + actions, + }, + }, + fee, + memo, + _funds, + ) + } distributeRewards = async ( { amount, diff --git a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.react-query.ts b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.react-query.ts index 5db72cd7f..90b128d6b 100644 --- a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.react-query.ts +++ b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.react-query.ts @@ -16,7 +16,10 @@ import { Coin, ExecuteMsg, OwnerUpdate, + Action, + ActionAmount, UpdateConfig, + ActionCoin, QueryMsg, ConfigResponse, } from './MarsRewardsCollectorBase.types' @@ -130,6 +133,38 @@ export function useMarsRewardsCollectorBaseDistributeRewardsMutation( options, ) } +export interface MarsRewardsCollectorBaseWithdrawFromCreditManagerMutation { + client: MarsRewardsCollectorBaseClient + msg: { + accountId: string + actions: Action[] + } + args?: { + fee?: number | StdFee | 'auto' + memo?: string + funds?: Coin[] + } +} +export function useMarsRewardsCollectorBaseWithdrawFromCreditManagerMutation( + options?: Omit< + UseMutationOptions< + ExecuteResult, + Error, + MarsRewardsCollectorBaseWithdrawFromCreditManagerMutation + >, + 'mutationFn' + >, +) { + return useMutation< + ExecuteResult, + Error, + MarsRewardsCollectorBaseWithdrawFromCreditManagerMutation + >( + ({ client, msg, args: { fee, memo, funds } = {} }) => + client.withdrawFromCreditManager(msg, fee, memo, funds), + options, + ) +} export interface MarsRewardsCollectorBaseWithdrawFromRedBankMutation { client: MarsRewardsCollectorBaseClient msg: { diff --git a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.types.ts b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.types.ts index eaef259bb..fc16e839c 100644 --- a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.types.ts +++ b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.types.ts @@ -43,6 +43,12 @@ export type ExecuteMsg = denom: string } } + | { + withdraw_from_credit_manager: { + account_id: string + actions: Action[] + } + } | { distribute_rewards: { amount?: Uint128 | null @@ -77,6 +83,24 @@ export type OwnerUpdate = } } | 'clear_emergency_owner' +export type Action = + | { + withdraw: Coin + } + | { + withdraw_liquidity: { + lp_token: ActionCoin + minimum_receive: Coin[] + } + } + | { + unknown: {} + } +export type ActionAmount = + | 'account_balance' + | { + exact: Uint128 + } export interface UpdateConfig { address_provider?: string | null channel_id?: string | null @@ -87,6 +111,10 @@ export interface UpdateConfig { slippage_tolerance?: Decimal | null timeout_seconds?: number | null } +export interface ActionCoin { + amount: ActionAmount + denom: string +} export type QueryMsg = { config: {} } From 0dd8835c470a93268715ed6bc762fd89a86e2be3 Mon Sep 17 00:00:00 2001 From: piobab Date: Sun, 6 Aug 2023 11:04:09 +0200 Subject: [PATCH 21/43] Deps and workflow (#297) * Bump deps. * Remove artifacts workflow. * Fix docker for optimizer. --- .github/workflows/artifacts.yml | 25 --------------------- Cargo.lock | 40 +++++++++++++++++++++++++-------- Cargo.toml | 24 ++++++++++---------- Makefile.toml | 4 ++-- 4 files changed, 45 insertions(+), 48 deletions(-) delete mode 100644 .github/workflows/artifacts.yml diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml deleted file mode 100644 index db63209d2..000000000 --- a/.github/workflows/artifacts.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Artifacts - -on: - push: - branches: - - master - - main - pull_request: - -env: - RUST_BACKTRACE: 1 - CARGO_TERM_COLOR: always - -jobs: - artifacts: - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v3 - - - name: Install cargo make - uses: davidB/rust-cargo-make@v1 - - - name: Compile contracts to wasm - run: cargo make rust-optimizer diff --git a/Cargo.lock b/Cargo.lock index b938e3260..f5f0b5b99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -842,7 +842,7 @@ dependencies = [ "regex", "serde", "serde_json", - "strum", + "strum 0.24.1", "test-tube", "thiserror", ] @@ -1884,9 +1884,9 @@ dependencies = [ [[package]] name = "mars-owner" -version = "1.2.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acd53908ffc561da878ce5ff4f5ec9f25a193af28ec0b6e7c8e6d1a0866d9dfc" +checksum = "ab46e0b2f81a8a98036b46730fbe33a337e98e87cb3d34553b45a5ae87c5828c" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1948,7 +1948,7 @@ dependencies = [ "cosmwasm-std", "mars-owner", "mars-utils", - "strum", + "strum 0.25.0", "thiserror", ] @@ -2969,9 +2969,9 @@ checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" [[package]] name = "serde" -version = "1.0.180" +version = "1.0.181" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea67f183f058fe88a4e3ec6e2788e003840893b91bac4559cabedd00863b3ed" +checksum = "6d3e73c93c3240c0bda063c239298e633114c69a888c3e37ca8bb33f343e9890" dependencies = [ "serde_derive", ] @@ -3005,9 +3005,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.180" +version = "1.0.181" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24e744d7782b686ab3b73267ef05697159cc0e5abbed3f47f9933165e5219036" +checksum = "be02f6cb0cd3a5ec20bbcfbcbd749f57daddb1a0882dc2e46a6c236c90b977ed" dependencies = [ "proc-macro2", "quote", @@ -3182,7 +3182,16 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" dependencies = [ - "strum_macros", + "strum_macros 0.24.3", +] + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros 0.25.1", ] [[package]] @@ -3198,6 +3207,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "strum_macros" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6069ca09d878a33f883cc06aaa9718ede171841d3832450354410b718b097232" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.28", +] + [[package]] name = "subtle" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 41b21ba77..8955b9a95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,32 +42,32 @@ keywords = [ ] [workspace.dependencies] -anyhow = "1.0.71" +anyhow = "1.0.72" bech32 = "0.9.1" -cosmwasm-schema = "1.2.6" +cosmwasm-schema = "1.3.1" cosmwasm-std = "1.3.1" cw2 = "1.1.0" -cw-storage-plus = "1.0.1" +cw-storage-plus = "1.1.0" cw-utils = "1.0.1" -mars-owner = { version = "1.2.0", features = ["emergency-owner"] } +mars-owner = { version = "2.0.0", features = ["emergency-owner"] } osmosis-std = "0.16.1" -prost = { version = "0.11.5", default-features = false, features = ["prost-derive"] } +prost = { version = "0.11.9", default-features = false, features = ["prost-derive"] } schemars = "0.8.12" -serde = { version = "1.0.163", default-features = false, features = ["derive"] } -thiserror = "1.0.40" -pyth-sdk-cw = "1.2.0" +serde = { version = "1.0.181", default-features = false, features = ["derive"] } +thiserror = "1.0.44" +pyth-sdk-cw = "1.2.0" cw-paginate = "0.2.1" astroport = "2.8.0" -strum = "0.24.1" -neutron-sdk = "0.6.0" +strum = "0.25.0" +neutron-sdk = "0.6.1" serde_json = "1.0" # dev-dependencies cw-multi-test = "0.16.5" cw-it = "0.1.0" osmosis-test-tube = "=16.1.1" -test-case = "3.0.0" -proptest = "1.1.0" +test-case = "3.1.0" +proptest = "1.2.0" # packages mars-health = { path = "./packages/health" } diff --git a/Makefile.toml b/Makefile.toml index 0f985ef21..a27c7d1ae 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -10,7 +10,7 @@ default_to_workspace = false # Directory with wasm files used by integration tests (another directory can be used instead, for example 'artifacts' from rust-optimizer) ARTIFACTS_DIR_PATH = "target/wasm32-unknown-unknown/release" # If you bump this version, verify RUST_VERSION correctness -RUST_OPTIMIZER_VERSION = "0.12.13" +RUST_OPTIMIZER_VERSION = "0.13.0" # Use rust version from rust-optimizer Dockerfile (see https://github.com/CosmWasm/rust-optimizer/blob/main/Dockerfile#L1) # to be sure that we compile / test against the same version RUST_VERSION = "1.69.0" @@ -44,7 +44,7 @@ else image="cosmwasm/workspace-optimizer:${RUST_OPTIMIZER_VERSION}" fi docker run --rm -v "$(pwd)":/code \ - --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ + --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \ --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ ${image} """ From e50415395f8be3b7910da9aa0895883f9d91c694 Mon Sep 17 00:00:00 2001 From: piobab Date: Mon, 7 Aug 2023 00:42:56 +0200 Subject: [PATCH 22/43] Add routes setup. (#298) --- scripts/deploy/base/index.ts | 2 +- scripts/deploy/osmosis/testnetConfig.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/deploy/base/index.ts b/scripts/deploy/base/index.ts index 5dedb8547..ef812eb5d 100644 --- a/scripts/deploy/base/index.ts +++ b/scripts/deploy/base/index.ts @@ -52,7 +52,7 @@ export const taskRunner = async (config: DeploymentConfig) => { await deployer.executeBorrow() await deployer.executeRepay() await deployer.executeWithdraw() - await deployer.executeRewardsSwap() + // await deployer.executeRewardsSwap() } if (config.multisigAddr) { diff --git a/scripts/deploy/osmosis/testnetConfig.ts b/scripts/deploy/osmosis/testnetConfig.ts index 35e554703..b920aa280 100644 --- a/scripts/deploy/osmosis/testnetConfig.ts +++ b/scripts/deploy/osmosis/testnetConfig.ts @@ -198,6 +198,9 @@ export const osmosisTestnetConfig: DeploymentConfig = { safetyFundDenom: aUSDC, swapRoutes: [ { denom_in: atom, denom_out: uosmo, route: [{ pool_id: 12, token_out_denom: uosmo }] }, + { denom_in: uosmo, denom_out: atom, route: [{ pool_id: 12, token_out_denom: atom }] }, + { denom_in: aUSDC, denom_out: uosmo, route: [{ pool_id: 5, token_out_denom: uosmo }] }, + { denom_in: uosmo, denom_out: aUSDC, route: [{ pool_id: 5, token_out_denom: aUSDC }] }, ], safetyFundAddr: safetyFundAddr, protocolAdminAddr: protocolAdminAddr, From 1b4b3f7feebcf0e228cb92ba06ac5214437a53fe Mon Sep 17 00:00:00 2001 From: piobab Date: Mon, 7 Aug 2023 18:07:24 +0200 Subject: [PATCH 23/43] Fix reclaim (#299) * Fix reclaim. * Add test for deposit and withdraw for credit manager. --- contracts/red-bank/src/borrow.rs | 1 + contracts/red-bank/src/collateral.rs | 1 + contracts/red-bank/src/contract.rs | 15 +- contracts/red-bank/src/health.rs | 23 ++- contracts/red-bank/src/liquidate.rs | 3 +- contracts/red-bank/src/query.rs | 3 + contracts/red-bank/src/user.rs | 5 +- contracts/red-bank/src/withdraw.rs | 4 +- .../red-bank/tests/test_credit_accounts.rs | 174 ++++++++++++++++++ contracts/red-bank/tests/test_misc.rs | 1 + packages/testing/src/integration/mock_env.rs | 10 + packages/testing/src/red_bank_querier.rs | 1 + packages/types/src/red_bank/msg.rs | 2 + schemas/mars-red-bank/mars-red-bank.json | 12 ++ .../mars-red-bank/MarsRedBank.client.ts | 28 ++- .../mars-red-bank/MarsRedBank.react-query.ts | 4 + .../mars-red-bank/MarsRedBank.types.ts | 2 + 17 files changed, 272 insertions(+), 17 deletions(-) create mode 100644 contracts/red-bank/tests/test_credit_accounts.rs diff --git a/contracts/red-bank/src/borrow.rs b/contracts/red-bank/src/borrow.rs index 527f2002f..ad57b1a14 100644 --- a/contracts/red-bank/src/borrow.rs +++ b/contracts/red-bank/src/borrow.rs @@ -75,6 +75,7 @@ pub fn borrow( &deps.as_ref(), &env, borrower.address(), + "", oracle_addr, params_addr, &denom, diff --git a/contracts/red-bank/src/collateral.rs b/contracts/red-bank/src/collateral.rs index 4f7577182..9e6fc65ea 100644 --- a/contracts/red-bank/src/collateral.rs +++ b/contracts/red-bank/src/collateral.rs @@ -50,6 +50,7 @@ pub fn update_asset_collateral_status( &deps.as_ref(), &env, user.address(), + "", oracle_addr, params_addr, false, diff --git a/contracts/red-bank/src/contract.rs b/contracts/red-bank/src/contract.rs index 9904d3034..f0e5de7f9 100644 --- a/contracts/red-bank/src/contract.rs +++ b/contracts/red-bank/src/contract.rs @@ -1,4 +1,6 @@ -use cosmwasm_std::{entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response}; +use cosmwasm_std::{ + entry_point, to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult, +}; use mars_red_bank_types::red_bank::{ExecuteMsg, InstantiateMsg, QueryMsg}; use crate::{ @@ -177,15 +179,17 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { let user_addr = deps.api.addr_validate(&user)?; - to_binary(&query::query_user_position(deps, env, user_addr, false)?) + to_binary(&query::query_user_position(deps, env, user_addr, account_id, false)?) } QueryMsg::UserPositionLiquidationPricing { user, + account_id, } => { let user_addr = deps.api.addr_validate(&user)?; - to_binary(&query::query_user_position(deps, env, user_addr, true)?) + to_binary(&query::query_user_position(deps, env, user_addr, account_id, true)?) } QueryMsg::ScaledLiquidityAmount { denom, @@ -206,3 +210,8 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result StdResult { + Ok(Response::default()) +} diff --git a/contracts/red-bank/src/health.rs b/contracts/red-bank/src/health.rs index 7909905a0..c22854353 100644 --- a/contracts/red-bank/src/health.rs +++ b/contracts/red-bank/src/health.rs @@ -16,12 +16,20 @@ pub fn get_health_and_positions( deps: &Deps, env: &Env, user_addr: &Addr, + account_id: &str, oracle_addr: &Addr, params_addr: &Addr, is_liquidation: bool, ) -> Result<(Health, HashMap), ContractError> { - let positions = - get_user_positions_map(deps, env, user_addr, oracle_addr, params_addr, is_liquidation)?; + let positions = get_user_positions_map( + deps, + env, + user_addr, + account_id, + oracle_addr, + params_addr, + is_liquidation, + )?; let health = compute_position_health(&positions)?; Ok((health, positions)) @@ -32,13 +40,14 @@ pub fn assert_below_liq_threshold_after_withdraw( deps: &Deps, env: &Env, user_addr: &Addr, + account_id: &str, oracle_addr: &Addr, params_addr: &Addr, denom: &str, withdraw_amount: Uint128, ) -> Result { let mut positions = - get_user_positions_map(deps, env, user_addr, oracle_addr, params_addr, false)?; + get_user_positions_map(deps, env, user_addr, account_id, oracle_addr, params_addr, false)?; // Update position to compute health factor after withdraw match positions.get_mut(denom) { Some(p) => { @@ -56,13 +65,14 @@ pub fn assert_below_max_ltv_after_borrow( deps: &Deps, env: &Env, user_addr: &Addr, + account_id: &str, oracle_addr: &Addr, params_addr: &Addr, denom: &str, borrow_amount: Uint128, ) -> Result { let mut positions = - get_user_positions_map(deps, env, user_addr, oracle_addr, params_addr, false)?; + get_user_positions_map(deps, env, user_addr, account_id, oracle_addr, params_addr, false)?; // Update position to compute health factor after borrow positions @@ -113,6 +123,7 @@ pub fn get_user_positions_map( deps: &Deps, env: &Env, user_addr: &Addr, + account_id: &str, oracle_addr: &Addr, params_addr: &Addr, is_liquidation: bool, @@ -121,7 +132,7 @@ pub fn get_user_positions_map( // Find all denoms that the user has a collateral or debt position in let collateral_denoms = COLLATERALS - .prefix((user_addr, "")) + .prefix((user_addr, account_id)) .keys(deps.storage, None, None, Order::Ascending) .collect::>>()?; let debt_denoms = DEBTS @@ -143,7 +154,7 @@ pub fn get_user_positions_map( let params = query_asset_params(&deps.querier, params_addr, &denom)?; let collateral_amount = - match COLLATERALS.may_load(deps.storage, (user_addr, "", &denom))? { + match COLLATERALS.may_load(deps.storage, (user_addr, account_id, &denom))? { Some(collateral) if collateral.enabled => { let amount_scaled = collateral.amount_scaled; get_underlying_liquidity_amount(amount_scaled, &market, block_time)? diff --git a/contracts/red-bank/src/liquidate.rs b/contracts/red-bank/src/liquidate.rs index b29f80a94..ff130947d 100644 --- a/contracts/red-bank/src/liquidate.rs +++ b/contracts/red-bank/src/liquidate.rs @@ -89,6 +89,7 @@ pub fn liquidate( &deps.as_ref(), &env, &liquidatee_addr, + "", oracle_addr, params_addr, true, @@ -255,7 +256,7 @@ fn assert_liq_threshold( prev_health: &Health, ) -> Result<(), ContractError> { let (new_health, _) = - get_health_and_positions(deps, env, user_addr, oracle_addr, params_addr, true)?; + get_health_and_positions(deps, env, user_addr, "", oracle_addr, params_addr, true)?; // liquidation_health_factor = None only if debt = 0 but liquidation is not possible match (prev_health.liquidation_health_factor, new_health.liquidation_health_factor) { diff --git a/contracts/red-bank/src/query.rs b/contracts/red-bank/src/query.rs index 1bca591e3..31e92a16b 100644 --- a/contracts/red-bank/src/query.rs +++ b/contracts/red-bank/src/query.rs @@ -252,6 +252,7 @@ pub fn query_user_position( deps: Deps, env: Env, user_addr: Addr, + account_id: Option, liquidation_pricing: bool, ) -> Result { let config = CONFIG.load(deps.storage)?; @@ -264,10 +265,12 @@ pub fn query_user_position( let oracle_addr = &addresses[&MarsAddressType::Oracle]; let params_addr = &addresses[&MarsAddressType::Params]; + let acc_id = account_id.unwrap_or("".to_string()); let positions = health::get_user_positions_map( &deps, &env, &user_addr, + &acc_id, oracle_addr, params_addr, liquidation_pricing, diff --git a/contracts/red-bank/src/user.rs b/contracts/red-bank/src/user.rs index dbee6f8df..39ad2311e 100644 --- a/contracts/red-bank/src/user.rs +++ b/contracts/red-bank/src/user.rs @@ -52,10 +52,9 @@ impl<'a> User<'a> { &self, store: &dyn Storage, denom: &str, - account_id: Option, + account_id: &str, ) -> StdResult { - let acc_id = account_id.unwrap_or("".to_string()); - COLLATERALS.load(store, (self.0, &acc_id, denom)) + COLLATERALS.load(store, (self.0, account_id, denom)) } /// Load the user's debt diff --git a/contracts/red-bank/src/withdraw.rs b/contracts/red-bank/src/withdraw.rs index a9da5ce72..a25cf747d 100644 --- a/contracts/red-bank/src/withdraw.rs +++ b/contracts/red-bank/src/withdraw.rs @@ -21,10 +21,11 @@ pub fn withdraw( account_id: Option, ) -> Result { let withdrawer = User(&info.sender); + let acc_id = account_id.clone().unwrap_or("".to_string()); let mut market = MARKETS.load(deps.storage, &denom)?; - let collateral = withdrawer.collateral(deps.storage, &denom, account_id.clone())?; + let collateral = withdrawer.collateral(deps.storage, &denom, &acc_id)?; let withdrawer_balance_scaled_before = collateral.amount_scaled; if withdrawer_balance_scaled_before.is_zero() { @@ -77,6 +78,7 @@ pub fn withdraw( &deps.as_ref(), &env, withdrawer.address(), + &acc_id, oracle_addr, params_addr, &denom, diff --git a/contracts/red-bank/tests/test_credit_accounts.rs b/contracts/red-bank/tests/test_credit_accounts.rs new file mode 100644 index 000000000..84ae85caa --- /dev/null +++ b/contracts/red-bank/tests/test_credit_accounts.rs @@ -0,0 +1,174 @@ +use std::str::FromStr; + +use cosmwasm_std::{coin, Addr, Decimal, Uint128}; +use helpers::assert_err; +use mars_params::types::asset::{AssetParams, CmSettings, LiquidationBonus, RedBankSettings}; +use mars_red_bank::error::ContractError; +use mars_red_bank_types::red_bank::{InitOrUpdateAssetParams, InterestRateModel, UserHealthStatus}; +use mars_testing::integration::mock_env::MockEnvBuilder; + +mod helpers; + +#[test] +fn deposit_and_withdraw_for_credit_account_works() { + let owner = Addr::unchecked("owner"); + let mut mock_env = MockEnvBuilder::new(None, owner.clone()).build(); + + let red_bank = mock_env.red_bank.clone(); + let params = mock_env.params.clone(); + let oracle = mock_env.oracle.clone(); + + let funded_amt = 1_000_000_000_000u128; + let provider = Addr::unchecked("provider"); // provides collateral to be borrowed by others + let credit_manager = Addr::unchecked("credit_manager"); + let account_id = "111".to_string(); + + // setup red-bank + let (market_params, asset_params) = osmo_asset_params(); + red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = usdc_asset_params(); + red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params); + params.init_params(&mut mock_env, asset_params); + + // setup oracle + oracle.set_price_source_fixed(&mut mock_env, "uosmo", Decimal::one()); + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(2u128, 1u128)); + + // fund accounts + mock_env.fund_accounts(&[&provider, &credit_manager], funded_amt, &["uosmo", "uusdc"]); + + // provider deposits collaterals + red_bank.deposit(&mut mock_env, &provider, coin(1000000000, "uusdc")).unwrap(); + + // credit manager deposits + let cm_osmo_deposit_amt = 100000000u128; + red_bank + .deposit_with_acc_id( + &mut mock_env, + &credit_manager, + coin(cm_osmo_deposit_amt, "uosmo"), + Some(account_id.clone()), + ) + .unwrap(); + + // credit manager try to borrow if no credit line set + let error_res = red_bank.borrow(&mut mock_env, &credit_manager, "uusdc", 100000000); + assert_err(error_res, ContractError::BorrowAmountExceedsGivenCollateral {}); + + // update credit line for credit manager + red_bank + .update_uncollateralized_loan_limit( + &mut mock_env, + &owner, + &credit_manager, + "uusdc", + Uint128::MAX, + ) + .unwrap(); + + // credit manager should be able to borrow + let cm_usdc_borrow_amt = 100000000u128; + red_bank.borrow(&mut mock_env, &credit_manager, "uusdc", cm_usdc_borrow_amt).unwrap(); + + // collateral is not tracked for credit manager (it is per account id). Debt is tracked for credit manager as a whole (not per account id) + let cm_collaterals = red_bank.query_user_collaterals(&mut mock_env, &credit_manager); + assert!(cm_collaterals.is_empty()); + let cm_debts = red_bank.query_user_debts(&mut mock_env, &credit_manager); + assert_eq!(cm_debts.len(), 1); + let cm_usdc_debt = cm_debts.get("uusdc").unwrap(); + assert!(cm_usdc_debt.uncollateralized); + assert_eq!(cm_usdc_debt.amount.u128(), cm_usdc_borrow_amt); + let cm_position = red_bank.query_user_position(&mut mock_env, &credit_manager); + assert!(cm_position.total_enabled_collateral.is_zero()); + assert!(cm_position.total_collateralized_debt.is_zero()); + assert_eq!(cm_position.health_status, UserHealthStatus::NotBorrowing); + + // collateral is tracked for credit manager account id. Debt is not tracked per account id + let cm_collaterals = red_bank.query_user_collaterals_with_acc_id( + &mut mock_env, + &credit_manager, + Some(account_id.clone()), + ); + assert_eq!(cm_collaterals.len(), 1); + let cm_osmo_collateral = cm_collaterals.get("uosmo").unwrap(); + assert_eq!(cm_osmo_collateral.amount.u128(), cm_osmo_deposit_amt); + let cm_position = red_bank.query_user_position_with_acc_id( + &mut mock_env, + &credit_manager, + Some(account_id.clone()), + ); + assert_eq!(cm_position.total_enabled_collateral.u128(), cm_osmo_deposit_amt); + assert!(cm_position.total_collateralized_debt.is_zero()); + assert_eq!(cm_position.health_status, UserHealthStatus::NotBorrowing); + + // withdraw total collateral for account id + red_bank + .withdraw_with_acc_id( + &mut mock_env, + &credit_manager, + "uosmo", + None, + Some(account_id.clone()), + ) + .unwrap(); + + // check collaterals and debts for credit manager account id after withdraw + let cm_collaterals = red_bank.query_user_collaterals_with_acc_id( + &mut mock_env, + &credit_manager, + Some(account_id.clone()), + ); + assert!(cm_collaterals.is_empty()); + let cm_position = + red_bank.query_user_position_with_acc_id(&mut mock_env, &credit_manager, Some(account_id)); + assert!(cm_position.total_enabled_collateral.is_zero()); + assert!(cm_position.total_collateralized_debt.is_zero()); + assert_eq!(cm_position.health_status, UserHealthStatus::NotBorrowing); +} + +fn osmo_asset_params() -> (InitOrUpdateAssetParams, AssetParams) { + default_asset_params_with("uosmo", Decimal::percent(70), Decimal::percent(78)) +} + +fn usdc_asset_params() -> (InitOrUpdateAssetParams, AssetParams) { + default_asset_params_with("uusdc", Decimal::percent(90), Decimal::percent(96)) +} + +fn default_asset_params_with( + denom: &str, + max_loan_to_value: Decimal, + liquidation_threshold: Decimal, +) -> (InitOrUpdateAssetParams, AssetParams) { + let market_params = InitOrUpdateAssetParams { + reserve_factor: Some(Decimal::percent(20)), + interest_rate_model: Some(InterestRateModel { + optimal_utilization_rate: Decimal::percent(10), + base: Decimal::percent(30), + slope_1: Decimal::percent(25), + slope_2: Decimal::percent(30), + }), + }; + let asset_params = AssetParams { + denom: denom.to_string(), + credit_manager: CmSettings { + whitelisted: false, + hls: None, + }, + red_bank: RedBankSettings { + deposit_enabled: true, + borrow_enabled: true, + }, + max_loan_to_value, + liquidation_threshold, + liquidation_bonus: LiquidationBonus { + starting_lb: Decimal::percent(1), + slope: Decimal::from_str("2.0").unwrap(), + min_lb: Decimal::percent(2), + max_lb: Decimal::percent(10), + }, + protocol_liquidation_fee: Decimal::percent(2), + deposit_cap: Uint128::MAX, + }; + (market_params, asset_params) +} diff --git a/contracts/red-bank/tests/test_misc.rs b/contracts/red-bank/tests/test_misc.rs index 870ee7360..bdee41932 100644 --- a/contracts/red-bank/tests/test_misc.rs +++ b/contracts/red-bank/tests/test_misc.rs @@ -358,6 +358,7 @@ fn update_asset_collateral() { &deps.as_ref(), &env, &user_addr, + "", &Addr::unchecked("oracle"), &Addr::unchecked("params"), false, diff --git a/packages/testing/src/integration/mock_env.rs b/packages/testing/src/integration/mock_env.rs index 8f160abb1..642ff1091 100644 --- a/packages/testing/src/integration/mock_env.rs +++ b/packages/testing/src/integration/mock_env.rs @@ -600,12 +600,22 @@ impl RedBank { } pub fn query_user_position(&self, env: &mut MockEnv, user: &Addr) -> UserPositionResponse { + self.query_user_position_with_acc_id(env, user, None) + } + + pub fn query_user_position_with_acc_id( + &self, + env: &mut MockEnv, + user: &Addr, + account_id: Option, + ) -> UserPositionResponse { env.app .wrap() .query_wasm_smart( self.contract_addr.clone(), &red_bank::QueryMsg::UserPosition { user: user.to_string(), + account_id, }, ) .unwrap() diff --git a/packages/testing/src/red_bank_querier.rs b/packages/testing/src/red_bank_querier.rs index 6c5a1fca7..05902a61c 100644 --- a/packages/testing/src/red_bank_querier.rs +++ b/packages/testing/src/red_bank_querier.rs @@ -39,6 +39,7 @@ impl RedBankQuerier { }, QueryMsg::UserPosition { user, + account_id: _, } => match self.users_positions.get(&user) { Some(market) => to_binary(&market).into(), None => Err(format!("[mock]: could not find the position for {user}")).into(), diff --git a/packages/types/src/red_bank/msg.rs b/packages/types/src/red_bank/msg.rs index b206e4b1a..e18551ae5 100644 --- a/packages/types/src/red_bank/msg.rs +++ b/packages/types/src/red_bank/msg.rs @@ -196,12 +196,14 @@ pub enum QueryMsg { #[returns(crate::red_bank::UserPositionResponse)] UserPosition { user: String, + account_id: Option, }, /// Get user position for liquidation #[returns(crate::red_bank::UserPositionResponse)] UserPositionLiquidationPricing { user: String, + account_id: Option, }, /// Get liquidity scaled amount for a given underlying asset amount. diff --git a/schemas/mars-red-bank/mars-red-bank.json b/schemas/mars-red-bank/mars-red-bank.json index 084a66da0..f36ad40b2 100644 --- a/schemas/mars-red-bank/mars-red-bank.json +++ b/schemas/mars-red-bank/mars-red-bank.json @@ -829,6 +829,12 @@ "user" ], "properties": { + "account_id": { + "type": [ + "string", + "null" + ] + }, "user": { "type": "string" } @@ -851,6 +857,12 @@ "user" ], "properties": { + "account_id": { + "type": [ + "string", + "null" + ] + }, "user": { "type": "string" } diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts index fe9cb5288..cf6e4d333 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts @@ -86,8 +86,20 @@ export interface MarsRedBankReadOnlyInterface { startAfter?: string user: string }) => Promise - userPosition: ({ user }: { user: string }) => Promise - userPositionLiquidationPricing: ({ user }: { user: string }) => Promise + userPosition: ({ + accountId, + user, + }: { + accountId?: string + user: string + }) => Promise + userPositionLiquidationPricing: ({ + accountId, + user, + }: { + accountId?: string + user: string + }) => Promise scaledLiquidityAmount: ({ amount, denom }: { amount: Uint128; denom: string }) => Promise scaledDebtAmount: ({ amount, denom }: { amount: Uint128; denom: string }) => Promise underlyingLiquidityAmount: ({ @@ -254,20 +266,30 @@ export class MarsRedBankQueryClient implements MarsRedBankReadOnlyInterface { }, }) } - userPosition = async ({ user }: { user: string }): Promise => { + userPosition = async ({ + accountId, + user, + }: { + accountId?: string + user: string + }): Promise => { return this.client.queryContractSmart(this.contractAddress, { user_position: { + account_id: accountId, user, }, }) } userPositionLiquidationPricing = async ({ + accountId, user, }: { + accountId?: string user: string }): Promise => { return this.client.queryContractSmart(this.contractAddress, { user_position_liquidation_pricing: { + account_id: accountId, user, }, }) diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts index 981852c24..4579f2509 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts @@ -234,6 +234,7 @@ export function useMarsRedBankScaledLiquidityAmountQuery({ export interface MarsRedBankUserPositionLiquidationPricingQuery extends MarsRedBankReactQuery { args: { + accountId?: string user: string } } @@ -247,6 +248,7 @@ export function useMarsRedBankUserPositionLiquidationPricingQuery client ? client.userPositionLiquidationPricing({ + accountId: args.accountId, user: args.user, }) : Promise.reject(new Error('Invalid client')), @@ -256,6 +258,7 @@ export function useMarsRedBankUserPositionLiquidationPricingQuery extends MarsRedBankReactQuery { args: { + accountId?: string user: string } } @@ -269,6 +272,7 @@ export function useMarsRedBankUserPositionQuery({ () => client ? client.userPosition({ + accountId: args.accountId, user: args.user, }) : Promise.reject(new Error('Invalid client')), diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts index 21b483d57..6c177061b 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts @@ -163,11 +163,13 @@ export type QueryMsg = } | { user_position: { + account_id?: string | null user: string } } | { user_position_liquidation_pricing: { + account_id?: string | null user: string } } From 07afd54061a02f1b0e8ee38610b9524167986de2 Mon Sep 17 00:00:00 2001 From: larry <26318510+larry0x@users.noreply.github.com> Date: Tue, 8 Aug 2023 13:03:38 +0100 Subject: [PATCH 24/43] Clean up (#300) --- Cargo.lock | 72 +++++++------------ Cargo.toml | 34 ++++----- contracts/address-provider/Cargo.toml | 17 ++--- contracts/incentives/Cargo.toml | 5 +- contracts/mock-pyth/Cargo.toml | 1 + contracts/oracle/base/Cargo.toml | 7 +- contracts/oracle/osmosis/Cargo.toml | 9 +-- contracts/oracle/wasm/Cargo.toml | 17 ++--- contracts/params/Cargo.toml | 5 +- contracts/red-bank/Cargo.toml | 7 +- contracts/rewards-collector/base/Cargo.toml | 19 +---- .../rewards-collector/neutron/Cargo.toml | 36 +++------- .../rewards-collector/osmosis/Cargo.toml | 32 +++------ contracts/swapper/astroport/Cargo.toml | 20 +++--- contracts/swapper/base/Cargo.toml | 5 +- contracts/swapper/mock/Cargo.toml | 5 +- contracts/swapper/osmosis/Cargo.toml | 8 +-- integration-tests/Cargo.toml | 14 ++-- packages/liquidation/Cargo.toml | 8 +-- packages/testing/Cargo.toml | 13 ++-- packages/types/Cargo.toml | 2 +- schemas/mars-params/mars-params.json | 2 +- 22 files changed, 114 insertions(+), 224 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f5f0b5b99..5fb78db7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -527,9 +527,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.79" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +dependencies = [ + "libc", +] [[package]] name = "cexpr" @@ -1897,7 +1900,7 @@ dependencies = [ [[package]] name = "mars-params" -version = "1.0.7" +version = "1.2.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -1910,8 +1913,6 @@ dependencies = [ "mars-red-bank-types", "mars-testing", "mars-utils", - "schemars", - "serde", "test-case", "thiserror", ] @@ -1959,12 +1960,9 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.1.0", - "mars-osmosis", "mars-owner", "mars-red-bank-types", - "mars-testing", "mars-utils", - "osmosis-std", "schemars", "serde", "thiserror", @@ -1974,30 +1972,18 @@ dependencies = [ name = "mars-rewards-collector-neutron" version = "1.2.0" dependencies = [ - "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 1.1.0", "cw2 1.1.0", - "mars-osmosis", - "mars-owner", "mars-red-bank-types", "mars-rewards-collector-base", - "mars-testing", - "mars-utils", "neutron-sdk", - "osmosis-std", - "schemars", - "serde", - "thiserror", ] [[package]] name = "mars-rewards-collector-osmosis" version = "1.2.0" dependencies = [ - "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 1.1.0", "cw2 1.1.0", "mars-osmosis", "mars-owner", @@ -2006,9 +1992,7 @@ dependencies = [ "mars-testing", "mars-utils", "osmosis-std", - "schemars", "serde", - "thiserror", ] [[package]] @@ -2047,9 +2031,7 @@ dependencies = [ name = "mars-swapper-mock" version = "1.2.0" dependencies = [ - "anyhow", "cosmwasm-std", - "cw-multi-test", "mars-red-bank-types", ] @@ -2409,18 +2391,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", @@ -2429,9 +2411,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" +checksum = "2c516611246607d0c04186886dbb3a754368ef82c79e9827a802c6d836dd111c" [[package]] name = "pin-utils" @@ -2698,9 +2680,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" dependencies = [ "aho-corasick", "memchr", @@ -2710,9 +2692,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" dependencies = [ "aho-corasick", "memchr", @@ -2812,9 +2794,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.4" +version = "0.38.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +checksum = "172891ebdceb05aa0005f533a6cbfca599ddd7d966f6f5d4d9b2e70478e70399" dependencies = [ "bitflags 2.3.3", "errno", @@ -2969,9 +2951,9 @@ checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" [[package]] name = "serde" -version = "1.0.181" +version = "1.0.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d3e73c93c3240c0bda063c239298e633114c69a888c3e37ca8bb33f343e9890" +checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" dependencies = [ "serde_derive", ] @@ -3005,9 +2987,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.181" +version = "1.0.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be02f6cb0cd3a5ec20bbcfbcbd749f57daddb1a0882dc2e46a6c236c90b977ed" +checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" dependencies = [ "proc-macro2", "quote", @@ -3191,7 +3173,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" dependencies = [ - "strum_macros 0.25.1", + "strum_macros 0.25.2", ] [[package]] @@ -3209,9 +3191,9 @@ dependencies = [ [[package]] name = "strum_macros" -version = "0.25.1" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6069ca09d878a33f883cc06aaa9718ede171841d3832450354410b718b097232" +checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" dependencies = [ "heck", "proc-macro2", @@ -3259,9 +3241,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.7.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" +checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651" dependencies = [ "cfg-if", "fastrand", diff --git a/Cargo.toml b/Cargo.toml index 8955b9a95..8df05ee17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,44 +30,39 @@ authors = [ "Ahmad Kaouk", "Harry Scholes", ] -edition = "2021" -license = "GPL-3.0-or-later" -repository = "https://github.com/mars-protocol/red-bank" -homepage = "https://marsprotocol.io" +edition = "2021" +license = "GPL-3.0-or-later" +repository = "https://github.com/mars-protocol/red-bank" +homepage = "https://marsprotocol.io" documentation = "https://docs.marsprotocol.io/" -keywords = [ - "mars", - "cosmos", - "cosmwasm", -] +keywords = ["mars", "cosmos", "cosmwasm"] [workspace.dependencies] anyhow = "1.0.72" +astroport = "2.8.0" bech32 = "0.9.1" cosmwasm-schema = "1.3.1" cosmwasm-std = "1.3.1" cw2 = "1.1.0" +cw-paginate = "0.2.1" cw-storage-plus = "1.1.0" cw-utils = "1.0.1" mars-owner = { version = "2.0.0", features = ["emergency-owner"] } +neutron-sdk = "0.6.1" osmosis-std = "0.16.1" -prost = { version = "0.11.9", default-features = false, features = ["prost-derive"] } -schemars = "0.8.12" -serde = { version = "1.0.181", default-features = false, features = ["derive"] } -thiserror = "1.0.44" +prost = { version = "0.11.9", default-features = false } pyth-sdk-cw = "1.2.0" -cw-paginate = "0.2.1" -astroport = "2.8.0" +schemars = "0.8.12" +serde = { version = "1.0.181", default-features = false } strum = "0.25.0" -neutron-sdk = "0.6.1" -serde_json = "1.0" +thiserror = "1.0.44" # dev-dependencies cw-multi-test = "0.16.5" cw-it = "0.1.0" osmosis-test-tube = "=16.1.1" -test-case = "3.1.0" proptest = "1.2.0" +test-case = "3.1.0" # packages mars-health = { path = "./packages/health" } @@ -88,9 +83,10 @@ mars-oracle-wasm = { path = "./contracts/oracle/wasm" } mars-params = { path = "./contracts/params" } mars-red-bank = { path = "./contracts/red-bank" } mars-rewards-collector-base = { path = "./contracts/rewards-collector/base" } +mars-rewards-collector-neutron = { path = "./contracts/rewards-collector/neutron" } mars-rewards-collector-osmosis = { path = "./contracts/rewards-collector/osmosis" } -mars-swapper-base = { path = "./contracts/swapper/base" } mars-swapper-astroport = { path = "./contracts/swapper/astroport" } +mars-swapper-base = { path = "./contracts/swapper/base" } mars-swapper-osmosis = { path = "./contracts/swapper/osmosis" } [profile.release] diff --git a/contracts/address-provider/Cargo.toml b/contracts/address-provider/Cargo.toml index cc8b186cd..373b90e19 100644 --- a/contracts/address-provider/Cargo.toml +++ b/contracts/address-provider/Cargo.toml @@ -11,22 +11,13 @@ documentation = { workspace = true } keywords = { workspace = true } [lib] -crate-type = [ - "cdylib", - "rlib", -] -doctest = false - -[profile.release] -overflow-checks = true +crate-type = ["cdylib", "rlib"] +doctest = false [features] # for more explicit tests, cargo test --features=backtraces -backtraces = [ - "cosmwasm-std/backtraces", -] -library = [ -] +backtraces = ["cosmwasm-std/backtraces"] +library = [] [dependencies] bech32 = { workspace = true } diff --git a/contracts/incentives/Cargo.toml b/contracts/incentives/Cargo.toml index 9dae3dccd..68d95a662 100644 --- a/contracts/incentives/Cargo.toml +++ b/contracts/incentives/Cargo.toml @@ -12,10 +12,7 @@ keywords = { workspace = true } [lib] crate-type = ["cdylib", "rlib"] -doctest = false - -[profile.release] -overflow-checks = true +doctest = false [features] # for more explicit tests, cargo test --features=backtraces diff --git a/contracts/mock-pyth/Cargo.toml b/contracts/mock-pyth/Cargo.toml index 85b62884f..0c4a6fd25 100644 --- a/contracts/mock-pyth/Cargo.toml +++ b/contracts/mock-pyth/Cargo.toml @@ -12,6 +12,7 @@ keywords = { workspace = true } [lib] crate-type = ["cdylib", "rlib"] +doctest = false [features] # for quicker tests, cargo test --lib diff --git a/contracts/oracle/base/Cargo.toml b/contracts/oracle/base/Cargo.toml index eea829f89..5afd4108f 100644 --- a/contracts/oracle/base/Cargo.toml +++ b/contracts/oracle/base/Cargo.toml @@ -12,13 +12,10 @@ keywords = { workspace = true } [lib] doctest = false -[profile.release] -overflow-checks = true - [features] -pyth = ["pyth-sdk-cw"] # for more explicit tests, cargo test --features=backtraces backtraces = ["cosmwasm-std/backtraces"] +pyth = ["pyth-sdk-cw"] [dependencies] cosmwasm-std = { workspace = true } @@ -26,7 +23,7 @@ cw2 = { workspace = true } cw-storage-plus = { workspace = true } mars-owner = { workspace = true } mars-red-bank-types = { workspace = true } +pyth-sdk-cw = { workspace = true, optional = true } schemars = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } -pyth-sdk-cw = { workspace = true, optional = true } diff --git a/contracts/oracle/osmosis/Cargo.toml b/contracts/oracle/osmosis/Cargo.toml index bcabeb060..47f0829b9 100644 --- a/contracts/oracle/osmosis/Cargo.toml +++ b/contracts/oracle/osmosis/Cargo.toml @@ -12,10 +12,7 @@ keywords = { workspace = true } [lib] crate-type = ["cdylib", "rlib"] -doctest = false - -[profile.release] -overflow-checks = true +doctest = false [features] # for more explicit tests, cargo test --features=backtraces @@ -26,9 +23,9 @@ cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw2 = { workspace = true } cw-storage-plus = { workspace = true } -mars-owner = { workspace = true } mars-oracle-base = { workspace = true, features = ["pyth"] } mars-osmosis = { workspace = true } +mars-owner = { workspace = true } mars-red-bank-types = { workspace = true } osmosis-std = { workspace = true } pyth-sdk-cw = { workspace = true } @@ -37,7 +34,7 @@ serde = { workspace = true } [dev-dependencies] cosmwasm-schema = { workspace = true } -mars-testing = { workspace = true } mars-owner = { workspace = true } +mars-testing = { workspace = true } mars-utils = { workspace = true } pyth-sdk-cw = { workspace = true } diff --git a/contracts/oracle/wasm/Cargo.toml b/contracts/oracle/wasm/Cargo.toml index eda692995..1b293cb07 100644 --- a/contracts/oracle/wasm/Cargo.toml +++ b/contracts/oracle/wasm/Cargo.toml @@ -14,29 +14,26 @@ keywords = { workspace = true } crate-type = ["cdylib", "rlib"] doctest = false -[profile.release] -overflow-checks = true - [features] # for more explicit tests, cargo test --features=backtraces -backtraces = ["cosmwasm-std/backtraces"] -library = [] +backtraces = ["cosmwasm-std/backtraces"] +library = [] osmosis-test-app = ["cw-it/osmosis-test-tube", "mars-testing/osmosis-test-tube"] [dependencies] +astroport = { workspace = true } +cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw2 = { workspace = true } cw-storage-plus = { workspace = true } mars-oracle-base = { workspace = true, features = ["pyth"] } mars-red-bank-types = { workspace = true } -cosmwasm-schema = { workspace = true } -astroport = { workspace = true } pyth-sdk-cw = { workspace = true } [dev-dependencies] cosmwasm-schema = { workspace = true } -mars-testing = { workspace = true, features = ["astroport"] } -mars-owner = { workspace = true } cw-it = { workspace = true, features = ["astroport", "astroport-multi-test"] } -test-case = { workspace = true } +mars-owner = { workspace = true } +mars-testing = { workspace = true, features = ["astroport"] } proptest = { workspace = true } +test-case = { workspace = true } diff --git a/contracts/params/Cargo.toml b/contracts/params/Cargo.toml index 88b963499..78ac63bee 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 = "1.0.7" +version = { workspace = true } authors = { workspace = true } license = { workspace = true } edition = { workspace = true } @@ -12,6 +12,7 @@ keywords = { workspace = true } [lib] crate-type = ["cdylib", "rlib"] +doctest = false [features] # for quicker tests, cargo test --lib @@ -28,8 +29,6 @@ mars-interest-rate = { workspace = true } mars-owner = { workspace = true } mars-red-bank-types = { workspace = true } mars-utils = { workspace = true } -schemars = { workspace = true } -serde = { workspace = true } thiserror = { workspace = true } [dev-dependencies] diff --git a/contracts/red-bank/Cargo.toml b/contracts/red-bank/Cargo.toml index 90c2a0e4c..9f0f9963a 100644 --- a/contracts/red-bank/Cargo.toml +++ b/contracts/red-bank/Cargo.toml @@ -12,10 +12,7 @@ keywords = { workspace = true } [lib] crate-type = ["cdylib", "rlib"] -doctest = false - -[profile.release] -overflow-checks = true +doctest = false [features] # for more explicit tests, cargo test --features=backtraces @@ -33,8 +30,8 @@ mars-owner = { workspace = true } mars-params = { workspace = true } mars-red-bank-types = { workspace = true } mars-utils = { workspace = true } -thiserror = { workspace = true } pyth-sdk-cw = { workspace = true } +thiserror = { workspace = true } [dev-dependencies] anyhow = { workspace = true } diff --git a/contracts/rewards-collector/base/Cargo.toml b/contracts/rewards-collector/base/Cargo.toml index 7054631c7..81a0ca230 100644 --- a/contracts/rewards-collector/base/Cargo.toml +++ b/contracts/rewards-collector/base/Cargo.toml @@ -10,20 +10,12 @@ documentation = { workspace = true } keywords = { workspace = true } [lib] -crate-type = [ - "cdylib", - "rlib", -] -doctest = false - -[profile.release] -overflow-checks = true +crate-type = ["cdylib", "rlib"] +doctest = false [features] # for more explicit tests, cargo test --features=backtraces -backtraces = [ - "cosmwasm-std/backtraces", -] +backtraces = ["cosmwasm-std/backtraces"] [dependencies] cosmwasm-schema = { workspace = true } @@ -35,8 +27,3 @@ mars-utils = { workspace = true } schemars = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } - -[dev-dependencies] -mars-osmosis = { workspace = true } -mars-testing = { workspace = true } -osmosis-std = { workspace = true } diff --git a/contracts/rewards-collector/neutron/Cargo.toml b/contracts/rewards-collector/neutron/Cargo.toml index b3165b981..c567a31f2 100644 --- a/contracts/rewards-collector/neutron/Cargo.toml +++ b/contracts/rewards-collector/neutron/Cargo.toml @@ -10,36 +10,16 @@ documentation = { workspace = true } keywords = { workspace = true } [lib] -crate-type = [ - "cdylib", - "rlib", -] -doctest = false - -[profile.release] -overflow-checks = true +crate-type = ["cdylib", "rlib"] +doctest = false [features] # for more explicit tests, cargo test --features=backtraces -backtraces = [ - "cosmwasm-std/backtraces", -] +backtraces = ["cosmwasm-std/backtraces"] [dependencies] -cosmwasm-schema = { workspace = true } -cosmwasm-std = { workspace = true, features = ["stargate"] } -cw2 = { workspace = true } -cw-storage-plus = { workspace = true } -mars-owner = { workspace = true } -mars-red-bank-types = { workspace = true } -mars-rewards-collector-base = { workspace = true } -mars-utils = { workspace = true } -schemars = { workspace = true } -serde = { workspace = true } -thiserror = { workspace = true } -neutron-sdk = { workspace = true } - -[dev-dependencies] -mars-osmosis = { workspace = true } -mars-testing = { workspace = true } -osmosis-std = { workspace = true } +cosmwasm-std = { workspace = true, features = ["stargate"] } +cw2 = { workspace = true } +mars-red-bank-types = { workspace = true } +mars-rewards-collector-base = { workspace = true } +neutron-sdk = { workspace = true } diff --git a/contracts/rewards-collector/osmosis/Cargo.toml b/contracts/rewards-collector/osmosis/Cargo.toml index 2b4a2acb7..daabdbdd3 100644 --- a/contracts/rewards-collector/osmosis/Cargo.toml +++ b/contracts/rewards-collector/osmosis/Cargo.toml @@ -10,35 +10,23 @@ documentation = { workspace = true } keywords = { workspace = true } [lib] -crate-type = [ - "cdylib", - "rlib", -] -doctest = false - -[profile.release] -overflow-checks = true +crate-type = ["cdylib", "rlib"] +doctest = false [features] # for more explicit tests, cargo test --features=backtraces -backtraces = [ - "cosmwasm-std/backtraces", -] +backtraces = ["cosmwasm-std/backtraces"] [dependencies] -cosmwasm-schema = { workspace = true } -cosmwasm-std = { workspace = true, features = ["stargate"] } -cw2 = { workspace = true } -cw-storage-plus = { workspace = true } -mars-owner = { workspace = true } -mars-red-bank-types = { workspace = true } -mars-rewards-collector-base = { workspace = true } -mars-utils = { workspace = true } -schemars = { workspace = true } -serde = { workspace = true } -thiserror = { workspace = true } +cosmwasm-std = { workspace = true, features = ["stargate"] } +cw2 = { workspace = true } +mars-red-bank-types = { workspace = true } +mars-rewards-collector-base = { workspace = true } [dev-dependencies] mars-osmosis = { workspace = true } +mars-owner = { workspace = true } mars-testing = { workspace = true } +mars-utils = { workspace = true } osmosis-std = { workspace = true } +serde = { workspace = true } diff --git a/contracts/swapper/astroport/Cargo.toml b/contracts/swapper/astroport/Cargo.toml index a285073d8..00f6aeb37 100644 --- a/contracts/swapper/astroport/Cargo.toml +++ b/contracts/swapper/astroport/Cargo.toml @@ -11,29 +11,27 @@ keywords = { workspace = true } [lib] crate-type = ["cdylib", "rlib"] - -[profile.release] -overflow-checks = true +doctest = false [features] -default = [] # for quicker tests, cargo test --lib # for more explicit tests, cargo test --features=backtraces -backtraces = ["cosmwasm-std/backtraces"] -library = [] +backtraces = ["cosmwasm-std/backtraces"] +default = [] +library = [] osmosis-test-tube = ["cw-it/osmosis-test-tube", "mars-testing/osmosis-test-tube"] [dependencies] +astroport = { workspace = true } cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw2 = { workspace = true } -mars-swapper-base = { workspace = true } -mars-oracle-wasm = { workspace = true } mars-red-bank-types = { workspace = true } -astroport = { workspace = true } +mars-swapper-base = { workspace = true } [dev-dependencies] anyhow = { workspace = true } -mars-testing = { workspace = true, features = ["astroport"] } cw-it = { workspace = true, features = ["astroport", "astroport-multi-test"] } -test-case = "3.0.0" +mars-oracle-wasm = { workspace = true } +mars-testing = { workspace = true, features = ["astroport"] } +test-case = { workspace = true } diff --git a/contracts/swapper/base/Cargo.toml b/contracts/swapper/base/Cargo.toml index 42c5d3221..2eedb45fc 100644 --- a/contracts/swapper/base/Cargo.toml +++ b/contracts/swapper/base/Cargo.toml @@ -10,10 +10,7 @@ documentation = { workspace = true } keywords = { workspace = true } [lib] -crate-type = ["cdylib", "rlib"] - -[profile.release] -overflow-checks = true +doctest = false [features] # for quicker tests, cargo test --lib diff --git a/contracts/swapper/mock/Cargo.toml b/contracts/swapper/mock/Cargo.toml index 924918749..97f755bf3 100644 --- a/contracts/swapper/mock/Cargo.toml +++ b/contracts/swapper/mock/Cargo.toml @@ -11,6 +11,7 @@ keywords = { workspace = true } [lib] crate-type = ["cdylib", "rlib"] +doctest = false [features] # for quicker tests, cargo test --lib @@ -21,7 +22,3 @@ library = [] [dependencies] cosmwasm-std = { workspace = true } mars-red-bank-types = { workspace = true } - -[dev-dependencies] -anyhow = { workspace = true } -cw-multi-test = { workspace = true } diff --git a/contracts/swapper/osmosis/Cargo.toml b/contracts/swapper/osmosis/Cargo.toml index 7bdb7ebc7..ce5a9c817 100644 --- a/contracts/swapper/osmosis/Cargo.toml +++ b/contracts/swapper/osmosis/Cargo.toml @@ -11,9 +11,7 @@ keywords = { workspace = true } [lib] crate-type = ["cdylib", "rlib"] - -[profile.release] -overflow-checks = true +doctest = false [features] # for quicker tests, cargo test --lib @@ -32,5 +30,5 @@ osmosis-std = { workspace = true } mars-red-bank-types = { workspace = true } [dev-dependencies] -anyhow = { workspace = true } -cw-it = { workspace = true, features = ["osmosis-test-tube"] } +anyhow = { workspace = true } +cw-it = { workspace = true, features = ["osmosis-test-tube"] } diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index a9650b18c..dcde9740e 100755 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -10,34 +10,28 @@ documentation = { workspace = true } keywords = { workspace = true } [lib] -crate-type = [ - "cdylib", - "rlib", -] doctest = false [features] # for more explicit tests, cargo test --features=backtraces -backtraces = [ - "cosmwasm-std/backtraces", -] +backtraces = ["cosmwasm-std/backtraces"] [dev-dependencies] anyhow = { workspace = true } cosmwasm-std = { workspace = true } -cw-multi-test = { workspace = true } cw-it = { workspace = true, features = ["osmosis-test-tube"] } +cw-multi-test = { workspace = true } mars-incentives = { workspace = true } -mars-oracle-osmosis = { workspace = true } mars-oracle-base = { workspace = true } +mars-oracle-osmosis = { workspace = true } mars-osmosis = { workspace = true } mars-params = { workspace = true } mars-red-bank = { workspace = true } mars-red-bank-types = { workspace = true } mars-rewards-collector-osmosis = { workspace = true } +mars-swapper-osmosis = { workspace = true } mars-testing = { workspace = true } mars-utils = { workspace = true } osmosis-std = { workspace = true } osmosis-test-tube = { workspace = true } serde = { workspace = true } -mars-swapper-osmosis = { workspace = true } diff --git a/packages/liquidation/Cargo.toml b/packages/liquidation/Cargo.toml index 6d3015bbe..472fe809f 100644 --- a/packages/liquidation/Cargo.toml +++ b/packages/liquidation/Cargo.toml @@ -19,7 +19,7 @@ doctest = false backtraces = ["cosmwasm-std/backtraces"] [dependencies] -cosmwasm-std = { workspace = true } -mars-health = { workspace = true } -mars-params = { workspace = true } -thiserror = { workspace = true } +cosmwasm-std = { workspace = true } +mars-health = { workspace = true } +mars-params = { workspace = true } +thiserror = { workspace = true } diff --git a/packages/testing/Cargo.toml b/packages/testing/Cargo.toml index 1e2f5767f..2e0de5e30 100644 --- a/packages/testing/Cargo.toml +++ b/packages/testing/Cargo.toml @@ -14,12 +14,10 @@ keywords = { workspace = true } doctest = false [features] -default = [] -# for quicker tests, cargo test --lib -# for more explicit tests, cargo test --features=backtraces -astroport = ["cw-it/astroport", "dep:astroport"] +astroport = ["cw-it/astroport", "dep:astroport"] +backtraces = ["cosmwasm-std/backtraces", "osmosis-std/backtraces"] +default = [] osmosis-test-tube = ["cw-it/osmosis-test-tube"] -backtraces = ["cosmwasm-std/backtraces", "osmosis-std/backtraces"] [dependencies] anyhow = { workspace = true } @@ -42,7 +40,6 @@ mars-swapper-astroport = { workspace = true } prost = { workspace = true } pyth-sdk-cw = { workspace = true } - [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -cw-it = { workspace = true, features = ["multi-test"] } -cw-multi-test = { workspace = true } +cw-it = { workspace = true, features = ["multi-test"] } +cw-multi-test = { workspace = true } diff --git a/packages/types/Cargo.toml b/packages/types/Cargo.toml index 5956de1c8..d4475675f 100644 --- a/packages/types/Cargo.toml +++ b/packages/types/Cargo.toml @@ -23,5 +23,5 @@ cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } mars-owner = { workspace = true } mars-utils = { workspace = true } +strum = { workspace = true, features = ["derive"] } thiserror = { workspace = true } -strum = { workspace = true, features = ["derive"] } diff --git a/schemas/mars-params/mars-params.json b/schemas/mars-params/mars-params.json index e9daf2f0b..dbf0f6338 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.0.7", + "contract_version": "1.2.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", From 297a0139197e7a11f2ed0fac11778e375c2fa99c Mon Sep 17 00:00:00 2001 From: piobab Date: Fri, 18 Aug 2023 10:24:27 +0200 Subject: [PATCH 25/43] MP-3122 price source checks (#302) * Add checks for denoms in price source setup. * Fix audit warn. * Remove unused error type. --- .cargo/audit.toml | 6 ++ Cargo.lock | 5 +- Cargo.toml | 2 +- contracts/oracle/base/Cargo.toml | 1 + contracts/oracle/base/src/contract.rs | 6 +- contracts/oracle/base/src/error.rs | 9 +-- contracts/oracle/base/src/lib.rs | 2 - contracts/oracle/base/src/utils.rs | 32 -------- contracts/oracle/osmosis/Cargo.toml | 1 + contracts/oracle/osmosis/src/helpers.rs | 6 ++ contracts/oracle/osmosis/src/price_source.rs | 7 ++ contracts/oracle/osmosis/tests/test_admin.rs | 17 +++-- .../osmosis/tests/test_set_price_source.rs | 76 +++++++++++++++++-- 13 files changed, 109 insertions(+), 61 deletions(-) create mode 100644 .cargo/audit.toml delete mode 100644 contracts/oracle/base/src/utils.rs diff --git a/.cargo/audit.toml b/.cargo/audit.toml new file mode 100644 index 000000000..80dec5a81 --- /dev/null +++ b/.cargo/audit.toml @@ -0,0 +1,6 @@ +# Reference: https://github.com/rustsec/rustsec/blob/main/cargo-audit/audit.toml.example + +[advisories] +# Ignore the following advisory IDs. +# RUSTSEC-2022-0093 is reported for test-tube which is only used for testing. +ignore = ["RUSTSEC-2022-0093"] \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 5fb78db7f..ce2082c52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -815,9 +815,9 @@ dependencies = [ [[package]] name = "cw-it" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e3120d46b30b900c4b9ab0f996ce527c1187095c75ffa61d9ea5aa6d1e93b41" +checksum = "6c7ad91046afef49ea3b922c36395298254802844cd360911cf2e1b96825c9df" dependencies = [ "anyhow", "apollo-cw-multi-test", @@ -1830,6 +1830,7 @@ dependencies = [ "cw2 1.1.0", "mars-owner", "mars-red-bank-types", + "mars-utils", "pyth-sdk-cw", "schemars", "serde", diff --git a/Cargo.toml b/Cargo.toml index 8df05ee17..71a9d92e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,7 @@ thiserror = "1.0.44" # dev-dependencies cw-multi-test = "0.16.5" -cw-it = "0.1.0" +cw-it = "0.1.1" osmosis-test-tube = "=16.1.1" proptest = "1.2.0" test-case = "3.1.0" diff --git a/contracts/oracle/base/Cargo.toml b/contracts/oracle/base/Cargo.toml index 5afd4108f..d7c357996 100644 --- a/contracts/oracle/base/Cargo.toml +++ b/contracts/oracle/base/Cargo.toml @@ -22,6 +22,7 @@ cosmwasm-std = { workspace = true } cw2 = { workspace = true } cw-storage-plus = { workspace = true } mars-owner = { workspace = true } +mars-utils = { workspace = true } mars-red-bank-types = { workspace = true } pyth-sdk-cw = { workspace = true, optional = true } schemars = { workspace = true } diff --git a/contracts/oracle/base/src/contract.rs b/contracts/oracle/base/src/contract.rs index e0268d8d0..b402c913b 100644 --- a/contracts/oracle/base/src/contract.rs +++ b/contracts/oracle/base/src/contract.rs @@ -10,11 +10,9 @@ use mars_red_bank_types::oracle::msg::{ ActionKind, Config, ConfigResponse, ExecuteMsg, InstantiateMsg, PriceResponse, PriceSourceResponse, QueryMsg, }; +use mars_utils::helpers::validate_native_denom; -use crate::{ - error::ContractResult, utils::validate_native_denom, ContractError, PriceSourceChecked, - PriceSourceUnchecked, -}; +use crate::{error::ContractResult, ContractError, PriceSourceChecked, PriceSourceUnchecked}; const DEFAULT_LIMIT: u32 = 10; const MAX_LIMIT: u32 = 30; diff --git a/contracts/oracle/base/src/error.rs b/contracts/oracle/base/src/error.rs index 8b71779a8..06cf2c50e 100644 --- a/contracts/oracle/base/src/error.rs +++ b/contracts/oracle/base/src/error.rs @@ -3,6 +3,7 @@ use cosmwasm_std::{ DecimalRangeExceeded, DivideByZeroError, OverflowError, StdError, }; use mars_owner::OwnerError; +use mars_utils::error::ValidationError; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -10,12 +11,8 @@ pub enum ContractError { #[error("{0}")] Std(#[from] StdError), - // #[error("{0}")] - // Mars(#[from] MarsError), - #[error("Invalid denom: {reason}")] - InvalidDenom { - reason: String, - }, + #[error("{0}")] + Validation(#[from] ValidationError), #[error("{0}")] Version(#[from] cw2::VersionError), diff --git a/contracts/oracle/base/src/lib.rs b/contracts/oracle/base/src/lib.rs index 48deeef95..0756db31a 100644 --- a/contracts/oracle/base/src/lib.rs +++ b/contracts/oracle/base/src/lib.rs @@ -1,7 +1,6 @@ mod contract; mod error; mod traits; -mod utils; #[cfg(feature = "pyth")] pub mod pyth; @@ -9,4 +8,3 @@ pub mod pyth; pub use contract::*; pub use error::*; pub use traits::*; -pub use utils::*; diff --git a/contracts/oracle/base/src/utils.rs b/contracts/oracle/base/src/utils.rs deleted file mode 100644 index 8b5cb83b5..000000000 --- a/contracts/oracle/base/src/utils.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::ContractError; - -/// follows cosmos SDK validation logic where denoms can be 3 - 128 characters long -/// and starts with a letter, followed but either a letter, number, or separator ( ‘/' , ‘:' , ‘.’ , ‘_’ , or '-') -/// reference: https://github.com/cosmos/cosmos-sdk/blob/7728516abfab950dc7a9120caad4870f1f962df5/types/coin.go#L865-L867 -pub fn validate_native_denom(denom: &str) -> Result<(), ContractError> { - if denom.len() < 3 || denom.len() > 128 { - return Err(ContractError::InvalidDenom { - reason: "Invalid denom length".to_string(), - }); - } - - let mut chars = denom.chars(); - let first = chars.next().unwrap(); - if !first.is_ascii_alphabetic() { - return Err(ContractError::InvalidDenom { - reason: "First character is not ASCII alphabetic".to_string(), - }); - } - - let set = ['/', ':', '.', '_', '-']; - for c in chars { - if !(c.is_ascii_alphanumeric() || set.contains(&c)) { - return Err(ContractError::InvalidDenom { - reason: "Not all characters are ASCII alphanumeric or one of: / : . _ -" - .to_string(), - }); - } - } - - Ok(()) -} diff --git a/contracts/oracle/osmosis/Cargo.toml b/contracts/oracle/osmosis/Cargo.toml index 47f0829b9..2a6e48688 100644 --- a/contracts/oracle/osmosis/Cargo.toml +++ b/contracts/oracle/osmosis/Cargo.toml @@ -26,6 +26,7 @@ cw-storage-plus = { workspace = true } mars-oracle-base = { workspace = true, features = ["pyth"] } mars-osmosis = { workspace = true } mars-owner = { workspace = true } +mars-utils = { workspace = true } mars-red-bank-types = { workspace = true } osmosis-std = { workspace = true } pyth-sdk-cw = { workspace = true } diff --git a/contracts/oracle/osmosis/src/helpers.rs b/contracts/oracle/osmosis/src/helpers.rs index 4ffc2a1ec..c0937bb4c 100644 --- a/contracts/oracle/osmosis/src/helpers.rs +++ b/contracts/oracle/osmosis/src/helpers.rs @@ -49,6 +49,12 @@ fn assert_osmosis_pool_contains_two_assets( let pool_id = pool.get_pool_id(); let pool_denoms = pool.get_pool_denoms(); + if denom == base_denom { + return Err(ContractError::InvalidPriceSource { + reason: "denom and base denom can't be the same".to_string(), + }); + } + if !pool_denoms.contains(&base_denom.to_string()) { return Err(ContractError::InvalidPriceSource { reason: format!("pool {} does not contain the base denom {}", pool_id, base_denom), diff --git a/contracts/oracle/osmosis/src/price_source.rs b/contracts/oracle/osmosis/src/price_source.rs index 22f0eba1a..adb229297 100644 --- a/contracts/oracle/osmosis/src/price_source.rs +++ b/contracts/oracle/osmosis/src/price_source.rs @@ -11,6 +11,7 @@ use mars_osmosis::helpers::{ recovered_since_downtime_of_length, Pool, }; use mars_red_bank_types::oracle::{ActionKind, Config}; +use mars_utils::helpers::validate_native_denom; use pyth_sdk_cw::PriceIdentifier; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -381,9 +382,12 @@ impl PriceSourceUnchecked for OsmosisPriceSour window_size, downtime_detector, } => { + validate_native_denom(transitive_denom)?; + let pool = query_pool(&deps.querier, *pool_id)?; helpers::assert_osmosis_pool_assets(&pool, denom, transitive_denom)?; helpers::assert_osmosis_twap(*window_size, downtime_detector)?; + Ok(OsmosisPriceSourceChecked::StakedGeometricTwap { transitive_denom: transitive_denom.to_string(), pool_id: *pool_id, @@ -415,12 +419,15 @@ impl PriceSourceUnchecked for OsmosisPriceSour geometric_twap, redemption_rate, } => { + validate_native_denom(transitive_denom)?; + let pool = query_pool(&deps.querier, geometric_twap.pool_id)?; helpers::assert_osmosis_pool_assets(&pool, denom, transitive_denom)?; helpers::assert_osmosis_twap( geometric_twap.window_size, &geometric_twap.downtime_detector, )?; + Ok(OsmosisPriceSourceChecked::Lsd { transitive_denom: transitive_denom.to_string(), geometric_twap: geometric_twap.clone(), diff --git a/contracts/oracle/osmosis/tests/test_admin.rs b/contracts/oracle/osmosis/tests/test_admin.rs index b996215d3..5812bc5e8 100644 --- a/contracts/oracle/osmosis/tests/test_admin.rs +++ b/contracts/oracle/osmosis/tests/test_admin.rs @@ -4,6 +4,7 @@ use mars_oracle_osmosis::{contract::entry, msg::ExecuteMsg}; use mars_owner::OwnerError::NotOwner; use mars_red_bank_types::oracle::{ConfigResponse, InstantiateMsg, QueryMsg}; use mars_testing::{mock_dependencies, mock_info}; +use mars_utils::error::ValidationError; mod helpers; @@ -35,9 +36,9 @@ fn instantiating_incorrect_denom() { ); assert_eq!( res, - Err(ContractError::InvalidDenom { + Err(ContractError::Validation(ValidationError::InvalidDenom { reason: "First character is not ASCII alphabetic".to_string() - }) + })) ); let res = entry::instantiate( @@ -52,10 +53,10 @@ fn instantiating_incorrect_denom() { ); assert_eq!( res, - Err(ContractError::InvalidDenom { + Err(ContractError::Validation(ValidationError::InvalidDenom { reason: "Not all characters are ASCII alphanumeric or one of: / : . _ -" .to_string() - }) + })) ); let res = entry::instantiate( @@ -70,9 +71,9 @@ fn instantiating_incorrect_denom() { ); assert_eq!( res, - Err(ContractError::InvalidDenom { + Err(ContractError::Validation(ValidationError::InvalidDenom { reason: "Invalid denom length".to_string() - }) + })) ); } @@ -99,9 +100,9 @@ fn update_config_with_invalid_base_denom() { let res_err = entry::execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); assert_eq!( res_err, - ContractError::InvalidDenom { + ContractError::Validation(ValidationError::InvalidDenom { reason: "First character is not ASCII alphabetic".to_string() - } + }) ); } diff --git a/contracts/oracle/osmosis/tests/test_set_price_source.rs b/contracts/oracle/osmosis/tests/test_set_price_source.rs index d56fa5c91..4cd0c60f2 100644 --- a/contracts/oracle/osmosis/tests/test_set_price_source.rs +++ b/contracts/oracle/osmosis/tests/test_set_price_source.rs @@ -11,6 +11,7 @@ use mars_oracle_osmosis::{ use mars_owner::OwnerError::NotOwner; use mars_red_bank_types::oracle::msg::QueryMsg; use mars_testing::mock_info; +use mars_utils::error::ValidationError; use pyth_sdk_cw::PriceIdentifier; mod helpers; @@ -83,9 +84,9 @@ fn setting_price_source_incorrect_denom() { ); assert_eq!( res, - Err(ContractError::InvalidDenom { + Err(ContractError::Validation(ValidationError::InvalidDenom { reason: "First character is not ASCII alphabetic".to_string() - }) + })) ); let res_two = execute( @@ -101,10 +102,10 @@ fn setting_price_source_incorrect_denom() { ); assert_eq!( res_two, - Err(ContractError::InvalidDenom { + Err(ContractError::Validation(ValidationError::InvalidDenom { reason: "Not all characters are ASCII alphanumeric or one of: / : . _ -" .to_string() - }) + })) ); let res_three = execute( @@ -120,9 +121,9 @@ fn setting_price_source_incorrect_denom() { ); assert_eq!( res_three, - Err(ContractError::InvalidDenom { + Err(ContractError::Validation(ValidationError::InvalidDenom { reason: "Invalid denom length".to_string() - }) + })) ); } @@ -144,6 +145,15 @@ fn setting_price_source_spot() { ) }; + // attempting to set price source for base denom; should fail + let err = set_price_source_spot("uosmo", 1).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "denom and base denom can't be the same".to_string() + } + ); + // attempting to use a pool that does not contain the denom of interest; should fail let err = set_price_source_spot("umars", 1).unwrap_err(); assert_eq!( @@ -222,6 +232,15 @@ fn setting_price_source_arithmetic_twap_with_invalid_params() { ) }; + // attempting to set price source for base denom; should fail + let err = set_price_source_twap("uosmo", 1, 86400, None).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "denom and base denom can't be the same".to_string() + } + ); + // attempting to use a pool that does not contain the denom of interest; should fail let err = set_price_source_twap("umars", 1, 86400, None).unwrap_err(); assert_eq!( @@ -385,6 +404,15 @@ fn setting_price_source_geometric_twap_with_invalid_params() { ) }; + // attempting to set price source for base denom; should fail + let err = set_price_source_twap("uosmo", 1, 86400, None).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "denom and base denom can't be the same".to_string() + } + ); + // attempting to use a pool that does not contain the denom of interest; should fail let err = set_price_source_twap("umars", 1, 86400, None).unwrap_err(); assert_eq!( @@ -550,6 +578,24 @@ fn setting_price_source_staked_geometric_twap_with_invalid_params() { ) }; + // attempting to set price source for base denom; should fail + let err = set_price_source_twap("uosmo", "uosmo", 1, 86400, None).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "denom and base denom can't be the same".to_string() + } + ); + + // attempting to set price source with invalid transitive denom; should fail + let err = set_price_source_twap("ustatom", "!*jadfaefc", 803, 86400, None).unwrap_err(); + assert_eq!( + err, + ContractError::Validation(ValidationError::InvalidDenom { + reason: "First character is not ASCII alphabetic".to_string() + }) + ); + // attempting to use a pool that does not contain the denom of interest; should fail let err = set_price_source_twap("ustatom", "umars", 803, 86400, None).unwrap_err(); assert_eq!( @@ -724,6 +770,24 @@ fn setting_price_source_lsd_with_invalid_params() { ) }; + // attempting to set price source for base denom; should fail + let err = set_price_source_twap("uosmo", "uosmo", 1, 86400, None).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "denom and base denom can't be the same".to_string() + } + ); + + // attempting to set price source with invalid transitive denom; should fail + let err = set_price_source_twap("ustatom", "!*jadfaefc", 3333, 86400, None).unwrap_err(); + assert_eq!( + err, + ContractError::Validation(ValidationError::InvalidDenom { + reason: "First character is not ASCII alphabetic".to_string() + }) + ); + // attempting to use a pool that does not contain the denom of interest; should fail let err = set_price_source_twap("ustatom", "umars", 803, 86400, None).unwrap_err(); assert_eq!( From 906fce0d003e135b9df440a9c4d2079b017c30f7 Mon Sep 17 00:00:00 2001 From: piobab Date: Fri, 18 Aug 2023 11:00:36 +0200 Subject: [PATCH 26/43] Validate if denom_decimals <= 18. Validate if scaled pyth price != 0. (#303) --- contracts/oracle/base/Cargo.toml | 3 +- contracts/oracle/base/src/lib.rs | 1 - contracts/oracle/base/src/pyth.rs | 33 +++++++++- contracts/oracle/osmosis/Cargo.toml | 3 +- contracts/oracle/osmosis/src/price_source.rs | 6 +- .../osmosis/tests/test_set_price_source.rs | 54 +++++++++------- contracts/oracle/wasm/Cargo.toml | 2 +- contracts/oracle/wasm/src/price_source.rs | 2 +- .../oracle/wasm/tests/test_price_source.rs | 63 +++++++++++++++++++ 9 files changed, 136 insertions(+), 31 deletions(-) diff --git a/contracts/oracle/base/Cargo.toml b/contracts/oracle/base/Cargo.toml index d7c357996..535dcc096 100644 --- a/contracts/oracle/base/Cargo.toml +++ b/contracts/oracle/base/Cargo.toml @@ -15,7 +15,6 @@ doctest = false [features] # for more explicit tests, cargo test --features=backtraces backtraces = ["cosmwasm-std/backtraces"] -pyth = ["pyth-sdk-cw"] [dependencies] cosmwasm-std = { workspace = true } @@ -24,7 +23,7 @@ cw-storage-plus = { workspace = true } mars-owner = { workspace = true } mars-utils = { workspace = true } mars-red-bank-types = { workspace = true } -pyth-sdk-cw = { workspace = true, optional = true } +pyth-sdk-cw = { workspace = true } schemars = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } diff --git a/contracts/oracle/base/src/lib.rs b/contracts/oracle/base/src/lib.rs index 0756db31a..450ac1ac1 100644 --- a/contracts/oracle/base/src/lib.rs +++ b/contracts/oracle/base/src/lib.rs @@ -2,7 +2,6 @@ mod contract; mod error; mod traits; -#[cfg(feature = "pyth")] pub mod pyth; pub use contract::*; diff --git a/contracts/oracle/base/src/pyth.rs b/contracts/oracle/base/src/pyth.rs index 751937e0b..f0d25ed86 100644 --- a/contracts/oracle/base/src/pyth.rs +++ b/contracts/oracle/base/src/pyth.rs @@ -6,6 +6,9 @@ use pyth_sdk_cw::{query_price_feed, Price, PriceFeed, PriceFeedResponse, PriceId use super::*; use crate::error::ContractError::InvalidPrice; +// We don't support any denom with more than 18 decimals +const MAX_DENOM_DECIMALS: u8 = 18; + /// We want to discriminate which actions should trigger a circuit breaker check. /// The objective is to allow liquidations to happen without requiring too many checks (always be open for liquidations) /// while not allowing other actions to be taken in cases of extreme volatility (which could indicate price manipulation attacks). @@ -121,7 +124,11 @@ fn query_pyth_price_for_liquidation( } /// Assert Pyth configuration -pub fn assert_pyth(max_confidence: Decimal, max_deviation: Decimal) -> ContractResult<()> { +pub fn assert_pyth( + max_confidence: Decimal, + max_deviation: Decimal, + denom_decimals: u8, +) -> ContractResult<()> { if !max_confidence.le(&Decimal::percent(20u64)) { return Err(ContractError::InvalidPriceSource { reason: "max_confidence must be in the range of <0;0.2>".to_string(), @@ -134,6 +141,12 @@ pub fn assert_pyth(max_confidence: Decimal, max_deviation: Decimal) -> ContractR }); } + if denom_decimals > MAX_DENOM_DECIMALS { + return Err(ContractError::InvalidPriceSource { + reason: format!("denom_decimals must be <= {}", MAX_DENOM_DECIMALS), + }); + } + Ok(()) } @@ -255,6 +268,12 @@ pub fn scale_pyth_price( // 26 decimals used (overflow) !!! let price = usd_price.checked_mul(denom_scaled)?.checked_mul(pyth_price)?; + if price.is_zero() { + return Err(InvalidPrice { + reason: "price is zero".to_string(), + }); + } + Ok(price) } @@ -327,4 +346,16 @@ mod tests { .unwrap(); assert_eq!(ueth_price_in_uusd, Decimal::from_atomics(100000098000001u128, 20u32).unwrap()); } + + #[test] + fn return_error_if_scaled_pyth_price_is_zero() { + let price_err = + scale_pyth_price(1u128, -18, 18u8, Decimal::from_str("1000000").unwrap()).unwrap_err(); + assert_eq!( + price_err, + ContractError::InvalidPrice { + reason: "price is zero".to_string() + } + ); + } } diff --git a/contracts/oracle/osmosis/Cargo.toml b/contracts/oracle/osmosis/Cargo.toml index 2a6e48688..57e951f7b 100644 --- a/contracts/oracle/osmosis/Cargo.toml +++ b/contracts/oracle/osmosis/Cargo.toml @@ -23,7 +23,7 @@ cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw2 = { workspace = true } cw-storage-plus = { workspace = true } -mars-oracle-base = { workspace = true, features = ["pyth"] } +mars-oracle-base = { workspace = true } mars-osmosis = { workspace = true } mars-owner = { workspace = true } mars-utils = { workspace = true } @@ -38,4 +38,3 @@ cosmwasm-schema = { workspace = true } mars-owner = { workspace = true } mars-testing = { workspace = true } mars-utils = { workspace = true } -pyth-sdk-cw = { workspace = true } diff --git a/contracts/oracle/osmosis/src/price_source.rs b/contracts/oracle/osmosis/src/price_source.rs index adb229297..0b2e5b4f9 100644 --- a/contracts/oracle/osmosis/src/price_source.rs +++ b/contracts/oracle/osmosis/src/price_source.rs @@ -403,7 +403,11 @@ impl PriceSourceUnchecked for OsmosisPriceSour max_deviation, denom_decimals, } => { - mars_oracle_base::pyth::assert_pyth(*max_confidence, *max_deviation)?; + mars_oracle_base::pyth::assert_pyth( + *max_confidence, + *max_deviation, + *denom_decimals, + )?; mars_oracle_base::pyth::assert_usd_price_source(deps, price_sources)?; Ok(OsmosisPriceSourceChecked::Pyth { contract_addr: deps.api.addr_validate(contract_addr)?, diff --git a/contracts/oracle/osmosis/tests/test_set_price_source.rs b/contracts/oracle/osmosis/tests/test_set_price_source.rs index 4cd0c60f2..9048dbab6 100644 --- a/contracts/oracle/osmosis/tests/test_set_price_source.rs +++ b/contracts/oracle/osmosis/tests/test_set_price_source.rs @@ -1021,30 +1021,31 @@ fn setting_price_source_xyk_lp() { fn setting_price_source_pyth_with_invalid_params() { let mut deps = helpers::setup_test(); - let mut set_price_source_pyth = |max_confidence: Decimal, max_deviation: Decimal| { - execute( - deps.as_mut(), - mock_env(), - mock_info("owner"), - ExecuteMsg::SetPriceSource { - denom: "uatom".to_string(), - price_source: OsmosisPriceSourceUnchecked::Pyth { - contract_addr: "pyth_contract_addr".to_string(), - price_feed_id: PriceIdentifier::from_hex( - "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", - ) - .unwrap(), - max_staleness: 30, - max_confidence, - max_deviation, - denom_decimals: 6u8, + let mut set_price_source_pyth = + |max_confidence: Decimal, max_deviation: Decimal, denom_decimals: u8| { + execute( + deps.as_mut(), + mock_env(), + mock_info("owner"), + ExecuteMsg::SetPriceSource { + denom: "uatom".to_string(), + price_source: OsmosisPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), + price_feed_id: PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(), + max_staleness: 30, + max_confidence, + max_deviation, + denom_decimals, + }, }, - }, - ) - }; + ) + }; // attempting to set max_confidence > 20%; should fail - let err = set_price_source_pyth(Decimal::percent(21), Decimal::percent(6)).unwrap_err(); + let err = set_price_source_pyth(Decimal::percent(21), Decimal::percent(6), 6).unwrap_err(); assert_eq!( err, ContractError::InvalidPriceSource { @@ -1053,13 +1054,22 @@ fn setting_price_source_pyth_with_invalid_params() { ); // attempting to set max_deviation > 20%; should fail - let err = set_price_source_pyth(Decimal::percent(5), Decimal::percent(21)).unwrap_err(); + let err = set_price_source_pyth(Decimal::percent(5), Decimal::percent(21), 18).unwrap_err(); assert_eq!( err, ContractError::InvalidPriceSource { reason: "max_deviation must be in the range of <0;0.2>".to_string() } ); + + // attempting to set denom_decimals > 18; should fail + let err = set_price_source_pyth(Decimal::percent(5), Decimal::percent(20), 19).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "denom_decimals must be <= 18".to_string() + } + ); } #[test] diff --git a/contracts/oracle/wasm/Cargo.toml b/contracts/oracle/wasm/Cargo.toml index 1b293cb07..502618919 100644 --- a/contracts/oracle/wasm/Cargo.toml +++ b/contracts/oracle/wasm/Cargo.toml @@ -26,7 +26,7 @@ cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw2 = { workspace = true } cw-storage-plus = { workspace = true } -mars-oracle-base = { workspace = true, features = ["pyth"] } +mars-oracle-base = { workspace = true } mars-red-bank-types = { workspace = true } pyth-sdk-cw = { workspace = true } diff --git a/contracts/oracle/wasm/src/price_source.rs b/contracts/oracle/wasm/src/price_source.rs index f0c26bf7b..4df7140a7 100644 --- a/contracts/oracle/wasm/src/price_source.rs +++ b/contracts/oracle/wasm/src/price_source.rs @@ -201,7 +201,7 @@ impl PriceSourceUnchecked for WasmPriceSourceUnch max_deviation, denom_decimals, } => { - mars_oracle_base::pyth::assert_pyth(max_confidence, max_deviation)?; + mars_oracle_base::pyth::assert_pyth(max_confidence, max_deviation, denom_decimals)?; mars_oracle_base::pyth::assert_usd_price_source(deps, price_sources)?; Ok(WasmPriceSourceChecked::Pyth { contract_addr: deps.api.addr_validate(&contract_addr)?, diff --git a/contracts/oracle/wasm/tests/test_price_source.rs b/contracts/oracle/wasm/tests/test_price_source.rs index ca119203c..4c5b4f5f6 100644 --- a/contracts/oracle/wasm/tests/test_price_source.rs +++ b/contracts/oracle/wasm/tests/test_price_source.rs @@ -654,6 +654,69 @@ fn setting_price_source_pyth_if_missing_usd() { ); } +#[test] +fn setting_price_source_pyth_with_invalid_params() { + let runner = get_test_runner(); + let robot = WasmOracleTestRobot::new( + &runner, + get_contracts(&get_test_runner()), + &get_test_runner().init_default_accounts().unwrap()[0], + None, + ); + + let mut deps = helpers::setup_test(&robot.astroport_contracts.factory.address); + + let mut set_price_source_pyth = + |max_confidence: Decimal, max_deviation: Decimal, denom_decimals: u8| { + execute( + deps.as_mut(), + mock_env(), + mock_info("owner"), + ExecuteMsg::SetPriceSource { + denom: "uatom".to_string(), + price_source: WasmPriceSourceUnchecked::Pyth { + contract_addr: "pyth_contract_addr".to_string(), + price_feed_id: PriceIdentifier::from_hex( + "61226d39beea19d334f17c2febce27e12646d84675924ebb02b9cdaea68727e3", + ) + .unwrap(), + max_staleness: 30, + max_confidence, + max_deviation, + denom_decimals, + }, + }, + ) + }; + + // attempting to set max_confidence > 20%; should fail + let err = set_price_source_pyth(Decimal::percent(21), Decimal::percent(6), 6).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "max_confidence must be in the range of <0;0.2>".to_string() + } + ); + + // attempting to set max_deviation > 20%; should fail + let err = set_price_source_pyth(Decimal::percent(5), Decimal::percent(21), 18).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "max_deviation must be in the range of <0;0.2>".to_string() + } + ); + + // attempting to set denom_decimals > 18; should fail + let err = set_price_source_pyth(Decimal::percent(5), Decimal::percent(20), 19).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "denom_decimals must be <= 18".to_string() + } + ); +} + #[test] fn twap_window_size_not_gt_tolerance() { let runner = get_test_runner(); From 28b6905b2c0e968b7469a4c93c55202ca424ed66 Mon Sep 17 00:00:00 2001 From: piobab Date: Tue, 22 Aug 2023 12:31:20 +0200 Subject: [PATCH 27/43] Validate if slippage not too high. (#304) --- .../swapper/astroport/tests/test_swap.rs | 6 ++-- contracts/swapper/base/src/contract.rs | 11 ++++++ contracts/swapper/base/src/error.rs | 8 ++++- contracts/swapper/osmosis/tests/test_swap.rs | 35 +++++++++++++++++++ 4 files changed, 57 insertions(+), 3 deletions(-) diff --git a/contracts/swapper/astroport/tests/test_swap.rs b/contracts/swapper/astroport/tests/test_swap.rs index 002f25c2e..b2ab8870b 100644 --- a/contracts/swapper/astroport/tests/test_swap.rs +++ b/contracts/swapper/astroport/tests/test_swap.rs @@ -54,9 +54,11 @@ const DEFAULT_LIQ: [u128; 2] = [10000000000000000u128, 10000000000000000u128]; #[test_case(PoolType::Stable { amp: 10u64 }, "uatom", &DEFAULT_LIQ, &[6,6], Decimal::percent(5), true => panics ; "stable swap no route")] #[test_case(PoolType::Xyk {}, "uatom", &DEFAULT_LIQ, &[10,6], Decimal::percent(1), false; "xyk 10:6 decimals, even pool")] #[test_case(PoolType::Xyk {}, "uatom", &DEFAULT_LIQ, &[6,18], Decimal::percent(1), false; "xyk 6:18 decimals, even pool")] -#[test_case(PoolType::Stable { amp: 10u64 }, "uatom", &[100000000000,10000000000000], &[6,8], Decimal::percent(50), false; "stable 6:8 decimals, even adjusted pool")] -#[test_case(PoolType::Stable { amp: 10u64 }, "uatom", &[1000000000000,100000000000], &[7,6], Decimal::percent(50), false; "stable 8:6 decimals, even adjusted pool")] +#[test_case(PoolType::Stable { amp: 10u64 }, "uatom", &[100000000000,10000000000000], &[6,8], Decimal::percent(10), false; "stable 6:8 decimals, even adjusted pool")] +#[test_case(PoolType::Stable { amp: 10u64 }, "uatom", &[1000000000000,100000000000], &[7,6], Decimal::percent(10), false; "stable 8:6 decimals, even adjusted pool")] #[test_case(PoolType::Stable { amp: 10u64 }, "uatom", &[100000000000,100000000000000000000000], &[6,18], Decimal::percent(5), false; "stable 6:18 decimals, even adjusted pool")] +#[test_case(PoolType::Xyk {}, "uatom", &DEFAULT_LIQ, &[6,6], Decimal::percent(11), false => panics ; "xyk max slippage exceeded")] +#[test_case(PoolType::Stable { amp: 10u64 }, "uatom", &DEFAULT_LIQ, &[6,6], Decimal::percent(11), false => panics ; "stable max slippage exceeded")] fn swap( pool_type: PoolType, denom_out: &str, diff --git a/contracts/swapper/base/src/contract.rs b/contracts/swapper/base/src/contract.rs index 21408efc6..f597aa0e6 100644 --- a/contracts/swapper/base/src/contract.rs +++ b/contracts/swapper/base/src/contract.rs @@ -14,6 +14,9 @@ use mars_red_bank_types::swapper::{ use crate::{ContractError, ContractResult, Route}; +// Max allowed slippage percentage for swap +const MAX_SLIPPAGE_PERCENTAGE: u64 = 10; + pub struct SwapBase<'a, Q, M, R> where Q: CustomQuery, @@ -161,6 +164,14 @@ where denom_out: String, slippage: Decimal, ) -> ContractResult> { + let max_slippage = Decimal::percent(MAX_SLIPPAGE_PERCENTAGE); + if slippage > max_slippage { + return Err(ContractError::MaxSlippageExceeded { + max_slippage, + slippage, + }); + } + let swap_msg = self .routes .load(deps.storage, (coin_in.denom.clone(), denom_out.clone())) diff --git a/contracts/swapper/base/src/error.rs b/contracts/swapper/base/src/error.rs index f1e8e59ba..57047c5b4 100644 --- a/contracts/swapper/base/src/error.rs +++ b/contracts/swapper/base/src/error.rs @@ -1,5 +1,5 @@ use cosmwasm_std::{ - CheckedFromRatioError, CheckedMultiplyFractionError, CheckedMultiplyRatioError, + CheckedFromRatioError, CheckedMultiplyFractionError, CheckedMultiplyRatioError, Decimal, DecimalRangeExceeded, OverflowError, StdError, }; use mars_owner::OwnerError; @@ -50,6 +50,12 @@ pub enum ContractError { from: String, to: String, }, + + #[error("Max slippage of {max_slippage} exceeded. Slippage is {slippage}")] + MaxSlippageExceeded { + max_slippage: Decimal, + slippage: Decimal, + }, } pub type ContractResult = Result; diff --git a/contracts/swapper/osmosis/tests/test_swap.rs b/contracts/swapper/osmosis/tests/test_swap.rs index ec1c7bc1b..713209ad6 100644 --- a/contracts/swapper/osmosis/tests/test_swap.rs +++ b/contracts/swapper/osmosis/tests/test_swap.rs @@ -46,6 +46,41 @@ fn transfer_callback_only_internal() { ); } +#[test] +fn max_slippage_exeeded() { + let app = OsmosisTestApp::new(); + let wasm = Wasm::new(&app); + + let accs = app + .init_accounts(&[coin(1_000_000_000_000, "uosmo"), coin(1_000_000_000_000, "umars")], 2) + .unwrap(); + let owner = &accs[0]; + let other_guy = &accs[1]; + + let contract_addr = instantiate_contract(&wasm, owner); + + let res_err = wasm + .execute( + &contract_addr, + &ExecuteMsg::::SwapExactIn { + coin_in: coin(1_000_000, "umars"), + denom_out: "uosmo".to_string(), + slippage: Decimal::percent(11), + }, + &[coin(1_000_000, "umars")], + other_guy, + ) + .unwrap_err(); + + assert_err( + res_err, + ContractError::MaxSlippageExceeded { + max_slippage: Decimal::percent(10), + slippage: Decimal::percent(11), + }, + ); +} + #[test] fn swap_exact_in_slippage_too_high() { let app = OsmosisTestApp::new(); From 04b40a91ae989420ee90b8557495b67d1f9b9538 Mon Sep 17 00:00:00 2001 From: piobab Date: Wed, 23 Aug 2023 10:59:02 +0200 Subject: [PATCH 28/43] Enable test coverage (#305) * Enable test coverage. * Merge test files to reduce number of crates generated. * Fmt. * Remove unused comments. * Ignore audit for test-tube. --- .cargo/audit.toml | 2 +- .github/workflows/coverage.yml | 20 +++++------- contracts/address-provider/tests/all_tests.rs | 1 + .../{helpers.rs => tests/helpers/mod.rs} | 0 contracts/address-provider/tests/tests/mod.rs | 5 +++ .../tests/{ => tests}/test_addresses.rs | 4 +-- .../tests/{ => tests}/test_instantiate.rs | 4 +-- .../tests/{ => tests}/test_update_owner.rs | 4 +-- contracts/incentives/tests/all_tests.rs | 1 + .../{helpers.rs => tests/helpers/mod.rs} | 0 contracts/incentives/tests/tests/mod.rs | 10 ++++++ .../tests/{ => tests}/test_admin.rs | 4 +-- .../tests/{ => tests}/test_balance_change.rs | 4 +-- .../tests/{ => tests}/test_claim_rewards.rs | 4 +-- .../tests/{ => tests}/test_indices_usage.rs | 2 -- .../tests/{ => tests}/test_quering.rs | 32 +++++++++---------- .../{ => tests}/test_set_asset_incentive.rs | 4 +-- .../tests/{ => tests}/test_update_owner.rs | 4 +-- .../tests/{ => tests}/test_whitelist.rs | 4 +-- contracts/oracle/osmosis/tests/all_tests.rs | 1 + .../{helpers.rs => tests/helpers/mod.rs} | 0 contracts/oracle/osmosis/tests/tests/mod.rs | 10 ++++++ .../osmosis/tests/{ => tests}/test_admin.rs | 2 +- .../tests/{ => tests}/test_custom_execute.rs | 2 +- .../{ => tests}/test_price_source_fmt.rs | 2 -- .../tests/{ => tests}/test_query_price.rs | 5 ++- .../{ => tests}/test_query_price_for_pyth.rs | 2 +- .../{ => tests}/test_remove_price_source.rs | 2 +- .../{ => tests}/test_set_price_source.rs | 2 +- .../tests/{ => tests}/test_update_owner.rs | 5 ++- contracts/oracle/wasm/tests/all_tests.rs | 1 + .../{helpers.rs => tests/helpers/mod.rs} | 0 contracts/oracle/wasm/tests/tests/mod.rs | 6 ++++ .../prop_tests.proptest-regressions | 0 .../wasm/tests/{ => tests}/prop_tests.rs | 0 .../wasm/tests/{ => tests}/test_migrate.rs | 0 .../tests/{ => tests}/test_price_source.rs | 2 +- .../tests/{ => tests}/test_update_admin.rs | 0 contracts/params/tests/all_tests.rs | 1 + .../tests/{ => tests}/helpers/assertions.rs | 0 .../tests/{ => tests}/helpers/contracts.rs | 0 .../tests/{ => tests}/helpers/generator.rs | 0 .../tests/{ => tests}/helpers/mock_env.rs | 2 +- .../params/tests/{ => tests}/helpers/mod.rs | 0 contracts/params/tests/tests/mod.rs | 10 ++++++ .../{ => tests}/test_asset_validation.rs | 4 +-- .../tests/{ => tests}/test_deposit_cap.rs | 4 +-- .../{ => tests}/test_emergency_powers.rs | 4 +-- .../params/tests/{ => tests}/test_owner.rs | 4 +-- .../{ => tests}/test_target_health_factor.rs | 4 +-- .../{ => tests}/test_update_asset_params.rs | 4 +-- .../{ => tests}/test_vault_validation.rs | 4 +-- .../params/tests/{ => tests}/test_vaults.rs | 4 +-- contracts/red-bank/tests/all_tests.rs | 1 + .../{helpers.rs => tests/helpers/mod.rs} | 0 contracts/red-bank/tests/tests/mod.rs | 13 ++++++++ .../red-bank/tests/{ => tests}/test_admin.rs | 4 +-- .../red-bank/tests/{ => tests}/test_borrow.rs | 12 +++---- .../tests/{ => tests}/test_credit_accounts.rs | 3 +- .../tests/{ => tests}/test_deposit.rs | 10 +++--- .../red-bank/tests/{ => tests}/test_health.rs | 0 .../tests/{ => tests}/test_liquidate.rs | 8 ++--- .../red-bank/tests/{ => tests}/test_misc.rs | 13 +++----- .../tests/{ => tests}/test_payment.rs | 5 ++- .../red-bank/tests/{ => tests}/test_query.rs | 3 +- .../tests/{ => tests}/test_update_owner.rs | 4 +-- .../tests/{ => tests}/test_withdraw.rs | 11 +++---- .../osmosis/tests/all_tests.rs | 1 + .../{helpers.rs => tests/helpers/mod.rs} | 0 .../osmosis/tests/tests/mod.rs | 7 ++++ .../osmosis/tests/{ => tests}/test_admin.rs | 7 ++-- .../{ => tests}/test_distribute_rewards.rs | 2 +- .../osmosis/tests/{ => tests}/test_swap.rs | 4 +-- .../tests/{ => tests}/test_update_owner.rs | 4 +-- .../tests/{ => tests}/test_withdraw.rs | 2 +- .../swapper/astroport/tests/all_tests.rs | 1 + .../swapper/astroport/tests/tests/mod.rs | 4 +++ .../tests/{ => tests}/test_queries.rs | 0 .../tests/{ => tests}/test_routes.rs | 0 .../astroport/tests/{ => tests}/test_swap.rs | 0 .../tests/{ => tests}/test_transfer_result.rs | 0 contracts/swapper/osmosis/tests/all_tests.rs | 1 + .../{helpers.rs => tests/helpers/mod.rs} | 0 contracts/swapper/osmosis/tests/tests/mod.rs | 8 +++++ .../{ => tests}/test_enumerate_routes.rs | 4 +-- .../tests/{ => tests}/test_estimate.rs | 4 +-- .../tests/{ => tests}/test_instantiate.rs | 4 +-- .../tests/{ => tests}/test_set_route.rs | 4 +-- .../osmosis/tests/{ => tests}/test_swap.rs | 4 +-- .../tests/{ => tests}/test_update_admin.rs | 4 +-- 90 files changed, 175 insertions(+), 173 deletions(-) create mode 100644 contracts/address-provider/tests/all_tests.rs rename contracts/address-provider/tests/{helpers.rs => tests/helpers/mod.rs} (100%) create mode 100644 contracts/address-provider/tests/tests/mod.rs rename contracts/address-provider/tests/{ => tests}/test_addresses.rs (98%) rename contracts/address-provider/tests/{ => tests}/test_instantiate.rs (96%) rename contracts/address-provider/tests/{ => tests}/test_update_owner.rs (97%) create mode 100644 contracts/incentives/tests/all_tests.rs rename contracts/incentives/tests/{helpers.rs => tests/helpers/mod.rs} (100%) create mode 100644 contracts/incentives/tests/tests/mod.rs rename contracts/incentives/tests/{ => tests}/test_admin.rs (98%) rename contracts/incentives/tests/{ => tests}/test_balance_change.rs (99%) rename contracts/incentives/tests/{ => tests}/test_claim_rewards.rs (99%) rename contracts/incentives/tests/{ => tests}/test_indices_usage.rs (99%) rename contracts/incentives/tests/{ => tests}/test_quering.rs (92%) rename contracts/incentives/tests/{ => tests}/test_set_asset_incentive.rs (99%) rename contracts/incentives/tests/{ => tests}/test_update_owner.rs (97%) rename contracts/incentives/tests/{ => tests}/test_whitelist.rs (99%) create mode 100644 contracts/oracle/osmosis/tests/all_tests.rs rename contracts/oracle/osmosis/tests/{helpers.rs => tests/helpers/mod.rs} (100%) create mode 100644 contracts/oracle/osmosis/tests/tests/mod.rs rename contracts/oracle/osmosis/tests/{ => tests}/test_admin.rs (99%) rename contracts/oracle/osmosis/tests/{ => tests}/test_custom_execute.rs (96%) rename contracts/oracle/osmosis/tests/{ => tests}/test_price_source_fmt.rs (99%) rename contracts/oracle/osmosis/tests/{ => tests}/test_query_price.rs (99%) rename contracts/oracle/osmosis/tests/{ => tests}/test_query_price_for_pyth.rs (99%) rename contracts/oracle/osmosis/tests/{ => tests}/test_remove_price_source.rs (99%) rename contracts/oracle/osmosis/tests/{ => tests}/test_set_price_source.rs (99%) rename contracts/oracle/osmosis/tests/{ => tests}/test_update_owner.rs (97%) create mode 100644 contracts/oracle/wasm/tests/all_tests.rs rename contracts/oracle/wasm/tests/{helpers.rs => tests/helpers/mod.rs} (100%) create mode 100644 contracts/oracle/wasm/tests/tests/mod.rs rename contracts/oracle/wasm/tests/{ => tests}/prop_tests.proptest-regressions (100%) rename contracts/oracle/wasm/tests/{ => tests}/prop_tests.rs (100%) rename contracts/oracle/wasm/tests/{ => tests}/test_migrate.rs (100%) rename contracts/oracle/wasm/tests/{ => tests}/test_price_source.rs (99%) rename contracts/oracle/wasm/tests/{ => tests}/test_update_admin.rs (100%) create mode 100644 contracts/params/tests/all_tests.rs rename contracts/params/tests/{ => tests}/helpers/assertions.rs (100%) rename contracts/params/tests/{ => tests}/helpers/contracts.rs (100%) rename contracts/params/tests/{ => tests}/helpers/generator.rs (100%) rename contracts/params/tests/{ => tests}/helpers/mock_env.rs (99%) rename contracts/params/tests/{ => tests}/helpers/mod.rs (100%) create mode 100644 contracts/params/tests/tests/mod.rs rename contracts/params/tests/{ => tests}/test_asset_validation.rs (99%) rename contracts/params/tests/{ => tests}/test_deposit_cap.rs (97%) rename contracts/params/tests/{ => tests}/test_emergency_powers.rs (98%) rename contracts/params/tests/{ => tests}/test_owner.rs (94%) rename contracts/params/tests/{ => tests}/test_target_health_factor.rs (97%) rename contracts/params/tests/{ => tests}/test_update_asset_params.rs (98%) rename contracts/params/tests/{ => tests}/test_vault_validation.rs (98%) rename contracts/params/tests/{ => tests}/test_vaults.rs (98%) create mode 100644 contracts/red-bank/tests/all_tests.rs rename contracts/red-bank/tests/{helpers.rs => tests/helpers/mod.rs} (100%) create mode 100644 contracts/red-bank/tests/tests/mod.rs rename contracts/red-bank/tests/{ => tests}/test_admin.rs (99%) rename contracts/red-bank/tests/{ => tests}/test_borrow.rs (99%) rename contracts/red-bank/tests/{ => tests}/test_credit_accounts.rs (99%) rename contracts/red-bank/tests/{ => tests}/test_deposit.rs (98%) rename contracts/red-bank/tests/{ => tests}/test_health.rs (100%) rename contracts/red-bank/tests/{ => tests}/test_liquidate.rs (99%) rename contracts/red-bank/tests/{ => tests}/test_misc.rs (98%) rename contracts/red-bank/tests/{ => tests}/test_payment.rs (98%) rename contracts/red-bank/tests/{ => tests}/test_query.rs (99%) rename contracts/red-bank/tests/{ => tests}/test_update_owner.rs (97%) rename contracts/red-bank/tests/{ => tests}/test_withdraw.rs (99%) create mode 100644 contracts/rewards-collector/osmosis/tests/all_tests.rs rename contracts/rewards-collector/osmosis/tests/{helpers.rs => tests/helpers/mod.rs} (100%) create mode 100644 contracts/rewards-collector/osmosis/tests/tests/mod.rs rename contracts/rewards-collector/osmosis/tests/{ => tests}/test_admin.rs (98%) rename contracts/rewards-collector/osmosis/tests/{ => tests}/test_distribute_rewards.rs (99%) rename contracts/rewards-collector/osmosis/tests/{ => tests}/test_swap.rs (98%) rename contracts/rewards-collector/osmosis/tests/{ => tests}/test_update_owner.rs (97%) rename contracts/rewards-collector/osmosis/tests/{ => tests}/test_withdraw.rs (99%) create mode 100644 contracts/swapper/astroport/tests/all_tests.rs create mode 100644 contracts/swapper/astroport/tests/tests/mod.rs rename contracts/swapper/astroport/tests/{ => tests}/test_queries.rs (100%) rename contracts/swapper/astroport/tests/{ => tests}/test_routes.rs (100%) rename contracts/swapper/astroport/tests/{ => tests}/test_swap.rs (100%) rename contracts/swapper/astroport/tests/{ => tests}/test_transfer_result.rs (100%) create mode 100644 contracts/swapper/osmosis/tests/all_tests.rs rename contracts/swapper/osmosis/tests/{helpers.rs => tests/helpers/mod.rs} (100%) create mode 100644 contracts/swapper/osmosis/tests/tests/mod.rs rename contracts/swapper/osmosis/tests/{ => tests}/test_enumerate_routes.rs (98%) rename contracts/swapper/osmosis/tests/{ => tests}/test_estimate.rs (99%) rename contracts/swapper/osmosis/tests/{ => tests}/test_instantiate.rs (94%) rename contracts/swapper/osmosis/tests/{ => tests}/test_set_route.rs (99%) rename contracts/swapper/osmosis/tests/{ => tests}/test_swap.rs (99%) rename contracts/swapper/osmosis/tests/{ => tests}/test_update_admin.rs (98%) diff --git a/.cargo/audit.toml b/.cargo/audit.toml index 80dec5a81..65c70990d 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -3,4 +3,4 @@ [advisories] # Ignore the following advisory IDs. # RUSTSEC-2022-0093 is reported for test-tube which is only used for testing. -ignore = ["RUSTSEC-2022-0093"] \ No newline at end of file +ignore = ["RUSTSEC-2022-0093", "RUSTSEC-2023-0052"] \ No newline at end of file diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 010df50c6..e876778b7 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -29,15 +29,11 @@ jobs: - name: Compile workspace run: cargo make build - - name: Run test - run: cargo make test - - # disabled because of "no space left" error. - # - name: Run test coverage - # run: cargo make coverage-lcov - - # - name: Upload coverage to Codecov - # uses: codecov/codecov-action@v3 - # with: - # token: ${{ secrets.CODECOV_TOKEN }} - # files: target/coverage/lcov.info + - name: Run test coverage + run: cargo make coverage-lcov + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: target/coverage/lcov.info diff --git a/contracts/address-provider/tests/all_tests.rs b/contracts/address-provider/tests/all_tests.rs new file mode 100644 index 000000000..14f00389d --- /dev/null +++ b/contracts/address-provider/tests/all_tests.rs @@ -0,0 +1 @@ +mod tests; diff --git a/contracts/address-provider/tests/helpers.rs b/contracts/address-provider/tests/tests/helpers/mod.rs similarity index 100% rename from contracts/address-provider/tests/helpers.rs rename to contracts/address-provider/tests/tests/helpers/mod.rs diff --git a/contracts/address-provider/tests/tests/mod.rs b/contracts/address-provider/tests/tests/mod.rs new file mode 100644 index 000000000..d89823fb5 --- /dev/null +++ b/contracts/address-provider/tests/tests/mod.rs @@ -0,0 +1,5 @@ +mod helpers; + +mod test_addresses; +mod test_instantiate; +mod test_update_owner; diff --git a/contracts/address-provider/tests/test_addresses.rs b/contracts/address-provider/tests/tests/test_addresses.rs similarity index 98% rename from contracts/address-provider/tests/test_addresses.rs rename to contracts/address-provider/tests/tests/test_addresses.rs index be3d1b1ae..ced40337a 100644 --- a/contracts/address-provider/tests/test_addresses.rs +++ b/contracts/address-provider/tests/tests/test_addresses.rs @@ -5,9 +5,7 @@ use mars_red_bank_types::address_provider::{ AddressResponseItem, ExecuteMsg, MarsAddressType, QueryMsg, }; -use crate::helpers::{th_query, th_setup}; - -mod helpers; +use super::helpers::{th_query, th_setup}; #[test] fn setting_address_if_unauthorized() { diff --git a/contracts/address-provider/tests/test_instantiate.rs b/contracts/address-provider/tests/tests/test_instantiate.rs similarity index 96% rename from contracts/address-provider/tests/test_instantiate.rs rename to contracts/address-provider/tests/tests/test_instantiate.rs index d075a9ce8..71c36766c 100644 --- a/contracts/address-provider/tests/test_instantiate.rs +++ b/contracts/address-provider/tests/tests/test_instantiate.rs @@ -2,9 +2,7 @@ use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use mars_address_provider::{contract::instantiate, error::ContractError}; use mars_red_bank_types::address_provider::{ConfigResponse, InstantiateMsg, QueryMsg}; -use crate::helpers::th_query; - -mod helpers; +use super::helpers::th_query; #[test] fn invalid_chain_prefix() { diff --git a/contracts/address-provider/tests/test_update_owner.rs b/contracts/address-provider/tests/tests/test_update_owner.rs similarity index 97% rename from contracts/address-provider/tests/test_update_owner.rs rename to contracts/address-provider/tests/tests/test_update_owner.rs index 511aefdca..a61716795 100644 --- a/contracts/address-provider/tests/test_update_owner.rs +++ b/contracts/address-provider/tests/tests/test_update_owner.rs @@ -3,9 +3,7 @@ use mars_address_provider::{contract::execute, error::ContractError}; use mars_owner::{OwnerError::NotOwner, OwnerUpdate}; use mars_red_bank_types::address_provider::{ConfigResponse, ExecuteMsg, QueryMsg}; -use crate::helpers::{th_query, th_setup}; - -mod helpers; +use super::helpers::{th_query, th_setup}; #[test] fn initialized_state() { diff --git a/contracts/incentives/tests/all_tests.rs b/contracts/incentives/tests/all_tests.rs new file mode 100644 index 000000000..14f00389d --- /dev/null +++ b/contracts/incentives/tests/all_tests.rs @@ -0,0 +1 @@ +mod tests; diff --git a/contracts/incentives/tests/helpers.rs b/contracts/incentives/tests/tests/helpers/mod.rs similarity index 100% rename from contracts/incentives/tests/helpers.rs rename to contracts/incentives/tests/tests/helpers/mod.rs diff --git a/contracts/incentives/tests/tests/mod.rs b/contracts/incentives/tests/tests/mod.rs new file mode 100644 index 000000000..85b241bca --- /dev/null +++ b/contracts/incentives/tests/tests/mod.rs @@ -0,0 +1,10 @@ +mod helpers; + +mod test_admin; +mod test_balance_change; +mod test_claim_rewards; +mod test_indices_usage; +mod test_quering; +mod test_set_asset_incentive; +mod test_update_owner; +mod test_whitelist; diff --git a/contracts/incentives/tests/test_admin.rs b/contracts/incentives/tests/tests/test_admin.rs similarity index 98% rename from contracts/incentives/tests/test_admin.rs rename to contracts/incentives/tests/tests/test_admin.rs index fc0807433..628272a19 100644 --- a/contracts/incentives/tests/test_admin.rs +++ b/contracts/incentives/tests/tests/test_admin.rs @@ -10,9 +10,7 @@ use mars_owner::OwnerError::NotOwner; use mars_red_bank_types::incentives::{ConfigResponse, ExecuteMsg, InstantiateMsg, QueryMsg}; use mars_testing::mock_dependencies; -use crate::helpers::{th_query, th_setup}; - -mod helpers; +use super::helpers::{th_query, th_setup}; #[test] fn proper_initialization() { diff --git a/contracts/incentives/tests/test_balance_change.rs b/contracts/incentives/tests/tests/test_balance_change.rs similarity index 99% rename from contracts/incentives/tests/test_balance_change.rs rename to contracts/incentives/tests/tests/test_balance_change.rs index 07b49c8e8..e68a68458 100644 --- a/contracts/incentives/tests/test_balance_change.rs +++ b/contracts/incentives/tests/tests/test_balance_change.rs @@ -15,9 +15,7 @@ use mars_red_bank_types::{ }; use mars_testing::MockEnvParams; -use crate::helpers::{th_setup, ths_setup_with_epoch_duration}; - -mod helpers; +use super::helpers::{th_setup, ths_setup_with_epoch_duration}; #[test] fn balance_change_unauthorized() { diff --git a/contracts/incentives/tests/test_claim_rewards.rs b/contracts/incentives/tests/tests/test_claim_rewards.rs similarity index 99% rename from contracts/incentives/tests/test_claim_rewards.rs rename to contracts/incentives/tests/tests/test_claim_rewards.rs index af545c28d..bb090bde1 100644 --- a/contracts/incentives/tests/test_claim_rewards.rs +++ b/contracts/incentives/tests/tests/test_claim_rewards.rs @@ -14,9 +14,7 @@ use mars_red_bank_types::{ }; use mars_testing::MockEnvParams; -use crate::helpers::{th_setup, ths_setup_with_epoch_duration}; - -mod helpers; +use super::helpers::{th_setup, ths_setup_with_epoch_duration}; #[test] fn execute_claim_rewards() { diff --git a/contracts/incentives/tests/test_indices_usage.rs b/contracts/incentives/tests/tests/test_indices_usage.rs similarity index 99% rename from contracts/incentives/tests/test_indices_usage.rs rename to contracts/incentives/tests/tests/test_indices_usage.rs index 78a2ce8e0..e57ae7026 100644 --- a/contracts/incentives/tests/test_indices_usage.rs +++ b/contracts/incentives/tests/tests/test_indices_usage.rs @@ -11,8 +11,6 @@ use mars_incentives::{ }; use mars_red_bank_types::incentives::{Config, IncentiveState}; -mod helpers; - fn store_config_with_epoch_duration(storage: &mut dyn Storage, epoch_duration: u64) { CONFIG .save( diff --git a/contracts/incentives/tests/test_quering.rs b/contracts/incentives/tests/tests/test_quering.rs similarity index 92% rename from contracts/incentives/tests/test_quering.rs rename to contracts/incentives/tests/tests/test_quering.rs index c041f5a21..ba4a12f5c 100644 --- a/contracts/incentives/tests/test_quering.rs +++ b/contracts/incentives/tests/tests/test_quering.rs @@ -6,9 +6,7 @@ use mars_red_bank_types::incentives::{ use mars_testing::{mock_env, MockEnvParams}; use test_case::test_case; -use crate::helpers::th_setup; - -mod helpers; +use super::helpers::{th_query, th_query_with_env, th_setup}; #[test] fn query_incentive_state() { @@ -31,7 +29,7 @@ fn query_incentive_state() { }; INCENTIVE_STATES.save(deps.as_mut().storage, ("uusdc", "umars"), &uusdc_incentive).unwrap(); - let res: IncentiveStateResponse = helpers::th_query( + let res: IncentiveStateResponse = th_query( deps.as_ref(), QueryMsg::IncentiveState { collateral_denom: "uatom".to_string(), @@ -66,7 +64,7 @@ fn query_incentive_states() { INCENTIVE_STATES.save(deps.as_mut().storage, ("uusdc", "umars"), &uusdc_incentive).unwrap(); // NOTE: responses are ordered alphabetically by denom - let res: Vec = helpers::th_query( + let res: Vec = th_query( deps.as_ref(), QueryMsg::IncentiveStates { start_after_collateral_denom: None, @@ -88,7 +86,7 @@ fn query_incentive_states() { ); // NOTE: responses are ordered alphabetically by denom - let res: Vec = helpers::th_query( + let res: Vec = th_query( deps.as_ref(), QueryMsg::IncentiveStates { start_after_collateral_denom: Some("uatom".to_string()), @@ -116,7 +114,7 @@ fn query_emission() { .unwrap(); // Query before emission start - let res: Uint128 = helpers::th_query( + let res: Uint128 = th_query( deps.as_ref(), QueryMsg::Emission { collateral_denom: "uosmo".to_string(), @@ -127,7 +125,7 @@ fn query_emission() { assert_eq!(res, Uint128::zero()); // Query at timestamp of first emission start - let res: Uint128 = helpers::th_query( + let res: Uint128 = th_query( deps.as_ref(), QueryMsg::Emission { collateral_denom: "uosmo".to_string(), @@ -138,7 +136,7 @@ fn query_emission() { assert_eq!(res, Uint128::new(100)); // Query at timestamp of second emission start - let res: Uint128 = helpers::th_query( + let res: Uint128 = th_query( deps.as_ref(), QueryMsg::Emission { collateral_denom: "uosmo".to_string(), @@ -149,7 +147,7 @@ fn query_emission() { assert_eq!(res, Uint128::new(50)); // Query one second before second emission start - let res: Uint128 = helpers::th_query( + let res: Uint128 = th_query( deps.as_ref(), QueryMsg::Emission { collateral_denom: "uosmo".to_string(), @@ -160,7 +158,7 @@ fn query_emission() { assert_eq!(res, Uint128::new(100)); // Query at timestamp some time into second emission start - let res: Uint128 = helpers::th_query( + let res: Uint128 = th_query( deps.as_ref(), QueryMsg::Emission { collateral_denom: "uosmo".to_string(), @@ -171,7 +169,7 @@ fn query_emission() { assert_eq!(res, Uint128::new(50)); // Query the second before emission end - let res: Uint128 = helpers::th_query( + let res: Uint128 = th_query( deps.as_ref(), QueryMsg::Emission { collateral_denom: "uosmo".to_string(), @@ -182,7 +180,7 @@ fn query_emission() { assert_eq!(res, Uint128::new(50)); // Query the second after emission end - let res: Uint128 = helpers::th_query( + let res: Uint128 = th_query( deps.as_ref(), QueryMsg::Emission { collateral_denom: "uosmo".to_string(), @@ -203,7 +201,7 @@ fn query_emissions() { .save(deps.as_mut().storage, ("uusdc", "umars", 604800 * 2), &Uint128::new(50)) .unwrap(); - let res: Vec = helpers::th_query( + let res: Vec = th_query( deps.as_ref(), QueryMsg::Emissions { collateral_denom: "uusdc".to_string(), @@ -221,7 +219,7 @@ fn query_emissions() { ] ); - let res: Vec = helpers::th_query( + let res: Vec = th_query( deps.as_ref(), QueryMsg::Emissions { collateral_denom: "uusdc".to_string(), @@ -238,7 +236,7 @@ fn query_emissions() { ] ); - let res: Vec = helpers::th_query( + let res: Vec = th_query( deps.as_ref(), QueryMsg::Emissions { collateral_denom: "uusdc".to_string(), @@ -297,7 +295,7 @@ fn query_active_emissions(query_at_time: u64) -> Vec<(String, Uint128)> { .save(deps.as_mut().storage, ("uusdc", "uosmo", 604800 * 2), &Uint128::new(100)) .unwrap(); - helpers::th_query_with_env::>( + th_query_with_env::>( deps.as_ref(), mock_env(MockEnvParams { block_time: Timestamp::from_seconds(query_at_time), diff --git a/contracts/incentives/tests/test_set_asset_incentive.rs b/contracts/incentives/tests/tests/test_set_asset_incentive.rs similarity index 99% rename from contracts/incentives/tests/test_set_asset_incentive.rs rename to contracts/incentives/tests/tests/test_set_asset_incentive.rs index a508b1706..bf24c9d07 100644 --- a/contracts/incentives/tests/test_set_asset_incentive.rs +++ b/contracts/incentives/tests/tests/test_set_asset_incentive.rs @@ -12,12 +12,10 @@ use mars_red_bank_types::{incentives::ExecuteMsg, red_bank::Market}; use mars_testing::MockEnvParams; use mars_utils::error::ValidationError; -use crate::helpers::{ +use super::helpers::{ th_setup, th_setup_with_env, th_whitelist_denom, ths_setup_with_epoch_duration, }; -mod helpers; - const ONE_WEEK_IN_SECS: u64 = 604800; #[test] diff --git a/contracts/incentives/tests/test_update_owner.rs b/contracts/incentives/tests/tests/test_update_owner.rs similarity index 97% rename from contracts/incentives/tests/test_update_owner.rs rename to contracts/incentives/tests/tests/test_update_owner.rs index 00b7dc4ba..3989240ad 100644 --- a/contracts/incentives/tests/test_update_owner.rs +++ b/contracts/incentives/tests/tests/test_update_owner.rs @@ -3,9 +3,7 @@ use mars_incentives::{contract::execute, ContractError}; use mars_owner::{OwnerError::NotOwner, OwnerUpdate}; use mars_red_bank_types::incentives::{ConfigResponse, ExecuteMsg, QueryMsg}; -use crate::helpers::{th_query, th_setup}; - -mod helpers; +use super::helpers::{th_query, th_setup}; #[test] fn initialized_state() { diff --git a/contracts/incentives/tests/test_whitelist.rs b/contracts/incentives/tests/tests/test_whitelist.rs similarity index 99% rename from contracts/incentives/tests/test_whitelist.rs rename to contracts/incentives/tests/tests/test_whitelist.rs index 959e1d09b..120097fb3 100644 --- a/contracts/incentives/tests/test_whitelist.rs +++ b/contracts/incentives/tests/tests/test_whitelist.rs @@ -16,12 +16,10 @@ use mars_red_bank_types::{ use mars_testing::MockEnvParams; use mars_utils::error::ValidationError; -use crate::helpers::{ +use super::helpers::{ th_query, th_query_with_env, th_setup, th_setup_with_env, ths_setup_with_epoch_duration, }; -mod helpers; - #[test] fn initialized_state() { let deps = th_setup(); diff --git a/contracts/oracle/osmosis/tests/all_tests.rs b/contracts/oracle/osmosis/tests/all_tests.rs new file mode 100644 index 000000000..14f00389d --- /dev/null +++ b/contracts/oracle/osmosis/tests/all_tests.rs @@ -0,0 +1 @@ +mod tests; diff --git a/contracts/oracle/osmosis/tests/helpers.rs b/contracts/oracle/osmosis/tests/tests/helpers/mod.rs similarity index 100% rename from contracts/oracle/osmosis/tests/helpers.rs rename to contracts/oracle/osmosis/tests/tests/helpers/mod.rs diff --git a/contracts/oracle/osmosis/tests/tests/mod.rs b/contracts/oracle/osmosis/tests/tests/mod.rs new file mode 100644 index 000000000..13eec4284 --- /dev/null +++ b/contracts/oracle/osmosis/tests/tests/mod.rs @@ -0,0 +1,10 @@ +mod helpers; + +mod test_admin; +mod test_custom_execute; +mod test_price_source_fmt; +mod test_query_price; +mod test_query_price_for_pyth; +mod test_remove_price_source; +mod test_set_price_source; +mod test_update_owner; diff --git a/contracts/oracle/osmosis/tests/test_admin.rs b/contracts/oracle/osmosis/tests/tests/test_admin.rs similarity index 99% rename from contracts/oracle/osmosis/tests/test_admin.rs rename to contracts/oracle/osmosis/tests/tests/test_admin.rs index 5812bc5e8..06758b60c 100644 --- a/contracts/oracle/osmosis/tests/test_admin.rs +++ b/contracts/oracle/osmosis/tests/tests/test_admin.rs @@ -6,7 +6,7 @@ use mars_red_bank_types::oracle::{ConfigResponse, InstantiateMsg, QueryMsg}; use mars_testing::{mock_dependencies, mock_info}; use mars_utils::error::ValidationError; -mod helpers; +use super::helpers; #[test] fn instantiating() { diff --git a/contracts/oracle/osmosis/tests/test_custom_execute.rs b/contracts/oracle/osmosis/tests/tests/test_custom_execute.rs similarity index 96% rename from contracts/oracle/osmosis/tests/test_custom_execute.rs rename to contracts/oracle/osmosis/tests/tests/test_custom_execute.rs index e8405a703..a0b7e8fcc 100644 --- a/contracts/oracle/osmosis/tests/test_custom_execute.rs +++ b/contracts/oracle/osmosis/tests/tests/test_custom_execute.rs @@ -4,7 +4,7 @@ use mars_oracle_osmosis::contract::entry; use mars_red_bank_types::oracle::ExecuteMsg; use mars_testing::mock_info; -mod helpers; +use super::helpers; #[test] fn custom_execute() { diff --git a/contracts/oracle/osmosis/tests/test_price_source_fmt.rs b/contracts/oracle/osmosis/tests/tests/test_price_source_fmt.rs similarity index 99% rename from contracts/oracle/osmosis/tests/test_price_source_fmt.rs rename to contracts/oracle/osmosis/tests/tests/test_price_source_fmt.rs index 99bd613d1..0ece290c8 100644 --- a/contracts/oracle/osmosis/tests/test_price_source_fmt.rs +++ b/contracts/oracle/osmosis/tests/tests/test_price_source_fmt.rs @@ -4,8 +4,6 @@ use mars_oracle_osmosis::{ }; use pyth_sdk_cw::PriceIdentifier; -mod helpers; - #[test] fn display_downtime_detector() { let dd = DowntimeDetector { diff --git a/contracts/oracle/osmosis/tests/test_query_price.rs b/contracts/oracle/osmosis/tests/tests/test_query_price.rs similarity index 99% rename from contracts/oracle/osmosis/tests/test_query_price.rs rename to contracts/oracle/osmosis/tests/tests/test_query_price.rs index dc5a1c611..c16f18281 100644 --- a/contracts/oracle/osmosis/tests/test_query_price.rs +++ b/contracts/oracle/osmosis/tests/tests/test_query_price.rs @@ -5,6 +5,7 @@ use cosmwasm_std::{ testing::{mock_env, MockApi, MockStorage}, Decimal, OwnedDeps, StdError, }; +use helpers::prepare_query_balancer_pool_response; use mars_oracle_base::{pyth::scale_pyth_price, ContractError}; use mars_oracle_osmosis::{ contract::entry, stride::RedemptionRateResponse, Downtime, DowntimeDetector, GeometricTwap, @@ -18,9 +19,7 @@ use osmosis_std::types::osmosis::{ }; use pyth_sdk_cw::{Price, PriceFeed, PriceFeedResponse, PriceIdentifier}; -use crate::helpers::prepare_query_balancer_pool_response; - -mod helpers; +use super::helpers; #[test] fn querying_fixed_price() { diff --git a/contracts/oracle/osmosis/tests/test_query_price_for_pyth.rs b/contracts/oracle/osmosis/tests/tests/test_query_price_for_pyth.rs similarity index 99% rename from contracts/oracle/osmosis/tests/test_query_price_for_pyth.rs rename to contracts/oracle/osmosis/tests/tests/test_query_price_for_pyth.rs index 3effd960e..be8145645 100644 --- a/contracts/oracle/osmosis/tests/test_query_price_for_pyth.rs +++ b/contracts/oracle/osmosis/tests/tests/test_query_price_for_pyth.rs @@ -5,7 +5,7 @@ use mars_red_bank_types::oracle::{ActionKind, PriceResponse, QueryMsg}; use mars_testing::mock_env_at_block_time; use pyth_sdk_cw::{Price, PriceFeed, PriceFeedResponse, PriceIdentifier}; -mod helpers; +use super::helpers; #[test] fn querying_default_pyth_price_if_publish_price_too_old() { diff --git a/contracts/oracle/osmosis/tests/test_remove_price_source.rs b/contracts/oracle/osmosis/tests/tests/test_remove_price_source.rs similarity index 99% rename from contracts/oracle/osmosis/tests/test_remove_price_source.rs rename to contracts/oracle/osmosis/tests/tests/test_remove_price_source.rs index 7e41ffc42..330622199 100644 --- a/contracts/oracle/osmosis/tests/test_remove_price_source.rs +++ b/contracts/oracle/osmosis/tests/tests/test_remove_price_source.rs @@ -9,7 +9,7 @@ use mars_owner::OwnerError::NotOwner; use mars_red_bank_types::oracle::msg::QueryMsg; use mars_testing::mock_info; -mod helpers; +use super::helpers; #[test] fn remove_price_source_by_non_owner() { diff --git a/contracts/oracle/osmosis/tests/test_set_price_source.rs b/contracts/oracle/osmosis/tests/tests/test_set_price_source.rs similarity index 99% rename from contracts/oracle/osmosis/tests/test_set_price_source.rs rename to contracts/oracle/osmosis/tests/tests/test_set_price_source.rs index 9048dbab6..e47d703ec 100644 --- a/contracts/oracle/osmosis/tests/test_set_price_source.rs +++ b/contracts/oracle/osmosis/tests/tests/test_set_price_source.rs @@ -14,7 +14,7 @@ use mars_testing::mock_info; use mars_utils::error::ValidationError; use pyth_sdk_cw::PriceIdentifier; -mod helpers; +use super::helpers; #[test] fn setting_price_source_by_non_owner() { diff --git a/contracts/oracle/osmosis/tests/test_update_owner.rs b/contracts/oracle/osmosis/tests/tests/test_update_owner.rs similarity index 97% rename from contracts/oracle/osmosis/tests/test_update_owner.rs rename to contracts/oracle/osmosis/tests/tests/test_update_owner.rs index 21130aa84..c56f49c64 100644 --- a/contracts/oracle/osmosis/tests/test_update_owner.rs +++ b/contracts/oracle/osmosis/tests/tests/test_update_owner.rs @@ -1,12 +1,11 @@ use cosmwasm_std::testing::{mock_env, mock_info}; +use helpers::{query, setup_test_with_pools}; use mars_oracle_base::ContractError; use mars_oracle_osmosis::contract::entry::execute; use mars_owner::{OwnerError::NotOwner, OwnerUpdate}; use mars_red_bank_types::oracle::msg::{ConfigResponse, ExecuteMsg, QueryMsg}; -use crate::helpers::{query, setup_test_with_pools}; - -mod helpers; +use super::helpers; #[test] fn initialized_state() { diff --git a/contracts/oracle/wasm/tests/all_tests.rs b/contracts/oracle/wasm/tests/all_tests.rs new file mode 100644 index 000000000..14f00389d --- /dev/null +++ b/contracts/oracle/wasm/tests/all_tests.rs @@ -0,0 +1 @@ +mod tests; diff --git a/contracts/oracle/wasm/tests/helpers.rs b/contracts/oracle/wasm/tests/tests/helpers/mod.rs similarity index 100% rename from contracts/oracle/wasm/tests/helpers.rs rename to contracts/oracle/wasm/tests/tests/helpers/mod.rs diff --git a/contracts/oracle/wasm/tests/tests/mod.rs b/contracts/oracle/wasm/tests/tests/mod.rs new file mode 100644 index 000000000..2d3303374 --- /dev/null +++ b/contracts/oracle/wasm/tests/tests/mod.rs @@ -0,0 +1,6 @@ +mod helpers; + +mod prop_tests; +mod test_migrate; +mod test_price_source; +mod test_update_admin; diff --git a/contracts/oracle/wasm/tests/prop_tests.proptest-regressions b/contracts/oracle/wasm/tests/tests/prop_tests.proptest-regressions similarity index 100% rename from contracts/oracle/wasm/tests/prop_tests.proptest-regressions rename to contracts/oracle/wasm/tests/tests/prop_tests.proptest-regressions diff --git a/contracts/oracle/wasm/tests/prop_tests.rs b/contracts/oracle/wasm/tests/tests/prop_tests.rs similarity index 100% rename from contracts/oracle/wasm/tests/prop_tests.rs rename to contracts/oracle/wasm/tests/tests/prop_tests.rs diff --git a/contracts/oracle/wasm/tests/test_migrate.rs b/contracts/oracle/wasm/tests/tests/test_migrate.rs similarity index 100% rename from contracts/oracle/wasm/tests/test_migrate.rs rename to contracts/oracle/wasm/tests/tests/test_migrate.rs diff --git a/contracts/oracle/wasm/tests/test_price_source.rs b/contracts/oracle/wasm/tests/tests/test_price_source.rs similarity index 99% rename from contracts/oracle/wasm/tests/test_price_source.rs rename to contracts/oracle/wasm/tests/tests/test_price_source.rs index 4c5b4f5f6..18e0353ee 100644 --- a/contracts/oracle/wasm/tests/test_price_source.rs +++ b/contracts/oracle/wasm/tests/tests/test_price_source.rs @@ -40,7 +40,7 @@ use mars_testing::{ use pyth_sdk_cw::{Price, PriceFeed, PriceFeedResponse}; use test_case::test_case; -mod helpers; +use super::helpers; #[test] fn test_contract_initialization() { diff --git a/contracts/oracle/wasm/tests/test_update_admin.rs b/contracts/oracle/wasm/tests/tests/test_update_admin.rs similarity index 100% rename from contracts/oracle/wasm/tests/test_update_admin.rs rename to contracts/oracle/wasm/tests/tests/test_update_admin.rs diff --git a/contracts/params/tests/all_tests.rs b/contracts/params/tests/all_tests.rs new file mode 100644 index 000000000..14f00389d --- /dev/null +++ b/contracts/params/tests/all_tests.rs @@ -0,0 +1 @@ +mod tests; diff --git a/contracts/params/tests/helpers/assertions.rs b/contracts/params/tests/tests/helpers/assertions.rs similarity index 100% rename from contracts/params/tests/helpers/assertions.rs rename to contracts/params/tests/tests/helpers/assertions.rs diff --git a/contracts/params/tests/helpers/contracts.rs b/contracts/params/tests/tests/helpers/contracts.rs similarity index 100% rename from contracts/params/tests/helpers/contracts.rs rename to contracts/params/tests/tests/helpers/contracts.rs diff --git a/contracts/params/tests/helpers/generator.rs b/contracts/params/tests/tests/helpers/generator.rs similarity index 100% rename from contracts/params/tests/helpers/generator.rs rename to contracts/params/tests/tests/helpers/generator.rs diff --git a/contracts/params/tests/helpers/mock_env.rs b/contracts/params/tests/tests/helpers/mock_env.rs similarity index 99% rename from contracts/params/tests/helpers/mock_env.rs rename to contracts/params/tests/tests/helpers/mock_env.rs index 729a9d331..f812739ba 100644 --- a/contracts/params/tests/helpers/mock_env.rs +++ b/contracts/params/tests/tests/helpers/mock_env.rs @@ -11,7 +11,7 @@ use mars_params::{ types::{asset::AssetParams, vault::VaultConfig}, }; -use crate::helpers::mock_params_contract; +use super::contracts::mock_params_contract; pub struct MockEnv { pub app: BasicApp, diff --git a/contracts/params/tests/helpers/mod.rs b/contracts/params/tests/tests/helpers/mod.rs similarity index 100% rename from contracts/params/tests/helpers/mod.rs rename to contracts/params/tests/tests/helpers/mod.rs diff --git a/contracts/params/tests/tests/mod.rs b/contracts/params/tests/tests/mod.rs new file mode 100644 index 000000000..bdfca4340 --- /dev/null +++ b/contracts/params/tests/tests/mod.rs @@ -0,0 +1,10 @@ +mod helpers; + +mod test_asset_validation; +mod test_deposit_cap; +mod test_emergency_powers; +mod test_owner; +mod test_target_health_factor; +mod test_update_asset_params; +mod test_vault_validation; +mod test_vaults; diff --git a/contracts/params/tests/test_asset_validation.rs b/contracts/params/tests/tests/test_asset_validation.rs similarity index 99% rename from contracts/params/tests/test_asset_validation.rs rename to contracts/params/tests/tests/test_asset_validation.rs index 0c7f7b352..5dd216883 100644 --- a/contracts/params/tests/test_asset_validation.rs +++ b/contracts/params/tests/tests/test_asset_validation.rs @@ -8,9 +8,7 @@ use mars_params::{ }; use mars_utils::error::ValidationError::{InvalidDenom, InvalidParam}; -use crate::helpers::{assert_err, default_asset_params, MockEnv}; - -pub mod helpers; +use super::helpers::{assert_err, default_asset_params, MockEnv}; #[test] fn denom_must_be_native() { diff --git a/contracts/params/tests/test_deposit_cap.rs b/contracts/params/tests/tests/test_deposit_cap.rs similarity index 97% rename from contracts/params/tests/test_deposit_cap.rs rename to contracts/params/tests/tests/test_deposit_cap.rs index 7da5648af..63a51a60c 100644 --- a/contracts/params/tests/test_deposit_cap.rs +++ b/contracts/params/tests/tests/test_deposit_cap.rs @@ -1,5 +1,3 @@ -pub mod helpers; - use std::str::FromStr; use cosmwasm_std::{coins, Addr, Decimal, Uint128}; @@ -13,7 +11,7 @@ use mars_red_bank_types::red_bank::{Market, UserDebtResponse}; use mars_testing::{mock_dependencies, mock_env_at_block_time}; use test_case::test_case; -use crate::helpers::default_asset_params; +use super::helpers::default_asset_params; const CREDIT_MANAGER: &str = "credit_manager"; const MOCK_DENOM: &str = "utoken"; diff --git a/contracts/params/tests/test_emergency_powers.rs b/contracts/params/tests/tests/test_emergency_powers.rs similarity index 98% rename from contracts/params/tests/test_emergency_powers.rs rename to contracts/params/tests/tests/test_emergency_powers.rs index 748c972e0..5e4c08ac8 100644 --- a/contracts/params/tests/test_emergency_powers.rs +++ b/contracts/params/tests/tests/test_emergency_powers.rs @@ -8,9 +8,7 @@ use mars_params::{ }, }; -use crate::helpers::{assert_err, default_asset_params, default_vault_config, MockEnv}; - -pub mod helpers; +use super::helpers::{assert_err, default_asset_params, default_vault_config, MockEnv}; #[test] fn only_owner_can_invoke_emergency_powers() { diff --git a/contracts/params/tests/test_owner.rs b/contracts/params/tests/tests/test_owner.rs similarity index 94% rename from contracts/params/tests/test_owner.rs rename to contracts/params/tests/tests/test_owner.rs index df525b854..79ca8aaa6 100644 --- a/contracts/params/tests/test_owner.rs +++ b/contracts/params/tests/tests/test_owner.rs @@ -2,9 +2,7 @@ use cosmwasm_std::Addr; use mars_owner::{OwnerError, OwnerUpdate}; use mars_params::error::ContractError::Owner; -use crate::helpers::{assert_err, MockEnv}; - -pub mod helpers; +use super::helpers::{assert_err, MockEnv}; #[test] fn owner_set_on_init() { diff --git a/contracts/params/tests/test_target_health_factor.rs b/contracts/params/tests/tests/test_target_health_factor.rs similarity index 97% rename from contracts/params/tests/test_target_health_factor.rs rename to contracts/params/tests/tests/test_target_health_factor.rs index 47c2cf2a6..9eaa6f1ee 100644 --- a/contracts/params/tests/test_target_health_factor.rs +++ b/contracts/params/tests/tests/test_target_health_factor.rs @@ -5,9 +5,7 @@ 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; +use super::helpers::{assert_err, MockEnv}; #[test] fn thf_set_on_init() { diff --git a/contracts/params/tests/test_update_asset_params.rs b/contracts/params/tests/tests/test_update_asset_params.rs similarity index 98% rename from contracts/params/tests/test_update_asset_params.rs rename to contracts/params/tests/tests/test_update_asset_params.rs index f62b2792c..ee0601045 100644 --- a/contracts/params/tests/test_update_asset_params.rs +++ b/contracts/params/tests/tests/test_update_asset_params.rs @@ -2,9 +2,7 @@ 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; +use super::helpers::{assert_contents_equal, assert_err, default_asset_params, MockEnv}; #[test] fn initial_state_of_params() { diff --git a/contracts/params/tests/test_vault_validation.rs b/contracts/params/tests/tests/test_vault_validation.rs similarity index 98% rename from contracts/params/tests/test_vault_validation.rs rename to contracts/params/tests/tests/test_vault_validation.rs index d1b29a8d9..ecf4021bf 100644 --- a/contracts/params/tests/test_vault_validation.rs +++ b/contracts/params/tests/tests/test_vault_validation.rs @@ -8,9 +8,7 @@ use mars_params::{ }; use mars_utils::error::ValidationError::InvalidParam; -use crate::helpers::{assert_err, default_vault_config, MockEnv}; - -pub mod helpers; +use super::helpers::{assert_err, default_vault_config, MockEnv}; #[test] fn vault_addr_must_be_valid() { diff --git a/contracts/params/tests/test_vaults.rs b/contracts/params/tests/tests/test_vaults.rs similarity index 98% rename from contracts/params/tests/test_vaults.rs rename to contracts/params/tests/tests/test_vaults.rs index 84d0ab4d2..2f87735ae 100644 --- a/contracts/params/tests/test_vaults.rs +++ b/contracts/params/tests/tests/test_vaults.rs @@ -6,9 +6,7 @@ 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; +use super::helpers::{assert_contents_equal, assert_err, default_vault_config, MockEnv}; #[test] fn initial_state_of_vault_configs() { diff --git a/contracts/red-bank/tests/all_tests.rs b/contracts/red-bank/tests/all_tests.rs new file mode 100644 index 000000000..14f00389d --- /dev/null +++ b/contracts/red-bank/tests/all_tests.rs @@ -0,0 +1 @@ +mod tests; diff --git a/contracts/red-bank/tests/helpers.rs b/contracts/red-bank/tests/tests/helpers/mod.rs similarity index 100% rename from contracts/red-bank/tests/helpers.rs rename to contracts/red-bank/tests/tests/helpers/mod.rs diff --git a/contracts/red-bank/tests/tests/mod.rs b/contracts/red-bank/tests/tests/mod.rs new file mode 100644 index 000000000..63cccf890 --- /dev/null +++ b/contracts/red-bank/tests/tests/mod.rs @@ -0,0 +1,13 @@ +mod helpers; + +mod test_admin; +mod test_borrow; +mod test_credit_accounts; +mod test_deposit; +mod test_health; +mod test_liquidate; +mod test_misc; +mod test_payment; +mod test_query; +mod test_update_owner; +mod test_withdraw; diff --git a/contracts/red-bank/tests/test_admin.rs b/contracts/red-bank/tests/tests/test_admin.rs similarity index 99% rename from contracts/red-bank/tests/test_admin.rs rename to contracts/red-bank/tests/tests/test_admin.rs index ae6a9b162..2fb396789 100644 --- a/contracts/red-bank/tests/test_admin.rs +++ b/contracts/red-bank/tests/tests/test_admin.rs @@ -17,9 +17,7 @@ use mars_red_bank_types::{ use mars_testing::{mock_dependencies, mock_env, mock_env_at_block_time, MockEnvParams}; use mars_utils::error::ValidationError; -use crate::helpers::{th_get_expected_indices, th_init_market, th_setup}; - -mod helpers; +use super::helpers::{th_get_expected_indices, th_init_market, th_setup}; #[test] fn proper_initialization() { diff --git a/contracts/red-bank/tests/test_borrow.rs b/contracts/red-bank/tests/tests/test_borrow.rs similarity index 99% rename from contracts/red-bank/tests/test_borrow.rs rename to contracts/red-bank/tests/tests/test_borrow.rs index 7daa4ba90..6c56c2985 100644 --- a/contracts/red-bank/tests/test_borrow.rs +++ b/contracts/red-bank/tests/tests/test_borrow.rs @@ -2,10 +2,6 @@ use cosmwasm_std::{ attr, coin, coins, testing::mock_info, Addr, BankMsg, CosmosMsg, Decimal, SubMsg, Uint128, }; use cw_utils::PaymentError; -use helpers::{ - has_collateral_position, has_debt_position, set_collateral, th_build_interests_updated_event, - th_get_expected_indices_and_rates, th_init_market, th_setup, TestUtilizationDeltaInfo, -}; use mars_interest_rate::{ calculate_applied_linear_interest_rate, compute_scaled_amount, compute_underlying_amount, ScalingOperation, SCALING_FACTOR, @@ -19,9 +15,11 @@ use mars_red_bank::{ use mars_red_bank_types::red_bank::{ExecuteMsg, Market}; use mars_testing::{mock_env, mock_env_at_block_time, MockEnvParams}; -use crate::helpers::th_default_asset_params; - -mod helpers; +use super::helpers::{ + has_collateral_position, has_debt_position, set_collateral, th_build_interests_updated_event, + th_default_asset_params, th_get_expected_indices_and_rates, th_init_market, th_setup, + TestUtilizationDeltaInfo, +}; #[test] fn borrow_and_repay() { diff --git a/contracts/red-bank/tests/test_credit_accounts.rs b/contracts/red-bank/tests/tests/test_credit_accounts.rs similarity index 99% rename from contracts/red-bank/tests/test_credit_accounts.rs rename to contracts/red-bank/tests/tests/test_credit_accounts.rs index 84ae85caa..d8ef269ea 100644 --- a/contracts/red-bank/tests/test_credit_accounts.rs +++ b/contracts/red-bank/tests/tests/test_credit_accounts.rs @@ -1,13 +1,12 @@ use std::str::FromStr; use cosmwasm_std::{coin, Addr, Decimal, Uint128}; -use helpers::assert_err; use mars_params::types::asset::{AssetParams, CmSettings, LiquidationBonus, RedBankSettings}; use mars_red_bank::error::ContractError; use mars_red_bank_types::red_bank::{InitOrUpdateAssetParams, InterestRateModel, UserHealthStatus}; use mars_testing::integration::mock_env::MockEnvBuilder; -mod helpers; +use super::helpers::assert_err; #[test] fn deposit_and_withdraw_for_credit_account_works() { diff --git a/contracts/red-bank/tests/test_deposit.rs b/contracts/red-bank/tests/tests/test_deposit.rs similarity index 98% rename from contracts/red-bank/tests/test_deposit.rs rename to contracts/red-bank/tests/tests/test_deposit.rs index a10f508af..6593a5363 100644 --- a/contracts/red-bank/tests/test_deposit.rs +++ b/contracts/red-bank/tests/tests/test_deposit.rs @@ -6,9 +6,6 @@ use cosmwasm_std::{ to_binary, Addr, Decimal, OwnedDeps, StdError, SubMsg, Uint128, WasmMsg, }; use cw_utils::PaymentError; -use helpers::{ - set_collateral, th_build_interests_updated_event, th_get_expected_indices_and_rates, th_setup, -}; use mars_interest_rate::{ compute_scaled_amount, get_underlying_liquidity_amount, ScalingOperation, SCALING_FACTOR, }; @@ -26,9 +23,10 @@ use mars_red_bank_types::{ use mars_testing::{mock_env_at_block_time, MarsMockQuerier}; use test_case::test_case; -use crate::helpers::th_default_asset_params; - -mod helpers; +use super::helpers::{ + set_collateral, th_build_interests_updated_event, th_default_asset_params, + th_get_expected_indices_and_rates, th_setup, +}; struct TestSuite { deps: OwnedDeps, diff --git a/contracts/red-bank/tests/test_health.rs b/contracts/red-bank/tests/tests/test_health.rs similarity index 100% rename from contracts/red-bank/tests/test_health.rs rename to contracts/red-bank/tests/tests/test_health.rs diff --git a/contracts/red-bank/tests/test_liquidate.rs b/contracts/red-bank/tests/tests/test_liquidate.rs similarity index 99% rename from contracts/red-bank/tests/test_liquidate.rs rename to contracts/red-bank/tests/tests/test_liquidate.rs index 6dd854671..d20976e86 100644 --- a/contracts/red-bank/tests/test_liquidate.rs +++ b/contracts/red-bank/tests/tests/test_liquidate.rs @@ -6,7 +6,6 @@ use cosmwasm_std::{ to_binary, Addr, Decimal, SubMsg, Uint128, WasmMsg, }; use cw_utils::PaymentError; -use helpers::{th_get_expected_indices_and_rates, th_query, th_setup, TestUtilizationDeltaInfo}; use mars_params::types::asset::{AssetParams, CmSettings, LiquidationBonus, RedBankSettings}; use mars_red_bank::{contract::execute, error::ContractError}; use mars_red_bank_types::{ @@ -22,13 +21,12 @@ use mars_testing::{ mock_env_at_block_time, }; -use crate::helpers::{ +use super::helpers::{ assert_err, liq_threshold_hf, merge_collaterals_and_debts, th_build_interests_updated_event, - th_get_scaled_liquidity_amount, + th_get_expected_indices_and_rates, th_get_scaled_liquidity_amount, th_query, th_setup, + TestUtilizationDeltaInfo, }; -mod helpers; - // NOTE: See spreadsheet with liquidation numbers for reference: // contracts/red-bank/tests/files/Red Bank - Dynamic LB & CF test cases v1.1.xlsx diff --git a/contracts/red-bank/tests/test_misc.rs b/contracts/red-bank/tests/tests/test_misc.rs similarity index 98% rename from contracts/red-bank/tests/test_misc.rs rename to contracts/red-bank/tests/tests/test_misc.rs index bdee41932..a8b37f3e4 100644 --- a/contracts/red-bank/tests/test_misc.rs +++ b/contracts/red-bank/tests/tests/test_misc.rs @@ -1,11 +1,6 @@ use cosmwasm_std::{ attr, coin, coins, testing::mock_info, Addr, BankMsg, CosmosMsg, Decimal, SubMsg, Uint128, }; -use helpers::{ - has_collateral_enabled, has_collateral_position, has_debt_position, set_collateral, set_debt, - th_build_interests_updated_event, th_get_expected_indices_and_rates, th_init_market, th_setup, - TestUtilizationDeltaInfo, -}; use mars_interest_rate::{ compute_scaled_amount, compute_underlying_amount, get_scaled_debt_amount, get_updated_liquidity_index, ScalingOperation, SCALING_FACTOR, @@ -21,9 +16,11 @@ use mars_red_bank::{ use mars_red_bank_types::red_bank::{Debt, ExecuteMsg, Market}; use mars_testing::{mock_env, mock_env_at_block_time, MockEnvParams}; -use crate::helpers::th_default_asset_params; - -mod helpers; +use super::helpers::{ + has_collateral_enabled, has_collateral_position, has_debt_position, set_collateral, set_debt, + th_build_interests_updated_event, th_default_asset_params, th_get_expected_indices_and_rates, + th_init_market, th_setup, TestUtilizationDeltaInfo, +}; #[test] fn uncollateralized_loan_limits() { diff --git a/contracts/red-bank/tests/test_payment.rs b/contracts/red-bank/tests/tests/test_payment.rs similarity index 98% rename from contracts/red-bank/tests/test_payment.rs rename to contracts/red-bank/tests/tests/test_payment.rs index b3ef8dde7..454283f29 100644 --- a/contracts/red-bank/tests/test_payment.rs +++ b/contracts/red-bank/tests/tests/test_payment.rs @@ -1,15 +1,14 @@ -mod helpers; - use cosmwasm_std::{ coins, testing::{mock_env, mock_info}, Uint128, }; use cw_utils::PaymentError; -use helpers::th_setup; use mars_red_bank::contract; use mars_red_bank_types::red_bank::ExecuteMsg; +use super::helpers::th_setup; + /// The Red Bank contract has 6 user-facing functions: deposit, withdraw, borrow, /// repay, liquidate, and update_asset_collateral_status; amount these, 3 do not /// expect the user to send any payment. This test verifies that they properly diff --git a/contracts/red-bank/tests/test_query.rs b/contracts/red-bank/tests/tests/test_query.rs similarity index 99% rename from contracts/red-bank/tests/test_query.rs rename to contracts/red-bank/tests/tests/test_query.rs index 6547db7c2..1aa27173c 100644 --- a/contracts/red-bank/tests/test_query.rs +++ b/contracts/red-bank/tests/tests/test_query.rs @@ -1,5 +1,4 @@ use cosmwasm_std::{testing::mock_env, Addr, Decimal, Uint128}; -use helpers::{set_collateral, th_init_market, th_setup}; use mars_interest_rate::{get_scaled_debt_amount, get_underlying_debt_amount, SCALING_FACTOR}; use mars_red_bank::{ query::{query_user_collaterals, query_user_debt, query_user_debts}, @@ -7,7 +6,7 @@ use mars_red_bank::{ }; use mars_red_bank_types::red_bank::{Debt, Market, UserCollateralResponse, UserDebtResponse}; -mod helpers; +use super::helpers::{set_collateral, th_init_market, th_setup}; #[test] fn query_collateral() { diff --git a/contracts/red-bank/tests/test_update_owner.rs b/contracts/red-bank/tests/tests/test_update_owner.rs similarity index 97% rename from contracts/red-bank/tests/test_update_owner.rs rename to contracts/red-bank/tests/tests/test_update_owner.rs index 6960a59ba..8cd8975f5 100644 --- a/contracts/red-bank/tests/test_update_owner.rs +++ b/contracts/red-bank/tests/tests/test_update_owner.rs @@ -3,9 +3,7 @@ use mars_owner::{OwnerError::NotOwner, OwnerUpdate}; use mars_red_bank::{contract::execute, error::ContractError}; use mars_red_bank_types::red_bank::{ConfigResponse, ExecuteMsg, QueryMsg}; -use crate::helpers::{th_query, th_setup}; - -mod helpers; +use super::helpers::{th_query, th_setup}; #[test] fn initialized_state() { diff --git a/contracts/red-bank/tests/test_withdraw.rs b/contracts/red-bank/tests/tests/test_withdraw.rs similarity index 99% rename from contracts/red-bank/tests/test_withdraw.rs rename to contracts/red-bank/tests/tests/test_withdraw.rs index 4496a9c86..30180c77a 100644 --- a/contracts/red-bank/tests/test_withdraw.rs +++ b/contracts/red-bank/tests/tests/test_withdraw.rs @@ -3,10 +3,6 @@ use cosmwasm_std::{ testing::{mock_env, mock_info, MockApi, MockStorage}, to_binary, Addr, BankMsg, CosmosMsg, Decimal, OwnedDeps, SubMsg, Uint128, WasmMsg, }; -use helpers::{ - has_collateral_position, set_collateral, th_build_interests_updated_event, - th_get_expected_indices_and_rates, th_setup, TestUtilizationDeltaInfo, -}; use mars_interest_rate::{ compute_scaled_amount, compute_underlying_amount, get_scaled_liquidity_amount, get_updated_borrow_index, get_updated_liquidity_index, ScalingOperation, SCALING_FACTOR, @@ -24,9 +20,10 @@ use mars_red_bank_types::{ }; use mars_testing::{mock_env_at_block_time, MarsMockQuerier}; -use crate::helpers::th_default_asset_params; - -mod helpers; +use super::helpers::{ + has_collateral_position, set_collateral, th_build_interests_updated_event, + th_default_asset_params, th_get_expected_indices_and_rates, th_setup, TestUtilizationDeltaInfo, +}; struct TestSuite { deps: OwnedDeps, diff --git a/contracts/rewards-collector/osmosis/tests/all_tests.rs b/contracts/rewards-collector/osmosis/tests/all_tests.rs new file mode 100644 index 000000000..14f00389d --- /dev/null +++ b/contracts/rewards-collector/osmosis/tests/all_tests.rs @@ -0,0 +1 @@ +mod tests; diff --git a/contracts/rewards-collector/osmosis/tests/helpers.rs b/contracts/rewards-collector/osmosis/tests/tests/helpers/mod.rs similarity index 100% rename from contracts/rewards-collector/osmosis/tests/helpers.rs rename to contracts/rewards-collector/osmosis/tests/tests/helpers/mod.rs diff --git a/contracts/rewards-collector/osmosis/tests/tests/mod.rs b/contracts/rewards-collector/osmosis/tests/tests/mod.rs new file mode 100644 index 000000000..9bf248c6c --- /dev/null +++ b/contracts/rewards-collector/osmosis/tests/tests/mod.rs @@ -0,0 +1,7 @@ +mod helpers; + +mod test_admin; +mod test_distribute_rewards; +mod test_swap; +mod test_update_owner; +mod test_withdraw; diff --git a/contracts/rewards-collector/osmosis/tests/test_admin.rs b/contracts/rewards-collector/osmosis/tests/tests/test_admin.rs similarity index 98% rename from contracts/rewards-collector/osmosis/tests/test_admin.rs rename to contracts/rewards-collector/osmosis/tests/tests/test_admin.rs index 9fff2f58e..8cbe282b0 100644 --- a/contracts/rewards-collector/osmosis/tests/test_admin.rs +++ b/contracts/rewards-collector/osmosis/tests/tests/test_admin.rs @@ -6,9 +6,10 @@ use mars_rewards_collector_osmosis::entry::{execute, instantiate}; use mars_testing::mock_info; use mars_utils::error::ValidationError; -use crate::helpers::{mock_config, mock_instantiate_msg}; - -mod helpers; +use super::{ + helpers, + helpers::{mock_config, mock_instantiate_msg}, +}; #[test] fn instantiating() { diff --git a/contracts/rewards-collector/osmosis/tests/test_distribute_rewards.rs b/contracts/rewards-collector/osmosis/tests/tests/test_distribute_rewards.rs similarity index 99% rename from contracts/rewards-collector/osmosis/tests/test_distribute_rewards.rs rename to contracts/rewards-collector/osmosis/tests/tests/test_distribute_rewards.rs index 0334c8ea3..fd7373b68 100644 --- a/contracts/rewards-collector/osmosis/tests/test_distribute_rewards.rs +++ b/contracts/rewards-collector/osmosis/tests/tests/test_distribute_rewards.rs @@ -6,7 +6,7 @@ use mars_rewards_collector_base::ContractError; use mars_rewards_collector_osmosis::entry::execute; use mars_testing::{mock_env as mock_env_at_height_and_time, mock_info, MockEnvParams}; -mod helpers; +use super::helpers; #[test] fn distributing_rewards() { diff --git a/contracts/rewards-collector/osmosis/tests/test_swap.rs b/contracts/rewards-collector/osmosis/tests/tests/test_swap.rs similarity index 98% rename from contracts/rewards-collector/osmosis/tests/test_swap.rs rename to contracts/rewards-collector/osmosis/tests/tests/test_swap.rs index 331d67ab8..3bbaa7e38 100644 --- a/contracts/rewards-collector/osmosis/tests/test_swap.rs +++ b/contracts/rewards-collector/osmosis/tests/tests/test_swap.rs @@ -9,9 +9,7 @@ use mars_rewards_collector_osmosis::entry::execute; use mars_testing::mock_info; use osmosis_std::types::osmosis::twap::v1beta1::ArithmeticTwapToNowResponse; -use crate::helpers::mock_instantiate_msg; - -mod helpers; +use super::{helpers, helpers::mock_instantiate_msg}; #[test] fn swapping_asset() { diff --git a/contracts/rewards-collector/osmosis/tests/test_update_owner.rs b/contracts/rewards-collector/osmosis/tests/tests/test_update_owner.rs similarity index 97% rename from contracts/rewards-collector/osmosis/tests/test_update_owner.rs rename to contracts/rewards-collector/osmosis/tests/tests/test_update_owner.rs index 7b41394c5..102b46a91 100644 --- a/contracts/rewards-collector/osmosis/tests/test_update_owner.rs +++ b/contracts/rewards-collector/osmosis/tests/tests/test_update_owner.rs @@ -4,9 +4,7 @@ use mars_red_bank_types::rewards_collector::{ConfigResponse, ExecuteMsg, QueryMs use mars_rewards_collector_base::ContractError; use mars_rewards_collector_osmosis::entry::execute; -use crate::helpers::{query, setup_test}; - -mod helpers; +use super::helpers::{query, setup_test}; #[test] fn initialized_state() { diff --git a/contracts/rewards-collector/osmosis/tests/test_withdraw.rs b/contracts/rewards-collector/osmosis/tests/tests/test_withdraw.rs similarity index 99% rename from contracts/rewards-collector/osmosis/tests/test_withdraw.rs rename to contracts/rewards-collector/osmosis/tests/tests/test_withdraw.rs index 48d4b3662..df085fdcd 100644 --- a/contracts/rewards-collector/osmosis/tests/test_withdraw.rs +++ b/contracts/rewards-collector/osmosis/tests/tests/test_withdraw.rs @@ -7,7 +7,7 @@ use mars_rewards_collector_base::ContractError; use mars_rewards_collector_osmosis::entry::execute; use mars_testing::mock_info; -mod helpers; +use super::helpers; #[test] fn withdrawing_from_red_bank() { diff --git a/contracts/swapper/astroport/tests/all_tests.rs b/contracts/swapper/astroport/tests/all_tests.rs new file mode 100644 index 000000000..14f00389d --- /dev/null +++ b/contracts/swapper/astroport/tests/all_tests.rs @@ -0,0 +1 @@ +mod tests; diff --git a/contracts/swapper/astroport/tests/tests/mod.rs b/contracts/swapper/astroport/tests/tests/mod.rs new file mode 100644 index 000000000..1d69d56e7 --- /dev/null +++ b/contracts/swapper/astroport/tests/tests/mod.rs @@ -0,0 +1,4 @@ +mod test_queries; +mod test_routes; +mod test_swap; +mod test_transfer_result; diff --git a/contracts/swapper/astroport/tests/test_queries.rs b/contracts/swapper/astroport/tests/tests/test_queries.rs similarity index 100% rename from contracts/swapper/astroport/tests/test_queries.rs rename to contracts/swapper/astroport/tests/tests/test_queries.rs diff --git a/contracts/swapper/astroport/tests/test_routes.rs b/contracts/swapper/astroport/tests/tests/test_routes.rs similarity index 100% rename from contracts/swapper/astroport/tests/test_routes.rs rename to contracts/swapper/astroport/tests/tests/test_routes.rs diff --git a/contracts/swapper/astroport/tests/test_swap.rs b/contracts/swapper/astroport/tests/tests/test_swap.rs similarity index 100% rename from contracts/swapper/astroport/tests/test_swap.rs rename to contracts/swapper/astroport/tests/tests/test_swap.rs diff --git a/contracts/swapper/astroport/tests/test_transfer_result.rs b/contracts/swapper/astroport/tests/tests/test_transfer_result.rs similarity index 100% rename from contracts/swapper/astroport/tests/test_transfer_result.rs rename to contracts/swapper/astroport/tests/tests/test_transfer_result.rs diff --git a/contracts/swapper/osmosis/tests/all_tests.rs b/contracts/swapper/osmosis/tests/all_tests.rs new file mode 100644 index 000000000..14f00389d --- /dev/null +++ b/contracts/swapper/osmosis/tests/all_tests.rs @@ -0,0 +1 @@ +mod tests; diff --git a/contracts/swapper/osmosis/tests/helpers.rs b/contracts/swapper/osmosis/tests/tests/helpers/mod.rs similarity index 100% rename from contracts/swapper/osmosis/tests/helpers.rs rename to contracts/swapper/osmosis/tests/tests/helpers/mod.rs diff --git a/contracts/swapper/osmosis/tests/tests/mod.rs b/contracts/swapper/osmosis/tests/tests/mod.rs new file mode 100644 index 000000000..51ee89c92 --- /dev/null +++ b/contracts/swapper/osmosis/tests/tests/mod.rs @@ -0,0 +1,8 @@ +mod helpers; + +mod test_enumerate_routes; +mod test_estimate; +mod test_instantiate; +mod test_set_route; +mod test_swap; +mod test_update_admin; diff --git a/contracts/swapper/osmosis/tests/test_enumerate_routes.rs b/contracts/swapper/osmosis/tests/tests/test_enumerate_routes.rs similarity index 98% rename from contracts/swapper/osmosis/tests/test_enumerate_routes.rs rename to contracts/swapper/osmosis/tests/tests/test_enumerate_routes.rs index c9a679a99..fa55a4fa5 100644 --- a/contracts/swapper/osmosis/tests/test_enumerate_routes.rs +++ b/contracts/swapper/osmosis/tests/tests/test_enumerate_routes.rs @@ -7,9 +7,7 @@ use cw_it::osmosis_test_tube::{Gamm, Module, OsmosisTestApp, SigningAccount, Was use mars_red_bank_types::swapper::{ExecuteMsg, QueryMsg, RouteResponse}; use mars_swapper_osmosis::route::{OsmosisRoute, SwapAmountInRoute}; -use crate::helpers::instantiate_contract; - -pub mod helpers; +use super::helpers::instantiate_contract; #[test] fn enumerating_routes() { diff --git a/contracts/swapper/osmosis/tests/test_estimate.rs b/contracts/swapper/osmosis/tests/tests/test_estimate.rs similarity index 99% rename from contracts/swapper/osmosis/tests/test_estimate.rs rename to contracts/swapper/osmosis/tests/tests/test_estimate.rs index 8ac107320..42aeff7d8 100644 --- a/contracts/swapper/osmosis/tests/test_estimate.rs +++ b/contracts/swapper/osmosis/tests/tests/test_estimate.rs @@ -3,12 +3,10 @@ use cw_it::osmosis_test_tube::{Gamm, Module, OsmosisTestApp, RunnerResult, Wasm} use mars_red_bank_types::swapper::{EstimateExactInSwapResponse, ExecuteMsg, QueryMsg}; use mars_swapper_osmosis::route::{OsmosisRoute, SwapAmountInRoute}; -use crate::helpers::{ +use super::helpers::{ assert_err, instantiate_contract, query_price_from_pool, swap_to_create_twap_records, }; -pub mod helpers; - #[test] fn error_on_route_not_found() { let app = OsmosisTestApp::new(); diff --git a/contracts/swapper/osmosis/tests/test_instantiate.rs b/contracts/swapper/osmosis/tests/tests/test_instantiate.rs similarity index 94% rename from contracts/swapper/osmosis/tests/test_instantiate.rs rename to contracts/swapper/osmosis/tests/tests/test_instantiate.rs index 3c0008aa8..684230e7d 100644 --- a/contracts/swapper/osmosis/tests/test_instantiate.rs +++ b/contracts/swapper/osmosis/tests/tests/test_instantiate.rs @@ -3,9 +3,7 @@ use cw_it::osmosis_test_tube::{Account, Module, OsmosisTestApp, Wasm}; use mars_owner::OwnerResponse; use mars_red_bank_types::swapper::{InstantiateMsg, QueryMsg}; -use crate::helpers::{instantiate_contract, wasm_file}; - -pub mod helpers; +use super::helpers::{instantiate_contract, wasm_file}; #[test] fn owner_set_on_instantiate() { diff --git a/contracts/swapper/osmosis/tests/test_set_route.rs b/contracts/swapper/osmosis/tests/tests/test_set_route.rs similarity index 99% rename from contracts/swapper/osmosis/tests/test_set_route.rs rename to contracts/swapper/osmosis/tests/tests/test_set_route.rs index 83cdcbf85..150d23325 100644 --- a/contracts/swapper/osmosis/tests/test_set_route.rs +++ b/contracts/swapper/osmosis/tests/tests/test_set_route.rs @@ -5,9 +5,7 @@ use mars_red_bank_types::swapper::{ExecuteMsg, QueryMsg, RouteResponse}; use mars_swapper_base::ContractError; use mars_swapper_osmosis::route::{OsmosisRoute, SwapAmountInRoute}; -use crate::helpers::{assert_err, instantiate_contract}; - -pub mod helpers; +use super::helpers::{assert_err, instantiate_contract}; #[test] fn only_owner_can_set_routes() { diff --git a/contracts/swapper/osmosis/tests/test_swap.rs b/contracts/swapper/osmosis/tests/tests/test_swap.rs similarity index 99% rename from contracts/swapper/osmosis/tests/test_swap.rs rename to contracts/swapper/osmosis/tests/tests/test_swap.rs index 713209ad6..9f39206ca 100644 --- a/contracts/swapper/osmosis/tests/test_swap.rs +++ b/contracts/swapper/osmosis/tests/tests/test_swap.rs @@ -7,12 +7,10 @@ use mars_red_bank_types::swapper::ExecuteMsg; use mars_swapper_base::ContractError; use mars_swapper_osmosis::route::{OsmosisRoute, SwapAmountInRoute}; -use crate::helpers::{ +use super::helpers::{ assert_err, instantiate_contract, query_balance, swap_to_create_twap_records, }; -pub mod helpers; - #[test] fn transfer_callback_only_internal() { let app = OsmosisTestApp::new(); diff --git a/contracts/swapper/osmosis/tests/test_update_admin.rs b/contracts/swapper/osmosis/tests/tests/test_update_admin.rs similarity index 98% rename from contracts/swapper/osmosis/tests/test_update_admin.rs rename to contracts/swapper/osmosis/tests/tests/test_update_admin.rs index 84abb85e0..a47e7ba8e 100644 --- a/contracts/swapper/osmosis/tests/test_update_admin.rs +++ b/contracts/swapper/osmosis/tests/tests/test_update_admin.rs @@ -4,9 +4,7 @@ use mars_owner::{OwnerResponse, OwnerUpdate}; use mars_red_bank_types::swapper::{ExecuteMsg, QueryMsg}; use mars_swapper_osmosis::route::OsmosisRoute; -use crate::helpers::instantiate_contract; - -pub mod helpers; +use super::helpers::instantiate_contract; #[test] fn initial_state() { From 6b5f55efc9a58b75e54b619a219c304efe3e222c Mon Sep 17 00:00:00 2001 From: piobab Date: Thu, 24 Aug 2023 17:28:26 +0200 Subject: [PATCH 29/43] MP-3292. Oak no. 1 (#306) * Add liquidation_related flag for withdraw. It is used by credit manager to indicate liquidation. * Add tests. --- contracts/red-bank/src/contract.rs | 12 +- contracts/red-bank/src/health.rs | 12 +- contracts/red-bank/src/withdraw.rs | 7 + contracts/red-bank/tests/tests/helpers/mod.rs | 65 +++++- .../tests/tests/test_credit_accounts.rs | 53 +---- .../red-bank/tests/tests/test_payment.rs | 1 + .../red-bank/tests/tests/test_withdraw.rs | 191 +++++++++++++++++- .../rewards-collector/base/src/contract.rs | 1 + .../osmosis/tests/tests/test_withdraw.rs | 3 +- integration-tests/tests/test_incentives.rs | 2 + packages/testing/src/integration/mock_env.rs | 4 +- packages/types/src/red_bank/msg.rs | 3 + schemas/mars-red-bank/mars-red-bank.json | 6 + .../mars-red-bank/MarsRedBank.client.ts | 5 + .../mars-red-bank/MarsRedBank.react-query.ts | 1 + .../mars-red-bank/MarsRedBank.types.ts | 1 + 16 files changed, 308 insertions(+), 59 deletions(-) diff --git a/contracts/red-bank/src/contract.rs b/contracts/red-bank/src/contract.rs index f0e5de7f9..532868c1e 100644 --- a/contracts/red-bank/src/contract.rs +++ b/contracts/red-bank/src/contract.rs @@ -59,9 +59,19 @@ pub fn execute( amount, recipient, account_id, + liquidation_related, } => { cw_utils::nonpayable(&info)?; - withdraw::withdraw(deps, env, info, denom, amount, recipient, account_id) + withdraw::withdraw( + deps, + env, + info, + denom, + amount, + recipient, + account_id, + liquidation_related.unwrap_or(false), + ) } ExecuteMsg::Borrow { denom, diff --git a/contracts/red-bank/src/health.rs b/contracts/red-bank/src/health.rs index c22854353..280fa1c7b 100644 --- a/contracts/red-bank/src/health.rs +++ b/contracts/red-bank/src/health.rs @@ -45,9 +45,17 @@ pub fn assert_below_liq_threshold_after_withdraw( params_addr: &Addr, denom: &str, withdraw_amount: Uint128, + is_liquidation: bool, ) -> Result { - let mut positions = - get_user_positions_map(deps, env, user_addr, account_id, oracle_addr, params_addr, false)?; + let mut positions = get_user_positions_map( + deps, + env, + user_addr, + account_id, + oracle_addr, + params_addr, + is_liquidation, + )?; // Update position to compute health factor after withdraw match positions.get_mut(denom) { Some(p) => { diff --git a/contracts/red-bank/src/withdraw.rs b/contracts/red-bank/src/withdraw.rs index a25cf747d..8e7318886 100644 --- a/contracts/red-bank/src/withdraw.rs +++ b/contracts/red-bank/src/withdraw.rs @@ -19,6 +19,7 @@ pub fn withdraw( amount: Option, recipient: Option, account_id: Option, + liquidation_related: bool, ) -> Result { let withdrawer = User(&info.sender); let acc_id = account_id.clone().unwrap_or("".to_string()); @@ -63,12 +64,17 @@ pub fn withdraw( MarsAddressType::Incentives, MarsAddressType::RewardsCollector, MarsAddressType::Params, + MarsAddressType::CreditManager, ], )?; let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; let incentives_addr = &addresses[&MarsAddressType::Incentives]; let oracle_addr = &addresses[&MarsAddressType::Oracle]; let params_addr = &addresses[&MarsAddressType::Params]; + let credit_manager_addr = &addresses[&MarsAddressType::CreditManager]; + + // if withdraw is part of the liquidation in credit manager we need to use correct pricing for the assets + let liquidation_related = info.sender == credit_manager_addr && liquidation_related; // if asset is used as collateral and user is borrowing we need to validate health factor after withdraw, // otherwise no reasons to block the withdraw @@ -83,6 +89,7 @@ pub fn withdraw( params_addr, &denom, withdraw_amount, + liquidation_related, )? { return Err(ContractError::InvalidHealthFactorAfterWithdraw {}); diff --git a/contracts/red-bank/tests/tests/helpers/mod.rs b/contracts/red-bank/tests/tests/helpers/mod.rs index 944b10908..512cd91e2 100644 --- a/contracts/red-bank/tests/tests/helpers/mod.rs +++ b/contracts/red-bank/tests/tests/helpers/mod.rs @@ -1,6 +1,6 @@ #![allow(dead_code)] -use std::collections::HashMap; +use std::{collections::HashMap, fmt::Display, str::FromStr}; use anyhow::Result as AnyResult; use cosmwasm_schema::serde; @@ -21,8 +21,9 @@ use mars_red_bank::{ state::{COLLATERALS, DEBTS, MARKETS}, }; use mars_red_bank_types::red_bank::{ - Collateral, CreateOrUpdateConfig, Debt, InstantiateMsg, Market, QueryMsg, - UserCollateralResponse, UserDebtResponse, UserHealthStatus, UserPositionResponse, + Collateral, CreateOrUpdateConfig, Debt, InitOrUpdateAssetParams, InstantiateMsg, + InterestRateModel, Market, QueryMsg, UserCollateralResponse, UserDebtResponse, + UserHealthStatus, UserPositionResponse, }; use mars_testing::{mock_dependencies, mock_env, mock_info, MarsMockQuerier, MockEnvParams}; @@ -372,3 +373,61 @@ pub fn assert_err(res: AnyResult, err: ContractError) { } } } + +pub fn assert_err_with_str(res: AnyResult, expected: impl Display) { + match res { + Ok(_) => panic!("Result was not an error"), + Err(generic_err) => { + let contract_err: ContractError = generic_err.downcast().unwrap(); + let msg = contract_err.to_string(); + println!("error: {}", msg); // print error for debugging + assert!(msg.contains(&format!("{expected}"))) + } + } +} + +pub fn osmo_asset_params() -> (InitOrUpdateAssetParams, AssetParams) { + default_asset_params_with("uosmo", Decimal::percent(70), Decimal::percent(78)) +} + +pub fn usdc_asset_params() -> (InitOrUpdateAssetParams, AssetParams) { + default_asset_params_with("uusdc", Decimal::percent(90), Decimal::percent(96)) +} + +pub fn default_asset_params_with( + denom: &str, + max_loan_to_value: Decimal, + liquidation_threshold: Decimal, +) -> (InitOrUpdateAssetParams, AssetParams) { + let market_params = InitOrUpdateAssetParams { + reserve_factor: Some(Decimal::percent(20)), + interest_rate_model: Some(InterestRateModel { + optimal_utilization_rate: Decimal::percent(10), + base: Decimal::percent(30), + slope_1: Decimal::percent(25), + slope_2: Decimal::percent(30), + }), + }; + let asset_params = AssetParams { + denom: denom.to_string(), + credit_manager: CmSettings { + whitelisted: false, + hls: None, + }, + red_bank: RedBankSettings { + deposit_enabled: true, + borrow_enabled: true, + }, + max_loan_to_value, + liquidation_threshold, + liquidation_bonus: LiquidationBonus { + starting_lb: Decimal::percent(1), + slope: Decimal::from_str("2.0").unwrap(), + min_lb: Decimal::percent(2), + max_lb: Decimal::percent(10), + }, + protocol_liquidation_fee: Decimal::percent(2), + deposit_cap: Uint128::MAX, + }; + (market_params, asset_params) +} diff --git a/contracts/red-bank/tests/tests/test_credit_accounts.rs b/contracts/red-bank/tests/tests/test_credit_accounts.rs index d8ef269ea..8a5b42d70 100644 --- a/contracts/red-bank/tests/tests/test_credit_accounts.rs +++ b/contracts/red-bank/tests/tests/test_credit_accounts.rs @@ -1,12 +1,10 @@ -use std::str::FromStr; - use cosmwasm_std::{coin, Addr, Decimal, Uint128}; -use mars_params::types::asset::{AssetParams, CmSettings, LiquidationBonus, RedBankSettings}; use mars_red_bank::error::ContractError; -use mars_red_bank_types::red_bank::{InitOrUpdateAssetParams, InterestRateModel, UserHealthStatus}; +use mars_red_bank_types::red_bank::UserHealthStatus; use mars_testing::integration::mock_env::MockEnvBuilder; use super::helpers::assert_err; +use crate::tests::helpers::{osmo_asset_params, usdc_asset_params}; #[test] fn deposit_and_withdraw_for_credit_account_works() { @@ -109,6 +107,7 @@ fn deposit_and_withdraw_for_credit_account_works() { "uosmo", None, Some(account_id.clone()), + None, ) .unwrap(); @@ -125,49 +124,3 @@ fn deposit_and_withdraw_for_credit_account_works() { assert!(cm_position.total_collateralized_debt.is_zero()); assert_eq!(cm_position.health_status, UserHealthStatus::NotBorrowing); } - -fn osmo_asset_params() -> (InitOrUpdateAssetParams, AssetParams) { - default_asset_params_with("uosmo", Decimal::percent(70), Decimal::percent(78)) -} - -fn usdc_asset_params() -> (InitOrUpdateAssetParams, AssetParams) { - default_asset_params_with("uusdc", Decimal::percent(90), Decimal::percent(96)) -} - -fn default_asset_params_with( - denom: &str, - max_loan_to_value: Decimal, - liquidation_threshold: Decimal, -) -> (InitOrUpdateAssetParams, AssetParams) { - let market_params = InitOrUpdateAssetParams { - reserve_factor: Some(Decimal::percent(20)), - interest_rate_model: Some(InterestRateModel { - optimal_utilization_rate: Decimal::percent(10), - base: Decimal::percent(30), - slope_1: Decimal::percent(25), - slope_2: Decimal::percent(30), - }), - }; - let asset_params = AssetParams { - denom: denom.to_string(), - credit_manager: CmSettings { - whitelisted: false, - hls: None, - }, - red_bank: RedBankSettings { - deposit_enabled: true, - borrow_enabled: true, - }, - max_loan_to_value, - liquidation_threshold, - liquidation_bonus: LiquidationBonus { - starting_lb: Decimal::percent(1), - slope: Decimal::from_str("2.0").unwrap(), - min_lb: Decimal::percent(2), - max_lb: Decimal::percent(10), - }, - protocol_liquidation_fee: Decimal::percent(2), - deposit_cap: Uint128::MAX, - }; - (market_params, asset_params) -} diff --git a/contracts/red-bank/tests/tests/test_payment.rs b/contracts/red-bank/tests/tests/test_payment.rs index 454283f29..551b6cfbb 100644 --- a/contracts/red-bank/tests/tests/test_payment.rs +++ b/contracts/red-bank/tests/tests/test_payment.rs @@ -30,6 +30,7 @@ fn rejecting_unexpected_payments() { amount: None, recipient: None, account_id: None, + liquidation_related: None, }, ) .unwrap_err(); diff --git a/contracts/red-bank/tests/tests/test_withdraw.rs b/contracts/red-bank/tests/tests/test_withdraw.rs index 30180c77a..2d6584396 100644 --- a/contracts/red-bank/tests/tests/test_withdraw.rs +++ b/contracts/red-bank/tests/tests/test_withdraw.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use cosmwasm_std::{ attr, coin, coins, testing::{mock_env, mock_info, MockApi, MockStorage}, @@ -18,12 +20,15 @@ use mars_red_bank_types::{ incentives, red_bank::{Collateral, Debt, ExecuteMsg, Market}, }; -use mars_testing::{mock_env_at_block_time, MarsMockQuerier}; +use mars_testing::{ + integration::mock_env::MockEnvBuilder, mock_env_at_block_time, MarsMockQuerier, +}; use super::helpers::{ has_collateral_position, set_collateral, th_build_interests_updated_event, th_default_asset_params, th_get_expected_indices_and_rates, th_setup, TestUtilizationDeltaInfo, }; +use crate::tests::helpers::{assert_err_with_str, osmo_asset_params, usdc_asset_params}; struct TestSuite { deps: OwnedDeps, @@ -82,6 +87,7 @@ fn withdrawing_more_than_balance() { amount: Some(Uint128::from(2000u128)), recipient: None, account_id: None, + liquidation_related: None, }, ) .unwrap_err(); @@ -126,6 +132,7 @@ fn withdrawing_partially() { amount: Some(withdraw_amount), recipient: None, account_id: None, + liquidation_related: None, }, ) .unwrap(); @@ -263,6 +270,7 @@ fn withdrawing_completely() { amount: None, recipient: None, account_id: None, + liquidation_related: None, }, ) .unwrap(); @@ -373,6 +381,7 @@ fn withdrawing_to_another_user() { amount: None, recipient: Some(recipient_addr.to_string()), account_id: None, + liquidation_related: None, }, ) .unwrap(); @@ -670,6 +679,7 @@ fn withdrawing_if_health_factor_not_met() { amount: Some(withdraw_amount), recipient: None, account_id: None, + liquidation_related: None, }, ) .unwrap_err(); @@ -707,6 +717,7 @@ fn withdrawing_if_health_factor_met() { amount: Some(withdraw_amount), recipient: None, account_id: None, + liquidation_related: None, }, ) .unwrap(); @@ -750,3 +761,181 @@ fn withdrawing_if_health_factor_met() { let market = MARKETS.load(deps.as_ref().storage, denoms[2]).unwrap(); assert_eq!(market.collateral_total_scaled, expected_collateral_total_amount_scaled_after); } + +// Withdraw should be blocked if circuit breakers are activated in oracle contract except for the +// case where the withdrawer is the credit manager contract and the withdraw is for liquidation. +#[test] +fn withdraw_for_credit_manager_works_during_liquidation() { + let owner = Addr::unchecked("owner"); + let mut mock_env = MockEnvBuilder::new(None, owner.clone()).build(); + + let red_bank = mock_env.red_bank.clone(); + let params = mock_env.params.clone(); + let oracle = mock_env.oracle.clone(); + let pyth = mock_env.pyth.clone(); + let credit_manager = mock_env.credit_manager.clone(); + + let funded_amt = 1_000_000_000_000u128; + let provider = Addr::unchecked("provider"); // provides collateral to be borrowed by others + let account_id = "111".to_string(); + + // setup red-bank + let (market_params, asset_params) = osmo_asset_params(); + red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = usdc_asset_params(); + red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params); + params.init_params(&mut mock_env, asset_params); + + // setup oracle + oracle.set_price_source_fixed(&mut mock_env, "uosmo", Decimal::one()); + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(2u128, 1u128)); + + // fund accounts + mock_env.fund_accounts(&[&provider, &credit_manager], funded_amt, &["uosmo", "uusdc"]); + + // provider deposits collaterals + red_bank.deposit(&mut mock_env, &provider, coin(1000000000, "uusdc")).unwrap(); + + // credit manager deposits + let cm_osmo_deposit_amt = 100000000u128; + red_bank + .deposit_with_acc_id( + &mut mock_env, + &credit_manager, + coin(cm_osmo_deposit_amt, "uosmo"), + Some(account_id.clone()), + ) + .unwrap(); + + // update credit line for credit manager + red_bank + .update_uncollateralized_loan_limit( + &mut mock_env, + &owner, + &credit_manager, + "uusdc", + Uint128::MAX, + ) + .unwrap(); + + // credit manager should be able to borrow + let cm_usdc_borrow_amt = 100000000u128; + red_bank.borrow(&mut mock_env, &credit_manager, "uusdc", cm_usdc_borrow_amt).unwrap(); + + // check collaterals for credit manager account id before withdraw + let cm_collaterals = red_bank.query_user_collaterals_with_acc_id( + &mut mock_env, + &credit_manager, + Some(account_id.clone()), + ); + assert_eq!(cm_collaterals.len(), 1); + let cm_osmo_collateral = cm_collaterals.get("uosmo").unwrap(); + assert_eq!(cm_osmo_collateral.amount.u128(), cm_osmo_deposit_amt); + + // activate circuit breakers using pyth mocked invalid price + oracle.set_price_source_fixed(&mut mock_env, "usd", Decimal::from_str("1000000").unwrap()); + oracle.set_price_source_pyth( + &mut mock_env, + "uusdc", + pyth.to_string(), + Decimal::percent(10u64), + Decimal::percent(15u64), + ); + + // try to withdraw total collateral for account id, should fail because of circuit breakers + let res = red_bank.withdraw_with_acc_id( + &mut mock_env, + &credit_manager, + "uosmo", + None, + Some(account_id.clone()), + None, + ); + assert_err_with_str( + res, + "Invalid price: price confidence deviation 0.748898678414096916 exceeds max allowed 0.1", + ); + + // withdraw total collateral for account id during liquidation, should pass + red_bank + .withdraw_with_acc_id( + &mut mock_env, + &credit_manager, + "uosmo", + None, + Some(account_id.clone()), + Some(true), + ) + .unwrap(); + + // check collaterals for credit manager account id after withdraw + let cm_collaterals = red_bank.query_user_collaterals_with_acc_id( + &mut mock_env, + &credit_manager, + Some(account_id), + ); + assert!(cm_collaterals.is_empty()); +} + +// Withdraw for a red bank user (without account id) should be blocked if circuit breakers are activated in oracle contract. +#[test] +fn withdraw_if_oracle_circuit_breakers_activated() { + let owner = Addr::unchecked("owner"); + let mut mock_env = MockEnvBuilder::new(None, owner).build(); + + let red_bank = mock_env.red_bank.clone(); + let params = mock_env.params.clone(); + let oracle = mock_env.oracle.clone(); + let pyth = mock_env.pyth.clone(); + + let funded_amt = 1_000_000_000_000u128; + let provider = Addr::unchecked("provider"); // provides collateral to be borrowed by others + let user = Addr::unchecked("user"); + + // setup red-bank + let (market_params, asset_params) = osmo_asset_params(); + red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = usdc_asset_params(); + red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params); + params.init_params(&mut mock_env, asset_params); + + // setup oracle + oracle.set_price_source_fixed(&mut mock_env, "uosmo", Decimal::one()); + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(2u128, 1u128)); + + // fund accounts + mock_env.fund_accounts(&[&provider, &user], funded_amt, &["uosmo", "uusdc"]); + + // provider deposits collaterals + red_bank.deposit(&mut mock_env, &provider, coin(1000000000, "uusdc")).unwrap(); + + // user deposits + let cm_osmo_deposit_amt = 100000000u128; + red_bank.deposit(&mut mock_env, &user, coin(cm_osmo_deposit_amt, "uosmo")).unwrap(); + + // user borrows + let cm_usdc_borrow_amt = 100u128; + red_bank.borrow(&mut mock_env, &user, "uusdc", cm_usdc_borrow_amt).unwrap(); + + // activate circuit breakers using pyth mocked invalid price + oracle.set_price_source_fixed(&mut mock_env, "usd", Decimal::from_str("1000000").unwrap()); + oracle.set_price_source_pyth( + &mut mock_env, + "uusdc", + pyth.to_string(), + Decimal::percent(10u64), + Decimal::percent(15u64), + ); + + // try to withdraw with different `liquidation_related` value, should fail because of circuit breakers + let expected_msg = + "Invalid price: price confidence deviation 0.748898678414096916 exceeds max allowed 0.1"; + let res = red_bank.withdraw_with_acc_id(&mut mock_env, &user, "uosmo", None, None, None); + assert_err_with_str(res, expected_msg); + let res = red_bank.withdraw_with_acc_id(&mut mock_env, &user, "uosmo", None, None, Some(false)); + assert_err_with_str(res, expected_msg); + let res = red_bank.withdraw_with_acc_id(&mut mock_env, &user, "uosmo", None, None, Some(true)); + assert_err_with_str(res, expected_msg); +} diff --git a/contracts/rewards-collector/base/src/contract.rs b/contracts/rewards-collector/base/src/contract.rs index 7615ab34a..40c2d26ec 100644 --- a/contracts/rewards-collector/base/src/contract.rs +++ b/contracts/rewards-collector/base/src/contract.rs @@ -188,6 +188,7 @@ where amount, recipient: None, account_id: None, + liquidation_related: None, })?, funds: vec![], }); diff --git a/contracts/rewards-collector/osmosis/tests/tests/test_withdraw.rs b/contracts/rewards-collector/osmosis/tests/tests/test_withdraw.rs index df085fdcd..40138bd30 100644 --- a/contracts/rewards-collector/osmosis/tests/tests/test_withdraw.rs +++ b/contracts/rewards-collector/osmosis/tests/tests/test_withdraw.rs @@ -34,7 +34,8 @@ fn withdrawing_from_red_bank() { denom: "uatom".to_string(), amount: Some(Uint128::new(42069)), recipient: None, - account_id: None + account_id: None, + liquidation_related: None }) .unwrap(), funds: vec![] diff --git a/integration-tests/tests/test_incentives.rs b/integration-tests/tests/test_incentives.rs index 8fc9859c6..ee0a30985 100644 --- a/integration-tests/tests/test_incentives.rs +++ b/integration-tests/tests/test_incentives.rs @@ -188,6 +188,7 @@ fn rewards_claim_for_credit_account() { "uusdc", Some(Uint128::from(withdraw_amt_acc_id_1)), Some(acc_id_1.clone()), + None, ) .unwrap(); red_bank @@ -197,6 +198,7 @@ fn rewards_claim_for_credit_account() { "uusdc", Some(Uint128::from(withdraw_amt_acc_id_2)), Some(acc_id_2.clone()), + None, ) .unwrap(); let user_collateral = red_bank.query_user_collateral(&mut mock_env, &credit_manager, "uusdc"); diff --git a/packages/testing/src/integration/mock_env.rs b/packages/testing/src/integration/mock_env.rs index 642ff1091..52003578e 100644 --- a/packages/testing/src/integration/mock_env.rs +++ b/packages/testing/src/integration/mock_env.rs @@ -397,7 +397,7 @@ impl RedBank { denom: &str, amount: Option, ) -> AnyResult { - self.withdraw_with_acc_id(env, sender, denom, amount, None) + self.withdraw_with_acc_id(env, sender, denom, amount, None, None) } pub fn withdraw_with_acc_id( @@ -407,6 +407,7 @@ impl RedBank { denom: &str, amount: Option, account_id: Option, + liquidation_related: Option, ) -> AnyResult { env.app.execute_contract( sender.clone(), @@ -416,6 +417,7 @@ impl RedBank { amount, recipient: None, account_id, + liquidation_related, }, &[], ) diff --git a/packages/types/src/red_bank/msg.rs b/packages/types/src/red_bank/msg.rs index e18551ae5..f4cc862ab 100644 --- a/packages/types/src/red_bank/msg.rs +++ b/packages/types/src/red_bank/msg.rs @@ -68,6 +68,9 @@ pub enum ExecuteMsg { recipient: Option, /// Credit account id (Rover) account_id: Option, + // Withdraw action related to liquidation process initiated in credit manager. + // This flag is used to identify different way for pricing assets during liquidation. + liquidation_related: Option, }, /// Borrow native coins. If borrow allowed, amount is added to caller's debt diff --git a/schemas/mars-red-bank/mars-red-bank.json b/schemas/mars-red-bank/mars-red-bank.json index f36ad40b2..5f8b24d6c 100644 --- a/schemas/mars-red-bank/mars-red-bank.json +++ b/schemas/mars-red-bank/mars-red-bank.json @@ -238,6 +238,12 @@ "description": "Asset to withdraw", "type": "string" }, + "liquidation_related": { + "type": [ + "boolean", + "null" + ] + }, "recipient": { "description": "The address where the withdrawn amount is sent", "type": [ diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts index cf6e4d333..3c34abe51 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts @@ -423,11 +423,13 @@ export interface MarsRedBankInterface extends MarsRedBankReadOnlyInterface { accountId, amount, denom, + liquidationRelated, recipient, }: { accountId?: string amount?: Uint128 denom: string + liquidationRelated?: boolean recipient?: string }, fee?: number | StdFee | 'auto', @@ -657,11 +659,13 @@ export class MarsRedBankClient extends MarsRedBankQueryClient implements MarsRed accountId, amount, denom, + liquidationRelated, recipient, }: { accountId?: string amount?: Uint128 denom: string + liquidationRelated?: boolean recipient?: string }, fee: number | StdFee | 'auto' = 'auto', @@ -676,6 +680,7 @@ export class MarsRedBankClient extends MarsRedBankQueryClient implements MarsRed account_id: accountId, amount, denom, + liquidation_related: liquidationRelated, recipient, }, }, diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts index 4579f2509..fa1f1ccae 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts @@ -580,6 +580,7 @@ export interface MarsRedBankWithdrawMutation { accountId?: string amount?: Uint128 denom: string + liquidationRelated?: boolean recipient?: string } args?: { diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts index 6c177061b..b19096d67 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts @@ -50,6 +50,7 @@ export type ExecuteMsg = account_id?: string | null amount?: Uint128 | null denom: string + liquidation_related?: boolean | null recipient?: string | null } } From 7f76efec034b8e94b2dcdcc78cb8540efde8d122 Mon Sep 17 00:00:00 2001 From: piobab Date: Fri, 25 Aug 2023 21:10:02 +0200 Subject: [PATCH 30/43] MP-3293. Oak no. 2. (#307) * Use correct contract naming during migration. * Use --locked for grcov install. --- contracts/oracle/osmosis/src/migrations.rs | 26 ++++++++++++++++++---- coverage_grcov.Makefile.toml | 3 ++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/contracts/oracle/osmosis/src/migrations.rs b/contracts/oracle/osmosis/src/migrations.rs index a10f865b3..8ffa77965 100644 --- a/contracts/oracle/osmosis/src/migrations.rs +++ b/contracts/oracle/osmosis/src/migrations.rs @@ -10,7 +10,11 @@ pub mod v1_0_1 { pub fn migrate(deps: DepsMut) -> ContractResult { // make sure we're migrating the correct contract and from the correct version - cw2::assert_contract_version(deps.as_ref().storage, CONTRACT_NAME, FROM_VERSION)?; + cw2::assert_contract_version( + deps.as_ref().storage, + &format!("crates.io:{CONTRACT_NAME}"), + FROM_VERSION, + )?; // map old owner struct to new one let old_owner = old_state::OWNER.load(deps.storage)?; @@ -32,7 +36,11 @@ pub mod v1_0_1 { )?; // update contract version - cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + cw2::set_contract_version( + deps.storage, + format!("crates.io:{CONTRACT_NAME}"), + CONTRACT_VERSION, + )?; Ok(Response::new() .add_attribute("action", "migrate") @@ -83,7 +91,12 @@ pub mod v1_0_1 { fn migration_owner_from_state_b() { let mut deps = mock_dependencies(); - cw2::set_contract_version(deps.as_mut().storage, CONTRACT_NAME, FROM_VERSION).unwrap(); + cw2::set_contract_version( + deps.as_mut().storage, + format!("crates.io:{CONTRACT_NAME}"), + FROM_VERSION, + ) + .unwrap(); old_state::OWNER .save( @@ -113,7 +126,12 @@ pub mod v1_0_1 { fn migration_owner_from_state_c() { let mut deps = mock_dependencies(); - cw2::set_contract_version(deps.as_mut().storage, CONTRACT_NAME, FROM_VERSION).unwrap(); + cw2::set_contract_version( + deps.as_mut().storage, + format!("crates.io:{CONTRACT_NAME}"), + FROM_VERSION, + ) + .unwrap(); old_state::OWNER .save( diff --git a/coverage_grcov.Makefile.toml b/coverage_grcov.Makefile.toml index 8bc7601e2..68cf6b7f4 100644 --- a/coverage_grcov.Makefile.toml +++ b/coverage_grcov.Makefile.toml @@ -28,7 +28,8 @@ LLVM_PROFILE_FILE = "${COVERAGE_PROF_OUTPUT}/coverage-%p-%m.profraw" [tasks.install-grcov] condition = { env_not_set = ["SKIP_INSTALL_GRCOV"] } private = true -install_crate = { crate_name = "grcov" } +command = "cargo" +args = ["install", "grcov", "--locked"] [tasks.coverage-grcov] condition = { rust_version = { min = "1.60.0" } } From 57fb06aaff538b9eac1e547de972044b22b949ab Mon Sep 17 00:00:00 2001 From: piobab Date: Mon, 28 Aug 2023 07:51:33 +0200 Subject: [PATCH 31/43] MP-3302. Oak no. 11. (#308) * Validate slopes for ir model. * Update doc in ir model to be consistent with impl. * Update schema. --- contracts/red-bank/tests/tests/test_admin.rs | 33 +++++++++++++++++-- .../types/src/red_bank/interest_rate_model.rs | 13 ++++++-- schemas/mars-red-bank/mars-red-bank.json | 12 +++---- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/contracts/red-bank/tests/tests/test_admin.rs b/contracts/red-bank/tests/tests/test_admin.rs index 2fb396789..37033cedb 100644 --- a/contracts/red-bank/tests/tests/test_admin.rs +++ b/contracts/red-bank/tests/tests/test_admin.rs @@ -137,7 +137,7 @@ fn init_asset() { optimal_utilization_rate: Decimal::one(), base: Decimal::percent(5), slope_1: Decimal::zero(), - slope_2: Decimal::zero(), + slope_2: Decimal::one(), }; let params = InitOrUpdateAssetParams { @@ -269,6 +269,33 @@ fn init_asset() { ); } + // init asset where slope_1 >= slope_2 + { + let invalid_asset_params = InitOrUpdateAssetParams { + interest_rate_model: Some(InterestRateModel { + slope_1: Decimal::percent(10), + slope_2: Decimal::percent(10), + ..ir_model + }), + ..params + }; + let msg = ExecuteMsg::InitAsset { + denom: "someasset".to_string(), + params: invalid_asset_params, + }; + let info = mock_info("owner", &[]); + let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); + assert_eq!( + error_res, + ValidationError::InvalidParam { + param_name: "slope_1".to_string(), + invalid_value: "0.1".to_string(), + predicate: "< 0.1".to_string() + } + .into() + ); + } + // owner is authorized { let msg = ExecuteMsg::InitAsset { @@ -322,7 +349,7 @@ fn update_asset() { optimal_utilization_rate: Decimal::one(), base: Decimal::percent(5), slope_1: Decimal::zero(), - slope_2: Decimal::zero(), + slope_2: Decimal::one(), }; let params = InitOrUpdateAssetParams { @@ -459,7 +486,7 @@ fn update_asset_with_new_interest_rate_model_params() { optimal_utilization_rate: Decimal::one(), base: Decimal::percent(5), slope_1: Decimal::zero(), - slope_2: Decimal::zero(), + slope_2: Decimal::one(), }; let params = InitOrUpdateAssetParams { diff --git a/packages/types/src/red_bank/interest_rate_model.rs b/packages/types/src/red_bank/interest_rate_model.rs index d94e1c7e2..26f844eaa 100644 --- a/packages/types/src/red_bank/interest_rate_model.rs +++ b/packages/types/src/red_bank/interest_rate_model.rs @@ -11,15 +11,24 @@ pub struct InterestRateModel { pub optimal_utilization_rate: Decimal, /// Base rate pub base: Decimal, - /// Slope parameter for interest rate model function when utilization_rate < optimal_utilization_rate + /// Slope parameter for interest rate model function when utilization_rate <= optimal_utilization_rate pub slope_1: Decimal, - /// Slope parameter for interest rate model function when utilization_rate >= optimal_utilization_rate + /// Slope parameter for interest rate model function when utilization_rate > optimal_utilization_rate pub slope_2: Decimal, } impl InterestRateModel { pub fn validate(&self) -> Result<(), ValidationError> { decimal_param_le_one(self.optimal_utilization_rate, "optimal_utilization_rate")?; + + if self.slope_1 >= self.slope_2 { + return Err(ValidationError::InvalidParam { + param_name: "slope_1".to_string(), + invalid_value: self.slope_1.to_string(), + predicate: format!("< {}", self.slope_2), + }); + } + Ok(()) } diff --git a/schemas/mars-red-bank/mars-red-bank.json b/schemas/mars-red-bank/mars-red-bank.json index 5f8b24d6c..569158af9 100644 --- a/schemas/mars-red-bank/mars-red-bank.json +++ b/schemas/mars-red-bank/mars-red-bank.json @@ -454,7 +454,7 @@ ] }, "slope_1": { - "description": "Slope parameter for interest rate model function when utilization_rate < optimal_utilization_rate", + "description": "Slope parameter for interest rate model function when utilization_rate <= optimal_utilization_rate", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -462,7 +462,7 @@ ] }, "slope_2": { - "description": "Slope parameter for interest rate model function when utilization_rate >= optimal_utilization_rate", + "description": "Slope parameter for interest rate model function when utilization_rate > optimal_utilization_rate", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -1146,7 +1146,7 @@ ] }, "slope_1": { - "description": "Slope parameter for interest rate model function when utilization_rate < optimal_utilization_rate", + "description": "Slope parameter for interest rate model function when utilization_rate <= optimal_utilization_rate", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -1154,7 +1154,7 @@ ] }, "slope_2": { - "description": "Slope parameter for interest rate model function when utilization_rate >= optimal_utilization_rate", + "description": "Slope parameter for interest rate model function when utilization_rate > optimal_utilization_rate", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -1208,7 +1208,7 @@ ] }, "slope_1": { - "description": "Slope parameter for interest rate model function when utilization_rate < optimal_utilization_rate", + "description": "Slope parameter for interest rate model function when utilization_rate <= optimal_utilization_rate", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -1216,7 +1216,7 @@ ] }, "slope_2": { - "description": "Slope parameter for interest rate model function when utilization_rate >= optimal_utilization_rate", + "description": "Slope parameter for interest rate model function when utilization_rate > optimal_utilization_rate", "allOf": [ { "$ref": "#/definitions/Decimal" From 2789f1011d3c18f5a3c00cceda21bd5b7c1d31ac Mon Sep 17 00:00:00 2001 From: brimigs <85972460+brimigs@users.noreply.github.com> Date: Thu, 31 Aug 2023 09:26:25 +0100 Subject: [PATCH 32/43] MP-3311. Oak no. 20. Add max limit (#315) --- contracts/params/src/query.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/params/src/query.rs b/contracts/params/src/query.rs index c5e53bd0d..71e512e3f 100644 --- a/contracts/params/src/query.rs +++ b/contracts/params/src/query.rs @@ -13,6 +13,7 @@ use crate::{ }; pub const DEFAULT_LIMIT: u32 = 10; +pub const MAX_LIMIT: u32 = 30; pub fn query_all_asset_params( deps: Deps, @@ -20,7 +21,7 @@ pub fn query_all_asset_params( 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; + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; ASSET_PARAMS .range(deps.storage, start, None, Order::Ascending) .take(limit) @@ -47,7 +48,7 @@ pub fn query_all_vault_configs( None => None, }; - let limit = limit.unwrap_or(DEFAULT_LIMIT) as usize; + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; VAULT_CONFIGS .range(deps.storage, start, None, Order::Ascending) From 757bf3cf797e35c89c8ed0a20aa8faffea006e6f Mon Sep 17 00:00:00 2001 From: piobab Date: Thu, 31 Aug 2023 21:41:56 +0200 Subject: [PATCH 33/43] MP-3296 pagination (#312) * Add pagination with metadata. * Fix return type. * Address comments. * Add test case for v2 query. * Update schema. --- contracts/red-bank/src/contract.rs | 16 +++ contracts/red-bank/src/query.rs | 41 ++++-- contracts/red-bank/tests/tests/test_query.rs | 75 ++++++++++- packages/types/src/lib.rs | 13 ++ packages/types/src/red_bank/msg.rs | 9 ++ packages/types/src/red_bank/types.rs | 4 + schemas/mars-red-bank/mars-red-bank.json | 117 ++++++++++++++++++ .../mars-red-bank/MarsRedBank.client.ts | 34 +++++ .../mars-red-bank/MarsRedBank.react-query.ts | 32 +++++ .../mars-red-bank/MarsRedBank.types.ts | 15 +++ 10 files changed, 348 insertions(+), 8 deletions(-) diff --git a/contracts/red-bank/src/contract.rs b/contracts/red-bank/src/contract.rs index 532868c1e..061eae5cf 100644 --- a/contracts/red-bank/src/contract.rs +++ b/contracts/red-bank/src/contract.rs @@ -187,6 +187,22 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { + let user_addr = deps.api.addr_validate(&user)?; + to_binary(&query::query_user_collaterals_v2( + deps, + &env.block, + user_addr, + account_id, + start_after, + limit, + )?) + } QueryMsg::UserPosition { user, account_id, diff --git a/contracts/red-bank/src/query.rs b/contracts/red-bank/src/query.rs index 31e92a16b..68c89dab3 100644 --- a/contracts/red-bank/src/query.rs +++ b/contracts/red-bank/src/query.rs @@ -7,9 +7,11 @@ use mars_interest_rate::{ use mars_red_bank_types::{ address_provider::{self, MarsAddressType}, red_bank::{ - Collateral, ConfigResponse, Debt, Market, UncollateralizedLoanLimitResponse, - UserCollateralResponse, UserDebtResponse, UserHealthStatus, UserPositionResponse, + Collateral, ConfigResponse, Debt, Market, PaginatedUserCollateralResponse, + UncollateralizedLoanLimitResponse, UserCollateralResponse, UserDebtResponse, + UserHealthStatus, UserPositionResponse, }, + Metadata, }; use crate::{ @@ -18,8 +20,8 @@ use crate::{ state::{COLLATERALS, CONFIG, DEBTS, MARKETS, OWNER, UNCOLLATERALIZED_LOAN_LIMITS}, }; -const DEFAULT_LIMIT: u32 = 5; -const MAX_LIMIT: u32 = 10; +const DEFAULT_LIMIT: u32 = 10; +const MAX_LIMIT: u32 = 30; pub fn query_config(deps: Deps) -> StdResult { let owner_state = OWNER.query(deps.storage)?; @@ -179,6 +181,18 @@ pub fn query_user_collaterals( start_after: Option, limit: Option, ) -> Result, ContractError> { + let res_v2 = query_user_collaterals_v2(deps, block, user_addr, account_id, start_after, limit)?; + Ok(res_v2.data) +} + +pub fn query_user_collaterals_v2( + deps: Deps, + block: &BlockInfo, + user_addr: Addr, + account_id: Option, + start_after: Option, + limit: Option, +) -> Result { let block_time = block.time.seconds(); let start = start_after.map(|denom| Bound::ExclusiveRaw(denom.into_bytes())); @@ -186,10 +200,10 @@ pub fn query_user_collaterals( let acc_id = account_id.unwrap_or("".to_string()); - COLLATERALS + let user_collaterals_res: Result, ContractError> = COLLATERALS .prefix((&user_addr, &acc_id)) .range(deps.storage, start, None, Order::Ascending) - .take(limit) + .take(limit + 1) // Fetch one extra item to determine if there are more .map(|item| { let (denom, collateral) = item?; @@ -205,7 +219,20 @@ pub fn query_user_collaterals( enabled: collateral.enabled, }) }) - .collect() + .collect(); + + let mut user_collaterals = user_collaterals_res?; + let has_more = user_collaterals.len() > limit; + if has_more { + user_collaterals.pop(); // Remove the extra item used for checking if there are more items + } + + Ok(PaginatedUserCollateralResponse { + data: user_collaterals, + metadata: Metadata { + has_more, + }, + }) } pub fn query_scaled_liquidity_amount( diff --git a/contracts/red-bank/tests/tests/test_query.rs b/contracts/red-bank/tests/tests/test_query.rs index 1aa27173c..92a213bd4 100644 --- a/contracts/red-bank/tests/tests/test_query.rs +++ b/contracts/red-bank/tests/tests/test_query.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{testing::mock_env, Addr, Decimal, Uint128}; use mars_interest_rate::{get_scaled_debt_amount, get_underlying_debt_amount, SCALING_FACTOR}; use mars_red_bank::{ - query::{query_user_collaterals, query_user_debt, query_user_debts}, + query::{query_user_collaterals, query_user_collaterals_v2, query_user_debt, query_user_debts}, state::DEBTS, }; use mars_red_bank_types::red_bank::{Debt, Market, UserCollateralResponse, UserDebtResponse}; @@ -67,6 +67,79 @@ fn query_collateral() { ); } +#[test] +fn paginate_user_collaterals_v2() { + let mut deps = th_setup(&[]); + let env = mock_env(); + + let user_addr = Addr::unchecked("user"); + + let market_1 = th_init_market(deps.as_mut(), "uosmo", &Default::default()); + let market_2 = th_init_market(deps.as_mut(), "uatom", &Default::default()); + let market_3 = th_init_market(deps.as_mut(), "untrn", &Default::default()); + let market_4 = th_init_market(deps.as_mut(), "ujuno", &Default::default()); + let market_5 = th_init_market(deps.as_mut(), "uusdc", &Default::default()); + let market_6 = th_init_market(deps.as_mut(), "ujake", &Default::default()); + + set_collateral(deps.as_mut(), &user_addr, &market_1.denom, Uint128::one(), true); + set_collateral(deps.as_mut(), &user_addr, &market_2.denom, Uint128::one(), true); + set_collateral(deps.as_mut(), &user_addr, &market_3.denom, Uint128::one(), true); + set_collateral(deps.as_mut(), &user_addr, &market_4.denom, Uint128::one(), true); + set_collateral(deps.as_mut(), &user_addr, &market_5.denom, Uint128::one(), true); + set_collateral(deps.as_mut(), &user_addr, &market_6.denom, Uint128::one(), false); + + // Check pagination with default params + let collaterals = + query_user_collaterals_v2(deps.as_ref(), &env.block, user_addr.clone(), None, None, None) + .unwrap(); + assert_eq!( + to_denoms(&collaterals.data), + vec!["uatom", "ujake", "ujuno", "untrn", "uosmo", "uusdc"] + ); + assert!(!collaterals.metadata.has_more); + + // Paginate all collaterals + let collaterals = query_user_collaterals_v2( + deps.as_ref(), + &env.block, + user_addr.clone(), + None, + Some("uatom".to_string()), + Some(2), + ) + .unwrap(); + assert_eq!(to_denoms(&collaterals.data), vec!["ujake", "ujuno"]); + assert!(collaterals.metadata.has_more); + + let collaterals = query_user_collaterals_v2( + deps.as_ref(), + &env.block, + user_addr.clone(), + None, + Some("ujuno".to_string()), + Some(2), + ) + .unwrap(); + assert_eq!(to_denoms(&collaterals.data), vec!["untrn", "uosmo"]); + assert!(collaterals.metadata.has_more); + + let collaterals = query_user_collaterals_v2( + deps.as_ref(), + &env.block, + user_addr, + None, + Some("uosmo".to_string()), + Some(2), + ) + .unwrap(); + assert_eq!(to_denoms(&collaterals.data), vec!["uusdc"]); + assert!(!collaterals.metadata.has_more); +} + +fn to_denoms(res: &[UserCollateralResponse]) -> Vec<&str> { + res.iter().map(|item| item.denom.as_str()).collect() +} + #[test] fn test_query_user_debt() { let mut deps = th_setup(&[]); diff --git a/packages/types/src/lib.rs b/packages/types/src/lib.rs index ee8a737d6..5750ce359 100644 --- a/packages/types/src/lib.rs +++ b/packages/types/src/lib.rs @@ -5,3 +5,16 @@ pub mod oracle; pub mod red_bank; pub mod rewards_collector; pub mod swapper; + +use cosmwasm_schema::cw_serde; + +#[cw_serde] +pub struct PaginationResponse { + pub data: Vec, + pub metadata: Metadata, +} + +#[cw_serde] +pub struct Metadata { + pub has_more: bool, +} diff --git a/packages/types/src/red_bank/msg.rs b/packages/types/src/red_bank/msg.rs index f4cc862ab..b80f7ec8c 100644 --- a/packages/types/src/red_bank/msg.rs +++ b/packages/types/src/red_bank/msg.rs @@ -195,6 +195,15 @@ pub enum QueryMsg { limit: Option, }, + /// Get all collateral positions for a user + #[returns(crate::red_bank::PaginatedUserCollateralResponse)] + UserCollateralsV2 { + user: String, + account_id: Option, + start_after: Option, + limit: Option, + }, + /// Get user position #[returns(crate::red_bank::UserPositionResponse)] UserPosition { diff --git a/packages/types/src/red_bank/types.rs b/packages/types/src/red_bank/types.rs index 6495d3b99..d6b140449 100644 --- a/packages/types/src/red_bank/types.rs +++ b/packages/types/src/red_bank/types.rs @@ -1,6 +1,8 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Decimal, Uint128}; +use crate::PaginationResponse; + /// Global configuration #[cw_serde] pub struct Config { @@ -96,6 +98,8 @@ pub struct UserCollateralResponse { pub enabled: bool, } +pub type PaginatedUserCollateralResponse = PaginationResponse; + #[cw_serde] pub struct UserPositionResponse { /// Total value of all enabled collateral assets. diff --git a/schemas/mars-red-bank/mars-red-bank.json b/schemas/mars-red-bank/mars-red-bank.json index 569158af9..6bafea4e5 100644 --- a/schemas/mars-red-bank/mars-red-bank.json +++ b/schemas/mars-red-bank/mars-red-bank.json @@ -822,6 +822,48 @@ }, "additionalProperties": false }, + { + "description": "Get all collateral positions for a user", + "type": "object", + "required": [ + "user_collaterals_v2" + ], + "properties": { + "user_collaterals_v2": { + "type": "object", + "required": [ + "user" + ], + "properties": { + "account_id": { + "type": [ + "string", + "null" + ] + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + }, + "user": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Get user position", "type": "object", @@ -1508,6 +1550,81 @@ } } }, + "user_collaterals_v2": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PaginationResponse_for_UserCollateralResponse", + "type": "object", + "required": [ + "data", + "metadata" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/UserCollateralResponse" + } + }, + "metadata": { + "$ref": "#/definitions/Metadata" + } + }, + "additionalProperties": false, + "definitions": { + "Metadata": { + "type": "object", + "required": [ + "has_more" + ], + "properties": { + "has_more": { + "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" + }, + "UserCollateralResponse": { + "type": "object", + "required": [ + "amount", + "amount_scaled", + "denom", + "enabled" + ], + "properties": { + "amount": { + "description": "Underlying asset amount that is actually deposited at the current block", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "amount_scaled": { + "description": "Scaled collateral amount stored in contract state", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "denom": { + "description": "Asset denom", + "type": "string" + }, + "enabled": { + "description": "Wether the user is using asset as collateral or not", + "type": "boolean" + } + }, + "additionalProperties": false + } + } + }, "user_debt": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "UserDebtResponse", diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts index 3c34abe51..a48ed7d52 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts @@ -24,6 +24,8 @@ import { ArrayOfUncollateralizedLoanLimitResponse, UserCollateralResponse, ArrayOfUserCollateralResponse, + PaginationResponseForUserCollateralResponse, + Metadata, UserDebtResponse, ArrayOfUserDebtResponse, UserHealthStatus, @@ -86,6 +88,17 @@ export interface MarsRedBankReadOnlyInterface { startAfter?: string user: string }) => Promise + userCollateralsV2: ({ + accountId, + limit, + startAfter, + user, + }: { + accountId?: string + limit?: number + startAfter?: string + user: string + }) => Promise userPosition: ({ accountId, user, @@ -133,6 +146,7 @@ export class MarsRedBankQueryClient implements MarsRedBankReadOnlyInterface { this.userDebts = this.userDebts.bind(this) this.userCollateral = this.userCollateral.bind(this) this.userCollaterals = this.userCollaterals.bind(this) + this.userCollateralsV2 = this.userCollateralsV2.bind(this) this.userPosition = this.userPosition.bind(this) this.userPositionLiquidationPricing = this.userPositionLiquidationPricing.bind(this) this.scaledLiquidityAmount = this.scaledLiquidityAmount.bind(this) @@ -266,6 +280,26 @@ export class MarsRedBankQueryClient implements MarsRedBankReadOnlyInterface { }, }) } + userCollateralsV2 = async ({ + accountId, + limit, + startAfter, + user, + }: { + accountId?: string + limit?: number + startAfter?: string + user: string + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + user_collaterals_v2: { + account_id: accountId, + limit, + start_after: startAfter, + user, + }, + }) + } userPosition = async ({ accountId, user, diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts index fa1f1ccae..b7502eb79 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts @@ -25,6 +25,8 @@ import { ArrayOfUncollateralizedLoanLimitResponse, UserCollateralResponse, ArrayOfUserCollateralResponse, + PaginationResponseForUserCollateralResponse, + Metadata, UserDebtResponse, ArrayOfUserDebtResponse, UserHealthStatus, @@ -79,6 +81,10 @@ export const marsRedBankQueryKeys = { [ { ...marsRedBankQueryKeys.address(contractAddress)[0], method: 'user_collaterals', args }, ] as const, + userCollateralsV2: (contractAddress: string | undefined, args?: Record) => + [ + { ...marsRedBankQueryKeys.address(contractAddress)[0], method: 'user_collaterals_v2', args }, + ] as const, userPosition: (contractAddress: string | undefined, args?: Record) => [ { ...marsRedBankQueryKeys.address(contractAddress)[0], method: 'user_position', args }, @@ -279,6 +285,32 @@ export function useMarsRedBankUserPositionQuery({ { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, ) } +export interface MarsRedBankUserCollateralsV2Query + extends MarsRedBankReactQuery { + args: { + accountId?: string + limit?: number + startAfter?: string + user: string + } +} +export function useMarsRedBankUserCollateralsV2Query< + TData = PaginationResponseForUserCollateralResponse, +>({ client, args, options }: MarsRedBankUserCollateralsV2Query) { + return useQuery( + marsRedBankQueryKeys.userCollateralsV2(client?.contractAddress, args), + () => + client + ? client.userCollateralsV2({ + accountId: args.accountId, + limit: args.limit, + startAfter: args.startAfter, + user: args.user, + }) + : Promise.reject(new Error('Invalid client')), + { ...options, enabled: !!client && (options?.enabled != undefined ? options.enabled : true) }, + ) +} export interface MarsRedBankUserCollateralsQuery extends MarsRedBankReactQuery { args: { diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts index b19096d67..9552b2fdf 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts @@ -162,6 +162,14 @@ export type QueryMsg = user: string } } + | { + user_collaterals_v2: { + account_id?: string | null + limit?: number | null + start_after?: string | null + user: string + } + } | { user_position: { account_id?: string | null @@ -228,6 +236,13 @@ export interface UserCollateralResponse { enabled: boolean } export type ArrayOfUserCollateralResponse = UserCollateralResponse[] +export interface PaginationResponseForUserCollateralResponse { + data: UserCollateralResponse[] + metadata: Metadata +} +export interface Metadata { + has_more: boolean +} export interface UserDebtResponse { amount: Uint128 amount_scaled: Uint128 From 8c908426a9a472cf96acceed69857be6c6574a33 Mon Sep 17 00:00:00 2001 From: piobab Date: Thu, 31 Aug 2023 21:42:43 +0200 Subject: [PATCH 34/43] Fix Protocol Liquidation Fee calculation. (#313) --- ...ank - Dynamic LB & CF test cases v1.1.xlsx | Bin 20427 -> 20419 bytes .../red-bank/tests/tests/test_liquidate.rs | 17 ++++++++--------- packages/liquidation/src/liquidation.rs | 12 +++++++----- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/contracts/red-bank/tests/files/Red Bank - Dynamic LB & CF test cases v1.1.xlsx b/contracts/red-bank/tests/files/Red Bank - Dynamic LB & CF test cases v1.1.xlsx index 5d50903e1da697fe1e22a4e7cc3c929cc7aad216..e649148e784f0ba45c2eca2f972123cc01cd2bad 100644 GIT binary patch delta 11936 zcmZvi1yr27w#V^eEe?Zgahbv0-QC?CN^u+9-HH?|?i806io3g&;ts`%!<+V;d+&Mo zy>Df*l6*TdnYHGZ|K1tNh3?6Pexoc03x^Gbh=>SfheM*3dd#=M{$XVR|4*w@gn!j0U(xfgxU6uZKLtGpODd&dMfQer#a1dZjx@2hDl9kn zSx^nni(?BEn)88;65!q~!4o%Uykks$Wrf$iSP#XX%#LUbMjgH(xqg})nOQSgWR)LT zaK>Iaam+TrIa13Zj$@L^IGHJM?rd8ZP$$-Vj@iFhYhx@o_4!yCttHgEXr%Q!)} z=}5ls6G8k%!{a+|0Lt*IjlP>Te~-td+ahZcmaviwU@DQ>j--Gk{jn11rQa)m>PI*K z0%K}JQ?D0!aL~(uxNkhVaGx4c^g?@THICXdFF`6stN3nx*o7QVE>6jozsF!lO7LMy z^pBS>qrduQ`ji>|c-GlzJMlHG9qac|oB& zbnLo}7x&I#K9j7ci+ZKF=1c@7qBiroii{pDAV8IxMCMPH2<3D z&Ba>&Q??*iXYO{EwP<_dDM&po-xk#j3%8RyYzQR3VzMlDldc?xGzTD#$2xehOiFq*54^wror*jml? zioaGq2UAYkBA$imUsxh?grRd3iZJTJ*{v&-W&xE$peYIifI!5j$n_ooeLp&g^&4P0 zrgscL*N+Ht`3As`ah=BM)5hqJ078YFS5Rlu_QQd~0zjVuKD+X>n=Mo)hd&T3;An{x z7Q^&;$Rc0s@RId5gBF-kwLQ5YnOrj@0~0ggA#6W#P4$r@)o8AY}luTiK^A**kmv`1B2?uh^B4(?ky zD6b!pZ*NXB=+9rfNor<3KGBD>{_?~BRs2(-m54RR+c!M1CUt!BJ%^)>k!0KptHf8F z?S@h)Y&J>v$xxOT@T$v6Z5MFTu<10X<7!zMX{kRLowMOn49?x}J4UJ_9zRv@Fl9Dm zldt6iCysd`I2=Ej&2?7ThQPY}&WR;EkSc`#T_kKoUqC#XqF-|)eEbK>C z5tBhWdF3C9%2}97VO2DEZJ(2yoM^^!N?$Yj)~PEUMm=t=46xBz$~It1QIU_lHceb+ zpYDsLe5IoqAweH|5sLnOW-;hn#sxW_Jwn315m(`iA{nX4|~U3 zhcg+@Ci35zlcrWA_*G|)dFGY<(h6>YQX6S&? zabCSa9X}4=6ZE~}K)%7kq#PSr;}XKd!1ZP`oRbQ3Xr}{mwxx*_Uu`7a-EyO@g!f7Z zchBF-+xzv|uSY1yNP2^d8BxVOlrTGK>gBblDHoZ)&?lr1exy<0--su#6g1)TZs9jkaut?VT4OVaMqQu?Hjptr#%A4I5(t(+_3n-JT70t)W6>gvKkH%nJ##iJ zp`+fBA)N8x!aS}WoAwgK^R@;+w-=lr1L}h_XSYC6O*nGOOe=%06%V z>!@#hmwh(cYW+IEhzT#wp{@Je(Bn4|bPFEuK2l8ZV84q@q}zSg2B@lpmh>>7tno;y zsCzALWu+O$Nu(6;nG`b^Eps_3_IA+{jr@$7ZCYTaJd=0*@;Ty0+KJKo3+S}OYa%Vf z?rB?FXd}^Wqx&1CyNPFfxUL${yt-@GK-X&yb|T%fH|vbHx-aj__3-$&7y|ei&gvwZ z*0qA&-tn><9Rieg^1~Iis;F#leVgcJO4c?Ul@&d*qD@+t*|o58?k!!PS(N2oA->Rj z2v&Fn1!X`C1@&LJJ;$R6BdhQ_Ekqq^i|30F32AYhn&W9QBQs4h0R}mk>3etLnfn2?&Tf`k8d2P9nZ)C*0$}GCCDP$E zCLc*xoi@4))^%c1U}kvA{t5l84SrJgZ<=kE33%~;dn z?~(-+6wQOanri+`yY-K*t0m#vb|6}%4Cm*X3iDhcgExKssj%zQ?GI)gKSr(Mk}8BA zQ>m4T&b^c!wn)96>+dbTo*-r{9`XJV-P=_sxjg2yQBT+~&N|+ld2SiJ0hg?7eFYR? z<(8d0-6g~LB!8>e=ep~teEDDxuKMAeP!zsv^{D^io5a=?(v;+mPj6A0WMbdljc1Xb zVxo&SgS%|uOSi>#lMz z7eQi&Lqx|<8|fvLBP&9SPLg!*bkwU-ysuf90poj={<&@l0fl(vY=lHG*m!I;WEn|7 zVxL?T8Brj3GP^|}ro;3KgAz8z|3l&;K5QR=O!5r?HaJny`wM`+8y&+t0O&jOiLwts zE=dZMB<|1Qz-r{FmagY(5y6L&MWH$tO~aL*4XtEE-*g8paH1}HazQe=W=KYCM~Kk- z5d3=Ux+SHN+0Tgh6kNCb7>12FliGVrjc}KpVKs_lgoSsX?dD$i%I~{v#)Nz0r-v5l zrXG>RhWw>*FEw??rq>B-yOA>b*rlF<3{(bx>jZZyQbBy=Tgc zs&q|8?;6X};7HVcQ0Bsbg=4>7OKe4qM1 zJDV3tmS5fu+$dSGrpC_ZH+W*!t&-tpWTxT_EOTJX;UL3r)QrH z;NvZiUla z>9f)}!cc)KLc1AWQ!SU$Esn((difbjoiFnuo~3@k;;F=6%i;l1vUXLjhz@cV7SKPX ztW4JCz*ugay#uEidU=U&33SRZvME2Ie5Sw|j_?*(ieRnE+VDa7f=w%5Bx8sH~&_*F?IA6p-(zrVCLjl6i0{b}?N$O8RX5=>y z%O>S)ynS38$a)5miEV;kU-ph??k_WdCi2X<;HiC6jJR1=pHp^qqix!bA(h2qcdb7w20>eHZgQp9@cDSlIIO8S z)#=er-)b$TW7KI~V<*I$8Rl;lxsBPUkIeieq7H%w5j7EW;i;&Bc+!+o08`5;+MhlR z;DdqzZM!z!8`NA$ZD=O+>Gz`M}0gNQe?1@5+F| zimCbHUCEkLPElAo!Z@g*c}$=wGwt)epA$r=Wr~Ydz zfs2~Ee*~O{?J-2LbT-mOEku0SDYgSBxTq5=IilwKO-`oGk}GE}+Tw#>F4~pN*I6M2 z&t$wWc)!U>lc3ru1|g^V{~>4nIUe-i`tvt7@$oUiFk<#cn|k!-cG58BwvHU__3bq| zy@9j6APbsrk59u*j6-XzwQ@M<$1^2*YlqRjS2uh1LZ0%`E+0sl!+X!eg0E`Fx!QZy zc78tOE^JwI)5Z^(Bp+XjdGkD`ruD2_bt>duY+8Bc77qlHai@IfdDuEP*=*an7aU8P z9wMCv2Yc3!MKkh52}ScY`_fLEtD#uVL5aPv2Y72xHl`}-QA}Yg`0_~ z7Cvm#jHQ?EAz>=buB;A9Nw2 zeQwsgP7I!Nr^_4|*(l?dry9`CP#KaBc^(3rH92)ngmjKMZjUa;^34ljy}W=hCf>p+ zpTj+Do>;!M>r43>D=8uFn7GtX{HQxxe0R{YPCQ-Hm&b3>eb-q%?f2};gH5OpOO@!Ra>Z|puO?rU8g`5Mg&z13sjCNggqyX5n#1`7>w<%{Xbw4U6hEZcqu4rlBRWoEx=>cn;KCi@oY|qf|{Q;_3}~?og}I~nM7Y>d=j_q z{UTDkn=N({(m`>BK@K0%A+pb7h-Q`tfbAWGGMluZa_?6Lg+(O7_D>oYD#qhcN&v`3 zNzo;pbX_X}X1xHY{+6D_ddp9o12{JBuefN%glz)F4FvYnH*K*|qt~R8s)W{E`JZ z>Xe*q-^!3eMOu?>C}?k?7_8f!xU5&o8AR^zJzVqa=85^-p+)icl{iD80B<4b@ARLW z**5adKl)&f`?v=)7xk*jQ34KdXLyNTEsLWQSv%f%V(B&)g_^L-2n>Xq_y(qM7WQIN z2_N>5z4J-Ptc4IaJm3gDZ!)Sm8O^~ubZ<|C!fEO^alc0X*cCSg1IX94HS8hCjUYmX z-R>mFOOT)y_8<6w5sEQ=_CSH{DfpYX{XpU83TR{)2yuOX6Sw6LaSgKB75J)OL5TZv zNL8S0`aJ$DSF$dtEDA@DA4$g%J(^KIDpZ^Qk7iB7@9PGf9UO|IAybL^g?T%_GDSV? z27rUwfMGwF?Yr`pCyk}wH|#UYDKMPL#1N3fGk;A2)MKb}6JWkCHJQJ#rhp0W(HQu3 zoa_h8VJus)*HFRP9hn=L+asJpGJ~W7j$Z5!lVKU?ON5)}A0?Osf95Ti_Sm_=_Ib;> zJ<>ATK4}v!V6-R2^Ew7!)^xM8CG|NFgG0~=zfF6_x-2^}K)c95n2Y3~_g3V4`vGHR zyZ5IgG2dhAH6j6>JQ}%calFV|(MsvJ#iZA%5ZUS&Ho^2$IS-ght)jVV(KysxXO1Og zNn*jiKUMS!KI#=b@&rl>VVkC~*713Qq=>i=@|!uPc-?WcpH$?aLY1atTFN&jV3qv= zk<|iOE!QYCTWrNPOiB$u@0W@FnZrwIZ6Gz?#!2uQDgaYTyq;yTQ zA|_9+%J+WX*kJgxOw@=Nacjy=?Oj&*72ElM6m13jQm0EK1aeK`bsjJZB*~%WP&8L zOnH~LLrSXfioqyrd+$k?&?XL12uW!Sj&zh_T01Uc(f2=C$H^7$jf;h8&dI1$ouez zyx;h-?WG~)Rrx>UP5!M}E6EoBqggA+S;3WZatQ8pmWa~_GgXdm%~WZfLVMv;X1ba> z@4f&ETKPzd1{^+*#ChOtoq-J}y3b3?led_8um`S89~r9pJ8S&)1aWpI4h0pCFete- z@wOh?{I$6i8^0=D7G~n19VCt&N3$x~{<8DhQ%AGNJl4&B{9(Lsoy|WKw-sli(g6-F zSUjtD@Yb+b^75|aOJWtE;$Z7+n;Jfc%Sm`=-Bp>YV0@;-V*#x{XQ#b;W8&R&Jg@N~T1*HERYu+ZU;9qzY_sk^JSe6G;t#I1>n6|7z@x?GH?1EVsDTeG;x z=wVbwkdaSQY`zg`YX$Q(VR;8lvLS-VuCPTtPTv6UgQmCuNXA!glIB*ZaAw9p)o%dW z7*D|;eah%z2>GuNd$^c*F=GC zv4Z0NakT}+aI(d&3N})PfWQU9P4S$ z*T!&TIDCKyinEki)JJC-GM&z`nvGX$G!7OhpD?Ktb|<)B25PWm8Q6P&6`%8|IC!0K z*6lEP&KU{*Y6N;UW_sx2xh-LCEH-Ex#kj*B(sc@|nyC7$iGiqCbJwtO#oVT-ghE-C z)b<`t65& zMT6ai=rvqUp%~|4Y$E{ZX`S3EZm= zu%8oRiQWA8Y$wFC%{B&gW7zTUAc_{taP*#$dT>)_(8a9w=Q5QXDTlx-9Zm$48up-2 zNwHYsp?VnPSj}LA7FdOtsR)IL-=C!jCO`qFD`0kwxxpe5cEkr8|0_D|2e6?_%rXjy zpP1rWU#xORMdJ1ny_&bqiqt7|$~1P(v7)XVy@rdFE7wVzEe4WOEcXcv4l>y&dsoER zqh%Ji#9CfEdAC#?=SiQ^jU|p2eow4&%Upa*I{k<+@eO7sJDJgc2A$*>%Fbe0+O~!T zK1|8mu2so-otY3NK&T-Ud{7}p!M7+SaAaz>Jq*x^#?X2D>3tIg#+Y#}mobGK*5xS- z3j)lHwW(X54p+PHgdL{{33ApMw82GTz*sU&m;dRdz^MJEK?)MkAyM{Y{6Y9}PuoscS#FjIC8jo{L z*9B|%*a}gY6?5DH2j}vmM>02eRv!NQ%anuO=OfvjYt*b8Hq>|wqMp$nZ%jOwM3Y|kZvb5%7DKruwy9lJr1u@x3kr?;H{ob1N&6e&K3bo=#nX_V!%s}u;gmooQWt8=9u@h5TtUzAU zl$tWoA{MULsgHPUqdnD2$AdGDHV6Secsy-@$+Tk(UHg$6Or0_pI0;|>o<0j-y-N0K zg>p6?J^unXPmksZ5p;wYxRYddTa@BCT-F1u2p0`k){!YKO8VwK1j@%CZ~Do`Bsv_L z+4k*%0p)eUyb*D?b)Jr@PIPGzZkO!{D3jU0CApZbJ!7|iku4@Xr(sIPm6NQ1ka>NT zmJ_w4*7iLr7>&iv3MA1((uZ6BQ%`{Yh>3%NM!qH;GviFZ{hq(uQRq3QJq^CmM=ygH zYku8wbU|&WP*f%&f-mKbx>sy5?4B8Y1r0|9&6m7>PE^J@PE_Jr_@Eo> z(!~Xs)6QtZhZAU{ndk8`ZKR$fS}@k&%ms7oOiw<6j6?Ly=O>1E)?p_R&(8-@#UV9m z?W|tIaUKOOi6_kcJC+L_8r+*oMHOJ^-@-Nu7epWn!K>r{kJoSV$jQod6;{k_i$6zvJm(841tW!p?f&j*YVUg-*JiZID)?LiaLy_HmOu zzQDmD<&;=5Vy{re1;B+`|A-xD-z>*@?$*Rpe4cMFec?%(%KEq|=tZmBbcH`vG(hM{JT9!x_IyJ(XSrLt4*}C|OoxlKNjw0Gi z;qI-`XuVmh(wp-SzUoA38IQSI=Yl?ML?itgcpcOpHx_PPI-|r6THss0$lR#$J(aos z_4;F}>jCc-wnI~=!yvg_aF&^hwjO~*{e9B)1DI3}DGpD3tV1W0;lAOeYrg=!P;`=b&KMEznGeP1pB*v&dF|c;U zZ9LW1GsK_74q#?>i|UfUi&-sw7}Bs zILNMRX0?}QcACKtyA}Zr@q#bL+kIltS>FOr1bSyf>723|ciWdI{89#1&S~R4uGf~z zRZ}R+dQIM4{t3@cAA}bADNO8*SL?8dXl#BZxikqhya3amjXUbqR9@4YwYD61y=4yM zfH#EIp4jbha$`6fpRdyU?)Ff2uXM_DlYseruFB^tll z1+VMJNorf{#oFv#Su7&6>4y|~ScD4eQ%VT>Rh9B6u64w;DNSlr)KbIP1M)kxG#!va zPBB>XzJX^y@-^^icHaj$)FIVWUZqC8OgNEF%+6c42kA9IQMF1?mCxv5?uOFpB$&ra ztFPN8O_J)?@${F*i$`NDx&nf!h{X~L%@EUEQTju_&Kp~^mHbM>0cgBVC}&01;NTX~ zXt!c+KIIBtWU8(RHsi{x30(4u-m!f$c{pKqn)|Qf^C)ucXhcV{uoG_g}9J$IjUOwGt zP(a|g&t=pVz{2(w80Oc7FK0$?WcOp_62aGbE7*+G`A`+G%&h}W)A5o8pMe#;ffd~K zMiUI!Io?_UPY6~EWe)+UudZ47!Cz;u2}tNN5~VFO#$flSPg*Niy=unOp8N2#2jOz% zO5{KOm@CEZnc)nUD(I%Iu@%O^V{LTtP?%4RW7m`Xncu|T!`(B(9bQ)o9L~Su;7Ps= z)Zb>v2)uk>FDw!Q_-Xx%v}cAlJYEHP%LA-6=qn{@3fD_%p7*Y0$?aoP{?Xd_xnu~1 zQ{ZHvx=Z~L_jflU!ErkIPIpGv`)TP26iU-qFw! z(_nu3`MA1Tq!%A&UjC6TWujC)G0*onB3S~DcP`h`N2TtmHm6p6sLK4O#&u)CoXuc< z8?h&~^lyW(R&4mYDAnz_`4Ut1(D9uWHAKqq6tL0G}3d}IOfwAEq+X2_f1Po z6{zd(dwPP5XTUfTqwTao>m$M&n(5%CZ*%-OZ;BgUIV$0}*vf`Z=?BiK<=vRi+I#Kf zxUY^Nt!Fp1zMh@gZ>79>$DKGNc!ny`U2=URtSy-XPniBXR7E)uM_m}H*@Szv2%gQE zHER4wV&pw&wf@sX7)EsSz5q7zxvdTde7Pr9;Eb0*X8)bRmzbLe(}tnA@cU3PkG*nl zo^Hz{N$l6Yp!MA;HJ28`oqBU0p7Dj1`}5_g7Y^M=nBQQ-)Ou=7Mu39yBKUt}oz$TJ z=teN-Ur=*+pT&*-41Yt)|GZrY%EJ@(B=R5_kXb!gxvKWKNnPePv6V+Srh2-e5hnZ0 z;N3b>7)zPs=iXT1N0oTD^1)unTQ-Vy4DEy2rstN5%i{LRl{1*<`PaahdW_T#NYH-E z&tIB(Kq%Se(fxeok%z@U1q~7!l?ehvVx#`tB&1f=dX6MXjLg}OAf7>*_Ai5*1&@s_ z9wP%3kcOjw*Xb+GFU;BMeuFZu$L9Wys>vPVY zP`T0kN={3fD){z_Xkt<2h5ck?ilPi zb!t@O1ew0HKB#95gE7}Ke$H=4El|$_$+Kl*wK);z`QMjjPo5$AiAzK0JE42WaeV{0 z#z32z2Y~k3F!{_D|Lwya7si#4qD9X~L+q=dCU%R#Fhzwjn>8a!XFI@!_;58}ae@Ch zaU%q`mti9B6H*~gT)|z4`xfO4;=Z*z7!QN4Fmvb$@a}9jc??Xc<19KW-({}obJ7lE zHK2p|aFxGjEScpzy6~raaMn_2jO(#P-&MO`q2mp?^V268Jh?WNQx(ZGxM{EY-ag(P ziF*rtXu#XHKRvab9|l!o9aVwr(9`y{9Cv1x9FEeO94&x*T*Z3bi7R~jn%|N*C&Vg@ z?p&K3pIB$k3?^zwg*(r1oaPqWL**x3hdy!g!B&XK(S5eIfn!)RJ?#HHw*-e6-p%{o zr#ox7%}tFhu-rPyJ1(y_=AOs2=(XLCJ2>3334$(ycRsP17vbFkELX|>mvJN<^dZA@iM0R#P@)5d6qU8#UQCO># zj(QDNjAZ#A)P>>`$Ialzs%Mp19Da(=nPeI)q;XTXn06 z@y%CUtRlkO;Jq>$`x<7fenFxJ*Cz>4{Xu-2#Ghf?5r>z!MYmL7)c6eI;tMK^J_C)f z?P~2LCD5_uXV6CS52lNF{DJQ2!wUqrm?@#x9{@j#nt7s{d5|ZHk;MqCDstE;(DqkR zDj>$X(`7rH&}d4?1-^d1H#gf`_cP*Qz_)lK`^Tnq<83x?vfIl#9F%?6@?hFfy_p$2 zy0V5?`EwJ>MlZn`uCh;Bo^yDJQBLUCImFnKUK-)t{>*F)ZkBD;u1zwvQjhtphbRbr zT&M^)kiT$}qWS}IlKgGP^+D{9*$4P8@o3n|2bSg}zYtlnkg+n=3cFJi0lJ4+tU#gS zmG~*nU)4AGr_wF2b;->0<}w_G!dBwDVx>*)=ge|eR;a<+=pXwY9eZRW_QR*$Gqa3?8_QHs*6&>B%RY2s2?VnmI_+L~(oiAQL}WjJ<6s?J}J9 zq_p%O?J&}vCR^04e<)D!=bZ%ufs%8tBgW@rwQX-rJD4{2uAGJFEg=z3eMp3}9jW=D zVr7$*^fxe{8C)q(A;4@U{C|OI%<~s8>-l*tVE^dB`XT{Mn7>LdLPvj>PDP>rE?tT% z{v8*Uklg#r3@BCncN3WmLGqO}?O(c*Oh)uy1%l)NnSZwuB=`Tbf-3uuH>$F!}6%4zhC|{jthYL=cAJhl%x4; z8G3RAFq?RP`Vs(;lRwI#lKt-cPfzs!?z;y_2Fc+hugPH`{xjSd20FJbD+`xnFB=u1mJtRREoBmMW+BS_9wqDJVJ{yqNx E02ZC?+5i9m delta 11784 zcmZvi1ytN@x9@Q&E``C}Z3c?FySqCSFBG?d;xM?E;;zN5EiT2~t++cBhfDi@=iK+) zd$KZF*~#8JPv%)!zx?-3cQ*7;HZ;15JS-e86cQ2=6oUAzR^llvRSk$58t%W0I$!92 zS&6Z-@O8s5WdEG6JB5}0m!~28zdY+o5dO;=M`YyNS6h<7^#A0Cm zfc6AM+jcyW$L>v|Rgu>WW9a=U1;fAhRnv@DPD#sRSin-QcaDa5nl*w-vy!ao!SHr$ zK1SBjqBWK(ZT``cUV6_wf)M!apz4hW$9Dc_`y;h?vH0EUUTJbMHSGB*>2IE<`hC{| zvys7>D?igCQgvp96T!b;Kv-*3a4P4x4;$B3g?7D3?J-i45=B3KXI1Eit z1n;YviQxo2jt&!E_RjT$`EygCwiz+9TrM=Kx@M;=Sn<%;Ib6E8QoAi2ADjT&pU>_t z=Qwh&4aVDO^3$6fbnJS8O2dP-NTGW)4I9T-Me6eW4P9xMcIqrNGRG_$MBQBCM){x7Ta^L~CUuz%#lC@UboHSgO<9JG1XX(}gcc>6M3k$>6AQibG9)gv zDrDZ$()b;RvsY%U=c5SV(<@ZU*1vVqrMuH=w4ay}|2&>{y*Eh&AIbd&mz02>+qE*F ziuytpQ-b2qhDRe-;D9HgvpcvaPHSOOjTYVS9Ryn}zeGCKaUYj#*2I*VTIv9Ne|wT$ z7hA4u-4zV-`X(dggnur>Qn#xkeytm`(M=qp&l<7+Zg#PY+d69R5dsOxkQSpuYfuwb z=;GPILb?A^K4ybUHch{*ECbPC^H_T{@YK1=I4=`ow_j2WteI%QWuGBqO^=Q3{^0xS zLGKzp)-$VJ@jLoM@B0Gn!7i|f!VUGg59N@nCHtrAu5y@&h+((M9-QBR=oz{n<}t9GSx2?O#~Le8ZjJpr$3QOxP_H-6}x9s*FHH5>@?iCU*p!sLQl$?~@Vn zJp@7W$Lj0@TPT|yz&;Ec=g`0NR%8B7tw1rJRn)Ay6tiV5HMX@7xU2>^%EhL_C}Re? zQ_{gImtL(k(w-|O+z zPHQC_*R+G&ocZ3p`~+nU2BBwGEpnBtUoI3WWV?rb6XjiTU=7{2oidR2JZR6o1VgLH zBO-+yg@q|0LP4pLKtcUK%ssNPr|DoXJB+SHOOj?DP z=P$>Iv9hW~ck5;{MJFHPh`H{#1eXm_+h4o{k;8&60A!8lLc-Me-Nv%KkACM%j|^=7 zDL1k%#7LjSy+GfENy)5Y)E!R}nOTby39u-@Osie-r|$c-ErZ;}%)G=z36u-{(x(1i z*+NMy`oAH2wXW)0mTmk>++MV=6eXhbTBU9fHSN!_zr>5GZNByLPD2_*H0_Ob%_arU zxHEk2k6JI~8K&-+NLL*g4t`!RZ*=M$3vM5A+#3EF!8a?6bNUL=If_?^0vV_?oqC5wIi(KvNL2>eRXXePwALXVq%+Ya4f5*lsr zEt6uF+~#G@VxREEqxJQW_YxL+&750D$mmF^z|MlhWC?k9C)Yx6@so4<>5EcRzYj>N zSV(N(oOUmjOF0+i*-8HL4Ue@F&iX>b7)O zxVd4k5WPYb^EmhoypNoaeue10p_G|5Dz13|fY$^SRf_NU+^hsiTw)Kp>r62!2G=|o z;MW0g1Nh7;Oz$*PkDUBMu!N##iK|9wsTIj%wXh7tY>H}WH0|j7R%6;>)Y@R$aiX&X zBwr=72Yl@>tZ>_ZIQRfgo!Q|o5J_XD>Ej0dJRKG=ue$x*I(}MFMvZY*ed&&)p`O1aFS4-S>_QTW<{Rn3rHI({SVf`IVy;bwesc9ert)B-kWY2o5CQczc~);9b1`Lb5aw6x zXjf}FnC`$nJa&bnNrFoIy+xsvYfSiPZ=?o2)xos|)nL72=qbyYS`ILoHeU0KW<$RR zyLnx>7i+Poa<+(Dwup1K$hu#ki})PjUL?RvH?_)h+J!8L8##g^x>i$~AU*OK60&@1 z7gjpDhr2dSZ|_DUb#Q<6oIO!|y&DfS6&p%f&iQ9?Hj|y_&xVt!Zl<>NWZsV#a9`SA0|QPoAtVY9=IJCn=K^qVP-;j|5z@t z-xa`4QF}LQeF}*w2VTrOhIdLjc0Iq}1T7)W@d)6|2mh(%i)d7HO-vQ^y#Tu}DS^W7 zurGxvrBANw=wct5&17-B?y91RRAO3A<$GIaV4yJr^<;@MRd;cG=l4FCBLg?lk6Z3< z9Ya?`D~FP4#qjnWqt?R+5bCmfg82l&Kp`CW7~>D243my=XNBl_b~&RosJqw*>P0<2 z?7i5T|6kkxD3iJ zTe7wwh?sJye>R<4*EWxv%hkcql~?NhNE4Rx-8r3$I8r|s29vop^_yjhqqv$AQZcEn z1LIiDKKXa2`>`#fS;W@Lo`QypK%nLlQ1A!t96|CXBr@xQ$1^H4@=59ElgH4YKk-E$ zT)*ehSi%OYF71OK@BCJ=_|vX9k-5gI)1LhbwFH{+aFgU?66py-FY}5KYSeY3f9Qwl zH4*$5m_Ou90d>-fzmik>|08GJ89wxX1n3`Z0tm3d0ThjH=PYk)IfJ3nKkJX29G#TY zId^b6D=Z)b!R@sy@-9uNM{4kJ4_EuMHl_O5b!*v|D|;Z8@``z5NW^Be^uE8LeeKgq z@9jhOT;h9yGV}re#G^<*Z_snH*5=x7`$$&gg|%CDQCK|ryf*&kL-)GrLF>%D&`9DS z1L-9Ahi4tjpa5^U@DP8KH~o~w6kwY_)!XRTj%*`Y6YA6j3vsV|M&(qwncwi#Q!Wj| zl%Wgey7tY2_)N|ASUBt0tflJPrJy=imjP-@op1Y>d=4;?-q72JO%h3T+hd#E5-LLB z^!^s#j?T&>Xw_0WB?OI4~BoCywnxHYxpu2xoMhuJoRugWzlf{Dy`=;Q?B z8rDPTW3u6^a%J`_gN)^S6TZ+#X1^*|#j&3~M%>8wLUa8GDqBr9ho2zQ8Lk<`9^f#^ zng{+R;*(yE12_f27cBvX>eFC<#dcPA%}e!Kx6n=1tmCD%fsdCDvTevDi@J*rCH1+3 zp`b8Hwnd;#cRg7VRs9m&B&`b9yOOYn1hn&3XfHm#(*~?~)<4We5BAU^W?FCT;zs@< zhd$t|g{PcftEH$iOO_(mUS^5|zANjQ>TluxNM@&g&N4n2>_MCQ9h4i2tV)Y!HbJTL zFq-QwW{hc z|JYxi9IZs!Ia+j;=K;I;psYv%3y&Yh5468$R`InDU->Lu9wLsPA`a-Y9UU7Wz0>`m z+;!JsKVF!mV`~G)v0}>C>71ts*BP zE`(VO`NBs}3fD@eoorC$*BzWy$rxcb+}&4NdL*6@7WY?`1GwBKFC%)isE=8kDIwc-%HtS8i^t9O3iFoN@jgFE-i!5ishBIA9#q} z?2Y|uG5V%xt`|!g%eu^3?wjv@srXEp`AqHWL(?{}7uj(reZ&QHt4q2ONi|rwG!k*> zXVuG!^TSJ~YLKg}-5JQPErK45Lu_tj@D{w0$hg1(h3ABoHtpqbG6}4D49LfNG@=J9 zR|vCYf~ryCzDI1W@ZK&VSt2B_-E(hlBLzt(>BIL#I&)jMRfoJ#v#6;<>qk=rt$Qe# z-#meN!VwMjBbClYqq{PzdStY-S6@C}*j(R9NB}8zqi(N!u`aN<04eoqvuDLA{nFb?YobM=jk!h7_%zAB_1 z9u*Zrbo-2SBQJynOQK&I1B}B`Ir6V{lzjb7v^;-!BO5*S6Jrf`Ds z%$#TLhLfi>e=ci(!-Gb^MK7!L&_`Vx0_r?c(@t&&o|lGS(ySH9a*I+{hlI=Uj3cL2 z(V)&@BgnsUmxJLl+7IzfMUa?ca~18fnxe@_QW_GCEb3E5D)puSNk3@(_h1)Q^FtH5 z#5`IOb(K7JQxdg~D~;3}nj81UawM>xr=b4rPBbEFnhp%@Q^1`_`ucrYv^p-!7YUSz z_z4#$N@4~Y@dD}wIH~xA=eUH{Wfy~2t$JbU-5~$6(;Lon4~N4`5nO#-IaumLxws@T zjQKWpGd`fVZ5|&@!qPzeGH?Vm*O0imh>TH;LKG^>!jcxq)$1m>-JF%@BVdKjnwPto zlX~%MB`=1o_&ZuBr{n!){dal%eMjx+*Q?P>_9S1sCdr?LC{90!! zPf~g=ZgwAm-U>sEOfmT8?>zeRubNrh7tT-29-qbzb$LiN-CUMw!d|Bt0xq1ny_sxa*(T#XvmaAMts!T zmPohYybNLrwzm=90Q5{++{OsmSI8>k3`5I0=4Od!3=*Sl`()oy(*CId$i%z!~AagLBE^3vUJ{ zaV+@aX#PndYbjgVmp09u!X~xT070HWY=_!cHS_~P-W8^0u3_IUds|Ycf8BDbMH~~} z7USXaoK|qKSNJ2MFgasE$v$%NM_;1x@&YBTp;}MElVPcAK*{t9n*4TcOurFxEj+lzAH9l+v4)C)EB=}D9G{MeZa zo%*)N_X$r*jm~YC6taa<&`c$Xu+Kt$ZSti=P|47OPYX_0_(SP9p&{3|Tk^ai0W$8E z@7#DRv2nJ_KaZ5@KVeek z3nR3+Q*~6>tp(WWke+o%qVN#gqmsiVRUg^|Vif6Fmx6(xuYmo0W=30^Mjk~+F%;d| zWtYh4H+@BHu>fHHJ}HAoh{ow&F#3l6ogSL|I{G`16crc6pPv;~qX}ej+*HNm+a8NN zsm^Nt&%F>p^A$M3{K_bNeIIjwm(YwQmai*2?88jF;DaX<1kdisT|Zef#Pi2p^PO+^ zXVad~rUjhhb9-`gUkE5xN%M3S!~!waxxQn4&|VUaUsM;AW={;J(m(VzOZ6?;<@Q8K zNjv#sajuWC-icm4TNGXJTY>@TOIfik)R||zNrjD4JGF`-@xM(9L%%oUPpIJJRt&r8n~e3?`n|9U4;& z(5;jZ=dHTaKy_p-K@9y9i!-iHP>Fiu34{2@t<4iVk<_yN`R|pcp3B>Ss>7)VFl{zh zHr;SA0UtOvf8UE(X`3DKgj2_Gr`ju{`)s%uBxasJ0_Khgszd$^;#N*qI(n6~hGW%U z!rf{dUS&iw*Z#qEDqoR3JYJSX|s z;aWB;y(dB*At&)kA>_;^z#PUekjEvBLXSccixfPcCs9aB(dC+c(=#kJ56go2E^g=b z2uMA)gS7|rG>^4)x4i1+uxJ*ur zB^hp3J_b1&sU{@*nzF#Kq!fj@y>=XU@LVnqKzk=&W{cG@O{wF+8|o85c9^7O1)B20 z^I3$2s>jo=gB-PmI@T{3 z3tn(GbSS|d0r7F`U?Xa=@{Q}w7n`$QN5<6NmdV8IBZJUqEDtH!CX!)3R*Q z7CWIX)&ym_q~aZoF-xtw7i4NrUWic%S^0VFa4#)070=1QdEL(UMMVq$Xr?R0{W`3D zbI-t4D7K92Im=SGOt|qOOBLToAOFk4a%LpE{HkO$ihcBN@C1bb$&j1-Bi(8$#v@^R zi-yt5=S-UUbT1{z_MsTbC-v%v&{_J3sU447gBPJF{#8hQiWL#1eRzGYtJD*|LrHb+ zAciK^X9ok*O*x_sts%bwgnV~V8ObpJCd%c|uzm1PSgXpnQyiw;5noZfaZ=W8TE{!=aGrY;0D^c3N8u~9A-t^o>tNEomEI0GKuWo z`(QXsJ`%Ba;;@h^3s+95H-Hr(DlQK1go!Btfyosg#DXhM(ir=mFc%i-DGHB7rccD& z72156(>s<0A5bRao zB<1B=ayrVLoQLa7gI>Obku3}+`B<#|=inqk z%z|PNss~xDng&BM;j8kNE+!(c!i|^9rT&_ELI-V?ZoB|$55kDO ztCZm8z!YYg+8G&%HMxWiBE4F^zUtx#JkjcXR|xGspG2Z7NuGQw^k#>V9k4lF-Dl_` zx$ORP88)isOuLBYH)~eT6Ydb63dgdbO<>N~wneYdQe7(Cs^ex>F!Cb-rQbS#I(E#6O;L(AoDO7R-t7HfhL_|mQ ztLM_cj@-=*^QnitItX5h=i&DE+IjymxEK^`8fpABXyCA+V(v^vHc0}z+x|p6RcXv} zd(@B9$Vb~;U>iB>LvgU-FU?ZiiDCzsSgAlPquzk7Do@+Hlh2M!BsPtx`Qi?xq0V{A zo4A8{9Y|rzbFRa?ux&rJ1z-~+^vJ~_Xr3Amv3pZ|A+f43n{L`N0vXbIa6OXm!8xz5 zyKcBmxkD>T*U?Clt5BV=r3JQgs(y2-w>)&fr^Wb=6L~5zbV#`C>;#60gKU|T?3?W8 zJ*Q&pMJ32*x(2e+HZlfWj>U9aMvzlKg2P^Gyv#`cVvNRG46YoZ+tt=!{4Cz_;MDS% z`!Bn5o}d9OjK%4*0<>#o6JHt(aMG|>8e8*;Uij}E$Cs@h`}fPaaxx{X)VPdA9pT6c zldH}suy!?ia!DyFK`9~wxBz1&EaQ94uM(MvapXuQJbY-y2xIS=0oX`|Qqwt7Dj(qc zPSM^CGGL_h|Ip(nnFB6&U^+r|k$64?Pao11v_}T&u>c(u%dr^abc6Zkz_3k-$dk2u zXc9#hDf^!W>b1>vj$PJCJ(e$XSJN9Se?=jVM~pN%0=IvF_J>7>yRUCVbmZa*2mpxb z*os9_v_WMKT`FnvuvWi-7Ma|Uq0*+!in=$2c~x^2H^e{Z%iU&h^?Eaqjz)r(zCt(;<(K}uW?&AQmg&7vx$fl znCoJii~t2?Px!xfI>|u)LK-PMBG~R@1`qBN`~#W5^Hu>U2a#MLhCh0f`s9jp<@m(E zkV`3zFcF$x_T}jfrpz)^>%vGuG*!&#kJebKh~c$1i-!qywImz=W@4OvqS%CP+O?vv zhoSQ(VaFmf0mj&KQ=-#LaTng_^rX$EsI{Fjuk(CgnS@OlY~sP8^X~Io9`MH%m&-T{ z=l!kN0a2laZAm<`?XSH*^o8(bzgN;(`uwThS~+_IxKZmiIEn8ZQymiyeLoH+bk|TY zB>4Fzoup#yn=jN_vW2tCb4wcSe9NyJ;OM0moNpc&qAexC77VK$u@e~F*y!JPW@I2M z#9y28^M2HQn3)hTj8X121sCxYWfIMNNv{|78T`$18*(;x7yXH#MRn`}f8#}%cB3U2 zijj0v=lg1<=M>2DYyLiao}2~fD?>YOl$Sr>$&~m-At1R}0pVp=vvw?-t-toeOvl{S z^STaNb5zUHNV)K1I_+5DxtB)r`HjGf>Qn9NZ`$RlI21ANQQo|81ETKHpVj1*1DyFYc;v zhPi(Pd`!FDCNA0mI)vF^9Dju2q}$sW&(S;ix`X-EFs}H=<+Foif!phsN0^9ROS;6; zT@EnDbXR;reNkTVVBy$EM%#qi`|5;qCOfY5uWB(uEZ4E5Xp!CGR;jceDqn zcs6mNmK5?6A!zh3hC<)@J9Xc}Qdm03tZ9xIYV|q|!`QT{rFHb=G;`GT3*2pc0#EF7 zC^d)mnWFBhxDt=w5rXvuQ|s}!>`zW?X9t19IER|LNa|Nibr<_bjhyoOb(86rL{rS` zKaWV>F`39^?B>o?`SR&Exu@IQR9Vkq4743rlVjE+>{Mt< zYjw8w;)3qV1&%F~p3&Ahu1qWBFn=z{qM}#G_m9!B=_d3P3Ixj@)JXxQm$UL|nb2%H z7>1!vEd$E;q*ntFaDH6IS(!&1urwmm66?BAy?r`e6H?M}auqa*BDH9dWGd)6if_ zD>2g=XN%Lg0e2I=gm+8;Pwgsuy$RUok@~QhOn4B2mQ}*1Pw9P9duHUH*_Qze zK#d0kR2KoGKdN7y3u!X-9C6Oh3V?&N{T^Utp(LMDob0OV`;^tO_J>5}i$297!wiKA#BEe{| zACX%qL;-tf-ueV^@H_R+UXKZ0dhKH(=wCp= z^E9v2tZ7~|b}z@ezlGaV0lr>t%k1_EI_OOaP>c!AoMV6Yt9^$0;%mib7YQ4FRizkg z!)dCc2uM;4QAKpS7 z63uhBq)YK97f?B%{{2AwoEr<(at8F@#tFeB4q+8lgG%cB=Va+>-E*|8QWh6`fChN z9&b16I33PmIo^ad_6q+2qc;zZ$muH>D~SGAFzWFB1ID^{e3r0(E30*xf?6>D?A-_- z{%3DT4EjHNhZ0Kv>3#}I?f&CNFJ1JXLqIvg#3LE{e=4+p40hRn8J_<(+Wu|8$^EyR z7}}q&fdBi$r9`kC??36j%Mrq8VI(?Z&?G*|p_2dc`gfBS0QK)h4>Pet{%xX=JQnib zbkNst>k~WW-ok9+zh)pxjFiVC`*Y&o)#m@1VK1>=p6+jOp2c|5-{3?z1&+U?`)~c6 zD53D5(FH0H!eB7`&E2ek_jh#vekJ*zxp5i)j!uLTJ`qU~_3uS4xQ?m+8R04LZx&_6 zf3p00{{Cl{E70F8pB1tG`GdYH62dU>{tezy#QS?JL5UEWHIYb(@*fY;S2H!yK#A(_ zy>Ax7v)98wK@FopLA|d3TFw)*l!*TcX?qR%mAIfp`j7dbL Date: Fri, 1 Sep 2023 12:45:19 +0200 Subject: [PATCH 35/43] MP-3310. Oak no. 19. (#316) * add-whitelist * Fix whitelist. Update tests. --------- Co-authored-by: brimigs --- contracts/incentives/src/contract.rs | 1 + contracts/incentives/tests/tests/test_admin.rs | 5 +++++ contracts/incentives/tests/tests/test_whitelist.rs | 6 +++--- packages/types/src/incentives.rs | 2 ++ schemas/mars-incentives/mars-incentives.json | 9 ++++++++- .../generated/mars-incentives/MarsIncentives.types.ts | 1 + 6 files changed, 20 insertions(+), 4 deletions(-) diff --git a/contracts/incentives/src/contract.rs b/contracts/incentives/src/contract.rs index 4eb5e032e..22f3d099e 100644 --- a/contracts/incentives/src/contract.rs +++ b/contracts/incentives/src/contract.rs @@ -620,6 +620,7 @@ pub fn query_config(deps: Deps) -> StdResult { address_provider: config.address_provider, max_whitelisted_denoms: config.max_whitelisted_denoms, epoch_duration: EPOCH_DURATION.load(deps.storage)?, + whitelist_count: WHITELIST_COUNT.may_load(deps.storage)?.unwrap_or_default(), }) } diff --git a/contracts/incentives/tests/tests/test_admin.rs b/contracts/incentives/tests/tests/test_admin.rs index 628272a19..864f3da72 100644 --- a/contracts/incentives/tests/tests/test_admin.rs +++ b/contracts/incentives/tests/tests/test_admin.rs @@ -32,6 +32,9 @@ fn proper_initialization() { assert_eq!(config.owner, Some("owner".to_string())); assert_eq!(config.proposed_new_owner, None); assert_eq!(config.address_provider, "address_provider".to_string()); + assert_eq!(config.epoch_duration, 604800); + assert_eq!(config.max_whitelisted_denoms, 10); + assert_eq!(config.whitelist_count, 0); } #[test] @@ -87,5 +90,7 @@ fn update_config() { assert_eq!(new_config.owner, Some("owner".to_string())); assert_eq!(new_config.proposed_new_owner, None); assert_eq!(new_config.address_provider, Addr::unchecked("new_addr_provider")); + assert_eq!(new_config.epoch_duration, 604800); + assert_eq!(new_config.whitelist_count, 0); assert_eq!(new_config.max_whitelisted_denoms, 20); } diff --git a/contracts/incentives/tests/tests/test_whitelist.rs b/contracts/incentives/tests/tests/test_whitelist.rs index 120097fb3..0873e14b6 100644 --- a/contracts/incentives/tests/tests/test_whitelist.rs +++ b/contracts/incentives/tests/tests/test_whitelist.rs @@ -10,7 +10,7 @@ use mars_incentives::{ }; use mars_owner::OwnerError::NotOwner; use mars_red_bank_types::{ - incentives::{ExecuteMsg, QueryMsg, WhitelistEntry}, + incentives::{ConfigResponse, ExecuteMsg, QueryMsg, WhitelistEntry}, red_bank::{Market, UserCollateralResponse}, }; use mars_testing::MockEnvParams; @@ -356,8 +356,8 @@ fn cannot_whitelist_more_than_max_limit() { execute(deps.as_mut(), mock_env(), mock_info(owner, &[]), add_whitelist_msg).unwrap(); // Check whitelist count. Should still be 10. - let whitelist_count = WHITELIST_COUNT.load(&deps.storage).unwrap(); - assert_eq!(whitelist_count, 10); + let config: ConfigResponse = th_query(deps.as_ref(), QueryMsg::Config {}); + assert_eq!(config.max_whitelisted_denoms, 10); } #[test] diff --git a/packages/types/src/incentives.rs b/packages/types/src/incentives.rs index 41b4ad52a..c94cad794 100644 --- a/packages/types/src/incentives.rs +++ b/packages/types/src/incentives.rs @@ -303,4 +303,6 @@ pub struct ConfigResponse { pub max_whitelisted_denoms: u8, /// The epoch duration in seconds pub epoch_duration: u64, + /// The count of the number of whitelisted incentive denoms + pub whitelist_count: u8, } diff --git a/schemas/mars-incentives/mars-incentives.json b/schemas/mars-incentives/mars-incentives.json index 455cc19a9..ae8071490 100644 --- a/schemas/mars-incentives/mars-incentives.json +++ b/schemas/mars-incentives/mars-incentives.json @@ -688,7 +688,8 @@ "required": [ "address_provider", "epoch_duration", - "max_whitelisted_denoms" + "max_whitelisted_denoms", + "whitelist_count" ], "properties": { "address_provider": { @@ -724,6 +725,12 @@ "string", "null" ] + }, + "whitelist_count": { + "description": "The count of the number of whitelisted incentive denoms", + "type": "integer", + "format": "uint8", + "minimum": 0.0 } }, "additionalProperties": false, diff --git a/scripts/types/generated/mars-incentives/MarsIncentives.types.ts b/scripts/types/generated/mars-incentives/MarsIncentives.types.ts index 1098c5bb7..a463fd8f4 100644 --- a/scripts/types/generated/mars-incentives/MarsIncentives.types.ts +++ b/scripts/types/generated/mars-incentives/MarsIncentives.types.ts @@ -134,6 +134,7 @@ export interface ConfigResponse { max_whitelisted_denoms: number owner?: string | null proposed_new_owner?: string | null + whitelist_count: number } export type ArrayOfEmissionResponse = EmissionResponse[] export interface EmissionResponse { From c810de1b737ef603d735734bceb2dc6af1f73462 Mon Sep 17 00:00:00 2001 From: piobab Date: Fri, 8 Sep 2023 10:03:53 +0200 Subject: [PATCH 36/43] MP-3299. Oak no. 8. (#317) * Incentives migration. * Add tests for migration. * Copy helpers for incentives from v1.0.0 tag. Update schema. --- Cargo.lock | 161 ++++--- Cargo.toml | 16 +- contracts/incentives/Cargo.toml | 19 +- contracts/incentives/src/contract.rs | 7 +- contracts/incentives/src/error.rs | 3 + contracts/incentives/src/lib.rs | 1 + contracts/incentives/src/migrations/mod.rs | 1 + contracts/incentives/src/migrations/v2_0_0.rs | 286 +++++++++++++ contracts/incentives/tests/tests/mod.rs | 1 + .../tests/tests/test_migration_v2.rs | 401 ++++++++++++++++++ contracts/oracle/osmosis/src/migrations.rs | 4 +- packages/types/src/incentives.rs | 14 +- .../mars-address-provider.json | 2 +- schemas/mars-incentives/mars-incentives.json | 2 +- .../mars-oracle-osmosis.json | 2 +- .../mars-oracle-wasm/mars-oracle-wasm.json | 2 +- schemas/mars-params/mars-params.json | 2 +- schemas/mars-red-bank/mars-red-bank.json | 2 +- .../mars-rewards-collector-base.json | 2 +- .../mars-swapper-astroport.json | 2 +- .../mars-swapper-osmosis.json | 2 +- 21 files changed, 839 insertions(+), 93 deletions(-) create mode 100644 contracts/incentives/src/migrations/mod.rs create mode 100644 contracts/incentives/src/migrations/v2_0_0.rs create mode 100644 contracts/incentives/tests/tests/test_migration_v2.rs diff --git a/Cargo.lock b/Cargo.lock index ce2082c52..2309a7203 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1726,43 +1726,44 @@ checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" [[package]] name = "mars-address-provider" -version = "1.2.0" +version = "2.0.0" dependencies = [ "bech32", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.1.0", "cw2 1.1.0", - "mars-owner", - "mars-red-bank-types", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", "serde", "thiserror", ] [[package]] name = "mars-health" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-std", "mars-params", - "mars-red-bank-types", + "mars-red-bank-types 2.0.0", "mars-testing", "thiserror", ] [[package]] name = "mars-incentives" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.1.0", "cw2 1.1.0", - "mars-owner", + "mars-owner 2.0.0", "mars-red-bank", - "mars-red-bank-types", + "mars-red-bank-types 1.0.0", + "mars-red-bank-types 2.0.0", "mars-testing", - "mars-utils", + "mars-utils 2.0.0", "osmosis-std", "test-case", "thiserror", @@ -1770,7 +1771,7 @@ dependencies = [ [[package]] name = "mars-integration-tests" -version = "1.2.0" +version = "2.0.0" dependencies = [ "anyhow", "cosmwasm-std", @@ -1782,11 +1783,11 @@ dependencies = [ "mars-osmosis", "mars-params", "mars-red-bank", - "mars-red-bank-types", + "mars-red-bank-types 2.0.0", "mars-rewards-collector-osmosis", "mars-swapper-osmosis", "mars-testing", - "mars-utils", + "mars-utils 2.0.0", "osmosis-std", "osmosis-test-tube", "serde", @@ -1794,12 +1795,12 @@ dependencies = [ [[package]] name = "mars-interest-rate" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "mars-red-bank-types", - "mars-utils", + "mars-red-bank-types 2.0.0", + "mars-utils 2.0.0", ] [[package]] @@ -1814,7 +1815,7 @@ dependencies = [ [[package]] name = "mars-mock-pyth" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1823,14 +1824,14 @@ dependencies = [ [[package]] name = "mars-oracle-base" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-std", "cw-storage-plus 1.1.0", "cw2 1.1.0", - "mars-owner", - "mars-red-bank-types", - "mars-utils", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", + "mars-utils 2.0.0", "pyth-sdk-cw", "schemars", "serde", @@ -1839,7 +1840,7 @@ dependencies = [ [[package]] name = "mars-oracle-osmosis" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1847,10 +1848,10 @@ dependencies = [ "cw2 1.1.0", "mars-oracle-base", "mars-osmosis", - "mars-owner", - "mars-red-bank-types", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", "mars-testing", - "mars-utils", + "mars-utils 2.0.0", "osmosis-std", "pyth-sdk-cw", "schemars", @@ -1859,7 +1860,7 @@ dependencies = [ [[package]] name = "mars-oracle-wasm" -version = "1.2.0" +version = "2.0.0" dependencies = [ "astroport", "cosmwasm-schema", @@ -1868,8 +1869,8 @@ dependencies = [ "cw-storage-plus 1.1.0", "cw2 1.1.0", "mars-oracle-base", - "mars-owner", - "mars-red-bank-types", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", "mars-testing", "proptest", "pyth-sdk-cw", @@ -1878,7 +1879,7 @@ dependencies = [ [[package]] name = "mars-osmosis" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-std", "osmosis-std", @@ -1886,6 +1887,19 @@ dependencies = [ "serde", ] +[[package]] +name = "mars-owner" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acd53908ffc561da878ce5ff4f5ec9f25a193af28ec0b6e7c8e6d1a0866d9dfc" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.1.0", + "schemars", + "thiserror", +] + [[package]] name = "mars-owner" version = "2.0.0" @@ -1901,7 +1915,7 @@ dependencies = [ [[package]] name = "mars-params" -version = "1.2.0" +version = "2.0.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -1910,17 +1924,17 @@ dependencies = [ "cw-storage-plus 1.1.0", "cw2 1.1.0", "mars-interest-rate", - "mars-owner", - "mars-red-bank-types", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", "mars-testing", - "mars-utils", + "mars-utils 2.0.0", "test-case", "thiserror", ] [[package]] name = "mars-red-bank" -version = "1.2.0" +version = "2.0.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -1932,11 +1946,11 @@ dependencies = [ "mars-health", "mars-interest-rate", "mars-liquidation", - "mars-owner", + "mars-owner 2.0.0", "mars-params", - "mars-red-bank-types", + "mars-red-bank-types 2.0.0", "mars-testing", - "mars-utils", + "mars-utils 2.0.0", "pyth-sdk-cw", "test-case", "thiserror", @@ -1944,26 +1958,38 @@ dependencies = [ [[package]] name = "mars-red-bank-types" -version = "1.2.0" +version = "1.0.0" +source = "git+https://github.com/mars-protocol/red-bank?tag=v1.0.0#13fcc446fe687dfb8f08ffacd57b1ab6ad6cfcc9" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "mars-owner 1.2.0", + "mars-utils 1.0.0", + "thiserror", +] + +[[package]] +name = "mars-red-bank-types" +version = "2.0.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "mars-owner", - "mars-utils", + "mars-owner 2.0.0", + "mars-utils 2.0.0", "strum 0.25.0", "thiserror", ] [[package]] name = "mars-rewards-collector-base" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.1.0", - "mars-owner", - "mars-red-bank-types", - "mars-utils", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", + "mars-utils 2.0.0", "schemars", "serde", "thiserror", @@ -1971,34 +1997,34 @@ dependencies = [ [[package]] name = "mars-rewards-collector-neutron" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-std", "cw2 1.1.0", - "mars-red-bank-types", + "mars-red-bank-types 2.0.0", "mars-rewards-collector-base", "neutron-sdk", ] [[package]] name = "mars-rewards-collector-osmosis" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-std", "cw2 1.1.0", "mars-osmosis", - "mars-owner", - "mars-red-bank-types", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", "mars-rewards-collector-base", "mars-testing", - "mars-utils", + "mars-utils 2.0.0", "osmosis-std", "serde", ] [[package]] name = "mars-swapper-astroport" -version = "1.2.0" +version = "2.0.0" dependencies = [ "anyhow", "astroport", @@ -2007,7 +2033,7 @@ dependencies = [ "cw-it", "cw2 1.1.0", "mars-oracle-wasm", - "mars-red-bank-types", + "mars-red-bank-types 2.0.0", "mars-swapper-base", "mars-testing", "test-case", @@ -2015,14 +2041,14 @@ dependencies = [ [[package]] name = "mars-swapper-base" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-paginate", "cw-storage-plus 1.1.0", - "mars-owner", - "mars-red-bank-types", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", "schemars", "serde", "thiserror", @@ -2030,15 +2056,15 @@ dependencies = [ [[package]] name = "mars-swapper-mock" -version = "1.2.0" +version = "2.0.0" dependencies = [ "cosmwasm-std", - "mars-red-bank-types", + "mars-red-bank-types 2.0.0", ] [[package]] name = "mars-swapper-osmosis" -version = "1.2.0" +version = "2.0.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -2046,15 +2072,15 @@ dependencies = [ "cw-it", "cw2 1.1.0", "mars-osmosis", - "mars-owner", - "mars-red-bank-types", + "mars-owner 2.0.0", + "mars-red-bank-types 2.0.0", "mars-swapper-base", "osmosis-std", ] [[package]] name = "mars-testing" -version = "1.2.0" +version = "2.0.0" dependencies = [ "anyhow", "astroport", @@ -2068,10 +2094,10 @@ dependencies = [ "mars-oracle-osmosis", "mars-oracle-wasm", "mars-osmosis", - "mars-owner", + "mars-owner 2.0.0", "mars-params", "mars-red-bank", - "mars-red-bank-types", + "mars-red-bank-types 2.0.0", "mars-rewards-collector-osmosis", "mars-swapper-astroport", "osmosis-std", @@ -2081,7 +2107,16 @@ dependencies = [ [[package]] name = "mars-utils" -version = "1.2.0" +version = "1.0.0" +source = "git+https://github.com/mars-protocol/red-bank?tag=v1.0.0#13fcc446fe687dfb8f08ffacd57b1ab6ad6cfcc9" +dependencies = [ + "cosmwasm-std", + "thiserror", +] + +[[package]] +name = "mars-utils" +version = "2.0.0" dependencies = [ "cosmwasm-std", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 71a9d92e2..814aeb606 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ members = [ resolver = "2" [workspace.package] -version = "1.2.0" +version = "2.0.0" authors = [ "Gabe R. ", "Larry Engineer ", @@ -65,13 +65,13 @@ proptest = "1.2.0" test-case = "3.1.0" # packages -mars-health = { path = "./packages/health" } -mars-interest-rate = { path = "./packages/interest-rate" } -mars-liquidation = { path = "./packages/liquidation" } -mars-osmosis = { path = "./packages/chains/osmosis" } -mars-red-bank-types = { path = "./packages/types" } -mars-testing = { path = "./packages/testing" } -mars-utils = { path = "./packages/utils" } +mars-health = { path = "./packages/health" } +mars-interest-rate = { path = "./packages/interest-rate" } +mars-liquidation = { path = "./packages/liquidation" } +mars-osmosis = { path = "./packages/chains/osmosis" } +mars-red-bank-types = { path = "./packages/types" } +mars-testing = { path = "./packages/testing" } +mars-utils = { path = "./packages/utils" } # contracts mars-address-provider = { path = "./contracts/address-provider" } diff --git a/contracts/incentives/Cargo.toml b/contracts/incentives/Cargo.toml index 68d95a662..37ec5391e 100644 --- a/contracts/incentives/Cargo.toml +++ b/contracts/incentives/Cargo.toml @@ -19,16 +19,19 @@ doctest = false backtraces = ["cosmwasm-std/backtraces", "mars-testing/backtraces", "mars-utils/backtraces", "mars-red-bank/backtraces"] [dependencies] -cosmwasm-std = { workspace = true } -cw2 = { workspace = true } -cw-storage-plus = { workspace = true } -mars-owner = { workspace = true } -mars-red-bank-types = { workspace = true } -mars-utils = { workspace = true } -thiserror = { workspace = true } +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw2 = { workspace = true } +cw-storage-plus = { workspace = true } +mars-owner = { workspace = true } +mars-red-bank-types = { workspace = true } +mars-utils = { workspace = true } +thiserror = { workspace = true } + +# Old red-bank types used for migration. +mars-red-bank-types-old = { package = "mars-red-bank-types", git = "https://github.com/mars-protocol/red-bank", tag = "v1.0.0" } [dev-dependencies] -cosmwasm-schema = { workspace = true } mars-testing = { workspace = true } osmosis-std = { workspace = true } mars-red-bank = { workspace = true } diff --git a/contracts/incentives/src/contract.rs b/contracts/incentives/src/contract.rs index 22f3d099e..849a0055b 100644 --- a/contracts/incentives/src/contract.rs +++ b/contracts/incentives/src/contract.rs @@ -22,6 +22,7 @@ use crate::{ helpers::{ self, compute_user_accrued_rewards, compute_user_unclaimed_rewards, update_incentive_index, }, + migrations, state::{ self, CONFIG, DEFAULT_LIMIT, EMISSIONS, EPOCH_DURATION, INCENTIVE_STATES, MAX_LIMIT, OWNER, USER_ASSET_INDICES, USER_UNCLAIMED_REWARDS, WHITELIST, WHITELIST_COUNT, @@ -765,6 +766,8 @@ pub fn query_emissions( /// MIGRATION #[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { - Ok(Response::default()) +pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Result { + match msg { + MigrateMsg::V1_0_0ToV2_0_0(updates) => migrations::v2_0_0::migrate(deps, env, updates), + } } diff --git a/contracts/incentives/src/error.rs b/contracts/incentives/src/error.rs index 7a77a92f8..167efdfb0 100644 --- a/contracts/incentives/src/error.rs +++ b/contracts/incentives/src/error.rs @@ -66,6 +66,9 @@ pub enum ContractError { DuplicateDenom { denom: String, }, + + #[error("{0}")] + Version(#[from] cw2::VersionError), } impl From for StdError { diff --git a/contracts/incentives/src/lib.rs b/contracts/incentives/src/lib.rs index 627a6e152..88d83aeaf 100644 --- a/contracts/incentives/src/lib.rs +++ b/contracts/incentives/src/lib.rs @@ -1,6 +1,7 @@ pub mod contract; mod error; pub mod helpers; +pub mod migrations; pub mod state; pub use error::ContractError; diff --git a/contracts/incentives/src/migrations/mod.rs b/contracts/incentives/src/migrations/mod.rs new file mode 100644 index 000000000..7592b6f12 --- /dev/null +++ b/contracts/incentives/src/migrations/mod.rs @@ -0,0 +1 @@ +pub mod v2_0_0; diff --git a/contracts/incentives/src/migrations/v2_0_0.rs b/contracts/incentives/src/migrations/v2_0_0.rs new file mode 100644 index 000000000..4e3da098a --- /dev/null +++ b/contracts/incentives/src/migrations/v2_0_0.rs @@ -0,0 +1,286 @@ +use std::collections::HashMap; + +use cosmwasm_std::{DepsMut, Env, Order, Response, StdResult, Uint128}; +use cw2::{assert_contract_version, set_contract_version}; +use mars_owner::OwnerInit; +use mars_red_bank_types::incentives::{Config, IncentiveState, V2Updates}; + +use crate::{ + contract::{CONTRACT_NAME, CONTRACT_VERSION, MIN_EPOCH_DURATION}, + error::ContractError, + state::{ + CONFIG, EPOCH_DURATION, INCENTIVE_STATES, OWNER, USER_ASSET_INDICES, + USER_UNCLAIMED_REWARDS, WHITELIST, WHITELIST_COUNT, + }, +}; + +const FROM_VERSION: &str = "1.0.0"; + +pub mod v1_state { + use cosmwasm_schema::cw_serde; + use cosmwasm_std::{Addr, Decimal, Uint128}; + use cw_storage_plus::{Item, Map}; + use mars_red_bank_types_old::incentives::{AssetIncentive, Config}; + + pub const OWNER: Item = Item::new("owner"); + pub const CONFIG: Item = Item::new("config"); + + pub const ASSET_INCENTIVES: Map<&str, AssetIncentive> = Map::new("incentives"); + pub const USER_ASSET_INDICES: Map<(&Addr, &str), Decimal> = Map::new("indices"); + pub const USER_UNCLAIMED_REWARDS: Map<&Addr, Uint128> = Map::new("unclaimed_rewards"); + + #[cw_serde] + pub enum OwnerState { + B(OwnerSetNoneProposed), + } + + #[cw_serde] + pub struct OwnerSetNoneProposed { + pub owner: Addr, + } + + pub fn current_owner(state: OwnerState) -> Addr { + match state { + OwnerState::B(b) => b.owner, + } + } + + // Copy of helpers from v1.0.0 tag: + // https://github.com/mars-protocol/red-bank/blob/v1.0.0/contracts/incentives/src/helpers.rs + // Included as dependency coudn't generate proper schema for mars-incentive, even with specified + // version. + pub mod helpers { + use std::cmp::{max, min}; + + use cosmwasm_std::{ + Decimal, OverflowError, OverflowOperation, StdError, StdResult, Uint128, + }; + use mars_red_bank_types_old::incentives::AssetIncentive; + + /// Updates asset incentive index and last updated timestamp by computing + /// how many rewards were accrued since last time updated given incentive's + /// emission per second. + /// Total supply is the total (liquidity) token supply during the period being computed. + /// Note that this method does not commit updates to state as that should be executed by the + /// caller + pub fn update_asset_incentive_index( + asset_incentive: &mut AssetIncentive, + total_amount_scaled: Uint128, + current_block_time: u64, + ) -> StdResult<()> { + let end_time_sec = asset_incentive.start_time + asset_incentive.duration; + if (current_block_time != asset_incentive.last_updated) + && current_block_time > asset_incentive.start_time + && asset_incentive.last_updated < end_time_sec + && !total_amount_scaled.is_zero() + && !asset_incentive.emission_per_second.is_zero() + { + let time_start = max(asset_incentive.start_time, asset_incentive.last_updated); + let time_end = min(current_block_time, end_time_sec); + asset_incentive.index = compute_asset_incentive_index( + asset_incentive.index, + asset_incentive.emission_per_second, + total_amount_scaled, + time_start, + time_end, + )?; + } + asset_incentive.last_updated = current_block_time; + Ok(()) + } + + pub fn compute_asset_incentive_index( + previous_index: Decimal, + emission_per_second: Uint128, + total_amount_scaled: Uint128, + time_start: u64, + time_end: u64, + ) -> StdResult { + if time_start > time_end { + return Err(StdError::overflow(OverflowError::new( + OverflowOperation::Sub, + time_start, + time_end, + ))); + } + let seconds_elapsed = time_end - time_start; + let emission_for_elapsed_seconds = + emission_per_second.checked_mul(Uint128::from(seconds_elapsed))?; + let new_index = previous_index + + Decimal::from_ratio(emission_for_elapsed_seconds, total_amount_scaled); + Ok(new_index) + } + + /// Computes user accrued rewards using the difference between asset_incentive index and + /// user current index + /// asset_incentives index should be up to date. + pub fn compute_user_accrued_rewards( + user_amount_scaled: Uint128, + user_asset_index: Decimal, + asset_incentive_index: Decimal, + ) -> StdResult { + let result = (user_amount_scaled * asset_incentive_index) + .checked_sub(user_amount_scaled * user_asset_index)?; + Ok(result) + } + } +} + +pub fn migrate(mut deps: DepsMut, env: Env, updates: V2Updates) -> Result { + // make sure we're migrating the correct contract and from the correct version + assert_contract_version(deps.storage, &format!("crates.io:{CONTRACT_NAME}"), FROM_VERSION)?; + + // Owner package updated, re-initializing + let old_owner_state = v1_state::OWNER.load(deps.storage)?; + let old_owner = v1_state::current_owner(old_owner_state); + v1_state::OWNER.remove(deps.storage); + OWNER.initialize( + deps.storage, + deps.api, + OwnerInit::SetInitialOwner { + owner: old_owner.to_string(), + }, + )?; + + // CONFIG updated, re-initializing + let old_config_state = v1_state::CONFIG.load(deps.storage)?; + v1_state::CONFIG.remove(deps.storage); + CONFIG.save( + deps.storage, + &Config { + address_provider: old_config_state.address_provider, + max_whitelisted_denoms: updates.max_whitelisted_denoms, + }, + )?; + + // WHITELIST not existent in v1, initializing + WHITELIST.save(deps.storage, &old_config_state.mars_denom, &Uint128::one())?; + WHITELIST_COUNT.save(deps.storage, &1)?; + + // EPOCH_DURATION not existent in v1, initializing + if updates.epoch_duration < MIN_EPOCH_DURATION { + return Err(ContractError::EpochDurationTooShort { + min_epoch_duration: MIN_EPOCH_DURATION, + }); + } + EPOCH_DURATION.save(deps.storage, &updates.epoch_duration)?; + + migrate_indices_and_unclaimed_rewards(&mut deps, env, &old_config_state.mars_denom)?; + + set_contract_version(deps.storage, format!("crates.io:{CONTRACT_NAME}"), CONTRACT_VERSION)?; + + Ok(Response::new() + .add_attribute("action", "migrate") + .add_attribute("from_version", FROM_VERSION) + .add_attribute("to_version", CONTRACT_VERSION)) +} + +// Migrate indices and unclaimed rewards from v1 to v2 with helpers from v1.0.0 tag: +// https://github.com/mars-protocol/red-bank/blob/v1.0.0/contracts/incentives/src/helpers.rs +// +// This is done by querying the Red Bank contract for the collateral total supply and +// user collateral amount for each collateral denom. +fn migrate_indices_and_unclaimed_rewards( + deps: &mut DepsMut, + env: Env, + mars_denom: &str, +) -> Result<(), ContractError> { + let current_block_time = env.block.time.seconds(); + + let config = CONFIG.load(deps.storage)?; + + let red_bank_addr = mars_red_bank_types::address_provider::helpers::query_contract_addr( + deps.as_ref(), + &config.address_provider, + mars_red_bank_types::address_provider::MarsAddressType::RedBank, + )?; + + let mut asset_incentives = v1_state::ASSET_INCENTIVES + .range(deps.storage, None, None, Order::Ascending) + .collect::>>()?; + v1_state::ASSET_INCENTIVES.clear(deps.storage); + + for (denom, asset_incentive) in asset_incentives.iter_mut() { + let market: mars_red_bank_types::red_bank::Market = deps.querier.query_wasm_smart( + red_bank_addr.clone(), + &mars_red_bank_types::red_bank::QueryMsg::Market { + denom: denom.clone(), + }, + )?; + + v1_state::helpers::update_asset_incentive_index( + asset_incentive, + market.collateral_total_scaled, + current_block_time, + )?; + + // Update incentive state for collateral and incentive denom (Mars) + INCENTIVE_STATES.save( + deps.storage, + (denom, mars_denom), + &IncentiveState { + index: asset_incentive.index, + last_updated: current_block_time, + }, + )?; + } + + let user_asset_indices = v1_state::USER_ASSET_INDICES + .range(deps.storage, None, None, Order::Ascending) + .collect::>>()?; + v1_state::USER_ASSET_INDICES.clear(deps.storage); + + let mut user_unclaimed_rewards = v1_state::USER_UNCLAIMED_REWARDS + .range(deps.storage, None, None, Order::Ascending) + .collect::>>()?; + v1_state::USER_UNCLAIMED_REWARDS.clear(deps.storage); + + for ((user, denom), user_asset_index) in user_asset_indices { + let collateral: mars_red_bank_types::red_bank::UserCollateralResponse = + deps.querier.query_wasm_smart( + red_bank_addr.clone(), + &mars_red_bank_types::red_bank::QueryMsg::UserCollateral { + user: user.to_string(), + account_id: None, + denom: denom.clone(), + }, + )?; + + // Get asset incentive for a denom. It should be available but just in case we don't unwrap + let denom_idx = asset_incentives.get(&denom); + if let Some(asset_incentive) = denom_idx { + // Since we didn't track unclaimed rewards per collateral denom in v1 we add them + // to the user unclaimed rewards for the first user collateral denom. + let mut unclaimed_rewards = user_unclaimed_rewards.remove(&user).unwrap_or_default(); + + if user_asset_index != asset_incentive.index { + // Compute user accrued rewards + let asset_accrued_rewards = v1_state::helpers::compute_user_accrued_rewards( + collateral.amount_scaled, + user_asset_index, + asset_incentive.index, + )?; + + unclaimed_rewards += asset_accrued_rewards; + } + + if !unclaimed_rewards.is_zero() { + // Update user unclaimed rewards + USER_UNCLAIMED_REWARDS.save( + deps.storage, + ((&user, ""), &denom, mars_denom), + &unclaimed_rewards, + )?; + } + + // Update user asset index + USER_ASSET_INDICES.save( + deps.storage, + ((&user, ""), &denom, mars_denom), + &asset_incentive.index, + )?; + } + } + + Ok(()) +} diff --git a/contracts/incentives/tests/tests/mod.rs b/contracts/incentives/tests/tests/mod.rs index 85b241bca..e95d9f695 100644 --- a/contracts/incentives/tests/tests/mod.rs +++ b/contracts/incentives/tests/tests/mod.rs @@ -4,6 +4,7 @@ mod test_admin; mod test_balance_change; mod test_claim_rewards; mod test_indices_usage; +mod test_migration_v2; mod test_quering; mod test_set_asset_incentive; mod test_update_owner; diff --git a/contracts/incentives/tests/tests/test_migration_v2.rs b/contracts/incentives/tests/tests/test_migration_v2.rs new file mode 100644 index 000000000..463ca2529 --- /dev/null +++ b/contracts/incentives/tests/tests/test_migration_v2.rs @@ -0,0 +1,401 @@ +use std::collections::HashMap; + +use cosmwasm_std::{ + attr, testing::mock_env, Addr, Decimal, Event, Order, StdResult, Timestamp, Uint128, +}; +use cw2::VersionError; +use mars_incentives::{ + contract::migrate, + migrations::v2_0_0::v1_state::{self, OwnerSetNoneProposed}, + state::{ + CONFIG, INCENTIVE_STATES, OWNER, USER_ASSET_INDICES, USER_UNCLAIMED_REWARDS, WHITELIST, + WHITELIST_COUNT, + }, + ContractError, +}; +use mars_red_bank_types::{ + incentives::{Config, IncentiveState, MigrateMsg, V2Updates}, + red_bank::{Market, UserCollateralResponse}, +}; +use mars_testing::{mock_dependencies, MockEnvParams}; + +#[test] +fn wrong_contract_name() { + let mut deps = mock_dependencies(&[]); + cw2::set_contract_version(deps.as_mut().storage, "contract_xyz", "1.0.0").unwrap(); + + let err = migrate( + deps.as_mut(), + mock_env(), + MigrateMsg::V1_0_0ToV2_0_0(V2Updates { + epoch_duration: 604800, + max_whitelisted_denoms: 10, + }), + ) + .unwrap_err(); + + assert_eq!( + err, + ContractError::Version(VersionError::WrongContract { + expected: "crates.io:mars-incentives".to_string(), + found: "contract_xyz".to_string() + }) + ); +} + +#[test] +fn wrong_contract_version() { + let mut deps = mock_dependencies(&[]); + cw2::set_contract_version(deps.as_mut().storage, "crates.io:mars-incentives", "4.1.0").unwrap(); + + let err = migrate( + deps.as_mut(), + mock_env(), + MigrateMsg::V1_0_0ToV2_0_0(V2Updates { + epoch_duration: 604800, + max_whitelisted_denoms: 10, + }), + ) + .unwrap_err(); + + assert_eq!( + err, + ContractError::Version(VersionError::WrongVersion { + expected: "1.0.0".to_string(), + found: "4.1.0".to_string() + }) + ); +} + +#[test] +fn successful_migration() { + let mut deps = mock_dependencies(&[]); + cw2::set_contract_version(deps.as_mut().storage, "crates.io:mars-incentives", "1.0.0").unwrap(); + + let old_owner = "spiderman_246"; + v1_state::OWNER + .save( + deps.as_mut().storage, + &v1_state::OwnerState::B(OwnerSetNoneProposed { + owner: Addr::unchecked(old_owner), + }), + ) + .unwrap(); + + let mars_denom = "umars"; + let old_config = mars_red_bank_types_old::incentives::Config { + address_provider: Addr::unchecked("address_provider"), + mars_denom: mars_denom.to_string(), + }; + v1_state::CONFIG.save(deps.as_mut().storage, &old_config).unwrap(); + + let atom_denom = "uatom"; + let usdc_denom = "uusdc"; + let osmo_denom = "uosmo"; + + let incentive_start_time = 500_000u64; + let duration = 864_000u64; // 10 days + let migration_time = incentive_start_time + duration + 100u64; + + // The incentive will have to be recalculated for the entire duration + let atom_incentive = mars_red_bank_types_old::incentives::AssetIncentive { + emission_per_second: Uint128::new(100), + start_time: incentive_start_time, + duration, + index: Decimal::one(), + last_updated: incentive_start_time, + }; + v1_state::ASSET_INCENTIVES.save(deps.as_mut().storage, atom_denom, &atom_incentive).unwrap(); + + // The incentive will have to be recalculated for the part of the duration + let usdc_incentive = mars_red_bank_types_old::incentives::AssetIncentive { + emission_per_second: Uint128::new(50), + start_time: incentive_start_time, + duration, + index: Decimal::from_ratio(12u128, 10u128), + last_updated: incentive_start_time + 86400u64, // + 1 day + }; + v1_state::ASSET_INCENTIVES.save(deps.as_mut().storage, usdc_denom, &usdc_incentive).unwrap(); + + // The incentive won't be recalculated because it finished before migration time + let osmo_incentive = mars_red_bank_types_old::incentives::AssetIncentive { + emission_per_second: Uint128::new(50), + start_time: incentive_start_time, + duration, + index: Decimal::from_ratio(15u128, 10u128), + last_updated: migration_time - 10u64, + }; + v1_state::ASSET_INCENTIVES.save(deps.as_mut().storage, osmo_denom, &osmo_incentive).unwrap(); + + // Set user asset indices for all incentive assets + let user_1 = Addr::unchecked("user_1"); + let user_1_atom_idx_old = Decimal::one(); + v1_state::USER_ASSET_INDICES + .save(deps.as_mut().storage, (&user_1, atom_denom), &user_1_atom_idx_old) + .unwrap(); + let user_1_usdc_idx_old = Decimal::one(); + v1_state::USER_ASSET_INDICES + .save(deps.as_mut().storage, (&user_1, usdc_denom), &user_1_usdc_idx_old) + .unwrap(); + let user_1_osmo_idx_old = Decimal::one(); + v1_state::USER_ASSET_INDICES + .save(deps.as_mut().storage, (&user_1, osmo_denom), &user_1_osmo_idx_old) + .unwrap(); + + // Set user asset indices only for osmo. Index is up to date with asset incentive index. No rewards accured. + let user_2 = Addr::unchecked("user_2"); + let user_2_osmo_idx_old = osmo_incentive.index; + v1_state::USER_ASSET_INDICES + .save(deps.as_mut().storage, (&user_2, osmo_denom), &user_2_osmo_idx_old) + .unwrap(); + + // Set user asset indices only for atom + let user_3 = Addr::unchecked("user_3"); + let user_3_atom_idx_old = Decimal::one(); + v1_state::USER_ASSET_INDICES + .save(deps.as_mut().storage, (&user_3, atom_denom), &user_3_atom_idx_old) + .unwrap(); + + // Set unclaimed rewards only for user_1. + // user_2 doesn't accrue any new rewards because osmo incentive finished before migration time. + // user_3 not set in order to check if new state creation works for him. + let user_1_unclaimed_rewards = Uint128::new(1000); + v1_state::USER_UNCLAIMED_REWARDS + .save(deps.as_mut().storage, &user_1, &user_1_unclaimed_rewards) + .unwrap(); + + // Setup markets + let atom_collateral_total_scaled = Uint128::new(100_000_000); + deps.querier.set_redbank_market(create_market(atom_denom, atom_collateral_total_scaled)); + let usdc_collateral_total_scaled = Uint128::new(1_250_000_000); + deps.querier.set_redbank_market(create_market(usdc_denom, usdc_collateral_total_scaled)); + let osmo_collateral_total_scaled = Uint128::new(520_000_000); + deps.querier.set_redbank_market(create_market(osmo_denom, osmo_collateral_total_scaled)); + + // Setup atom collaterals. Sum of all positions should be equal to atom_collateral_total_scaled. + let user_1_atom_amount_scaled = Uint128::zero(); // Setting zero to check if user_1 index is updated correctly + deps.querier.set_red_bank_user_collateral( + &user_1, + create_user_collateral(atom_denom, user_1_atom_amount_scaled), + ); + let user_3_atom_amount_scaled = atom_collateral_total_scaled; + deps.querier.set_red_bank_user_collateral( + &user_3, + create_user_collateral(atom_denom, user_3_atom_amount_scaled), + ); + + // Setup usdc collaterals. Sum of all positions should be equal to usdc_collateral_total_scaled + let user_1_usdc_amount_scaled = usdc_collateral_total_scaled; + deps.querier.set_red_bank_user_collateral( + &user_1, + create_user_collateral(usdc_denom, user_1_usdc_amount_scaled), + ); + + // Setup osmo collaterals. Sum of all positions should be equal to osmo_collateral_total_scaled + let user_1_osmo_amount_scaled = Uint128::new(120_000_000); + deps.querier.set_red_bank_user_collateral( + &user_1, + create_user_collateral(osmo_denom, user_1_osmo_amount_scaled), + ); + let user_2_osmo_amount_scaled = Uint128::new(400_000_000); + deps.querier.set_red_bank_user_collateral( + &user_2, + create_user_collateral(osmo_denom, user_2_osmo_amount_scaled), + ); + + let env = mars_testing::mock_env(MockEnvParams { + block_time: Timestamp::from_seconds(migration_time), + ..Default::default() + }); + + let epoch_duration = 604800; + let max_whitelisted_denoms = 12; + let res = migrate( + deps.as_mut(), + env, + MigrateMsg::V1_0_0ToV2_0_0(V2Updates { + epoch_duration, + max_whitelisted_denoms, + }), + ) + .unwrap(); + + assert_eq!(res.messages, vec![]); + assert_eq!(res.events, vec![] as Vec); + assert!(res.data.is_none()); + assert_eq!( + res.attributes, + vec![attr("action", "migrate"), attr("from_version", "1.0.0"), attr("to_version", "2.0.0")] + ); + + let o = OWNER.query(deps.as_ref().storage).unwrap(); + assert_eq!(old_owner.to_string(), o.owner.unwrap()); + assert!(o.proposed.is_none()); + assert!(o.initialized); + assert!(!o.abolished); + assert!(o.emergency_owner.is_none()); + + let new_config = CONFIG.load(deps.as_ref().storage).unwrap(); + assert_eq!( + new_config, + Config { + address_provider: old_config.address_provider, + max_whitelisted_denoms + } + ); + + let whitelist_count = WHITELIST_COUNT.load(deps.as_ref().storage).unwrap(); + assert_eq!(whitelist_count, 1); + let whitelist = WHITELIST + .range(deps.as_ref().storage, None, None, Order::Ascending) + .collect::>>() + .unwrap(); + assert_eq!(whitelist.len(), 1); + assert_eq!(whitelist.get("umars").unwrap(), &Uint128::one()); + + // Update asset incentive indices and check if indices changed + let mut new_atom_incentive = atom_incentive.clone(); + v1_state::helpers::update_asset_incentive_index( + &mut new_atom_incentive, + atom_collateral_total_scaled, + migration_time, + ) + .unwrap(); + assert_ne!(atom_incentive.index, new_atom_incentive.index); + let mut new_usdc_incentive = usdc_incentive.clone(); + v1_state::helpers::update_asset_incentive_index( + &mut new_usdc_incentive, + usdc_collateral_total_scaled, + migration_time, + ) + .unwrap(); + assert_ne!(usdc_incentive.index, new_usdc_incentive.index); + let mut new_osmo_incentive = osmo_incentive.clone(); + v1_state::helpers::update_asset_incentive_index( + &mut new_osmo_incentive, + osmo_collateral_total_scaled, + migration_time, + ) + .unwrap(); + assert_eq!(osmo_incentive.index, new_osmo_incentive.index); // should be equal because last_updated is after incentive end time + + // Check if incentive states are updated correctly + let incentive_states = INCENTIVE_STATES + .range(deps.as_ref().storage, None, None, Order::Ascending) + .collect::>>() + .unwrap(); + assert_eq!(incentive_states.len(), 3); + assert_eq!( + incentive_states.get(&(atom_denom.to_string(), mars_denom.to_string())).unwrap(), + &IncentiveState { + index: new_atom_incentive.index, + last_updated: migration_time + } + ); + assert_eq!( + incentive_states.get(&(usdc_denom.to_string(), mars_denom.to_string())).unwrap(), + &IncentiveState { + index: new_usdc_incentive.index, + last_updated: migration_time + } + ); + assert_eq!( + incentive_states.get(&(osmo_denom.to_string(), mars_denom.to_string())).unwrap(), + &IncentiveState { + index: new_osmo_incentive.index, + last_updated: migration_time + } + ); + + // Check if user asset indices are updated correctly + let user_1_atom_idx = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_1, ""), atom_denom, mars_denom)) + .unwrap(); + assert_eq!(user_1_atom_idx, new_atom_incentive.index); + let user_1_usdc_idx = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_1, ""), usdc_denom, mars_denom)) + .unwrap(); + assert_eq!(user_1_usdc_idx, new_usdc_incentive.index); + let user_1_osmo_idx = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_1, ""), osmo_denom, mars_denom)) + .unwrap(); + assert_eq!(user_1_osmo_idx, new_osmo_incentive.index); + + let user_2_osmo_idx = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_2, ""), osmo_denom, mars_denom)) + .unwrap(); + assert_eq!(user_2_osmo_idx, new_osmo_incentive.index); + + let user_3_atom_idx = USER_ASSET_INDICES + .load(deps.as_ref().storage, ((&user_3, ""), atom_denom, mars_denom)) + .unwrap(); + assert_eq!(user_3_atom_idx, new_atom_incentive.index); + + // Check if user unclaimed rewards are migrated correctly + let user_1_atom_rewards = v1_state::helpers::compute_user_accrued_rewards( + user_1_atom_amount_scaled, + user_1_atom_idx_old, + new_atom_incentive.index, + ) + .unwrap(); + let user_1_atom_rewards_migrated = USER_UNCLAIMED_REWARDS + .load(deps.as_ref().storage, ((&user_1, ""), atom_denom, mars_denom)) + .unwrap(); + assert_eq!(user_1_atom_rewards_migrated, user_1_unclaimed_rewards + user_1_atom_rewards); + let user_1_usdc_rewards = v1_state::helpers::compute_user_accrued_rewards( + user_1_usdc_amount_scaled, + user_1_usdc_idx_old, + new_usdc_incentive.index, + ) + .unwrap(); + let user_1_usdc_rewards_migrated = USER_UNCLAIMED_REWARDS + .load(deps.as_ref().storage, ((&user_1, ""), usdc_denom, mars_denom)) + .unwrap(); + assert_eq!(user_1_usdc_rewards_migrated, user_1_usdc_rewards); + let user_1_osmo_rewards = v1_state::helpers::compute_user_accrued_rewards( + user_1_osmo_amount_scaled, + user_1_osmo_idx_old, + new_osmo_incentive.index, + ) + .unwrap(); + let user_1_osmo_rewards_migrated = USER_UNCLAIMED_REWARDS + .load(deps.as_ref().storage, ((&user_1, ""), osmo_denom, mars_denom)) + .unwrap(); + assert_eq!(user_1_osmo_rewards_migrated, user_1_osmo_rewards); + + let user_2_osmo_rewards = v1_state::helpers::compute_user_accrued_rewards( + user_2_osmo_amount_scaled, + user_2_osmo_idx_old, + new_osmo_incentive.index, + ) + .unwrap(); + assert_eq!(user_2_osmo_rewards, Uint128::zero()); + + let user_3_atom_rewards = v1_state::helpers::compute_user_accrued_rewards( + user_3_atom_amount_scaled, + user_3_atom_idx_old, + new_atom_incentive.index, + ) + .unwrap(); + let user_3_atom_rewards_migrated = USER_UNCLAIMED_REWARDS + .load(deps.as_ref().storage, ((&user_3, ""), atom_denom, mars_denom)) + .unwrap(); + assert_eq!(user_3_atom_rewards_migrated, user_3_atom_rewards); +} + +fn create_market(denom: &str, scaled_amt: Uint128) -> Market { + Market { + denom: denom.to_string(), + collateral_total_scaled: scaled_amt, + ..Default::default() + } +} + +fn create_user_collateral(denom: &str, scaled_amt: Uint128) -> UserCollateralResponse { + UserCollateralResponse { + denom: denom.to_string(), + amount_scaled: scaled_amt, + amount: Uint128::zero(), // doesn't matter for this test + enabled: true, + } +} diff --git a/contracts/oracle/osmosis/src/migrations.rs b/contracts/oracle/osmosis/src/migrations.rs index 8ffa77965..a74db63d1 100644 --- a/contracts/oracle/osmosis/src/migrations.rs +++ b/contracts/oracle/osmosis/src/migrations.rs @@ -114,7 +114,7 @@ pub mod v1_0_1 { vec![ attr("action", "migrate"), attr("from_version", "1.0.1"), - attr("to_version", "1.2.0") + attr("to_version", "2.0.0") ] ); @@ -149,7 +149,7 @@ pub mod v1_0_1 { vec![ attr("action", "migrate"), attr("from_version", "1.0.1"), - attr("to_version", "1.2.0") + attr("to_version", "2.0.0") ] ); diff --git a/packages/types/src/incentives.rs b/packages/types/src/incentives.rs index c94cad794..db37b8375 100644 --- a/packages/types/src/incentives.rs +++ b/packages/types/src/incentives.rs @@ -252,7 +252,19 @@ pub enum QueryMsg { } #[cw_serde] -pub struct MigrateMsg {} +pub struct V2Updates { + /// The amount of time in seconds for each incentive epoch. This is the minimum amount of time + /// that an incentive can last, and each incentive must be a multiple of this duration. + pub epoch_duration: u64, + /// The maximum number of incentive denoms that can be whitelisted at any given time. This is + /// a guard against accidentally whitelisting too many denoms, which could cause max gas errors. + pub max_whitelisted_denoms: u8, +} + +#[cw_serde] +pub enum MigrateMsg { + V1_0_0ToV2_0_0(V2Updates), +} #[cw_serde] pub struct EmissionResponse { diff --git a/schemas/mars-address-provider/mars-address-provider.json b/schemas/mars-address-provider/mars-address-provider.json index 34ce77c5b..a8ce16fdd 100644 --- a/schemas/mars-address-provider/mars-address-provider.json +++ b/schemas/mars-address-provider/mars-address-provider.json @@ -1,6 +1,6 @@ { "contract_name": "mars-address-provider", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-incentives/mars-incentives.json b/schemas/mars-incentives/mars-incentives.json index ae8071490..d2d6badd1 100644 --- a/schemas/mars-incentives/mars-incentives.json +++ b/schemas/mars-incentives/mars-incentives.json @@ -1,6 +1,6 @@ { "contract_name": "mars-incentives", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json b/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json index 1a1ef011e..a6324eacb 100644 --- a/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json +++ b/schemas/mars-oracle-osmosis/mars-oracle-osmosis.json @@ -1,6 +1,6 @@ { "contract_name": "mars-oracle-osmosis", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-oracle-wasm/mars-oracle-wasm.json b/schemas/mars-oracle-wasm/mars-oracle-wasm.json index 19d235a88..c5e3474dd 100644 --- a/schemas/mars-oracle-wasm/mars-oracle-wasm.json +++ b/schemas/mars-oracle-wasm/mars-oracle-wasm.json @@ -1,6 +1,6 @@ { "contract_name": "mars-oracle-wasm", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-params/mars-params.json b/schemas/mars-params/mars-params.json index dbf0f6338..2350dddbc 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.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-red-bank/mars-red-bank.json b/schemas/mars-red-bank/mars-red-bank.json index 6bafea4e5..d38a7dd5f 100644 --- a/schemas/mars-red-bank/mars-red-bank.json +++ b/schemas/mars-red-bank/mars-red-bank.json @@ -1,6 +1,6 @@ { "contract_name": "mars-red-bank", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json b/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json index 30e68fddc..c9f4e9ce1 100644 --- a/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json +++ b/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json @@ -1,6 +1,6 @@ { "contract_name": "mars-rewards-collector-base", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-swapper-astroport/mars-swapper-astroport.json b/schemas/mars-swapper-astroport/mars-swapper-astroport.json index 93ef8f7c2..778ee5515 100644 --- a/schemas/mars-swapper-astroport/mars-swapper-astroport.json +++ b/schemas/mars-swapper-astroport/mars-swapper-astroport.json @@ -1,6 +1,6 @@ { "contract_name": "mars-swapper-astroport", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-swapper-osmosis/mars-swapper-osmosis.json b/schemas/mars-swapper-osmosis/mars-swapper-osmosis.json index 512d773a2..6d4b6a009 100644 --- a/schemas/mars-swapper-osmosis/mars-swapper-osmosis.json +++ b/schemas/mars-swapper-osmosis/mars-swapper-osmosis.json @@ -1,6 +1,6 @@ { "contract_name": "mars-swapper-osmosis", - "contract_version": "1.2.0", + "contract_version": "2.0.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", From dd41e6cbfc321132d49fc188be30684cdc82cbb8 Mon Sep 17 00:00:00 2001 From: piobab Date: Fri, 8 Sep 2023 10:04:10 +0200 Subject: [PATCH 37/43] Update changelog. (#320) --- CHANGELOG.md | 9 + files/types_diff_v1_0_0__mars_v2.txt | 1514 ++++++++++++++++++++++++++ 2 files changed, 1523 insertions(+) create mode 100644 files/types_diff_v1_0_0__mars_v2.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 5504e2fb4..62424d67d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. +## v1.2.0 + +- Allow Credit account to lend/reclaim to the Red Bank (calls Deposit/Withdraw in Red Bank), claim incentive rewards from lending to the Red Bank (pass account_id to track Credit Manager users in `red-bank` and `incentives` contract). +- Pyth oracle price sourcing support for EMA, confidence. Different price query for liquidation and other actions (Deposit / Borrow / Repay etc.). +- New liquidation mechanism on Red Bank and Credit Manager to allow variable liquidation bonus and close factors (similar to Euler model). +- Previously Red Bank and Credit Manager had separate deposit caps to protect against holding to many assets in relation to chain liquidity - consolidated these deposit caps into one `params` contract. +- Common `swapper` contract for Red Bank and Credit Manager. +- Credit Manager and Red Bank use `params` contract for common asset params (see new [types](/contracts/params/src/types)). Previous `Market` / `Markets` queries has changed - part of the params are moved to `params` contract (see [diff](./files/types_diff_v1_0_0__mars_v2.txt)). + ## v1.0.0-rc0 This section documents the API changes compared to the Terra Classic deployment, found in the [`mars-core`](https://github.com/mars-protocol/mars-core) repository. This section is **not comprehensive**, as the changes are numerous. Changelog for later version start here should be made comprehensive. diff --git a/files/types_diff_v1_0_0__mars_v2.txt b/files/types_diff_v1_0_0__mars_v2.txt new file mode 100644 index 000000000..ab5575313 --- /dev/null +++ b/files/types_diff_v1_0_0__mars_v2.txt @@ -0,0 +1,1514 @@ +diff --git a/packages/types/Cargo.toml b/packages/types/Cargo.toml +index 7d900594..d4475675 100644 +--- a/packages/types/Cargo.toml ++++ b/packages/types/Cargo.toml +@@ -1,5 +1,6 @@ + [package] + name = "mars-red-bank-types" ++description = "Messages and types for Red Bank smart contracts" + version = { workspace = true } + authors = { workspace = true } + edition = { workspace = true } +@@ -22,7 +23,5 @@ cosmwasm-schema = { workspace = true } + cosmwasm-std = { workspace = true } + mars-owner = { workspace = true } + mars-utils = { workspace = true } ++strum = { workspace = true, features = ["derive"] } + thiserror = { workspace = true } +- +-[dev-dependencies] +-mars-testing = { workspace = true } +diff --git a/packages/types/src/address_provider.rs b/packages/types/src/address_provider.rs +index cab8fbfd..c212ec89 100644 +--- a/packages/types/src/address_provider.rs ++++ b/packages/types/src/address_provider.rs +@@ -3,14 +3,17 @@ use std::{any::type_name, fmt, str::FromStr}; + use cosmwasm_schema::{cw_serde, QueryResponses}; + use cosmwasm_std::StdError; + use mars_owner::OwnerUpdate; ++use strum::EnumIter; + + #[cw_serde] +-#[derive(Copy, Eq, Hash)] ++#[derive(Copy, Eq, Hash, EnumIter)] + pub enum MarsAddressType { + Incentives, + Oracle, + RedBank, + RewardsCollector, ++ Params, ++ CreditManager, + /// Protocol admin is an ICS-27 interchain account controlled by Mars Hub's x/gov module. + /// This account will take the owner and admin roles of red-bank contracts. + /// +@@ -30,18 +33,23 @@ pub enum MarsAddressType { + /// NOTE: This is a Mars Hub address with the `mars` bech32 prefix, which may not be recognized + /// by the `api.addr_validate` method. + SafetyFund, ++ /// The swapper contract on the chain ++ Swapper, + } + + impl fmt::Display for MarsAddressType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { ++ MarsAddressType::CreditManager => "credit_manager", + MarsAddressType::FeeCollector => "fee_collector", + MarsAddressType::Incentives => "incentives", + MarsAddressType::Oracle => "oracle", ++ MarsAddressType::Params => "params", + MarsAddressType::ProtocolAdmin => "protocol_admin", + MarsAddressType::RedBank => "red_bank", + MarsAddressType::RewardsCollector => "rewards_collector", + MarsAddressType::SafetyFund => "safety_fund", ++ MarsAddressType::Swapper => "swapper", + }; + write!(f, "{s}") + } +@@ -52,13 +60,16 @@ impl FromStr for MarsAddressType { + + fn from_str(s: &str) -> Result { + match s { ++ "credit_manager" => Ok(MarsAddressType::CreditManager), + "fee_collector" => Ok(MarsAddressType::FeeCollector), + "incentives" => Ok(MarsAddressType::Incentives), + "oracle" => Ok(MarsAddressType::Oracle), ++ "params" => Ok(MarsAddressType::Params), + "protocol_admin" => Ok(MarsAddressType::ProtocolAdmin), + "red_bank" => Ok(MarsAddressType::RedBank), + "rewards_collector" => Ok(MarsAddressType::RewardsCollector), + "safety_fund" => Ok(MarsAddressType::SafetyFund), ++ "swapper" => Ok(MarsAddressType::Swapper), + _ => Err(StdError::parse_err(type_name::(), s)), + } + } +@@ -194,3 +205,25 @@ pub mod helpers { + .map(|res| res.address) + } + } ++ ++#[cfg(test)] ++mod tests { ++ use std::str::FromStr; ++ ++ use strum::IntoEnumIterator; ++ ++ use super::MarsAddressType; ++ ++ #[test] ++ fn mars_address_type_fmt_and_from_string() { ++ for address_type in MarsAddressType::iter() { ++ assert_eq!(MarsAddressType::from_str(&address_type.to_string()).unwrap(), address_type); ++ } ++ } ++ ++ #[test] ++ #[should_panic] ++ fn mars_address_type_from_str_invalid_string() { ++ MarsAddressType::from_str("invalid_address_type").unwrap(); ++ } ++} +diff --git a/packages/types/src/error.rs b/packages/types/src/error.rs +index 0a2c667d..e9cb6311 100644 +--- a/packages/types/src/error.rs ++++ b/packages/types/src/error.rs +@@ -1,4 +1,6 @@ +-use cosmwasm_std::StdError; ++use cosmwasm_std::{ ++ CheckedFromRatioError, CheckedMultiplyFractionError, DivideByZeroError, OverflowError, StdError, ++}; + use thiserror::Error; + + #[derive(Error, Debug, PartialEq)] +@@ -22,6 +24,18 @@ pub enum MarsError { + Deserialize { + target_type: String, + }, ++ ++ #[error("{0}")] ++ Overflow(#[from] OverflowError), ++ ++ #[error("{0}")] ++ DivideByZero(#[from] DivideByZeroError), ++ ++ #[error("{0}")] ++ CheckedFromRatio(#[from] CheckedFromRatioError), ++ ++ #[error("{0}")] ++ CheckedMultiplyFraction(#[from] CheckedMultiplyFractionError), + } + + impl From for StdError { +diff --git a/packages/types/src/incentives.rs b/packages/types/src/incentives.rs +index 1ec39a76..c94cad79 100644 +--- a/packages/types/src/incentives.rs ++++ b/packages/types/src/incentives.rs +@@ -1,5 +1,5 @@ + use cosmwasm_schema::{cw_serde, QueryResponses}; +-use cosmwasm_std::{Addr, Decimal, Uint128}; ++use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; + use mars_owner::OwnerUpdate; + + /// Global configuration +@@ -7,20 +7,15 @@ use mars_owner::OwnerUpdate; + pub struct Config { + /// Address provider + pub address_provider: Addr, +- /// Mars Token Denom +- pub mars_denom: String, ++ /// The maximum number of incentive denoms that can be whitelisted at any given time. This is ++ /// a guard against accidentally whitelisting too many denoms, which could cause max gas errors. ++ pub max_whitelisted_denoms: u8, + } + + /// Incentive Metadata for a given incentive + #[cw_serde] +-pub struct AssetIncentive { +- /// How much MARS per second is emitted to be then distributed to all Red Bank depositors +- pub emission_per_second: Uint128, +- /// Start time of the incentive (in seconds) since the UNIX epoch (00:00:00 on 1970-01-01 UTC) +- pub start_time: u64, +- /// How many seconds the incentives last +- pub duration: u64, +- /// Total MARS assigned for distribution since the start of the incentive ++pub struct IncentiveState { ++ /// An index that represents how many incentive tokens have been distributed per unit of collateral + pub index: Decimal, + /// Last time (in seconds) index was updated + pub last_updated: u64, +@@ -28,30 +23,54 @@ pub struct AssetIncentive { + + /// Incentive Metadata for a given incentive denom + #[cw_serde] +-pub struct AssetIncentiveResponse { +- /// Asset denom +- pub denom: String, +- /// How much MARS per second is emitted to be then distributed to all Red Bank depositors +- pub emission_per_second: Uint128, +- /// Start time of the incentive (in seconds) since the UNIX epoch (00:00:00 on 1970-01-01 UTC) +- pub start_time: u64, +- /// How many seconds the incentives last +- pub duration: u64, +- /// Total MARS assigned for distribution since the start of the incentive ++pub struct IncentiveStateResponse { ++ /// The denom for which users get the incentive if they provide collateral in the Red Bank ++ pub collateral_denom: String, ++ /// The denom of the token these incentives are paid with ++ pub incentive_denom: String, ++ /// An index that represents how many incentive tokens have been distributed per unit of collateral + pub index: Decimal, + /// Last time (in seconds) index was updated + pub last_updated: u64, + } + +-impl AssetIncentiveResponse { +- pub fn from(denom: String, ai: AssetIncentive) -> Self { ++impl IncentiveStateResponse { ++ pub fn from( ++ collateral_denom: impl Into, ++ incentive_denom: impl Into, ++ is: IncentiveState, ++ ) -> Self { ++ Self { ++ collateral_denom: collateral_denom.into(), ++ incentive_denom: incentive_denom.into(), ++ index: is.index, ++ last_updated: is.last_updated, ++ } ++ } ++} ++ ++#[cw_serde] ++pub struct WhitelistEntry { ++ /// The incentive token denom that is whitelisted ++ pub denom: String, ++ /// The minimum emission rate per second for this incentive token ++ pub min_emission_rate: Uint128, ++} ++ ++impl From<&(&str, u128)> for WhitelistEntry { ++ fn from((denom, min_emission_rate): &(&str, u128)) -> Self { ++ Self { ++ denom: denom.to_string(), ++ min_emission_rate: Uint128::from(*min_emission_rate), ++ } ++ } ++} ++ ++impl From<(String, Uint128)> for WhitelistEntry { ++ fn from((denom, min_emission_rate): (String, Uint128)) -> Self { + Self { + denom, +- emission_per_second: ai.emission_per_second, +- start_time: ai.start_time, +- duration: ai.duration, +- index: ai.index, +- last_updated: ai.last_updated, ++ min_emission_rate, + } + } + } +@@ -62,26 +81,41 @@ pub struct InstantiateMsg { + pub owner: String, + /// Address provider + pub address_provider: String, +- /// Mars token denom +- pub mars_denom: String, ++ /// The amount of time in seconds for each incentive epoch. This is the minimum amount of time ++ /// that an incentive can last, and each incentive must be a multiple of this duration. ++ pub epoch_duration: u64, ++ /// The maximum number of incentive denoms that can be whitelisted at any given time. This is ++ /// a guard against accidentally whitelisting too many denoms, which could cause max gas errors. ++ pub max_whitelisted_denoms: u8, + } + + #[cw_serde] + pub enum ExecuteMsg { +- /// Set incentive params for an asset to its depositor at Red Bank. +- /// +- /// If there is no incentive for the asset, all params are required. +- /// New incentive can be set (rescheduled) if current one has finished (current_block_time > start_time + duration). ++ /// Add or remove incentive denoms from the whitelist. Only admin can do this. ++ UpdateWhitelist { ++ /// The denoms to add to the whitelist as well as a minimum emission rate per second for ++ /// each. If the denom is already in the whitelist, the minimum emission rate will be updated. ++ add_denoms: Vec, ++ /// The denoms to remove from the whitelist. This will update the index of the incentive ++ /// state and then remove any active incentive schedules. ++ /// ++ /// NB: If any incentive schedules are still active for this incentive denom, the incentive ++ /// tokens will be trapped forever in the contract. ++ remove_denoms: Vec, ++ }, ++ /// Add incentives for a given collateral denom and incentive denom pair + SetAssetIncentive { +- /// Asset denom associated with the incentives +- denom: String, +- /// How many MARS will be assigned per second to be distributed among all Red Bank +- /// depositors +- emission_per_second: Option, ++ /// The denom of the collatearal token to receive incentives ++ collateral_denom: String, ++ /// The denom of the token to give incentives with ++ incentive_denom: String, ++ /// How many `incentive_denom` tokens will be assigned per second to be distributed among ++ /// all Red Bank depositors ++ emission_per_second: Uint128, + /// Start time of the incentive (in seconds) since the UNIX epoch (00:00:00 on 1970-01-01 UTC). +- start_time: Option, ++ start_time: u64, + /// How many seconds the incentives last +- duration: Option, ++ duration: u64, + }, + + /// Handle balance change updating user and asset rewards. +@@ -91,6 +125,8 @@ pub enum ExecuteMsg { + /// User address. Address is trusted as it must be validated by the Red Bank + /// contract before calling this method + user_addr: Addr, ++ /// Credit account id (Rover) ++ account_id: Option, + /// Denom of the asset of which deposited balance is changed + denom: String, + /// The user's scaled collateral amount up to the instant before the change +@@ -101,12 +137,26 @@ pub enum ExecuteMsg { + + /// Claim rewards. MARS rewards accrued by the user will be staked into xMARS before + /// being sent. +- ClaimRewards {}, ++ ClaimRewards { ++ /// Credit account id (Rover) ++ account_id: Option, ++ /// Start pagination after this collateral denom ++ start_after_collateral_denom: Option, ++ /// Start pagination after this incentive denom. If supplied you must also supply ++ /// start_after_collateral_denom. ++ start_after_incentive_denom: Option, ++ /// The maximum number of results to return. If not set, 5 is used. If larger than 10, ++ /// 10 is used. ++ limit: Option, ++ }, + + /// Update contract config (only callable by owner) + UpdateConfig { ++ /// The address provider contract address + address_provider: Option, +- mars_denom: Option, ++ /// The maximum number of incentive denoms that can be whitelisted at any given time. This is ++ /// a guard against accidentally whitelisting too many denoms, which could cause max gas errors. ++ max_whitelisted_denoms: Option, + }, + + /// Manages admin role state +@@ -116,28 +166,128 @@ pub enum ExecuteMsg { + #[cw_serde] + #[derive(QueryResponses)] + pub enum QueryMsg { ++ /// Query all active incentive emissions for a collateral denom ++ #[returns(Vec)] ++ ActiveEmissions { ++ /// The denom of the token that users supply as collateral to receive incentives ++ collateral_denom: String, ++ }, ++ + /// Query contract config + #[returns(ConfigResponse)] + Config {}, + +- /// Query info about asset incentive for a given denom +- #[returns(AssetIncentiveResponse)] +- AssetIncentive { +- denom: String, ++ /// Query info about the state of an incentive for a given collateral and incentive denom pair ++ #[returns(IncentiveStateResponse)] ++ IncentiveState { ++ /// The denom of the token that users supply as collateral to receive incentives ++ collateral_denom: String, ++ /// The denom of the token which is used to give incentives with ++ incentive_denom: String, + }, + +- /// Enumerate asset incentives with pagination +- #[returns(Vec)] +- AssetIncentives { +- start_after: Option, ++ /// Enumerate incentive states with pagination ++ #[returns(Vec)] ++ IncentiveStates { ++ /// Start pagination after this collateral denom ++ start_after_collateral_denom: Option, ++ /// Start pagination after this incentive denom. If supplied you must also supply ++ /// start_after_collateral_denom. ++ start_after_incentive_denom: Option, ++ /// The maximum number of results to return. If not set, 5 is used. If larger than 10, ++ /// 10 is used. + limit: Option, + }, + +- /// Query user current unclaimed rewards ++ /// Queries the planned emission rate for a given collateral and incentive denom tuple at the ++ /// specified unix timestamp. The emission rate returned is the amount of incentive tokens ++ /// that will be emitted per second for each unit of collateral supplied during the epoch. ++ /// NB: that the returned value can change if someone adds incentives to the contract. + #[returns(Uint128)] ++ Emission { ++ /// The denom of the token that users supply as collateral to receive incentives ++ collateral_denom: String, ++ /// The denom of the token which is used to give incentives with ++ incentive_denom: String, ++ /// The unix timestamp in second to query the emission rate at. ++ timestamp: u64, ++ }, ++ ++ /// Enumerate all incentive emission rates with pagination for a specified collateral and ++ /// indentive denom pair ++ #[returns(Vec)] ++ Emissions { ++ /// The denom of the token that users supply as collateral to receive incentives ++ collateral_denom: String, ++ /// The denom of the token which is used to give incentives with ++ incentive_denom: String, ++ /// Start pagination after this timestamp ++ start_after_timestamp: Option, ++ /// The maximum number of results to return. If not set, 5 is used. If larger than 10, ++ /// 10 is used. ++ limit: Option, ++ }, ++ ++ /// Query user current unclaimed rewards ++ #[returns(Vec)] + UserUnclaimedRewards { ++ /// The user address for which to query unclaimed rewards + user: String, ++ /// Credit account id (Rover) ++ account_id: Option, ++ /// Start pagination after this collateral denom ++ start_after_collateral_denom: Option, ++ /// Start pagination after this incentive denom. If supplied you must also supply ++ /// start_after_collateral_denom. ++ start_after_incentive_denom: Option, ++ /// The maximum number of results to return. If not set, 5 is used. If larger than 10, ++ /// 10 is used. ++ limit: Option, + }, ++ ++ /// Queries the incentive denom whitelist. Returns a Vec<(String, Uint128)> containing the ++ /// denoms of all whitelisted incentive denoms, as well as the minimum emission rate for each. ++ #[returns(Vec)] ++ Whitelist {}, ++} ++ ++#[cw_serde] ++pub struct MigrateMsg {} ++ ++#[cw_serde] ++pub struct EmissionResponse { ++ /// The unix timestamp in seconds at which the emission epoch starts ++ pub epoch_start: u64, ++ /// The emission rate returned is the amount of incentive tokens that will be emitted per ++ /// second for each unit of collateral supplied during the epoch. ++ pub emission_rate: Uint128, ++} ++ ++impl From<(u64, Uint128)> for EmissionResponse { ++ fn from((epoch_start, emission_rate): (u64, Uint128)) -> Self { ++ Self { ++ epoch_start, ++ emission_rate, ++ } ++ } ++} ++ ++#[cw_serde] ++/// The currently active emission for a given incentive denom ++pub struct ActiveEmission { ++ /// The denom for which incentives are being distributed ++ pub denom: String, ++ /// The amount of incentive tokens that are being emitted per second ++ pub emission_rate: Uint128, ++} ++ ++impl From<(String, Uint128)> for ActiveEmission { ++ fn from((denom, emission_rate): (String, Uint128)) -> Self { ++ Self { ++ denom, ++ emission_rate, ++ } ++ } + } + + #[cw_serde] +@@ -148,6 +298,11 @@ pub struct ConfigResponse { + pub proposed_new_owner: Option, + /// Address provider + pub address_provider: Addr, +- /// Mars Token Denom +- pub mars_denom: String, ++ /// The maximum number of incentive denoms that can be whitelisted at any given time. This is ++ /// a guard against accidentally whitelisting too many denoms, which could cause max gas errors. ++ pub max_whitelisted_denoms: u8, ++ /// The epoch duration in seconds ++ pub epoch_duration: u64, ++ /// The count of the number of whitelisted incentive denoms ++ pub whitelist_count: u8, + } +diff --git a/packages/types/src/lib.rs b/packages/types/src/lib.rs +index 7ec7c2e0..5750ce35 100644 +--- a/packages/types/src/lib.rs ++++ b/packages/types/src/lib.rs +@@ -4,3 +4,17 @@ pub mod incentives; + pub mod oracle; + pub mod red_bank; + pub mod rewards_collector; ++pub mod swapper; ++ ++use cosmwasm_schema::cw_serde; ++ ++#[cw_serde] ++pub struct PaginationResponse { ++ pub data: Vec, ++ pub metadata: Metadata, ++} ++ ++#[cw_serde] ++pub struct Metadata { ++ pub has_more: bool, ++} +diff --git a/packages/types/src/oracle/mod.rs b/packages/types/src/oracle/mod.rs +new file mode 100644 +index 00000000..65238184 +--- /dev/null ++++ b/packages/types/src/oracle/mod.rs +@@ -0,0 +1,5 @@ ++pub mod msg; ++pub mod wasm_oracle; ++ ++pub use msg::*; ++pub use wasm_oracle::*; +diff --git a/packages/types/src/oracle.rs b/packages/types/src/oracle/msg.rs +similarity index 65% +rename from packages/types/src/oracle.rs +rename to packages/types/src/oracle/msg.rs +index d5b9d02c..572aae9b 100644 +--- a/packages/types/src/oracle.rs ++++ b/packages/types/src/oracle/msg.rs +@@ -1,13 +1,15 @@ + use cosmwasm_schema::{cw_serde, QueryResponses}; +-use cosmwasm_std::Decimal; ++use cosmwasm_std::{Decimal, Empty}; + use mars_owner::OwnerUpdate; + + #[cw_serde] +-pub struct InstantiateMsg { ++pub struct InstantiateMsg { + /// The contract's owner, who can update config and price sources + pub owner: String, + /// The asset in which prices are denominated in + pub base_denom: String, ++ /// Custom init params ++ pub custom_init: Option, + } + + #[cw_serde] +@@ -17,7 +19,7 @@ pub struct Config { + } + + #[cw_serde] +-pub enum ExecuteMsg { ++pub enum ExecuteMsg { + /// Specify the price source to be used for a coin + /// + /// NOTE: The input parameters for method are chain-specific. +@@ -31,6 +33,19 @@ pub enum ExecuteMsg { + }, + /// Manages admin role state + UpdateOwner(OwnerUpdate), ++ /// Update contract config (only callable by owner) ++ UpdateConfig { ++ base_denom: Option, ++ }, ++ /// Custom messages defined by the contract ++ Custom(C), ++} ++ ++/// Differentiator for the action (liquidate, withdraw, borrow etc.) being performed. ++#[cw_serde] ++pub enum ActionKind { ++ Default, ++ Liquidation, + } + + #[cw_serde] +@@ -61,6 +76,7 @@ pub enum QueryMsg { + #[returns(PriceResponse)] + Price { + denom: String, ++ kind: Option, + }, + /// Enumerate all coins' prices. + /// +@@ -70,6 +86,7 @@ pub enum QueryMsg { + Prices { + start_after: Option, + limit: Option, ++ kind: Option, + }, + } + +@@ -96,19 +113,44 @@ pub struct PriceResponse { + } + + pub mod helpers { +- use cosmwasm_std::{Decimal, QuerierWrapper, StdResult}; ++ use cosmwasm_std::{Decimal, QuerierWrapper, StdError, StdResult}; + +- use super::{PriceResponse, QueryMsg}; ++ use super::{ActionKind, PriceResponse, QueryMsg}; ++ use crate::oracle::ActionKind::Liquidation; + + pub fn query_price( + querier: &QuerierWrapper, + oracle: impl Into, + denom: impl Into, ++ ) -> StdResult { ++ let denom = denom.into(); ++ let res: PriceResponse = querier ++ .query_wasm_smart( ++ oracle.into(), ++ &QueryMsg::Price { ++ denom: denom.clone(), ++ kind: Some(ActionKind::Default), ++ }, ++ ) ++ .map_err(|e| { ++ StdError::generic_err(format!( ++ "failed to query price for denom: {}. Error: {}", ++ denom, e ++ )) ++ })?; ++ Ok(res.price) ++ } ++ ++ pub fn query_price_for_liquidate( ++ querier: &QuerierWrapper, ++ oracle: impl Into, ++ denom: impl Into, + ) -> StdResult { + let res: PriceResponse = querier.query_wasm_smart( + oracle.into(), + &QueryMsg::Price { + denom: denom.into(), ++ kind: Some(Liquidation), + }, + )?; + Ok(res.price) +diff --git a/packages/types/src/oracle/wasm_oracle.rs b/packages/types/src/oracle/wasm_oracle.rs +new file mode 100644 +index 00000000..1d772612 +--- /dev/null ++++ b/packages/types/src/oracle/wasm_oracle.rs +@@ -0,0 +1,23 @@ ++use cosmwasm_schema::cw_serde; ++use cosmwasm_std::Uint128; ++ ++#[cw_serde] ++pub struct WasmOracleCustomInitParams { ++ /// The Astroport factory contract address ++ pub astroport_factory: String, ++} ++ ++#[cw_serde] ++pub enum WasmOracleCustomExecuteMsg { ++ RecordTwapSnapshots { ++ denoms: Vec, ++ }, ++} ++ ++#[cw_serde] ++pub struct AstroportTwapSnapshot { ++ /// Timestamp of the most recent TWAP data update ++ pub timestamp: u64, ++ /// Cumulative price of the asset retrieved by the most recent TWAP data update ++ pub price_cumulative: Uint128, ++} +diff --git a/packages/types/src/red_bank/interest_rate_model.rs b/packages/types/src/red_bank/interest_rate_model.rs +index 9e2af312..26f844ea 100644 +--- a/packages/types/src/red_bank/interest_rate_model.rs ++++ b/packages/types/src/red_bank/interest_rate_model.rs +@@ -1,6 +1,8 @@ + use cosmwasm_schema::cw_serde; +-use cosmwasm_std::{Decimal, StdError, StdResult}; +-use mars_utils::{error::ValidationError, helpers::decimal_param_le_one, math}; ++use cosmwasm_std::Decimal; ++use mars_utils::{error::ValidationError, helpers::decimal_param_le_one}; ++ ++use crate::error::MarsError; + + #[cw_serde] + #[derive(Eq, Default)] +@@ -9,40 +11,47 @@ pub struct InterestRateModel { + pub optimal_utilization_rate: Decimal, + /// Base rate + pub base: Decimal, +- /// Slope parameter for interest rate model function when utilization_rate < optimal_utilization_rate ++ /// Slope parameter for interest rate model function when utilization_rate <= optimal_utilization_rate + pub slope_1: Decimal, +- /// Slope parameter for interest rate model function when utilization_rate >= optimal_utilization_rate ++ /// Slope parameter for interest rate model function when utilization_rate > optimal_utilization_rate + pub slope_2: Decimal, + } + + impl InterestRateModel { + pub fn validate(&self) -> Result<(), ValidationError> { + decimal_param_le_one(self.optimal_utilization_rate, "optimal_utilization_rate")?; ++ ++ if self.slope_1 >= self.slope_2 { ++ return Err(ValidationError::InvalidParam { ++ param_name: "slope_1".to_string(), ++ invalid_value: self.slope_1.to_string(), ++ predicate: format!("< {}", self.slope_2), ++ }); ++ } ++ + Ok(()) + } + +- pub fn get_borrow_rate(&self, current_utilization_rate: Decimal) -> StdResult { ++ pub fn get_borrow_rate(&self, current_utilization_rate: Decimal) -> Result { + let new_borrow_rate = if current_utilization_rate <= self.optimal_utilization_rate { + if current_utilization_rate.is_zero() { +- // prevent division by zero when optimal_utilization_rate is zero ++ // prevent division by zero when current_utilization_rate is zero + self.base + } else { + // The borrow interest rates increase slowly with utilization + self.base +- + self.slope_1.checked_mul(math::divide_decimal_by_decimal( +- current_utilization_rate, +- self.optimal_utilization_rate, +- )?)? ++ + self.slope_1.checked_mul( ++ current_utilization_rate.checked_div(self.optimal_utilization_rate)?, ++ )? + } + } else { + // The borrow interest rates increase sharply with utilization + self.base + + self.slope_1 +- + math::divide_decimal_by_decimal( +- self.slope_2 +- .checked_mul(current_utilization_rate - self.optimal_utilization_rate)?, +- Decimal::one() - self.optimal_utilization_rate, +- )? ++ + self ++ .slope_2 ++ .checked_mul(current_utilization_rate - self.optimal_utilization_rate)? ++ .checked_div(Decimal::one() - self.optimal_utilization_rate)? + }; + Ok(new_borrow_rate) + } +@@ -52,12 +61,11 @@ impl InterestRateModel { + borrow_rate: Decimal, + current_utilization_rate: Decimal, + reserve_factor: Decimal, +- ) -> StdResult { +- borrow_rate ++ ) -> Result { ++ Ok(borrow_rate + .checked_mul(current_utilization_rate)? + // This operation should not underflow as reserve_factor is checked to be <= 1 +- .checked_mul(Decimal::one() - reserve_factor) +- .map_err(StdError::from) ++ .checked_mul(Decimal::one() - reserve_factor)?) + } + } + +@@ -91,21 +99,13 @@ mod tests { + + market.update_interest_rates(utilization_rate).unwrap(); + +- let expected_borrow_rate = model.base +- + math::divide_decimal_by_decimal( +- model.slope_1.checked_mul(utilization_rate).unwrap(), +- model.optimal_utilization_rate, +- ) +- .unwrap(); ++ let expected_borrow_rate = ++ model.base + model.slope_1 * utilization_rate / model.optimal_utilization_rate; + + assert_eq!(market.borrow_rate, expected_borrow_rate); + assert_eq!( + market.liquidity_rate, +- expected_borrow_rate +- .checked_mul(utilization_rate) +- .unwrap() +- .checked_mul(Decimal::one() - reserve_factor) +- .unwrap() ++ expected_borrow_rate * utilization_rate * (Decimal::one() - reserve_factor) + ); + } + +@@ -124,11 +124,7 @@ mod tests { + let new_borrow_rate = model.get_borrow_rate(current_utilization_rate).unwrap(); + + let expected_borrow_rate = model.base +- + math::divide_decimal_by_decimal( +- model.slope_1.checked_mul(current_utilization_rate).unwrap(), +- model.optimal_utilization_rate, +- ) +- .unwrap(); ++ + model.slope_1 * current_utilization_rate / model.optimal_utilization_rate; + + assert_eq!(new_borrow_rate, expected_borrow_rate); + } +@@ -139,11 +135,7 @@ mod tests { + let new_borrow_rate = model.get_borrow_rate(current_utilization_rate).unwrap(); + + let expected_borrow_rate = model.base +- + math::divide_decimal_by_decimal( +- model.slope_1.checked_mul(current_utilization_rate).unwrap(), +- model.optimal_utilization_rate, +- ) +- .unwrap(); ++ + model.slope_1 * current_utilization_rate / model.optimal_utilization_rate; + + assert_eq!(new_borrow_rate, expected_borrow_rate); + } +@@ -155,14 +147,8 @@ mod tests { + + let expected_borrow_rate = model.base + + model.slope_1 +- + math::divide_decimal_by_decimal( +- model +- .slope_2 +- .checked_mul(current_utilization_rate - model.optimal_utilization_rate) +- .unwrap(), +- Decimal::one() - model.optimal_utilization_rate, +- ) +- .unwrap(); ++ + model.slope_2 * (current_utilization_rate - model.optimal_utilization_rate) ++ / (Decimal::one() - model.optimal_utilization_rate); + + assert_eq!(new_borrow_rate, expected_borrow_rate); + } +@@ -179,9 +165,7 @@ mod tests { + let current_utilization_rate = Decimal::percent(100); + let new_borrow_rate = model.get_borrow_rate(current_utilization_rate).unwrap(); + +- let expected_borrow_rate = Decimal::percent(7); +- +- assert_eq!(new_borrow_rate, expected_borrow_rate); ++ assert_eq!(new_borrow_rate, Decimal::percent(7)); + } + + // current utilization rate == 0% and optimal utilization rate == 0% +@@ -196,9 +180,7 @@ mod tests { + let current_utilization_rate = Decimal::percent(0); + let new_borrow_rate = model.get_borrow_rate(current_utilization_rate).unwrap(); + +- let expected_borrow_rate = Decimal::percent(2); +- +- assert_eq!(new_borrow_rate, expected_borrow_rate); ++ assert_eq!(new_borrow_rate, Decimal::percent(2)); + } + + // current utilization rate == 20% and optimal utilization rate == 0% +@@ -213,9 +195,8 @@ mod tests { + let current_utilization_rate = Decimal::percent(20); + let new_borrow_rate = model.get_borrow_rate(current_utilization_rate).unwrap(); + +- let expected_borrow_rate = model.base +- + model.slope_1 +- + model.slope_2.checked_mul(current_utilization_rate).unwrap(); ++ let expected_borrow_rate = ++ model.base + model.slope_1 + model.slope_2 * current_utilization_rate; + + assert_eq!(new_borrow_rate, expected_borrow_rate); + } +diff --git a/packages/types/src/red_bank/market.rs b/packages/types/src/red_bank/market.rs +index cb5f9688..2ffe0a04 100644 +--- a/packages/types/src/red_bank/market.rs ++++ b/packages/types/src/red_bank/market.rs +@@ -1,9 +1,6 @@ + use cosmwasm_schema::cw_serde; + use cosmwasm_std::{Decimal, StdResult, Uint128}; +-use mars_utils::{ +- error::ValidationError, +- helpers::{decimal_param_le_one, decimal_param_lt_one}, +-}; ++use mars_utils::{error::ValidationError, helpers::decimal_param_lt_one}; + + use crate::red_bank::InterestRateModel; + +@@ -11,14 +8,6 @@ use crate::red_bank::InterestRateModel; + pub struct Market { + /// Denom of the asset + pub denom: String, +- +- /// Max base asset that can be borrowed per "base asset" collateral when using the asset as collateral +- pub max_loan_to_value: Decimal, +- /// Base asset amount in debt position per "base asset" of asset collateral that if surpassed makes the user's position liquidatable. +- pub liquidation_threshold: Decimal, +- /// Bonus amount of collateral liquidator get when repaying user's debt (Will get collateral +- /// from user in an amount equal to debt repayed + bonus) +- pub liquidation_bonus: Decimal, + /// Portion of the borrow rate that is kept as protocol rewards + pub reserve_factor: Decimal, + +@@ -40,13 +29,6 @@ pub struct Market { + pub collateral_total_scaled: Uint128, + /// Total debt scaled for the market's currency + pub debt_total_scaled: Uint128, +- +- /// If false cannot deposit +- pub deposit_enabled: bool, +- /// If false cannot borrow +- pub borrow_enabled: bool, +- /// Deposit Cap (defined in terms of the asset) +- pub deposit_cap: Uint128, + } + + impl Default for Market { +@@ -57,18 +39,11 @@ impl Default for Market { + liquidity_index: Decimal::one(), + borrow_rate: Decimal::zero(), + liquidity_rate: Decimal::zero(), +- max_loan_to_value: Decimal::zero(), + reserve_factor: Decimal::zero(), + indexes_last_updated: 0, + collateral_total_scaled: Uint128::zero(), + debt_total_scaled: Uint128::zero(), +- liquidation_threshold: Decimal::one(), +- liquidation_bonus: Decimal::zero(), + interest_rate_model: InterestRateModel::default(), +- deposit_enabled: true, +- borrow_enabled: true, +- // By default the cap should be unlimited (no cap) +- deposit_cap: Uint128::MAX, + } + } + } +@@ -76,18 +51,6 @@ impl Default for Market { + impl Market { + pub fn validate(&self) -> Result<(), ValidationError> { + decimal_param_lt_one(self.reserve_factor, "reserve_factor")?; +- decimal_param_le_one(self.max_loan_to_value, "max_loan_to_value")?; +- decimal_param_le_one(self.liquidation_threshold, "liquidation_threshold")?; +- decimal_param_le_one(self.liquidation_bonus, "liquidation_bonus")?; +- +- // liquidation_threshold should be greater than max_loan_to_value +- if self.liquidation_threshold <= self.max_loan_to_value { +- return Err(ValidationError::InvalidParam { +- param_name: "liquidation_threshold".to_string(), +- invalid_value: self.liquidation_threshold.to_string(), +- predicate: format!("> {} (max LTV)", self.max_loan_to_value), +- }); +- } + + self.interest_rate_model.validate()?; + +diff --git a/packages/types/src/red_bank/msg.rs b/packages/types/src/red_bank/msg.rs +index c7ad2029..b80f7ec8 100644 +--- a/packages/types/src/red_bank/msg.rs ++++ b/packages/types/src/red_bank/msg.rs +@@ -8,8 +8,6 @@ use crate::red_bank::InterestRateModel; + pub struct InstantiateMsg { + /// Contract's owner + pub owner: String, +- /// Contract's emergency owner +- pub emergency_owner: String, + /// Market configuration + pub config: CreateOrUpdateConfig, + } +@@ -19,9 +17,6 @@ pub enum ExecuteMsg { + /// Manages owner state + UpdateOwner(OwnerUpdate), + +- /// Manages emergency owner state +- UpdateEmergencyOwner(OwnerUpdate), +- + /// Update contract config (only owner can call) + UpdateConfig { + config: CreateOrUpdateConfig, +@@ -59,8 +54,8 @@ pub enum ExecuteMsg { + /// Deposit native coins. Deposited coins must be sent in the transaction + /// this call is made + Deposit { +- /// Address that will receive the coins +- on_behalf_of: Option, ++ /// Credit account id (Rover) ++ account_id: Option, + }, + + /// Withdraw native coins +@@ -71,6 +66,11 @@ pub enum ExecuteMsg { + amount: Option, + /// The address where the withdrawn amount is sent + recipient: Option, ++ /// Credit account id (Rover) ++ account_id: Option, ++ // Withdraw action related to liquidation process initiated in credit manager. ++ // This flag is used to identify different way for pricing assets during liquidation. ++ liquidation_related: Option, + }, + + /// Borrow native coins. If borrow allowed, amount is added to caller's debt +@@ -117,30 +117,15 @@ pub enum ExecuteMsg { + #[cw_serde] + pub struct CreateOrUpdateConfig { + pub address_provider: Option, +- pub close_factor: Option, + } + + #[cw_serde] + pub struct InitOrUpdateAssetParams { + /// Portion of the borrow rate that is kept as protocol rewards + pub reserve_factor: Option, +- /// Max uusd that can be borrowed per uusd of collateral when using the asset as collateral +- pub max_loan_to_value: Option, +- /// uusd amount in debt position per uusd of asset collateral that if surpassed makes the user's position liquidatable. +- pub liquidation_threshold: Option, +- /// Bonus amount of collateral liquidator get when repaying user's debt (Will get collateral +- /// from user in an amount equal to debt repayed + bonus) +- pub liquidation_bonus: Option, + + /// Interest rate strategy to calculate borrow_rate and liquidity_rate + pub interest_rate_model: Option, +- +- /// If false cannot deposit +- pub deposit_enabled: Option, +- /// If false cannot borrow +- pub borrow_enabled: Option, +- /// Deposit Cap defined in terms of the asset (Unlimited by default) +- pub deposit_cap: Option, + } + + #[cw_serde] +@@ -197,6 +182,7 @@ pub enum QueryMsg { + #[returns(crate::red_bank::UserCollateralResponse)] + UserCollateral { + user: String, ++ account_id: Option, + denom: String, + }, + +@@ -204,6 +190,16 @@ pub enum QueryMsg { + #[returns(Vec)] + UserCollaterals { + user: String, ++ account_id: Option, ++ start_after: Option, ++ limit: Option, ++ }, ++ ++ /// Get all collateral positions for a user ++ #[returns(crate::red_bank::PaginatedUserCollateralResponse)] ++ UserCollateralsV2 { ++ user: String, ++ account_id: Option, + start_after: Option, + limit: Option, + }, +@@ -212,6 +208,14 @@ pub enum QueryMsg { + #[returns(crate::red_bank::UserPositionResponse)] + UserPosition { + user: String, ++ account_id: Option, ++ }, ++ ++ /// Get user position for liquidation ++ #[returns(crate::red_bank::UserPositionResponse)] ++ UserPositionLiquidationPricing { ++ user: String, ++ account_id: Option, + }, + + /// Get liquidity scaled amount for a given underlying asset amount. +diff --git a/packages/types/src/red_bank/types.rs b/packages/types/src/red_bank/types.rs +index c6cd1202..d6b14044 100644 +--- a/packages/types/src/red_bank/types.rs ++++ b/packages/types/src/red_bank/types.rs +@@ -1,21 +1,13 @@ + use cosmwasm_schema::cw_serde; + use cosmwasm_std::{Decimal, Uint128}; +-use mars_utils::{error::ValidationError, helpers::decimal_param_le_one}; ++ ++use crate::PaginationResponse; + + /// Global configuration + #[cw_serde] + pub struct Config { + /// Address provider returns addresses for all protocol contracts + pub address_provider: T, +- /// Maximum percentage of outstanding debt that can be covered by a liquidator +- pub close_factor: Decimal, +-} +- +-impl Config { +- pub fn validate(&self) -> Result<(), ValidationError> { +- decimal_param_le_one(self.close_factor, "close_factor")?; +- Ok(()) +- } + } + + #[cw_serde] +@@ -70,14 +62,8 @@ pub struct ConfigResponse { + pub owner: Option, + /// The contract's proposed owner + pub proposed_new_owner: Option, +- /// The contract's emergency owner +- pub emergency_owner: Option, +- /// The contract's proposed emergency owner +- pub proposed_new_emergency_owner: Option, + /// Address provider returns addresses for all protocol contracts + pub address_provider: String, +- /// Maximum percentage of outstanding debt that can be covered by a liquidator +- pub close_factor: Decimal, + } + + #[cw_serde] +@@ -112,6 +98,8 @@ pub struct UserCollateralResponse { + pub enabled: bool, + } + ++pub type PaginatedUserCollateralResponse = PaginationResponse; ++ + #[cw_serde] + pub struct UserPositionResponse { + /// Total value of all enabled collateral assets. +diff --git a/packages/types/src/rewards_collector.rs b/packages/types/src/rewards_collector.rs +index ce69309a..149490eb 100644 +--- a/packages/types/src/rewards_collector.rs ++++ b/packages/types/src/rewards_collector.rs +@@ -1,11 +1,13 @@ + use cosmwasm_schema::{cw_serde, QueryResponses}; +-use cosmwasm_std::{Addr, Api, Decimal, StdResult, Uint128}; ++use cosmwasm_std::{Addr, Api, Coin, Decimal, StdResult, Uint128}; + use mars_owner::OwnerUpdate; + use mars_utils::{ + error::ValidationError, + helpers::{decimal_param_le_one, integer_param_gt_zero, validate_native_denom}, + }; + ++use self::credit_manager::Action; ++ + const MAX_SLIPPAGE_TOLERANCE_PERCENTAGE: u64 = 50; + + #[cw_serde] +@@ -26,6 +28,8 @@ pub struct InstantiateMsg { + pub timeout_seconds: u64, + /// Maximum percentage of price movement (minimum amount you accept to receive during swap) + pub slippage_tolerance: Decimal, ++ /// Neutron Ibc config ++ pub neutron_ibc_config: Option, + } + + #[cw_serde] +@@ -44,6 +48,15 @@ pub struct Config { + pub timeout_seconds: u64, + /// Maximum percentage of price movement (minimum amount you accept to receive during swap) + pub slippage_tolerance: Decimal, ++ /// Neutron IBC config ++ pub neutron_ibc_config: Option, ++} ++ ++#[cw_serde] ++pub struct NeutronIbcConfig { ++ pub source_port: String, ++ pub acc_fee: Vec, ++ pub timeout_fee: Vec, + } + + impl Config { +@@ -77,6 +90,7 @@ impl Config { + channel_id: msg.channel_id, + timeout_seconds: msg.timeout_seconds, + slippage_tolerance: msg.slippage_tolerance, ++ neutron_ibc_config: msg.neutron_ibc_config, + }) + } + } +@@ -98,10 +112,12 @@ pub struct UpdateConfig { + pub timeout_seconds: Option, + /// Maximum percentage of price movement (minimum amount you accept to receive during swap) + pub slippage_tolerance: Option, ++ /// Neutron Ibc config ++ pub neutron_ibc_config: Option, + } + + #[cw_serde] +-pub enum ExecuteMsg { ++pub enum ExecuteMsg { + /// Manages admin role state + UpdateOwner(OwnerUpdate), + +@@ -110,22 +126,18 @@ pub enum ExecuteMsg { + new_cfg: UpdateConfig, + }, + +- /// Configure the route for swapping an asset +- /// +- /// This is chain-specific, and can include parameters such as slippage tolerance and the routes +- /// for multi-step swaps +- SetRoute { +- denom_in: String, +- denom_out: String, +- route: Route, +- }, +- + /// Withdraw coins from the red bank + WithdrawFromRedBank { + denom: String, + amount: Option, + }, + ++ /// Withdraw coins from the credit manager ++ WithdrawFromCreditManager { ++ account_id: String, ++ actions: Vec, ++ }, ++ + /// Distribute the accrued protocol income between the safety fund and the fee modules on mars hub, + /// according to the split set in config. + /// Callable by any address. +@@ -144,7 +156,16 @@ pub enum ExecuteMsg { + /// + /// We wanted to leave protocol rewards in the red-bank so they continue to work as liquidity (until the bot invokes WithdrawFromRedBank). + /// As an side effect to this, if the market is incentivised with MARS tokens, the contract will also accrue MARS token incentives. +- ClaimIncentiveRewards {}, ++ ClaimIncentiveRewards { ++ /// Start pagination after this collateral denom ++ start_after_collateral_denom: Option, ++ /// Start pagination after this incentive denom. If supplied you must also supply ++ /// start_after_collateral_denom. ++ start_after_incentive_denom: Option, ++ /// The maximum number of results to return. If not set, 5 is used. If larger than 10, ++ /// 10 is used. ++ limit: Option, ++ }, + } + + #[cw_serde] +@@ -167,6 +188,8 @@ pub struct ConfigResponse { + pub timeout_seconds: u64, + /// Maximum percentage of price movement (minimum amount you accept to receive during swap) + pub slippage_tolerance: Decimal, ++ /// Neutron Ibc config ++ pub neutron_ibc_config: Option, + } + + #[cw_serde] +@@ -175,29 +198,40 @@ pub enum QueryMsg { + /// Get config parameters + #[returns(ConfigResponse)] + Config {}, +- /// Get routes for swapping an input denom into an output denom. +- /// +- /// NOTE: The response type of this query is chain-specific. +- #[returns(RouteResponse)] +- Route { +- denom_in: String, +- denom_out: String, +- }, +- /// Enumerate all swap routes. +- /// +- /// NOTE: The response type of this query is chain-specific. +- #[returns(Vec>)] +- Routes { +- start_after: Option<(String, String)>, +- limit: Option, +- }, + } + +-#[cw_serde] +-pub struct RouteResponse { +- pub denom_in: String, +- pub denom_out: String, +- pub route: Route, +-} ++// TODO: rover is private repo for now so can't use it as a dependency. Use rover types once repo is public. ++pub mod credit_manager { ++ use cosmwasm_schema::cw_serde; ++ use cosmwasm_std::{Coin, Uint128}; ++ ++ #[cw_serde] ++ pub enum ExecuteMsg { ++ UpdateCreditAccount { ++ account_id: String, ++ actions: Vec, ++ }, ++ } + +-pub type RoutesResponse = Vec>; ++ #[cw_serde] ++ pub enum Action { ++ Withdraw(Coin), ++ WithdrawLiquidity { ++ lp_token: ActionCoin, ++ minimum_receive: Vec, ++ }, ++ Unknown {}, // Used to simulate allowance only for: Withdraw and WithdrawLiquidity ++ } ++ ++ #[cw_serde] ++ pub struct ActionCoin { ++ pub denom: String, ++ pub amount: ActionAmount, ++ } ++ ++ #[cw_serde] ++ pub enum ActionAmount { ++ Exact(Uint128), ++ AccountBalance, ++ } ++} +diff --git a/packages/types/src/swapper/adapter.rs b/packages/types/src/swapper/adapter.rs +new file mode 100644 +index 00000000..e5390172 +--- /dev/null ++++ b/packages/types/src/swapper/adapter.rs +@@ -0,0 +1,110 @@ ++use cosmwasm_schema::cw_serde; ++use cosmwasm_std::{to_binary, Addr, Api, Coin, CosmosMsg, Decimal, Empty, StdResult, WasmMsg}; ++ ++use crate::swapper::ExecuteMsg; ++ ++#[cw_serde] ++pub struct SwapperBase(T); ++ ++impl SwapperBase { ++ pub fn new(address: T) -> SwapperBase { ++ SwapperBase(address) ++ } ++ ++ pub fn address(&self) -> &T { ++ &self.0 ++ } ++} ++ ++pub type SwapperUnchecked = SwapperBase; ++pub type Swapper = SwapperBase; ++ ++impl From for SwapperUnchecked { ++ fn from(s: Swapper) -> Self { ++ Self(s.address().to_string()) ++ } ++} ++ ++impl SwapperUnchecked { ++ pub fn check(&self, api: &dyn Api) -> StdResult { ++ Ok(SwapperBase::new(api.addr_validate(self.address())?)) ++ } ++} ++ ++impl Swapper { ++ /// Generate message for performing a swapper ++ pub fn swap_exact_in_msg( ++ &self, ++ coin_in: &Coin, ++ denom_out: &str, ++ slippage: Decimal, ++ ) -> StdResult { ++ Ok(CosmosMsg::Wasm(WasmMsg::Execute { ++ contract_addr: self.address().to_string(), ++ msg: to_binary(&ExecuteMsg::::SwapExactIn { ++ coin_in: coin_in.clone(), ++ denom_out: denom_out.to_string(), ++ slippage, ++ })?, ++ funds: vec![coin_in.clone()], ++ })) ++ } ++} ++ ++#[cfg(test)] ++mod tests { ++ use cosmwasm_std::testing::MockApi; ++ ++ use super::*; ++ ++ #[test] ++ fn test_swapper_unchecked_from_swapper() { ++ let swapper = Swapper::new(Addr::unchecked("swapper")); ++ let swapper_unchecked = SwapperUnchecked::from(swapper.clone()); ++ assert_eq!(swapper_unchecked.address(), "swapper"); ++ assert_eq!(swapper_unchecked.check(&MockApi::default()).unwrap(), swapper); ++ } ++ ++ #[test] ++ fn test_swapper_unchecked_check() { ++ let swapper = SwapperUnchecked::new("swapper".to_string()); ++ assert_eq!( ++ swapper.check(&MockApi::default()).unwrap(), ++ Swapper::new(Addr::unchecked("swapper".to_string())) ++ ); ++ } ++ ++ #[test] ++ fn test_new_and_address() { ++ // Swapper ++ let swapper = Swapper::new(Addr::unchecked("swapper")); ++ assert_eq!(swapper.address(), &Addr::unchecked("swapper")); ++ ++ // SwapperUnchecked ++ let swapper_unchecked = SwapperUnchecked::new("swapper".to_string()); ++ assert_eq!(swapper_unchecked.address(), "swapper"); ++ } ++ ++ #[test] ++ fn test_swapper_swap_exact_in_msg() { ++ let swapper = Swapper::new(Addr::unchecked("swapper")); ++ let coin_in = Coin::new(100, "in"); ++ let denom_out = "out"; ++ let slippage = Decimal::percent(1); ++ ++ let msg = swapper.swap_exact_in_msg(&coin_in, denom_out, slippage).unwrap(); ++ assert_eq!( ++ msg, ++ CosmosMsg::Wasm(WasmMsg::Execute { ++ contract_addr: "swapper".to_string(), ++ msg: to_binary(&ExecuteMsg::::SwapExactIn { ++ coin_in: coin_in.clone(), ++ denom_out: denom_out.to_string(), ++ slippage, ++ }) ++ .unwrap(), ++ funds: vec![coin_in], ++ }) ++ ); ++ } ++} +diff --git a/packages/types/src/swapper/mod.rs b/packages/types/src/swapper/mod.rs +new file mode 100644 +index 00000000..6071d783 +--- /dev/null ++++ b/packages/types/src/swapper/mod.rs +@@ -0,0 +1,4 @@ ++pub mod adapter; ++pub mod msgs; ++ ++pub use self::{adapter::*, msgs::*}; +diff --git a/packages/types/src/swapper/msgs.rs b/packages/types/src/swapper/msgs.rs +new file mode 100644 +index 00000000..bd270548 +--- /dev/null ++++ b/packages/types/src/swapper/msgs.rs +@@ -0,0 +1,77 @@ ++use cosmwasm_schema::{cw_serde, QueryResponses}; ++use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; ++use mars_owner::OwnerUpdate; ++ ++#[cw_serde] ++pub struct InstantiateMsg { ++ /// The contract's owner, who can update config ++ pub owner: String, ++} ++ ++#[cw_serde] ++pub enum ExecuteMsg { ++ /// Manges owner role state ++ UpdateOwner(OwnerUpdate), ++ /// Configure the route for swapping an asset ++ /// ++ /// This is chain-specific, and can include parameters such as slippage tolerance and the routes ++ /// for multi-step swaps ++ SetRoute { ++ denom_in: String, ++ denom_out: String, ++ route: Route, ++ }, ++ /// Perform a swapper with an exact-in amount. Requires slippage allowance %. ++ SwapExactIn { ++ coin_in: Coin, ++ denom_out: String, ++ slippage: Decimal, ++ }, ++ /// Send swapper results back to swapper. Also refunds extra if sent more than needed. Internal use only. ++ TransferResult { ++ recipient: Addr, ++ denom_in: String, ++ denom_out: String, ++ }, ++} ++ ++#[cw_serde] ++#[derive(QueryResponses)] ++pub enum QueryMsg { ++ /// Query contract owner config ++ #[returns(mars_owner::OwnerResponse)] ++ Owner {}, ++ /// Get route for swapping an input denom into an output denom ++ #[returns(RouteResponse)] ++ Route { ++ denom_in: String, ++ denom_out: String, ++ }, ++ /// Enumerate all swapper routes ++ #[returns(RoutesResponse)] ++ Routes { ++ start_after: Option<(String, String)>, ++ limit: Option, ++ }, ++ /// Return current spot price swapping In for Out ++ /// Warning: Do not use this as an oracle price feed. Use Mars-Oracle for pricing. ++ #[returns(EstimateExactInSwapResponse)] ++ EstimateExactInSwap { ++ coin_in: Coin, ++ denom_out: String, ++ }, ++} ++ ++#[cw_serde] ++pub struct RouteResponse { ++ pub denom_in: String, ++ pub denom_out: String, ++ pub route: Route, ++} ++ ++pub type RoutesResponse = Vec>; ++ ++#[cw_serde] ++pub struct EstimateExactInSwapResponse { ++ pub amount: Uint128, ++} From 2987a8f481c89e0ed5c2b3f183667a2928c53600 Mon Sep 17 00:00:00 2001 From: piobab Date: Mon, 11 Sep 2023 09:06:39 +0200 Subject: [PATCH 38/43] Revert deposit on behalf of (#319) * Revert on_behalf_of. It is used by another app. * Don't allow to use on_behalf_of by credit manager contract. * Fix on_behalf_of check. --- contracts/red-bank/src/contract.rs | 11 +- contracts/red-bank/src/deposit.rs | 34 ++- .../red-bank/tests/tests/test_deposit.rs | 205 ++++++++++++++++++ .../red-bank/tests/tests/test_liquidate.rs | 4 + integration-tests/tests/test_oracles.rs | 2 + packages/testing/src/integration/mock_env.rs | 1 + packages/types/src/red_bank/msg.rs | 3 + schemas/mars-red-bank/mars-red-bank.json | 7 + .../mars-red-bank/MarsRedBank.client.ts | 5 + .../mars-red-bank/MarsRedBank.react-query.ts | 1 + .../mars-red-bank/MarsRedBank.types.ts | 1 + 11 files changed, 268 insertions(+), 6 deletions(-) diff --git a/contracts/red-bank/src/contract.rs b/contracts/red-bank/src/contract.rs index 061eae5cf..409729cc6 100644 --- a/contracts/red-bank/src/contract.rs +++ b/contracts/red-bank/src/contract.rs @@ -50,9 +50,18 @@ pub fn execute( } ExecuteMsg::Deposit { account_id, + on_behalf_of, } => { let sent_coin = cw_utils::one_coin(&info)?; - deposit::deposit(deps, env, info, sent_coin.denom, sent_coin.amount, account_id) + deposit::deposit( + deps, + env, + info, + on_behalf_of, + sent_coin.denom, + sent_coin.amount, + account_id, + ) } ExecuteMsg::Withdraw { denom, diff --git a/contracts/red-bank/src/deposit.rs b/contracts/red-bank/src/deposit.rs index 711f4e0ef..16efe17b5 100644 --- a/contracts/red-bank/src/deposit.rs +++ b/contracts/red-bank/src/deposit.rs @@ -1,6 +1,9 @@ -use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Uint128}; +use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response, Uint128}; use mars_interest_rate::get_scaled_liquidity_amount; -use mars_red_bank_types::address_provider::{self, MarsAddressType}; +use mars_red_bank_types::{ + address_provider::{self, MarsAddressType}, + error::MarsError, +}; use crate::{ error::ContractError, @@ -14,12 +17,11 @@ pub fn deposit( deps: DepsMut, env: Env, info: MessageInfo, + on_behalf_of: Option, denom: String, deposit_amount: Uint128, account_id: Option, ) -> Result { - let mut market = MARKETS.load(deps.storage, &denom)?; - let config = CONFIG.load(deps.storage)?; let addresses = address_provider::helpers::query_contract_addrs( @@ -29,11 +31,32 @@ pub fn deposit( MarsAddressType::Incentives, MarsAddressType::RewardsCollector, MarsAddressType::Params, + MarsAddressType::CreditManager, ], )?; let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; let incentives_addr = &addresses[&MarsAddressType::Incentives]; let params_addr = &addresses[&MarsAddressType::Params]; + let credit_manager_addr = &addresses[&MarsAddressType::CreditManager]; + + let user_addr: Addr; + let user = match on_behalf_of.as_ref() { + // A malicious user can permanently disable the lend action in credit-manager contract by performing the following steps: + // 1.) Wait for a new asset XXX to be listed and makes sure there is no coin lent out for XXX from the credit-manager to red-bank. + // 2.) Calls deposit on red-bank and sends 1 XXX and deposits on behalf of credit-manager. + // 3.) A user wants to lend out XXX from credit-manager but the call fails because TOTAL_LENT_SHARES is never initialized + // because this query red_bank.query_lent(&deps.querier, &env.contract.address, &coin.denom)? returns one. + Some(address) if address == credit_manager_addr.as_str() => { + return Err(ContractError::Mars(MarsError::Unauthorized {})); + } + Some(address) => { + user_addr = deps.api.addr_validate(address)?; + User(&user_addr) + } + None => User(&info.sender), + }; + + let mut market = MARKETS.load(deps.storage, &denom)?; let asset_params = query_asset_params(&deps.querier, params_addr, &denom)?; @@ -68,7 +91,7 @@ pub fn deposit( let deposit_amount_scaled = get_scaled_liquidity_amount(deposit_amount, &market, env.block.time.seconds())?; - response = User(&info.sender).increase_collateral( + response = user.increase_collateral( deps.storage, &market, deposit_amount_scaled, @@ -86,6 +109,7 @@ pub fn deposit( Ok(response .add_attribute("action", "deposit") .add_attribute("sender", &info.sender) + .add_attribute("on_behalf_of", user) .add_attribute("denom", denom) .add_attribute("amount", deposit_amount) .add_attribute("amount_scaled", deposit_amount_scaled)) diff --git a/contracts/red-bank/tests/tests/test_deposit.rs b/contracts/red-bank/tests/tests/test_deposit.rs index 6593a5363..c954ecc07 100644 --- a/contracts/red-bank/tests/tests/test_deposit.rs +++ b/contracts/red-bank/tests/tests/test_deposit.rs @@ -17,6 +17,7 @@ use mars_red_bank::{ }; use mars_red_bank_types::{ address_provider::MarsAddressType, + error::MarsError, incentives, red_bank::{Collateral, ExecuteMsg, Market}, }; @@ -113,6 +114,7 @@ fn depositing_with_no_coin_sent() { mock_info(depositor_addr.as_str(), &[]), ExecuteMsg::Deposit { account_id: None, + on_behalf_of: None, }, ) .unwrap_err(); @@ -135,6 +137,7 @@ fn depositing_with_multiple_coins_sent() { mock_info(depositor_addr.as_str(), &sent_coins), ExecuteMsg::Deposit { account_id: None, + on_behalf_of: None, }, ) .unwrap_err(); @@ -158,6 +161,7 @@ fn depositing_to_non_existent_market() { mock_info(depositor_addr.as_str(), &coins(123, false_denom)), ExecuteMsg::Deposit { account_id: None, + on_behalf_of: None, }, ) .unwrap_err(); @@ -195,6 +199,7 @@ fn depositing_to_disabled_market() { mock_info(depositor_addr.as_str(), &coins(123, denom)), ExecuteMsg::Deposit { account_id: None, + on_behalf_of: None, }, ) .unwrap_err(); @@ -252,6 +257,7 @@ fn depositing_above_cap(amount_to_deposit: u128, deposit_cap: u128, exp_ok: bool mock_info(depositor_addr.as_str(), &coins(amount_to_deposit, denom)), ExecuteMsg::Deposit { account_id: None, + on_behalf_of: None, }, ); @@ -295,6 +301,7 @@ fn depositing_without_existing_position() { mock_info(depositor_addr.as_str(), &coins(deposit_amount, denom)), ExecuteMsg::Deposit { account_id: None, + on_behalf_of: None, }, ) .unwrap(); @@ -323,6 +330,7 @@ fn depositing_without_existing_position() { vec![ attr("action", "deposit"), attr("sender", &depositor_addr), + attr("on_behalf_of", &depositor_addr), attr("denom", denom), attr("amount", deposit_amount.to_string()), attr("amount_scaled", expected_mint_amount), @@ -385,6 +393,7 @@ fn depositing_with_existing_position() { mock_info(depositor_addr.as_str(), &coins(deposit_amount, denom)), ExecuteMsg::Deposit { account_id: None, + on_behalf_of: None, }, ) .unwrap(); @@ -421,3 +430,199 @@ fn depositing_with_existing_position() { } ); } + +#[test] +fn depositing_on_behalf_of() { + let TestSuite { + mut deps, + denom, + depositor_addr, + initial_market, + } = setup_test(); + + let deposit_amount = 123456u128; + let on_behalf_of_addr = Addr::unchecked("jake"); + + // compute expected market parameters + let block_time = 10000300; + let expected_params = + th_get_expected_indices_and_rates(&initial_market, block_time, Default::default()); + let expected_mint_amount = compute_scaled_amount( + Uint128::from(deposit_amount), + expected_params.liquidity_index, + ScalingOperation::Truncate, + ) + .unwrap(); + let expected_reward_amount_scaled = compute_scaled_amount( + expected_params.protocol_rewards_to_distribute, + expected_params.liquidity_index, + ScalingOperation::Truncate, + ) + .unwrap(); + + let res = execute( + deps.as_mut(), + mock_env_at_block_time(block_time), + mock_info(depositor_addr.as_str(), &coins(deposit_amount, denom)), + ExecuteMsg::Deposit { + account_id: None, + on_behalf_of: Some(on_behalf_of_addr.clone().into()), + }, + ) + .unwrap(); + + // NOTE: For this test, the accrued protocol reward is non-zero, so we do expect a message to + // update the index of the rewards collector. + assert_eq!( + res.messages, + vec![ + SubMsg::new(WasmMsg::Execute { + contract_addr: MarsAddressType::Incentives.to_string(), + msg: to_binary(&incentives::ExecuteMsg::BalanceChange { + user_addr: Addr::unchecked(MarsAddressType::RewardsCollector.to_string()), + account_id: None, + denom: initial_market.denom.clone(), + user_amount_scaled_before: Uint128::zero(), + total_amount_scaled_before: initial_market.collateral_total_scaled, + }) + .unwrap(), + funds: vec![] + }), + SubMsg::new(WasmMsg::Execute { + contract_addr: MarsAddressType::Incentives.to_string(), + msg: to_binary(&incentives::ExecuteMsg::BalanceChange { + user_addr: on_behalf_of_addr.clone(), + account_id: None, + denom: initial_market.denom.clone(), + user_amount_scaled_before: Uint128::zero(), + // NOTE: New collateral shares were minted to the rewards collector first, so + // for the depositor this should be initial total supply + rewards shares minted + total_amount_scaled_before: initial_market.collateral_total_scaled + + expected_reward_amount_scaled, + }) + .unwrap(), + funds: vec![] + }) + ] + ); + + // depositor should not have created a new collateral position + let opt = COLLATERALS.may_load(deps.as_ref().storage, (&depositor_addr, "", denom)).unwrap(); + assert!(opt.is_none()); + + // the recipient should have created a new collateral position + let collateral = + COLLATERALS.load(deps.as_ref().storage, (&on_behalf_of_addr, "", denom)).unwrap(); + assert_eq!( + collateral, + Collateral { + amount_scaled: expected_mint_amount, + enabled: true, + } + ); +} + +#[test] +fn depositing_on_behalf_of_cannot_enable_collateral() { + let TestSuite { + mut deps, + denom, + depositor_addr, + .. + } = setup_test(); + + deps.querier.set_oracle_price(denom, Decimal::one()); + + let on_behalf_of_addr = Addr::unchecked("jake"); + + let block_time = 10000300; + + // 'on_behalf_of_addr' deposit funds to their own account + execute( + deps.as_mut(), + mock_env_at_block_time(block_time), + mock_info(on_behalf_of_addr.as_str(), &coins(1u128, denom)), + ExecuteMsg::Deposit { + account_id: None, + on_behalf_of: None, + }, + ) + .unwrap(); + + // 'on_behalf_of_addr' should have collateral enabled + let collateral = + COLLATERALS.load(deps.as_ref().storage, (&on_behalf_of_addr, "", denom)).unwrap(); + assert!(collateral.enabled); + + // 'on_behalf_of_addr' disables asset as collateral + execute( + deps.as_mut(), + mock_env_at_block_time(block_time), + mock_info(on_behalf_of_addr.as_str(), &[]), + ExecuteMsg::UpdateAssetCollateralStatus { + denom: denom.to_string(), + enable: false, + }, + ) + .unwrap(); + + // verify asset is disabled as collateral for 'on_behalf_of_addr' + let collateral = + COLLATERALS.load(deps.as_ref().storage, (&on_behalf_of_addr, "", denom)).unwrap(); + assert!(!collateral.enabled); + + // 'depositor_addr' deposits a small amount of funds to 'on_behalf_of_addr' to enable his asset as collateral + execute( + deps.as_mut(), + mock_env_at_block_time(block_time), + mock_info(depositor_addr.as_str(), &coins(1u128, denom)), + ExecuteMsg::Deposit { + account_id: None, + on_behalf_of: Some(on_behalf_of_addr.to_string()), + }, + ) + .unwrap(); + + // 'on_behalf_of_addr' doesn't have the asset enabled as collateral + let collateral = + COLLATERALS.load(deps.as_ref().storage, (&on_behalf_of_addr, "", denom)).unwrap(); + assert!(!collateral.enabled); +} + +#[test] +fn depositing_on_behalf_of_credit_manager() { + let TestSuite { + mut deps, + denom, + depositor_addr, + .. + } = setup_test(); + + // disable the market + deps.querier.set_redbank_params( + denom, + AssetParams { + credit_manager: CmSettings { + whitelisted: false, + hls: None, + }, + red_bank: RedBankSettings { + deposit_enabled: true, + borrow_enabled: true, + }, + ..th_default_asset_params() + }, + ); + + let err = execute( + deps.as_mut(), + mock_env(), + mock_info(depositor_addr.as_str(), &coins(123, denom)), + ExecuteMsg::Deposit { + account_id: None, + on_behalf_of: Some("credit_manager".to_string()), + }, + ) + .unwrap_err(); + assert_eq!(err, ContractError::Mars(MarsError::Unauthorized {})); +} diff --git a/contracts/red-bank/tests/tests/test_liquidate.rs b/contracts/red-bank/tests/tests/test_liquidate.rs index c062fb63d..d94f2548d 100644 --- a/contracts/red-bank/tests/tests/test_liquidate.rs +++ b/contracts/red-bank/tests/tests/test_liquidate.rs @@ -746,6 +746,7 @@ fn response_verification() { mock_info(provider.as_str(), &[coin(1000000, "uusdc")]), ExecuteMsg::Deposit { account_id: None, + on_behalf_of: None, }, ) .unwrap(); @@ -755,6 +756,7 @@ fn response_verification() { mock_info(provider.as_str(), &[coin(1000000, "untrn")]), ExecuteMsg::Deposit { account_id: None, + on_behalf_of: None, }, ) .unwrap(); @@ -766,6 +768,7 @@ fn response_verification() { mock_info(liquidatee.as_str(), &[coin(10000, "uosmo")]), ExecuteMsg::Deposit { account_id: None, + on_behalf_of: None, }, ) .unwrap(); @@ -775,6 +778,7 @@ fn response_verification() { mock_info(liquidatee.as_str(), &[coin(900, "uatom")]), ExecuteMsg::Deposit { account_id: None, + on_behalf_of: None, }, ) .unwrap(); diff --git a/integration-tests/tests/test_oracles.rs b/integration-tests/tests/test_oracles.rs index 2fc5b0dcf..bd74cb96f 100644 --- a/integration-tests/tests/test_oracles.rs +++ b/integration-tests/tests/test_oracles.rs @@ -953,6 +953,7 @@ fn redbank_should_fail_if_no_price() { &red_bank_addr, &Deposit { account_id: None, + on_behalf_of: None, }, &[coin(1_000_000, "uatom")], depositor, @@ -1015,6 +1016,7 @@ fn redbank_quering_oracle_successfully() { &red_bank_addr, &Deposit { account_id: None, + on_behalf_of: None, }, &[coin(1_000_000, "uatom")], depositor, diff --git a/packages/testing/src/integration/mock_env.rs b/packages/testing/src/integration/mock_env.rs index 52003578e..95ab2e404 100644 --- a/packages/testing/src/integration/mock_env.rs +++ b/packages/testing/src/integration/mock_env.rs @@ -355,6 +355,7 @@ impl RedBank { self.contract_addr.clone(), &red_bank::ExecuteMsg::Deposit { account_id, + on_behalf_of: None, }, &[coin], ) diff --git a/packages/types/src/red_bank/msg.rs b/packages/types/src/red_bank/msg.rs index b80f7ec8c..f3a93e256 100644 --- a/packages/types/src/red_bank/msg.rs +++ b/packages/types/src/red_bank/msg.rs @@ -56,6 +56,9 @@ pub enum ExecuteMsg { Deposit { /// Credit account id (Rover) account_id: Option, + + /// Address that will receive the coins + on_behalf_of: Option, }, /// Withdraw native coins diff --git a/schemas/mars-red-bank/mars-red-bank.json b/schemas/mars-red-bank/mars-red-bank.json index d38a7dd5f..76cb7ca91 100644 --- a/schemas/mars-red-bank/mars-red-bank.json +++ b/schemas/mars-red-bank/mars-red-bank.json @@ -196,6 +196,13 @@ "string", "null" ] + }, + "on_behalf_of": { + "description": "Address that will receive the coins", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts index a48ed7d52..d6c5cad7f 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts @@ -445,8 +445,10 @@ export interface MarsRedBankInterface extends MarsRedBankReadOnlyInterface { deposit: ( { accountId, + onBehalfOf, }: { accountId?: string + onBehalfOf?: string }, fee?: number | StdFee | 'auto', memo?: string, @@ -668,8 +670,10 @@ export class MarsRedBankClient extends MarsRedBankQueryClient implements MarsRed deposit = async ( { accountId, + onBehalfOf, }: { accountId?: string + onBehalfOf?: string }, fee: number | StdFee | 'auto' = 'auto', memo?: string, @@ -681,6 +685,7 @@ export class MarsRedBankClient extends MarsRedBankQueryClient implements MarsRed { deposit: { account_id: accountId, + on_behalf_of: onBehalfOf, }, }, fee, diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts index b7502eb79..bb32ebe4a 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts @@ -636,6 +636,7 @@ export interface MarsRedBankDepositMutation { client: MarsRedBankClient msg: { accountId?: string + onBehalfOf?: string } args?: { fee?: number | StdFee | 'auto' diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts index 9552b2fdf..5595744e7 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts @@ -43,6 +43,7 @@ export type ExecuteMsg = | { deposit: { account_id?: string | null + on_behalf_of?: string | null } } | { From 2f4714fe3ea3cad9ec65473b20dd4ac9311fccdc Mon Sep 17 00:00:00 2001 From: piobab Date: Mon, 11 Sep 2023 21:04:53 +0200 Subject: [PATCH 39/43] Don't allow red-bank users to create alternative account ids. (#321) --- contracts/red-bank/src/deposit.rs | 7 +++ contracts/red-bank/src/withdraw.rs | 47 +++++++++-------- .../tests/tests/test_credit_accounts.rs | 2 +- .../red-bank/tests/tests/test_deposit.rs | 50 +++++++++++++------ .../red-bank/tests/tests/test_withdraw.rs | 43 ++++++++++++++++ 5 files changed, 113 insertions(+), 36 deletions(-) diff --git a/contracts/red-bank/src/deposit.rs b/contracts/red-bank/src/deposit.rs index 16efe17b5..e3fe60668 100644 --- a/contracts/red-bank/src/deposit.rs +++ b/contracts/red-bank/src/deposit.rs @@ -39,6 +39,13 @@ pub fn deposit( let params_addr = &addresses[&MarsAddressType::Params]; let credit_manager_addr = &addresses[&MarsAddressType::CreditManager]; + // Don't allow red-bank users to create alternative account ids. + // Only allow credit-manager contract to create them. + // Even if account_id contains empty string we won't allow it. + if account_id.is_some() && info.sender != credit_manager_addr { + return Err(ContractError::Mars(MarsError::Unauthorized {})); + } + let user_addr: Addr; let user = match on_behalf_of.as_ref() { // A malicious user can permanently disable the lend action in credit-manager contract by performing the following steps: diff --git a/contracts/red-bank/src/withdraw.rs b/contracts/red-bank/src/withdraw.rs index 8e7318886..890feec52 100644 --- a/contracts/red-bank/src/withdraw.rs +++ b/contracts/red-bank/src/withdraw.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Uint128}; use mars_interest_rate::{get_scaled_liquidity_amount, get_underlying_liquidity_amount}; -use mars_red_bank_types::{address_provider, address_provider::MarsAddressType}; +use mars_red_bank_types::{address_provider, address_provider::MarsAddressType, error::MarsError}; use mars_utils::helpers::build_send_asset_msg; use crate::{ @@ -21,6 +21,32 @@ pub fn withdraw( account_id: Option, liquidation_related: bool, ) -> Result { + let config = CONFIG.load(deps.storage)?; + + let addresses = address_provider::helpers::query_contract_addrs( + deps.as_ref(), + &config.address_provider, + vec![ + MarsAddressType::Oracle, + MarsAddressType::Incentives, + MarsAddressType::RewardsCollector, + MarsAddressType::Params, + MarsAddressType::CreditManager, + ], + )?; + let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; + let incentives_addr = &addresses[&MarsAddressType::Incentives]; + let oracle_addr = &addresses[&MarsAddressType::Oracle]; + let params_addr = &addresses[&MarsAddressType::Params]; + let credit_manager_addr = &addresses[&MarsAddressType::CreditManager]; + + // Don't allow red-bank users to create alternative account ids. + // Only allow credit-manager contract to create them. + // Even if account_id contains empty string we won't allow it. + if account_id.is_some() && info.sender != credit_manager_addr { + return Err(ContractError::Mars(MarsError::Unauthorized {})); + } + let withdrawer = User(&info.sender); let acc_id = account_id.clone().unwrap_or("".to_string()); @@ -54,25 +80,6 @@ pub fn withdraw( None => withdrawer_balance_before, }; - let config = CONFIG.load(deps.storage)?; - - let addresses = address_provider::helpers::query_contract_addrs( - deps.as_ref(), - &config.address_provider, - vec![ - MarsAddressType::Oracle, - MarsAddressType::Incentives, - MarsAddressType::RewardsCollector, - MarsAddressType::Params, - MarsAddressType::CreditManager, - ], - )?; - let rewards_collector_addr = &addresses[&MarsAddressType::RewardsCollector]; - let incentives_addr = &addresses[&MarsAddressType::Incentives]; - let oracle_addr = &addresses[&MarsAddressType::Oracle]; - let params_addr = &addresses[&MarsAddressType::Params]; - let credit_manager_addr = &addresses[&MarsAddressType::CreditManager]; - // if withdraw is part of the liquidation in credit manager we need to use correct pricing for the assets let liquidation_related = info.sender == credit_manager_addr && liquidation_related; diff --git a/contracts/red-bank/tests/tests/test_credit_accounts.rs b/contracts/red-bank/tests/tests/test_credit_accounts.rs index 8a5b42d70..edbce8428 100644 --- a/contracts/red-bank/tests/tests/test_credit_accounts.rs +++ b/contracts/red-bank/tests/tests/test_credit_accounts.rs @@ -17,7 +17,7 @@ fn deposit_and_withdraw_for_credit_account_works() { let funded_amt = 1_000_000_000_000u128; let provider = Addr::unchecked("provider"); // provides collateral to be borrowed by others - let credit_manager = Addr::unchecked("credit_manager"); + let credit_manager = mock_env.credit_manager.clone(); let account_id = "111".to_string(); // setup red-bank diff --git a/contracts/red-bank/tests/tests/test_deposit.rs b/contracts/red-bank/tests/tests/test_deposit.rs index c954ecc07..81f9928e1 100644 --- a/contracts/red-bank/tests/tests/test_deposit.rs +++ b/contracts/red-bank/tests/tests/test_deposit.rs @@ -598,29 +598,49 @@ fn depositing_on_behalf_of_credit_manager() { .. } = setup_test(); - // disable the market - deps.querier.set_redbank_params( + let err = execute( + deps.as_mut(), + mock_env(), + mock_info(depositor_addr.as_str(), &coins(123, denom)), + ExecuteMsg::Deposit { + account_id: None, + on_behalf_of: Some("credit_manager".to_string()), + }, + ) + .unwrap_err(); + assert_eq!(err, ContractError::Mars(MarsError::Unauthorized {})); +} + +#[test] +fn depositing_with_account_id_by_non_credit_manager_user() { + let TestSuite { + mut deps, denom, - AssetParams { - credit_manager: CmSettings { - whitelisted: false, - hls: None, - }, - red_bank: RedBankSettings { - deposit_enabled: true, - borrow_enabled: true, - }, - ..th_default_asset_params() + depositor_addr, + .. + } = setup_test(); + + // non-credit-manager user cannot deposit with account_id (even with empty string) + let err = execute( + deps.as_mut(), + mock_env(), + mock_info(depositor_addr.as_str(), &coins(123, denom)), + ExecuteMsg::Deposit { + account_id: Some("".to_string()), + on_behalf_of: None, }, - ); + ) + .unwrap_err(); + assert_eq!(err, ContractError::Mars(MarsError::Unauthorized {})); + // non-credit-manager user cannot deposit with account_id let err = execute( deps.as_mut(), mock_env(), mock_info(depositor_addr.as_str(), &coins(123, denom)), ExecuteMsg::Deposit { - account_id: None, - on_behalf_of: Some("credit_manager".to_string()), + account_id: Some("1234".to_string()), + on_behalf_of: None, }, ) .unwrap_err(); diff --git a/contracts/red-bank/tests/tests/test_withdraw.rs b/contracts/red-bank/tests/tests/test_withdraw.rs index 2d6584396..d5d7af200 100644 --- a/contracts/red-bank/tests/tests/test_withdraw.rs +++ b/contracts/red-bank/tests/tests/test_withdraw.rs @@ -17,6 +17,7 @@ use mars_red_bank::{ }; use mars_red_bank_types::{ address_provider::MarsAddressType, + error::MarsError, incentives, red_bank::{Collateral, Debt, ExecuteMsg, Market}, }; @@ -939,3 +940,45 @@ fn withdraw_if_oracle_circuit_breakers_activated() { let res = red_bank.withdraw_with_acc_id(&mut mock_env, &user, "uosmo", None, None, Some(true)); assert_err_with_str(res, expected_msg); } + +#[test] +fn withdrawing_with_account_id_by_non_credit_manager_user() { + let TestSuite { + mut deps, + denom, + withdrawer_addr, + .. + } = setup_test(); + + // non-credit-manager user cannot withdraw with account_id (even with empty string) + let err = execute( + deps.as_mut(), + mock_env(), + mock_info(withdrawer_addr.as_str(), &[]), + ExecuteMsg::Withdraw { + denom: denom.to_string(), + amount: Some(Uint128::from(2000u128)), + recipient: None, + account_id: Some("".to_string()), + liquidation_related: None, + }, + ) + .unwrap_err(); + assert_eq!(err, ContractError::Mars(MarsError::Unauthorized {})); + + // non-credit-manager user cannot withdraw with account_id + let err = execute( + deps.as_mut(), + mock_env(), + mock_info(withdrawer_addr.as_str(), &[]), + ExecuteMsg::Withdraw { + denom: denom.to_string(), + amount: Some(Uint128::from(2000u128)), + recipient: None, + account_id: Some("1234".to_string()), + liquidation_related: None, + }, + ) + .unwrap_err(); + assert_eq!(err, ContractError::Mars(MarsError::Unauthorized {})); +} From f1c3e60278cd3c89f368ca6e2bb7f9439dec46e6 Mon Sep 17 00:00:00 2001 From: piobab Date: Mon, 11 Sep 2023 21:05:16 +0200 Subject: [PATCH 40/43] Assert pool contains only two assets. (#323) --- contracts/oracle/osmosis/src/helpers.rs | 49 +++++++++++-------- .../oracle/osmosis/tests/tests/helpers/mod.rs | 5 ++ .../tests/tests/test_set_price_source.rs | 49 ++++++++++++++++++- 3 files changed, 80 insertions(+), 23 deletions(-) diff --git a/contracts/oracle/osmosis/src/helpers.rs b/contracts/oracle/osmosis/src/helpers.rs index c0937bb4c..3d6d9a67e 100644 --- a/contracts/oracle/osmosis/src/helpers.rs +++ b/contracts/oracle/osmosis/src/helpers.rs @@ -15,22 +15,25 @@ pub fn assert_osmosis_pool_assets( denom: &str, base_denom: &str, ) -> ContractResult<()> { + assert_pool_has_two_assets(pool)?; + assert_pool_contains_assets(pool, denom, base_denom)?; + match pool { Pool::Balancer(balancer_pool) => { - assert_osmosis_xyk_pool(balancer_pool)?; + assert_equal_asset_weights(balancer_pool)?; } Pool::StableSwap(_) => {} }; - assert_osmosis_pool_contains_two_assets(pool, denom, base_denom)?; - Ok(()) } /// Assert the Osmosis pool indicated by `pool_id` is Balancer XYK type pub fn assert_osmosis_xyk_lp_pool(pool: &Pool) -> ContractResult<()> { + assert_pool_has_two_assets(pool)?; + match pool { - Pool::Balancer(balancer_pool) => assert_osmosis_xyk_pool(balancer_pool)?, + Pool::Balancer(balancer_pool) => assert_equal_asset_weights(balancer_pool)?, Pool::StableSwap(stable_swap_pool) => { return Err(ContractError::InvalidPriceSource { reason: format!("StableSwap pool not supported. Pool id {}", stable_swap_pool.id), @@ -41,11 +44,25 @@ pub fn assert_osmosis_xyk_lp_pool(pool: &Pool) -> ContractResult<()> { Ok(()) } -fn assert_osmosis_pool_contains_two_assets( - pool: &Pool, - denom: &str, - base_denom: &str, -) -> ContractResult<()> { +/// Assert the Osmosis pool has exactly two assets +fn assert_pool_has_two_assets(pool: &Pool) -> ContractResult<()> { + let pool_id = pool.get_pool_id(); + let pool_denoms = pool.get_pool_denoms(); + if pool_denoms.len() != 2 { + return Err(ContractError::InvalidPriceSource { + reason: format!( + "expecting pool {} to contain exactly two coins; found {}", + pool_id, + pool_denoms.len() + ), + }); + } + + Ok(()) +} + +/// Assert the Osmosis pool contains both `denom` and `base_denom`, and they are not the same +fn assert_pool_contains_assets(pool: &Pool, denom: &str, base_denom: &str) -> ContractResult<()> { let pool_id = pool.get_pool_id(); let pool_denoms = pool.get_pool_denoms(); @@ -70,18 +87,8 @@ fn assert_osmosis_pool_contains_two_assets( Ok(()) } -/// Assert the Osmosis pool indicated by `pool_id` is of XYK type -pub fn assert_osmosis_xyk_pool(pool: &BalancerPool) -> ContractResult<()> { - if pool.pool_assets.len() != 2 { - return Err(ContractError::InvalidPriceSource { - reason: format!( - "expecting pool {} to contain exactly two coins; found {}", - pool.id, - pool.pool_assets.len() - ), - }); - } - +/// Assert the Osmosis pool has assets with equal weights (for XYK pools) +fn assert_equal_asset_weights(pool: &BalancerPool) -> ContractResult<()> { if pool.pool_assets[0].weight != pool.pool_assets[1].weight { return Err(ContractError::InvalidPriceSource { reason: format!("assets in pool {} do not have equal weights", pool.id), diff --git a/contracts/oracle/osmosis/tests/tests/helpers/mod.rs b/contracts/oracle/osmosis/tests/tests/helpers/mod.rs index 82bb4ea76..a022146e9 100644 --- a/contracts/oracle/osmosis/tests/tests/helpers/mod.rs +++ b/contracts/oracle/osmosis/tests/tests/helpers/mod.rs @@ -91,6 +91,11 @@ pub fn setup_test_with_pools() -> OwnedDeps Date: Wed, 13 Sep 2023 09:56:11 +0200 Subject: [PATCH 41/43] Add CL pool support for price sources. (#301) (#325) --- Cargo.lock | 8 ++-- Cargo.toml | 2 +- contracts/oracle/osmosis/src/helpers.rs | 8 +++- contracts/oracle/osmosis/src/price_source.rs | 8 ++++ .../oracle/osmosis/tests/tests/helpers/mod.rs | 29 +++++++++++- .../tests/tests/test_set_price_source.rs | 11 ++++- packages/chains/osmosis/src/helpers.rs | 45 ++++++++++++++++++- packages/chains/osmosis/src/lib.rs | 7 ++- 8 files changed, 107 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2309a7203..af441b202 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2276,9 +2276,9 @@ checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" [[package]] name = "osmosis-std" -version = "0.16.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fa46d2ad5ae738572887974e000934374ce3546b820505c0ee19ca708e49622" +checksum = "75895e4db1a81ca29118e366365744f64314938327e4eedba8e6e462fb15e94f" dependencies = [ "chrono", "cosmwasm-std", @@ -2292,9 +2292,9 @@ dependencies = [ [[package]] name = "osmosis-std-derive" -version = "0.16.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11c2ba5535743617d6f44ae8d572d064fabab6d06ffcf403512f89c58954dbe9" +checksum = "f47f0b2f22adb341bb59e5a3a1b464dde033181954bd055b9ae86d6511ba465b" dependencies = [ "itertools", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 814aeb606..71ea9291f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,7 @@ cw-storage-plus = "1.1.0" cw-utils = "1.0.1" mars-owner = { version = "2.0.0", features = ["emergency-owner"] } neutron-sdk = "0.6.1" -osmosis-std = "0.16.1" +osmosis-std = "0.16.2" prost = { version = "0.11.9", default-features = false } pyth-sdk-cw = "1.2.0" schemars = "0.8.12" diff --git a/contracts/oracle/osmosis/src/helpers.rs b/contracts/oracle/osmosis/src/helpers.rs index 3d6d9a67e..e3b612ca2 100644 --- a/contracts/oracle/osmosis/src/helpers.rs +++ b/contracts/oracle/osmosis/src/helpers.rs @@ -9,7 +9,7 @@ use crate::DowntimeDetector; /// 48 hours in seconds const TWO_DAYS_IN_SECONDS: u64 = 172800u64; -/// Assert the Osmosis pool indicated by `pool_id` is of Balancer XYK or StableSwap and assets are OSMO and `denom` +/// Assert the Osmosis pool indicated by `pool_id` is of Balancer XYK, StableSwap or ConcentratedLiquidity and assets are OSMO and `denom` pub fn assert_osmosis_pool_assets( pool: &Pool, denom: &str, @@ -23,6 +23,7 @@ pub fn assert_osmosis_pool_assets( assert_equal_asset_weights(balancer_pool)?; } Pool::StableSwap(_) => {} + Pool::ConcentratedLiquidity(_) => {} }; Ok(()) @@ -39,6 +40,11 @@ pub fn assert_osmosis_xyk_lp_pool(pool: &Pool) -> ContractResult<()> { reason: format!("StableSwap pool not supported. Pool id {}", stable_swap_pool.id), }); } + Pool::ConcentratedLiquidity(cl_pool) => { + return Err(ContractError::InvalidPriceSource { + reason: format!("ConcentratedLiquidity pool not supported. Pool id {}", cl_pool.id), + }); + } }; Ok(()) diff --git a/contracts/oracle/osmosis/src/price_source.rs b/contracts/oracle/osmosis/src/price_source.rs index 0b2e5b4f9..1a0e9f95e 100644 --- a/contracts/oracle/osmosis/src/price_source.rs +++ b/contracts/oracle/osmosis/src/price_source.rs @@ -612,6 +612,14 @@ impl OsmosisPriceSourceChecked { reason: format!("StableSwap pool not supported. Pool id {}", pool.id), }) } + Pool::ConcentratedLiquidity(pool) => { + return Err(ContractError::InvalidPrice { + reason: format!( + "ConcentratedLiquidity pool not supported. Pool id {}", + pool.id + ), + }) + } }; let coin0 = Pool::unwrap_coin(&pool.pool_assets[0].token)?; diff --git a/contracts/oracle/osmosis/tests/tests/helpers/mod.rs b/contracts/oracle/osmosis/tests/tests/helpers/mod.rs index a022146e9..ba18c9670 100644 --- a/contracts/oracle/osmosis/tests/tests/helpers/mod.rs +++ b/contracts/oracle/osmosis/tests/tests/helpers/mod.rs @@ -9,7 +9,7 @@ use cosmwasm_std::{ }; use mars_oracle_base::ContractError; use mars_oracle_osmosis::{contract::entry, msg::ExecuteMsg, OsmosisPriceSourceUnchecked}; -use mars_osmosis::{BalancerPool, StableSwapPool}; +use mars_osmosis::{BalancerPool, ConcentratedLiquidityPool, StableSwapPool}; use mars_red_bank_types::oracle::msg::{InstantiateMsg, QueryMsg}; use mars_testing::{mock_info, MarsMockQuerier}; use osmosis_std::types::osmosis::{gamm::v1beta1::PoolAsset, poolmanager::v1beta1::PoolResponse}; @@ -96,6 +96,10 @@ pub fn setup_test_with_pools() -> OwnedDeps } } +pub fn prepare_query_cl_pool_response(pool_id: u64, token0: &str, token1: &str) -> PoolResponse { + let pool = ConcentratedLiquidityPool { + address: "osmo126pr9qp44aft4juw7x4ev4s2qdtnwe38jzwunec9pxt5cpzaaphqyagqpu".to_string(), + incentives_address: "osmo1h2mhtj3wmsdt3uacev9pgpg38hkcxhsmyyn9ums0ya6eddrsafjsxs9j03" + .to_string(), + spread_rewards_address: "osmo16j5sssw32xuk8a0kjj8n54g25ye6kr339nz5axf8lzyeajk0k22stsm36c" + .to_string(), + id: pool_id, + current_tick_liquidity: "3820025893854099618.699762490947860933".to_string(), + token0: token0.to_string(), + token1: token1.to_string(), + current_sqrt_price: "656651.537483144215151633465586753226461989".to_string(), + current_tick: 102311912, + tick_spacing: 100, + exponent_at_price_one: -6, + spread_factor: "0.002000000000000000".to_string(), + last_liquidity_update: None, + }; + PoolResponse { + pool: Some(pool.to_any()), + } +} + pub fn set_pyth_price_source(deps: DepsMut, denom: &str, price_id: PriceIdentifier) { set_price_source( deps, diff --git a/contracts/oracle/osmosis/tests/tests/test_set_price_source.rs b/contracts/oracle/osmosis/tests/tests/test_set_price_source.rs index f0f38ee5a..11613758d 100644 --- a/contracts/oracle/osmosis/tests/tests/test_set_price_source.rs +++ b/contracts/oracle/osmosis/tests/tests/test_set_price_source.rs @@ -1036,7 +1036,7 @@ fn setting_price_source_xyk_lp() { ); // attempting to use StableSwap pool - let err = set_price_source_xyk_lp("atom_mars_lp", 5555).unwrap_err(); + let err = set_price_source_xyk_lp("atom_uosmo_lp", 5555).unwrap_err(); assert_eq!( err, ContractError::InvalidPriceSource { @@ -1044,6 +1044,15 @@ fn setting_price_source_xyk_lp() { } ); + // attempting to use ConcentratedLiquid pool + let err = set_price_source_xyk_lp("ujuno_uosmo_lp", 7777).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidPriceSource { + reason: "ConcentratedLiquidity pool not supported. Pool id 7777".to_string() + } + ); + // properly set xyk lp price source let res = set_price_source_xyk_lp("uosmo_umars_lp", 89).unwrap(); assert_eq!(res.messages.len(), 0); diff --git a/packages/chains/osmosis/src/helpers.rs b/packages/chains/osmosis/src/helpers.rs index 6cb9e204d..875d38a13 100644 --- a/packages/chains/osmosis/src/helpers.rs +++ b/packages/chains/osmosis/src/helpers.rs @@ -8,6 +8,7 @@ use osmosis_std::{ types::{ cosmos::base::v1beta1::Coin, osmosis::{ + concentratedliquidity::v1beta1::Pool as ConcentratedLiquidityPool, downtimedetector::v1beta1::DowntimedetectorQuerier, gamm::{ poolmodels::stableswap::v1beta1::Pool as StableSwapPool, @@ -30,6 +31,7 @@ pub trait CommonPoolData { pub enum Pool { Balancer(BalancerPool), StableSwap(StableSwapPool), + ConcentratedLiquidity(ConcentratedLiquidityPool), } impl CommonPoolData for Pool { @@ -37,6 +39,7 @@ impl CommonPoolData for Pool { match self { Pool::Balancer(pool) => pool.id, Pool::StableSwap(pool) => pool.id, + Pool::ConcentratedLiquidity(pool) => pool.id, } } @@ -51,6 +54,9 @@ impl CommonPoolData for Pool { Pool::StableSwap(pool) => { pool.pool_liquidity.iter().map(|pl| pl.denom.clone()).collect() } + Pool::ConcentratedLiquidity(pool) => { + vec![pool.token0.clone(), pool.token1.clone()] + } } } } @@ -62,13 +68,18 @@ impl TryFrom for Pool { if let Ok(pool) = BalancerPool::decode(value.value.as_slice()) { return Ok(Pool::Balancer(pool)); } + if let Ok(pool) = StableSwapPool::decode(value.value.as_slice()) { return Ok(Pool::StableSwap(pool)); } + if let Ok(pool) = ConcentratedLiquidityPool::decode(value.value.as_slice()) { + return Ok(Pool::ConcentratedLiquidity(pool)); + } + Err(StdError::parse_err( "Pool", - "Unsupported pool: must be either `Balancer` or `StableSwap`.", + "Unsupported pool: must be either `Balancer`, `StableSwap` or `ConcentratedLiquidity`.", )) } } @@ -279,4 +290,36 @@ mod tests { assert_eq!(stable_swap_pool.id, pool.get_pool_id()); assert_eq!(vec!["denom_1".to_string(), "denom_2".to_string()], pool.get_pool_denoms()) } + + #[test] + fn common_data_for_concentrated_liquidity_pool() { + let concentrated_liquidity_pool = ConcentratedLiquidityPool { + address: "pool_address".to_string(), + incentives_address: "incentives_address".to_string(), + spread_rewards_address: "spread_rewards_address".to_string(), + id: 1066, + current_tick_liquidity: "3820025893854099618.699762490947860933".to_string(), + token0: "uosmo".to_string(), + token1: "ibc/0CD3A0285E1341859B5E86B6AB7682F023D03E97607CCC1DC95706411D866DF7" + .to_string(), + current_sqrt_price: "656651.537483144215151633465586753226461989".to_string(), + current_tick: 102311912, + tick_spacing: 100, + exponent_at_price_one: -6, + spread_factor: "0.002000000000000000".to_string(), + last_liquidity_update: None, + }; + + let any_pool = concentrated_liquidity_pool.to_any(); + let pool: Pool = any_pool.try_into().unwrap(); + + assert_eq!(concentrated_liquidity_pool.id, pool.get_pool_id()); + assert_eq!( + vec![ + "uosmo".to_string(), + "ibc/0CD3A0285E1341859B5E86B6AB7682F023D03E97607CCC1DC95706411D866DF7".to_string() + ], + pool.get_pool_denoms() + ); + } } diff --git a/packages/chains/osmosis/src/lib.rs b/packages/chains/osmosis/src/lib.rs index cfa498093..8d7ed0ea0 100644 --- a/packages/chains/osmosis/src/lib.rs +++ b/packages/chains/osmosis/src/lib.rs @@ -1,5 +1,8 @@ pub mod helpers; -pub use osmosis_std::types::osmosis::gamm::{ - poolmodels::stableswap::v1beta1::Pool as StableSwapPool, v1beta1::Pool as BalancerPool, +pub use osmosis_std::types::osmosis::{ + concentratedliquidity::v1beta1::Pool as ConcentratedLiquidityPool, + gamm::{ + poolmodels::stableswap::v1beta1::Pool as StableSwapPool, v1beta1::Pool as BalancerPool, + }, }; From 06308d508a9491f6398ebaf1f235ce905b41669b Mon Sep 17 00:00:00 2001 From: piobab Date: Wed, 13 Sep 2023 12:50:57 +0200 Subject: [PATCH 42/43] MP-3346. MDR cannot be less than zero (#322) * Handle edge case during liquidation when MDR < 0. * Should be possible to liquidate even if HF decrease after liquidation. * Move variable to if block. --- contracts/red-bank/src/error.rs | 8 - contracts/red-bank/src/liquidate.rs | 38 +-- ...ank - Dynamic LB & CF test cases v1.1.xlsx | Bin 20419 -> 106995 bytes .../red-bank/tests/tests/test_liquidate.rs | 250 ++++++++++++++++-- packages/liquidation/src/liquidation.rs | 35 ++- 5 files changed, 258 insertions(+), 73 deletions(-) diff --git a/contracts/red-bank/src/error.rs b/contracts/red-bank/src/error.rs index 9e30ea748..54b1972ac 100644 --- a/contracts/red-bank/src/error.rs +++ b/contracts/red-bank/src/error.rs @@ -155,12 +155,4 @@ pub enum ContractError { #[error("Cannot repay uncollateralized loan on behalf of another user")] CannotRepayUncollateralizedLoanOnBehalfOf {}, - - #[error( - "Liquidation did not result in improved health factor: before: {prev_hf:?}, after: {new_hf:?}" - )] - HealthNotImproved { - prev_hf: String, - new_hf: String, - }, } diff --git a/contracts/red-bank/src/liquidate.rs b/contracts/red-bank/src/liquidate.rs index ff130947d..ed1dc1e44 100644 --- a/contracts/red-bank/src/liquidate.rs +++ b/contracts/red-bank/src/liquidate.rs @@ -1,5 +1,4 @@ -use cosmwasm_std::{Addr, Deps, DepsMut, Env, MessageInfo, Response, Uint128}; -use mars_health::health::Health; +use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response, Uint128}; use mars_interest_rate::{ get_scaled_debt_amount, get_scaled_liquidity_amount, get_underlying_debt_amount, get_underlying_liquidity_amount, @@ -217,17 +216,7 @@ pub fn liquidate( response = update_interest_rates(&env, &mut debt_market_after, response)?; MARKETS.save(deps.storage, &debt_denom, &debt_market_after)?; - // 7. Assert improvement for liquidation HF - assert_liq_threshold( - &deps.as_ref(), - &env, - &liquidatee_addr, - oracle_addr, - params_addr, - &health, - )?; - - // 8. Build response + // 7. Build response // refund sent amount in excess of actual debt amount to liquidate if !refund_amount.is_zero() { response = @@ -246,26 +235,3 @@ pub fn liquidate( .add_attribute("debt_amount", debt_amount_to_repay) .add_attribute("debt_amount_scaled", debt_amount_scaled_delta)) } - -fn assert_liq_threshold( - deps: &Deps, - env: &Env, - user_addr: &Addr, - oracle_addr: &Addr, - params_addr: &Addr, - prev_health: &Health, -) -> Result<(), ContractError> { - let (new_health, _) = - get_health_and_positions(deps, env, user_addr, "", oracle_addr, params_addr, true)?; - - // liquidation_health_factor = None only if debt = 0 but liquidation is not possible - match (prev_health.liquidation_health_factor, new_health.liquidation_health_factor) { - (Some(prev_liq_hf), Some(new_liq_hf)) if prev_liq_hf >= new_liq_hf => { - Err(ContractError::HealthNotImproved { - prev_hf: prev_liq_hf.to_string(), - new_hf: new_liq_hf.to_string(), - }) - } - _ => Ok(()), - } -} diff --git a/contracts/red-bank/tests/files/Red Bank - Dynamic LB & CF test cases v1.1.xlsx b/contracts/red-bank/tests/files/Red Bank - Dynamic LB & CF test cases v1.1.xlsx index e649148e784f0ba45c2eca2f972123cc01cd2bad..6dea6c06c6bccc3b83e2da9ec1ac5863807f90b6 100644 GIT binary patch literal 106995 zcmeFa2{@H&7dM{FL*`JX%w(pJX@^Z@N+D(1n^ZC;Q^w3gNoJ9Vib8fNWZG;A$q*Sb zkD2Fr+WULZ8Q=Fkr}SL^tN-`ybM<=boa=hJpLPG9wbs3U_iwE;>bQ8c`-q5$_C1#1 zJ-_cy|1bc*ceNJ2E z_i@=WMudMLPwVpfbXvmF9f!rH7P2-|XX}`^3N@qg;23{Jjqi5w^_(#W`HWp|46l@L z8_cgt#pqt8?Z)dv$Fm!X)7huoP10XcdETkGcv@dhx=)O3wE6Xcp(9TtrJoVkt2yh} zXO1h+pCn3fe^-#qJuqI5n}9pGfYxmD&pS@CqL9S2F2&|gXSR;Na=9<-8Q}{ZxigzM z@4bD+L-z+s#2zh};QOMgxFt(0g+9F0U46FnLyKv3H*FOTvv!5RTP3&ZpqJbxrBN0t zF4I2wS8~|(xfN*V=jBX=^1rGB0@7uKYW~h`v`IO?K|_M zrWC`P5-d}SV@(N`DUV@I36?33V@(N`DJ8I`1k03?SX1uRlrDA-RyVF%SUBDg-ub`X zWavVtL#G7gmM_{*MLU)z zIc~hUj?H8rR#=v{8f%-o;q77?wQcsaol~|+#s??PJ!HNssCIvYY>2)y zI^SI=@Z8M(LDp>}F=C2VljY+2!$D@Xk36;xhtI#>yza(8`hl>&a;4kLW%JAGldCMk zf$8%iF-+#eDsq>PPHM9*coKSXZ#sFU!MIJ#ZelciF;!5HaLSXF=a_D{6~(+bcn&Rn zIELyi^lNV5y!wgcgXx1_F4d*ce&f-%`!K~Hmn)kq&j=W;^-WAqqYWUlU&XbXx2K)2-RGCLF0K=k9!(zI9z^64PUIcV*yE zlD~!==Ata0y-0{}f|v1R=2!2U)&orB^WBfOlH?_k$wP@(6^kOKMRo5!x<+PBLx+?K ztP$1dgm);d#GkoK+`@3Uwb|2>Y_5oStwRQLUYtG2-G3`cin1qcK#JOgah3bg`7zO? zh7SXlXIgb=qL+f61e>-XLOk>a#s<5RZ-y_Ond4}@Ml=&>G=G`scp&BR#|i=$@vM3^ zGZID4_~WoYz9k|;v=!1@!Ep3FrKngLL&u$Wbqsv(i9{XB7!KWWY@(?&pn4xHvd@1m zlRJs`J)UUbEzy?@?q4-N4-{}tjrS2);PEowYY#L!aU|rx1?eO2@lE)|7a&R&-3&ydct1jw5rWd-uZJAR3_a&{`Iyu4eR@Ef00$UXIUSw zbiQx&ym&dWKjmXyUFTTjXN&yI7P!|||H4Sl71a3K6i;;(al1}LYask#{F@K#@u_hi zL-U&x>jkDhl3c0J(aeoLlAGUXO?IxMH2i^cW#`!Q+**yI{8;1nkGfYP^Mxj-jPR}& z_jwV9Z#}-EaP>GB`H3+2D;bT7@sDppj!3n2F)vCKnVcdOw~Snm+IM*#|7=HO;XIxy zZamL){kg>Zu`zuD_DT5B#&gc=rL`}DH&b5>3nsiyV(WfLG_B3)bBN^51+Eu(_iwo> z!hL;Bxp~b*PoIHRiaixkTK6;!RD>K6QIfFQPeZ#YrU0+z(>T+obtaKoJ20DvzVKyK zo*hr!!;Aw09PzqF-kN++zP3UgejaG#ES~Uq{*M5GDB25SXIz>EQ>-Sc=%?jywyqrn- zVw|rhV&4!RVQGCg`GrQ0X&}wmk#up0MDD_Ko{OB3`DQB5Mfk>|I6l0#2@DsSHA)eA z!^Otm^U7q%knf9Pi&8>k{8D=pBYvnP2`$SZUPnIcN$?#*|;rG4W!4p$NaCpsEx zj=I`zNeDd^otwBx%c(${B0dp`!dQi;i~W&nt5Rg0LGM)kP_3&P)}EEty}@VhsJJ~9 zAcMMSQzLnDHO_N$v@k6pW9zVq@b|xdIs|!QV55n`(>fRLwkSmvbYTXbH6t-fBRtgDu zVVD|JhjNaBsZ65Nz8UEu4uWc3Y-(rRx!%9xUh(K~?=M7nmJ;2= z*ay5FmBOiC3Fd2ax&DB?;RNmVkO;o7Xak0`Isxew{G{C{RCIK2wht!8n%q^6PLqK@ zYL4Cd z#GgX4se^^}p98bd_MgKV)JaexlmvY=u0bO1QLO>N$EH;LZJ)5fRG(5P!qv z?L!TJZ1AWsJx(E>%NQVag8$JFzR0T&V>RT|_1)J?aOr3+hUMmR!XFW7mOR%wX8u9) z`U8(khhFNS;v;)WsajMCI9wiDYq`;rJ(UeqaMh(}Qnd~><42|AEXCDa6l8l{h}SCk z5dG4)S38tk%lN>T#le6^oSC-<+@fUBoc8FBl8VSHbWr1voX{njZ5w1~Olal;Z+H|a zF8pg@;ZHW$NlSJL3lF-#I9wj~$)4bEUpZ-yz%3$soS=Kzta zntMyv$49~+_LQwaw$=ubTbm2LnANori04{Rgeqm)Cj@ z1idi=+1jegDP5fq&ROivPwp*sTbf;SG4)v3szR0qK+vm@n~V^TttClQuY>5>VMyu5 z_(rm~CkMo9X^I{K*_!uW@09e$j16adt_Zu|==EF}4u@(jNEnw(K!37g^Gn_JE4l4~pJrj;1<*G@YxbdIT)F>B99Z}i%r z_vY#rYjW@VbXKoJGo~Zq+B2AN%vyK8u<&M1;s)kRPA3{(y0O{ae;$*Q*oJYj8~fTa zA}N_J>4XWu7npxgZHiv3O|t!n7?TG%>270$Y~ z7(Vk5-H&M-Jl~6+ZGd=h(oTyn_HJ(UdU;VKV}`Ypy$-rlVt1n~P}g2H}~Z64?Yi?ni2u zs1rH4pQ&ArOr+(0qGoY6@fdfu+Lfk6GVZjROJa!v+?h3(UnN$bipe>etXG=pVX*dU z-g3C}Lsd);fAaa#91j>X9eVTGBE{QYuj;K<2T5WL#i-E>54BdBXXGGqC2N??+DwQi zI(a6%OFe)2bby-W+54Zl!_=&r?k{rt)m#<3|B?H4&9zteC%NaHh8C( za5nv8OtA1&g$Hle0`Rl_IA4Xzk_*o5t0)Rz( zS8t))=x-Jbp#|(-cvX~~KnF6)BC9)1F<-CbOow|ziu0Rc-(@3Jt$+HP()*W!nrAz- zDy$#}^3$7V*Vm5~uTQp&%>?C49gBa*9inzE^8OI_Kba@8Iqh8EP|=5=7-9Zo{nBg? zqqV~Mo5LF)2wUO(6_&26_J>&}kIAlD7RV$81`S$Dd5#jXPKto>aJcJ2zew@RirM!R z6=O7y`s$Y81zVGggl)57%mh*GoT6%)Vo_a>Mffz2Ms++EwbYc3>bWFxNRvD2%O%kZ zni5gnPzN_hk_R5=*AVj-!_6P^W5W4O&&%g{n68yc3wTJ8*+G~B^PL%b*5R(6(qwkt z8yppeGGyE@)yyIjE^)tAgP%>%<$j}P-jv|TT~Y%ROVHrXtucF*V9i|=GPF2KE4B7w z9zI^zgH?Nm9#1K~kZZWe#YiH4hz=5!A|T{`y*m z_$%F{AcGO20OnBs)%O#ACh2ARNg)OkL@~^bIUn-yc4ifFc5~(ai-!VI0a_8~CZnaE zXx%?It-G32>iGP@weuxrI%YeCOo*DUTNvn@xYmIotrr(6{Z!{Jo#EG zTPv}7n$t(l7azuEfJf)Y1>-Xyi1jO<^&Q4fKzoo?C8l@=Avc$(9y3 z)-92KQS=ziS*k16mIIdRoja; zUqlnsX!mZeZ3Sc%SSO&P(p@$;(j_;85_`AC7IM6A=FapktqrmE%w;z8JfPUJD;-Qn zZdDBpGxwGj52l;e&kRnuc(|L-NXa1wUF;f=YMWo(z1BOuKbrI`bT7GUQOvA$Z>`CV z2-~f0wVGmv$C8`U%ey;A;*bywdO(um!O`@NXVGdY!G7wg3#h5XVs(o5tuG1Vi7>}G^~U8yqn!ps}`Zc&Fay2qPH z>uJ3?H~3lFNvou}@0hfvR&MjaYiS#;qA0v&BDz}vFe(GG*jrCvhYZLGDoA7LHF5@* ztIm17wYzRk@su@be|jUI+tbcAB%Yi`3r_><3dF-BU1@?|cymhDn*lSc(A=vvZpX8b zzp$yWHox}X3KHYxQ}OJ7Ls3E3_c`7L;=;3d1l_zienQbc<_IDH}dTJ2Qk z!pOr}#pUCCu7@@fXp8T7YlfOs%a3dVwim{zH!->oB%c`83OF~Wd!j`v?A*lX6H8it z&7;Ro)M?#r9)EpeN{hPvx4+C_Kf&L>(EM7P$V5}tSFEM6j_xGHjlBiOmOT!x ze6_C{BF8Mc09#uWeXyiB4G1JKS_=z(o@CjQv0g~Ybj$hMEPc5(Bj-KW#9KPG{fdG{ z;=FHcEPF`xuJ~3EINqdcDm-Yxp=kL1G>+_!H(JyP zf~1yg)!`1i7;OhrK9{2pPN#o(=6vnJ#q|1TE{7keq}NzFUwojSUT5h-{q9JH!actf zLS-1@EzI%nkH*YT4AXtg?)AO-C=+t=L;yPUPmW0{=6^eec*AfQ4ugPDywQJq?8txv z>DT48(`7BJ*(Rg2IR;6>O%P9Wv|E1`SA@O(q z%{wmXe|p)cnKLi@T&|dm+9%XY&Lx|Ut{t;TZ(dwR*G=VMP9$@$PLrv;JU0+6ZSg%D zQa<2YgP%yMLZ2lRZ;U8{xt7oSjyZLqT@!@PU$mXsJl0)qDQ!CJ?e%D+nKntgxz@WU zNju8BfhfJ5@A~N^vGlHI_D7Tc*}=fBmk;^Y;ir-6(|;6dHx?;|-7H^}w)K|Pj3Z^r z_wpGGAWt9jV4hqaFRf_ve)ihyti=+KHbxdwvZgdU?cz-&?@?Zc$-ZysPR@LyQY+-# zc=Y#t1O&H9x#_Ki7>p5xFy}vi$PtBIu?0(Z+H;CQ98RS)OPh+qr^dIgyRGJH`${4) zLtmTIDK?Pq(z^L8$m`EHUj+OS$^Hp2|GaI_YXyT1k@$0pZKlr`3@Jf`;XtdMLUCh% zT3yk?sYJ;(R-1=29laRzSOe6%V68j-%Z3aI(j(=jtBbdngu9jYJq|FoPAATtJRfZ% zZZf(K?M|9GD2HrZ2yv6vNcM{C>5}tYUbfR_E%9C{ifibS_gwmtS+wApX6Qc62-5y( z0sB|Fvwvvv*aqQ6kD8G+zt2;NzzH+NnEP164>Wgi62EOLI0mlx|7w^R;X*OE&%Y-|Aqr#B5z?g%13=$@N-F>>B)o*OhiiVcCv=o z4=13KOwYMKs^GC4>Fja|vLX>qeq6G#X-~1yoY;!VR*T)b9W#g2T|_gk{|uMlTr z&=6`j5-Ef^l~4M1j%2mA-avhciH$u>BU zuKiwx_?xbz>uYq2xI&{mMvdVGDaqFrAVa=hAMm&0N!L%%FZu!h!MHKJFeTYZ0Wvv& zEQzsqc^hLtl1~lzNr(Cs?U@(I8q|h&?#Q_N`v=;MCCNvS{y+c9G>&im?MLD$fxpDJ z-bHJMJdV<9>(X?E3)lMQn$;KWYH5Fulp9Tm5OKFE+cnx8}x@ zr)SW!6W(vPhLM}CqB6xLBM^^nIVKEOd-{3z0*Lo8YUZTWcVz!+Fd!Zp5E1vN}Q}>^Lb+$qy_ksm03Smbn=!jLO zOVG?y`>jKR&31}Nh{t=^B|*X7689B*dDGdzhdt|aMczH7?kmevYfATfqksNy=PT}t zE?7vU2LASdkqM6mVMkfnhF;oXhr`Kt2by)(J!d=W2iIgA16K)VdYH8btv0fnatwan zwx^~|UKn&CU!|=oww;>ukdturdPD)#G%(AGGx8qo?lU$kXmS5Lu8C!?@q zfrdNUVY8OBWK$$*Atr2NumM$o#N_vFv6i9}AqL7T%agLZHKU*L2@!=xBLyqTW&N~& zJ$e6=Gmke!%<)nx1+)2^5%U zrIoe5D`>)O%X@sw@&8wP__n}EreKAY6+Bzo3z?Ws?<-!m%k2a6%^N06J87yh@ALg% z$(WH5OAIoP&=)9Lo*Sk8zJ${EExtP`0sa3z&CM5Yyn)O(lRQvN|E-4m&)*~UN5OVO zk%IquwxN$3>CV}@nGYr|WoNvm8j5!69^18t^v`@ZJ|vN(H$8KBo^&|9_1TT_B!cu$ zmJSI?bm=XYH#(B=Zjdh1kqH?X{ZA)&bH&QLX9`qQ7MX4=*n$Sipc`}cH+#1FK!0yP z(6^fcUEB#K%pbZyRzb9674Ugu4f$QC!GHIYzq}!(=lun2zgs!|B%MC#i4Lv%&sY zUklg?0~~!{J=k#r+*V&Fm|Xil313Z^c>Df5UmKWAJTIOOaVjlTFxZT;<;Vwx&wji_ zI#j8gP|e`f#%H0;IE5wBC(&fg0{ckI*%1r^IO^qM2&Sxk{N+axhqG|3%B2x3*83RC zxe61z8QErB&l>z(co*8!$-yyX#F#Ybf{Cgp*o|wm1Fz5jhT;lx0C;s z>({>(13PGNp!L=p7_$L|#H}zGO*>iMt!x-$J2^KAK+^N#>yV~0K#vDIP%a%IQPlP4 zCDx%y<#l{M;CO2~hx{HShVZX9O8Co8BS^9ctjaDT$g>C;%Ty4=)&z!S`iKM8gfsz1 zjw(d^X5j0Pr5=W!2zH~yXFIAG?VmxcLo2O!UHW9s_7h9Nd?Lkp!s_G&zrWr)ISAu2 zz}NRX4ihvWZ1p<;<7mf|@Dqgbw-e;~g(y-)684j7QO`Yge4+leH6>R_bpBzZnluvAJYSMXWNH%CGh zbp3b~STj zx&YQmVE^rMY6PtSZf!XUf;ww|X8BbjVX#pbEjElw54r6lnvr^d68mwrKh@7KHSH_-VheW$!A}#LEZKMLN;+6WFpB zfcV|~`jEU!IJyjAQV@Jh_JfW|QqVDJ2tFoJpktB_bWG-h??$!uFcWbiXpOSGvkF*q^M_SigphsF<(Ea2u4oN18#_g(_9rzy(_;nWS zB7f5?{#5N4yWM|MfBu9eSyJ+WkkbQxH3TxtoB^d<@6hyO$3Hdp)tDr$mS&L}5EoS(H8@}e z6U_2kLf}ZCxZ80hD(KotI$_%F*gZNA=$D;tXVx#jW`Ju2XU+;bfS{5nXF(jwE7NZ$a7Q_}!ard*=Lr#eW*MRlA?IYRwl9O1eF`(`CzdmXjL84!A z*no5bCTD#s3_-kns}6ymhmvTg$*Ul5CtVIqLh$-$`11y21n>rC1o8%D1n~xE1oMVu zgz$zMJui@LtfKCxh$ZTE_B)86b`H`*9BwVUXh1jt$F=s?L=ajB3L*~Xl|Mu1T8Bo5_O0&qnC z3Bjq=PeN;PJq4)Hu#j0BNdp{yxQhXPCj3BFfD?j6qD-Y7&%Nw&`+h?><0Nq>oNyA~ zy+X@?R#8_mTJf_Yt&*-%w9;oKTBt5G8u}SZt1P6qIpX8B*?$KL7&n%A_PdNn*jA`;I6Hq3fEJZ3I+z~j95l{!wJ_-*vmk2 zCCT7DZFL5BYYF)JN!9)Bipohq^1?`PT@!Y3dv)qq%l)|J&>JTqKzN0Ki(wy7!z%@- z;nfSQ@CtZ3^N?9wP<;t2xEWm;s2Lq0s2SZa3c0yn!u2&EMudP0VQp|BYz9(>xC>z# zP$8TQE`;BK3Sk~_Aq?OI#6!t13OS5?V=r*nM1uRWjG%%JySTm95U`fpgXNGEixWWX z2RGp~1=n$+Ky{q-pgK;Fk4yoe#%f`3V>KMqSpADa4&#}!9V2?dh46b&AuI_lguxAm zVW2|T4qONWt`3xfS^ywI2x^++4el`=ySN`OnC$WgSj+vm<$zKX z#tWb~Dna$?rl7hy?Be#!T4A^K$N4_sXr27)@`t=41 zS?^KvVhUEQ#!}WlsiDDA)_a67ma^Wva0^^E!W-NJ9d>a)&I&d#vSSTwnX{6h24|n?qz@;^Ka^D?RHX$ik@BRIiQOFeqpyuJ1(G#|GRn` zWr8@gyemBiRPo<0>@KQc*;d8>O|Vb1>|2MB+>xGdtK$Dz*o~2GY{0BxxKNnxHbUQS z7$^%w%Cn5RhwLjVpGFXFE93tr+V?MkiBBHbk)H#~_hP~Q~2=9)6YwSSTT1klyuZf$%M#Dn8X7QlNNaQa4Ww{_UL9pBEpd<$q4 z@wXlp4-xd%fhF*rZVW)9h=1;Nu*+=isuVzh9CmTPx5U_kQ^#)WPBW1ovYa>TXcf>m zWmjbj7KV2zOVGmWE@cTy^aAcZQtaH_1;x_=eN%RY-JtoBU12vUp%qZZuaGa`>61}*UaqL2ehP2NB? z2eZ@n3$&NrZEp$|=&+01?Y{a$1(Th|uvp9O!E!*UsTVj#Ai*t4+}`P70hX)7E^fEy z?~k)qSj+vm<+e2x+x$ma_iy2$vt{ zz_66{t`gwKFCk(n>%E27?fEj`R$sv_Nn#hbr>yB0-c~GS{pZmU;TI; z3`<#m8{hVWH^G7HYNp^;bFh^49wCgStoIDzUT`4{ZqkIMtbhFO+s>^)EM@(zH{>4F z5#UzAvBc{hh*vCS{iji@KU7i2Qr3T&1pDLJFqRemaaMSDyzeh+IV@%Uml@SRe#n7< zn<0WvSuZ24J7lOk38JB&1j7U`yg~^LQ0zY<&q*}t&32CFz=4O8Eg>_t>)V5!|B!VT zRs3!8v~WAQ`@ag5|2BF04_PF#d*Mb_>7_hIKI2J z|2FWP-a4q{J9P1A^fwhSC_B5g3hL7~L4De9nqm+(7MNoHr{Uy(sQ>o{6X0oEe~c3c z;b|*>8UX&gRDY8EZ@rL8U^2ka`G4<=i?0!5dY}Wid|eM4Coc} z-KhlDa)0e^`(2g;bVY-JUgY;xJ`eOIF+ir=QFfpU)6z1H&A_E0}@ zj`&3(2k45D0XLisE`)z?9uGL`lDY&s3+>`ZgCkC)$^)Cv!ZPH>3NF#o49uEqH?Z8c6 zf1h^_N*Ns7Q-3eG5Z=Ak0@jIX_gV{R5C%8h{6!%Ll$wyhF~S?%m!4Ys^8JUpkThM5$?46A#uo zeOD6?TDtj*LJpv;d$E)?#a>ykEi7e?ppqzOK^)rY%>ux!gvfl0@_7kxm zOIiDCA_%Q_2Q6;Pl)p>z^6TD`z*5#fd9cP()<5orcgJF5DeIj!5qpsfVkzsrR&%hH z+k@qRQWKW4{)ti($cg{|Ta-1%qPaezT_Qyw)b^Q$dLC^ld+<&6V4rtr`lc40=Dr%Q zq*e5!XXgR^vg!YrvU|B5H?Qm=;$R-2>&9^}@A-j&;`^P0{yubC0vMHzzwDolIIukv zn{+wwp9hMsHsEfdlQE|3o4uZ%N7_MJ;N0)BT`*pE;&-8KH~ddB`yJVC9gyex$iexo z{iYDO%f2P>e;rZ2Qiba&Oa+rPz$vQu`+)L9I7Rz@ca(lR!S?KQeD{if9eF$g>eJdl zeOhuj;kpTXnJwbyVNxIqT#=(#EP>wYhT%_=^Z#w?ymg=)jC7LN5YA{F#EhT^`1f-KBoq+26Rte1RU&h$)w&2p4nE+`*V z5Go2i4m|;tgDOCkp=wY~sLs&Y%AGb+kZ+;_Fx=9w3ps$+Hr*CGgZr}IYBV5$4!gMR zjCn7gE7o#*upDsKSOL7rZ=wfqy!yAs+n@w%?BaI2mi}Niyc;C2+Zu)-{z1!a(|DVI z(1IX^>)*uFAcgC1N&t|;^*8Y}NEza9Of)#YOQr6z&3+#W3VH{~fviODzRDM-=!H!=#lxF4;A zVJ)|}qxA-maekBRgM{JV?mvK3ll&&x2PuO6CfNrmLj?Db^X>ivNEsryC+pow6rg2@ zzbNE@CT9?!{0ad#;{-Pe`%OFz4urkItu%jQqCtW%$oVF?OwPC4wjg=UUlelNl=T{x zvPQ7%^o7S#)&Us-ynz{kyg?a3yulg4ydfDOyrD+Vv6MC4Z^M{>oSw&0*1LlZ_H;L= zjJm%Qq)QiA%6fN}#t&8`u#`1SFw1Xgchm=#6+X>&jV+YzIok)eaW)e6qwJ^Iud#=+ z#~t0h;tg(P7~~2)Xp6&A)&|=`yPatoKgexjS>gXHEr+G7|Cj{0rwkEGSp(wfJ!Obk z${Gd;vVQ#THc;EdQr3IC=!d1Oe;jgH%G&46w)Aw*H`TGM@E%y<-ARz(R_H-1TVpBf zpFCP)S>Zjf!n*@2z@;`pyCwk~t+AB##O|Qgy~t>g;O;Ayvfg8c2-+mKWp@8UoKX+FEoE^>q=Y(^? zx#2u;UN|58D4ZWI02hP{omh4#e%;Wf?rthcL<;B2^5^Nl;7ZksQgJ=l*6>cr=W4T;T(t%z^byo?)s;^|rPdZol4K^coW?MnSHfF3Sr?SH;svz=rH6*S8? z>0e?`c^9Q7MM~d)w9CIpM1#|s^A2Yg=SR*-PBJb5E)6bgt~*>=TpzirOzy@lZvlp* z;0Mca-Zz28;Rk}XxSX80W%(9LI@DPMVr^9JEPGZ!*zD0_V#9vpRZ97QBm_M_k%J+;)-BgqUFNbK-0+l(-Z5`m!ue3_*j53iTI@QnlAmFzBL0gnD12?qj>OksP>ZjCs)bFWVslP^=3kRuhxONA7T;-U*3lfCe!m5KL*k?dlU6r~!c5!>! zSFGiJ;*0PAKz9w)FCrb>F9N%`ANO>-Uj){2KW;f7M$8vdt)+%l%YYORO(fY(UsK(z z0x1WZP#L-n?g#csWr!K1BrJz9LbxNi*qluuAZC_RmQ)yZv7IOfm54al6F|{_uASJu z{|Hk+ls%mgO0R&}1s<>3uF9Zhpcb#zuEucM;B@@y_R|dN2I}$ZcT1Rrr<^$El^G*k zK+5S#Y@wy##(sJmXwOTKQp8^vbVeO*K%#RV+?Rb0?icn3)GsU(+%K#J)GsUt+%HTI z+%F8=p!2JOuD_|&8%UgTzyfiv@KiE-zPGn@>nwV;HXO1!?Ty^p+_0<6hoI+#d$&fC zF*Dl8&8cBKlf|NB0^XB9soN*PVY9}^5(Vdfg!d|f}Y7R_9zDhSw z6hl~8pWZ!ZI~^q;#IvU4Kq+2STs@Gs`ljH9I}Fm51ydhpW%APbIv(;Ak6GTk4hRldw#Wt{y8tfueqC!UFktXR-(Mx_wCU+ znU|6w#^janV{m($iZ;xT_kFx_H!>5l8N;ppWbWn}n?cr_=-hRSk}0D5?Gw^{iX&fj zSr#TGEOqbo!xAS4n$ZQ-E8gjygCz`UG|A8A?AD_8yGNB}j!LeTW?|y2v-|AtJqh}H zWzz`b5h-5opBL$T=%__TB-|Q>qPK{ThF_#;rt7fq;2Y#WG=9F;Q!TJy%GbyyX`>@l z)rtj5n(Fpq2S2 z`KfwBjpi=;A7+P!d=6IjgVlBUcM!`dkFqI}5Xt#9CKbrtDK%T9;=sM*6&KNd2)B~q zhzdCau3vevjVC2Ed&x5`bh zmUREh$AhLc^y^7Z&W{N_d#>GYaDJ1zQJ|Vvp%5`S{$(sww@N7cRRvHnj1%sy_iw$S z;I5z|e&u|wo1*9)nu*C*`vOO~-k3SNH9voneG&d~?ai}TJQiJd(w0Dx$8{dC7XHiW z6Z*p9OFhnuDylPtHHOTS-9}PZi~C;qi}&O*I|^~dUOJ3AlONwP*s7XHv_Jj4!{N8Z zA=%AMD5-?ULS6Eb!Vyh@(MOsLRmaPDue(Pl6dcRKtGYG8kkTIdWh_V{54CW6oO7lz zwTP;zGNf-D%Fe*nV4!!2^d2!04=q8p`|*=Fq&l~X?+PoCa6BH_U>_=C@u3=jr6O{& zClhDhQwx4e*8Nt(J*g%extEO=NyqeLp39`Ze`$Z|iOTbZ)v6WkD`GKWpC7$;rGC|Q zgzo-*-GM=tWu{9PIOy(Qp;bS5Q~Lr>;khGhM~{6DkfAg`bLc&#ZlEY$9GsM3IFLXo zp-B14h3CdMwb^xv`RvDH-o?kHCH$5nP8a zJflkDNF{yAe(*!Eo7|URvFyZtchX{7UX3SemkwUho0?1d*e+o9u-3A%ndeM5&zUZs zGp?p7U5#%D3fWamTA%Qdy(^NE5O|x#%QQ=O7n6s^HbikjBOIJ!BCowb*i71pW z!Hi5cuP@vefollSeHt~Ry8BtoefpTL=@4&ejZXV@HI2-dO<$2RhIk`aE(vtWQcFL> z6rS}wp0;YM0;gdUJ^ReMVD0T}X0_;`laqSJ68loGm z&F61j)sli8Ynm|kR1O{Ut0vMXO2+5p7NJ+=IK^Og`4jJ~`|Gi^Dg&!R=@qhM6$2bW z%BCA1@5{T9vC0ux-FxR~UKQNTNBMD>?BcsHC&vSgBN6=z)iCKRUrC8+zK}k-(JEo9 z8YLh0ao%>nc)~k88|8Di{fIrD-BV4I4+9=ZuyB|ZaiaylE}~Pf;E)?*o{+kxzd%zr zT?K_W*=0xwv6CCjbc0CYJ^0@o&9>qhA=?$!&#q4 z=L_n2x+1k%KH-vc{In0nY|4J!+f?MX=sz;vSK>DJ+B=6I)We0<-{G~+yH1rW^4E=T>=DBwk zsYJMMpDy!11>%3qMs~vSf6GSn6`%>u9E$m8DMUD%3G>uc2h#E-87qBBrbi73yHV-_NxaIQ?E00 zD+H&wC5YHhR6ld?6qkVM%g&sKyO1 z@6b$cv$Q$$S(MihS(z%p=k<`~z3;q&!JK)1FSB=xGhaegZT~YV4bCcq`#Lom{^%O> zJiGD+e`KBQ(8u;LnN|o51|v$N1yAWpqiHiZawT;>+u&n-@PmT^lRt zWp2u;Op&!fts!4Dd7>Rp&@!HvU(G8jg6LCpefg~CuoQ%J9y@30!J^x_dg>u23vFN{ zt;oGxNM6wRRz9Xmwn&rl;3cW{$DKp3A@vJey+w6%CbDT-WuF(e(~%9W(esNF9;U4j zE<=7Prb|Ui3oDGvRMT{yR;aeE;MJywiaMEe@uU zY$ioVv<*i}^4OaaPPKc_;`V3LRz0+5tZKtC=B&CTAvm9|pmS66j6Q2D%G;ujdq6KV zJzU&wTwXtQz%V6-$WHl%tDxcms#NH;cf|^buSrT%{-x}TRBZVEr3#}bOeoA#7;xWJ z?=x?-;B)u0{49*?6FojJ%=&_n<6Wvirq!WEC%y;4Ex>}RJ7+_PL-%SXz5HS z$*XkI(g$i>joKelbngkiVzJQIjzAp7nfHM>?qjWiI7Q|b6}Mcy5%5ixxL2)DW-^C_ zGVAPAeMF4aG+<`;dhg#if4J%(JL=M!%kiYaP%w8aPv0?_cI>hgZv7L$(?4%;bNyjv0UJwGQ$ZXA7xo1st;WNo9uOgp?9yF9CzFj+<$zo1rtY-KD{yVgfU%9LC^B5y)^8yP zbxb_6(uUceg0Za0HO!&IjvvQC4ah}^Z49{-dWyybvWnbNBb7WRxx1OwF;>@^gP`# z4WrHD3#tYp-1G~plV&VQ&-9j=EHmV*bUf~**$+g<>zh9jbd^j98sQM5Gm)o}wwwjh z5wWz@t1A7J+v&)I{I$3CnY?0ydQPetO94KjY|gY|dbBU%gDA=nS9bG}Q)cZ4tvq;` z?46&swRJv@uUAq*D7x|=5q%`yqnc z$?u?rvLfoKPBxR zxxxB{qG6nrk(I}I;)3>_x1}zFwD-wfJ{y@`y7N|&E;QP(f2u0g&-OrCb+%WsP2sYG zk@AT`jD_?PkdVYeANcw1BqTQ~9lqX$<^Tx^zdKVnp1^YDz0c+`*LmvPxh-Ch@>K=U zKGYqNAoDAI4Rdp&YpaYI90(O!k}8M^4E58h_H$YI;Kelm91e&%zGNC)$P%S-KQ3(nfWE1yT};r7-_YG^6y!PD)ZHOKYtPq$E0f zPf2!S@_$W9p#MKfNm|ZTKiP9ia{ND166(Nrt(Ez@)vd5Ur6f3QKuRLBlageqs}nf$ zT_$Q8YtFWH%IAzlLPmloS@>6(x)HXDD*Or(t1;(z3@1Jo??nD==GK%m;=H}3m8r6P-Va$W-dna1lSfwXo`xEESti(Bt#tF31|ly8~7KkQVIWAX!XCnF|wz4MYBW5tc+;5oa> zsD17WWmcp4m5;7rMyyNvT%)g4O<$SsLVFB|mou#Z<&lpTh&ZfU@GEaJLFiRs2Q@>gtl%STP3!~Y3WaY^S@-=U$v_NSbW*u=g$&OeCUUlnf%lH9d|%}m%0PlgghUmRg3{D?BFaZ`->aH4 z8HiIS4pKW6`5leMNqK!zk=en*2P%EZ#^=cSDx*#j*kE_N2u!%ULj>kxZOuSALm_cT zvhQqD(V}NmmtOALK|ac->UZb#?)1B)evPj!{VKF5^U0YrP=4l3R_uoUC(pA_i{>&k zR;DJ+!V+HBCX6ErcM=lfX1DiO8$LOXO&aRABJN00*~H>f{&hXqOrAG3ADvMbL_yq8I(=qoJ2^8sVHHc*|>x2kn$y%QuVb^ zf#GBR<`-;l62XpRP%T)E%VTp6W#pqoZax2?#Zj%X)D)m?WaDB zGk?r9kU>SYoT39#E@xFh|UGaH_K&mm|C&K|q>w%r`(qDU7)$cRxv(zMpXcuI8Oj!J5I_ z|N9aw$dD%C$nd#a=(R-%Zmm1*p%O~mI1`6E9i*bN%N?F4?%Bu^MFQi{xVYLhR zWER$KoB8TFWsW4PWU`%s_#NO^^ySgR#$yTM9Pc`nj@?#kA5$fyO~&#uR=O!zuX<7O5E6mj%SI7J(fNlb^JOmFA%Y4ptXa+`>zm zRbOomR16AL*&8SKV8qv6ugSu>KQ`WE(rg&CKN@?&KBs(MEU2!NDe#KW-Lc~m9h)vu zQc{U2hi)uHkG(@roE}~xOI#5r3#VeLh?!TClr#6Tu(+IOX-kq_dQ0VzF&mX$e`LW| zK?dgocl$~1zxd*xd6JK{G9RiG34!`&`V;}waq|M!bGzumu;GOy&Xxds+6XoV#dwkV-be;)UJ}buD>Ye z<^>mtyS%;x_9(5R$Nf=LqI}d**rh#fwWdrX4KV` z3uiCf&UivseyvtCLCC8h>Zy3q|Eb<9gX&t=ZG#4PcLKrP-Q5Wg+}+(>6WkqwyF+k? z;0XlR1cE!k9o~X{&I#e>p0{iNcvY{dqE^A0qel1a?$5@Tyk0ZI820YN$5D9)7J?jX zn#Bb&7MOxcGkX|#T1R7g;cmPhw3?sl+!Q<1%#@^36-h|R2O7;+TwPYYcVW#5V72b* z$sEX2i$OT1JWM98!l3T?A{^sM(!U!q6r`G&cd<`9k05jjAR(J=69*u3ds+s zNN8qo9gJ|!v5VfL>0L9LZc}1gmdz{ieoPUBpwVMV0Gtx5f9{k#TID}FC363tI3-6c zd&K|PDPj4yQ!+31kSBSWnBntjYQe!Cv%ru3Q_%uN^Xxa}o&p=@Hz{zYTraHHIP z#6ub-pJ|lnYM#}o3oyTW* z#vb}nT}2kgE$_3Wef6J=s0BUYF_;{>v{%O4cf2GLI1)hd?`>#g;3?-!@=utsG!Z9X zg}O>#zSjO+qu(eCcbMp*h@+5vlc9Xb3( zezJG5W>@sI&uyM@ThDsiPckD_U*2e|xxtpdFUPVaG~(JWdGB7lZmqSYHy>|$y4^?? zYgfjzK6}yNI{U6@My>mhOd9UX!Di##ZSMYs!j#&aWm2{7T+lH&yw=AbgF)E53qmIK zhjHNrmPyLg=dZCP=D-k?4F zvKhERWlk+sCamD0>0fXym0GJ8GDjXw(FmRkaFi-!DMB0q?@V=9Fd}te&I*OVByivi zFpvPp;Qq2n2q@FE@(|TBUZsqRh5^%E;fk=}_UmOrf(UH_cP-@#ze}MP3d7TZaSsDA z&u?NcHyOvx7Tf{>lx#Ky%c-L(;Z;=$;!fxR)f$r)b!_mHF-~l7hdw~cf*%J?>8vf#lZdRO`)iBc5@6^M_=S&ZpmI zi{He(D*B{rd;H^gqr!M>D=^nndSqj`T$PqzX}J~eSl3tUh~i+_O1+}=ltRC;VaxS3 zjW08p7KqBodYiQ)<-zb&nfiCf>++3+E6_SVL3PXRZvsZf)vNrWc$=U^ITP7Agr#<& znB{ipolAT1P2or1CMJu*A!zgxDu8y=GNTwQP z5H6;;XcqGoU#*(Gw=hyD6miA@!=D&eFe+CcBEbQ=H?Va|$Aq!qECCfm2{x*j0ZNIP zjbEwRn5KT8)qjS%yu@FhHM7EZ)-`m^>{j=7>n()|0V)$+_A9Eraf~;`Y8+9;%eBlT zm1_0WX$~B(=~~A}k}%OsAg;`V%QbQ+DLs33gF2exKB$Y-s$U|0akGgUOBxJb;2bU< z>(R`?vf5dYd2+OvQSuj%&9S}4(goLw;e@*$%=CWH z49Nniw57>Hl4PMFza zk9{>(DU*GV?Q8h>12BG?oZ)wmbG(a`hgxw~?)MNC(F6TNWRPjwFzc9Bm$mb4_)sL^ zm9_6zUq!o`9k%a|I6TATQteJu4p(Dh@1cqG(j8Ag=+t9Ys0V(Gg+Diw`7I9d{dk*H*D=Y33YY)!rIjqIotZ@7sD|&PLgu-_goW$c z2d9N0$i?)Jjj$8rn~g;0wrtS@JO?nsT?MD-d@3Rzz_62G1Lfs>Q5E?h>vh>a=YcWW zFoX>62oKrQFVtjT2O);mZ*rqR9h#{!gB80V`;NPEr}SQGwuGHu>wO%Ecy|@(yix4N z-et5Sf&%M7zqmfCVAp`XR%77KK0G~tbvQSA&#ZO>{7?l!Qoe1B1%L6v73&{YK_1Q9 zpRD_o@c%PShLh@Bh<~ohn9Df!_{Y|r^My&j$lJ}V z&Y#$Lv)`TdeNhY(U8Co*Sn>1{qv_*~wYzaToWEhG^@@WBXfgoi-Id*o4G-VAT-^#U zhKeTrhMv8j^ih*>Q`_J?)c2stkgszt+5ocxXfkZO=_s|jA2k_`!>k2V>^RsluV(v1 zF}*IMcoL}9&-^y49TZj#+c-H=xfG-r{K8XPEMhnaMxn{(TS&60XuQ(!3Sy4rMGm>Y z74!k@JMNe9wF^ZYqY(YMK>69vx%aaQfGP-WKs$|9+~@vqx6yWl|5QORHkMCydr~9F z-EpOr5o?9ej#G{#tCBVfR(QCHwL|1x2MvzAwO(J}T-s5gmt$R|eCNd}0dl5y=4`&| z&M-aFz{(JvWVrjV!CS07HUA+xxU~qH#?wk>jecZ5rWXt|vTlHeXD@Qd<6gOmo2Q?C z8?+SrK84p=XA$x?#4Gk-=*qii3M)cX)y>4Km-ckT_>|be#N4z#YcR-(C5&C_W-N#t zJO7prH|raT91Fj(pubsDdo`gJC!wt9#_fEV7&btZK?o3Kyu~r@vV64gJiVc=(Y%o% z6y`D2SCuiBl;$j|6>lPVs9PGZ6_W?Ok-jDWoLUW;CYhikOcxJVVS$+9vM)^kdH~@m zBnFgajo=o++JWH^2Bc@;L%+$0#Hl~?pEAfWADXZz8A293L{A9`u45RY9?T2<34NR- z#w|2J8RYH1${_D~R_B0&ucG1sWe}cqKm`Q)08jxj-x>}AD$%#>_Hk>k*S+zLt7geN zC|;*2>9JPvWzZxCltJXaC(r7CxUu6(a%QP~r8KNg8+uvpbb^3B;KW4{qj~F4S4^BI zMd_%r;Bj$txg+Gpo%j)b)nb3&Y-*6c6nUqt8X;j**=lWk)^aDI&dQK(gEe2hD|()D z^KC~A%ZOl!_N7Ce)h)yLf#ygBK41F*s`cbdbD-2$hk-u%@?g!EEt*Uyk7+ZarSE1tA_U$e=#j-L8ZyGN~|RTbu0IUo(AJxD2P zZ8*^+F25F!yJJE|+sVV1bA<6XMG%Tj(}V6bfvt2;P5}xSDtpqBCZ?!jYUI9S6j2}F zxXci&_YRc~n!N%R?NEhw?;|%vwL2C1lARz?sZqvT%KJ^i67`LGU{M*4xp}wLd$k>b zV(FO>1X+oSyphqv`k-IRC*)`?e+o_Nrf5zlups~7w5d?YDGIq=@zEG+ z+sF)by!rEyqbz6T=a~Cq4X|ni2APw|R=h#;k)7dUx>u9>ALi#tR1oMf5E<7;;rY&q zySWE4a~Y3c$p-3(jj6ip34U-8Wmt%YDNgAM*lU}TflEKG;3PgO#Ewzjzn7a=@rj+s zz1FPNGEBLiEsuEt0tH@E7O4T#&Z{E_geqbZsAEC0auaVoo~ucqdym7P-GRN$R7L_* z0_k5}M7N#!rNQ=sPsuEcj8sh~NmwS+f+CDhI7Wf@0Ck`tUsygM1?5<5|IRQ$tl8Hy z)?9oUP28`#gpS*KJES=jp{idEV9U6JkYUh6WeTCDL~Q`nL31MulP zFFJ;>%7s&Ee8(ABzX$2z2C`1%!GfsI24R}0m)FMYTMAc%u(`NH;a(ekX}mX5&GX7! zr`+$3Yv|osqaxprHLPAt%#rrunDC~f6Pbj4KRgw#Vs@d|M!mdoV#7;e1dvZ?0OS+R zkoDIk^UIH7&O1s6qJ4nl(1`ty9fwED`A5egFobDOB)}>UPe&{t-Xb>Ylc=>dO9&V` zjj`7{_FcA{3{i6|ygxIN-3K6|oKid~(jQ}KQ|>y1*`sV~f=EVCa`7e0QbfHD$XA_U z2?)rho^x=T~h;^OAF0rcT*$Z(UWoJgru^H2cCp^;9t?Dd7(Mn}plK+I`5 z&;KCi6nY}&#QjUmY14-colW&ATkrDGd%C@vy8XX94#H<<`#WR`O=~>Qp6sHKpykZN{p@yv`Y57Sp>n40lTp$!sGcLv7#{#&?X2 z9Z^Fi?U-Qj0ijIB<|f)I2ncAax`WHiH>dXZu0Xy~Nr6f0VWmHi5r3K;Zy{9}2rvN0 zaUxT;1O>j+6p7YXWsLg~*EyHb-pd=A%|y`5 z#by*R5E6hKk{3f0^AQ1oE{#0=T>t|4(mL@`Ld=+8T#(O^U}ijg<&Rd)J4CLw`8RNQ zbee*a_XKkq+2DL@2=&wLSjr$>?i;Up3YM{#-8`^-{P~3GHRkQn?!K2)ffr)IHCk72 zPoHwS_%Gs`H8#$wX?oT7xP!FpXwrJr`=6_|@FMM47|-IiEG`>&yBM=J@0I#bX9nc& zdim5@E-qS`JAee+U9B0qgbMTy7u_VM&52^(#U8f;Z!Q<_4fuFahy{QIHm{TCzvk9L zF!pB?O-Rf7!6`G z724^}9-JNerN*1|!PTfG0c!mnR6TixK46qaxp$BnAwIn$b8lwB}NjE{}QK z<351}djLMz#Q3vtIBkbAmJnB$}X)X(=A5Zz=LEFRNr{;DT2j%i^BPyi^4V3|c5pOV4HJ2dXb$K7=WTIlj7NATM5&!Tq<}2Ia4VW^bZv zXK0|{U}tJ&Z2xf50u|@2=J*~?T6e$Mkz61~3b<0ZEs7Okj5vkF71F70RbS}0P=DgV?E;vY&q+j4lMib+HKeLJkT-peFfsTTq(_KXxprpQ6-Fq9fLxW6gOg2uTp6}W7qnTY6L#Mxxk;_E2qU& z#{1MHSGS?*P-KyHaRB_%eN|x^8l=0sBM81G^?Du=MnwJu7M%6`D}LvRt_y1$HY~_u zkgxXk+1#apMqrx=+{C+rvjI$pIa1!@IK&AaF2~`eu^+_Bqm)@OmJ(Mno1s`7Qv!6M z?8r(%19s~aDQjFJF4}?9QYb25IhW^H5Lh2+#xip~e+IY(Nnu-U@xfHaiM%-cjGzc> zmQJ%b$?pTnG@@xjR10!v(4Hm(WmH@02T6l7el}_Uc_u+(nmnY7xC(?Kl`}RDu4zNa zsBvkDj_AJG9?WP>b}!Jwq`DDi$6P6?ScfCcmx4I2o}+>YSurJ|Cb(SFN_+zUyU;z5 z>Zg))&->QW5IorQP8qojgFzPe6q&xCfqa#2870Ly3M+aQmNB2{s1=V|jL zE9h3JRZf^l5FvlaPs*WS#W)UI8N$6haeH?Ukl(+A81@ghLI-~F;u`F~Z7kS-Z7c^@ z3&Y19wWgwNJx7Od4|)w3y{HZ%jj1S3!9qT+Rrij;w1UL8qxfE^ZYwjx%Snm=mmgM# zB>#|axB2+M%b91mb95zSbO}sS7&MaqzV0RPXU1q`p2*HfQ#VFn-?8@_@1Y{}D|?~1 zNbq0fO{4Hq(@pvEFHwL)mkqsyP^#;IDN>i$vtkO2f-z-`w=a|kmM`CZCGsP48e1R; z;w9V<-AQ=)x_8}TB!NhQZC^nBv&qDQ&ZB#6VC3HtRe2_%alj007`p1~NahZN$z@NY zQbq{{E~$YftJvZCUxyO}PniGoD;!W5A%-H6Cn=2bO&igR%5UA)@4=zpg&~b0FcU_N z439#j$wd`-x7}u3>C0=@>5M1S>#Q=Dg84Er@DO7Lyx5HlM4arK)o0NqP;mn%IsE|z zMo5;GttKXz#v-+s3bU>H(gPk@?P4GI+k_w`?1uF3E-j^2szObj#DK7qjNNP4El%H3 zwz!p?n~$iM{lrmE!F&IOoXZVch9sx*%e>6!c9~n!RT^Y+s@0qYt_I+11jos0SmU!}9X-Yjtpy*;dS!fakb9Gt&^t;KPplt-d1zt)tMQdoxJ#2}RjOjRY3 zb{=-^KG1PUSy38|=10m>!<-?SZDH}KFE8#fBZTz+5E8d(W~If*G+^QEyam!p8$3v^ zrDTTvxp-5k{=k13qL5=-XogFq}?L?f7wlDXDn+GJr114YF$$7f3 zTwePJ=I+hL_T818BUH1XFdV!-ylk%(?6Iuu9`4!&?zx=p#%@)l=5E?n`$Q|pf#`Ix z+2Rf-e&ct}p$N9NE=gSNZ(^`F26>WT4nz*lmH}OqBuou-Sw@;l*1eiT6c*f@6*;9{ zB?KsSb$9pvQIXYHcOW>l-)LplT{>&NzAUic9vOmP8tZY4LRh#lwqC$#X=I&|aOT=& zHic=MZMJ~&n~jl^ZWwQEM50T&G;_vY`q-R$y~_G(*u)lyy~ZMU z?#tlCy+1put;uK8c|X^TO4*0}HZxYsv|h7nvAr!)O9c^`4Xl?_qslpK*YVcS^+dP+ z&`Vj)8(q0nyr_&VvYH>-c30hD;?1=V;i&n{(F`~!D)uJ>Snm|6^#wntU8%w@grEv@ z>ciAT_fVR2LLgY;>d0`bm3X6O&a_E}i_IOTgzs8n1_bO*NU{R7_yMIjdIp5MXM69X zs8uaKj&9wqI(Fl{wZPYK)D5%3^2`Y7t$>@-@ZKNXqC05Tqq$i2_f#I{5v3q)p}Oan z{?UfkUCZ}f%n4(ig(&fTskQAj=7pQmVM`JD%6*?2YCt!&^eGTyuR?DUw;tI7F;IfcsWCqC zC9$k1Fws>$%r$fOFmGV(S4g^EeEWIRAWyt7$2)=j#1Cv{6eVM>gvOX>YltjkN_=5V zq94;fxuO&VoCGDFigSyI^+TX@A%gLVYYF(}30|>i>G*0+NvrJz#$|em!)8{d;FTMhmSIl4jkHtzRc_ zdS|s3q_#0g?5S`1>!jfcb`Sk$Vxsfm7u(DP6<+#ncA3033b?zB;%mM&UaWFvs+*MH zL+a^}Lv6B9qFy?^aA*fI5-)z^q?YA_NWFX9{Eg8mW&v~lqrV}IkSY-D=+BNuYOLM7 z@DrcGJ;k|`7!6Z1p8jQG^HozE;{_>3g7!g!cW->y;~*yxjg7`}K>(~P1Rx~)-+Swz0qgqLqW&xF;)nC2Ljd~<=%F(@dt;*;mQ_hlVgZYn zStm%yS72JwwYT>s96B-f^4KfCG|^QL{zR96GPB=K#{_k&6ANhitcCjpw7eQMX*HQn z&kM&|a$2^5Br5^lh7Ke>e;*zZ=T)vkJY!4-JCpHzBHp1S5$lFnvpDcenvwH`c5FG% zq|#Afj~r;7z3rd)sZCaLF0ULjMtkv5wS$Qzt?8gtNbZPvzrXLL%IYYI@#a!#eKgs* z)>#DAfQgU+ru)B7^&#j|7qqr=Ftl>eR&=#7wAXwLx)LO10DC7$wgdN-wfssQAAg}K zI#+^^5PJuLY)WB-rR&1YzJ z;sRfj-|M488@3-sZ{gyx8z^3;O5@Y}*E);}9=A!P17dG9a<+reYXd5`IoGNx)$mLF zWfqCFBut#JPiThR?~b~@u90_?C?aazW>f`s$UvkU3Wo?3?2NtpWI*MYi8C6{@ZGGr z_!{;Yw$$BsjC752pj~*`(H7w0K- z5wqhXeE~82uCA#K3w=Cd4ky3;akz!dok|ea&8D3WShYk>8~>?xz@M&*LC(V*6Be}e zu0_h@qYO4GglB?^1J_-9bIPn%kf2W0VQ(kg99NI8j99N9qh$Ql%r`cGmxT%S@WOzA zQC|G&{Cufr>JJ9^-%Eg11IlA9=kKF*0dygMKzU5; zJV&`FP4t%rd|wlwBl-PJ!$LlyJn8s-YsLGTbHJzFNx##2rTZN4 zS8MQdl&9@Iztbu)d5-dg*5khZ<7z!sDF05Y()2mtBdtGf?xzyp-)ZsKK1X>%>zB6q z`8=O$lzyirX!jiOk=7qi%Ttlh@3h8UpQAjX^-JCKoYvFg>hH8>+@1qI()#1^ds?0R zomQ3abCf5vewAgP(|Ve^{hd~w-*do6T7NvxPjjEY)7lPxj`D=oucYsDT2FI457ztF zI0$(Je4apk4)8RVe@F%W8Y&Tw0RJN~^c>-7#PSgH{~A?MeN~Xsl!ISV})xvke*ssa(-Tnz3ak zU8yYDlYLL3ED^sky7xErd;dG<`8?lqp68tReb0IeT?-yOVqro}!wx_o5WtJ$cxpKt zd#V{NYq_j2NKIis3kQF5yrlZg0i*e!A<5#%kd}+Gn~9?(_#Cu;ZcN zDQr9rgr-igcKr-0PPBIcFH@{rJA;Ei*MvcN;^cJ@L9tqs146OO9^jjQ5d~sk0O&!1 z1o~45C=#J4u$=CICX&Ka{gbOOs6?EP0+}fGVelmBwa7zh;_h7#L4B|d>y`*AV?>I1 zr^Hwx_e?KX_F#)x;xuA>25O%MV|5zpU2!_o&r?NPRos2u+xaf}$bnv`73?ZLe>1N= zo)>S^JX7&mx2z(AF~$8hN@~;#?}jzAF(m;f{$nVY%w|Z~^76amwrDzK_2>94}QT^G=<~bW5hT zzJb5#2D{!z=MXeYG@Ge_?@D{{gZqujF5}o5V>NvI(z}6Z!8oMf`Am$2PR@?1Ed>ihUW;z?tg=r3MX^X44(A&O*CAI z*KNL0-6TBhF@t&q!qoBt%C9x3**V+pDO~BeqjbNsEHcG*fVTHO zkKujGum^WVrpxG&w^cV%HCvh{ z%F^a(O@L`aobiZAmW!v`TAhWem=u4~?B5m{Eu}B2wC1$M(ku&avGdz8#}%4io%T9a zGq{pky=UI;hkQggJGtA+lUuq}0l+i%f)$=038|JFU#UE{dD#U)tQvtvZEkLp<-YRT;GI$)*W!LFDBEz~HwMX5A(rL(Uk1TAI%qzfd zuyW0(XNbi?1gMN22h`V_bl=T!^kp{?n~XMFk#djem~kdlm9&4ZIa`GPUo@;$87o z4EwO*qO)gzt8Gm8=y$yf$*(++dcreM`djzI)E^A7B~E8`+j6bQ^`^`hjPt1JXQ!&< zJH!bmxu{MXdafwHr){+D`@+FB>ceMqCKg(X)5{tYm{QFV?G>VfjTHFc-; z(2jw{wCH2b^;uo`^P(4*xYJ5YZwnFnWmm^~zP64OUxcXpi{&0si;i#j{)!GQr3r3vI zQR44iX48-L(5~r@ErhlPc3Ww~x-tE+;U*D;Z!K1eaX#0pYMd6+Y7xvnZ@#>C@`0nd ze<)QtR@C_KboJ>;S5LvGGlo|U%1|0S+?h=V7w7vFR{9h|OCP;eEvplGD7}!y^e4T{ z9UEmdW|4W(YlAMZWo~h5r}MqaqB)0(N&-4iDQHenn@UIj-kNDXwBUc#uiyHldwtfV;JBS}aia=)_3|=UA z$kAu`9=$~^VhPW*j)h%Y(@s7n`Gh8oeKBY)uHf~v)9l$&&2icCdzg}DG|c(B9p=0& zn?&d^jJuopNZEPnN-(ZCM9waIhXe&$j$eS0gT?Zyn+G@^oeYs@9Vh{qzZa*$hDNb=l>lx&1Jx=nt4k z&rJsXAngg_;PM`LQ0wDca;Odv0Oo0aueDSLD2eGdZjoW98y8*WZ>}a@uPrrqsNrJ^ zda@4cpoY_)8yUTMghoAR8ES%DNxO3Urq15_JuRh?w@=+fE1$|bYQfqXEw~K5qKz{G_+Aaw~$}Z32>@it^f*e4v@2vj{^mY#N-h$;$&fkOPDbhY& zCaJdA*Rb_Z(PMl5u0l0&%93+=_?9ZZXn`~hMSD( z7zc{QPp^+@-Y^$zp%PQ#479i~1zmNSw;heLZ^1yfL(brCxKtW^qOUJ$LiCE7 zx;zN~YTqDjBFa=^Qw<1&f3b+yd%JU@U#U}ezon)$!yQ3qAXKhv)0Kakh(t3(PNm-> zuDV~6D(7e}ye?(ITQ1(5l3AjgoCg1{)224nZ_|LNk6!=F;#NWUUt2`OI?t8KDeT^} zq>c+@LWSFMo4Yd#Z zoTKKOwY0=ub?>H63c-#yq%-BGS)lh>tREtcHYOY+%!-VQ@Lcg5#R-fZ-~1d13JCP% z`XWMphw7jtm$t+t-AN_kVL0etkcu5(2k3texY)96kPmw{8x$j#hHTxYuyp_DX1h-b z+5KYW{9`fWKt8Hw`qV}?j)Sq5je#^9+Lab9nLWdP!GRo9HY`|w94JdxRnH-@4y+`W z7dwgrnaIwi1A$4d1BwqTlml{}I8bO*XZJRw2LMZc0N@5FpE+)FEZ-cE4RSyhTzi*> zUB!W%RDmZBMM%RiCt2Dv7v!T3g9F$vIp9e${NAeL;UNqaKZGrFf5IT1ln2WxvN$dm zWIMbEk^~V$!(se-_+e1E%Apcz zrasJ2I**j0h$?_J%L7lq4~|1QL$Ony8%WXsu!(sfoQzTaj!1tbif7?0fPI$-@{%#i zFBuC31OS6N0G2TyJVrhXEBlQ>KFQ~LWfKAQ^ kxyg!@EqL@7jMo4gkqL5OKjecDW*&eCSR!3wv*7{rU!=6Dga7~l diff --git a/contracts/red-bank/tests/tests/test_liquidate.rs b/contracts/red-bank/tests/tests/test_liquidate.rs index d94f2548d..3fdb852ce 100644 --- a/contracts/red-bank/tests/tests/test_liquidate.rs +++ b/contracts/red-bank/tests/tests/test_liquidate.rs @@ -550,6 +550,7 @@ fn same_asset_for_debt_and_collateral_with_refund() { let red_bank = mock_env.red_bank.clone(); let params = mock_env.params.clone(); let oracle = mock_env.oracle.clone(); + let rewards_collector = mock_env.rewards_collector.clone(); let funded_amt = 1_000_000_000_000u128; let provider = Addr::unchecked("provider"); // provides collateral to be borrowed by others @@ -584,22 +585,223 @@ fn same_asset_for_debt_and_collateral_with_refund() { // change price to be able to liquidate oracle.set_price_source_fixed(&mut mock_env, "uatom", Decimal::from_ratio(2u128, 1u128)); + // liquidatee should be liquidatable + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let prev_liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + // liquidate user let osmo_repay_amt = 1000; - let error_res = red_bank.liquidate( - &mut mock_env, - &liquidator, - &liquidatee, - "uosmo", - &[coin(osmo_repay_amt, "uosmo")], + red_bank + .liquidate( + &mut mock_env, + &liquidator, + &liquidatee, + "uosmo", + &[coin(osmo_repay_amt, "uosmo")], + ) + .unwrap(); + + // check provider positions + let provider_collaterals = red_bank.query_user_collaterals(&mut mock_env, &provider); + assert_eq!(provider_collaterals.len(), 1); + assert_eq!(provider_collaterals.get("uosmo").unwrap().amount.u128(), 1000000); + let provider_debts = red_bank.query_user_debts(&mut mock_env, &provider); + assert_eq!(provider_debts.len(), 0); + + // check liquidatee positions + let liquidatee_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_collaterals.len(), 2); + assert_eq!(liquidatee_collaterals.get("uosmo").unwrap().amount.u128(), 1); + assert_eq!(liquidatee_collaterals.get("uatom").unwrap().amount.u128(), 1000); + let liquidatee_debts = red_bank.query_user_debts(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_debts.len(), 1); + assert_eq!(liquidatee_debts.get("uosmo").unwrap().amount.u128(), 2020); + + // check liquidator positions + let liquidator_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidator); + assert_eq!(liquidator_collaterals.len(), 1); + assert_eq!(liquidator_collaterals.get("uosmo").unwrap().amount.u128(), 999); + let liquidator_debts = red_bank.query_user_debts(&mut mock_env, &liquidator); + assert_eq!(liquidator_debts.len(), 0); + + // check rewards-collector positions (protocol fee) + let rc_collaterals = + red_bank.query_user_collaterals(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_collaterals.len(), 0); + let rc_debts = red_bank.query_user_debts(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_debts.len(), 0); + + let (merged_collaterals, merged_debts, merged_balances) = merge_collaterals_and_debts( + &[&provider_collaterals, &liquidatee_collaterals, &liquidator_collaterals, &rc_collaterals], + &[&provider_debts, &liquidatee_debts, &liquidator_debts, &rc_debts], ); - assert_err( - error_res, - ContractError::HealthNotImproved { - prev_hf: "0.66".to_string(), - new_hf: "0.594059405940594059".to_string(), + + // check if users collaterals and debts are equal to markets scaled amounts + let markets = red_bank.query_markets(&mut mock_env); + assert_eq!(markets.len(), 2); + let osmo_market = markets.get("uosmo").unwrap(); + let atom_market = markets.get("uatom").unwrap(); + assert_eq!(merged_collaterals.get_or_default("uosmo"), osmo_market.collateral_total_scaled); + assert_eq!(merged_debts.get_or_default("uosmo"), osmo_market.debt_total_scaled); + assert_eq!(merged_collaterals.get_or_default("uatom"), atom_market.collateral_total_scaled); + assert_eq!(merged_debts.get_or_default("uatom"), atom_market.debt_total_scaled); + + // check red bank underlying balances + let balances = mock_env.query_all_balances(&red_bank.contract_addr); + assert_eq!(merged_balances.get("uosmo"), balances.get("uosmo")); + assert_eq!(merged_balances.get("uatom"), balances.get("uatom")); + + // check liquidator account balance + let usdc_liquidator_balance = mock_env.query_balance(&liquidator, "uosmo").unwrap(); + assert_eq!(usdc_liquidator_balance.amount.u128(), funded_amt - osmo_repay_amt + 20); // 20 refunded + + // liquidatee hf degradated + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + assert!(liq_threshold_hf < prev_liq_threshold_hf); +} + +#[test] +fn mdr_negative() { + let mut mock_env = MockEnvBuilder::new(None, Addr::unchecked("owner")) + .target_health_factor(Decimal::from_ratio(104u128, 100u128)) + .build(); + + let red_bank = mock_env.red_bank.clone(); + let params = mock_env.params.clone(); + let oracle = mock_env.oracle.clone(); + let rewards_collector = mock_env.rewards_collector.clone(); + + let funded_amt = 1_000_000_000_000u128; + let provider = Addr::unchecked("provider"); // provides collateral to be borrowed by others + let liquidatee = Addr::unchecked("liquidatee"); + let liquidator = Addr::unchecked("liquidator"); + + // setup red-bank + let (market_params, asset_params) = _default_asset_params_with( + "uosmo", + Decimal::percent(70), + Decimal::percent(98), + LiquidationBonus { + starting_lb: Decimal::percent(10), + slope: Decimal::from_str("2.0").unwrap(), + min_lb: Decimal::percent(10), + max_lb: Decimal::percent(10), }, ); + red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = + default_asset_params_with("ujake", Decimal::percent(50), Decimal::percent(55)); + red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = + default_asset_params_with("uusdc", Decimal::percent(82), Decimal::percent(90)); + red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params); + params.init_params(&mut mock_env, asset_params); + + // setup oracle + oracle.set_price_source_fixed(&mut mock_env, "uosmo", Decimal::from_ratio(3u128, 1u128)); + oracle.set_price_source_fixed(&mut mock_env, "ujake", Decimal::one()); + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(2u128, 1u128)); + + // fund accounts + mock_env.fund_accounts( + &[&provider, &liquidatee, &liquidator], + funded_amt, + &["uosmo", "ujake", "uusdc"], + ); + + // provider deposits collaterals + red_bank.deposit(&mut mock_env, &provider, coin(1000000, "uusdc")).unwrap(); + + // liquidatee deposits and borrows + red_bank.deposit(&mut mock_env, &liquidatee, coin(10000, "uosmo")).unwrap(); + red_bank.deposit(&mut mock_env, &liquidatee, coin(2000, "ujake")).unwrap(); + red_bank.borrow(&mut mock_env, &liquidatee, "uusdc", 3000).unwrap(); + + // change price to be able to liquidate + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(12u128, 1u128)); + + // liquidatee should be liquidatable + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let prev_liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + + // liquidate user + let usdc_repay_amt = 3000; + red_bank + .liquidate( + &mut mock_env, + &liquidator, + &liquidatee, + "uosmo", + &[coin(usdc_repay_amt, "uusdc")], + ) + .unwrap(); + + // check provider positions + let provider_collaterals = red_bank.query_user_collaterals(&mut mock_env, &provider); + assert_eq!(provider_collaterals.len(), 1); + assert_eq!(provider_collaterals.get("uusdc").unwrap().amount.u128(), 1000000); + let provider_debts = red_bank.query_user_debts(&mut mock_env, &provider); + assert_eq!(provider_debts.len(), 0); + + // check liquidatee positions + let liquidatee_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_collaterals.len(), 2); + assert_eq!(liquidatee_collaterals.get("uosmo").unwrap().amount.u128(), 4); + assert_eq!(liquidatee_collaterals.get("ujake").unwrap().amount.u128(), 2000); + let liquidatee_debts = red_bank.query_user_debts(&mut mock_env, &liquidatee); + assert_eq!(liquidatee_debts.len(), 1); + assert_eq!(liquidatee_debts.get("uusdc").unwrap().amount.u128(), 728); + + // check liquidator positions + let liquidator_collaterals = red_bank.query_user_collaterals(&mut mock_env, &liquidator); + assert_eq!(liquidator_collaterals.len(), 1); + assert_eq!(liquidator_collaterals.get("uosmo").unwrap().amount.u128(), 9978); + let liquidator_debts = red_bank.query_user_debts(&mut mock_env, &liquidator); + assert_eq!(liquidator_debts.len(), 0); + + // check rewards-collector positions (protocol fee) + let rc_collaterals = + red_bank.query_user_collaterals(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_collaterals.len(), 1); + assert_eq!(rc_collaterals.get("uosmo").unwrap().amount.u128(), 18); + let rc_debts = red_bank.query_user_debts(&mut mock_env, &rewards_collector.contract_addr); + assert_eq!(rc_debts.len(), 0); + + let (merged_collaterals, merged_debts, merged_balances) = merge_collaterals_and_debts( + &[&provider_collaterals, &liquidatee_collaterals, &liquidator_collaterals, &rc_collaterals], + &[&provider_debts, &liquidatee_debts, &liquidator_debts, &rc_debts], + ); + + // check if users collaterals and debts are equal to markets scaled amounts + let markets = red_bank.query_markets(&mut mock_env); + assert_eq!(markets.len(), 3); + let osmo_market = markets.get("uosmo").unwrap(); + let jake_market = markets.get("ujake").unwrap(); + let usdc_market = markets.get("uusdc").unwrap(); + assert_eq!(merged_collaterals.get_or_default("uosmo"), osmo_market.collateral_total_scaled); + assert_eq!(merged_debts.get_or_default("uosmo"), osmo_market.debt_total_scaled); + assert_eq!(merged_collaterals.get_or_default("ujake"), jake_market.collateral_total_scaled); + assert_eq!(merged_debts.get_or_default("ujake"), jake_market.debt_total_scaled); + assert_eq!(merged_collaterals.get_or_default("uusdc"), usdc_market.collateral_total_scaled); + assert_eq!(merged_debts.get_or_default("uusdc"), usdc_market.debt_total_scaled); + + // check red bank underlying balances + let balances = mock_env.query_all_balances(&red_bank.contract_addr); + assert_eq!(merged_balances.get("uosmo"), balances.get("uosmo")); + assert_eq!(merged_balances.get("ujake"), balances.get("ujake")); + assert_eq!(merged_balances.get("uusdc"), balances.get("uusdc")); + + // check liquidator account balance + let usdc_liquidator_balance = mock_env.query_balance(&liquidator, "uusdc").unwrap(); + assert_eq!(usdc_liquidator_balance.amount.u128(), funded_amt - usdc_repay_amt + 728); // 728 refunded + + // liquidatee hf degradated + let liquidatee_position = red_bank.query_user_position(&mut mock_env, &liquidatee); + let liq_threshold_hf = liq_threshold_hf(&liquidatee_position); + assert!(liq_threshold_hf < prev_liq_threshold_hf); } #[test] @@ -1114,6 +1316,25 @@ fn default_asset_params_with( denom: &str, max_loan_to_value: Decimal, liquidation_threshold: Decimal, +) -> (InitOrUpdateAssetParams, AssetParams) { + _default_asset_params_with( + denom, + max_loan_to_value, + liquidation_threshold, + LiquidationBonus { + starting_lb: Decimal::percent(1), + slope: Decimal::from_str("2.0").unwrap(), + min_lb: Decimal::percent(2), + max_lb: Decimal::percent(10), + }, + ) +} + +fn _default_asset_params_with( + denom: &str, + max_loan_to_value: Decimal, + liquidation_threshold: Decimal, + liquidation_bonus: LiquidationBonus, ) -> (InitOrUpdateAssetParams, AssetParams) { let market_params = InitOrUpdateAssetParams { reserve_factor: Some(Decimal::percent(20)), @@ -1136,12 +1357,7 @@ fn default_asset_params_with( }, max_loan_to_value, liquidation_threshold, - liquidation_bonus: LiquidationBonus { - starting_lb: Decimal::percent(1), - slope: Decimal::from_str("2.0").unwrap(), - min_lb: Decimal::percent(2), - max_lb: Decimal::percent(10), - }, + liquidation_bonus, protocol_liquidation_fee: Decimal::percent(2), deposit_cap: Uint128::MAX, }; diff --git a/packages/liquidation/src/liquidation.rs b/packages/liquidation/src/liquidation.rs index 5362cd92f..54a00d5c1 100644 --- a/packages/liquidation/src/liquidation.rs +++ b/packages/liquidation/src/liquidation.rs @@ -54,15 +54,25 @@ pub fn calculate_liquidation_amounts( collateral_params, )?; - let max_debt_repayable_numerator = (target_health_factor * health.total_debt_value) - - health.liquidation_threshold_adjusted_collateral; - let max_debt_repayable_denominator = target_health_factor - - (collateral_params.liquidation_threshold * (Decimal::one() + liquidation_bonus)); - - let max_debt_repayable_value = - max_debt_repayable_numerator.checked_div_floor(max_debt_repayable_denominator)?; - - let max_debt_repayable_amount = max_debt_repayable_value.checked_div_floor(debt_price)?; + // All debt is liquidatable: When MDR < 0, it means even repaying the whole debt is not going to be enough + // to bring the account back to the THF, so the liquidator should be able to repay all the available debt. + // Given the numerator in the MDR formula is always > 0, MDR < 0 happens when the denominator is < 0 + // (we include the case where it’s 0 given it would make MDR = infinite). + let formula = collateral_params.liquidation_threshold * (Decimal::one() + liquidation_bonus); + let max_debt_repayable_amount = if formula < target_health_factor { + let max_debt_repayable_numerator = (target_health_factor * health.total_debt_value) + - health.liquidation_threshold_adjusted_collateral; + + let max_debt_repayable_denominator = target_health_factor - formula; + + let max_debt_repayable_value = + max_debt_repayable_numerator.checked_div_floor(max_debt_repayable_denominator)?; + + let max_debt_repayable_amount = max_debt_repayable_value.checked_div_floor(debt_price)?; + Some(max_debt_repayable_amount) + } else { + None + }; // calculate possible debt to repay based on available collateral let debt_amount_possible_to_repay = user_collateral_value @@ -70,12 +80,13 @@ pub fn calculate_liquidation_amounts( .checked_div_floor(debt_price)?; let debt_amount_to_repay = *[ - debt_amount, - debt_requested_to_repay, + Some(debt_amount), + Some(debt_requested_to_repay), max_debt_repayable_amount, - debt_amount_possible_to_repay, + Some(debt_amount_possible_to_repay), ] .iter() + .flatten() .min() .ok_or_else(|| StdError::generic_err("Minimum not found"))?; From f55cfb5b1869cad065cdae5194525644e9f6fe99 Mon Sep 17 00:00:00 2001 From: piobab Date: Wed, 13 Sep 2023 12:56:06 +0200 Subject: [PATCH 43/43] Use latest credit-manager schema for withdraw. (#324) --- .../osmosis/tests/tests/test_withdraw.rs | 26 ++++++++++++++----- packages/types/src/rewards_collector.rs | 6 ++--- .../mars-rewards-collector-base.json | 11 +++----- .../MarsRewardsCollectorBase.types.ts | 4 +-- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/contracts/rewards-collector/osmosis/tests/tests/test_withdraw.rs b/contracts/rewards-collector/osmosis/tests/tests/test_withdraw.rs index 40138bd30..9ce1f6616 100644 --- a/contracts/rewards-collector/osmosis/tests/tests/test_withdraw.rs +++ b/contracts/rewards-collector/osmosis/tests/tests/test_withdraw.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{coin, testing::mock_env, to_binary, CosmosMsg, SubMsg, Uint128, WasmMsg}; +use cosmwasm_std::{testing::mock_env, to_binary, CosmosMsg, Decimal, SubMsg, Uint128, WasmMsg}; use mars_red_bank_types::rewards_collector::{ credit_manager::{self, Action, ActionAmount, ActionCoin}, ExecuteMsg, @@ -55,14 +55,17 @@ fn withdrawing_from_cm_if_action_not_allowed() { ExecuteMsg::WithdrawFromCreditManager { account_id: "random_id".to_string(), actions: vec![ - Action::Withdraw(coin(100u128, "uatom")), + Action::Withdraw(ActionCoin { + denom: "uatom".to_string(), + amount: ActionAmount::Exact(Uint128::new(100)), + }), Action::Unknown {}, Action::WithdrawLiquidity { lp_token: ActionCoin { denom: "gamm/pool/1".to_string(), amount: ActionAmount::AccountBalance, }, - minimum_receive: vec![], + slippage: Decimal::percent(5), }, ], }, @@ -77,16 +80,25 @@ fn withdrawing_from_cm_successfully() { let account_id = "random_id".to_string(); let actions = vec![ - Action::Withdraw(coin(100u128, "uusdc")), + Action::Withdraw(ActionCoin { + denom: "uusdc".to_string(), + amount: ActionAmount::Exact(Uint128::new(100)), + }), Action::WithdrawLiquidity { lp_token: ActionCoin { denom: "gamm/pool/1".to_string(), amount: ActionAmount::AccountBalance, }, - minimum_receive: vec![], + slippage: Decimal::percent(5), }, - Action::Withdraw(coin(120u128, "uatom")), - Action::Withdraw(coin(140u128, "uosmo")), + Action::Withdraw(ActionCoin { + denom: "uatom".to_string(), + amount: ActionAmount::Exact(Uint128::new(120)), + }), + Action::Withdraw(ActionCoin { + denom: "uosmo".to_string(), + amount: ActionAmount::Exact(Uint128::new(140)), + }), ]; // anyone can execute a withdrawal diff --git a/packages/types/src/rewards_collector.rs b/packages/types/src/rewards_collector.rs index 149490eb5..c7b0e2d3a 100644 --- a/packages/types/src/rewards_collector.rs +++ b/packages/types/src/rewards_collector.rs @@ -203,7 +203,7 @@ pub enum QueryMsg { // TODO: rover is private repo for now so can't use it as a dependency. Use rover types once repo is public. pub mod credit_manager { use cosmwasm_schema::cw_serde; - use cosmwasm_std::{Coin, Uint128}; + use cosmwasm_std::{Decimal, Uint128}; #[cw_serde] pub enum ExecuteMsg { @@ -215,10 +215,10 @@ pub mod credit_manager { #[cw_serde] pub enum Action { - Withdraw(Coin), + Withdraw(ActionCoin), WithdrawLiquidity { lp_token: ActionCoin, - minimum_receive: Vec, + slippage: Decimal, // value validated in credit-manager }, Unknown {}, // Used to simulate allowance only for: Withdraw and WithdrawLiquidity } diff --git a/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json b/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json index c9f4e9ce1..4a4e229bb 100644 --- a/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json +++ b/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json @@ -338,7 +338,7 @@ ], "properties": { "withdraw": { - "$ref": "#/definitions/Coin" + "$ref": "#/definitions/ActionCoin" } }, "additionalProperties": false @@ -353,17 +353,14 @@ "type": "object", "required": [ "lp_token", - "minimum_receive" + "slippage" ], "properties": { "lp_token": { "$ref": "#/definitions/ActionCoin" }, - "minimum_receive": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } + "slippage": { + "$ref": "#/definitions/Decimal" } }, "additionalProperties": false diff --git a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.types.ts b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.types.ts index fc16e839c..a526497c2 100644 --- a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.types.ts +++ b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.types.ts @@ -85,12 +85,12 @@ export type OwnerUpdate = | 'clear_emergency_owner' export type Action = | { - withdraw: Coin + withdraw: ActionCoin } | { withdraw_liquidity: { lp_token: ActionCoin - minimum_receive: Coin[] + slippage: Decimal } } | {