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 000000000..5d50903e1 Binary files /dev/null and b/contracts/red-bank/tests/files/Red Bank - Dynamic LB & CF test cases v1.1.xlsx differ 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[]