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