Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contracts/red-bank/src/borrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ pub fn borrow(
&deps.as_ref(),
&env,
borrower.address(),
"",
oracle_addr,
params_addr,
&denom,
Expand Down
1 change: 1 addition & 0 deletions contracts/red-bank/src/collateral.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub fn update_asset_collateral_status(
&deps.as_ref(),
&env,
user.address(),
"",
oracle_addr,
params_addr,
false,
Expand Down
15 changes: 12 additions & 3 deletions contracts/red-bank/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use cosmwasm_std::{entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response};
use cosmwasm_std::{
entry_point, to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};
use mars_red_bank_types::red_bank::{ExecuteMsg, InstantiateMsg, QueryMsg};

use crate::{
Expand Down Expand Up @@ -177,15 +179,17 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result<Binary, ContractErro
}
QueryMsg::UserPosition {
user,
account_id,
} => {
let user_addr = deps.api.addr_validate(&user)?;
to_binary(&query::query_user_position(deps, env, user_addr, false)?)
to_binary(&query::query_user_position(deps, env, user_addr, account_id, false)?)
}
QueryMsg::UserPositionLiquidationPricing {
user,
account_id,
} => {
let user_addr = deps.api.addr_validate(&user)?;
to_binary(&query::query_user_position(deps, env, user_addr, true)?)
to_binary(&query::query_user_position(deps, env, user_addr, account_id, true)?)
}
QueryMsg::ScaledLiquidityAmount {
denom,
Expand All @@ -206,3 +210,8 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result<Binary, ContractErro
};
res.map_err(Into::into)
}

#[entry_point]
pub fn migrate(_deps: DepsMut, _env: Env, _msg: Empty) -> StdResult<Response> {
Ok(Response::default())
}
23 changes: 17 additions & 6 deletions contracts/red-bank/src/health.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,20 @@ pub fn get_health_and_positions(
deps: &Deps,
env: &Env,
user_addr: &Addr,
account_id: &str,
oracle_addr: &Addr,
params_addr: &Addr,
is_liquidation: bool,
) -> Result<(Health, HashMap<String, Position>), ContractError> {
let positions =
get_user_positions_map(deps, env, user_addr, oracle_addr, params_addr, is_liquidation)?;
let positions = get_user_positions_map(
deps,
env,
user_addr,
account_id,
oracle_addr,
params_addr,
is_liquidation,
)?;
let health = compute_position_health(&positions)?;

Ok((health, positions))
Expand All @@ -32,13 +40,14 @@ pub fn assert_below_liq_threshold_after_withdraw(
deps: &Deps,
env: &Env,
user_addr: &Addr,
account_id: &str,
oracle_addr: &Addr,
params_addr: &Addr,
denom: &str,
withdraw_amount: Uint128,
) -> Result<bool, ContractError> {
let mut positions =
get_user_positions_map(deps, env, user_addr, oracle_addr, params_addr, false)?;
get_user_positions_map(deps, env, user_addr, account_id, oracle_addr, params_addr, false)?;
// Update position to compute health factor after withdraw
match positions.get_mut(denom) {
Some(p) => {
Expand All @@ -56,13 +65,14 @@ pub fn assert_below_max_ltv_after_borrow(
deps: &Deps,
env: &Env,
user_addr: &Addr,
account_id: &str,
oracle_addr: &Addr,
params_addr: &Addr,
denom: &str,
borrow_amount: Uint128,
) -> Result<bool, ContractError> {
let mut positions =
get_user_positions_map(deps, env, user_addr, oracle_addr, params_addr, false)?;
get_user_positions_map(deps, env, user_addr, account_id, oracle_addr, params_addr, false)?;

// Update position to compute health factor after borrow
positions
Expand Down Expand Up @@ -113,6 +123,7 @@ pub fn get_user_positions_map(
deps: &Deps,
env: &Env,
user_addr: &Addr,
account_id: &str,
oracle_addr: &Addr,
params_addr: &Addr,
is_liquidation: bool,
Expand All @@ -121,7 +132,7 @@ pub fn get_user_positions_map(

// Find all denoms that the user has a collateral or debt position in
let collateral_denoms = COLLATERALS
.prefix((user_addr, ""))
.prefix((user_addr, account_id))
.keys(deps.storage, None, None, Order::Ascending)
.collect::<StdResult<Vec<_>>>()?;
let debt_denoms = DEBTS
Expand All @@ -143,7 +154,7 @@ pub fn get_user_positions_map(
let params = query_asset_params(&deps.querier, params_addr, &denom)?;

let collateral_amount =
match COLLATERALS.may_load(deps.storage, (user_addr, "", &denom))? {
match COLLATERALS.may_load(deps.storage, (user_addr, account_id, &denom))? {
Some(collateral) if collateral.enabled => {
let amount_scaled = collateral.amount_scaled;
get_underlying_liquidity_amount(amount_scaled, &market, block_time)?
Expand Down
3 changes: 2 additions & 1 deletion contracts/red-bank/src/liquidate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ pub fn liquidate(
&deps.as_ref(),
&env,
&liquidatee_addr,
"",
oracle_addr,
params_addr,
true,
Expand Down Expand Up @@ -255,7 +256,7 @@ fn assert_liq_threshold(
prev_health: &Health,
) -> Result<(), ContractError> {
let (new_health, _) =
get_health_and_positions(deps, env, user_addr, oracle_addr, params_addr, true)?;
get_health_and_positions(deps, env, user_addr, "", oracle_addr, params_addr, true)?;

// liquidation_health_factor = None only if debt = 0 but liquidation is not possible
match (prev_health.liquidation_health_factor, new_health.liquidation_health_factor) {
Expand Down
3 changes: 3 additions & 0 deletions contracts/red-bank/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ pub fn query_user_position(
deps: Deps,
env: Env,
user_addr: Addr,
account_id: Option<String>,
liquidation_pricing: bool,
) -> Result<UserPositionResponse, ContractError> {
let config = CONFIG.load(deps.storage)?;
Expand All @@ -264,10 +265,12 @@ pub fn query_user_position(
let oracle_addr = &addresses[&MarsAddressType::Oracle];
let params_addr = &addresses[&MarsAddressType::Params];

let acc_id = account_id.unwrap_or("".to_string());
let positions = health::get_user_positions_map(
&deps,
&env,
&user_addr,
&acc_id,
oracle_addr,
params_addr,
liquidation_pricing,
Expand Down
5 changes: 2 additions & 3 deletions contracts/red-bank/src/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,9 @@ impl<'a> User<'a> {
&self,
store: &dyn Storage,
denom: &str,
account_id: Option<String>,
account_id: &str,
) -> StdResult<Collateral> {
let acc_id = account_id.unwrap_or("".to_string());
COLLATERALS.load(store, (self.0, &acc_id, denom))
COLLATERALS.load(store, (self.0, account_id, denom))
}

/// Load the user's debt
Expand Down
4 changes: 3 additions & 1 deletion contracts/red-bank/src/withdraw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ pub fn withdraw(
account_id: Option<String>,
) -> Result<Response, ContractError> {
let withdrawer = User(&info.sender);
let acc_id = account_id.clone().unwrap_or("".to_string());

let mut market = MARKETS.load(deps.storage, &denom)?;

let collateral = withdrawer.collateral(deps.storage, &denom, account_id.clone())?;
let collateral = withdrawer.collateral(deps.storage, &denom, &acc_id)?;
let withdrawer_balance_scaled_before = collateral.amount_scaled;

if withdrawer_balance_scaled_before.is_zero() {
Expand Down Expand Up @@ -77,6 +78,7 @@ pub fn withdraw(
&deps.as_ref(),
&env,
withdrawer.address(),
&acc_id,
oracle_addr,
params_addr,
&denom,
Expand Down
174 changes: 174 additions & 0 deletions contracts/red-bank/tests/test_credit_accounts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
use std::str::FromStr;

use cosmwasm_std::{coin, Addr, Decimal, Uint128};
use helpers::assert_err;
use mars_params::types::asset::{AssetParams, CmSettings, LiquidationBonus, RedBankSettings};
use mars_red_bank::error::ContractError;
use mars_red_bank_types::red_bank::{InitOrUpdateAssetParams, InterestRateModel, UserHealthStatus};
use mars_testing::integration::mock_env::MockEnvBuilder;

mod helpers;

#[test]
fn deposit_and_withdraw_for_credit_account_works() {
let owner = Addr::unchecked("owner");
let mut mock_env = MockEnvBuilder::new(None, owner.clone()).build();

let red_bank = mock_env.red_bank.clone();
let params = mock_env.params.clone();
let oracle = mock_env.oracle.clone();

let funded_amt = 1_000_000_000_000u128;
let provider = Addr::unchecked("provider"); // provides collateral to be borrowed by others
let credit_manager = Addr::unchecked("credit_manager");
let account_id = "111".to_string();

// setup red-bank
let (market_params, asset_params) = osmo_asset_params();
red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params);
params.init_params(&mut mock_env, asset_params);
let (market_params, asset_params) = usdc_asset_params();
red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params);
params.init_params(&mut mock_env, asset_params);

// setup oracle
oracle.set_price_source_fixed(&mut mock_env, "uosmo", Decimal::one());
oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(2u128, 1u128));

// fund accounts
mock_env.fund_accounts(&[&provider, &credit_manager], funded_amt, &["uosmo", "uusdc"]);

// provider deposits collaterals
red_bank.deposit(&mut mock_env, &provider, coin(1000000000, "uusdc")).unwrap();

// credit manager deposits
let cm_osmo_deposit_amt = 100000000u128;
red_bank
.deposit_with_acc_id(
&mut mock_env,
&credit_manager,
coin(cm_osmo_deposit_amt, "uosmo"),
Some(account_id.clone()),
)
.unwrap();

// credit manager try to borrow if no credit line set
let error_res = red_bank.borrow(&mut mock_env, &credit_manager, "uusdc", 100000000);
assert_err(error_res, ContractError::BorrowAmountExceedsGivenCollateral {});

// update credit line for credit manager
red_bank
.update_uncollateralized_loan_limit(
&mut mock_env,
&owner,
&credit_manager,
"uusdc",
Uint128::MAX,
)
.unwrap();

// credit manager should be able to borrow
let cm_usdc_borrow_amt = 100000000u128;
red_bank.borrow(&mut mock_env, &credit_manager, "uusdc", cm_usdc_borrow_amt).unwrap();

// collateral is not tracked for credit manager (it is per account id). Debt is tracked for credit manager as a whole (not per account id)
let cm_collaterals = red_bank.query_user_collaterals(&mut mock_env, &credit_manager);
assert!(cm_collaterals.is_empty());
let cm_debts = red_bank.query_user_debts(&mut mock_env, &credit_manager);
assert_eq!(cm_debts.len(), 1);
let cm_usdc_debt = cm_debts.get("uusdc").unwrap();
assert!(cm_usdc_debt.uncollateralized);
assert_eq!(cm_usdc_debt.amount.u128(), cm_usdc_borrow_amt);
let cm_position = red_bank.query_user_position(&mut mock_env, &credit_manager);
assert!(cm_position.total_enabled_collateral.is_zero());
assert!(cm_position.total_collateralized_debt.is_zero());
assert_eq!(cm_position.health_status, UserHealthStatus::NotBorrowing);

// collateral is tracked for credit manager account id. Debt is not tracked per account id
let cm_collaterals = red_bank.query_user_collaterals_with_acc_id(
&mut mock_env,
&credit_manager,
Some(account_id.clone()),
);
assert_eq!(cm_collaterals.len(), 1);
let cm_osmo_collateral = cm_collaterals.get("uosmo").unwrap();
assert_eq!(cm_osmo_collateral.amount.u128(), cm_osmo_deposit_amt);
let cm_position = red_bank.query_user_position_with_acc_id(
&mut mock_env,
&credit_manager,
Some(account_id.clone()),
);
assert_eq!(cm_position.total_enabled_collateral.u128(), cm_osmo_deposit_amt);
assert!(cm_position.total_collateralized_debt.is_zero());
assert_eq!(cm_position.health_status, UserHealthStatus::NotBorrowing);

// withdraw total collateral for account id
red_bank
.withdraw_with_acc_id(
&mut mock_env,
&credit_manager,
"uosmo",
None,
Some(account_id.clone()),
)
.unwrap();

// check collaterals and debts for credit manager account id after withdraw
let cm_collaterals = red_bank.query_user_collaterals_with_acc_id(
&mut mock_env,
&credit_manager,
Some(account_id.clone()),
);
assert!(cm_collaterals.is_empty());
let cm_position =
red_bank.query_user_position_with_acc_id(&mut mock_env, &credit_manager, Some(account_id));
assert!(cm_position.total_enabled_collateral.is_zero());
assert!(cm_position.total_collateralized_debt.is_zero());
assert_eq!(cm_position.health_status, UserHealthStatus::NotBorrowing);
}

fn osmo_asset_params() -> (InitOrUpdateAssetParams, AssetParams) {
default_asset_params_with("uosmo", Decimal::percent(70), Decimal::percent(78))
}

fn usdc_asset_params() -> (InitOrUpdateAssetParams, AssetParams) {
default_asset_params_with("uusdc", Decimal::percent(90), Decimal::percent(96))
}

fn default_asset_params_with(
denom: &str,
max_loan_to_value: Decimal,
liquidation_threshold: Decimal,
) -> (InitOrUpdateAssetParams, AssetParams) {
let market_params = InitOrUpdateAssetParams {
reserve_factor: Some(Decimal::percent(20)),
interest_rate_model: Some(InterestRateModel {
optimal_utilization_rate: Decimal::percent(10),
base: Decimal::percent(30),
slope_1: Decimal::percent(25),
slope_2: Decimal::percent(30),
}),
};
let asset_params = AssetParams {
denom: denom.to_string(),
credit_manager: CmSettings {
whitelisted: false,
hls: None,
},
red_bank: RedBankSettings {
deposit_enabled: true,
borrow_enabled: true,
},
max_loan_to_value,
liquidation_threshold,
liquidation_bonus: LiquidationBonus {
starting_lb: Decimal::percent(1),
slope: Decimal::from_str("2.0").unwrap(),
min_lb: Decimal::percent(2),
max_lb: Decimal::percent(10),
},
protocol_liquidation_fee: Decimal::percent(2),
deposit_cap: Uint128::MAX,
};
(market_params, asset_params)
}
1 change: 1 addition & 0 deletions contracts/red-bank/tests/test_misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ fn update_asset_collateral() {
&deps.as_ref(),
&env,
&user_addr,
"",
&Addr::unchecked("oracle"),
&Addr::unchecked("params"),
false,
Expand Down
Loading