From ef792ebbd599603b2396d859bf98db9b04be0ec5 Mon Sep 17 00:00:00 2001 From: Piotr Babel Date: Mon, 7 Aug 2023 12:52:25 +0200 Subject: [PATCH 1/2] Fix reclaim. --- contracts/red-bank/src/borrow.rs | 1 + contracts/red-bank/src/collateral.rs | 1 + contracts/red-bank/src/contract.rs | 15 ++++++++-- contracts/red-bank/src/health.rs | 23 +++++++++++---- contracts/red-bank/src/liquidate.rs | 3 +- contracts/red-bank/src/query.rs | 3 ++ contracts/red-bank/src/user.rs | 5 ++-- contracts/red-bank/src/withdraw.rs | 4 ++- contracts/red-bank/tests/test_misc.rs | 1 + packages/testing/src/integration/mock_env.rs | 1 + packages/testing/src/red_bank_querier.rs | 1 + packages/types/src/red_bank/msg.rs | 2 ++ schemas/mars-red-bank/mars-red-bank.json | 12 ++++++++ .../mars-red-bank/MarsRedBank.client.ts | 28 +++++++++++++++++-- .../mars-red-bank/MarsRedBank.react-query.ts | 4 +++ .../mars-red-bank/MarsRedBank.types.ts | 2 ++ 16 files changed, 89 insertions(+), 17 deletions(-) diff --git a/contracts/red-bank/src/borrow.rs b/contracts/red-bank/src/borrow.rs index 527f2002f..ad57b1a14 100644 --- a/contracts/red-bank/src/borrow.rs +++ b/contracts/red-bank/src/borrow.rs @@ -75,6 +75,7 @@ pub fn borrow( &deps.as_ref(), &env, borrower.address(), + "", oracle_addr, params_addr, &denom, diff --git a/contracts/red-bank/src/collateral.rs b/contracts/red-bank/src/collateral.rs index 4f7577182..9e6fc65ea 100644 --- a/contracts/red-bank/src/collateral.rs +++ b/contracts/red-bank/src/collateral.rs @@ -50,6 +50,7 @@ pub fn update_asset_collateral_status( &deps.as_ref(), &env, user.address(), + "", oracle_addr, params_addr, false, diff --git a/contracts/red-bank/src/contract.rs b/contracts/red-bank/src/contract.rs index 9904d3034..f0e5de7f9 100644 --- a/contracts/red-bank/src/contract.rs +++ b/contracts/red-bank/src/contract.rs @@ -1,4 +1,6 @@ -use cosmwasm_std::{entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response}; +use cosmwasm_std::{ + entry_point, to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult, +}; use mars_red_bank_types::red_bank::{ExecuteMsg, InstantiateMsg, QueryMsg}; use crate::{ @@ -177,15 +179,17 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { let user_addr = deps.api.addr_validate(&user)?; - to_binary(&query::query_user_position(deps, env, user_addr, false)?) + to_binary(&query::query_user_position(deps, env, user_addr, account_id, false)?) } QueryMsg::UserPositionLiquidationPricing { user, + account_id, } => { let user_addr = deps.api.addr_validate(&user)?; - to_binary(&query::query_user_position(deps, env, user_addr, true)?) + to_binary(&query::query_user_position(deps, env, user_addr, account_id, true)?) } QueryMsg::ScaledLiquidityAmount { denom, @@ -206,3 +210,8 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result StdResult { + Ok(Response::default()) +} diff --git a/contracts/red-bank/src/health.rs b/contracts/red-bank/src/health.rs index 7909905a0..c22854353 100644 --- a/contracts/red-bank/src/health.rs +++ b/contracts/red-bank/src/health.rs @@ -16,12 +16,20 @@ pub fn get_health_and_positions( deps: &Deps, env: &Env, user_addr: &Addr, + account_id: &str, oracle_addr: &Addr, params_addr: &Addr, is_liquidation: bool, ) -> Result<(Health, HashMap), ContractError> { - let positions = - get_user_positions_map(deps, env, user_addr, oracle_addr, params_addr, is_liquidation)?; + let positions = get_user_positions_map( + deps, + env, + user_addr, + account_id, + oracle_addr, + params_addr, + is_liquidation, + )?; let health = compute_position_health(&positions)?; Ok((health, positions)) @@ -32,13 +40,14 @@ pub fn assert_below_liq_threshold_after_withdraw( deps: &Deps, env: &Env, user_addr: &Addr, + account_id: &str, oracle_addr: &Addr, params_addr: &Addr, denom: &str, withdraw_amount: Uint128, ) -> Result { let mut positions = - get_user_positions_map(deps, env, user_addr, oracle_addr, params_addr, false)?; + get_user_positions_map(deps, env, user_addr, account_id, oracle_addr, params_addr, false)?; // Update position to compute health factor after withdraw match positions.get_mut(denom) { Some(p) => { @@ -56,13 +65,14 @@ pub fn assert_below_max_ltv_after_borrow( deps: &Deps, env: &Env, user_addr: &Addr, + account_id: &str, oracle_addr: &Addr, params_addr: &Addr, denom: &str, borrow_amount: Uint128, ) -> Result { let mut positions = - get_user_positions_map(deps, env, user_addr, oracle_addr, params_addr, false)?; + get_user_positions_map(deps, env, user_addr, account_id, oracle_addr, params_addr, false)?; // Update position to compute health factor after borrow positions @@ -113,6 +123,7 @@ pub fn get_user_positions_map( deps: &Deps, env: &Env, user_addr: &Addr, + account_id: &str, oracle_addr: &Addr, params_addr: &Addr, is_liquidation: bool, @@ -121,7 +132,7 @@ pub fn get_user_positions_map( // Find all denoms that the user has a collateral or debt position in let collateral_denoms = COLLATERALS - .prefix((user_addr, "")) + .prefix((user_addr, account_id)) .keys(deps.storage, None, None, Order::Ascending) .collect::>>()?; let debt_denoms = DEBTS @@ -143,7 +154,7 @@ pub fn get_user_positions_map( let params = query_asset_params(&deps.querier, params_addr, &denom)?; let collateral_amount = - match COLLATERALS.may_load(deps.storage, (user_addr, "", &denom))? { + match COLLATERALS.may_load(deps.storage, (user_addr, account_id, &denom))? { Some(collateral) if collateral.enabled => { let amount_scaled = collateral.amount_scaled; get_underlying_liquidity_amount(amount_scaled, &market, block_time)? diff --git a/contracts/red-bank/src/liquidate.rs b/contracts/red-bank/src/liquidate.rs index b29f80a94..ff130947d 100644 --- a/contracts/red-bank/src/liquidate.rs +++ b/contracts/red-bank/src/liquidate.rs @@ -89,6 +89,7 @@ pub fn liquidate( &deps.as_ref(), &env, &liquidatee_addr, + "", oracle_addr, params_addr, true, @@ -255,7 +256,7 @@ fn assert_liq_threshold( prev_health: &Health, ) -> Result<(), ContractError> { let (new_health, _) = - get_health_and_positions(deps, env, user_addr, oracle_addr, params_addr, true)?; + get_health_and_positions(deps, env, user_addr, "", oracle_addr, params_addr, true)?; // liquidation_health_factor = None only if debt = 0 but liquidation is not possible match (prev_health.liquidation_health_factor, new_health.liquidation_health_factor) { diff --git a/contracts/red-bank/src/query.rs b/contracts/red-bank/src/query.rs index 1bca591e3..31e92a16b 100644 --- a/contracts/red-bank/src/query.rs +++ b/contracts/red-bank/src/query.rs @@ -252,6 +252,7 @@ pub fn query_user_position( deps: Deps, env: Env, user_addr: Addr, + account_id: Option, liquidation_pricing: bool, ) -> Result { let config = CONFIG.load(deps.storage)?; @@ -264,10 +265,12 @@ pub fn query_user_position( let oracle_addr = &addresses[&MarsAddressType::Oracle]; let params_addr = &addresses[&MarsAddressType::Params]; + let acc_id = account_id.unwrap_or("".to_string()); let positions = health::get_user_positions_map( &deps, &env, &user_addr, + &acc_id, oracle_addr, params_addr, liquidation_pricing, diff --git a/contracts/red-bank/src/user.rs b/contracts/red-bank/src/user.rs index dbee6f8df..39ad2311e 100644 --- a/contracts/red-bank/src/user.rs +++ b/contracts/red-bank/src/user.rs @@ -52,10 +52,9 @@ impl<'a> User<'a> { &self, store: &dyn Storage, denom: &str, - account_id: Option, + account_id: &str, ) -> StdResult { - let acc_id = account_id.unwrap_or("".to_string()); - COLLATERALS.load(store, (self.0, &acc_id, denom)) + COLLATERALS.load(store, (self.0, account_id, denom)) } /// Load the user's debt diff --git a/contracts/red-bank/src/withdraw.rs b/contracts/red-bank/src/withdraw.rs index a9da5ce72..a25cf747d 100644 --- a/contracts/red-bank/src/withdraw.rs +++ b/contracts/red-bank/src/withdraw.rs @@ -21,10 +21,11 @@ pub fn withdraw( account_id: Option, ) -> Result { let withdrawer = User(&info.sender); + let acc_id = account_id.clone().unwrap_or("".to_string()); let mut market = MARKETS.load(deps.storage, &denom)?; - let collateral = withdrawer.collateral(deps.storage, &denom, account_id.clone())?; + let collateral = withdrawer.collateral(deps.storage, &denom, &acc_id)?; let withdrawer_balance_scaled_before = collateral.amount_scaled; if withdrawer_balance_scaled_before.is_zero() { @@ -77,6 +78,7 @@ pub fn withdraw( &deps.as_ref(), &env, withdrawer.address(), + &acc_id, oracle_addr, params_addr, &denom, diff --git a/contracts/red-bank/tests/test_misc.rs b/contracts/red-bank/tests/test_misc.rs index 870ee7360..bdee41932 100644 --- a/contracts/red-bank/tests/test_misc.rs +++ b/contracts/red-bank/tests/test_misc.rs @@ -358,6 +358,7 @@ fn update_asset_collateral() { &deps.as_ref(), &env, &user_addr, + "", &Addr::unchecked("oracle"), &Addr::unchecked("params"), false, diff --git a/packages/testing/src/integration/mock_env.rs b/packages/testing/src/integration/mock_env.rs index 8f160abb1..9ec2bc91c 100644 --- a/packages/testing/src/integration/mock_env.rs +++ b/packages/testing/src/integration/mock_env.rs @@ -606,6 +606,7 @@ impl RedBank { self.contract_addr.clone(), &red_bank::QueryMsg::UserPosition { user: user.to_string(), + account_id: None, }, ) .unwrap() diff --git a/packages/testing/src/red_bank_querier.rs b/packages/testing/src/red_bank_querier.rs index 6c5a1fca7..05902a61c 100644 --- a/packages/testing/src/red_bank_querier.rs +++ b/packages/testing/src/red_bank_querier.rs @@ -39,6 +39,7 @@ impl RedBankQuerier { }, QueryMsg::UserPosition { user, + account_id: _, } => match self.users_positions.get(&user) { Some(market) => to_binary(&market).into(), None => Err(format!("[mock]: could not find the position for {user}")).into(), diff --git a/packages/types/src/red_bank/msg.rs b/packages/types/src/red_bank/msg.rs index b206e4b1a..e18551ae5 100644 --- a/packages/types/src/red_bank/msg.rs +++ b/packages/types/src/red_bank/msg.rs @@ -196,12 +196,14 @@ pub enum QueryMsg { #[returns(crate::red_bank::UserPositionResponse)] UserPosition { user: String, + account_id: Option, }, /// Get user position for liquidation #[returns(crate::red_bank::UserPositionResponse)] UserPositionLiquidationPricing { user: String, + account_id: Option, }, /// Get liquidity scaled amount for a given underlying asset amount. diff --git a/schemas/mars-red-bank/mars-red-bank.json b/schemas/mars-red-bank/mars-red-bank.json index 084a66da0..f36ad40b2 100644 --- a/schemas/mars-red-bank/mars-red-bank.json +++ b/schemas/mars-red-bank/mars-red-bank.json @@ -829,6 +829,12 @@ "user" ], "properties": { + "account_id": { + "type": [ + "string", + "null" + ] + }, "user": { "type": "string" } @@ -851,6 +857,12 @@ "user" ], "properties": { + "account_id": { + "type": [ + "string", + "null" + ] + }, "user": { "type": "string" } diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts index fe9cb5288..cf6e4d333 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.client.ts @@ -86,8 +86,20 @@ export interface MarsRedBankReadOnlyInterface { startAfter?: string user: string }) => Promise - userPosition: ({ user }: { user: string }) => Promise - userPositionLiquidationPricing: ({ user }: { user: string }) => Promise + userPosition: ({ + accountId, + user, + }: { + accountId?: string + user: string + }) => Promise + userPositionLiquidationPricing: ({ + accountId, + user, + }: { + accountId?: string + user: string + }) => Promise scaledLiquidityAmount: ({ amount, denom }: { amount: Uint128; denom: string }) => Promise scaledDebtAmount: ({ amount, denom }: { amount: Uint128; denom: string }) => Promise underlyingLiquidityAmount: ({ @@ -254,20 +266,30 @@ export class MarsRedBankQueryClient implements MarsRedBankReadOnlyInterface { }, }) } - userPosition = async ({ user }: { user: string }): Promise => { + userPosition = async ({ + accountId, + user, + }: { + accountId?: string + user: string + }): Promise => { return this.client.queryContractSmart(this.contractAddress, { user_position: { + account_id: accountId, user, }, }) } userPositionLiquidationPricing = async ({ + accountId, user, }: { + accountId?: string user: string }): Promise => { return this.client.queryContractSmart(this.contractAddress, { user_position_liquidation_pricing: { + account_id: accountId, user, }, }) diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts index 981852c24..4579f2509 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.react-query.ts @@ -234,6 +234,7 @@ export function useMarsRedBankScaledLiquidityAmountQuery({ export interface MarsRedBankUserPositionLiquidationPricingQuery extends MarsRedBankReactQuery { args: { + accountId?: string user: string } } @@ -247,6 +248,7 @@ export function useMarsRedBankUserPositionLiquidationPricingQuery client ? client.userPositionLiquidationPricing({ + accountId: args.accountId, user: args.user, }) : Promise.reject(new Error('Invalid client')), @@ -256,6 +258,7 @@ export function useMarsRedBankUserPositionLiquidationPricingQuery extends MarsRedBankReactQuery { args: { + accountId?: string user: string } } @@ -269,6 +272,7 @@ export function useMarsRedBankUserPositionQuery({ () => client ? client.userPosition({ + accountId: args.accountId, user: args.user, }) : Promise.reject(new Error('Invalid client')), diff --git a/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts b/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts index 21b483d57..6c177061b 100644 --- a/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts +++ b/scripts/types/generated/mars-red-bank/MarsRedBank.types.ts @@ -163,11 +163,13 @@ export type QueryMsg = } | { user_position: { + account_id?: string | null user: string } } | { user_position_liquidation_pricing: { + account_id?: string | null user: string } } From 763f9ac1f0d96ded0a6ff9c997fe4df0e15fc773 Mon Sep 17 00:00:00 2001 From: Piotr Babel Date: Mon, 7 Aug 2023 17:19:25 +0200 Subject: [PATCH 2/2] Add test for deposit and withdraw for credit manager. --- .../red-bank/tests/test_credit_accounts.rs | 174 ++++++++++++++++++ packages/testing/src/integration/mock_env.rs | 11 +- 2 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 contracts/red-bank/tests/test_credit_accounts.rs diff --git a/contracts/red-bank/tests/test_credit_accounts.rs b/contracts/red-bank/tests/test_credit_accounts.rs new file mode 100644 index 000000000..84ae85caa --- /dev/null +++ b/contracts/red-bank/tests/test_credit_accounts.rs @@ -0,0 +1,174 @@ +use std::str::FromStr; + +use cosmwasm_std::{coin, Addr, Decimal, Uint128}; +use helpers::assert_err; +use mars_params::types::asset::{AssetParams, CmSettings, LiquidationBonus, RedBankSettings}; +use mars_red_bank::error::ContractError; +use mars_red_bank_types::red_bank::{InitOrUpdateAssetParams, InterestRateModel, UserHealthStatus}; +use mars_testing::integration::mock_env::MockEnvBuilder; + +mod helpers; + +#[test] +fn deposit_and_withdraw_for_credit_account_works() { + let owner = Addr::unchecked("owner"); + let mut mock_env = MockEnvBuilder::new(None, owner.clone()).build(); + + let red_bank = mock_env.red_bank.clone(); + let params = mock_env.params.clone(); + let oracle = mock_env.oracle.clone(); + + let funded_amt = 1_000_000_000_000u128; + let provider = Addr::unchecked("provider"); // provides collateral to be borrowed by others + let credit_manager = Addr::unchecked("credit_manager"); + let account_id = "111".to_string(); + + // setup red-bank + let (market_params, asset_params) = osmo_asset_params(); + red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params); + params.init_params(&mut mock_env, asset_params); + let (market_params, asset_params) = usdc_asset_params(); + red_bank.init_asset(&mut mock_env, &asset_params.denom, market_params); + params.init_params(&mut mock_env, asset_params); + + // setup oracle + oracle.set_price_source_fixed(&mut mock_env, "uosmo", Decimal::one()); + oracle.set_price_source_fixed(&mut mock_env, "uusdc", Decimal::from_ratio(2u128, 1u128)); + + // fund accounts + mock_env.fund_accounts(&[&provider, &credit_manager], funded_amt, &["uosmo", "uusdc"]); + + // provider deposits collaterals + red_bank.deposit(&mut mock_env, &provider, coin(1000000000, "uusdc")).unwrap(); + + // credit manager deposits + let cm_osmo_deposit_amt = 100000000u128; + red_bank + .deposit_with_acc_id( + &mut mock_env, + &credit_manager, + coin(cm_osmo_deposit_amt, "uosmo"), + Some(account_id.clone()), + ) + .unwrap(); + + // credit manager try to borrow if no credit line set + let error_res = red_bank.borrow(&mut mock_env, &credit_manager, "uusdc", 100000000); + assert_err(error_res, ContractError::BorrowAmountExceedsGivenCollateral {}); + + // update credit line for credit manager + red_bank + .update_uncollateralized_loan_limit( + &mut mock_env, + &owner, + &credit_manager, + "uusdc", + Uint128::MAX, + ) + .unwrap(); + + // credit manager should be able to borrow + let cm_usdc_borrow_amt = 100000000u128; + red_bank.borrow(&mut mock_env, &credit_manager, "uusdc", cm_usdc_borrow_amt).unwrap(); + + // collateral is not tracked for credit manager (it is per account id). Debt is tracked for credit manager as a whole (not per account id) + let cm_collaterals = red_bank.query_user_collaterals(&mut mock_env, &credit_manager); + assert!(cm_collaterals.is_empty()); + let cm_debts = red_bank.query_user_debts(&mut mock_env, &credit_manager); + assert_eq!(cm_debts.len(), 1); + let cm_usdc_debt = cm_debts.get("uusdc").unwrap(); + assert!(cm_usdc_debt.uncollateralized); + assert_eq!(cm_usdc_debt.amount.u128(), cm_usdc_borrow_amt); + let cm_position = red_bank.query_user_position(&mut mock_env, &credit_manager); + assert!(cm_position.total_enabled_collateral.is_zero()); + assert!(cm_position.total_collateralized_debt.is_zero()); + assert_eq!(cm_position.health_status, UserHealthStatus::NotBorrowing); + + // collateral is tracked for credit manager account id. Debt is not tracked per account id + let cm_collaterals = red_bank.query_user_collaterals_with_acc_id( + &mut mock_env, + &credit_manager, + Some(account_id.clone()), + ); + assert_eq!(cm_collaterals.len(), 1); + let cm_osmo_collateral = cm_collaterals.get("uosmo").unwrap(); + assert_eq!(cm_osmo_collateral.amount.u128(), cm_osmo_deposit_amt); + let cm_position = red_bank.query_user_position_with_acc_id( + &mut mock_env, + &credit_manager, + Some(account_id.clone()), + ); + assert_eq!(cm_position.total_enabled_collateral.u128(), cm_osmo_deposit_amt); + assert!(cm_position.total_collateralized_debt.is_zero()); + assert_eq!(cm_position.health_status, UserHealthStatus::NotBorrowing); + + // withdraw total collateral for account id + red_bank + .withdraw_with_acc_id( + &mut mock_env, + &credit_manager, + "uosmo", + None, + Some(account_id.clone()), + ) + .unwrap(); + + // check collaterals and debts for credit manager account id after withdraw + let cm_collaterals = red_bank.query_user_collaterals_with_acc_id( + &mut mock_env, + &credit_manager, + Some(account_id.clone()), + ); + assert!(cm_collaterals.is_empty()); + let cm_position = + red_bank.query_user_position_with_acc_id(&mut mock_env, &credit_manager, Some(account_id)); + assert!(cm_position.total_enabled_collateral.is_zero()); + assert!(cm_position.total_collateralized_debt.is_zero()); + assert_eq!(cm_position.health_status, UserHealthStatus::NotBorrowing); +} + +fn osmo_asset_params() -> (InitOrUpdateAssetParams, AssetParams) { + default_asset_params_with("uosmo", Decimal::percent(70), Decimal::percent(78)) +} + +fn usdc_asset_params() -> (InitOrUpdateAssetParams, AssetParams) { + default_asset_params_with("uusdc", Decimal::percent(90), Decimal::percent(96)) +} + +fn default_asset_params_with( + denom: &str, + max_loan_to_value: Decimal, + liquidation_threshold: Decimal, +) -> (InitOrUpdateAssetParams, AssetParams) { + let market_params = InitOrUpdateAssetParams { + reserve_factor: Some(Decimal::percent(20)), + interest_rate_model: Some(InterestRateModel { + optimal_utilization_rate: Decimal::percent(10), + base: Decimal::percent(30), + slope_1: Decimal::percent(25), + slope_2: Decimal::percent(30), + }), + }; + let asset_params = AssetParams { + denom: denom.to_string(), + credit_manager: CmSettings { + whitelisted: false, + hls: None, + }, + red_bank: RedBankSettings { + deposit_enabled: true, + borrow_enabled: true, + }, + max_loan_to_value, + liquidation_threshold, + liquidation_bonus: LiquidationBonus { + starting_lb: Decimal::percent(1), + slope: Decimal::from_str("2.0").unwrap(), + min_lb: Decimal::percent(2), + max_lb: Decimal::percent(10), + }, + protocol_liquidation_fee: Decimal::percent(2), + deposit_cap: Uint128::MAX, + }; + (market_params, asset_params) +} diff --git a/packages/testing/src/integration/mock_env.rs b/packages/testing/src/integration/mock_env.rs index 9ec2bc91c..642ff1091 100644 --- a/packages/testing/src/integration/mock_env.rs +++ b/packages/testing/src/integration/mock_env.rs @@ -600,13 +600,22 @@ impl RedBank { } pub fn query_user_position(&self, env: &mut MockEnv, user: &Addr) -> UserPositionResponse { + self.query_user_position_with_acc_id(env, user, None) + } + + pub fn query_user_position_with_acc_id( + &self, + env: &mut MockEnv, + user: &Addr, + account_id: Option, + ) -> UserPositionResponse { env.app .wrap() .query_wasm_smart( self.contract_addr.clone(), &red_bank::QueryMsg::UserPosition { user: user.to_string(), - account_id: None, + account_id, }, ) .unwrap()