From ffff685879b887441c491b748d90569a086993e6 Mon Sep 17 00:00:00 2001 From: Larry Engineer <26318510+larry0x@users.noreply.github.com> Date: Sat, 17 Sep 2022 03:40:44 +0100 Subject: [PATCH] Remove maToken (#76) * remove matoken from outposts package * remove matoken from testing package * remove cw20-related helper function and dependencies * delete matoken contract * update entry points * update workspace members * update query functions * update the function for compiling user positions map * remove a cw20-related helper function * update init and setting config execute functions * update init and updating market execute functions * add `User` helper class * update an interest rate function to use `User` * implement helper methods for `Market` for updating collateral/debt amounts * update deposit and withdraw functions * remove finalize matoken transfer function * update borrow function * update liquidate function * update update asset collateral status function * remove unused imports, helper functions, storage variables * remove unused variables * fix ownership errors and other issues * remove cw20 from red bank dependencies * fix clippy warnings * update admin tests * return collateral amount in queries * return uncollateralized status in debt response * update query tests * update deposit tests * update misc tests and fix bug * update withdraw tests * revert the changes to the order of event attributes * update borrow/repay tests and fix a bug * shorten a variable name to improve formatting * update liquidation tests * disable incentives contract from workspace for now so that CI passes * fix clippy warnings\ * update changelog * remove two TODO messages * remove an unused helper function * remove two TODO items that are no longer relevant * rename `{mint,burn}_amount` to `{deposit,withdraw}_amount_scaled` * remove mentions of "matoken" in comments and readmes * improve docs * fix a typo * fix a import * fix tests * update event attributes --- CHANGELOG.md | 94 ++ Cargo.lock | 58 - Cargo.toml | 3 +- contracts/incentives/README.md | 3 +- contracts/ma-token/Cargo.toml | 34 - contracts/ma-token/README.md | 6 - contracts/ma-token/src/allowances.rs | 349 ------ contracts/ma-token/src/contract.rs | 1136 -------------------- contracts/ma-token/src/core.rs | 127 --- contracts/ma-token/src/lib.rs | 9 - contracts/ma-token/src/state.rs | 6 - contracts/ma-token/src/test_helpers.rs | 78 -- contracts/red-bank/Cargo.toml | 2 - contracts/red-bank/src/contract.rs | 47 +- contracts/red-bank/src/execute.rs | 453 +++----- contracts/red-bank/src/health.rs | 7 +- contracts/red-bank/src/helpers.rs | 10 +- contracts/red-bank/src/interest_rates.rs | 28 +- contracts/red-bank/src/lib.rs | 1 + contracts/red-bank/src/query.rs | 63 +- contracts/red-bank/src/state.rs | 15 +- contracts/red-bank/src/user.rs | 184 ++++ contracts/red-bank/tests/helpers.rs | 16 +- contracts/red-bank/tests/test_admin.rs | 311 ++---- contracts/red-bank/tests/test_borrow.rs | 171 ++- contracts/red-bank/tests/test_deposit.rs | 482 ++++++--- contracts/red-bank/tests/test_liquidate.rs | 767 +++++-------- contracts/red-bank/tests/test_misc.rs | 55 +- contracts/red-bank/tests/test_query.rs | 75 +- contracts/red-bank/tests/test_withdraw.rs | 922 ++++++++-------- packages/outpost/Cargo.toml | 2 - packages/outpost/src/cw20_core.rs | 123 --- packages/outpost/src/helpers.rs | 48 +- packages/outpost/src/lib.rs | 13 +- packages/outpost/src/ma_token.rs | 189 ---- packages/outpost/src/red_bank/market.rs | 28 +- packages/outpost/src/red_bank/msg.rs | 44 +- packages/outpost/src/red_bank/types.rs | 16 +- packages/testing/Cargo.toml | 2 - packages/testing/src/cw20_querier.rs | 199 ---- packages/testing/src/lib.rs | 2 - packages/testing/src/mars_mock_querier.rs | 49 +- 42 files changed, 1769 insertions(+), 4458 deletions(-) delete mode 100755 contracts/ma-token/Cargo.toml delete mode 100755 contracts/ma-token/README.md delete mode 100755 contracts/ma-token/src/allowances.rs delete mode 100755 contracts/ma-token/src/contract.rs delete mode 100644 contracts/ma-token/src/core.rs delete mode 100755 contracts/ma-token/src/lib.rs delete mode 100755 contracts/ma-token/src/state.rs delete mode 100644 contracts/ma-token/src/test_helpers.rs create mode 100644 contracts/red-bank/src/user.rs delete mode 100644 packages/outpost/src/cw20_core.rs delete mode 100644 packages/outpost/src/ma_token.rs delete mode 100644 packages/testing/src/cw20_querier.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 506894e8..5bbd034e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,100 @@ All notable changes to this project will be documented in this file. This section documents the API changes compared to the Terra Classic deployment, found in the [`mars-core`](https://github.com/mars-protocol/mars-core) repository. This section is **not comprehensive**, as the changes are numerous. Changelog for later version start here should be made comprehensive. +- ([#76](https://github.com/mars-protocol/outposts/pull/76/files)) Red Bank: Execute messages for creating and updating markets have been simplified: + +```diff +enum ExecuteMsg { + InitAsset { + denom: String, +- asset_params: InitOrUpdateAssetParams, +- asset_symbol: Option, ++ params: InitOrUpdateAssetParams, + }, + UpdateAsset { + denom: String, +- asset_params: InitOrUpdateAssetParams, ++ params: InitOrUpdateAssetParams, + }, +} +``` + +- ([#76](https://github.com/mars-protocol/outposts/pull/76/files)) Red Bank: `underlying_liquidity_amount` query now takes the asset's denom instead of the maToken contract address: + +```diff +enum QueryMsg { + UnderlyingLiquidityAmount { +- ma_token_address: String, ++ denom: String, + amount_scaled: Uint128, + }, +} +``` + +- ([#76](https://github.com/mars-protocol/outposts/pull/76/files)) Red Bank: `market` query now no longer returns the maToken address. Additionally, it now returns the total scaled amount of collateral. The frontend should now query this method instead of the maToken's total supply: + +```diff +struct Market { + pub denom: String, +- pub ma_token_address: Addr, + pub max_loan_to_value: Decimal, + pub liquidation_threshold: Decimal, + pub liquidation_bonus: Decimal, + pub reserve_factor: Decimal, + pub interest_rate_model: InterestRateModel, + pub borrow_index: Decimal, + pub liquidity_index: Decimal, + pub borrow_rate: Decimal, + pub liquidity_rate: Decimal, + pub indexes_last_updated: u64, ++ pub collateral_total_scaled: Uint128, + pub debt_total_scaled: Uint128, + pub deposit_enabled: bool, + pub borrow_enabled: bool, + pub deposit_cap: Uint128, +} +``` + +- ([#76](https://github.com/mars-protocol/outposts/pull/76/files)) Red Bank: `user_collateral` query now returns the collateral scaled amount and amount. The frontend should now query this method instead of the maToken contracts: + +```diff +struct UserCollateralResponse { + pub denom: String, ++ pub amount_scaled: Uint128, ++ pub amount: Uint128, + pub enabled: bool, +} +``` + +- ([#76](https://github.com/mars-protocol/outposts/pull/76/files)) Red Bank: `user_debt` query now returns whether the debt is being borrowed as uncollateralized loan: + +```diff +struct UserDebtResponse { + pub denom: String, + pub amount_scaled: Uint128, + pub amount: Uint128, ++ pub uncollateralized: bool, +} +``` + +- ([#76](https://github.com/mars-protocol/outposts/pull/76/files)) Red Bank: Parameters related to maToken have been removed from instantiate message, the `update_config` execute message, and the response type for config query: + +```diff +struct CreateOrUpdateConfig { + pub owner: Option, + pub address_provider: Option, +- pub ma_token_code_id: Option, + pub close_factor: Option, +} + +struct Config { + pub owner: Addr, + pub address_provider: Addr, +- pub ma_token_code_id: u64, + pub close_factor: Decimal, +} +``` + - ([#69](https://github.com/mars-protocol/outposts/pull/69/files)) Red Bank: `Market` no longer includes an index: ```diff diff --git a/Cargo.lock b/Cargo.lock index bc739f0a..9e4f056a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -185,35 +185,6 @@ dependencies = [ "serde", ] -[[package]] -name = "cw20" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f446f59c519fbac5ab8b9f6c7f8dcaa05ee761703971406b28221ea778bb737" -dependencies = [ - "cosmwasm-std", - "cw-utils", - "schemars", - "serde", -] - -[[package]] -name = "cw20-base" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e39bf97c985a50f2e340833137b3f14999f58708c4ca9928cd8f87d530c4109c" -dependencies = [ - "cosmwasm-std", - "cw-storage-plus", - "cw-utils", - "cw2", - "cw20", - "schemars", - "semver", - "serde", - "thiserror", -] - [[package]] name = "der" version = "0.5.1" @@ -418,29 +389,6 @@ dependencies = [ "mars-testing", ] -[[package]] -name = "mars-incentives" -version = "1.0.0" -dependencies = [ - "cosmwasm-std", - "cw-storage-plus", - "mars-outpost", - "mars-testing", - "thiserror", -] - -[[package]] -name = "mars-ma-token" -version = "0.1.0" -dependencies = [ - "cosmwasm-std", - "cw-storage-plus", - "cw2", - "cw20", - "cw20-base", - "mars-outpost", -] - [[package]] name = "mars-oracle-base" version = "0.1.0" @@ -474,8 +422,6 @@ name = "mars-outpost" version = "0.1.0" dependencies = [ "cosmwasm-std", - "cw20", - "cw20-base", "mars-testing", "schemars", "serde", @@ -489,8 +435,6 @@ dependencies = [ "cosmwasm-std", "cw-storage-plus", "cw-utils", - "cw20", - "cw20-base", "mars-health", "mars-outpost", "mars-testing", @@ -532,8 +476,6 @@ name = "mars-testing" version = "0.1.0" dependencies = [ "cosmwasm-std", - "cw20", - "cw20-base", "mars-outpost", "osmosis-std", "prost", diff --git a/Cargo.toml b/Cargo.toml index 7141c7f5..3a861cb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,7 @@ [workspace] members = [ "contracts/address-provider", - "contracts/incentives", - "contracts/ma-token", + # "contracts/incentives", "contracts/oracle/*", "contracts/red-bank", "contracts/rewards-collector/*", diff --git a/contracts/incentives/README.md b/contracts/incentives/README.md index e212677a..0c4dd8e7 100644 --- a/contracts/incentives/README.md +++ b/contracts/incentives/README.md @@ -1,2 +1,3 @@ # Incentives -Manage MARS incentives for maToken holders (depositors). + +Manage MARS incentives for depositors. diff --git a/contracts/ma-token/Cargo.toml b/contracts/ma-token/Cargo.toml deleted file mode 100755 index ee3633f2..00000000 --- a/contracts/ma-token/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "mars-ma-token" -version = "0.1.0" -authors = [ - "larry_0x ", - "Piotr Babel ", - "Spike Spiegel ", - "Ahmad Kaouk", - "Harry Scholes" -] -edition = "2021" - -exclude = [ - # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. - "contract.wasm", - "hash.txt", -] - -[lib] -crate-type = ["cdylib", "rlib"] -doctest = false - -[features] -# for more explicit tests, cargo test --features=backtraces -backtraces = ["cosmwasm-std/backtraces"] - -[dependencies] -mars-outpost = { path = "../../packages/outpost", version = "0.1.0" } - -cosmwasm-std = "1.0" -cw2 = "0.14" -cw20 = "0.14" -cw20-base = { version = "0.14", features = ["library"] } -cw-storage-plus = "0.14" diff --git a/contracts/ma-token/README.md b/contracts/ma-token/README.md deleted file mode 100755 index 79821b09..00000000 --- a/contracts/ma-token/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# maToken - -maToken is a modified cw20 that is minted in representation of a deposited asset. -Each deposited asset has a corresponding instance of the maToken and accumulate interest in the way that they are redeemable for an ever increasing amount of their underlying asset. -The Red Bank can do forced transfers/burns when user positions are being liquidated. -On each contract call that changes a balance, the maToken will call the incentives contract in order to manage MARS rewards. diff --git a/contracts/ma-token/src/allowances.rs b/contracts/ma-token/src/allowances.rs deleted file mode 100755 index 0e00b0fa..00000000 --- a/contracts/ma-token/src/allowances.rs +++ /dev/null @@ -1,349 +0,0 @@ -use cosmwasm_std::{Binary, DepsMut, Env, MessageInfo, Response, Uint128}; -use cw20::Cw20ReceiveMsg; -use cw20_base::allowances::deduct_allowance; -use cw20_base::ContractError; - -use crate::core; -use crate::state::CONFIG; - -pub fn execute_transfer_from( - deps: DepsMut, - env: Env, - info: MessageInfo, - owner: String, - recipient: String, - amount: Uint128, -) -> Result { - let rcpt_addr = deps.api.addr_validate(&recipient)?; - let owner_addr = deps.api.addr_validate(&owner)?; - - // deduct allowance before doing anything else have enough allowance - deduct_allowance(deps.storage, &owner_addr, &info.sender, &env.block, amount)?; - - let config = CONFIG.load(deps.storage)?; - let messages = core::transfer(deps.storage, &config, owner_addr, rcpt_addr, amount, true)?; - - let res = Response::new() - .add_messages(messages) - .add_attribute("action", "transfer_from") - .add_attribute("from", owner) - .add_attribute("to", recipient) - .add_attribute("by", info.sender) - .add_attribute("amount", amount); - Ok(res) -} - -pub fn execute_send_from( - deps: DepsMut, - env: Env, - info: MessageInfo, - owner: String, - contract: String, - amount: Uint128, - msg: Binary, -) -> Result { - let rcpt_addr = deps.api.addr_validate(&contract)?; - let owner_addr = deps.api.addr_validate(&owner)?; - - // deduct allowance before doing anything else have enough allowance - deduct_allowance(deps.storage, &owner_addr, &info.sender, &env.block, amount)?; - - let config = CONFIG.load(deps.storage)?; - let transfer_messages = - core::transfer(deps.storage, &config, owner_addr, rcpt_addr, amount, true)?; - - let res = Response::new() - .add_attribute("action", "send_from") - .add_attribute("from", &owner) - .add_attribute("to", &contract) - .add_attribute("by", &info.sender) - .add_attribute("amount", amount) - .add_messages(transfer_messages) - .add_message( - Cw20ReceiveMsg { - sender: info.sender.to_string(), - amount, - msg, - } - .into_cosmos_msg(contract)?, - ); - Ok(res) -} - -#[cfg(test)] -mod tests { - use super::*; - - use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - use cosmwasm_std::{attr, to_binary, Addr, Binary, CosmosMsg, StdError, SubMsg, WasmMsg}; - - use cw20::{AllowanceResponse, Cw20ReceiveMsg, Expiration}; - use cw20_base::allowances::query_allowance; - - use crate::contract::execute; - use crate::msg::ExecuteMsg; - use crate::test_helpers::{do_instantiate, get_balance}; - - #[test] - fn transfer_from_respects_limits() { - let mut deps = mock_dependencies(); - let owner = String::from("addr0001"); - let spender = String::from("addr0002"); - let rcpt = String::from("addr0003"); - - let start = Uint128::new(999999); - do_instantiate(deps.as_mut(), &owner, start); - - // provide an allowance - let allow1 = Uint128::new(77777); - let msg = ExecuteMsg::IncreaseAllowance { - spender: spender.clone(), - amount: allow1, - expires: None, - }; - let info = mock_info(owner.as_ref(), &[]); - let env = mock_env(); - execute(deps.as_mut(), env, info, msg).unwrap(); - - // valid transfer of part of the allowance - let transfer = Uint128::new(44444); - let msg = ExecuteMsg::TransferFrom { - owner: owner.clone(), - recipient: rcpt.clone(), - amount: transfer, - }; - let info = mock_info(spender.as_ref(), &[]); - let env = mock_env(); - let res = execute(deps.as_mut(), env, info, msg).unwrap(); - - assert_eq!( - res.attributes, - vec![ - attr("action", "transfer_from"), - attr("from", owner.clone()), - attr("to", rcpt.clone()), - attr("by", spender.clone()), - attr("amount", transfer), - ] - ); - - assert_eq!( - res.messages, - vec![ - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::from("red_bank"), - msg: to_binary( - &mars_outpost::red_bank::ExecuteMsg::FinalizeLiquidityTokenTransfer { - sender_address: Addr::unchecked(&owner), - recipient_address: Addr::unchecked(&rcpt), - sender_previous_balance: start, - recipient_previous_balance: Uint128::zero(), - amount: transfer, - } - ) - .unwrap(), - funds: vec![], - })), - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::from("incentives"), - msg: to_binary(&mars_outpost::incentives::msg::ExecuteMsg::BalanceChange { - user_address: Addr::unchecked(&owner), - user_balance_before: start, - total_supply_before: start, - },) - .unwrap(), - funds: vec![], - })), - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::from("incentives"), - msg: to_binary(&mars_outpost::incentives::msg::ExecuteMsg::BalanceChange { - user_address: Addr::unchecked(&rcpt), - user_balance_before: Uint128::zero(), - total_supply_before: start, - },) - .unwrap(), - funds: vec![], - })), - ] - ); - - // make sure money arrived - assert_eq!(get_balance(deps.as_ref(), owner.clone()), start.checked_sub(transfer).unwrap()); - assert_eq!(get_balance(deps.as_ref(), rcpt.clone()), transfer); - - // ensure it looks good - let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); - let expect = AllowanceResponse { - allowance: allow1.checked_sub(transfer).unwrap(), - expires: Expiration::Never {}, - }; - assert_eq!(expect, allowance); - - // cannot send more than the allowance - let msg = ExecuteMsg::TransferFrom { - owner: owner.clone(), - recipient: rcpt.clone(), - amount: Uint128::new(33443), - }; - let info = mock_info(spender.as_ref(), &[]); - let env = mock_env(); - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); - - // let us increase limit, but set the expiration (default env height is 12_345) - let info = mock_info(owner.as_ref(), &[]); - let env = mock_env(); - let msg = ExecuteMsg::IncreaseAllowance { - spender: spender.clone(), - amount: Uint128::new(1000), - expires: Some(Expiration::AtHeight(env.block.height)), - }; - execute(deps.as_mut(), env, info, msg).unwrap(); - - // we should now get the expiration error - let msg = ExecuteMsg::TransferFrom { - owner, - recipient: rcpt, - amount: Uint128::new(33443), - }; - let info = mock_info(spender.as_ref(), &[]); - let env = mock_env(); - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(err, ContractError::Expired {}); - } - - #[test] - fn send_from_respects_limits() { - let mut deps = mock_dependencies(); - let owner = String::from("addr0001"); - let spender = String::from("addr0002"); - let contract = String::from("cool-dex"); - let send_msg = Binary::from(r#"{"some":123}"#.as_bytes()); - - let start = Uint128::new(999999); - do_instantiate(deps.as_mut(), &owner, start); - - // provide an allowance - let allow1 = Uint128::new(77777); - let msg = ExecuteMsg::IncreaseAllowance { - spender: spender.clone(), - amount: allow1, - expires: None, - }; - let info = mock_info(owner.as_ref(), &[]); - let env = mock_env(); - execute(deps.as_mut(), env, info, msg).unwrap(); - - // valid send of part of the allowance - let transfer = Uint128::new(44444); - let msg = ExecuteMsg::SendFrom { - owner: owner.clone(), - amount: transfer, - contract: contract.clone(), - msg: send_msg.clone(), - }; - let info = mock_info(spender.as_ref(), &[]); - let env = mock_env(); - let res = execute(deps.as_mut(), env, info, msg).unwrap(); - assert_eq!(res.attributes[0], attr("action", "send_from")); - - // we record this as sent by the one who requested, not the one who was paying - let binary_msg = Cw20ReceiveMsg { - sender: spender.clone(), - amount: transfer, - msg: send_msg.clone(), - } - .into_binary() - .unwrap(); - assert_eq!( - res.messages, - vec![ - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::from("red_bank"), - msg: to_binary( - &mars_outpost::red_bank::ExecuteMsg::FinalizeLiquidityTokenTransfer { - sender_address: Addr::unchecked(&owner), - recipient_address: Addr::unchecked(&contract), - sender_previous_balance: start, - recipient_previous_balance: Uint128::zero(), - amount: transfer, - } - ) - .unwrap(), - funds: vec![], - })), - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::from("incentives"), - msg: to_binary(&mars_outpost::incentives::msg::ExecuteMsg::BalanceChange { - user_address: Addr::unchecked(&owner), - user_balance_before: start, - total_supply_before: start, - },) - .unwrap(), - funds: vec![], - })), - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::from("incentives"), - msg: to_binary(&mars_outpost::incentives::msg::ExecuteMsg::BalanceChange { - user_address: Addr::unchecked(&contract), - user_balance_before: Uint128::zero(), - total_supply_before: start, - },) - .unwrap(), - funds: vec![], - })), - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: contract.clone(), - msg: binary_msg, - funds: vec![], - })) - ] - ); - - // make sure money sent - assert_eq!(get_balance(deps.as_ref(), owner.clone()), start.checked_sub(transfer).unwrap()); - assert_eq!(get_balance(deps.as_ref(), contract.clone()), transfer); - - // ensure it looks good - let allowance = query_allowance(deps.as_ref(), owner.clone(), spender.clone()).unwrap(); - let expect = AllowanceResponse { - allowance: allow1.checked_sub(transfer).unwrap(), - expires: Expiration::Never {}, - }; - assert_eq!(expect, allowance); - - // cannot send more than the allowance - let msg = ExecuteMsg::SendFrom { - owner: owner.clone(), - amount: Uint128::new(33443), - contract: contract.clone(), - msg: send_msg.clone(), - }; - let info = mock_info(spender.as_ref(), &[]); - let env = mock_env(); - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); - - // let us increase limit, but set the expiration to current block (expired) - let info = mock_info(owner.as_ref(), &[]); - let env = mock_env(); - let msg = ExecuteMsg::IncreaseAllowance { - spender: spender.clone(), - amount: Uint128::new(1000), - expires: Some(Expiration::AtHeight(env.block.height)), - }; - execute(deps.as_mut(), env, info, msg).unwrap(); - - // we should now get the expiration error - let msg = ExecuteMsg::SendFrom { - owner, - amount: Uint128::new(33443), - contract, - msg: send_msg, - }; - let info = mock_info(spender.as_ref(), &[]); - let env = mock_env(); - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(err, ContractError::Expired {}); - } -} diff --git a/contracts/ma-token/src/contract.rs b/contracts/ma-token/src/contract.rs deleted file mode 100755 index 4eb7e7fd..00000000 --- a/contracts/ma-token/src/contract.rs +++ /dev/null @@ -1,1136 +0,0 @@ -#[cfg(not(feature = "library"))] -use cosmwasm_std::entry_point; -use cosmwasm_std::{ - to_binary, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, QueryRequest, Response, - StdResult, Uint128, WasmMsg, WasmQuery, -}; -use cw2::set_contract_version; -use cw20::{BalanceResponse, Cw20ReceiveMsg}; -use cw20_base::allowances::{ - execute_decrease_allowance, execute_increase_allowance, query_allowance, -}; -use cw20_base::contract::{ - create_accounts, execute_update_marketing, execute_upload_logo, query_balance, - query_download_logo, query_marketing_info, query_minter, query_token_info, -}; -use cw20_base::enumerable::{query_all_accounts, query_owner_allowances}; -use cw20_base::state::{BALANCES, TOKEN_INFO}; -use cw20_base::ContractError; - -use mars_outpost::cw20_core::instantiate_token_info_and_marketing; -use mars_outpost::red_bank; - -use crate::allowances::{execute_send_from, execute_transfer_from}; -use crate::core; -use crate::msg::{BalanceAndTotalSupplyResponse, ExecuteMsg, InstantiateMsg, QueryMsg}; -use crate::state::CONFIG; -use crate::Config; - -// version info for migration info -const CONTRACT_NAME: &str = "crates.io:ma-token"; -const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn instantiate( - mut deps: DepsMut, - _env: Env, - _info: MessageInfo, - msg: InstantiateMsg, -) -> Result { - set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - - let base_msg = cw20_base::msg::InstantiateMsg { - name: msg.name, - symbol: msg.symbol, - decimals: msg.decimals, - initial_balances: msg.initial_balances, - mint: msg.mint, - marketing: msg.marketing, - }; - base_msg.validate()?; - - let total_supply = create_accounts(&mut deps, &base_msg.initial_balances)?; - instantiate_token_info_and_marketing(&mut deps, base_msg, total_supply)?; - - // store token config - CONFIG.save( - deps.storage, - &Config { - red_bank_address: deps.api.addr_validate(&msg.red_bank_address)?, - incentives_address: deps.api.addr_validate(&msg.incentives_address)?, - }, - )?; - - let mut res = Response::new(); - if let Some(hook) = msg.init_hook { - res = res.add_message(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: hook.contract_addr, - msg: hook.msg, - funds: vec![], - })); - } - - Ok(res) -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn execute( - deps: DepsMut, - env: Env, - info: MessageInfo, - msg: ExecuteMsg, -) -> Result { - match msg { - ExecuteMsg::Transfer { - recipient, - amount, - } => execute_transfer(deps, env, info, recipient, amount), - ExecuteMsg::TransferOnLiquidation { - sender, - recipient, - amount, - } => execute_transfer_on_liquidation(deps, env, info, sender, recipient, amount), - ExecuteMsg::Burn { - user, - amount, - } => execute_burn(deps, env, info, user, amount), - ExecuteMsg::Send { - contract, - amount, - msg, - } => execute_send(deps, env, info, contract, amount, msg), - ExecuteMsg::Mint { - recipient, - amount, - } => execute_mint(deps, env, info, recipient, amount), - ExecuteMsg::IncreaseAllowance { - spender, - amount, - expires, - } => Ok(execute_increase_allowance(deps, env, info, spender, amount, expires)?), - ExecuteMsg::DecreaseAllowance { - spender, - amount, - expires, - } => Ok(execute_decrease_allowance(deps, env, info, spender, amount, expires)?), - ExecuteMsg::TransferFrom { - owner, - recipient, - amount, - } => execute_transfer_from(deps, env, info, owner, recipient, amount), - ExecuteMsg::SendFrom { - owner, - contract, - amount, - msg, - } => execute_send_from(deps, env, info, owner, contract, amount, msg), - ExecuteMsg::UpdateMarketing { - project, - description, - marketing, - } => execute_update_marketing(deps, env, info, project, description, marketing), - ExecuteMsg::UploadLogo(logo) => execute_upload_logo(deps, env, info, logo), - } -} - -pub fn execute_transfer( - deps: DepsMut, - _env: Env, - info: MessageInfo, - recipient_unchecked: String, - amount: Uint128, -) -> Result { - if amount.is_zero() { - return Err(ContractError::InvalidZeroAmount {}); - } - - let config = CONFIG.load(deps.storage)?; - - let recipient = deps.api.addr_validate(&recipient_unchecked)?; - let messages = - core::transfer(deps.storage, &config, info.sender.clone(), recipient, amount, true)?; - - let res = Response::new() - .add_attribute("action", "transfer") - .add_attribute("from", info.sender) - .add_attribute("to", recipient_unchecked) - .add_attribute("amount", amount) - .add_messages(messages); - Ok(res) -} - -pub fn execute_transfer_on_liquidation( - deps: DepsMut, - _env: Env, - info: MessageInfo, - sender_unchecked: String, - recipient_unchecked: String, - amount: Uint128, -) -> Result { - // only red bank can call - let config = CONFIG.load(deps.storage)?; - if info.sender != config.red_bank_address { - return Err(ContractError::Unauthorized {}); - } - - let sender = deps.api.addr_validate(&sender_unchecked)?; - let recipient = deps.api.addr_validate(&recipient_unchecked)?; - - let messages = core::transfer(deps.storage, &config, sender, recipient, amount, false)?; - - let res = Response::new() - .add_messages(messages) - .add_attribute("action", "transfer") - .add_attribute("from", sender_unchecked) - .add_attribute("to", recipient_unchecked) - .add_attribute("amount", amount); - Ok(res) -} - -pub fn execute_burn( - deps: DepsMut, - _env: Env, - info: MessageInfo, - user_unchecked: String, - amount: Uint128, -) -> Result { - // only money market can burn - let config = CONFIG.load(deps.storage)?; - if info.sender != config.red_bank_address { - return Err(ContractError::Unauthorized {}); - } - - if amount.is_zero() { - return Err(ContractError::InvalidZeroAmount {}); - } - - // lower balance - let user_address = deps.api.addr_validate(&user_unchecked)?; - let user_balance_before = core::decrease_balance(deps.storage, &user_address, amount)?; - - // reduce total_supply - let mut total_supply_before = Uint128::zero(); - TOKEN_INFO.update(deps.storage, |mut info| -> StdResult<_> { - total_supply_before = info.total_supply; - info.total_supply = info.total_supply.checked_sub(amount)?; - Ok(info) - })?; - - let res = Response::new() - .add_message(core::balance_change_msg( - config.incentives_address, - user_address, - user_balance_before, - total_supply_before, - )?) - .add_attribute("action", "burn") - .add_attribute("user", user_unchecked) - .add_attribute("amount", amount); - Ok(res) -} - -pub fn execute_mint( - deps: DepsMut, - _env: Env, - info: MessageInfo, - recipient_unchecked: String, - amount: Uint128, -) -> Result { - if amount.is_zero() { - return Err(ContractError::InvalidZeroAmount {}); - } - - let mut token_info = TOKEN_INFO.load(deps.storage)?; - if token_info.mint.is_none() || token_info.mint.as_ref().unwrap().minter != info.sender { - return Err(ContractError::Unauthorized {}); - } - - let total_supply_before = token_info.total_supply; - - // update supply and enforce cap - token_info.total_supply += amount; - if let Some(limit) = token_info.get_cap() { - if token_info.total_supply > limit { - return Err(ContractError::CannotExceedCap {}); - } - } - TOKEN_INFO.save(deps.storage, &token_info)?; - - // add amount to recipient balance - let rcpt_address = deps.api.addr_validate(&recipient_unchecked)?; - let rcpt_balance_before = core::increase_balance(deps.storage, &rcpt_address, amount)?; - - let config = CONFIG.load(deps.storage)?; - - let res = Response::new() - .add_message(core::balance_change_msg( - config.incentives_address, - rcpt_address, - rcpt_balance_before, - total_supply_before, - )?) - .add_attribute("action", "mint") - .add_attribute("to", recipient_unchecked) - .add_attribute("amount", amount); - Ok(res) -} - -pub fn execute_send( - deps: DepsMut, - _env: Env, - info: MessageInfo, - contract_unchecked: String, - amount: Uint128, - msg: Binary, -) -> Result { - if amount.is_zero() { - return Err(ContractError::InvalidZeroAmount {}); - } - - // move the tokens to the contract - let config = CONFIG.load(deps.storage)?; - let contract_address = deps.api.addr_validate(&contract_unchecked)?; - - let transfer_messages = - core::transfer(deps.storage, &config, info.sender.clone(), contract_address, amount, true)?; - - let res = Response::new() - .add_attribute("action", "send") - .add_attribute("from", info.sender.to_string()) - .add_attribute("to", &contract_unchecked) - .add_attribute("amount", amount) - .add_messages(transfer_messages) - .add_message( - Cw20ReceiveMsg { - sender: info.sender.to_string(), - amount, - msg, - } - .into_cosmos_msg(contract_unchecked)?, - ); - - Ok(res) -} - -// QUERY - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { - match msg { - QueryMsg::Balance { - address, - } => to_binary(&query_balance(deps, address)?), - QueryMsg::BalanceAndTotalSupply { - address, - } => to_binary(&query_balance_and_total_supply(deps, address)?), - QueryMsg::TokenInfo {} => to_binary(&query_token_info(deps)?), - QueryMsg::Minter {} => to_binary(&query_minter(deps)?), - QueryMsg::Allowance { - owner, - spender, - } => to_binary(&query_allowance(deps, owner, spender)?), - QueryMsg::AllAllowances { - owner, - start_after, - limit, - } => to_binary(&query_owner_allowances(deps, owner, start_after, limit)?), - QueryMsg::AllAccounts { - start_after, - limit, - } => to_binary(&query_all_accounts(deps, start_after, limit)?), - QueryMsg::MarketingInfo {} => to_binary(&query_marketing_info(deps)?), - QueryMsg::DownloadLogo {} => to_binary(&query_download_logo(deps)?), - QueryMsg::UnderlyingAssetBalance { - address, - } => to_binary(&query_underlying_asset_balance(deps, env, address)?), - } -} - -fn query_balance_and_total_supply( - deps: Deps, - address_unchecked: String, -) -> StdResult { - let address = deps.api.addr_validate(&address_unchecked)?; - let balance = BALANCES.may_load(deps.storage, &address)?.unwrap_or_default(); - let info = TOKEN_INFO.load(deps.storage)?; - Ok(BalanceAndTotalSupplyResponse { - balance, - total_supply: info.total_supply, - }) -} - -pub fn query_underlying_asset_balance( - deps: Deps, - env: Env, - address: String, -) -> StdResult { - let address = deps.api.addr_validate(&address)?; - let balance = BALANCES.may_load(deps.storage, &address)?.unwrap_or_default(); - - let config = CONFIG.load(deps.storage)?; - - let query: Uint128 = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart { - contract_addr: config.red_bank_address.into(), - msg: to_binary(&red_bank::QueryMsg::UnderlyingLiquidityAmount { - ma_token_address: env.contract.address.into(), - amount_scaled: balance, - })?, - }))?; - - Ok(BalanceResponse { - balance: query, - }) -} - -#[cfg(test)] -mod tests { - use cosmwasm_std::testing::{ - mock_dependencies, mock_dependencies_with_balance, mock_env, mock_info, - }; - use cosmwasm_std::{coins, Addr, CosmosMsg, StdError, SubMsg, WasmMsg}; - - use cw20::{ - Cw20Coin, Logo, LogoInfo, MarketingInfoResponse, MinterResponse, TokenInfoResponse, - }; - use cw20_base::msg::InstantiateMarketingInfo; - - use super::*; - use crate::msg::InitHook; - use crate::test_helpers::{do_instantiate, do_instantiate_with_minter, get_balance}; - - mod instantiate { - use super::*; - - #[test] - fn basic() { - let mut deps = mock_dependencies(); - let amount = Uint128::from(11223344u128); - let hook_msg = Binary::from(r#"{"some": 123}"#.as_bytes()); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![Cw20Coin { - address: String::from("addr0000"), - amount, - }], - mint: None, - marketing: None, - init_hook: Some(InitHook { - contract_addr: String::from("hook_dest"), - msg: hook_msg.clone(), - }), - red_bank_address: String::from("red_bank"), - incentives_address: String::from("incentives"), - }; - let info = mock_info("creator", &[]); - let env = mock_env(); - let res = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap(); - assert_eq!( - res.messages, - vec![SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::from("hook_dest"), - msg: hook_msg, - funds: vec![], - }))] - ); - - assert_eq!( - query_token_info(deps.as_ref()).unwrap(), - TokenInfoResponse { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - total_supply: amount, - } - ); - assert_eq!(get_balance(deps.as_ref(), "addr0000"), Uint128::new(11223344)); - } - - #[test] - fn mintable() { - let mut deps = mock_dependencies(); - let amount = Uint128::new(11223344); - let minter = String::from("asmodat"); - let limit = Uint128::new(511223344); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![Cw20Coin { - address: "addr0000".into(), - amount, - }], - mint: Some(MinterResponse { - minter: minter.clone(), - cap: Some(limit), - }), - marketing: None, - init_hook: None, - red_bank_address: String::from("red_bank"), - incentives_address: String::from("incentives"), - }; - let info = mock_info("creator", &[]); - let env = mock_env(); - let res = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap(); - assert_eq!(0, res.messages.len()); - - assert_eq!( - query_token_info(deps.as_ref()).unwrap(), - TokenInfoResponse { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - total_supply: amount, - } - ); - assert_eq!(get_balance(deps.as_ref(), "addr0000"), Uint128::new(11223344)); - assert_eq!( - query_minter(deps.as_ref()).unwrap(), - Some(MinterResponse { - minter, - cap: Some(limit), - }), - ); - } - - #[test] - fn mintable_over_cap() { - let mut deps = mock_dependencies(); - let amount = Uint128::new(11223344); - let minter = String::from("asmodat"); - let limit = Uint128::new(11223300); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![Cw20Coin { - address: String::from("addr0000"), - amount, - }], - mint: Some(MinterResponse { - minter, - cap: Some(limit), - }), - marketing: None, - init_hook: None, - red_bank_address: String::from("red_bank"), - incentives_address: String::from("incentives"), - }; - let info = mock_info("creator", &[]); - let env = mock_env(); - let err = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap_err(); - assert_eq!(err, StdError::generic_err("Initial supply greater than cap").into()); - } - - mod marketing { - use super::*; - - #[test] - fn basic() { - let mut deps = mock_dependencies(); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![], - mint: None, - marketing: Some(InstantiateMarketingInfo { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some("marketing".to_owned()), - logo: Some(Logo::Url("url".to_owned())), - }), - init_hook: None, - red_bank_address: String::from("red_bank"), - incentives_address: String::from("incentives"), - }; - - let info = mock_info("creator", &[]); - let env = mock_env(); - let res = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap(); - assert_eq!(0, res.messages.len()); - - assert_eq!( - query_marketing_info(deps.as_ref()).unwrap(), - MarketingInfoResponse { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some(Addr::unchecked("marketing")), - logo: Some(LogoInfo::Url("url".to_owned())), - } - ); - - let err = query_download_logo(deps.as_ref()).unwrap_err(); - assert!( - matches!(err, StdError::NotFound { .. }), - "Expected StdError::NotFound, received {}", - err - ); - } - - #[test] - fn invalid_marketing() { - let mut deps = mock_dependencies(); - let instantiate_msg = InstantiateMsg { - name: "Cash Token".to_string(), - symbol: "CASH".to_string(), - decimals: 9, - initial_balances: vec![], - mint: None, - marketing: Some(InstantiateMarketingInfo { - project: Some("Project".to_owned()), - description: Some("Description".to_owned()), - marketing: Some("m".to_owned()), - logo: Some(Logo::Url("url".to_owned())), - }), - init_hook: None, - red_bank_address: String::from("red_bank"), - incentives_address: String::from("incentives"), - }; - - let info = mock_info("creator", &[]); - let env = mock_env(); - instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap_err(); - - let err = query_download_logo(deps.as_ref()).unwrap_err(); - assert!( - matches!(err, StdError::NotFound { .. }), - "Expected StdError::NotFound, received {}", - err - ); - } - } - } - - #[test] - fn can_mint_by_minter() { - let mut deps = mock_dependencies(); - - let genesis = String::from("genesis"); - let amount = Uint128::new(11223344); - let minter = String::from("asmodat"); - let limit = Uint128::new(511223344); - do_instantiate_with_minter(deps.as_mut(), &genesis, amount, &minter, Some(limit)); - - // minter can mint coins to some winner - let winner = String::from("lucky"); - let prize = Uint128::new(222_222_222); - let msg = ExecuteMsg::Mint { - recipient: winner.clone(), - amount: prize, - }; - - let info = mock_info(minter.as_ref(), &[]); - let env = mock_env(); - let res = execute(deps.as_mut(), env, info, msg).unwrap(); - - assert_eq!( - res.messages, - vec![SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::from("incentives"), - msg: to_binary(&mars_outpost::incentives::msg::ExecuteMsg::BalanceChange { - user_address: Addr::unchecked(&winner), - user_balance_before: Uint128::zero(), - total_supply_before: amount, - },) - .unwrap(), - funds: vec![], - })),] - ); - assert_eq!(get_balance(deps.as_ref(), genesis), amount); - assert_eq!(get_balance(deps.as_ref(), winner.clone()), prize); - assert_eq!(query_token_info(deps.as_ref()).unwrap().total_supply, amount + prize); - - // but cannot mint nothing - let msg = ExecuteMsg::Mint { - recipient: winner.clone(), - amount: Uint128::zero(), - }; - let info = mock_info(minter.as_ref(), &[]); - let env = mock_env(); - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(err, ContractError::InvalidZeroAmount {}); - - // but if it exceeds cap (even over multiple rounds), it fails - // cap is enforced - let msg = ExecuteMsg::Mint { - recipient: winner, - amount: Uint128::new(333_222_222), - }; - let info = mock_info(minter.as_ref(), &[]); - let env = mock_env(); - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(err, ContractError::CannotExceedCap {}); - } - - #[test] - fn others_cannot_mint() { - let mut deps = mock_dependencies(); - do_instantiate_with_minter( - deps.as_mut(), - &String::from("genesis"), - Uint128::new(1234), - &String::from("minter"), - None, - ); - - let msg = ExecuteMsg::Mint { - recipient: String::from("lucky"), - amount: Uint128::new(222), - }; - let info = mock_info("anyone else", &[]); - let env = mock_env(); - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(err, ContractError::Unauthorized {}); - } - - #[test] - fn no_one_mints_if_minter_unset() { - let mut deps = mock_dependencies(); - do_instantiate(deps.as_mut(), &String::from("genesis"), Uint128::new(1234)); - - let msg = ExecuteMsg::Mint { - recipient: String::from("lucky"), - amount: Uint128::new(222), - }; - let info = mock_info("genesis", &[]); - let env = mock_env(); - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(err, ContractError::Unauthorized {}); - } - - #[test] - fn instantiate_multiple_accounts() { - let mut deps = mock_dependencies(); - let amount1 = Uint128::from(11223344u128); - let addr1 = String::from("addr0001"); - let amount2 = Uint128::from(7890987u128); - let addr2 = String::from("addr0002"); - let instantiate_msg = InstantiateMsg { - name: "Bash Shell".to_string(), - symbol: "BASH".to_string(), - decimals: 6, - initial_balances: vec![ - Cw20Coin { - address: addr1.clone(), - amount: amount1, - }, - Cw20Coin { - address: addr2.clone(), - amount: amount2, - }, - ], - mint: None, - marketing: None, - init_hook: None, - red_bank_address: String::from("red_bank"), - incentives_address: String::from("incentives"), - }; - let info = mock_info("creator", &[]); - let env = mock_env(); - let res = instantiate(deps.as_mut(), env, info, instantiate_msg).unwrap(); - assert_eq!(0, res.messages.len()); - - assert_eq!( - query_token_info(deps.as_ref()).unwrap(), - TokenInfoResponse { - name: "Bash Shell".to_string(), - symbol: "BASH".to_string(), - decimals: 6, - total_supply: amount1 + amount2, - } - ); - assert_eq!(get_balance(deps.as_ref(), addr1), amount1); - assert_eq!(get_balance(deps.as_ref(), addr2), amount2); - } - - #[test] - fn transfer() { - let mut deps = mock_dependencies_with_balance(&coins(2, "token")); - let addr1 = String::from("addr0001"); - let addr2 = String::from("addr0002"); - let amount1 = Uint128::from(12340000u128); - let transfer = Uint128::from(76543u128); - let too_much = Uint128::from(12340321u128); - - do_instantiate(deps.as_mut(), &addr1, amount1); - - // cannot transfer nothing - let info = mock_info(addr1.as_ref(), &[]); - let env = mock_env(); - let msg = ExecuteMsg::Transfer { - recipient: addr2.clone(), - amount: Uint128::zero(), - }; - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(err, ContractError::InvalidZeroAmount {}); - - // cannot send more than we have - let info = mock_info(addr1.as_ref(), &[]); - let env = mock_env(); - let msg = ExecuteMsg::Transfer { - recipient: addr2.clone(), - amount: too_much, - }; - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); - - // cannot send from empty account - let info = mock_info(addr2.as_ref(), &[]); - let env = mock_env(); - let msg = ExecuteMsg::Transfer { - recipient: addr1.clone(), - amount: transfer, - }; - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); - - // cannot send to self - let info = mock_info(addr1.as_ref(), &[]); - let env = mock_env(); - let msg = ExecuteMsg::Transfer { - recipient: addr1.clone(), - amount: transfer, - }; - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!( - err, - ContractError::Std(StdError::generic_err("Sender and recipient cannot be the same")) - ); - - // valid transfer - let info = mock_info(addr1.as_ref(), &[]); - let env = mock_env(); - let msg = ExecuteMsg::Transfer { - recipient: addr2.clone(), - amount: transfer, - }; - let res = execute(deps.as_mut(), env, info, msg).unwrap(); - assert_eq!( - res.messages, - vec![ - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::from("red_bank"), - msg: to_binary(&red_bank::ExecuteMsg::FinalizeLiquidityTokenTransfer { - sender_address: Addr::unchecked(&addr1), - recipient_address: Addr::unchecked(&addr2), - sender_previous_balance: amount1, - recipient_previous_balance: Uint128::zero(), - amount: transfer, - }) - .unwrap(), - funds: vec![], - })), - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::from("incentives"), - msg: to_binary(&mars_outpost::incentives::msg::ExecuteMsg::BalanceChange { - user_address: Addr::unchecked(&addr1), - user_balance_before: amount1, - total_supply_before: amount1, - },) - .unwrap(), - funds: vec![], - })), - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::from("incentives"), - msg: to_binary(&mars_outpost::incentives::msg::ExecuteMsg::BalanceChange { - user_address: Addr::unchecked(&addr2), - user_balance_before: Uint128::zero(), - total_supply_before: amount1, - },) - .unwrap(), - funds: vec![], - })), - ], - ); - - let remainder = amount1.checked_sub(transfer).unwrap(); - assert_eq!(get_balance(deps.as_ref(), addr1), remainder); - assert_eq!(get_balance(deps.as_ref(), addr2), transfer); - assert_eq!(query_token_info(deps.as_ref()).unwrap().total_supply, amount1); - } - - #[test] - fn transfer_on_liquidation() { - let mut deps = mock_dependencies_with_balance(&coins(2, "token")); - let addr1 = String::from("addr0001"); - let addr2 = String::from("addr0002"); - let amount1 = Uint128::from(12340000u128); - let transfer = Uint128::from(76543u128); - let too_much = Uint128::from(12340321u128); - - do_instantiate(deps.as_mut(), &addr1, amount1); - - // cannot transfer nothing - { - let info = mock_info("red_bank", &[]); - let env = mock_env(); - let msg = ExecuteMsg::TransferOnLiquidation { - sender: addr1.clone(), - recipient: addr2.clone(), - amount: Uint128::zero(), - }; - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(err, ContractError::InvalidZeroAmount {}); - } - - // cannot send more than we have - { - let info = mock_info("red_bank", &[]); - let env = mock_env(); - let msg = ExecuteMsg::TransferOnLiquidation { - sender: addr1.clone(), - recipient: addr2.clone(), - amount: too_much, - }; - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); - } - - // cannot send from empty account - { - let info = mock_info("red_bank", &[]); - let env = mock_env(); - let msg = ExecuteMsg::TransferOnLiquidation { - sender: addr2.clone(), - recipient: addr1.clone(), - amount: transfer, - }; - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); - } - - // only money market can call transfer on liquidation - { - let info = mock_info(addr1.as_ref(), &[]); - let env = mock_env(); - let msg = ExecuteMsg::TransferOnLiquidation { - sender: addr1.clone(), - recipient: addr2.clone(), - amount: transfer, - }; - let res_error = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(res_error, ContractError::Unauthorized {}); - } - - // valid transfer on liquidation - { - let info = mock_info("red_bank", &[]); - let env = mock_env(); - let msg = ExecuteMsg::TransferOnLiquidation { - sender: addr1.clone(), - recipient: addr2.clone(), - amount: transfer, - }; - let res = execute(deps.as_mut(), env, info, msg).unwrap(); - assert_eq!( - res.messages, - vec![ - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::from("incentives"), - msg: to_binary(&mars_outpost::incentives::msg::ExecuteMsg::BalanceChange { - user_address: Addr::unchecked(&addr1), - user_balance_before: amount1, - total_supply_before: amount1, - },) - .unwrap(), - funds: vec![], - })), - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::from("incentives"), - msg: to_binary(&mars_outpost::incentives::msg::ExecuteMsg::BalanceChange { - user_address: Addr::unchecked(&addr2), - user_balance_before: Uint128::zero(), - total_supply_before: amount1, - },) - .unwrap(), - funds: vec![], - })), - ] - ); - - let remainder = amount1.checked_sub(transfer).unwrap(); - assert_eq!(get_balance(deps.as_ref(), addr1), remainder); - assert_eq!(get_balance(deps.as_ref(), addr2), transfer); - assert_eq!(query_token_info(deps.as_ref()).unwrap().total_supply, amount1); - } - } - - #[test] - fn burn() { - let mut deps = mock_dependencies_with_balance(&coins(2, "token")); - let addr1 = String::from("addr0001"); - let amount1 = Uint128::from(12340000u128); - let burn = Uint128::from(76543u128); - let too_much = Uint128::from(12340321u128); - - do_instantiate(deps.as_mut(), &addr1, amount1); - - // cannot burn nothing - let info = mock_info("red_bank", &[]); - let env = mock_env(); - let msg = ExecuteMsg::Burn { - user: addr1.clone(), - amount: Uint128::zero(), - }; - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(err, ContractError::InvalidZeroAmount {}); - assert_eq!(query_token_info(deps.as_ref()).unwrap().total_supply, amount1); - - // cannot burn more than we have - let info = mock_info("red_bank", &[]); - let env = mock_env(); - let msg = ExecuteMsg::Burn { - user: addr1.clone(), - amount: too_much, - }; - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); - assert_eq!(query_token_info(deps.as_ref()).unwrap().total_supply, amount1); - - // only red bank can burn - let info = mock_info(addr1.as_ref(), &[]); - let env = mock_env(); - let msg = ExecuteMsg::Burn { - user: addr1.clone(), - amount: burn, - }; - let res_error = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(res_error, ContractError::Unauthorized {}); - assert_eq!(query_token_info(deps.as_ref()).unwrap().total_supply, amount1); - - // valid burn reduces total supply - let info = mock_info("red_bank", &[]); - let env = mock_env(); - let msg = ExecuteMsg::Burn { - user: addr1.clone(), - amount: burn, - }; - let res = execute(deps.as_mut(), env, info, msg).unwrap(); - assert_eq!( - res.messages, - vec![SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::from("incentives"), - msg: to_binary(&mars_outpost::incentives::msg::ExecuteMsg::BalanceChange { - user_address: Addr::unchecked(&addr1), - user_balance_before: amount1, - total_supply_before: amount1, - },) - .unwrap(), - funds: vec![], - })),] - ); - - let remainder = amount1.checked_sub(burn).unwrap(); - assert_eq!(get_balance(deps.as_ref(), addr1), remainder); - assert_eq!(query_token_info(deps.as_ref()).unwrap().total_supply, remainder); - } - - #[test] - fn send() { - let mut deps = mock_dependencies_with_balance(&coins(2, "token")); - let addr1 = String::from("addr0001"); - let contract = String::from("addr0002"); - let amount1 = Uint128::from(12340000u128); - let transfer = Uint128::from(76543u128); - let too_much = Uint128::from(12340321u128); - let send_msg = Binary::from(r#"{"some":123}"#.as_bytes()); - - do_instantiate(deps.as_mut(), &addr1, amount1); - - // cannot send nothing - let info = mock_info(addr1.as_ref(), &[]); - let env = mock_env(); - let msg = ExecuteMsg::Send { - contract: contract.clone(), - amount: Uint128::zero(), - msg: send_msg.clone(), - }; - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(err, ContractError::InvalidZeroAmount {}); - - // cannot send more than we have - let info = mock_info(addr1.as_ref(), &[]); - let env = mock_env(); - let msg = ExecuteMsg::Send { - contract: contract.clone(), - amount: too_much, - msg: send_msg.clone(), - }; - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert!(matches!(err, ContractError::Std(StdError::Overflow { .. }))); - - // valid transfer - let info = mock_info(addr1.as_ref(), &[]); - let env = mock_env(); - let msg = ExecuteMsg::Send { - contract: contract.clone(), - amount: transfer, - msg: send_msg.clone(), - }; - let res = execute(deps.as_mut(), env, info, msg).unwrap(); - - // ensure proper send message sent - // this is the message we want delivered to the other side - let binary_msg = Cw20ReceiveMsg { - sender: addr1.clone(), - amount: transfer, - msg: send_msg, - } - .into_binary() - .unwrap(); - - assert_eq!( - res.messages, - vec![ - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::from("red_bank"), - msg: to_binary( - &mars_outpost::red_bank::ExecuteMsg::FinalizeLiquidityTokenTransfer { - sender_address: Addr::unchecked(&addr1), - recipient_address: Addr::unchecked(&contract), - sender_previous_balance: amount1, - recipient_previous_balance: Uint128::zero(), - amount: transfer, - } - ) - .unwrap(), - funds: vec![], - })), - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::from("incentives"), - - msg: to_binary(&mars_outpost::incentives::msg::ExecuteMsg::BalanceChange { - user_address: Addr::unchecked(&addr1), - user_balance_before: amount1, - total_supply_before: amount1, - },) - .unwrap(), - funds: vec![], - })), - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: String::from("incentives"), - msg: to_binary(&mars_outpost::incentives::msg::ExecuteMsg::BalanceChange { - user_address: Addr::unchecked(&contract), - user_balance_before: Uint128::zero(), - total_supply_before: amount1, - },) - .unwrap(), - funds: vec![], - })), - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: contract.clone(), - msg: binary_msg, - funds: vec![], - })), - ] - ); - - // ensure balance is properly transferred - let remainder = amount1.checked_sub(transfer).unwrap(); - assert_eq!(get_balance(deps.as_ref(), addr1), remainder); - assert_eq!(get_balance(deps.as_ref(), contract), transfer); - assert_eq!(query_token_info(deps.as_ref()).unwrap().total_supply, amount1); - } -} diff --git a/contracts/ma-token/src/core.rs b/contracts/ma-token/src/core.rs deleted file mode 100644 index 11afcf8d..00000000 --- a/contracts/ma-token/src/core.rs +++ /dev/null @@ -1,127 +0,0 @@ -use cosmwasm_std::{to_binary, Addr, CosmosMsg, StdError, StdResult, Storage, Uint128, WasmMsg}; - -use cw20_base::state::{BALANCES, TOKEN_INFO}; -use cw20_base::ContractError; - -use crate::Config; - -/// Deduct amount from sender balance and add it to recipient balance -/// Returns messages to be sent on the final response -pub fn transfer( - storage: &mut dyn Storage, - config: &Config, - sender_address: Addr, - recipient_address: Addr, - amount: Uint128, - finalize_on_red_bank: bool, -) -> Result, ContractError> { - if sender_address == recipient_address { - return Err(StdError::generic_err("Sender and recipient cannot be the same").into()); - } - - if amount.is_zero() { - return Err(ContractError::InvalidZeroAmount {}); - } - - let sender_previous_balance = decrease_balance(storage, &sender_address, amount)?; - - let recipient_previous_balance = increase_balance(storage, &recipient_address, amount)?; - - let total_supply = TOKEN_INFO.load(storage)?.total_supply; - - let mut messages = vec![]; - - // If the transfer results from a method called on the money market, - // it is finalized there. Else it needs to update state and perform some validations - // to ensure the transfer can be executed - if finalize_on_red_bank { - messages.push(finalize_transfer_msg( - config.red_bank_address.clone(), - sender_address.clone(), - recipient_address.clone(), - sender_previous_balance, - recipient_previous_balance, - amount, - )?); - } - - // Build incentives messagess - messages.push(balance_change_msg( - config.incentives_address.clone(), - sender_address, - sender_previous_balance, - total_supply, - )?); - messages.push(balance_change_msg( - config.incentives_address.clone(), - recipient_address, - recipient_previous_balance, - total_supply, - )?); - - Ok(messages) -} - -/// Lower user balance and commit to store, returns previous balance -pub fn decrease_balance( - storage: &mut dyn Storage, - address: &Addr, - amount: Uint128, -) -> Result { - let previous_balance = BALANCES.load(storage, address).unwrap_or_default(); - let new_balance = previous_balance.checked_sub(amount)?; - BALANCES.save(storage, address, &new_balance)?; - - Ok(previous_balance) -} - -/// Increase user balance and commit to store, returns previous balance -pub fn increase_balance( - storage: &mut dyn Storage, - address: &Addr, - amount: Uint128, -) -> Result { - let previous_balance = BALANCES.load(storage, address).unwrap_or_default(); - let new_balance = previous_balance + amount; - BALANCES.save(storage, address, &new_balance)?; - - Ok(previous_balance) -} - -pub fn finalize_transfer_msg( - red_bank_address: Addr, - sender_address: Addr, - recipient_address: Addr, - sender_previous_balance: Uint128, - recipient_previous_balance: Uint128, - amount: Uint128, -) -> StdResult { - Ok(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: red_bank_address.into(), - msg: to_binary(&mars_outpost::red_bank::ExecuteMsg::FinalizeLiquidityTokenTransfer { - sender_address, - recipient_address, - sender_previous_balance, - recipient_previous_balance, - amount, - })?, - funds: vec![], - })) -} - -pub fn balance_change_msg( - incentives_address: Addr, - user_address: Addr, - user_balance_before: Uint128, - total_supply_before: Uint128, -) -> StdResult { - Ok(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: incentives_address.into(), - msg: to_binary(&mars_outpost::incentives::msg::ExecuteMsg::BalanceChange { - user_address, - user_balance_before, - total_supply_before, - })?, - funds: vec![], - })) -} diff --git a/contracts/ma-token/src/lib.rs b/contracts/ma-token/src/lib.rs deleted file mode 100755 index b2d1dcc4..00000000 --- a/contracts/ma-token/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod allowances; -pub mod contract; -pub mod core; -pub mod state; - -#[cfg(test)] -mod test_helpers; - -pub use mars_outpost::ma_token::*; diff --git a/contracts/ma-token/src/state.rs b/contracts/ma-token/src/state.rs deleted file mode 100755 index 2bba5ba4..00000000 --- a/contracts/ma-token/src/state.rs +++ /dev/null @@ -1,6 +0,0 @@ -/// state: contains state specific to ma_token (not included in cw20_base) -use cw_storage_plus::Item; - -use crate::Config; - -pub const CONFIG: Item = Item::new("config"); diff --git a/contracts/ma-token/src/test_helpers.rs b/contracts/ma-token/src/test_helpers.rs deleted file mode 100644 index 65601b82..00000000 --- a/contracts/ma-token/src/test_helpers.rs +++ /dev/null @@ -1,78 +0,0 @@ -use cosmwasm_std::{Deps, DepsMut, Uint128}; - -use cosmwasm_std::testing::{mock_env, mock_info}; - -use cw20::{Cw20Coin, MinterResponse, TokenInfoResponse}; -use cw20_base::contract::{query_balance, query_minter, query_token_info}; - -use crate::contract::instantiate; -use crate::msg::InstantiateMsg; - -pub fn get_balance>(deps: Deps, address: T) -> Uint128 { - query_balance(deps, address.into()).unwrap().balance -} - -// this will set up the instantiation for other tests -pub fn do_instantiate_with_minter( - deps: DepsMut, - addr: &str, - amount: Uint128, - minter: &str, - cap: Option, -) -> TokenInfoResponse { - _do_instantiate( - deps, - addr, - amount, - Some(MinterResponse { - minter: minter.to_string(), - cap, - }), - ) -} - -// this will set up the instantiation for other tests -pub fn do_instantiate(deps: DepsMut, addr: &str, amount: Uint128) -> TokenInfoResponse { - _do_instantiate(deps, addr, amount, None) -} - -// this will set up the instantiation for other tests -fn _do_instantiate( - mut deps: DepsMut, - addr: &str, - amount: Uint128, - mint: Option, -) -> TokenInfoResponse { - let instantiate_msg = InstantiateMsg { - name: "Auto Gen".to_string(), - symbol: "AUTO".to_string(), - decimals: 3, - initial_balances: vec![Cw20Coin { - address: addr.to_string(), - amount, - }], - mint: mint.clone(), - marketing: None, - init_hook: None, - red_bank_address: String::from("red_bank"), - incentives_address: String::from("incentives"), - }; - let info = mock_info("creator", &[]); - let env = mock_env(); - let res = instantiate(deps.branch(), env, info, instantiate_msg).unwrap(); - assert_eq!(0, res.messages.len()); - - let meta = query_token_info(deps.as_ref()).unwrap(); - assert_eq!( - meta, - TokenInfoResponse { - name: "Auto Gen".to_string(), - symbol: "AUTO".to_string(), - decimals: 3, - total_supply: amount, - } - ); - assert_eq!(get_balance(deps.as_ref(), addr), amount); - assert_eq!(query_minter(deps.as_ref()).unwrap(), mint,); - meta -} diff --git a/contracts/red-bank/Cargo.toml b/contracts/red-bank/Cargo.toml index a87b4993..f1cc2628 100644 --- a/contracts/red-bank/Cargo.toml +++ b/contracts/red-bank/Cargo.toml @@ -29,8 +29,6 @@ mars-outpost = { path = "../../packages/outpost", version = "0.1.0" } mars-health = { path = "../../packages/health", version = "0.1.0" } cosmwasm-std = "1.0" -cw20 = "0.14" -cw20-base = { version = "0.14", features = ["library"] } cw-storage-plus = "0.14" cw-utils = "0.14" serde = { version = "1.0.144", default-features = false, features = ["derive"] } diff --git a/contracts/red-bank/src/contract.rs b/contracts/red-bank/src/contract.rs index ac97dac6..d53686f6 100644 --- a/contracts/red-bank/src/contract.rs +++ b/contracts/red-bank/src/contract.rs @@ -29,16 +29,12 @@ pub fn execute( } => execute::update_config(deps, info, config), ExecuteMsg::InitAsset { denom, - asset_params, - asset_symbol, - } => execute::init_asset(deps, env, info, denom, asset_params, asset_symbol), - ExecuteMsg::InitAssetTokenCallback { - denom, - } => execute::init_asset_token_callback(deps, info, denom), + params, + } => execute::init_asset(deps, env, info, denom, params), ExecuteMsg::UpdateAsset { denom, - asset_params, - } => execute::update_asset(deps, env, info, denom, asset_params), + params, + } => execute::update_asset(deps, env, info, denom, params), ExecuteMsg::UpdateUncollateralizedLoanLimit { user, denom, @@ -89,22 +85,6 @@ pub fn execute( denom, enable, } => execute::update_asset_collateral_status(deps, env, info, denom, enable), - ExecuteMsg::FinalizeLiquidityTokenTransfer { - sender_address, - recipient_address, - sender_previous_balance, - recipient_previous_balance, - amount, - } => execute::finalize_liquidity_token_transfer( - deps, - env, - info, - sender_address, - recipient_address, - sender_previous_balance, - recipient_previous_balance, - amount, - ), } } @@ -159,7 +139,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { denom, } => { let user_addr = deps.api.addr_validate(&user)?; - to_binary(&query::query_user_collateral(deps, user_addr, denom)?) + to_binary(&query::query_user_collateral(deps, &env.block, user_addr, denom)?) } QueryMsg::UserCollaterals { user, @@ -167,7 +147,13 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { limit, } => { let user_addr = deps.api.addr_validate(&user)?; - to_binary(&query::query_user_collaterals(deps, user_addr, start_after, limit)?) + to_binary(&query::query_user_collaterals( + deps, + &env.block, + user_addr, + start_after, + limit, + )?) } QueryMsg::UserPosition { user, @@ -184,14 +170,9 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { amount, } => to_binary(&query::query_scaled_debt_amount(deps, env, denom, amount)?), QueryMsg::UnderlyingLiquidityAmount { - ma_token_address, - amount_scaled, - } => to_binary(&query::query_underlying_liquidity_amount( - deps, - env, - ma_token_address, + denom, amount_scaled, - )?), + } => to_binary(&query::query_underlying_liquidity_amount(deps, env, denom, amount_scaled)?), QueryMsg::UnderlyingDebtAmount { denom, amount_scaled, diff --git a/contracts/red-bank/src/execute.rs b/contracts/red-bank/src/execute.rs index 87acf79f..92284c30 100644 --- a/contracts/red-bank/src/execute.rs +++ b/contracts/red-bank/src/execute.rs @@ -1,37 +1,27 @@ use std::str; -use cosmwasm_std::{ - to_binary, Addr, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Response, StdResult, Uint128, - WasmMsg, -}; -use cw20::{Cw20ExecuteMsg, MinterResponse}; -use cw20_base::msg::InstantiateMarketingInfo; +use cosmwasm_std::{Addr, Decimal, DepsMut, Env, MessageInfo, Response, StdResult, Uint128}; use mars_outpost::address_provider::{self, MarsContract}; use mars_outpost::error::MarsError; -use mars_outpost::helpers::{ - build_send_asset_msg, cw20_get_balance, option_string_to_addr, zero_address, -}; +use mars_outpost::helpers::{build_send_asset_msg, option_string_to_addr, zero_address}; +use mars_outpost::math; use mars_outpost::red_bank::{ - Collateral, Config, CreateOrUpdateConfig, Debt, ExecuteMsg, InitOrUpdateAssetParams, - InstantiateMsg, Market, + Config, CreateOrUpdateConfig, Debt, InitOrUpdateAssetParams, InstantiateMsg, Market, }; -use mars_outpost::{ma_token, math}; use crate::error::ContractError; use crate::health::{ assert_below_liq_threshold_after_withdraw, assert_below_max_ltv_after_borrow, assert_liquidatable, }; -use crate::helpers::query_total_deposits; + use crate::interest_rates::{ apply_accumulated_interests, get_scaled_debt_amount, get_scaled_liquidity_amount, get_underlying_debt_amount, get_underlying_liquidity_amount, update_interest_rates, }; -use crate::state::{ - user_is_borrowing, COLLATERALS, CONFIG, DEBTS, MARKETS, MARKET_DENOMS_BY_MA_TOKEN, - UNCOLLATERALIZED_LOAN_LIMITS, -}; +use crate::state::{COLLATERALS, CONFIG, DEBTS, MARKETS, UNCOLLATERALIZED_LOAN_LIMITS}; +use crate::user::User; pub fn instantiate(deps: DepsMut, msg: InstantiateMsg) -> Result { // Destructuring a struct’s fields into separate variables in order to force @@ -39,15 +29,11 @@ pub fn instantiate(deps: DepsMut, msg: InstantiateMsg) -> Result Result, + params: InitOrUpdateAssetParams, ) -> Result { let config = CONFIG.load(deps.storage)?; @@ -123,59 +105,10 @@ pub fn init_asset( return Err(ContractError::AssetAlreadyInitialized {}); } - let new_market = create_market(env.block.time.seconds(), &denom, asset_params)?; + let new_market = create_market(env.block.time.seconds(), &denom, params)?; MARKETS.save(deps.storage, &denom, &new_market)?; - let symbol = asset_symbol_option.unwrap_or_else(|| denom.clone()); - - // Prepare response, should instantiate an maToken - // and use the Register hook. - // A new maToken should be created which callbacks this contract in order to be registered. - let addresses = address_provider::helpers::query_addresses( - deps.as_ref(), - &config.address_provider, - vec![MarsContract::Incentives, MarsContract::ProtocolAdmin], - )?; - // TODO: protocol admin may be a marshub address, which can't be validated into `Addr` - let protocol_admin_addr = &addresses[&MarsContract::ProtocolAdmin]; - let incentives_addr = &addresses[&MarsContract::Incentives]; - - let token_symbol = format!("ma{}", symbol); - Ok(Response::new() - .add_message(CosmosMsg::Wasm(WasmMsg::Instantiate { - admin: Some(protocol_admin_addr.to_string()), - code_id: config.ma_token_code_id, - msg: to_binary(&ma_token::msg::InstantiateMsg { - name: format!("Mars {} Liquidity Token", symbol), - symbol: token_symbol.clone(), - decimals: 6, - initial_balances: vec![], - mint: Some(MinterResponse { - minter: env.contract.address.to_string(), - cap: None, - }), - marketing: Some(InstantiateMarketingInfo { - project: Some(String::from("Mars Protocol")), - description: Some(format!( - "Interest earning token representing deposits for {}", - symbol - )), - marketing: Some(protocol_admin_addr.to_string()), - logo: None, - }), - init_hook: Some(ma_token::msg::InitHook { - contract_addr: env.contract.address.to_string(), - msg: to_binary(&ExecuteMsg::InitAssetTokenCallback { - denom: denom.clone(), - })?, - }), - red_bank_address: env.contract.address.to_string(), - incentives_address: incentives_addr.into(), - })?, - funds: vec![], - label: token_symbol, - })) .add_attribute("action", "outposts/red-bank/init_asset") .add_attribute("denom", denom)) } @@ -216,7 +149,6 @@ pub fn create_market( let new_market = Market { denom: denom.to_string(), - ma_token_address: Addr::unchecked(""), borrow_index: Decimal::one(), liquidity_index: Decimal::one(), borrow_rate: borrow_rate.unwrap(), @@ -224,6 +156,7 @@ pub fn create_market( max_loan_to_value: max_loan_to_value.unwrap(), reserve_factor: reserve_factor.unwrap(), indexes_last_updated: block_time, + collateral_total_scaled: Uint128::zero(), debt_total_scaled: Uint128::zero(), liquidation_threshold: liquidation_threshold.unwrap(), liquidation_bonus: liquidation_bonus.unwrap(), @@ -239,36 +172,13 @@ pub fn create_market( Ok(new_market) } -pub fn init_asset_token_callback( - deps: DepsMut, - info: MessageInfo, - denom: String, -) -> Result { - let mut market = MARKETS.load(deps.storage, &denom)?; - - if market.ma_token_address == zero_address() { - let ma_contract_addr = info.sender; - - market.ma_token_address = ma_contract_addr.clone(); - MARKETS.save(deps.storage, &denom, &market)?; - - // save ma token contract to reference mapping - MARKET_DENOMS_BY_MA_TOKEN.save(deps.storage, &ma_contract_addr, &denom)?; - - Ok(Response::new().add_attribute("action", "outposts/red-bank/init_asset_token_callback")) - } else { - // Can do this only once - Err(MarsError::Unauthorized {}.into()) - } -} - /// Update asset with new params. pub fn update_asset( deps: DepsMut, env: Env, info: MessageInfo, denom: String, - asset_params: InitOrUpdateAssetParams, + params: InitOrUpdateAssetParams, ) -> Result { let config = CONFIG.load(deps.storage)?; @@ -292,7 +202,7 @@ pub fn update_asset( deposit_enabled, borrow_enabled, deposit_cap, - } = asset_params; + } = params; // If reserve factor or interest rates are updated we update indexes with // current values before applying the change to prevent applying this @@ -310,11 +220,11 @@ pub fn update_asset( &config.address_provider, MarsContract::ProtocolRewardsCollector, )?; - response = apply_accumulated_interests( + apply_accumulated_interests( + deps.storage, &env, &protocol_rewards_collector_addr, &mut market, - response, )?; } @@ -408,10 +318,12 @@ pub fn deposit( denom: String, deposit_amount: Uint128, ) -> Result { - let user_addr = if let Some(address) = on_behalf_of { - deps.api.addr_validate(&address)? + let user_addr: Addr; + let user = if let Some(address) = on_behalf_of { + user_addr = deps.api.addr_validate(&address)?; + User(&user_addr) } else { - info.sender.clone() + User(&info.sender) }; let mut market = MARKETS.load(deps.storage, &denom)?; @@ -421,7 +333,7 @@ pub fn deposit( }); } - let total_scaled_deposits = query_total_deposits(&deps.querier, &market.ma_token_address)?; + let total_scaled_deposits = market.collateral_total_scaled; let total_deposits = get_underlying_liquidity_amount(total_scaled_deposits, &market, env.block.time.seconds())?; if total_deposits.checked_add(deposit_amount)? > market.deposit_cap { @@ -430,19 +342,6 @@ pub fn deposit( }); } - // Update the user's collateral status - // TODO: Currently, the logic is: - // - If the user already has a collateral position, do nothing; - // - If not, initialize a new one with `enable` default to `true`. - // We don't increment the user's collateral shares here, as the collateral shares are tokenized - // as maTokens. - // Once maToken is removed, we increment the user's collateral shares here. - COLLATERALS.update(deps.storage, (&user_addr, &denom), |collateral| -> StdResult<_> { - Ok(collateral.unwrap_or(Collateral { - enabled: true, - })) - })?; - let mut response = Response::new(); let config = CONFIG.load(deps.storage)?; @@ -453,30 +352,27 @@ pub fn deposit( &config.address_provider, MarsContract::ProtocolRewardsCollector, )?; - response = apply_accumulated_interests(&env, &rewards_collector_addr, &mut market, response)?; + apply_accumulated_interests(deps.storage, &env, &rewards_collector_addr, &mut market)?; response = update_interest_rates(&deps, &env, &mut market, Uint128::zero(), &denom, response)?; - MARKETS.save(deps.storage, &denom, &market)?; if market.liquidity_index.is_zero() { return Err(ContractError::InvalidLiquidityIndex {}); } - let mint_amount = + let deposit_amount_scaled = get_scaled_liquidity_amount(deposit_amount, &market, env.block.time.seconds())?; + user.increase_collateral(deps.storage, &denom, deposit_amount_scaled)?; + + market.increase_collateral(deposit_amount_scaled)?; + MARKETS.save(deps.storage, &denom, &market)?; + Ok(response - .add_message(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: market.ma_token_address.into(), - msg: to_binary(&Cw20ExecuteMsg::Mint { - recipient: user_addr.to_string(), - amount: mint_amount, - })?, - funds: vec![], - })) .add_attribute("action", "outposts/red-bank/deposit") + .add_attribute("sender", &info.sender) + .add_attribute("on_behalf_of", user) .add_attribute("denom", denom) - .add_attribute("sender", info.sender) - .add_attribute("user", user_addr) - .add_attribute("amount", deposit_amount)) + .add_attribute("amount", deposit_amount) + .add_attribute("amount_scaled", deposit_amount_scaled)) } /// Burns sent maAsset in exchange of underlying asset @@ -488,17 +384,16 @@ pub fn withdraw( amount: Option, recipient: Option, ) -> Result { - let withdrawer_addr = info.sender; + let withdrawer = User(&info.sender); let mut market = MARKETS.load(deps.storage, &denom)?; - let asset_ma_addr = market.ma_token_address.clone(); - let withdrawer_balance_scaled_before = - cw20_get_balance(&deps.querier, asset_ma_addr, withdrawer_addr.clone())?; + let collateral = withdrawer.collateral(deps.storage, &denom)?; + let withdrawer_balance_scaled_before = collateral.amount_scaled; if withdrawer_balance_scaled_before.is_zero() { return Err(ContractError::UserNoCollateralBalance { - user: withdrawer_addr.into(), + user: withdrawer.into(), denom, }); } @@ -535,19 +430,14 @@ pub fn withdraw( let rewards_collector_addr = &addresses[&MarsContract::ProtocolRewardsCollector]; let oracle_addr = &addresses[&MarsContract::Oracle]; - // NOTE: this load command currently doesn't work for the rewards collector, which doesn't have - // a collateral position. however, this will be automatically solved in a later PR, once we - // remove maToken and store collateral shares here in Red Bank. - let collateral = COLLATERALS.load(deps.storage, (&withdrawer_addr, &denom))?; - // if asset is used as collateral and user is borrowing we need to validate health factor after withdraw, // otherwise no reasons to block the withdraw if collateral.enabled - && user_is_borrowing(deps.storage, &withdrawer_addr) + && withdrawer.is_borrowing(deps.storage) && !assert_below_liq_threshold_after_withdraw( &deps.as_ref(), &env, - &withdrawer_addr, + withdrawer.address(), oracle_addr, &denom, withdraw_amount, @@ -559,46 +449,37 @@ pub fn withdraw( let mut response = Response::new(); // update indexes and interest rates - response = apply_accumulated_interests(&env, rewards_collector_addr, &mut market, response)?; + apply_accumulated_interests(deps.storage, &env, rewards_collector_addr, &mut market)?; response = update_interest_rates(&deps, &env, &mut market, withdraw_amount, &denom, response)?; - MARKETS.save(deps.storage, &denom, &market)?; - // burn maToken + // reduce the withdrawer's scaled collateral amount let withdrawer_balance_after = withdrawer_balance_before.checked_sub(withdraw_amount)?; let withdrawer_balance_scaled_after = get_scaled_liquidity_amount(withdrawer_balance_after, &market, env.block.time.seconds())?; - let burn_amount = + let withdraw_amount_scaled = withdrawer_balance_scaled_before.checked_sub(withdrawer_balance_scaled_after)?; - response = response.add_message(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: market.ma_token_address.to_string(), - msg: to_binary(&ma_token::msg::ExecuteMsg::Burn { - user: withdrawer_addr.to_string(), - amount: burn_amount, - })?, - funds: vec![], - })); - - // if the user's maToken amount is reduced to zero, then delete the collateral position - if withdrawer_balance_scaled_after.is_zero() { - COLLATERALS.remove(deps.storage, (&withdrawer_addr, &denom)); - } + + withdrawer.decrease_collateral(deps.storage, &denom, withdraw_amount_scaled)?; + + market.decrease_collateral(withdraw_amount_scaled)?; + MARKETS.save(deps.storage, &denom, &market)?; // send underlying asset to user or another recipient let recipient_addr = if let Some(recipient) = recipient { deps.api.addr_validate(&recipient)? } else { - withdrawer_addr.clone() + withdrawer.address().clone() }; Ok(response .add_message(build_send_asset_msg(&recipient_addr, &denom, withdraw_amount)) .add_attribute("action", "outposts/red-bank/withdraw") - .add_attribute("denom", denom) - .add_attribute("user", withdrawer_addr) + .add_attribute("sender", withdrawer) .add_attribute("recipient", recipient_addr) - .add_attribute("burn_amount", burn_amount) - .add_attribute("withdraw_amount", withdraw_amount)) + .add_attribute("denom", denom) + .add_attribute("amount", withdraw_amount) + .add_attribute("amount_scaled", withdraw_amount_scaled)) } /// Add debt for the borrower and send the borrowed funds @@ -610,7 +491,7 @@ pub fn borrow( borrow_amount: Uint128, recipient: Option, ) -> Result { - let borrower_addr = info.sender; + let borrower = User(&info.sender); // Cannot borrow zero amount if borrow_amount.is_zero() { @@ -628,9 +509,7 @@ pub fn borrow( }); } - let uncollateralized_loan_limit = UNCOLLATERALIZED_LOAN_LIMITS - .may_load(deps.storage, (&borrower_addr, &denom))? - .unwrap_or_else(Uint128::zero); + let uncollateralized_loan_limit = borrower.uncollateralized_loan_limit(deps.storage, &denom)?; let config = CONFIG.load(deps.storage)?; @@ -648,7 +527,7 @@ pub fn borrow( if !assert_below_max_ltv_after_borrow( &deps.as_ref(), &env, - &borrower_addr, + borrower.address(), oracle_addr, &denom, borrow_amount, @@ -659,15 +538,11 @@ pub fn borrow( // Uncollateralized loan: check borrow amount plus debt does not exceed uncollateralized loan limit uncollateralized_debt = true; - let borrower_debt = - DEBTS.may_load(deps.storage, (&borrower_addr, &denom))?.unwrap_or(Debt { - amount_scaled: Uint128::zero(), - uncollateralized: uncollateralized_debt, - }); + let debt_amount_scaled = borrower.debt_amount_scaled(deps.storage, &denom)?; let asset_market = MARKETS.load(deps.storage, &denom)?; let debt_amount = get_underlying_debt_amount( - borrower_debt.amount_scaled, + debt_amount_scaled, &asset_market, env.block.time.seconds(), )?; @@ -680,20 +555,14 @@ pub fn borrow( let mut response = Response::new(); - response = - apply_accumulated_interests(&env, rewards_collector_addr, &mut borrow_market, response)?; + apply_accumulated_interests(deps.storage, &env, rewards_collector_addr, &mut borrow_market)?; // Set new debt - let mut debt = DEBTS.may_load(deps.storage, (&borrower_addr, &denom))?.unwrap_or(Debt { - amount_scaled: Uint128::zero(), - uncollateralized: uncollateralized_debt, - }); let borrow_amount_scaled = get_scaled_debt_amount(borrow_amount, &borrow_market, env.block.time.seconds())?; - debt.amount_scaled = debt.amount_scaled.checked_add(borrow_amount_scaled)?; - DEBTS.save(deps.storage, (&borrower_addr, &denom), &debt)?; - borrow_market.debt_total_scaled += borrow_amount_scaled; + borrow_market.increase_debt(borrow_amount_scaled)?; + borrower.increase_debt(deps.storage, &denom, borrow_amount_scaled, uncollateralized_debt)?; response = update_interest_rates(&deps, &env, &mut borrow_market, borrow_amount, &denom, response)?; @@ -703,16 +572,17 @@ pub fn borrow( let recipient_addr = if let Some(recipient) = recipient { deps.api.addr_validate(&recipient)? } else { - borrower_addr.clone() + borrower.address().clone() }; Ok(response .add_message(build_send_asset_msg(&recipient_addr, &denom, borrow_amount)) .add_attribute("action", "outposts/red-bank/borrow") - .add_attribute("denom", denom) - .add_attribute("user", borrower_addr) + .add_attribute("sender", borrower) .add_attribute("recipient", recipient_addr) - .add_attribute("amount", borrow_amount)) + .add_attribute("denom", denom) + .add_attribute("amount", borrow_amount) + .add_attribute("amount_scaled", borrow_amount_scaled)) } /// Handle the repay of native tokens. Refund extra funds if they exist @@ -724,22 +594,22 @@ pub fn repay( denom: String, repay_amount: Uint128, ) -> Result { - let user_addr = if let Some(address) = on_behalf_of { - let on_behalf_of_addr = deps.api.addr_validate(&address)?; + let user_addr: Addr; + let user = if let Some(address) = on_behalf_of { + user_addr = deps.api.addr_validate(&address)?; + let user = User(&user_addr); // Uncollateralized loans should not have 'on behalf of' because it creates accounting complexity for them - match UNCOLLATERALIZED_LOAN_LIMITS.may_load(deps.storage, (&on_behalf_of_addr, &denom))? { - Some(limit) if !limit.is_zero() => { - return Err(ContractError::CannotRepayUncollateralizedLoanOnBehalfOf {}) - } - _ => on_behalf_of_addr, + if !user.uncollateralized_loan_limit(deps.storage, &denom)?.is_zero() { + return Err(ContractError::CannotRepayUncollateralizedLoanOnBehalfOf {}); } + user } else { - info.sender.clone() + User(&info.sender) }; // Check new debt - let mut debt = DEBTS - .may_load(deps.storage, (&user_addr, &denom))? + let debt = DEBTS + .may_load(deps.storage, (user.address(), &denom))? .ok_or(ContractError::CannotRepayZeroDebt {})?; let config = CONFIG.load(deps.storage)?; @@ -754,7 +624,7 @@ pub fn repay( let mut response = Response::new(); - response = apply_accumulated_interests(&env, &rewards_collector_addr, &mut market, response)?; + apply_accumulated_interests(deps.storage, &env, &rewards_collector_addr, &mut market)?; let debt_amount_scaled_before = debt.amount_scaled; let debt_amount_before = @@ -765,7 +635,7 @@ pub fn repay( let mut debt_amount_after = Uint128::zero(); if repay_amount > debt_amount_before { refund_amount = repay_amount - debt_amount_before; - let refund_msg = build_send_asset_msg(&user_addr, &denom, refund_amount); + let refund_msg = build_send_asset_msg(user.address(), &denom, refund_amount); response = response.add_message(refund_msg); } else { debt_amount_after = debt_amount_before - repay_amount; @@ -773,29 +643,23 @@ pub fn repay( let debt_amount_scaled_after = get_scaled_debt_amount(debt_amount_after, &market, env.block.time.seconds())?; - debt.amount_scaled = debt_amount_scaled_after; let debt_amount_scaled_delta = debt_amount_scaled_before.checked_sub(debt_amount_scaled_after)?; - market.debt_total_scaled = market.debt_total_scaled.checked_sub(debt_amount_scaled_delta)?; + market.decrease_debt(debt_amount_scaled_delta)?; + user.decrease_debt(deps.storage, &denom, debt_amount_scaled_delta)?; response = update_interest_rates(&deps, &env, &mut market, Uint128::zero(), &denom, response)?; MARKETS.save(deps.storage, &denom, &market)?; - // TODO: this logic can be extracted to a helper function to simplify the content of `excute.rs` - if debt.amount_scaled.is_zero() { - DEBTS.remove(deps.storage, (&user_addr, &denom)); - } else { - DEBTS.save(deps.storage, (&user_addr, &denom), &debt)?; - } - Ok(response .add_attribute("action", "outposts/red-bank/repay") + .add_attribute("sender", &info.sender) + .add_attribute("on_behalf_of", user) .add_attribute("denom", denom) - .add_attribute("sender", info.sender) - .add_attribute("user", user_addr) - .add_attribute("amount", repay_amount.checked_sub(refund_amount)?)) + .add_attribute("amount", repay_amount.checked_sub(refund_amount)?) + .add_attribute("amount_scaled", debt_amount_scaled_delta)) } /// Execute loan liquidations on under-collateralized loans @@ -809,16 +673,14 @@ pub fn liquidate( sent_debt_asset_amount: Uint128, ) -> Result { let block_time = env.block.time.seconds(); + let user = User(&user_addr); + let liquidator = User(&info.sender); // 1. Validate liquidation // If user (contract) has a positive uncollateralized limit then the user // cannot be liquidated - if let Some(limit) = - UNCOLLATERALIZED_LOAN_LIMITS.may_load(deps.storage, (&user_addr, &debt_denom))? - { - if !limit.is_zero() { - return Err(ContractError::CannotLiquidateWhenPositiveUncollateralizedLoanLimit {}); - } + 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 @@ -833,11 +695,7 @@ pub fn liquidate( // check if user has available collateral in specified collateral asset to be liquidated let collateral_market = MARKETS.load(deps.storage, &collateral_denom)?; - let user_collateral_balance_scaled = cw20_get_balance( - &deps.querier, - collateral_market.ma_token_address.clone(), - user_addr.clone(), - )?; + let user_collateral_balance_scaled = collateral.amount_scaled; let user_collateral_balance = get_underlying_liquidity_amount( user_collateral_balance_scaled, &collateral_market, @@ -848,7 +706,7 @@ pub fn liquidate( } // check if user has outstanding debt in the deposited asset that needs to be repayed - let mut user_debt = DEBTS + let user_debt = DEBTS .may_load(deps.storage, (&user_addr, &debt_denom))? .ok_or(ContractError::CannotLiquidateWhenNoDebtBalance {})?; @@ -911,15 +769,16 @@ pub fn liquidate( block_time, )?; - response = response.add_message(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: collateral_market.ma_token_address.to_string(), - msg: to_binary(&mars_outpost::ma_token::msg::ExecuteMsg::TransferOnLiquidation { - sender: user_addr.to_string(), - recipient: info.sender.to_string(), - amount: collateral_amount_to_liquidate_scaled, - })?, - funds: vec![], - })); + user.decrease_collateral( + deps.storage, + &collateral_denom, + collateral_amount_to_liquidate_scaled, + )?; + liquidator.increase_collateral( + deps.storage, + &collateral_denom, + collateral_amount_to_liquidate_scaled, + )?; // if max collateral to liquidate equals the user's balance, delete the collateral position if collateral_amount_to_liquidate_scaled == user_collateral_balance_scaled { @@ -939,9 +798,7 @@ pub fn liquidate( let debt_amount_scaled_delta = user_debt.amount_scaled.checked_sub(user_debt_asset_debt_amount_scaled_after)?; - user_debt.amount_scaled = user_debt_asset_debt_amount_scaled_after; - - DEBTS.save(deps.storage, (&user_addr, &debt_denom), &user_debt)?; + 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)?; @@ -955,11 +812,11 @@ pub fn liquidate( let mut asset_market_after = collateral_market; let denom = &collateral_denom; - response = apply_accumulated_interests( + apply_accumulated_interests( + deps.storage, &env, rewards_collector_addr, &mut asset_market_after, - response, )?; asset_market_after.debt_total_scaled = debt_market_debt_total_scaled_after; @@ -977,11 +834,11 @@ pub fn liquidate( } else { let mut debt_market_after = debt_market; - response = apply_accumulated_interests( + apply_accumulated_interests( + deps.storage, &env, rewards_collector_addr, &mut debt_market_after, - response, )?; debt_market_after.debt_total_scaled = debt_market_debt_total_scaled_after; @@ -1007,13 +864,14 @@ pub fn liquidate( Ok(response .add_attribute("action", "outposts/red-bank/liquidate") + .add_attribute("user", user) + .add_attribute("liquidator", liquidator) .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("user", user_addr.as_str()) - .add_attribute("liquidator", info.sender) - .add_attribute("collateral_amount_liquidated", collateral_amount_to_liquidate.to_string()) - .add_attribute("debt_amount_repaid", debt_amount_to_repay.to_string()) - .add_attribute("refund_amount", refund_amount.to_string())) + .add_attribute("debt_amount", debt_amount_to_repay) + .add_attribute("debt_amount_scaled", debt_amount_scaled_delta)) } /// Computes debt to repay (in debt asset), @@ -1074,32 +932,24 @@ pub fn update_asset_collateral_status( denom: String, enable: bool, ) -> Result { - let user_addr = info.sender; + let user = User(&info.sender); + let mut collateral = - COLLATERALS.may_load(deps.storage, (&user_addr, &denom))?.unwrap_or_default(); - - let collateral_market = MARKETS.load(deps.storage, &denom)?; - - if !collateral.enabled && enable { - let collateral_ma_address = collateral_market.ma_token_address; - let user_collateral_balance = - cw20_get_balance(&deps.querier, collateral_ma_address, user_addr.clone())?; - if !user_collateral_balance.is_zero() { - // enable collateral asset - collateral.enabled = true; - COLLATERALS.save(deps.storage, (&user_addr, &denom), &collateral)?; - } else { - return Err(ContractError::UserNoCollateralBalance { - user: user_addr.to_string(), - denom, - }); - } - } else if collateral.enabled && !enable { - // disable collateral asset - collateral.enabled = false; - COLLATERALS.save(deps.storage, (&user_addr, &denom), &collateral)?; + COLLATERALS.may_load(deps.storage, (user.address(), &denom))?.ok_or_else(|| { + ContractError::UserNoCollateralBalance { + user: user.into(), + denom: denom.clone(), + } + })?; + + let previously_enabled = collateral.enabled; - // check health factor after disabling collateral + collateral.enabled = enable; + COLLATERALS.save(deps.storage, (user.address(), &denom), &collateral)?; + + // if the collateral was previously enabled, but is not disabled, it is necessary to ensure the + // user is not liquidatable after disabling + if previously_enabled && !enable { let config = CONFIG.load(deps.storage)?; let oracle_addr = address_provider::helpers::query_address( deps.as_ref(), @@ -1108,7 +958,7 @@ pub fn update_asset_collateral_status( )?; let (liquidatable, _) = - assert_liquidatable(&deps.as_ref(), &env, &user_addr, &oracle_addr)?; + assert_liquidatable(&deps.as_ref(), &env, user.address(), &oracle_addr)?; if liquidatable { return Err(ContractError::InvalidHealthFactorAfterDisablingCollateral {}); @@ -1117,56 +967,7 @@ pub fn update_asset_collateral_status( Ok(Response::new() .add_attribute("action", "outposts/red-bank/update_asset_collateral_status") - .add_attribute("user", user_addr.as_str()) + .add_attribute("user", user) .add_attribute("denom", denom) .add_attribute("enable", enable.to_string())) } - -/// Update uncollateralized loan limit by a given amount in base asset -pub fn finalize_liquidity_token_transfer( - deps: DepsMut, - env: Env, - info: MessageInfo, - from_address: Addr, - to_address: Addr, - from_previous_balance: Uint128, - to_previous_balance: Uint128, - amount: Uint128, -) -> Result { - // Get liquidity token market - let denom = MARKET_DENOMS_BY_MA_TOKEN.load(deps.storage, &info.sender)?; - - // Check user health factor is above 1 - let config = CONFIG.load(deps.storage)?; - let oracle_addr = address_provider::helpers::query_address( - deps.as_ref(), - &config.address_provider, - MarsContract::Oracle, - )?; - - let (liquidatable, _) = assert_liquidatable(&deps.as_ref(), &env, &from_address, &oracle_addr)?; - - if liquidatable { - return Err(ContractError::CannotTransferTokenWhenInvalidHealthFactor {}); - } - - // Update users's positions - if from_address != to_address { - if from_previous_balance.checked_sub(amount)?.is_zero() { - COLLATERALS.remove(deps.storage, (&from_address, &denom)); - } - - if to_previous_balance.is_zero() && !amount.is_zero() { - COLLATERALS.save( - deps.storage, - (&to_address, &denom), - &Collateral { - enabled: true, - }, - )?; - } - } - - Ok(Response::new() - .add_attribute("action", "outposts/red-bank/finalize_liquidity_token_transfer")) -} diff --git a/contracts/red-bank/src/health.rs b/contracts/red-bank/src/health.rs index 96369f5c..f7e170a4 100644 --- a/contracts/red-bank/src/health.rs +++ b/contracts/red-bank/src/health.rs @@ -2,7 +2,6 @@ use std::collections::{HashMap, HashSet}; use cosmwasm_std::{Addr, Decimal, Deps, Env, Order, StdError, StdResult, Uint128}; use mars_health::health::{Health, Position as HealthPosition}; -use mars_outpost::helpers::cw20_get_balance; use mars_outpost::oracle; use mars_outpost::red_bank::Position; @@ -135,11 +134,7 @@ pub fn get_user_positions_map( let collateral_amount = match COLLATERALS.may_load(deps.storage, (user_addr, &denom))? { Some(collateral) if collateral.enabled => { - let amount_scaled = cw20_get_balance( - &deps.querier, - market.ma_token_address.clone(), - user_addr.clone(), - )?; + let amount_scaled = collateral.amount_scaled; get_underlying_liquidity_amount(amount_scaled, &market, block_time)? } _ => Uint128::zero(), diff --git a/contracts/red-bank/src/helpers.rs b/contracts/red-bank/src/helpers.rs index 0d4f6775..3262eae9 100644 --- a/contracts/red-bank/src/helpers.rs +++ b/contracts/red-bank/src/helpers.rs @@ -1,9 +1,7 @@ use std::collections::HashMap; -use cosmwasm_std::{Addr, QuerierWrapper, StdResult, Uint128}; -use cw20::TokenInfoResponse; +use cosmwasm_std::{StdResult, Uint128}; -use mars_outpost::ma_token::msg::QueryMsg; use mars_outpost::red_bank::Position; pub fn get_uncollaterized_debt(positions: &HashMap) -> StdResult { @@ -14,9 +12,3 @@ pub fn get_uncollaterized_debt(positions: &HashMap) -> StdResu Ok(total) }) } - -pub fn query_total_deposits(querier: &QuerierWrapper, ma_token_addr: &Addr) -> StdResult { - Ok(querier - .query_wasm_smart::(ma_token_addr.clone(), &QueryMsg::TokenInfo {})? - .total_supply) -} diff --git a/contracts/red-bank/src/interest_rates.rs b/contracts/red-bank/src/interest_rates.rs index 95942fa5..c36be1da 100644 --- a/contracts/red-bank/src/interest_rates.rs +++ b/contracts/red-bank/src/interest_rates.rs @@ -1,15 +1,14 @@ use std::str; use cosmwasm_std::{ - to_binary, Addr, CosmosMsg, Decimal, DepsMut, Env, Event, Response, StdError, StdResult, - Uint128, WasmMsg, + Addr, Decimal, DepsMut, Env, Event, Response, StdError, StdResult, Storage, Uint128, }; -use cw20::Cw20ExecuteMsg; use mars_outpost::math; use mars_outpost::red_bank::Market; use crate::error::ContractError; +use crate::user::User; /// Scaling factor used to keep more precision during division / multiplication by index. pub const SCALING_FACTOR: Uint128 = Uint128::new(1_000_000); @@ -27,11 +26,11 @@ const SECONDS_PER_YEAR: u64 = 31536000u64; /// as it would apply the new interest rates instead of the ones that were valid during /// the period between indexes_last_updated and current_block pub fn apply_accumulated_interests( + store: &mut dyn Storage, env: &Env, rewards_collector_addr: &Addr, market: &mut Market, - mut response: Response, -) -> StdResult { +) -> StdResult<()> { let current_timestamp = env.block.time.seconds(); let previous_borrow_index = market.borrow_index; @@ -80,21 +79,20 @@ pub fn apply_accumulated_interests( let accrued_protocol_rewards = borrow_interest_accrued * market.reserve_factor; if !accrued_protocol_rewards.is_zero() { - let mint_amount = compute_scaled_amount( + let reward_amount_scaled = compute_scaled_amount( accrued_protocol_rewards, market.liquidity_index, ScalingOperation::Truncate, )?; - response = response.add_message(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: market.ma_token_address.clone().into(), - msg: to_binary(&Cw20ExecuteMsg::Mint { - recipient: rewards_collector_addr.into(), - amount: mint_amount, - })?, - funds: vec![], - })) + User(rewards_collector_addr).increase_collateral( + store, + &market.denom, + reward_amount_scaled, + )?; + market.increase_collateral(reward_amount_scaled)?; } - Ok(response) + + Ok(()) } pub fn calculate_applied_linear_interest_rate( diff --git a/contracts/red-bank/src/lib.rs b/contracts/red-bank/src/lib.rs index af126e56..66a1ffb2 100644 --- a/contracts/red-bank/src/lib.rs +++ b/contracts/red-bank/src/lib.rs @@ -7,3 +7,4 @@ pub mod helpers; pub mod interest_rates; pub mod query; pub mod state; +pub mod user; diff --git a/contracts/red-bank/src/query.rs b/contracts/red-bank/src/query.rs index 4314c05f..f7afe37a 100644 --- a/contracts/red-bank/src/query.rs +++ b/contracts/red-bank/src/query.rs @@ -4,8 +4,8 @@ use cw_storage_plus::Bound; use mars_outpost::address_provider::{self, MarsContract}; use mars_outpost::error::MarsError; use mars_outpost::red_bank::{ - ConfigResponse, Market, UncollateralizedLoanLimitResponse, UserCollateralResponse, - UserDebtResponse, UserHealthStatus, UserPositionResponse, + Collateral, ConfigResponse, Debt, Market, UncollateralizedLoanLimitResponse, + UserCollateralResponse, UserDebtResponse, UserHealthStatus, UserPositionResponse, }; use crate::health; @@ -14,9 +14,7 @@ use crate::interest_rates::{ get_scaled_debt_amount, get_scaled_liquidity_amount, get_underlying_debt_amount, get_underlying_liquidity_amount, }; -use crate::state::{ - COLLATERALS, CONFIG, DEBTS, MARKETS, MARKET_DENOMS_BY_MA_TOKEN, UNCOLLATERALIZED_LOAN_LIMITS, -}; +use crate::state::{COLLATERALS, CONFIG, DEBTS, MARKETS, UNCOLLATERALIZED_LOAN_LIMITS}; const DEFAULT_LIMIT: u32 = 5; const MAX_LIMIT: u32 = 10; @@ -26,7 +24,6 @@ pub fn query_config(deps: Deps) -> StdResult { Ok(ConfigResponse { owner: config.owner.to_string(), address_provider: config.address_provider.to_string(), - ma_token_code_id: config.ma_token_code_id, close_factor: config.close_factor, }) } @@ -96,22 +93,20 @@ pub fn query_user_debt( user_addr: Addr, denom: String, ) -> StdResult { - let market = MARKETS.load(deps.storage, &denom)?; - - let (amount_scaled, amount) = match DEBTS.may_load(deps.storage, (&user_addr, &denom))? { - Some(debt) => { - let amount_scaled = debt.amount_scaled; - let amount = get_underlying_debt_amount(amount_scaled, &market, block.time.seconds())?; - (amount_scaled, amount) - } + let Debt { + amount_scaled, + uncollateralized, + } = DEBTS.may_load(deps.storage, (&user_addr, &denom))?.unwrap_or_default(); - None => (Uint128::zero(), Uint128::zero()), - }; + let block_time = block.time.seconds(); + let market = MARKETS.load(deps.storage, &denom)?; + let amount = get_underlying_debt_amount(amount_scaled, &market, block_time)?; Ok(UserDebtResponse { denom, amount_scaled, amount, + uncollateralized, }) } @@ -143,6 +138,7 @@ pub fn query_user_debts( denom, amount_scaled, amount, + uncollateralized: debt.uncollateralized, }) }) .collect() @@ -150,31 +146,36 @@ pub fn query_user_debts( pub fn query_user_collateral( deps: Deps, + block: &BlockInfo, user_addr: Addr, denom: String, ) -> StdResult { - let enabled = match COLLATERALS.may_load(deps.storage, (&user_addr, &denom))? { - Some(collateral) => { - // TODO: For now, we just return whether the collateral is enabled. - // Once maToken is removed, we will compute the underlying collateral amount here, - // similar as with the `query_user_debt` query. - collateral.enabled - } - None => false, - }; + let Collateral { + amount_scaled, + enabled, + } = COLLATERALS.may_load(deps.storage, (&user_addr, &denom))?.unwrap_or_default(); + + let block_time = block.time.seconds(); + let market = MARKETS.load(deps.storage, &denom)?; + let amount = get_underlying_liquidity_amount(amount_scaled, &market, block_time)?; Ok(UserCollateralResponse { denom, + amount_scaled, + amount, enabled, }) } pub fn query_user_collaterals( deps: Deps, + block: &BlockInfo, user_addr: Addr, start_after: Option, limit: Option, ) -> StdResult> { + let block_time = block.time.seconds(); + let start = start_after.map(|denom| Bound::ExclusiveRaw(denom.into_bytes())); let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; @@ -184,8 +185,16 @@ pub fn query_user_collaterals( .take(limit) .map(|item| { let (denom, collateral) = item?; + + let market = MARKETS.load(deps.storage, &denom)?; + + let amount_scaled = collateral.amount_scaled; + let amount = get_underlying_liquidity_amount(amount_scaled, &market, block_time)?; + Ok(UserCollateralResponse { denom, + amount_scaled, + amount, enabled: collateral.enabled, }) }) @@ -215,11 +224,9 @@ pub fn query_scaled_debt_amount( pub fn query_underlying_liquidity_amount( deps: Deps, env: Env, - ma_token: String, + denom: String, amount_scaled: Uint128, ) -> StdResult { - let ma_token_addr = deps.api.addr_validate(&ma_token)?; - let denom = MARKET_DENOMS_BY_MA_TOKEN.load(deps.storage, &ma_token_addr)?; let market = MARKETS.load(deps.storage, &denom)?; get_underlying_liquidity_amount(amount_scaled, &market, env.block.time.seconds()) } diff --git a/contracts/red-bank/src/state.rs b/contracts/red-bank/src/state.rs index c996ab8c..a965fc42 100644 --- a/contracts/red-bank/src/state.rs +++ b/contracts/red-bank/src/state.rs @@ -1,23 +1,10 @@ -use cosmwasm_std::{Addr, Order, Storage, Uint128}; +use cosmwasm_std::{Addr, Uint128}; use cw_storage_plus::{Item, Map}; use mars_outpost::red_bank::{Collateral, Config, Debt, Market}; pub const CONFIG: Item = Item::new("config"); - pub const MARKETS: Map<&str, Market> = Map::new("markets"); -pub const MARKET_DENOMS_BY_MA_TOKEN: Map<&Addr, String> = Map::new("market_denoms_by_ma_token"); - pub const COLLATERALS: Map<(&Addr, &str), Collateral> = Map::new("collaterals"); pub const DEBTS: Map<(&Addr, &str), Debt> = Map::new("debts"); - pub const UNCOLLATERALIZED_LOAN_LIMITS: Map<(&Addr, &str), Uint128> = Map::new("limits"); - -/// Return `true` if the user is borrowing a non-zero amount in _any_ asset; return `false` if the -/// user is not borrowing any asset. -/// -/// The user is borrowing if, in the `DEBTS` map, there is at least one denom stored under the user -/// address prefix. -pub fn user_is_borrowing(store: &dyn Storage, addr: &Addr) -> bool { - DEBTS.prefix(addr).range(store, None, None, Order::Ascending).next().is_some() -} diff --git a/contracts/red-bank/src/user.rs b/contracts/red-bank/src/user.rs new file mode 100644 index 00000000..14642a2b --- /dev/null +++ b/contracts/red-bank/src/user.rs @@ -0,0 +1,184 @@ +use cosmwasm_std::{Addr, Order, StdResult, Storage, Uint128}; + +use mars_outpost::red_bank::{Collateral, Debt}; + +use crate::state::{COLLATERALS, DEBTS, UNCOLLATERALIZED_LOAN_LIMITS}; + +/// A helper class providing an intuitive API for managing user positions in the contract store. +/// +/// For example, to increase a user's debt shares, instead of: +/// +/// ```rust +/// DEBTS.update(deps.storage, &user_addr, |opt| -> StdResult<_> { +/// let mut debt = opt.unwrap_or_default(); +/// debt.amount_scaled = debt.amount_scaled.checked_add(new_debt)?; +/// Ok(debt) +/// })?; +/// ``` +/// +/// The `User` struct allows you simply do +/// +/// ```rust +/// let user = User(&user_addr); +/// user.increase_debt(deps.storage, new_debt)?; +/// ``` +#[derive(Clone, Copy)] +pub struct User<'a>(pub &'a Addr); + +// Implement Into for User so that it can be easily used in event attributes, e.g. +// +// ```rust +// let user = User(&user_addr); +// let res = Response::new().add_attribute("user", user); +// ``` +impl<'a> From> for String { + fn from(user: User) -> String { + user.0.to_string() + } +} + +impl<'a> User<'a> { + /// Returns a reference to the user's address + pub fn address(&self) -> &Addr { + self.0 + } + + /// Load the user's collateral + pub fn collateral(&self, store: &dyn Storage, denom: &str) -> StdResult { + COLLATERALS.load(store, (self.0, denom)) + } + + /// Load the user's debt + pub fn debt(&self, store: &dyn Storage, denom: &str) -> StdResult { + DEBTS.load(store, (self.0, denom)) + } + + /// Load the user's scaled debt amount; default to zero if not borrowing. + pub fn debt_amount_scaled(&self, store: &dyn Storage, denom: &str) -> StdResult { + let amount_scaled = DEBTS + .may_load(store, (self.0, denom))? + .map(|debt| debt.amount_scaled) + .unwrap_or_else(Uint128::zero); + Ok(amount_scaled) + } + + /// Load the user's uncollateralized loan limit. Return zero if the user has not been given an + /// uncollateralized loan limit. + pub fn uncollateralized_loan_limit( + &self, + store: &dyn Storage, + denom: &str, + ) -> StdResult { + let limit = UNCOLLATERALIZED_LOAN_LIMITS + .may_load(store, (self.0, denom))? + .unwrap_or_else(Uint128::zero); + Ok(limit) + } + + /// Return `true` if the user is borrowing a non-zero amount in _any_ asset; return `false` if + /// the user is not borrowing any asset. + /// + /// The user is borrowing if, in the `DEBTS` map, there is at least one denom stored under the + /// user address prefix. + pub fn is_borrowing(&self, store: &dyn Storage) -> bool { + DEBTS.prefix(self.0).range(store, None, None, Order::Ascending).next().is_some() + } + + /// Increase a user's collateral shares by the specified amount. + /// + /// If the user does not already have a collateral amount, the asset is enabled as collateral by + /// default. To disable, send a separate `update_asset_collateral_status` execute message. + /// + /// This may be invoked if a user makes a deposit, or when a liquidator liquidates a position. + pub fn increase_collateral( + &self, + store: &mut dyn Storage, + denom: &str, + amount_scaled: Uint128, + ) -> StdResult<()> { + COLLATERALS.update(store, (self.0, denom), |opt| -> StdResult<_> { + match opt { + Some(mut col) => { + col.amount_scaled = col.amount_scaled.checked_add(amount_scaled)?; + Ok(col) + } + None => Ok(Collateral { + amount_scaled, + enabled: true, // enable by default + }), + } + })?; + Ok(()) + } + + /// Increase a user's debt shares by the specified amount. + /// + /// This may be invoked if a user makes a new borrowing. + pub fn increase_debt( + &self, + store: &mut dyn Storage, + denom: &str, + amount_scaled: Uint128, + uncollateralized: bool, + ) -> StdResult<()> { + DEBTS.update(store, (self.0, denom), |opt| -> StdResult<_> { + match opt { + Some(debt) => Ok(Debt { + amount_scaled: debt.amount_scaled.checked_add(amount_scaled)?, + uncollateralized, + }), + None => Ok(Debt { + amount_scaled, + uncollateralized, + }), + } + })?; + Ok(()) + } + + /// Decrease a user's collateral shares by the specified amount. If reduced to zero, delete the + /// collateral position from contract storage. + /// + /// This may be invoked if a user makes a withdrawal, or gets liquidated. + pub fn decrease_collateral( + &self, + store: &mut dyn Storage, + denom: &str, + amount_scaled: Uint128, + ) -> StdResult<()> { + let mut collateral = COLLATERALS.load(store, (self.0, denom))?; + + collateral.amount_scaled = collateral.amount_scaled.checked_sub(amount_scaled)?; + + if collateral.amount_scaled.is_zero() { + COLLATERALS.remove(store, (self.0, denom)); + } else { + COLLATERALS.save(store, (self.0, denom), &collateral)?; + } + + Ok(()) + } + + /// Decrease a user's debt shares by the specified amount. If reduced to zero, delete the debt + /// position from contract storage. + /// + /// This may be invoked if a user makes a repayment, or gets liquidated. + pub fn decrease_debt( + &self, + store: &mut dyn Storage, + denom: &str, + amount_scaled: Uint128, + ) -> StdResult<()> { + let mut debt = DEBTS.load(store, (self.0, denom))?; + + debt.amount_scaled = debt.amount_scaled.checked_sub(amount_scaled)?; + + if debt.amount_scaled.is_zero() { + DEBTS.remove(store, (self.0, denom)); + } else { + DEBTS.save(store, (self.0, denom), &debt)?; + } + + Ok(()) + } +} diff --git a/contracts/red-bank/tests/helpers.rs b/contracts/red-bank/tests/helpers.rs index 4fd20fd1..857d8932 100644 --- a/contracts/red-bank/tests/helpers.rs +++ b/contracts/red-bank/tests/helpers.rs @@ -11,10 +11,17 @@ use mars_red_bank::interest_rates::{ calculate_applied_linear_interest_rate, compute_scaled_amount, compute_underlying_amount, ScalingOperation, }; -use mars_red_bank::state::{COLLATERALS, DEBTS, MARKETS, MARKET_DENOMS_BY_MA_TOKEN}; +use mars_red_bank::state::{COLLATERALS, DEBTS, MARKETS}; -pub fn set_collateral(deps: DepsMut, user_addr: &Addr, denom: &str, enabled: bool) { +pub fn set_collateral( + deps: DepsMut, + user_addr: &Addr, + denom: &str, + amount_scaled: Uint128, + enabled: bool, +) { let collateral = Collateral { + amount_scaled, enabled, }; COLLATERALS.save(deps.storage, (user_addr, denom), &collateral).unwrap(); @@ -64,7 +71,6 @@ pub fn th_setup(contract_balances: &[Coin]) -> OwnedDeps Market { MARKETS.save(deps.storage, denom, &new_market).unwrap(); - MARKET_DENOMS_BY_MA_TOKEN - .save(deps.storage, &new_market.ma_token_address, &denom.to_string()) - .unwrap(); - new_market } diff --git a/contracts/red-bank/tests/test_admin.rs b/contracts/red-bank/tests/test_admin.rs index df260ad3..e04cf8df 100644 --- a/contracts/red-bank/tests/test_admin.rs +++ b/contracts/red-bank/tests/test_admin.rs @@ -1,16 +1,7 @@ -use std::any::type_name; - -use cosmwasm_std::testing::{mock_info, MOCK_CONTRACT_ADDR}; -use cosmwasm_std::{ - attr, coin, from_binary, to_binary, Addr, CosmosMsg, Decimal, Event, StdError, SubMsg, Uint128, - WasmMsg, -}; -use cw20::MinterResponse; -use cw20_base::msg::InstantiateMarketingInfo; +use cosmwasm_std::testing::mock_info; +use cosmwasm_std::{attr, coin, from_binary, Addr, Decimal, Event, Uint128}; use mars_outpost::error::MarsError; -use mars_outpost::helpers::zero_address; -use mars_outpost::ma_token; use mars_outpost::red_bank::{ ConfigResponse, CreateOrUpdateConfig, ExecuteMsg, InitOrUpdateAssetParams, InstantiateMsg, InterestRateModel, Market, QueryMsg, @@ -22,7 +13,7 @@ use mars_red_bank::error::ContractError; use mars_red_bank::interest_rates::{ compute_scaled_amount, compute_underlying_amount, ScalingOperation, }; -use mars_red_bank::state::{CONFIG, MARKETS}; +use mars_red_bank::state::{COLLATERALS, CONFIG, MARKETS}; use crate::helpers::{th_get_expected_indices, th_init_market, th_setup}; @@ -37,7 +28,6 @@ fn test_proper_initialization() { let base_config = CreateOrUpdateConfig { owner: Some("owner".to_string()), address_provider: Some("address_provider".to_string()), - ma_token_code_id: Some(10u64), close_factor: None, }; @@ -47,7 +37,6 @@ fn test_proper_initialization() { let empty_config = CreateOrUpdateConfig { owner: None, address_provider: None, - ma_token_code_id: None, close_factor: None, }; let msg = InstantiateMsg { @@ -100,7 +89,8 @@ fn test_proper_initialization() { // it worked, let's query the state let res = query(deps.as_ref(), env, QueryMsg::Config {}).unwrap(); let value: ConfigResponse = from_binary(&res).unwrap(); - assert_eq!(10, value.ma_token_code_id); + assert_eq!(value.owner, "owner"); + assert_eq!(value.address_provider, "address_provider"); } #[test] @@ -115,7 +105,6 @@ fn test_update_config() { let init_config = CreateOrUpdateConfig { owner: Some("owner".to_string()), address_provider: Some("address_provider".to_string()), - ma_token_code_id: Some(20u64), close_factor: Some(close_factor), }; let msg = InstantiateMsg { @@ -166,7 +155,6 @@ fn test_update_config() { let config = CreateOrUpdateConfig { owner: Some("new_owner".to_string()), address_provider: Some("new_address_provider".to_string()), - ma_token_code_id: Some(40u64), close_factor: Some(close_factor), }; let msg = ExecuteMsg::UpdateConfig { @@ -183,7 +171,6 @@ fn test_update_config() { assert_eq!(new_config.owner, Addr::unchecked("new_owner")); assert_eq!(new_config.address_provider, Addr::unchecked(config.address_provider.unwrap())); - assert_eq!(new_config.ma_token_code_id, config.ma_token_code_id.unwrap()); assert_eq!(new_config.close_factor, config.close_factor.unwrap()); } @@ -195,7 +182,6 @@ fn test_init_asset() { let config = CreateOrUpdateConfig { owner: Some("owner".to_string()), address_provider: Some("address_provider".to_string()), - ma_token_code_id: Some(5u64), close_factor: Some(Decimal::from_ratio(1u128, 2u128)), }; let msg = InstantiateMsg { @@ -211,7 +197,7 @@ fn test_init_asset() { slope_2: Decimal::zero(), }; - let asset_params = InitOrUpdateAssetParams { + let params = InitOrUpdateAssetParams { initial_borrow_rate: Some(Decimal::from_ratio(20u128, 100u128)), max_loan_to_value: Some(Decimal::from_ratio(8u128, 10u128)), reserve_factor: Some(Decimal::from_ratio(1u128, 100u128)), @@ -227,8 +213,7 @@ fn test_init_asset() { { let msg = ExecuteMsg::InitAsset { denom: "someasset".to_string(), - asset_params: asset_params.clone(), - asset_symbol: None, + params: params.clone(), }; let info = mock_info("somebody", &[]); let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); @@ -241,13 +226,11 @@ fn test_init_asset() { max_loan_to_value: None, liquidation_threshold: None, liquidation_bonus: None, - ..asset_params.clone() + ..params.clone() }; let msg = ExecuteMsg::InitAsset { denom: "someasset".to_string(), - - asset_params: empty_asset_params, - asset_symbol: None, + params: empty_asset_params, }; let info = mock_info("owner", &[]); let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); @@ -258,12 +241,11 @@ fn test_init_asset() { { let invalid_asset_params = InitOrUpdateAssetParams { max_loan_to_value: Some(Decimal::from_ratio(11u128, 10u128)), - ..asset_params.clone() + ..params.clone() }; let msg = ExecuteMsg::InitAsset { denom: "someasset".to_string(), - asset_params: invalid_asset_params, - asset_symbol: None, + params: invalid_asset_params, }; let info = mock_info("owner", &[]); let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); @@ -282,12 +264,11 @@ fn test_init_asset() { { let invalid_asset_params = InitOrUpdateAssetParams { liquidation_threshold: Some(Decimal::from_ratio(11u128, 10u128)), - ..asset_params.clone() + ..params.clone() }; let msg = ExecuteMsg::InitAsset { denom: "someasset".to_string(), - asset_params: invalid_asset_params, - asset_symbol: None, + params: invalid_asset_params, }; let info = mock_info("owner", &[]); let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); @@ -306,12 +287,11 @@ fn test_init_asset() { { let invalid_asset_params = InitOrUpdateAssetParams { liquidation_bonus: Some(Decimal::from_ratio(11u128, 10u128)), - ..asset_params.clone() + ..params.clone() }; let msg = ExecuteMsg::InitAsset { denom: "someasset".to_string(), - asset_params: invalid_asset_params, - asset_symbol: None, + params: invalid_asset_params, }; let info = mock_info("owner", &[]); let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); @@ -331,12 +311,11 @@ fn test_init_asset() { let invalid_asset_params = InitOrUpdateAssetParams { max_loan_to_value: Some(Decimal::from_ratio(5u128, 10u128)), liquidation_threshold: Some(Decimal::from_ratio(5u128, 10u128)), - ..asset_params.clone() + ..params.clone() }; let msg = ExecuteMsg::InitAsset { denom: "someasset".to_string(), - asset_params: invalid_asset_params, - asset_symbol: None, + params: invalid_asset_params, }; let info = mock_info("owner", &[]); let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); @@ -358,12 +337,11 @@ fn test_init_asset() { optimal_utilization_rate: Decimal::percent(110), ..ir_model }), - ..asset_params + ..params }; let msg = ExecuteMsg::InitAsset { denom: "someasset".to_string(), - asset_params: invalid_asset_params, - asset_symbol: None, + params: invalid_asset_params, }; let info = mock_info("owner", &[]); let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); @@ -382,60 +360,18 @@ fn test_init_asset() { { let msg = ExecuteMsg::InitAsset { denom: "someasset".to_string(), - asset_params: asset_params.clone(), - asset_symbol: None, + params: params.clone(), }; let info = mock_info("owner", &[]); let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); // should have asset market with Canonical default address let market = MARKETS.load(&deps.storage, "someasset").unwrap(); - assert_eq!(zero_address(), market.ma_token_address); + assert_eq!(market.denom, "someasset"); // should have unlimited deposit cap assert_eq!(market.deposit_cap, Uint128::MAX); - // should instantiate a liquidity token - assert_eq!( - res.messages, - vec![SubMsg::new(CosmosMsg::Wasm(WasmMsg::Instantiate { - admin: Some("protocol_admin".to_string()), - code_id: 5u64, - msg: to_binary(&ma_token::msg::InstantiateMsg { - name: String::from("Mars someasset Liquidity Token"), - symbol: String::from("masomeasset"), - decimals: 6, - initial_balances: vec![], - mint: Some(MinterResponse { - minter: MOCK_CONTRACT_ADDR.to_string(), - cap: None, - }), - init_hook: Some(ma_token::msg::InitHook { - contract_addr: MOCK_CONTRACT_ADDR.to_string(), - msg: to_binary(&ExecuteMsg::InitAssetTokenCallback { - denom: "someasset".to_string(), - }) - .unwrap() - }), - marketing: Some(InstantiateMarketingInfo { - project: Some("Mars Protocol".to_string()), - description: Some( - "Interest earning token representing deposits for someasset" - .to_string() - ), - - marketing: Some("protocol_admin".to_string()), - logo: None, - }), - red_bank_address: MOCK_CONTRACT_ADDR.to_string(), - incentives_address: "incentives".to_string(), - }) - .unwrap(), - funds: vec![], - label: "masomeasset".to_string() - })),] - ); - assert_eq!( res.attributes, vec![attr("action", "outposts/red-bank/init_asset"), attr("denom", "someasset")] @@ -446,108 +382,14 @@ fn test_init_asset() { { let msg = ExecuteMsg::InitAsset { denom: "someasset".to_string(), - asset_params, - asset_symbol: None, + params, }; let info = mock_info("owner", &[]); - let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!(error_res, ContractError::AssetAlreadyInitialized {}); - } - - // callback comes back with created token - { - let msg = ExecuteMsg::InitAssetTokenCallback { - denom: "someasset".to_string(), - }; - let info = mock_info("mtokencontract", &[]); - execute(deps.as_mut(), env.clone(), info, msg).unwrap(); - - // should have asset market with contract address - let market = MARKETS.load(&deps.storage, "someasset").unwrap(); - assert_eq!(Addr::unchecked("mtokencontract"), market.ma_token_address); - assert_eq!(Decimal::one(), market.liquidity_index); - } - - // calling this again should not be allowed - { - let msg = ExecuteMsg::InitAssetTokenCallback { - denom: "someasset".to_string(), - }; - let info = mock_info("mtokencontract", &[]); let error_res = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(error_res, MarsError::Unauthorized {}.into()); + assert_eq!(error_res, ContractError::AssetAlreadyInitialized {}); } } -#[test] -fn test_init_asset_with_msg_symbol() { - let mut deps = th_setup(&[]); - let asset_params = InitOrUpdateAssetParams { - initial_borrow_rate: Some(Decimal::from_ratio(20u128, 100u128)), - max_loan_to_value: Some(Decimal::from_ratio(8u128, 10u128)), - reserve_factor: Some(Decimal::from_ratio(1u128, 100u128)), - liquidation_threshold: Some(Decimal::one()), - liquidation_bonus: Some(Decimal::zero()), - interest_rate_model: Some(InterestRateModel { - optimal_utilization_rate: Decimal::one(), - base: Decimal::percent(5), - slope_1: Decimal::zero(), - slope_2: Decimal::zero(), - }), - deposit_enabled: Some(true), - borrow_enabled: Some(true), - deposit_cap: None, - }; - let msg = ExecuteMsg::InitAsset { - denom: "someasset".to_string(), - asset_params, - asset_symbol: Some("COIN".to_string()), - }; - let info = mock_info("owner", &[]); - let env = mock_env(MockEnvParams::default()); - let res = execute(deps.as_mut(), env, info, msg).unwrap(); - - // should instantiate a liquidity token - assert_eq!( - res.messages, - vec![SubMsg::new(CosmosMsg::Wasm(WasmMsg::Instantiate { - admin: Some("protocol_admin".to_string()), - code_id: 1u64, - msg: to_binary(&ma_token::msg::InstantiateMsg { - name: String::from("Mars COIN Liquidity Token"), - symbol: String::from("maCOIN"), - decimals: 6, - initial_balances: vec![], - mint: Some(MinterResponse { - minter: MOCK_CONTRACT_ADDR.to_string(), - cap: None, - }), - init_hook: Some(ma_token::msg::InitHook { - contract_addr: MOCK_CONTRACT_ADDR.to_string(), - msg: to_binary(&ExecuteMsg::InitAssetTokenCallback { - denom: "someasset".to_string(), - }) - .unwrap() - }), - marketing: Some(InstantiateMarketingInfo { - project: Some("Mars Protocol".to_string()), - description: Some( - "Interest earning token representing deposits for COIN".to_string() - ), - - marketing: Some("protocol_admin".to_string()), - logo: None, - }), - red_bank_address: MOCK_CONTRACT_ADDR.to_string(), - incentives_address: "incentives".to_string(), - }) - .unwrap(), - funds: vec![], - label: "maCOIN".to_string() - })),] - ); -} - #[test] fn test_update_asset() { let mut deps = mock_dependencies(&[]); @@ -557,7 +399,6 @@ fn test_update_asset() { let config = CreateOrUpdateConfig { owner: Some("owner".to_string()), address_provider: Some("address_provider".to_string()), - ma_token_code_id: Some(5u64), close_factor: Some(Decimal::from_ratio(1u128, 2u128)), }; let msg = InstantiateMsg { @@ -573,7 +414,7 @@ fn test_update_asset() { slope_2: Decimal::zero(), }; - let asset_params = InitOrUpdateAssetParams { + let params = InitOrUpdateAssetParams { initial_borrow_rate: Some(Decimal::from_ratio(20u128, 100u128)), max_loan_to_value: Some(Decimal::from_ratio(50u128, 100u128)), reserve_factor: Some(Decimal::from_ratio(1u128, 100u128)), @@ -589,7 +430,7 @@ fn test_update_asset() { { let msg = ExecuteMsg::UpdateAsset { denom: "someasset".to_string(), - asset_params: asset_params.clone(), + params: params.clone(), }; let info = mock_info("somebody", &[]); let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); @@ -600,7 +441,7 @@ fn test_update_asset() { { let msg = ExecuteMsg::UpdateAsset { denom: "someasset".to_string(), - asset_params: asset_params.clone(), + params: params.clone(), }; let info = mock_info("owner", &[]); let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); @@ -611,8 +452,7 @@ fn test_update_asset() { { let msg = ExecuteMsg::InitAsset { denom: "someasset".to_string(), - asset_params: asset_params.clone(), - asset_symbol: None, + params: params.clone(), }; let info = mock_info("owner", &[]); let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); @@ -622,11 +462,11 @@ fn test_update_asset() { { let invalid_asset_params = InitOrUpdateAssetParams { max_loan_to_value: Some(Decimal::from_ratio(11u128, 10u128)), - ..asset_params.clone() + ..params.clone() }; let msg = ExecuteMsg::UpdateAsset { denom: "someasset".to_string(), - asset_params: invalid_asset_params, + params: invalid_asset_params, }; let info = mock_info("owner", &[]); let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); @@ -645,11 +485,11 @@ fn test_update_asset() { { let invalid_asset_params = InitOrUpdateAssetParams { liquidation_threshold: Some(Decimal::from_ratio(11u128, 10u128)), - ..asset_params.clone() + ..params.clone() }; let msg = ExecuteMsg::UpdateAsset { denom: "someasset".to_string(), - asset_params: invalid_asset_params, + params: invalid_asset_params, }; let info = mock_info("owner", &[]); let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); @@ -668,11 +508,11 @@ fn test_update_asset() { { let invalid_asset_params = InitOrUpdateAssetParams { liquidation_bonus: Some(Decimal::from_ratio(11u128, 10u128)), - ..asset_params.clone() + ..params.clone() }; let msg = ExecuteMsg::UpdateAsset { denom: "someasset".to_string(), - asset_params: invalid_asset_params, + params: invalid_asset_params, }; let info = mock_info("owner", &[]); let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); @@ -692,11 +532,11 @@ fn test_update_asset() { let invalid_asset_params = InitOrUpdateAssetParams { max_loan_to_value: Some(Decimal::from_ratio(6u128, 10u128)), liquidation_threshold: Some(Decimal::from_ratio(5u128, 10u128)), - ..asset_params + ..params }; let msg = ExecuteMsg::UpdateAsset { denom: "someasset".to_string(), - asset_params: invalid_asset_params, + params: invalid_asset_params, }; let info = mock_info("owner", &[]); let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); @@ -718,11 +558,11 @@ fn test_update_asset() { optimal_utilization_rate: Decimal::percent(110), ..ir_model }), - ..asset_params + ..params }; let msg = ExecuteMsg::UpdateAsset { denom: "someasset".to_string(), - asset_params: invalid_asset_params, + params: invalid_asset_params, }; let info = mock_info("owner", &[]); let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); @@ -739,7 +579,7 @@ fn test_update_asset() { // update asset with new params { - let asset_params = InitOrUpdateAssetParams { + let params = InitOrUpdateAssetParams { initial_borrow_rate: Some(Decimal::from_ratio(20u128, 100u128)), max_loan_to_value: Some(Decimal::from_ratio(60u128, 100u128)), reserve_factor: Some(Decimal::from_ratio(10u128, 100u128)), @@ -752,7 +592,7 @@ fn test_update_asset() { }; let msg = ExecuteMsg::UpdateAsset { denom: "someasset".to_string(), - asset_params: asset_params.clone(), + params: params.clone(), }; let info = mock_info("owner", &[]); @@ -764,11 +604,11 @@ fn test_update_asset() { ); let new_market = MARKETS.load(&deps.storage, "someasset").unwrap(); - assert_eq!(asset_params.max_loan_to_value.unwrap(), new_market.max_loan_to_value); - assert_eq!(asset_params.reserve_factor.unwrap(), new_market.reserve_factor); - assert_eq!(asset_params.liquidation_threshold.unwrap(), new_market.liquidation_threshold); - assert_eq!(asset_params.liquidation_bonus.unwrap(), new_market.liquidation_bonus); - assert_eq!(asset_params.interest_rate_model.unwrap(), new_market.interest_rate_model); + assert_eq!(params.max_loan_to_value.unwrap(), new_market.max_loan_to_value); + assert_eq!(params.reserve_factor.unwrap(), new_market.reserve_factor); + assert_eq!(params.liquidation_threshold.unwrap(), new_market.liquidation_threshold); + assert_eq!(params.liquidation_bonus.unwrap(), new_market.liquidation_bonus); + assert_eq!(params.interest_rate_model.unwrap(), new_market.interest_rate_model); } // update asset with empty params @@ -788,7 +628,7 @@ fn test_update_asset() { }; let msg = ExecuteMsg::UpdateAsset { denom: "someasset".to_string(), - asset_params: empty_asset_params, + params: empty_asset_params, }; let info = mock_info("owner", &[]); let res = execute(deps.as_mut(), env, info, msg).unwrap(); @@ -815,7 +655,6 @@ fn test_update_asset_with_new_interest_rate_model_params() { let config = CreateOrUpdateConfig { owner: Some("owner".to_string()), address_provider: Some("address_provider".to_string()), - ma_token_code_id: Some(5u64), close_factor: Some(Decimal::from_ratio(1u128, 2u128)), }; let msg = InstantiateMsg { @@ -832,7 +671,7 @@ fn test_update_asset_with_new_interest_rate_model_params() { slope_2: Decimal::zero(), }; - let asset_params = InitOrUpdateAssetParams { + let params = InitOrUpdateAssetParams { initial_borrow_rate: Some(Decimal::from_ratio(15u128, 100u128)), max_loan_to_value: Some(Decimal::from_ratio(50u128, 100u128)), reserve_factor: Some(Decimal::from_ratio(2u128, 100u128)), @@ -846,8 +685,7 @@ fn test_update_asset_with_new_interest_rate_model_params() { let msg = ExecuteMsg::InitAsset { denom: "someasset".to_string(), - asset_params: asset_params.clone(), - asset_symbol: None, + params: params.clone(), }; let info = mock_info("owner", &[]); let env = mock_env_at_block_time(1_000_000); @@ -864,11 +702,11 @@ fn test_update_asset_with_new_interest_rate_model_params() { }; let asset_params_with_new_ir_model = InitOrUpdateAssetParams { interest_rate_model: Some(new_ir_model.clone()), - ..asset_params + ..params }; let msg = ExecuteMsg::UpdateAsset { denom: "someasset".to_string(), - asset_params: asset_params_with_new_ir_model, + params: asset_params_with_new_ir_model, }; let info = mock_info("owner", &[]); let env = mock_env_at_block_time(2_000_000); @@ -912,8 +750,6 @@ fn test_update_asset_new_reserve_factor_accrues_interest_rate() { let asset_liquidity = Uint128::from(10_000_000_000_000_u128); let mut deps = th_setup(&[coin(asset_liquidity.into(), "somecoin")]); - let ma_token_address = Addr::unchecked("ma_token"); - let ir_model = InterestRateModel { optimal_utilization_rate: Decimal::from_ratio(80u128, 100u128), base: Decimal::zero(), @@ -938,13 +774,12 @@ fn test_update_asset_new_reserve_factor_accrues_interest_rate() { ScalingOperation::Ceil, ) .unwrap(), - ma_token_address, interest_rate_model: ir_model.clone(), ..Default::default() }, ); - let asset_params = InitOrUpdateAssetParams { + let params = InitOrUpdateAssetParams { initial_borrow_rate: None, max_loan_to_value: None, reserve_factor: Some(Decimal::from_ratio(2_u128, 10_u128)), @@ -957,7 +792,7 @@ fn test_update_asset_new_reserve_factor_accrues_interest_rate() { }; let msg = ExecuteMsg::UpdateAsset { denom: "somecoin".to_string(), - asset_params, + params, }; let info = mock_info("owner", &[]); let env = mock_env_at_block_time(1_500_000); @@ -1013,36 +848,18 @@ fn test_update_asset_new_reserve_factor_accrues_interest_rate() { ) .unwrap(); let interest_accrued = current_debt_total - asset_initial_debt; - let expected_protocol_rewards = interest_accrued * market_before.reserve_factor; - // mint message is sent because debt is non zero - assert_eq!( - res.messages, - vec![SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: market_before.ma_token_address.to_string(), - msg: to_binary(&ma_token::msg::ExecuteMsg::Mint { - recipient: "protocol_rewards_collector".to_string(), - amount: compute_scaled_amount( - expected_protocol_rewards, - new_market.liquidity_index, - ScalingOperation::Truncate - ) - .unwrap() - }) - .unwrap(), - funds: vec![] - })),] - ); -} - -#[test] -fn test_init_asset_callback_cannot_be_called_on_its_own() { - let mut deps = th_setup(&[]); + let expected_rewards = interest_accrued * market_before.reserve_factor; + let expected_rewards_scaled = compute_scaled_amount( + expected_rewards, + new_market.liquidity_index, + ScalingOperation::Truncate, + ) + .unwrap(); - let env = mock_env(MockEnvParams::default()); - let info = mock_info("mtokencontract", &[]); - let msg = ExecuteMsg::InitAssetTokenCallback { - denom: "uluna".to_string(), - }; - let error_res = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(error_res, StdError::not_found(type_name::()).into()); + // the rewards collector previously did not have a collateral possition + // now it should have one with the expected rewards scaled amount + let collateral = COLLATERALS + .load(deps.as_ref().storage, (&Addr::unchecked("protocol_rewards_collector"), "somecoin")) + .unwrap(); + assert_eq!(collateral.amount_scaled, expected_rewards_scaled); } diff --git a/contracts/red-bank/tests/test_borrow.rs b/contracts/red-bank/tests/test_borrow.rs index ed71c700..55a2a81a 100644 --- a/contracts/red-bank/tests/test_borrow.rs +++ b/contracts/red-bank/tests/test_borrow.rs @@ -1,11 +1,9 @@ use cosmwasm_std::testing::mock_info; use cosmwasm_std::{attr, coin, coins, Addr, BankMsg, CosmosMsg, Decimal, SubMsg, Uint128}; - use cw_utils::PaymentError; + use mars_outpost::math; use mars_outpost::red_bank::{ExecuteMsg, Market}; -use mars_testing::{mock_env, mock_env_at_block_time, MockEnvParams}; - use mars_red_bank::contract::execute; use mars_red_bank::error::ContractError; use mars_red_bank::interest_rates::{ @@ -13,6 +11,7 @@ use mars_red_bank::interest_rates::{ ScalingOperation, SCALING_FACTOR, }; use mars_red_bank::state::{DEBTS, MARKETS, UNCOLLATERALIZED_LOAN_LIMITS}; +use mars_testing::{mock_env, mock_env_at_block_time, MockEnvParams}; use helpers::{ has_collateral_position, has_debt_position, set_collateral, th_build_interests_updated_event, @@ -41,7 +40,6 @@ fn test_borrow_and_repay() { deps.querier.set_oracle_price("uusd", Decimal::one()); let mock_market_1 = Market { - ma_token_address: Addr::unchecked("ma-uosmo"), borrow_index: Decimal::from_ratio(12u128, 10u128), liquidity_index: Decimal::from_ratio(8u128, 10u128), borrow_rate: Decimal::from_ratio(20u128, 100u128), @@ -52,13 +50,11 @@ fn test_borrow_and_repay() { ..Default::default() }; let mock_market_2 = Market { - ma_token_address: Addr::unchecked("ma-uusd"), borrow_index: Decimal::one(), liquidity_index: Decimal::one(), ..Default::default() }; let mock_market_3 = Market { - ma_token_address: Addr::unchecked("ma-uatom"), borrow_index: Decimal::one(), liquidity_index: Decimal::from_ratio(11u128, 10u128), max_loan_to_value: Decimal::from_ratio(7u128, 10u128), @@ -77,13 +73,12 @@ fn test_borrow_and_repay() { let borrower_addr = Addr::unchecked("borrower"); // Set user as having the market_collateral deposited - set_collateral(deps.as_mut(), &borrower_addr, "uatom", true); - - // Set the querier to return a certain collateral balance - let deposit_coin_address = Addr::unchecked("ma-uatom"); - deps.querier.set_cw20_balances( - deposit_coin_address, - &[(borrower_addr.clone(), Uint128::new(10000) * SCALING_FACTOR)], + set_collateral( + deps.as_mut(), + &borrower_addr, + "uatom", + Uint128::new(10000) * SCALING_FACTOR, + true, ); // * @@ -114,6 +109,13 @@ fn test_borrow_and_repay() { }, ); + let expected_debt_scaled_1_after_borrow = compute_scaled_amount( + borrow_amount, + expected_params_uosmo.borrow_index, + ScalingOperation::Ceil, + ) + .unwrap(); + // check correct messages and logging assert_eq!( res.messages, @@ -126,10 +128,11 @@ fn test_borrow_and_repay() { res.attributes, vec![ attr("action", "outposts/red-bank/borrow"), - attr("denom", "uosmo"), - attr("user", "borrower"), + attr("sender", "borrower"), attr("recipient", "borrower"), + attr("denom", "uosmo"), attr("amount", borrow_amount.to_string()), + attr("amount_scaled", expected_debt_scaled_1_after_borrow), ] ); assert_eq!(res.events, vec![th_build_interests_updated_event("uosmo", &expected_params_uosmo)]); @@ -139,16 +142,9 @@ fn test_borrow_and_repay() { assert!(!has_debt_position(deps.as_ref(), &borrower_addr, "uusd")); let debt = DEBTS.load(&deps.storage, (&borrower_addr, "uosmo")).unwrap(); - let expected_debt_scaled_1_after_borrow = compute_scaled_amount( - borrow_amount, - expected_params_uosmo.borrow_index, - ScalingOperation::Ceil, - ) - .unwrap(); + assert_eq!(expected_debt_scaled_1_after_borrow, debt.amount_scaled); let market_1_after_borrow = MARKETS.load(&deps.storage, "uosmo").unwrap(); - - assert_eq!(expected_debt_scaled_1_after_borrow, debt.amount_scaled); assert_eq!(expected_debt_scaled_1_after_borrow, market_1_after_borrow.debt_total_scaled); assert_eq!(expected_params_uosmo.borrow_rate, market_1_after_borrow.borrow_rate); assert_eq!(expected_params_uosmo.liquidity_rate, market_1_after_borrow.liquidity_rate); @@ -232,6 +228,13 @@ fn test_borrow_and_repay() { }, ); + let expected_debt_scaled_2_after_borrow_2 = compute_scaled_amount( + borrow_amount, + expected_params_uusd.borrow_index, + ScalingOperation::Ceil, + ) + .unwrap(); + // check correct messages and logging assert_eq!( res.messages, @@ -244,24 +247,19 @@ fn test_borrow_and_repay() { res.attributes, vec![ attr("action", "outposts/red-bank/borrow"), - attr("denom", "uusd"), - attr("user", "borrower"), + attr("sender", "borrower"), attr("recipient", "borrower"), + attr("denom", "uusd"), attr("amount", borrow_amount.to_string()), + attr("amount_scaled", expected_debt_scaled_2_after_borrow_2), ] ); assert_eq!(res.events, vec![th_build_interests_updated_event("uusd", &expected_params_uusd)]); let debt2 = DEBTS.load(&deps.storage, (&borrower_addr, "uusd")).unwrap(); - let market_2_after_borrow_2 = MARKETS.load(&deps.storage, "uusd").unwrap(); - - let expected_debt_scaled_2_after_borrow_2 = compute_scaled_amount( - borrow_amount, - expected_params_uusd.borrow_index, - ScalingOperation::Ceil, - ) - .unwrap(); assert_eq!(expected_debt_scaled_2_after_borrow_2, debt2.amount_scaled); + + let market_2_after_borrow_2 = MARKETS.load(&deps.storage, "uusd").unwrap(); assert_eq!(expected_debt_scaled_2_after_borrow_2, market_2_after_borrow_2.debt_total_scaled); assert_eq!(expected_params_uusd.borrow_rate, market_2_after_borrow_2.borrow_rate); assert_eq!(expected_params_uusd.liquidity_rate, market_2_after_borrow_2.liquidity_rate); @@ -313,15 +311,23 @@ fn test_borrow_and_repay() { }, ); + let expected_repay_amount_scaled = compute_scaled_amount( + repay_amount, + expected_params_uusd.borrow_index, + ScalingOperation::Ceil, + ) + .unwrap(); + assert_eq!(res.messages, vec![]); assert_eq!( res.attributes, vec![ attr("action", "outposts/red-bank/repay"), - attr("denom", "uusd"), attr("sender", "borrower"), - attr("user", "borrower"), + attr("on_behalf_of", "borrower"), + attr("denom", "uusd"), attr("amount", repay_amount.to_string()), + attr("amount_scaled", expected_repay_amount_scaled), ] ); assert_eq!(res.events, vec![th_build_interests_updated_event("uusd", &expected_params_uusd)]); @@ -333,13 +339,8 @@ fn test_borrow_and_repay() { let debt2 = DEBTS.load(&deps.storage, (&borrower_addr, "uusd")).unwrap(); let market_2_after_repay_some_2 = MARKETS.load(&deps.storage, "uusd").unwrap(); - let expected_debt_scaled_2_after_repay_some_2 = expected_debt_scaled_2_after_borrow_2 - - compute_scaled_amount( - repay_amount, - expected_params_uusd.borrow_index, - ScalingOperation::Ceil, - ) - .unwrap(); + let expected_debt_scaled_2_after_repay_some_2 = + expected_debt_scaled_2_after_borrow_2 - expected_repay_amount_scaled; assert_eq!(expected_debt_scaled_2_after_repay_some_2, debt2.amount_scaled); assert_eq!( expected_debt_scaled_2_after_repay_some_2, @@ -384,10 +385,11 @@ fn test_borrow_and_repay() { res.attributes, vec![ attr("action", "outposts/red-bank/repay"), - attr("denom", "uusd"), attr("sender", "borrower"), - attr("user", "borrower"), + attr("on_behalf_of", "borrower"), + attr("denom", "uusd"), attr("amount", repay_amount.to_string()), + attr("amount_scaled", expected_debt_scaled_2_after_repay_some_2), ] ); assert_eq!(res.events, vec![th_build_interests_updated_event("uusd", &expected_params_uusd),]); @@ -453,10 +455,11 @@ fn test_borrow_and_repay() { res.attributes, vec![ attr("action", "outposts/red-bank/repay"), - attr("denom", "uosmo"), attr("sender", "borrower"), - attr("user", "borrower"), + attr("on_behalf_of", "borrower"), + attr("denom", "uosmo"), attr("amount", (repay_amount - expected_refund_amount).to_string()), + attr("amount_scaled", expected_debt_scaled_1_after_borrow_again), ] ); assert_eq!( @@ -481,14 +484,12 @@ fn test_repay_on_behalf_of() { deps.querier.set_oracle_price("borrowedcoinnative", Decimal::one()); let mock_market_1 = Market { - ma_token_address: Addr::unchecked("matoken1"), liquidity_index: Decimal::one(), borrow_index: Decimal::one(), max_loan_to_value: Decimal::from_ratio(50u128, 100u128), ..Default::default() }; let mock_market_2 = Market { - ma_token_address: Addr::unchecked("matoken2"), liquidity_index: Decimal::one(), borrow_index: Decimal::one(), max_loan_to_value: Decimal::from_ratio(50u128, 100u128), @@ -502,13 +503,12 @@ fn test_repay_on_behalf_of() { let user_addr = Addr::unchecked("user"); // Set user as having the market_1_initial (collateral) deposited - set_collateral(deps.as_mut(), &borrower_addr, &market_1_initial.denom, true); - - // Set the querier to return a certain collateral balance - let deposit_coin_address = Addr::unchecked("matoken1"); - deps.querier.set_cw20_balances( - deposit_coin_address, - &[(borrower_addr.clone(), Uint128::new(10000) * SCALING_FACTOR)], + set_collateral( + deps.as_mut(), + &borrower_addr, + &market_1_initial.denom, + Uint128::new(10000) * SCALING_FACTOR, + true, ); // * @@ -553,10 +553,11 @@ fn test_repay_on_behalf_of() { res.attributes, vec![ attr("action", "outposts/red-bank/repay"), - attr("denom", "borrowedcoinnative"), attr("sender", "user"), - attr("user", "borrower"), + attr("on_behalf_of", "borrower"), + attr("denom", "borrowedcoinnative"), attr("amount", repay_amount.to_string()), + attr("amount_scaled", Uint128::new(repay_amount) * SCALING_FACTOR), ] ); } @@ -591,7 +592,6 @@ fn test_borrow_uusd() { let ltv = Decimal::from_ratio(7u128, 10u128); let mock_market = Market { - ma_token_address: Addr::unchecked("matoken"), liquidity_index: Decimal::one(), max_loan_to_value: ltv, borrow_index: Decimal::from_ratio(20u128, 10u128), @@ -605,12 +605,7 @@ fn test_borrow_uusd() { // Set user as having the market_collateral deposited let deposit_amount_scaled = Uint128::new(110_000) * SCALING_FACTOR; - set_collateral(deps.as_mut(), &borrower_addr, "uusd", true); - - // Set the querier to return collateral balance - let deposit_coin_address = Addr::unchecked("matoken"); - deps.querier - .set_cw20_balances(deposit_coin_address, &[(borrower_addr.clone(), deposit_amount_scaled)]); + set_collateral(deps.as_mut(), &borrower_addr, "uusd", deposit_amount_scaled, true); // borrow with insufficient collateral, should fail let new_block_time = 120u64; @@ -675,7 +670,6 @@ fn test_borrow_full_liquidity_and_then_repay() { let ltv = Decimal::one(); let mock_market = Market { - ma_token_address: Addr::unchecked("matoken"), liquidity_index: Decimal::one(), max_loan_to_value: ltv, borrow_index: Decimal::one(), @@ -690,13 +684,12 @@ fn test_borrow_full_liquidity_and_then_repay() { // User should have amount of collateral more than initial liquidity in order to borrow full liquidity let deposit_amount = initial_liquidity + 1000u128; - set_collateral(deps.as_mut(), &borrower_addr, "uusd", true); - - // Set the querier to return collateral balance - let deposit_coin_address = Addr::unchecked("matoken"); - deps.querier.set_cw20_balances( - deposit_coin_address, - &[(borrower_addr, Uint128::new(deposit_amount) * SCALING_FACTOR)], + set_collateral( + deps.as_mut(), + &borrower_addr, + "uusd", + Uint128::new(deposit_amount) * SCALING_FACTOR, + true, ); // Borrow full liquidity @@ -769,7 +762,6 @@ fn test_borrow_collateral_check() { // NOTE: base asset price (asset3) should be set to 1 by the oracle helper let mock_market_1 = Market { - ma_token_address: Addr::unchecked("matoken1"), max_loan_to_value: Decimal::from_ratio(8u128, 10u128), debt_total_scaled: Uint128::zero(), liquidity_index: Decimal::one(), @@ -777,7 +769,6 @@ fn test_borrow_collateral_check() { ..Default::default() }; let mock_market_2 = Market { - ma_token_address: Addr::unchecked("matoken2"), max_loan_to_value: Decimal::from_ratio(6u128, 10u128), debt_total_scaled: Uint128::zero(), liquidity_index: Decimal::one(), @@ -785,7 +776,6 @@ fn test_borrow_collateral_check() { ..Default::default() }; let mock_market_3 = Market { - ma_token_address: Addr::unchecked("matoken3"), max_loan_to_value: Decimal::from_ratio(4u128, 10u128), debt_total_scaled: Uint128::zero(), liquidity_index: Decimal::one(), @@ -802,23 +792,14 @@ fn test_borrow_collateral_check() { let borrower_addr = Addr::unchecked("borrower"); - // Set user as having all the markets as collateral - set_collateral(deps.as_mut(), &borrower_addr, &market_1_initial.denom, true); - set_collateral(deps.as_mut(), &borrower_addr, &market_2_initial.denom, true); - set_collateral(deps.as_mut(), &borrower_addr, &market_3_initial.denom, true); - - let ma_token_address_1 = Addr::unchecked("matoken1"); - let ma_token_address_2 = Addr::unchecked("matoken2"); - let ma_token_address_3 = Addr::unchecked("matoken3"); - let balance_1 = Uint128::new(4_000_000) * SCALING_FACTOR; let balance_2 = Uint128::new(7_000_000) * SCALING_FACTOR; let balance_3 = Uint128::new(3_000_000) * SCALING_FACTOR; - // Set the querier to return a certain collateral balance - deps.querier.set_cw20_balances(ma_token_address_1, &[(borrower_addr.clone(), balance_1)]); - deps.querier.set_cw20_balances(ma_token_address_2, &[(borrower_addr.clone(), balance_2)]); - deps.querier.set_cw20_balances(ma_token_address_3, &[(borrower_addr, balance_3)]); + // Set user as having all the markets as collateral + set_collateral(deps.as_mut(), &borrower_addr, &market_1_initial.denom, balance_1, true); + set_collateral(deps.as_mut(), &borrower_addr, &market_2_initial.denom, balance_2, true); + set_collateral(deps.as_mut(), &borrower_addr, &market_3_initial.denom, balance_3, true); let max_borrow_allowed_in_base_asset = (market_1_initial.max_loan_to_value * compute_underlying_amount( @@ -876,7 +857,6 @@ fn test_cannot_borrow_if_market_not_enabled() { let mut deps = th_setup(&[]); let mock_market = Market { - ma_token_address: Addr::unchecked("ma_somecoin"), borrow_enabled: false, ..Default::default() }; @@ -908,7 +888,6 @@ fn test_borrow_and_send_funds_to_another_user() { let another_user_addr = Addr::unchecked("another_user"); let mock_market = Market { - ma_token_address: Addr::unchecked("matoken"), liquidity_index: Decimal::one(), borrow_index: Decimal::one(), max_loan_to_value: Decimal::from_ratio(5u128, 10u128), @@ -919,12 +898,7 @@ fn test_borrow_and_send_funds_to_another_user() { // Set user as having the market_collateral deposited let deposit_amount_scaled = Uint128::new(100_000) * SCALING_FACTOR; - set_collateral(deps.as_mut(), &borrower_addr, &market.denom, true); - - // Set the querier to return collateral balance - let deposit_coin_address = Addr::unchecked("matoken"); - deps.querier - .set_cw20_balances(deposit_coin_address, &[(borrower_addr.clone(), deposit_amount_scaled)]); + set_collateral(deps.as_mut(), &borrower_addr, &market.denom, deposit_amount_scaled, true); let borrow_amount = Uint128::from(1000u128); let msg = ExecuteMsg::Borrow { @@ -969,10 +943,11 @@ fn test_borrow_and_send_funds_to_another_user() { res.attributes, vec![ attr("action", "outposts/red-bank/borrow"), - attr("denom", "uusd"), - attr("user", borrower_addr), + attr("sender", borrower_addr), attr("recipient", another_user_addr), + attr("denom", "uusd"), attr("amount", borrow_amount.to_string()), + attr("amount_scaled", borrow_amount * SCALING_FACTOR), ] ); } diff --git a/contracts/red-bank/tests/test_deposit.rs b/contracts/red-bank/tests/test_deposit.rs index 2216501c..35eba2af 100644 --- a/contracts/red-bank/tests/test_deposit.rs +++ b/contracts/red-bank/tests/test_deposit.rs @@ -1,65 +1,230 @@ use std::any::type_name; -use cosmwasm_std::testing::mock_info; -use cosmwasm_std::{ - attr, coin, to_binary, Addr, CosmosMsg, Decimal, StdError, SubMsg, Uint128, WasmMsg, -}; -use cw20::Cw20ExecuteMsg; +use cosmwasm_std::testing::{mock_env, mock_info, MockApi, MockStorage}; +use cosmwasm_std::{attr, coin, coins, Addr, Decimal, OwnedDeps, StdError, StdResult, Uint128}; use cw_utils::PaymentError; -use mars_outpost::red_bank::{ExecuteMsg, Market}; -use mars_testing::{mock_env, mock_env_at_block_time, MockEnvParams}; - +use mars_outpost::red_bank::{Collateral, ExecuteMsg, Market}; use mars_red_bank::contract::execute; use mars_red_bank::error::ContractError; use mars_red_bank::interest_rates::{compute_scaled_amount, ScalingOperation, SCALING_FACTOR}; use mars_red_bank::state::{COLLATERALS, MARKETS}; +use mars_testing::{mock_env_at_block_time, MarsMockQuerier}; use helpers::{ - th_build_interests_updated_event, th_get_expected_indices_and_rates, th_init_market, th_setup, + set_collateral, th_build_interests_updated_event, th_get_expected_indices_and_rates, th_setup, }; mod helpers; -#[test] -fn test_deposit_native_asset() { - let initial_liquidity = Uint128::from(10000000_u128); - let mut deps = th_setup(&[coin(initial_liquidity.into(), "somecoin")]); - let reserve_factor = Decimal::from_ratio(1u128, 10u128); - let ma_token_addr = Addr::unchecked("matoken"); +struct TestSuite { + deps: OwnedDeps, + denom: &'static str, + depositor_addr: Addr, + initial_market: Market, + initial_liquidity: Uint128, +} - deps.querier.set_cw20_total_supply(ma_token_addr.clone(), Uint128::new(10_000_000)); +fn setup_test() -> TestSuite { + let denom = "uosmo"; + let initial_liquidity = Uint128::new(10_000_000); - let mock_market = Market { - ma_token_address: ma_token_addr, + let mut deps = th_setup(&[coin(initial_liquidity.u128(), denom)]); + + let market = Market { + denom: denom.to_string(), liquidity_index: Decimal::from_ratio(11u128, 10u128), max_loan_to_value: Decimal::one(), borrow_index: Decimal::from_ratio(1u128, 1u128), borrow_rate: Decimal::from_ratio(10u128, 100u128), liquidity_rate: Decimal::from_ratio(10u128, 100u128), - reserve_factor, + reserve_factor: Decimal::from_ratio(1u128, 10u128), + collateral_total_scaled: Uint128::new(10_000_000) * SCALING_FACTOR, debt_total_scaled: Uint128::new(10_000_000) * SCALING_FACTOR, indexes_last_updated: 10000000, deposit_cap: Uint128::new(12_000_000), ..Default::default() }; - let market = th_init_market(deps.as_mut(), "somecoin", &mock_market); + MARKETS.save(deps.as_mut().storage, denom, &market).unwrap(); + + TestSuite { + deps, + denom, + depositor_addr: Addr::unchecked("larry"), + initial_market: market, + initial_liquidity, + } +} + +#[test] +fn depositing_with_no_coin_sent() { + let TestSuite { + mut deps, + depositor_addr, + .. + } = setup_test(); + + let err = execute( + deps.as_mut(), + mock_env(), + mock_info(depositor_addr.as_str(), &[]), + ExecuteMsg::Deposit { + on_behalf_of: None, + }, + ) + .unwrap_err(); + assert_eq!(err, PaymentError::NoFunds {}.into()); +} + +#[test] +fn depositing_with_multiple_coins_sent() { + let TestSuite { + mut deps, + depositor_addr, + .. + } = setup_test(); + + let sent_coins = vec![coin(123, "uatom"), coin(456, "uosmo")]; + + let err = execute( + deps.as_mut(), + mock_env(), + mock_info(depositor_addr.as_str(), &sent_coins), + ExecuteMsg::Deposit { + on_behalf_of: None, + }, + ) + .unwrap_err(); + assert_eq!(err, PaymentError::MultipleDenoms {}.into()); +} + +#[test] +fn depositing_to_non_existent_market() { + let TestSuite { + mut deps, + depositor_addr, + .. + } = setup_test(); + + // there isn't a market for this denom + let false_denom = "usteak"; + + let err = execute( + deps.as_mut(), + mock_env(), + mock_info(depositor_addr.as_str(), &coins(123, false_denom)), + ExecuteMsg::Deposit { + on_behalf_of: None, + }, + ) + .unwrap_err(); + assert_eq!(err, StdError::not_found(type_name::()).into()); +} + +#[test] +fn depositing_to_disabled_market() { + let TestSuite { + mut deps, + denom, + depositor_addr, + .. + } = setup_test(); + + // disable the market + MARKETS + .update(deps.as_mut().storage, denom, |opt| -> StdResult<_> { + let mut market = opt.unwrap(); + market.deposit_enabled = false; + Ok(market) + }) + .unwrap(); + + let err = execute( + deps.as_mut(), + mock_env(), + mock_info(depositor_addr.as_str(), &coins(123, denom)), + ExecuteMsg::Deposit { + on_behalf_of: None, + }, + ) + .unwrap_err(); + assert_eq!( + err, + ContractError::DepositNotEnabled { + denom: denom.to_string(), + } + ); +} + +#[test] +fn depositing_above_cap() { + let TestSuite { + mut deps, + denom, + depositor_addr, + .. + } = setup_test(); + + // set a deposit cap + MARKETS + .update(deps.as_mut().storage, denom, |opt| -> StdResult<_> { + let mut market = opt.unwrap(); + market.collateral_total_scaled = Uint128::new(9_000_000) * SCALING_FACTOR; + market.deposit_cap = Uint128::new(10_000_000); + Ok(market) + }) + .unwrap(); + + // try deposit with a big amount, should fail + let err = execute( + deps.as_mut(), + mock_env_at_block_time(10000100), + mock_info(depositor_addr.as_str(), &coins(1_000_001, denom)), + ExecuteMsg::Deposit { + on_behalf_of: None, + }, + ) + .unwrap_err(); + assert_eq!( + err, + ContractError::DepositCapExceeded { + denom: denom.to_string() + } + ); + + // deposit a smaller amount, should work + let result = execute( + deps.as_mut(), + mock_env_at_block_time(10000100), + mock_info(depositor_addr.as_str(), &coins(123, denom)), + ExecuteMsg::Deposit { + on_behalf_of: None, + }, + ); + assert!(result.is_ok()); +} + +#[test] +fn depositing_without_existing_position() { + let TestSuite { + mut deps, + denom, + depositor_addr, + initial_market, + initial_liquidity, + } = setup_test(); + + let block_time = 10000100; let deposit_amount = 110000; - let env = mock_env_at_block_time(10000100); - let info = cosmwasm_std::testing::mock_info("depositor", &[coin(deposit_amount, "somecoin")]); - let msg = ExecuteMsg::Deposit { - on_behalf_of: None, - }; - let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + // compute expected market parameters let expected_params = th_get_expected_indices_and_rates( - &market, - env.block.time.seconds(), + &initial_market, + block_time, initial_liquidity, Default::default(), ); - let expected_mint_amount = compute_scaled_amount( Uint128::from(deposit_amount), expected_params.liquidity_index, @@ -67,194 +232,155 @@ fn test_deposit_native_asset() { ) .unwrap(); - // mints coin_amount/liquidity_index - assert_eq!( - res.messages, - vec![SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: "matoken".to_string(), - msg: to_binary(&Cw20ExecuteMsg::Mint { - recipient: "depositor".to_string(), - amount: expected_mint_amount, - }) - .unwrap(), - funds: vec![] - }))] - ); + let res = execute( + deps.as_mut(), + mock_env_at_block_time(block_time), + mock_info(depositor_addr.as_str(), &coins(deposit_amount, denom)), + ExecuteMsg::Deposit { + on_behalf_of: None, + }, + ) + .unwrap(); + assert_eq!(res.messages, vec![]); assert_eq!( res.attributes, vec![ attr("action", "outposts/red-bank/deposit"), - attr("denom", "somecoin"), - attr("sender", "depositor"), - attr("user", "depositor"), + attr("sender", &depositor_addr), + attr("on_behalf_of", &depositor_addr), + attr("denom", denom), attr("amount", deposit_amount.to_string()), + attr("amount_scaled", expected_mint_amount), ] ); - assert_eq!(res.events, vec![th_build_interests_updated_event("somecoin", &expected_params)]); + assert_eq!(res.events, vec![th_build_interests_updated_event(denom, &expected_params)]); - let market = MARKETS.load(&deps.storage, "somecoin").unwrap(); + // indexes and interest rates should have been updated + let market = MARKETS.load(deps.as_ref().storage, denom).unwrap(); + assert_eq!(market.borrow_index, expected_params.borrow_index); + assert_eq!(market.liquidity_index, expected_params.liquidity_index); assert_eq!(market.borrow_rate, expected_params.borrow_rate); assert_eq!(market.liquidity_rate, expected_params.liquidity_rate); - assert_eq!(market.liquidity_index, expected_params.liquidity_index); - assert_eq!(market.borrow_index, expected_params.borrow_index); - - // send many native coins - let info = cosmwasm_std::testing::mock_info( - "depositor", - &[coin(100, "somecoin1"), coin(200, "somecoin2")], - ); - let msg = ExecuteMsg::Deposit { - on_behalf_of: None, - }; - let error_res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!(error_res, PaymentError::MultipleDenoms {}.into()); - // empty deposit fails - let info = mock_info("depositor", &[]); - let msg = ExecuteMsg::Deposit { - on_behalf_of: None, - }; - let error_res = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(error_res, PaymentError::NoFunds {}.into()); -} - -#[test] -fn test_cannot_deposit_if_no_market() { - let mut deps = th_setup(&[]); - let env = mock_env(MockEnvParams::default()); + // total collateral amount should have been updated + let expected = initial_market.collateral_total_scaled + expected_mint_amount; + assert_eq!(market.collateral_total_scaled, expected); - let info = cosmwasm_std::testing::mock_info("depositer", &[coin(110000, "somecoin")]); - let msg = ExecuteMsg::Deposit { - on_behalf_of: None, - }; - let error_res = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!(error_res, StdError::not_found(type_name::()).into()); -} - -#[test] -fn test_cannot_deposit_if_market_not_enabled() { - let mut deps = th_setup(&[]); - - let mock_market = Market { - ma_token_address: Addr::unchecked("ma_somecoin"), - deposit_enabled: false, - ..Default::default() - }; - th_init_market(deps.as_mut(), "somecoin", &mock_market); - - // Check error when deposit not allowed on market - let env = mock_env(MockEnvParams::default()); - let info = cosmwasm_std::testing::mock_info("depositor", &[coin(110000, "somecoin")]); - let msg = ExecuteMsg::Deposit { - on_behalf_of: None, - }; - let error_res = execute(deps.as_mut(), env, info, msg).unwrap_err(); + // the depositor previously did not have a collateral position + // a position should have been created with the correct scaled amount, and enabled by default + let collateral = COLLATERALS.load(deps.as_ref().storage, (&depositor_addr, denom)).unwrap(); assert_eq!( - error_res, - ContractError::DepositNotEnabled { - denom: "somecoin".to_string() + collateral, + Collateral { + amount_scaled: expected_mint_amount, + enabled: true } ); } #[test] -fn test_deposit_on_behalf_of() { - let initial_liquidity = 10000000; - let mut deps = th_setup(&[coin(initial_liquidity, "somecoin")]); - let ma_token_addr = Addr::unchecked("matoken"); - - deps.querier.set_cw20_total_supply(ma_token_addr.clone(), Uint128::new(10_000_000)); +fn depositing_with_existing_position() { + let TestSuite { + mut deps, + denom, + depositor_addr, + initial_market, + initial_liquidity, + } = setup_test(); - let mock_market = Market { - ma_token_address: ma_token_addr, - liquidity_index: Decimal::one(), - borrow_index: Decimal::one(), - ..Default::default() - }; - let market = th_init_market(deps.as_mut(), "somecoin", &mock_market); + // create a collateral position for the user, with the `enabled` parameter as false + let collateral_amount_scaled = Uint128::new(123456); + set_collateral(deps.as_mut(), &depositor_addr, denom, collateral_amount_scaled, false); - let depositor_addr = Addr::unchecked("depositor"); - let another_user_addr = Addr::unchecked("another_user"); + let block_time = 10000100; let deposit_amount = 110000; - let env = mock_env(MockEnvParams::default()); - let info = cosmwasm_std::testing::mock_info( - depositor_addr.as_str(), - &[coin(deposit_amount, "somecoin")], - ); - let msg = ExecuteMsg::Deposit { - on_behalf_of: Some(another_user_addr.to_string()), - }; - let res = execute(deps.as_mut(), env, info, msg).unwrap(); + // compute expected market parameters + let expected_params = th_get_expected_indices_and_rates( + &initial_market, + block_time, + initial_liquidity, + Default::default(), + ); let expected_mint_amount = compute_scaled_amount( Uint128::from(deposit_amount), - market.liquidity_index, + expected_params.liquidity_index, ScalingOperation::Truncate, ) .unwrap(); - // 'depositor' should not have created a new collateral position - let opt = COLLATERALS.may_load(deps.as_ref().storage, (&depositor_addr, "somecoin")).unwrap(); - assert!(opt.is_none()); - - // 'another_user' should have created a new collateral position - let collateral = - COLLATERALS.load(deps.as_ref().storage, (&another_user_addr, "somecoin")).unwrap(); - assert!(collateral.enabled); + execute( + deps.as_mut(), + mock_env_at_block_time(block_time), + mock_info(depositor_addr.as_str(), &coins(deposit_amount, denom)), + ExecuteMsg::Deposit { + on_behalf_of: None, + }, + ) + .unwrap(); - // recipient should be `another_user` + // the depositor's scaled collateral amount should have been increased + // however, the `enabled` status should not been affected + let collateral = COLLATERALS.load(deps.as_ref().storage, (&depositor_addr, denom)).unwrap(); + let expected = collateral_amount_scaled + expected_mint_amount; assert_eq!( - res.messages, - vec![SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: "matoken".to_string(), - msg: to_binary(&Cw20ExecuteMsg::Mint { - recipient: another_user_addr.to_string(), - amount: expected_mint_amount, - }) - .unwrap(), - funds: vec![] - }))] - ); - assert_eq!( - res.attributes, - vec![ - attr("action", "outposts/red-bank/deposit"), - attr("denom", "somecoin"), - attr("sender", depositor_addr), - attr("user", another_user_addr), - attr("amount", deposit_amount.to_string()), - ] + collateral, + Collateral { + amount_scaled: expected, + enabled: false + } ); } #[test] -fn test_exceeding_deposit_cap() { - let initial_liquidity = Uint128::from(10_000_000u128); - let mut deps = th_setup(&[coin(initial_liquidity.into(), "somecoin")]); - let ma_token_addr = Addr::unchecked("matoken"); +fn depositing_on_behalf_of() { + let TestSuite { + mut deps, + denom, + depositor_addr, + initial_market, + initial_liquidity, + } = setup_test(); - // Set the scaled total supply of the maToken `somecoin` - deps.querier.set_cw20_total_supply(ma_token_addr.clone(), Uint128::new(9_000_000_000_000)); + let deposit_amount = 123456u128; + let on_behalf_of_addr = Addr::unchecked("jake"); - let mock_market = Market { - ma_token_address: ma_token_addr, - deposit_cap: Uint128::new(10_000_000), - ..Default::default() - }; - th_init_market(deps.as_mut(), "somecoin", &mock_market); + // compute expected market parameters + let block_time = 10000300; + let expected_params = th_get_expected_indices_and_rates( + &initial_market, + block_time, + initial_liquidity, + Default::default(), + ); + let expected_mint_amount = compute_scaled_amount( + Uint128::from(deposit_amount), + expected_params.liquidity_index, + ScalingOperation::Truncate, + ) + .unwrap(); - let deposit_amount = 1_000_001; - let env = mock_env_at_block_time(10000100); - let info = cosmwasm_std::testing::mock_info("depositor", &[coin(deposit_amount, "somecoin")]); - let msg = ExecuteMsg::Deposit { - on_behalf_of: None, - }; - let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + execute( + deps.as_mut(), + mock_env_at_block_time(block_time), + mock_info(depositor_addr.as_str(), &coins(deposit_amount, denom)), + ExecuteMsg::Deposit { + on_behalf_of: Some(on_behalf_of_addr.clone().into()), + }, + ) + .unwrap(); + // depositor should not have created a new collateral position + let opt = COLLATERALS.may_load(deps.as_ref().storage, (&depositor_addr, denom)).unwrap(); + assert!(opt.is_none()); + + // the recipient should have created a new collateral position + let collateral = COLLATERALS.load(deps.as_ref().storage, (&on_behalf_of_addr, denom)).unwrap(); assert_eq!( - err, - ContractError::DepositCapExceeded { - denom: "somecoin".to_string() + collateral, + Collateral { + amount_scaled: expected_mint_amount, + enabled: true, } ); } diff --git a/contracts/red-bank/tests/test_liquidate.rs b/contracts/red-bank/tests/test_liquidate.rs index c72166e7..c5f2b53d 100644 --- a/contracts/red-bank/tests/test_liquidate.rs +++ b/contracts/red-bank/tests/test_liquidate.rs @@ -1,24 +1,23 @@ use cosmwasm_std::testing::mock_info; use cosmwasm_std::{ - attr, coin, coins, to_binary, Addr, BankMsg, CosmosMsg, Decimal, StdResult, SubMsg, Uint128, - WasmMsg, + attr, coin, coins, Addr, BankMsg, CosmosMsg, Decimal, StdResult, SubMsg, Uint128, }; use cw_utils::PaymentError; +use mars_outpost::address_provider::MarsContract; +use mars_outpost::math; use mars_outpost::red_bank::{Debt, ExecuteMsg, InterestRateModel, Market}; -use mars_outpost::{ma_token, math}; -use mars_testing::{mock_env, mock_env_at_block_time, MockEnvParams}; - use mars_red_bank::contract::execute; use mars_red_bank::error::ContractError; use mars_red_bank::interest_rates::{ compute_scaled_amount, compute_underlying_amount, get_scaled_liquidity_amount, ScalingOperation, SCALING_FACTOR, }; -use mars_red_bank::state::{CONFIG, DEBTS, MARKETS}; +use mars_red_bank::state::{COLLATERALS, CONFIG, DEBTS, MARKETS}; +use mars_testing::{mock_env, mock_env_at_block_time, MockEnvParams}; use helpers::{ - has_collateral_position, has_debt_position, set_collateral, th_build_interests_updated_event, + 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, unset_collateral, TestUtilizationDeltaInfo, }; @@ -55,7 +54,9 @@ fn test_liquidate() { let second_block_time = 16_000_000; // Global debt for the debt market + let expected_global_collateral_scaled = Uint128::new(1_500_000_000) * SCALING_FACTOR; let mut expected_global_debt_scaled = Uint128::new(1_800_000_000) * SCALING_FACTOR; + let mut expected_global_reward_scaled = Uint128::zero(); // can be any number, but just using zero for now for convenience CONFIG .update(deps.as_mut().storage, |mut config| -> StdResult<_> { @@ -78,12 +79,11 @@ fn test_liquidate() { slope_2: Decimal::zero(), }; - let collateral_market_ma_token_addr = Addr::unchecked("ma_collateral"); let collateral_market = Market { - ma_token_address: collateral_market_ma_token_addr.clone(), max_loan_to_value: collateral_max_ltv, liquidation_threshold: collateral_liquidation_threshold, liquidation_bonus: collateral_liquidation_bonus, + collateral_total_scaled: expected_global_collateral_scaled, debt_total_scaled: Uint128::new(800_000_000) * SCALING_FACTOR, liquidity_index: Decimal::one(), borrow_index: Decimal::one(), @@ -96,8 +96,8 @@ fn test_liquidate() { }; let debt_market = Market { - ma_token_address: Addr::unchecked("ma_debt"), max_loan_to_value: Decimal::from_ratio(6u128, 10u128), + collateral_total_scaled: expected_global_reward_scaled, debt_total_scaled: expected_global_debt_scaled, liquidity_index: Decimal::from_ratio(12u128, 10u128), borrow_index: Decimal::from_ratio(14u128, 10u128), @@ -118,10 +118,16 @@ fn test_liquidate() { let debt_market_initial = th_init_market(deps.as_mut(), "debt", &debt_market); th_init_market(deps.as_mut(), "uncollateralized_debt", &uncollateralized_debt_market); + let mut expected_user_collateral_scaled = + Uint128::new(user_collateral_balance) * SCALING_FACTOR; + let mut expected_liquidator_collateral_scaled = Uint128::zero(); + let mut expected_user_debt_scaled = compute_scaled_amount(user_debt, debt_market_initial.borrow_index, ScalingOperation::Ceil) .unwrap(); + let mut expected_total_reward_scaled = Uint128::zero(); + // trying to liquidate user with zero collateral balance should fail { let liquidate_msg = ExecuteMsg::Liquidate { @@ -136,12 +142,12 @@ fn test_liquidate() { } // Create collateral position for the user - set_collateral(deps.as_mut(), &user_addr, &collateral_market_initial.denom, true); - - // Set the querier to return positive collateral balance - deps.querier.set_cw20_balances( - collateral_market_ma_token_addr.clone(), - &[(user_addr.clone(), Uint128::new(user_collateral_balance) * SCALING_FACTOR)], + set_collateral( + deps.as_mut(), + &user_addr, + &collateral_market_initial.denom, + Uint128::new(user_collateral_balance) * SCALING_FACTOR, + true, ); // trying to liquidate user with zero outstanding debt should fail (uncollateralized has not impact) @@ -243,52 +249,27 @@ fn test_liquidate() { ) .unwrap(); - assert_eq!( - res.messages, - vec![ - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: collateral_market_ma_token_addr.to_string(), - msg: to_binary( - &mars_outpost::ma_token::msg::ExecuteMsg::TransferOnLiquidation { - sender: user_addr.to_string(), - recipient: liquidator_addr.to_string(), - amount: expected_liquidated_collateral_amount_scaled, - } - ) - .unwrap(), - funds: vec![] - })), - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: debt_market.ma_token_address.to_string(), - msg: to_binary(&ma_token::msg::ExecuteMsg::Mint { - recipient: "protocol_rewards_collector".to_string(), - amount: compute_scaled_amount( - expected_debt_rates.protocol_rewards_to_distribute, - expected_debt_rates.liquidity_index, - ScalingOperation::Truncate - ) - .unwrap(), - }) - .unwrap(), - funds: vec![] - })), - ] - ); + let expected_reward_amount_scaled = compute_scaled_amount( + expected_debt_rates.protocol_rewards_to_distribute, + expected_debt_rates.liquidity_index, + ScalingOperation::Truncate, + ) + .unwrap(); + + assert_eq!(res.messages, vec![]); mars_testing::assert_eq_vec( res.attributes, vec![ attr("action", "outposts/red-bank/liquidate"), - attr("collateral_denom", "collateral"), - attr("debt_denom", "debt"), attr("user", user_addr.as_str()), attr("liquidator", liquidator_addr.as_str()), - attr( - "collateral_amount_liquidated", - expected_liquidated_collateral_amount.to_string(), - ), - attr("debt_amount_repaid", first_debt_to_repay.to_string()), - attr("refund_amount", "0"), + attr("collateral_denom", "collateral"), + attr("collateral_amount", expected_liquidated_collateral_amount), + attr("collateral_amount_scaled", expected_liquidated_collateral_amount_scaled), + attr("debt_denom", "debt"), + attr("debt_amount", first_debt_to_repay), + attr("debt_amount_scaled", expected_debt_rates.less_debt_scaled), ], ); assert_eq!( @@ -296,10 +277,17 @@ fn test_liquidate() { vec![th_build_interests_updated_event("debt", &expected_debt_rates)] ); - // check user still has deposited collateral asset and - // still has outstanding debt in debt asset - assert!(has_collateral_position(deps.as_ref(), &user_addr, "collateral")); - assert!(has_debt_position(deps.as_ref(), &user_addr, "debt")); + // user's collateral scaled amount should have been correctly decreased + let collateral = + COLLATERALS.load(deps.as_ref().storage, (&user_addr, "collateral")).unwrap(); + expected_user_collateral_scaled -= expected_liquidated_collateral_amount_scaled; + assert_eq!(collateral.amount_scaled, expected_user_collateral_scaled); + + // liquidator's collateral scaled amount should have been correctly increased + let collateral = + COLLATERALS.load(deps.as_ref().storage, (&liquidator_addr, "collateral")).unwrap(); + expected_liquidator_collateral_scaled += expected_liquidated_collateral_amount_scaled; + assert_eq!(collateral.amount_scaled, expected_liquidator_collateral_scaled); // check user's debt decreased by the appropriate amount let debt = DEBTS.load(&deps.storage, (&user_addr, "debt")).unwrap(); @@ -310,6 +298,20 @@ fn test_liquidate() { // check global debt decreased by the appropriate amount expected_global_debt_scaled -= expected_less_debt_scaled; assert_eq!(expected_global_debt_scaled, debt_market_after.debt_total_scaled); + + // rewards collector's collateral scaled amount **of the debt asset** should have been correctly increased + expected_total_reward_scaled += expected_reward_amount_scaled; + let collateral = COLLATERALS + .load( + deps.as_ref().storage, + (&Addr::unchecked(MarsContract::ProtocolRewardsCollector.to_string()), "debt"), + ) + .unwrap(); + assert_eq!(collateral.amount_scaled, expected_total_reward_scaled); + + // global collateral scaled amount **of the debt asset** should have been correctly increased + expected_global_reward_scaled += expected_reward_amount_scaled; + assert_eq!(debt_market_after.collateral_total_scaled, expected_global_reward_scaled); } // Perform second successful liquidation sending an excess amount (should refund) @@ -377,52 +379,32 @@ fn test_liquidate() { ) .unwrap(); + let expected_reward_amount_scaled = compute_scaled_amount( + expected_debt_rates.protocol_rewards_to_distribute, + expected_debt_rates.liquidity_index, + ScalingOperation::Truncate, + ) + .unwrap(); + assert_eq!( res.messages, - vec![ - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: collateral_market_ma_token_addr.to_string(), - msg: to_binary( - &mars_outpost::ma_token::msg::ExecuteMsg::TransferOnLiquidation { - sender: user_addr.to_string(), - recipient: liquidator_addr.to_string(), - amount: expected_liquidated_collateral_amount_scaled, - } - ) - .unwrap(), - funds: vec![] - })), - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: debt_market.ma_token_address.to_string(), - msg: to_binary(&ma_token::msg::ExecuteMsg::Mint { - recipient: "protocol_rewards_collector".to_string(), - amount: compute_scaled_amount( - expected_debt_rates.protocol_rewards_to_distribute, - expected_debt_rates.liquidity_index, - ScalingOperation::Truncate - ) - .unwrap(), - }) - .unwrap(), - funds: vec![] - })), - SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: liquidator_addr.to_string(), - amount: coins(expected_refund_amount.u128(), "debt") - })), - ] + vec![SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: liquidator_addr.to_string(), + amount: coins(expected_refund_amount.u128(), "debt") + })),] ); mars_testing::assert_eq_vec( vec![ attr("action", "outposts/red-bank/liquidate"), - attr("collateral_denom", "collateral"), - attr("debt_denom", "debt"), attr("user", user_addr.as_str()), attr("liquidator", liquidator_addr.as_str()), - attr("collateral_amount_liquidated", expected_liquidated_collateral_amount), - attr("debt_amount_repaid", expected_less_debt.to_string()), - attr("refund_amount", expected_refund_amount.to_string()), + attr("collateral_denom", "collateral"), + attr("collateral_amount", expected_liquidated_collateral_amount), + attr("collateral_amount_scaled", expected_liquidated_collateral_amount_scaled), + attr("debt_denom", "debt"), + attr("debt_amount", expected_less_debt), + attr("debt_amount_scaled", expected_debt_rates.less_debt_scaled), ], res.attributes, ); @@ -431,10 +413,17 @@ fn test_liquidate() { vec![th_build_interests_updated_event("debt", &expected_debt_rates)], ); - // check user still has deposited collateral asset and - // still has outstanding debt in debt asset - assert!(has_collateral_position(deps.as_ref(), &user_addr, "collateral")); - assert!(has_debt_position(deps.as_ref(), &user_addr, "debt")); + // user's collateral scaled amount should have been correctly decreased + let collateral = + COLLATERALS.load(deps.as_ref().storage, (&user_addr, "collateral")).unwrap(); + expected_user_collateral_scaled -= expected_liquidated_collateral_amount_scaled; + assert_eq!(collateral.amount_scaled, expected_user_collateral_scaled); + + // liquidator's collateral scaled amount should have been correctly increased + let collateral = + COLLATERALS.load(deps.as_ref().storage, (&liquidator_addr, "collateral")).unwrap(); + expected_liquidator_collateral_scaled += expected_liquidated_collateral_amount_scaled; + assert_eq!(collateral.amount_scaled, expected_liquidator_collateral_scaled); // check user's debt decreased by the appropriate amount let debt = DEBTS.load(&deps.storage, (&user_addr, "debt")).unwrap(); @@ -445,6 +434,20 @@ fn test_liquidate() { // check global debt decreased by the appropriate amount expected_global_debt_scaled -= expected_less_debt_scaled; assert_eq!(expected_global_debt_scaled, debt_market_after.debt_total_scaled); + + // rewards collector's collateral scaled amount **of the debt asset** should have been correctly increased + expected_total_reward_scaled += expected_reward_amount_scaled; + let collateral = COLLATERALS + .load( + deps.as_ref().storage, + (&Addr::unchecked(MarsContract::ProtocolRewardsCollector.to_string()), "debt"), + ) + .unwrap(); + assert_eq!(collateral.amount_scaled, expected_total_reward_scaled); + + // global collateral scaled amount **of the debt asset** should have been correctly increased + expected_global_reward_scaled += expected_reward_amount_scaled; + assert_eq!(debt_market_after.collateral_total_scaled, expected_global_reward_scaled); } // Perform full liquidation (user should not be able to use asset as collateral) @@ -453,10 +456,12 @@ fn test_liquidate() { let mut expected_user_debt_scaled = Uint128::new(400) * SCALING_FACTOR; let debt_to_repay = Uint128::from(300u128); - // Set the querier to return positive collateral balance - deps.querier.set_cw20_balances( - collateral_market_ma_token_addr.clone(), - &[(user_addr.clone(), user_collateral_balance_scaled)], + set_collateral( + deps.as_mut(), + &user_addr, + "collateral", + user_collateral_balance_scaled, + true, ); // set user to have positive debt amount in debt asset @@ -534,36 +539,23 @@ fn test_liquidate() { assert_eq!( res.messages, - vec![ - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: collateral_market_ma_token_addr.to_string(), - msg: to_binary( - &mars_outpost::ma_token::msg::ExecuteMsg::TransferOnLiquidation { - sender: user_addr.to_string(), - recipient: liquidator_addr.to_string(), - amount: expected_liquidated_collateral_amount_scaled, - } - ) - .unwrap(), - funds: vec![] - })), - SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: liquidator_addr.to_string(), - amount: coins(expected_refund_amount.u128(), "debt") - })) - ] + vec![SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: liquidator_addr.to_string(), + amount: coins(expected_refund_amount.u128(), "debt") + }))] ); mars_testing::assert_eq_vec( vec![ attr("action", "outposts/red-bank/liquidate"), - attr("collateral_denom", "collateral"), - attr("debt_denom", "debt"), attr("user", user_addr.as_str()), attr("liquidator", liquidator_addr.as_str()), - attr("collateral_amount_liquidated", user_collateral_balance.to_string()), - attr("debt_amount_repaid", expected_less_debt.to_string()), - attr("refund_amount", expected_refund_amount.to_string()), + attr("collateral_denom", "collateral"), + attr("collateral_amount", user_collateral_balance), + attr("collateral_amount_scaled", expected_liquidated_collateral_amount_scaled), + attr("debt_denom", "debt"), + attr("debt_amount", expected_less_debt), + attr("debt_amount_scaled", expected_debt_rates.less_debt_scaled), ], res.attributes, ); @@ -579,7 +571,12 @@ fn test_liquidate() { // the user will still have a dust amount of collateral shares left, leading to the position // not being deleted. This will be addresses in a follow-up PR. assert!(has_collateral_position(deps.as_ref(), &user_addr, "collateral")); - assert!(has_debt_position(deps.as_ref(), &user_addr, "debt")); + + // liquidator's collateral scaled amount should have been correctly increased + let collateral = + COLLATERALS.load(deps.as_ref().storage, (&liquidator_addr, "collateral")).unwrap(); + expected_liquidator_collateral_scaled += expected_liquidated_collateral_amount_scaled; + assert_eq!(collateral.amount_scaled, expected_liquidator_collateral_scaled); // check user's debt decreased by the appropriate amount let debt = DEBTS.load(&deps.storage, (&user_addr, "debt")).unwrap(); @@ -618,22 +615,20 @@ fn test_liquidate_with_same_asset_for_debt_and_collateral() { let user_addr = Addr::unchecked("user"); let liquidator_addr = Addr::unchecked("liquidator"); - let ma_token_address = Addr::unchecked("mathe_asset"); let asset_max_ltv = Decimal::from_ratio(5u128, 10u128); let asset_liquidation_threshold = Decimal::from_ratio(6u128, 10u128); let asset_liquidation_bonus = Decimal::from_ratio(1u128, 10u128); let asset_price = Decimal::from_ratio(2_u128, 1_u128); - let initial_user_debt_balance = Uint128::from(3_000_000_u64); - // NOTE: this should change in practice but it will stay static on this test - // as the balance is mocked and does not get updated - let user_collateral_balance = Uint128::from(2_000_000_u64); - let close_factor = Decimal::from_ratio(1u128, 2u128); - // Global debt for the market (starts at index 1.000000000...) + let initial_user_debt_balance = Uint128::from(3_000_000_u64); + let initial_user_collateral_scaled = Uint128::from(2_000_000_u64) * SCALING_FACTOR; + + let initial_global_collateral_scaled = Uint128::new(400_000_000) * SCALING_FACTOR; let initial_global_debt_scaled = Uint128::new(500_000_000) * SCALING_FACTOR; + let liquidation_block_time = 15_000_000; CONFIG @@ -647,10 +642,10 @@ fn test_liquidate_with_same_asset_for_debt_and_collateral() { deps.querier.set_oracle_price(denom, asset_price); let asset_market = Market { - ma_token_address: ma_token_address.clone(), max_loan_to_value: asset_max_ltv, liquidation_threshold: asset_liquidation_threshold, liquidation_bonus: asset_liquidation_bonus, + collateral_total_scaled: initial_global_collateral_scaled, debt_total_scaled: initial_global_debt_scaled, liquidity_index: Decimal::one(), borrow_index: Decimal::one(), @@ -677,12 +672,12 @@ fn test_liquidate_with_same_asset_for_debt_and_collateral() { .unwrap(); // Create collateral position for the user - set_collateral(deps.as_mut(), &user_addr, &asset_market_initial.denom, true); - - // Set the querier to return positive collateral balance - deps.querier.set_cw20_balances( - ma_token_address.clone(), - &[(user_addr.clone(), user_collateral_balance * SCALING_FACTOR)], + set_collateral( + deps.as_mut(), + &user_addr, + &asset_market_initial.denom, + initial_user_collateral_scaled, + true, ); // set user to have positive debt amount in debt asset @@ -739,361 +734,95 @@ fn test_liquidate_with_same_asset_for_debt_and_collateral() { ) .unwrap(); - assert_eq!( - res.messages, - vec![ - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: ma_token_address.to_string(), - msg: to_binary( - &mars_outpost::ma_token::msg::ExecuteMsg::TransferOnLiquidation { - sender: user_addr.to_string(), - recipient: liquidator_addr.to_string(), - amount: expected_liquidated_amount_scaled, - } - ) - .unwrap(), - funds: vec![] - })), - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: ma_token_address.to_string(), - msg: to_binary(&ma_token::msg::ExecuteMsg::Mint { - recipient: "protocol_rewards_collector".to_string(), - amount: compute_scaled_amount( - expected_rates.protocol_rewards_to_distribute, - expected_rates.liquidity_index, - ScalingOperation::Truncate - ) - .unwrap(), - }) - .unwrap(), - funds: vec![] - })), - ] - ); - - mars_testing::assert_eq_vec( - res.attributes, - vec![ - attr("action", "outposts/red-bank/liquidate"), - attr("collateral_denom", denom), - attr("debt_denom", denom), - attr("user", user_addr.as_str()), - attr("liquidator", liquidator_addr.as_str()), - attr("collateral_amount_liquidated", expected_liquidated_amount.to_string()), - attr("debt_amount_repaid", debt_to_repay.to_string()), - attr("refund_amount", "0"), - ], - ); - assert_eq!(res.events, vec![th_build_interests_updated_event(denom, &expected_rates)]); - - // check user still has deposited collateral asset and - // still has outstanding debt in debt asset - assert!(has_collateral_position(deps.as_ref(), &user_addr, denom)); - assert!(has_debt_position(deps.as_ref(), &user_addr, denom)); - - // TODO!!!!!!!! - // check liquidator gets its collateral bit set - // assert!(has_collateral_position(deps.as_ref(), &liquidator_addr, denom)); - - // check user's debt decreased by the appropriate amount - let debt = DEBTS.load(&deps.storage, (&user_addr, denom)).unwrap(); - let expected_less_debt_scaled = expected_rates.less_debt_scaled; - let expected_user_debt_scaled = initial_user_debt_scaled - expected_less_debt_scaled; - assert_eq!(expected_user_debt_scaled, debt.amount_scaled); - - // check global debt decreased by the appropriate amount - let expected_global_debt_scaled = initial_global_debt_scaled - expected_less_debt_scaled; - assert_eq!(expected_global_debt_scaled, asset_market_after.debt_total_scaled); - } - - // Reset state for next test - { - let debt = Debt { - amount_scaled: initial_user_debt_scaled, - uncollateralized: false, - }; - DEBTS.save(deps.as_mut().storage, (&user_addr, denom), &debt).unwrap(); - - MARKETS.save(deps.as_mut().storage, denom, &asset_market_initial).unwrap(); - - // NOTE: Do not reset liquidator in order to check that position is not reset in next - // liquidation receiving ma tokens - } - - // Perform partial liquidation - { - let debt_to_repay = Uint128::from(400_000_u64); - let liquidate_msg = ExecuteMsg::Liquidate { - user: user_addr.to_string(), - collateral_denom: denom.to_string(), - }; - - let asset_market_before = MARKETS.load(&deps.storage, denom).unwrap(); - - let block_time = liquidation_block_time; - let env = mock_env_at_block_time(block_time); - let info = cosmwasm_std::testing::mock_info( - liquidator_addr.as_str(), - &[coin(debt_to_repay.into(), denom)], - ); - let res = execute(deps.as_mut(), env, info, liquidate_msg).unwrap(); - - let asset_market_after = MARKETS.load(&deps.storage, denom).unwrap(); - let expected_liquidated_amount = math::divide_uint128_by_decimal( - debt_to_repay * asset_price * (Decimal::one() + asset_liquidation_bonus), - asset_price, - ) - .unwrap(); - - // get expected indices and rates for debt market - let expected_rates = th_get_expected_indices_and_rates( - &asset_market_before, - block_time, - available_liquidity, - TestUtilizationDeltaInfo { - less_debt: debt_to_repay, - user_current_debt_scaled: initial_user_debt_scaled, - ..Default::default() - }, - ); - - let expected_liquidated_amount_scaled = compute_scaled_amount( - expected_liquidated_amount, + let expected_reward_amount_scaled = compute_scaled_amount( + expected_rates.protocol_rewards_to_distribute, expected_rates.liquidity_index, ScalingOperation::Truncate, ) .unwrap(); - assert_eq!( - res.messages, - vec![ - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: ma_token_address.to_string(), - msg: to_binary( - &mars_outpost::ma_token::msg::ExecuteMsg::TransferOnLiquidation { - sender: user_addr.to_string(), - recipient: liquidator_addr.to_string(), - amount: expected_liquidated_amount_scaled, - } - ) - .unwrap(), - funds: vec![] - })), - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: ma_token_address.to_string(), - msg: to_binary(&ma_token::msg::ExecuteMsg::Mint { - recipient: "protocol_rewards_collector".to_string(), - amount: compute_scaled_amount( - expected_rates.protocol_rewards_to_distribute, - expected_rates.liquidity_index, - ScalingOperation::Truncate - ) - .unwrap(), - }) - .unwrap(), - funds: vec![] - })), - ] - ); + assert_eq!(res.messages, vec![]); mars_testing::assert_eq_vec( res.attributes, vec![ attr("action", "outposts/red-bank/liquidate"), - attr("collateral_denom", denom), - attr("debt_denom", denom), attr("user", user_addr.as_str()), attr("liquidator", liquidator_addr.as_str()), - attr("collateral_amount_liquidated", expected_liquidated_amount.to_string()), - attr("debt_amount_repaid", debt_to_repay.to_string()), - attr("refund_amount", "0"), + attr("collateral_denom", denom), + attr("collateral_amount", expected_liquidated_amount), + attr("collateral_amount_scaled", expected_liquidated_amount_scaled), + attr("debt_denom", denom), + attr("debt_amount", debt_to_repay), + attr("debt_amount_scaled", expected_rates.less_debt_scaled), ], ); - assert_eq!(res.events, vec![th_build_interests_updated_event(denom, &expected_rates)],); + assert_eq!(res.events, vec![th_build_interests_updated_event(denom, &expected_rates)]); - // check user still has deposited collateral asset and - // still has outstanding debt in debt asset - assert!(has_collateral_position(deps.as_ref(), &user_addr, denom)); - assert!(has_debt_position(deps.as_ref(), &user_addr, denom)); + // user's collateral scaled amount should have been correctly decreased + let collateral = + COLLATERALS.load(deps.as_ref().storage, (&user_addr, "the_asset")).unwrap(); + let expected_user_collateral_scaled = + initial_user_collateral_scaled - expected_liquidated_amount_scaled; + assert_eq!(collateral.amount_scaled, expected_user_collateral_scaled); + + // liquidator's collateral scaled amount should have been correctly increased + let collateral = + COLLATERALS.load(deps.as_ref().storage, (&liquidator_addr, "the_asset")).unwrap(); + assert_eq!(collateral.amount_scaled, expected_liquidated_amount_scaled); + + // rewards collector's collateral scaled amount **of the debt asset** should have been correctly increased + let collateral = COLLATERALS + .load( + deps.as_ref().storage, + (&Addr::unchecked(MarsContract::ProtocolRewardsCollector.to_string()), "the_asset"), + ) + .unwrap(); + assert_eq!(collateral.amount_scaled, expected_reward_amount_scaled); // check user's debt decreased by the appropriate amount let debt = DEBTS.load(&deps.storage, (&user_addr, denom)).unwrap(); - - let expected_less_debt_scaled = expected_rates.less_debt_scaled; - - let expected_user_debt_scaled = initial_user_debt_scaled - expected_less_debt_scaled; - + let expected_user_debt_scaled = initial_user_debt_scaled - expected_rates.less_debt_scaled; assert_eq!(expected_user_debt_scaled, debt.amount_scaled); - // check global debt decreased by the appropriate amount - let expected_global_debt_scaled = initial_global_debt_scaled - expected_less_debt_scaled; + // global collateral scaled amount **of the debt asset** should have been correctly increased + let expected_global_collateral_scaled = + initial_global_collateral_scaled + expected_reward_amount_scaled; + assert_eq!(asset_market_after.collateral_total_scaled, expected_global_collateral_scaled); + // check global debt decreased by the appropriate amount + let expected_global_debt_scaled = + initial_global_debt_scaled - expected_rates.less_debt_scaled; assert_eq!(expected_global_debt_scaled, asset_market_after.debt_total_scaled); } // Reset state for next test { + // user debt let debt = Debt { amount_scaled: initial_user_debt_scaled, uncollateralized: false, }; DEBTS.save(deps.as_mut().storage, (&user_addr, denom), &debt).unwrap(); - MARKETS.save(deps.as_mut().storage, denom, &asset_market_initial).unwrap(); - - // NOTE: Do not reset liquidator having the asset as collateral in order to check - // position changed event is not emitted - } - - // Perform overpaid liquidation - { - let block_time = liquidation_block_time; - // Since debt is being over repayed, we expect to max out the liquidatable debt - // get expected indices and rates for debt and collateral markets - let expected_indices = th_get_expected_indices(&asset_market_initial, block_time); - let user_debt_balance_before = compute_underlying_amount( - initial_user_debt_scaled, - expected_indices.borrow, - ScalingOperation::Ceil, - ) - .unwrap(); - let debt_to_repay = user_debt_balance_before; - let expected_less_debt = user_debt_balance_before * close_factor; - let expected_refund_amount = debt_to_repay - expected_less_debt; - - let liquidate_msg = ExecuteMsg::Liquidate { - user: user_addr.to_string(), - collateral_denom: denom.to_string(), - }; - - let asset_market_before = MARKETS.load(&deps.storage, denom).unwrap(); - - let env = mock_env_at_block_time(block_time); - let info = cosmwasm_std::testing::mock_info( - liquidator_addr.as_str(), - &[coin(debt_to_repay.into(), denom)], - ); - let res = execute(deps.as_mut(), env, info, liquidate_msg).unwrap(); - - let asset_market_after = MARKETS.load(&deps.storage, denom).unwrap(); - let expected_liquidated_amount = math::divide_uint128_by_decimal( - expected_less_debt * asset_price * (Decimal::one() + asset_liquidation_bonus), - asset_price, - ) - .unwrap(); - - // get expected indices and rates for debt market - let expected_rates = th_get_expected_indices_and_rates( - &asset_market_before, - block_time, - available_liquidity, - TestUtilizationDeltaInfo { - less_debt: expected_less_debt, - less_liquidity: expected_refund_amount, - user_current_debt_scaled: initial_user_debt_scaled, - ..Default::default() - }, - ); - - let expected_liquidated_amount_scaled = compute_scaled_amount( - expected_liquidated_amount, - expected_rates.liquidity_index, - ScalingOperation::Truncate, - ) - .unwrap(); - - assert_eq!( - res.messages, - vec![ - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: ma_token_address.to_string(), - msg: to_binary( - &mars_outpost::ma_token::msg::ExecuteMsg::TransferOnLiquidation { - sender: user_addr.to_string(), - recipient: liquidator_addr.to_string(), - amount: expected_liquidated_amount_scaled, - } - ) - .unwrap(), - funds: vec![] - })), - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: ma_token_address.to_string(), - msg: to_binary(&ma_token::msg::ExecuteMsg::Mint { - recipient: "protocol_rewards_collector".to_string(), - amount: compute_scaled_amount( - expected_rates.protocol_rewards_to_distribute, - expected_rates.liquidity_index, - ScalingOperation::Truncate - ) - .unwrap(), - }) - .unwrap(), - funds: vec![] - })), - // NOTE: Tax set to 0 so no tax should be charged - SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: liquidator_addr.to_string(), - amount: coins(expected_refund_amount.u128(), denom) - })), - ] + // user collateral + set_collateral( + deps.as_mut(), + &user_addr, + &asset_market_initial.denom, + initial_user_collateral_scaled, + true, ); - mars_testing::assert_eq_vec( - res.attributes, - vec![ - attr("action", "outposts/red-bank/liquidate"), - attr("collateral_denom", denom), - attr("debt_denom", denom), - attr("user", user_addr.as_str()), - attr("liquidator", liquidator_addr.as_str()), - attr("collateral_amount_liquidated", expected_liquidated_amount.to_string()), - attr("debt_amount_repaid", expected_less_debt.to_string()), - attr("refund_amount", expected_refund_amount), - ], - ); - assert_eq!( - res.events, - vec![ - th_build_interests_updated_event(denom, &expected_rates), - // NOTE: Should not emit position change event as it was changed on the - // first call and was not reset - ] + // liquidator and collector collateral + unset_collateral(deps.as_mut(), &liquidator_addr, &asset_market_initial.denom); + unset_collateral( + deps.as_mut(), + &Addr::unchecked(MarsContract::ProtocolRewardsCollector.to_string()), + &asset_market_initial.denom, ); - // check user still has deposited collateral asset and - // still has outstanding debt in debt asset - assert!(has_collateral_position(deps.as_ref(), &user_addr, denom)); - assert!(has_debt_position(deps.as_ref(), &user_addr, denom)); - - // check user's debt decreased by the appropriate amount - let debt = DEBTS.load(&deps.storage, (&user_addr, denom)).unwrap(); - - let expected_less_debt_scaled = expected_rates.less_debt_scaled; - - let expected_user_debt_scaled = initial_user_debt_scaled - expected_less_debt_scaled; - - assert_eq!(expected_user_debt_scaled, debt.amount_scaled); - - // check global debt decreased by the appropriate amount - let expected_global_debt_scaled = initial_global_debt_scaled - expected_less_debt_scaled; - - assert_eq!(expected_global_debt_scaled, asset_market_after.debt_total_scaled); - } - - // Reset state for next test - { - let debt = Debt { - amount_scaled: initial_user_debt_scaled, - uncollateralized: false, - }; - DEBTS.save(deps.as_mut().storage, (&user_addr, denom), &debt).unwrap(); - MARKETS.save(deps.as_mut().storage, denom, &asset_market_initial).unwrap(); - - // NOTE: reset liquidator to not having the asset as collateral in order to check - // position is not changed when receiving underlying asset - unset_collateral(deps.as_mut(), &liquidator_addr, denom); } // Perform overpaid liquidation @@ -1153,75 +882,71 @@ fn test_liquidate_with_same_asset_for_debt_and_collateral() { ) .unwrap(); + let expected_reward_amount_scaled = compute_scaled_amount( + expected_rates.protocol_rewards_to_distribute, + expected_rates.liquidity_index, + ScalingOperation::Truncate, + ) + .unwrap(); + assert_eq!( res.messages, - vec![ - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: ma_token_address.to_string(), - msg: to_binary( - &mars_outpost::ma_token::msg::ExecuteMsg::TransferOnLiquidation { - sender: user_addr.to_string(), - recipient: liquidator_addr.to_string(), - amount: expected_liquidated_amount_scaled, - } - ) - .unwrap(), - funds: vec![] - })), - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: ma_token_address.to_string(), - msg: to_binary(&ma_token::msg::ExecuteMsg::Mint { - recipient: "protocol_rewards_collector".to_string(), - amount: compute_scaled_amount( - expected_rates.protocol_rewards_to_distribute, - expected_rates.liquidity_index, - ScalingOperation::Truncate - ) - .unwrap(), - }) - .unwrap(), - funds: vec![] - })), - // NOTE: Tax set to 0 so no tax should be charged - SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: liquidator_addr.to_string(), - amount: coins(expected_refund_amount.u128(), denom) - })), - ] + vec![SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: liquidator_addr.to_string(), + amount: coins(expected_refund_amount.u128(), denom) + })),] ); mars_testing::assert_eq_vec( res.attributes, vec![ attr("action", "outposts/red-bank/liquidate"), - attr("collateral_denom", denom), - attr("debt_denom", denom), attr("user", user_addr.as_str()), attr("liquidator", liquidator_addr.as_str()), - attr("collateral_amount_liquidated", expected_liquidated_amount.to_string()), - attr("debt_amount_repaid", expected_less_debt.to_string()), - attr("refund_amount", expected_refund_amount), + attr("collateral_denom", denom), + attr("collateral_amount", expected_liquidated_amount), + attr("collateral_amount_scaled", expected_liquidated_amount_scaled), + attr("debt_denom", denom), + attr("debt_amount", expected_less_debt), + attr("debt_amount_scaled", expected_rates.less_debt_scaled), ], ); assert_eq!(res.events, vec![th_build_interests_updated_event(denom, &expected_rates),],); - // check user still has deposited collateral asset and - // still has outstanding debt in debt asset - assert!(has_collateral_position(deps.as_ref(), &user_addr, denom)); - assert!(has_debt_position(deps.as_ref(), &user_addr, denom)); - - // TODO!!!!!!! - // liquidator receives maTokens, should have collateral bit set - // assert!(has_collateral_position(deps.as_ref(), &liquidator_addr, denom)); + // user's collateral scaled amount should have been correctly decreased + let collateral = + COLLATERALS.load(deps.as_ref().storage, (&user_addr, "the_asset")).unwrap(); + let expected_user_collateral_scaled = + initial_user_collateral_scaled - expected_liquidated_amount_scaled; + assert_eq!(collateral.amount_scaled, expected_user_collateral_scaled); + + // liquidator's collateral scaled amount should have been correctly increased + let collateral = + COLLATERALS.load(deps.as_ref().storage, (&liquidator_addr, "the_asset")).unwrap(); + assert_eq!(collateral.amount_scaled, expected_liquidated_amount_scaled); + + // rewards collector's collateral scaled amount **of the debt asset** should have been correctly increased + let collateral = COLLATERALS + .load( + deps.as_ref().storage, + (&Addr::unchecked(MarsContract::ProtocolRewardsCollector.to_string()), "the_asset"), + ) + .unwrap(); + assert_eq!(collateral.amount_scaled, expected_reward_amount_scaled); // check user's debt decreased by the appropriate amount let debt = DEBTS.load(&deps.storage, (&user_addr, denom)).unwrap(); - let expected_less_debt_scaled = expected_rates.less_debt_scaled; - let expected_user_debt_scaled = initial_user_debt_scaled - expected_less_debt_scaled; + let expected_user_debt_scaled = initial_user_debt_scaled - expected_rates.less_debt_scaled; assert_eq!(expected_user_debt_scaled, debt.amount_scaled); + // global collateral scaled amount **of the debt asset** should have been correctly increased + let expected_global_collateral_scaled = + initial_global_collateral_scaled + expected_reward_amount_scaled; + assert_eq!(asset_market_after.collateral_total_scaled, expected_global_collateral_scaled); + // check global debt decreased by the appropriate amount - let expected_global_debt_scaled = initial_global_debt_scaled - expected_less_debt_scaled; + let expected_global_debt_scaled = + initial_global_debt_scaled - expected_rates.less_debt_scaled; assert_eq!(expected_global_debt_scaled, asset_market_after.debt_total_scaled); } } @@ -1245,7 +970,6 @@ fn test_liquidation_health_factor_check() { let collateral_liquidation_bonus = Decimal::from_ratio(1u128, 10u128); let collateral_market = Market { - ma_token_address: Addr::unchecked("collateral"), max_loan_to_value: collateral_ltv, liquidation_threshold: collateral_liquidation_threshold, liquidation_bonus: collateral_liquidation_bonus, @@ -1255,7 +979,6 @@ fn test_liquidation_health_factor_check() { ..Default::default() }; let debt_market = Market { - ma_token_address: Addr::unchecked("debt"), max_loan_to_value: Decimal::from_ratio(6u128, 10u128), debt_total_scaled: Uint128::new(20_000_000) * SCALING_FACTOR, liquidity_index: Decimal::one(), @@ -1275,17 +998,14 @@ fn test_liquidation_health_factor_check() { // test health factor check let healthy_user_addr = Addr::unchecked("healthy_user"); - // Set user as having collateral and debt in respective markets - set_collateral(deps.as_mut(), &healthy_user_addr, "collateral", true); - // set initial collateral and debt balances for user - let collateral_address = Addr::unchecked("collateral"); let healthy_user_collateral_balance_scaled = Uint128::new(10_000_000) * SCALING_FACTOR; - - // Set the querier to return a certain collateral balance - deps.querier.set_cw20_balances( - collateral_address, - &[(healthy_user_addr.clone(), healthy_user_collateral_balance_scaled)], + set_collateral( + deps.as_mut(), + &healthy_user_addr, + "collateral", + healthy_user_collateral_balance_scaled, + true, ); let healthy_user_debt_amount_scaled = @@ -1329,15 +1049,12 @@ fn test_liquidate_if_collateral_disabled() { let mut deps = th_setup(&[]); let collateral_market_1 = Market { - ma_token_address: Addr::unchecked("collateral1"), ..Default::default() }; let collateral_market_2 = Market { - ma_token_address: Addr::unchecked("collateral2"), ..Default::default() }; let debt_market = Market { - ma_token_address: Addr::unchecked("debt"), ..Default::default() }; @@ -1348,8 +1065,8 @@ fn test_liquidate_if_collateral_disabled() { // Set user as having collateral and debt in respective markets let user_addr = Addr::unchecked("user"); - set_collateral(deps.as_mut(), &user_addr, "collateral1", true); - set_collateral(deps.as_mut(), &user_addr, "collateral2", false); + 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"); diff --git a/contracts/red-bank/tests/test_misc.rs b/contracts/red-bank/tests/test_misc.rs index a8fde2df..3d2e7917 100644 --- a/contracts/red-bank/tests/test_misc.rs +++ b/contracts/red-bank/tests/test_misc.rs @@ -13,7 +13,7 @@ use mars_red_bank::interest_rates::{ compute_scaled_amount, compute_underlying_amount, get_scaled_debt_amount, get_updated_liquidity_index, ScalingOperation, SCALING_FACTOR, }; -use mars_red_bank::state::{DEBTS, UNCOLLATERALIZED_LOAN_LIMITS}; +use mars_red_bank::state::{DEBTS, MARKETS, UNCOLLATERALIZED_LOAN_LIMITS}; use helpers::{ has_collateral_enabled, has_collateral_position, has_debt_position, set_collateral, set_debt, @@ -29,7 +29,6 @@ fn test_uncollateralized_loan_limits() { let mut deps = th_setup(&[coin(available_liquidity.into(), "somecoin")]); let mock_market = Market { - ma_token_address: Addr::unchecked("matoken"), borrow_index: Decimal::from_ratio(12u128, 10u128), liquidity_index: Decimal::from_ratio(8u128, 10u128), borrow_rate: Decimal::from_ratio(20u128, 100u128), @@ -112,6 +111,10 @@ fn test_uncollateralized_loan_limits() { }, ); + let market = MARKETS.load(deps.as_ref().storage, "somecoin").unwrap(); + let expected_borrow_amount_scaled = + get_scaled_debt_amount(initial_borrow_amount, &market, block_time).unwrap(); + assert_eq!( res.messages, vec![SubMsg::new(CosmosMsg::Bank(BankMsg::Send { @@ -124,10 +127,11 @@ fn test_uncollateralized_loan_limits() { res.attributes, vec![ attr("action", "outposts/red-bank/borrow"), - attr("denom", "somecoin"), - attr("user", "borrower"), + attr("sender", "borrower"), attr("recipient", "borrower"), - attr("amount", initial_borrow_amount.to_string()), + attr("denom", "somecoin"), + attr("amount", initial_borrow_amount), + attr("amount_scaled", expected_borrow_amount_scaled), ] ); assert_eq!(res.events, vec![th_build_interests_updated_event("somecoin", &expected_params)]); @@ -199,9 +203,7 @@ fn test_update_asset_collateral() { let user_addr = Addr::unchecked(String::from("user")); let denom_1 = "depositedcoin1"; - let ma_token_addr_1 = Addr::unchecked("matoken1"); let mock_market_1 = Market { - ma_token_address: ma_token_addr_1.clone(), liquidity_index: Decimal::one(), borrow_index: Decimal::one(), max_loan_to_value: Decimal::from_ratio(40u128, 100u128), @@ -209,9 +211,7 @@ fn test_update_asset_collateral() { ..Default::default() }; let denom_2 = "depositedcoin2"; - let ma_token_addr_2 = Addr::unchecked("matoken2"); let mock_market_2 = Market { - ma_token_address: ma_token_addr_2.clone(), liquidity_index: Decimal::from_ratio(1u128, 2u128), borrow_index: Decimal::one(), max_loan_to_value: Decimal::from_ratio(50u128, 100u128), @@ -219,9 +219,7 @@ fn test_update_asset_collateral() { ..Default::default() }; let denom_3 = "depositedcoin3"; - let ma_token_addr_3 = Addr::unchecked("matoken3"); let mock_market_3 = Market { - ma_token_address: ma_token_addr_3, liquidity_index: Decimal::one(), borrow_index: Decimal::from_ratio(2u128, 1u128), max_loan_to_value: Decimal::from_ratio(20u128, 100u128), @@ -246,13 +244,9 @@ fn test_update_asset_collateral() { { // Set second asset as collateral - set_collateral(deps.as_mut(), &user_addr, &market_2_initial.denom, true); + set_collateral(deps.as_mut(), &user_addr, &market_2_initial.denom, Uint128::new(123), true); - // Set the querier to return zero for the first asset - deps.querier - .set_cw20_balances(ma_token_addr_1.clone(), &[(user_addr.clone(), Uint128::zero())]); - - // Enable first market index which is currently disabled as collateral and ma-token balance is 0 + // Enable denom 1 as collateral in which the user currently doesn't have a position in let update_msg = ExecuteMsg::UpdateAssetCollateralStatus { denom: denom_1.to_string(), enable: true, @@ -271,10 +265,7 @@ fn test_update_asset_collateral() { assert!(!has_collateral_position(deps.as_ref(), &user_addr, &market_1_initial.denom)); // Set the querier to return balance more than zero for the first asset - deps.querier.set_cw20_balances( - ma_token_addr_1.clone(), - &[(user_addr.clone(), Uint128::new(100_000))], - ); + set_collateral(deps.as_mut(), &user_addr, denom_1, Uint128::new(100_000), false); // Enable first market index which is currently disabled as collateral and ma-token balance is more than 0 execute(deps.as_mut(), env.clone(), info.clone(), update_msg).unwrap(); @@ -293,16 +284,22 @@ fn test_update_asset_collateral() { { // Initialize user with market_1 and market_2 as collaterals // User borrows market_3, which will be set up later in the test - set_collateral(deps.as_mut(), &user_addr, &market_1_initial.denom, true); - set_collateral(deps.as_mut(), &user_addr, &market_2_initial.denom, true); - - // Set the querier to return collateral balances (ma_token_1 and ma_token_2) let ma_token_1_balance_scaled = Uint128::new(150_000) * SCALING_FACTOR; - deps.querier - .set_cw20_balances(ma_token_addr_1, &[(user_addr.clone(), ma_token_1_balance_scaled)]); + set_collateral( + deps.as_mut(), + &user_addr, + &market_1_initial.denom, + ma_token_1_balance_scaled, + true, + ); let ma_token_2_balance_scaled = Uint128::new(220_000) * SCALING_FACTOR; - deps.querier - .set_cw20_balances(ma_token_addr_2, &[(user_addr.clone(), ma_token_2_balance_scaled)]); + set_collateral( + deps.as_mut(), + &user_addr, + &market_2_initial.denom, + ma_token_2_balance_scaled, + true, + ); // Calculate maximum debt for the user to have valid health factor let token_1_weighted_lt_in_base_asset = compute_underlying_amount( diff --git a/contracts/red-bank/tests/test_query.rs b/contracts/red-bank/tests/test_query.rs index 244a8767..dd354bd6 100644 --- a/contracts/red-bank/tests/test_query.rs +++ b/contracts/red-bank/tests/test_query.rs @@ -1,9 +1,11 @@ +use cosmwasm_std::testing::mock_env; use cosmwasm_std::{Addr, Decimal, Uint128}; -use mars_outpost::red_bank::{Debt, Market, UserDebtResponse}; -use mars_testing::{mock_env, MockEnvParams}; +use mars_outpost::red_bank::{Debt, Market, UserCollateralResponse, UserDebtResponse}; -use mars_red_bank::interest_rates::{get_scaled_debt_amount, get_underlying_debt_amount}; +use mars_red_bank::interest_rates::{ + get_scaled_debt_amount, get_underlying_debt_amount, SCALING_FACTOR, +}; use mars_red_bank::query::{query_user_collaterals, query_user_debt, query_user_debts}; use mars_red_bank::state::DEBTS; @@ -18,7 +20,7 @@ fn test_query_collateral() { let user_addr = Addr::unchecked("user"); // Setup first market containing a CW20 asset - let market_1_initial = th_init_market( + let market_1 = th_init_market( deps.as_mut(), "uosmo", &Market { @@ -27,7 +29,7 @@ fn test_query_collateral() { ); // Setup second market containing a native asset - let market_2_initial = th_init_market( + let market_2 = th_init_market( deps.as_mut(), "uusd", &Market { @@ -35,25 +37,50 @@ fn test_query_collateral() { }, ); + let amount_1 = Uint128::new(12345); + let amount_2 = Uint128::new(54321); + + let env = mock_env(); + // Create and enable a collateral position for the 2nd asset - set_collateral(deps.as_mut(), &user_addr, &market_2_initial.denom, true); + set_collateral(deps.as_mut(), &user_addr, &market_2.denom, amount_2 * SCALING_FACTOR, true); // Assert markets correctly return collateral status - let collaterals = query_user_collaterals(deps.as_ref(), user_addr.clone(), None, None).unwrap(); - assert_eq!(collaterals.len(), 1); - assert_eq!(collaterals[0].denom, String::from("uusd")); - assert!(collaterals[0].enabled); + let collaterals = + query_user_collaterals(deps.as_ref(), &env.block, user_addr.clone(), None, None).unwrap(); + assert_eq!( + collaterals, + vec![UserCollateralResponse { + denom: market_2.denom.clone(), + amount_scaled: amount_2 * SCALING_FACTOR, + amount: amount_2, + enabled: true, + }] + ); // Create a collateral position for the 1st asset, but not enabled - set_collateral(deps.as_mut(), &user_addr, &market_1_initial.denom, false); + set_collateral(deps.as_mut(), &user_addr, &market_1.denom, amount_1 * SCALING_FACTOR, false); // Assert markets correctly return collateral status - let collaterals = query_user_collaterals(deps.as_ref(), user_addr, None, None).unwrap(); - assert_eq!(collaterals.len(), 2); - assert_eq!(collaterals[0].denom, String::from("uosmo")); - assert!(!collaterals[0].enabled); - assert_eq!(collaterals[1].denom, String::from("uusd")); - assert!(collaterals[1].enabled); + let collaterals = + query_user_collaterals(deps.as_ref(), &env.block, user_addr, None, None).unwrap(); + assert_eq!( + collaterals, + vec![ + UserCollateralResponse { + denom: market_1.denom, + amount_scaled: amount_1 * SCALING_FACTOR, + amount: amount_1, + enabled: false, + }, + UserCollateralResponse { + denom: market_2.denom, + amount_scaled: amount_2 * SCALING_FACTOR, + amount: amount_2, + enabled: true, + } + ] + ); } #[test] @@ -91,7 +118,7 @@ fn test_query_user_debt() { }, ); - let env = mock_env(MockEnvParams::default()); + let env = mock_env(); // Save debt for market 1 let debt_amount_1 = Uint128::new(1234000u128); @@ -133,6 +160,7 @@ fn test_query_user_debt() { denom: "coin_1".to_string(), amount_scaled: debt_amount_scaled_1, amount: debt_amount_at_query_1, + uncollateralized: false, } ); assert_eq!( @@ -140,7 +168,8 @@ fn test_query_user_debt() { UserDebtResponse { denom: "coin_3".to_string(), amount_scaled: debt_amount_scaled_3, - amount: debt_amount_at_query_3 + amount: debt_amount_at_query_3, + uncollateralized: false, } ); } @@ -171,7 +200,7 @@ fn test_query_user_asset_debt() { }, ); - let env = mock_env(MockEnvParams::default()); + let env = mock_env(); // Save debt for market 1 let debt_amount_1 = Uint128::new(1234567u128); @@ -199,7 +228,8 @@ fn test_query_user_asset_debt() { UserDebtResponse { denom: "coin_1".to_string(), amount_scaled: debt_amount_scaled_1, - amount: debt_amount_at_query_1 + amount: debt_amount_at_query_1, + uncollateralized: false, } ); } @@ -213,7 +243,8 @@ fn test_query_user_asset_debt() { UserDebtResponse { denom: "coin_2".to_string(), amount_scaled: Uint128::zero(), - amount: Uint128::zero() + amount: Uint128::zero(), + uncollateralized: false, } ); } diff --git a/contracts/red-bank/tests/test_withdraw.rs b/contracts/red-bank/tests/test_withdraw.rs index 99f0f090..d856b5a5 100644 --- a/contracts/red-bank/tests/test_withdraw.rs +++ b/contracts/red-bank/tests/test_withdraw.rs @@ -1,82 +1,140 @@ -use cosmwasm_std::testing::mock_info; +use cosmwasm_std::testing::{mock_env, mock_info, MockApi, MockStorage}; use cosmwasm_std::{ - attr, coin, coins, to_binary, Addr, BankMsg, CosmosMsg, Decimal, SubMsg, Uint128, WasmMsg, + attr, coin, coins, Addr, BankMsg, CosmosMsg, Decimal, OwnedDeps, SubMsg, Uint128, }; -use mars_outpost::red_bank::{Debt, ExecuteMsg, Market}; -use mars_outpost::{ma_token, math}; -use mars_testing::{mock_env, mock_env_at_block_time, MockEnvParams}; - +use mars_outpost::address_provider::MarsContract; +use mars_outpost::math; +use mars_outpost::red_bank::{Collateral, Debt, ExecuteMsg, Market}; use mars_red_bank::contract::execute; use mars_red_bank::error::ContractError; use mars_red_bank::interest_rates::{ compute_scaled_amount, compute_underlying_amount, get_scaled_liquidity_amount, get_updated_borrow_index, get_updated_liquidity_index, ScalingOperation, SCALING_FACTOR, }; -use mars_red_bank::state::{DEBTS, MARKETS, MARKET_DENOMS_BY_MA_TOKEN}; +use mars_red_bank::state::{COLLATERALS, DEBTS, MARKETS}; +use mars_testing::{mock_env_at_block_time, MarsMockQuerier}; use helpers::{ has_collateral_position, set_collateral, th_build_interests_updated_event, - th_get_expected_indices_and_rates, th_init_market, th_setup, TestUtilizationDeltaInfo, + th_get_expected_indices_and_rates, th_setup, TestUtilizationDeltaInfo, }; mod helpers; -#[test] -fn test_withdraw_native() { - // Withdraw native token - let initial_available_liquidity = Uint128::from(12000000u128); - let mut deps = th_setup(&[coin(initial_available_liquidity.into(), "somecoin")]); - - let initial_liquidity_index = Decimal::from_ratio(15u128, 10u128); - let mock_market = Market { - denom: "somecoin".to_string(), - ma_token_address: Addr::unchecked("matoken"), - liquidity_index: initial_liquidity_index, +struct TestSuite { + deps: OwnedDeps, + denom: &'static str, + withdrawer_addr: Addr, + initial_market: Market, + initial_liquidity: Uint128, +} + +fn setup_test() -> TestSuite { + let denom = "uosmo"; + let initial_liquidity = Uint128::new(12_000_000); + + let mut deps = th_setup(&[coin(initial_liquidity.u128(), denom)]); + + let market = Market { + denom: denom.to_string(), + reserve_factor: Decimal::from_ratio(1u128, 10u128), borrow_index: Decimal::from_ratio(2u128, 1u128), + liquidity_index: Decimal::from_ratio(15u128, 10u128), borrow_rate: Decimal::from_ratio(20u128, 100u128), liquidity_rate: Decimal::from_ratio(10u128, 100u128), - reserve_factor: Decimal::from_ratio(1u128, 10u128), - - debt_total_scaled: Uint128::new(10_000_000) * SCALING_FACTOR, indexes_last_updated: 10000000, + collateral_total_scaled: Uint128::new(2_000_000) * SCALING_FACTOR, + debt_total_scaled: Uint128::new(10_000_000) * SCALING_FACTOR, ..Default::default() }; - let withdraw_amount = Uint128::from(20000u128); - let seconds_elapsed = 2000u64; - let initial_deposit_amount_scaled = Uint128::new(2_000_000) * SCALING_FACTOR; - deps.querier.set_cw20_balances( - Addr::unchecked("matoken"), - &[(Addr::unchecked("withdrawer"), initial_deposit_amount_scaled)], - ); + MARKETS.save(deps.as_mut().storage, denom, &market).unwrap(); - let market_initial = th_init_market(deps.as_mut(), "somecoin", &mock_market); - MARKET_DENOMS_BY_MA_TOKEN - .save(deps.as_mut().storage, &Addr::unchecked("matoken"), &"somecoin".to_string()) - .unwrap(); + TestSuite { + deps, + denom, + withdrawer_addr: Addr::unchecked("larry"), + initial_market: market, + initial_liquidity, + } +} +#[test] +fn withdrawing_more_than_balance() { + let TestSuite { + mut deps, + denom, + withdrawer_addr, + .. + } = setup_test(); + + // give withdrawer a small collateral position + set_collateral(deps.as_mut(), &withdrawer_addr, denom, Uint128::new(200), false); + + let err = execute( + deps.as_mut(), + mock_env(), + mock_info(withdrawer_addr.as_str(), &[]), + ExecuteMsg::Withdraw { + denom: denom.to_string(), + amount: Some(Uint128::from(2000u128)), + recipient: None, + }, + ) + .unwrap_err(); + assert_eq!( + err, + ContractError::InvalidWithdrawAmount { + denom: denom.to_string() + } + ); +} + +#[test] +fn withdrawing_partially() { + let TestSuite { + mut deps, + denom, + withdrawer_addr, + initial_market, + initial_liquidity, + } = setup_test(); + + let block_time = initial_market.indexes_last_updated + 2000; + let withdraw_amount = Uint128::new(20_000); + + // create a collateral position for the user // for this test, we assume the user has NOT enabled the asset as collateral // the health factor check should have been skipped (no need to set mock oracle price) - let withdrawer_addr = Addr::unchecked("withdrawer"); - set_collateral(deps.as_mut(), &withdrawer_addr, &mock_market.denom, false); - - let msg = ExecuteMsg::Withdraw { - denom: "somecoin".to_string(), - amount: Some(withdraw_amount), - recipient: None, - }; + let initial_deposit_amount_scaled = initial_market.collateral_total_scaled; + set_collateral( + deps.as_mut(), + &withdrawer_addr, + &initial_market.denom, + initial_deposit_amount_scaled, + false, + ); - let env = mock_env_at_block_time(mock_market.indexes_last_updated + seconds_elapsed); - let info = mock_info("withdrawer", &[]); - let res = execute(deps.as_mut(), env, info, msg).unwrap(); + let res = execute( + deps.as_mut(), + mock_env_at_block_time(block_time), + mock_info(withdrawer_addr.as_str(), &[]), + ExecuteMsg::Withdraw { + denom: denom.to_string(), + amount: Some(withdraw_amount), + recipient: None, + }, + ) + .unwrap(); - let market = MARKETS.load(&deps.storage, "somecoin").unwrap(); + let market = MARKETS.load(deps.as_ref().storage, denom).unwrap(); + // compute expected market parameters let expected_params = th_get_expected_indices_and_rates( - &market_initial, - mock_market.indexes_last_updated + seconds_elapsed, - initial_available_liquidity, + &initial_market, + block_time, + initial_liquidity, TestUtilizationDeltaInfo { less_liquidity: withdraw_amount, ..Default::default() @@ -91,480 +149,454 @@ fn test_withdraw_native() { .unwrap(); let expected_withdraw_amount_remaining = expected_deposit_balance - withdraw_amount; + let expected_withdraw_amount_scaled_remaining = compute_scaled_amount( expected_withdraw_amount_remaining, expected_params.liquidity_index, ScalingOperation::Truncate, ) .unwrap(); + let expected_burn_amount = initial_deposit_amount_scaled - expected_withdraw_amount_scaled_remaining; + let expected_rewards_amount_scaled = compute_scaled_amount( + expected_params.protocol_rewards_to_distribute, + market.liquidity_index, + ScalingOperation::Truncate, + ) + .unwrap(); + + let expected_total_collateral_amount_scaled = initial_market.collateral_total_scaled + - expected_burn_amount + + expected_rewards_amount_scaled; + assert_eq!( res.messages, - vec![ - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: "matoken".to_string(), - msg: to_binary(&ma_token::msg::ExecuteMsg::Mint { - recipient: "protocol_rewards_collector".to_string(), - amount: compute_scaled_amount( - expected_params.protocol_rewards_to_distribute, - market.liquidity_index, - ScalingOperation::Truncate - ) - .unwrap(), - }) - .unwrap(), - funds: vec![] - })), - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: "matoken".to_string(), - msg: to_binary(&ma_token::msg::ExecuteMsg::Burn { - user: withdrawer_addr.to_string(), - amount: expected_burn_amount, - }) - .unwrap(), - funds: vec![] - })), - SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: withdrawer_addr.to_string(), - amount: coins(withdraw_amount.u128(), "somecoin") - })), - ] + vec![SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: withdrawer_addr.to_string(), + amount: coins(withdraw_amount.u128(), denom) + })),] ); assert_eq!( res.attributes, vec![ attr("action", "outposts/red-bank/withdraw"), - attr("denom", "somecoin"), - attr("user", "withdrawer"), - attr("recipient", "withdrawer"), - attr("burn_amount", expected_burn_amount.to_string()), - attr("withdraw_amount", withdraw_amount.to_string()), + attr("sender", &withdrawer_addr), + attr("recipient", &withdrawer_addr), + attr("denom", denom), + attr("amount", withdraw_amount), + attr("amount_scaled", expected_burn_amount), ] ); - assert_eq!(res.events, vec![th_build_interests_updated_event("somecoin", &expected_params)]); + assert_eq!(res.events, vec![th_build_interests_updated_event(denom, &expected_params)]); + // market parameters should have been updated + assert_eq!(market.borrow_index, expected_params.borrow_index); + assert_eq!(market.liquidity_index, expected_params.liquidity_index); assert_eq!(market.borrow_rate, expected_params.borrow_rate); assert_eq!(market.liquidity_rate, expected_params.liquidity_rate); - assert_eq!(market.liquidity_index, expected_params.liquidity_index); - assert_eq!(market.borrow_index, expected_params.borrow_index); + + // the market's total collateral scaled amount should have been decreased + assert_eq!(market.collateral_total_scaled, expected_total_collateral_amount_scaled); + + // the user's collateral scaled amount should have been decreased + let collateral = COLLATERALS.load(deps.as_ref().storage, (&withdrawer_addr, denom)).unwrap(); + assert_eq!(collateral.amount_scaled, expected_withdraw_amount_scaled_remaining); + + // the reward collector's collateral scaled amount should have been increased + let rewards_addr = Addr::unchecked(MarsContract::ProtocolRewardsCollector.to_string()); + let collateral = COLLATERALS.load(deps.as_ref().storage, (&rewards_addr, denom)).unwrap(); + assert_eq!(collateral.amount_scaled, expected_rewards_amount_scaled); } #[test] -fn test_withdraw_and_send_funds_to_another_user() { - // Withdraw cw20 token - let mut deps = th_setup(&[]); - let denom = "somecoin"; - let initial_available_liquidity = Uint128::from(12000000u128); +fn withdrawing_completely() { + let TestSuite { + mut deps, + denom, + withdrawer_addr, + initial_market, + initial_liquidity, + } = setup_test(); + + let block_time = initial_market.indexes_last_updated + 2000; + + // create a collateral position for the withdrawer + let withdrawer_balance_scaled = Uint128::new(123_456) * SCALING_FACTOR; + set_collateral(deps.as_mut(), &withdrawer_addr, denom, withdrawer_balance_scaled, true); + + let res = execute( + deps.as_mut(), + mock_env_at_block_time(block_time), + mock_info(withdrawer_addr.as_str(), &[]), + ExecuteMsg::Withdraw { + denom: denom.to_string(), + amount: None, + recipient: None, + }, + ) + .unwrap(); - let ma_token_addr = Addr::unchecked("matoken"); + let market = MARKETS.load(&deps.storage, denom).unwrap(); - let withdrawer_addr = Addr::unchecked("withdrawer"); - let another_user_addr = Addr::unchecked("another_user"); + let withdrawer_balance = compute_underlying_amount( + withdrawer_balance_scaled, + get_updated_liquidity_index(&initial_market, block_time).unwrap(), + ScalingOperation::Truncate, + ) + .unwrap(); - deps.querier.set_contract_balances(&coins(initial_available_liquidity.u128(), denom)); - let ma_token_balance_scaled = Uint128::new(2_000_000) * SCALING_FACTOR; - deps.querier.set_cw20_balances( - ma_token_addr.clone(), - &[(withdrawer_addr.clone(), ma_token_balance_scaled)], + let expected_params = th_get_expected_indices_and_rates( + &initial_market, + block_time, + initial_liquidity, + TestUtilizationDeltaInfo { + less_liquidity: withdrawer_balance, + ..Default::default() + }, ); - let mock_market = Market { - ma_token_address: Addr::unchecked("matoken"), - liquidity_index: Decimal::one(), - borrow_index: Decimal::one(), - reserve_factor: Decimal::zero(), - ..Default::default() - }; - - let market_initial = th_init_market(deps.as_mut(), denom, &mock_market); - MARKET_DENOMS_BY_MA_TOKEN - .save(deps.as_mut().storage, &ma_token_addr, &denom.to_string()) - .unwrap(); + assert_eq!( + res.messages, + vec![SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: withdrawer_addr.to_string(), + amount: coins(withdrawer_balance.u128(), denom) + })),] + ); + assert_eq!( + res.attributes, + vec![ + attr("action", "outposts/red-bank/withdraw"), + attr("sender", &withdrawer_addr), + attr("recipient", &withdrawer_addr), + attr("denom", denom), + attr("amount", withdrawer_balance.to_string()), + attr("amount_scaled", withdrawer_balance_scaled.to_string()), + ] + ); + assert_eq!(res.events, vec![th_build_interests_updated_event(denom, &expected_params)]); - // assume the user has a collateral position but not enabled - set_collateral(deps.as_mut(), &withdrawer_addr, denom, false); + assert_eq!(market.borrow_index, expected_params.borrow_index); + assert_eq!(market.liquidity_index, expected_params.liquidity_index); + assert_eq!(market.borrow_rate, expected_params.borrow_rate); + assert_eq!(market.liquidity_rate, expected_params.liquidity_rate); - let msg = ExecuteMsg::Withdraw { - denom: denom.to_string(), - amount: None, - recipient: Some(another_user_addr.to_string()), - }; + // withdrawer's collateral position should have been deleted after full withdraw + assert!(!has_collateral_position(deps.as_ref(), &withdrawer_addr, denom)); +} - let env = mock_env(MockEnvParams::default()); - let info = mock_info("withdrawer", &[]); - let res = execute(deps.as_mut(), env, info, msg).unwrap(); +#[test] +fn withdrawing_to_another_user() { + let TestSuite { + mut deps, + denom, + withdrawer_addr, + initial_market, + .. + } = setup_test(); + + let block_time = initial_market.indexes_last_updated + 2000; + let recipient_addr = Addr::unchecked("jake"); + + // create a collateral position for the withdrawer + let withdrawer_balance_scaled = Uint128::new(123_456) * SCALING_FACTOR; + set_collateral(deps.as_mut(), &withdrawer_addr, denom, withdrawer_balance_scaled, true); + + let res = execute( + deps.as_mut(), + mock_env_at_block_time(block_time), + mock_info(withdrawer_addr.as_str(), &[]), + ExecuteMsg::Withdraw { + denom: denom.to_string(), + amount: None, + recipient: Some(recipient_addr.to_string()), + }, + ) + .unwrap(); - // User should have unset bit for collateral after full withdraw - assert!(!has_collateral_position(deps.as_ref(), &withdrawer_addr, &market_initial.denom)); + let market = MARKETS.load(deps.as_ref().storage, denom).unwrap(); let withdraw_amount = compute_underlying_amount( - ma_token_balance_scaled, - market_initial.liquidity_index, + withdrawer_balance_scaled, + market.liquidity_index, ScalingOperation::Truncate, ) .unwrap(); - // Check if maToken is received by `another_user` + // check if the withdrew funds are properly sent to the designated recipient assert_eq!( res.messages, - vec![ - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: ma_token_addr.to_string(), - msg: to_binary(&ma_token::msg::ExecuteMsg::Burn { - user: withdrawer_addr.to_string(), - amount: ma_token_balance_scaled, - }) - .unwrap(), - funds: vec![] - })), - SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: another_user_addr.to_string(), - amount: coins(withdraw_amount.u128(), denom) - })) - ] + vec![SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: recipient_addr.to_string(), + amount: coins(withdraw_amount.u128(), denom) + }))] ); assert_eq!( res.attributes, vec![ attr("action", "outposts/red-bank/withdraw"), + attr("sender", &withdrawer_addr), + attr("recipient", &recipient_addr), attr("denom", denom.to_string()), - attr("user", withdrawer_addr), - attr("recipient", another_user_addr), - attr("burn_amount", ma_token_balance_scaled.to_string()), - attr("withdraw_amount", withdraw_amount.to_string()), + attr("amount", withdraw_amount.to_string()), + attr("amount_scaled", withdrawer_balance_scaled.to_string()), ] ); -} - -#[test] -fn test_withdraw_cannot_exceed_balance() { - let mut deps = th_setup(&[]); - let env = mock_env(MockEnvParams::default()); - - let mock_market = Market { - ma_token_address: Addr::unchecked("matoken"), - liquidity_index: Decimal::from_ratio(15u128, 10u128), - ..Default::default() - }; - - deps.querier.set_cw20_balances( - Addr::unchecked("matoken"), - &[(Addr::unchecked("withdrawer"), Uint128::new(200u128))], - ); - - th_init_market(deps.as_mut(), "somecoin", &mock_market); - let msg = ExecuteMsg::Withdraw { - denom: "somecoin".to_string(), - amount: Some(Uint128::from(2000u128)), - recipient: None, - }; + // withdrawer's collateral position should have been deleted after full withdraw + assert!(!has_collateral_position(deps.as_ref(), &withdrawer_addr, denom)); +} - let info = mock_info("withdrawer", &[]); - let error_res = execute(deps.as_mut(), env, info, msg).unwrap_err(); - assert_eq!( - error_res, - ContractError::InvalidWithdrawAmount { - denom: "somecoin".to_string() - } - ); +struct HealthCheckTestSuite { + deps: OwnedDeps, + denoms: [&'static str; 3], + markets: [Market; 3], + prices: [Decimal; 3], + collaterals: [Collateral; 3], + debts: [Debt; 3], + withdrawer_addr: Addr, } -#[test] -fn test_withdraw_if_health_factor_not_met() { - let initial_available_liquidity = Uint128::from(10000000u128); - let mut deps = th_setup(&[coin(initial_available_liquidity.into(), "token3")]); +fn setup_health_check_test() -> HealthCheckTestSuite { + let denoms = ["uatom", "uosmo", "umars"]; + let initial_liquidity = Uint128::from(10000000u128); + + let mut deps = th_setup(&[coin(initial_liquidity.into(), denoms[2])]); let withdrawer_addr = Addr::unchecked("withdrawer"); - // Initialize markets - let ma_token_1_addr = Addr::unchecked("matoken1"); - let market_1 = Market { - ma_token_address: ma_token_1_addr.clone(), - liquidity_index: Decimal::one(), - borrow_index: Decimal::one(), - max_loan_to_value: Decimal::from_ratio(40u128, 100u128), - liquidation_threshold: Decimal::from_ratio(60u128, 100u128), - ..Default::default() - }; - let ma_token_2_addr = Addr::unchecked("matoken2"); - let market_2 = Market { - ma_token_address: ma_token_2_addr, - liquidity_index: Decimal::one(), - borrow_index: Decimal::one(), - max_loan_to_value: Decimal::from_ratio(50u128, 100u128), - liquidation_threshold: Decimal::from_ratio(80u128, 100u128), - ..Default::default() - }; - let ma_token_3_addr = Addr::unchecked("matoken3"); - let market_3 = Market { - ma_token_address: ma_token_3_addr.clone(), - liquidity_index: Decimal::one(), - borrow_index: Decimal::one(), - max_loan_to_value: Decimal::from_ratio(20u128, 100u128), - liquidation_threshold: Decimal::from_ratio(40u128, 100u128), - ..Default::default() - }; - let market_1_initial = th_init_market(deps.as_mut(), "token1", &market_1); - let market_2_initial = th_init_market(deps.as_mut(), "token2", &market_2); - let market_3_initial = th_init_market(deps.as_mut(), "token3", &market_3); - - // Initialize user with market_1 and market_3 as collaterals - // User borrows market_2; the debt position will be configured later in the test - set_collateral(deps.as_mut(), &withdrawer_addr, &market_1_initial.denom, true); - set_collateral(deps.as_mut(), &withdrawer_addr, &market_3_initial.denom, true); - - // Set the querier to return collateral balances (ma_token_1 and ma_token_3) - let ma_token_1_balance_scaled = Uint128::new(100_000) * SCALING_FACTOR; - deps.querier.set_cw20_balances( - ma_token_1_addr, - &[(withdrawer_addr.clone(), ma_token_1_balance_scaled)], - ); - let ma_token_3_balance_scaled = Uint128::new(600_000) * SCALING_FACTOR; - deps.querier.set_cw20_balances( - ma_token_3_addr, - &[(withdrawer_addr.clone(), ma_token_3_balance_scaled)], - ); + let markets = [ + Market { + liquidity_index: Decimal::one(), + borrow_index: Decimal::one(), + max_loan_to_value: Decimal::from_ratio(40u128, 100u128), + liquidation_threshold: Decimal::from_ratio(60u128, 100u128), + collateral_total_scaled: Uint128::new(100_000) * SCALING_FACTOR, + ..Default::default() + }, + Market { + liquidity_index: Decimal::one(), + borrow_index: Decimal::one(), + max_loan_to_value: Decimal::from_ratio(50u128, 100u128), + liquidation_threshold: Decimal::from_ratio(80u128, 100u128), + collateral_total_scaled: Uint128::new(100_000) * SCALING_FACTOR, + ..Default::default() + }, + Market { + liquidity_index: Decimal::one(), + borrow_index: Decimal::one(), + max_loan_to_value: Decimal::from_ratio(20u128, 100u128), + liquidation_threshold: Decimal::from_ratio(40u128, 100u128), + collateral_total_scaled: Uint128::new(100_000) * SCALING_FACTOR, + ..Default::default() + }, + ]; + + let prices = [ + Decimal::from_ratio(3u128, 1u128), + Decimal::from_ratio(2u128, 1u128), + Decimal::from_ratio(1u128, 1u128), + ]; + + let collaterals = [ + Collateral { + amount_scaled: Uint128::new(100_000) * SCALING_FACTOR, + enabled: true, + }, + Collateral { + amount_scaled: Uint128::zero(), + enabled: false, + }, + Collateral { + amount_scaled: Uint128::new(600_000) * SCALING_FACTOR, + enabled: true, + }, + ]; - // Set user to have positive debt amount in debt asset - // Uncollateralized debt shouldn't count for health factor - let token_2_debt_scaled = Uint128::new(200_000) * SCALING_FACTOR; - let debt = Debt { - amount_scaled: token_2_debt_scaled, - uncollateralized: false, - }; - let uncollateralized_debt = Debt { - amount_scaled: Uint128::new(200_000) * SCALING_FACTOR, - uncollateralized: true, - }; - DEBTS.save(deps.as_mut().storage, (&withdrawer_addr, "token2"), &debt).unwrap(); - DEBTS - .save(deps.as_mut().storage, (&withdrawer_addr, "token3"), &uncollateralized_debt) - .unwrap(); + let debts = [ + Debt { + amount_scaled: Uint128::zero(), + uncollateralized: false, + }, + Debt { + amount_scaled: Uint128::new(200_000) * SCALING_FACTOR, + uncollateralized: false, + }, + Debt { + amount_scaled: Uint128::new(200_000) * SCALING_FACTOR, + uncollateralized: true, + }, + ]; - // Set the querier to return native exchange rates - let token_1_exchange_rate = Decimal::from_ratio(3u128, 1u128); - let token_2_exchange_rate = Decimal::from_ratio(2u128, 1u128); - let token_3_exchange_rate = Decimal::from_ratio(1u128, 1u128); - - deps.querier.set_oracle_price("token1", token_1_exchange_rate); - deps.querier.set_oracle_price("token2", token_2_exchange_rate); - deps.querier.set_oracle_price("token3", token_3_exchange_rate); - - let env = mock_env(MockEnvParams::default()); - let info = mock_info("withdrawer", &[]); - - // Calculate how much to withdraw to have health factor equal to one - let how_much_to_withdraw = { - let token_1_weighted_lt_in_base_asset = compute_underlying_amount( - ma_token_1_balance_scaled, - get_updated_liquidity_index(&market_1_initial, env.block.time.seconds()).unwrap(), - ScalingOperation::Truncate, - ) - .unwrap() - * market_1_initial.liquidation_threshold - * token_1_exchange_rate; - let token_3_weighted_lt_in_base_asset = compute_underlying_amount( - ma_token_3_balance_scaled, - get_updated_liquidity_index(&market_3_initial, env.block.time.seconds()).unwrap(), - ScalingOperation::Truncate, - ) - .unwrap() - * market_3_initial.liquidation_threshold - * token_3_exchange_rate; - let weighted_liquidation_threshold_in_base_asset = - token_1_weighted_lt_in_base_asset + token_3_weighted_lt_in_base_asset; - - let total_collateralized_debt_in_base_asset = compute_underlying_amount( - token_2_debt_scaled, - get_updated_borrow_index(&market_2_initial, env.block.time.seconds()).unwrap(), - ScalingOperation::Ceil, - ) - .unwrap() - * token_2_exchange_rate; - - // How much to withdraw in base asset to have health factor equal to one - let how_much_to_withdraw_in_base_asset = math::divide_uint128_by_decimal( - weighted_liquidation_threshold_in_base_asset - total_collateralized_debt_in_base_asset, - market_3_initial.liquidation_threshold, - ) + denoms + .iter() + .zip(markets.iter()) + .try_for_each(|(denom, market)| MARKETS.save(deps.as_mut().storage, denom, market)) .unwrap(); - math::divide_uint128_by_decimal(how_much_to_withdraw_in_base_asset, token_3_exchange_rate) - .unwrap() - }; - // Withdraw token3 with failure - // The withdraw amount needs to be a little bit greater to have health factor less than one - { - let withdraw_amount = how_much_to_withdraw + Uint128::from(10u128); - let msg = ExecuteMsg::Withdraw { - denom: "token3".to_string(), - amount: Some(withdraw_amount), - recipient: None, - }; - let error_res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap_err(); - assert_eq!(error_res, ContractError::InvalidHealthFactorAfterWithdraw {}); - } + denoms + .iter() + .zip(prices.iter()) + .for_each(|(denom, price)| deps.querier.set_oracle_price(denom, *price)); - // Withdraw token3 with success - // The withdraw amount needs to be a little bit smaller to have health factor greater than one - { - let withdraw_amount = how_much_to_withdraw - Uint128::from(10u128); - let msg = ExecuteMsg::Withdraw { - denom: "token3".to_string(), - amount: Some(withdraw_amount), - recipient: None, - }; - let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); - - let withdraw_amount_scaled = get_scaled_liquidity_amount( - withdraw_amount, - &market_3_initial, - env.block.time.seconds(), - ) - .unwrap(); + denoms.iter().zip(collaterals.iter()).for_each(|(denom, collateral)| { + if !collateral.amount_scaled.is_zero() { + COLLATERALS.save(deps.as_mut().storage, (&withdrawer_addr, denom), collateral).unwrap(); + } + }); - assert_eq!( - res.messages, - vec![ - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: "matoken3".to_string(), - msg: to_binary(&ma_token::msg::ExecuteMsg::Burn { - user: withdrawer_addr.to_string(), - amount: withdraw_amount_scaled, - }) - .unwrap(), - funds: vec![] - })), - SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: withdrawer_addr.to_string(), - amount: coins(withdraw_amount.u128(), "token3") - })), - ] - ); + denoms.iter().zip(debts.iter()).for_each(|(denom, debt)| { + if !debt.amount_scaled.is_zero() { + DEBTS.save(deps.as_mut().storage, (&withdrawer_addr, denom), debt).unwrap(); + } + }); + + HealthCheckTestSuite { + deps, + denoms, + markets, + prices, + collaterals, + debts, + withdrawer_addr, } } -#[test] -fn test_withdraw_total_balance() { - // Withdraw native token - let initial_available_liquidity = Uint128::from(12000000u128); - let mut deps = th_setup(&[coin(initial_available_liquidity.into(), "somecoin")]); - - let initial_liquidity_index = Decimal::from_ratio(15u128, 10u128); - let mock_market = Market { - ma_token_address: Addr::unchecked("matoken"), - liquidity_index: initial_liquidity_index, - borrow_index: Decimal::from_ratio(2u128, 1u128), - borrow_rate: Decimal::from_ratio(20u128, 100u128), - liquidity_rate: Decimal::from_ratio(10u128, 100u128), - reserve_factor: Decimal::from_ratio(1u128, 10u128), - debt_total_scaled: Uint128::new(10_000_000) * SCALING_FACTOR, - indexes_last_updated: 10000000, - ..Default::default() - }; - let withdrawer_balance_scaled = Uint128::new(123_456) * SCALING_FACTOR; - let seconds_elapsed = 2000u64; - - deps.querier.set_cw20_balances( - Addr::unchecked("matoken"), - &[(Addr::unchecked("withdrawer"), withdrawer_balance_scaled)], - ); - - let market_initial = th_init_market(deps.as_mut(), "somecoin", &mock_market); - MARKET_DENOMS_BY_MA_TOKEN - .save(deps.as_mut().storage, &Addr::unchecked("matoken"), &"somecoin".to_string()) - .unwrap(); - - // Mark the market as collateral for the user - let withdrawer_addr = Addr::unchecked("withdrawer"); - set_collateral(deps.as_mut(), &withdrawer_addr, &market_initial.denom, true); +/// Calculate how much to withdraw to have health factor equal to one +fn how_much_to_withdraw(suite: &HealthCheckTestSuite, block_time: u64) -> Uint128 { + let HealthCheckTestSuite { + markets, + prices, + collaterals, + debts, + .. + } = suite; + + let token_1_weighted_lt_in_base_asset = compute_underlying_amount( + collaterals[0].amount_scaled, + get_updated_liquidity_index(&markets[0], block_time).unwrap(), + ScalingOperation::Truncate, + ) + .unwrap() + * markets[0].liquidation_threshold + * prices[0]; - let msg = ExecuteMsg::Withdraw { - denom: "somecoin".to_string(), - amount: None, - recipient: None, - }; + let token_3_weighted_lt_in_base_asset = compute_underlying_amount( + collaterals[2].amount_scaled, + get_updated_liquidity_index(&markets[2], block_time).unwrap(), + ScalingOperation::Truncate, + ) + .unwrap() + * markets[2].liquidation_threshold + * prices[2]; - let env = mock_env_at_block_time(mock_market.indexes_last_updated + seconds_elapsed); - let info = mock_info("withdrawer", &[]); - let res = execute(deps.as_mut(), env, info, msg).unwrap(); + let weighted_liquidation_threshold_in_base_asset = + token_1_weighted_lt_in_base_asset + token_3_weighted_lt_in_base_asset; - let market = MARKETS.load(&deps.storage, "somecoin").unwrap(); + let total_collateralized_debt_in_base_asset = compute_underlying_amount( + debts[1].amount_scaled, + get_updated_borrow_index(&markets[1], block_time).unwrap(), + ScalingOperation::Ceil, + ) + .unwrap() + * prices[1]; - let withdrawer_balance = compute_underlying_amount( - withdrawer_balance_scaled, - get_updated_liquidity_index( - &market_initial, - market_initial.indexes_last_updated + seconds_elapsed, - ) - .unwrap(), - ScalingOperation::Truncate, + // How much to withdraw in base asset to have health factor equal to one + let how_much_to_withdraw_in_base_asset = math::divide_uint128_by_decimal( + weighted_liquidation_threshold_in_base_asset - total_collateralized_debt_in_base_asset, + markets[2].liquidation_threshold, ) .unwrap(); - let expected_params = th_get_expected_indices_and_rates( - &market_initial, - mock_market.indexes_last_updated + seconds_elapsed, - initial_available_liquidity, - TestUtilizationDeltaInfo { - less_liquidity: withdrawer_balance, - ..Default::default() + math::divide_uint128_by_decimal(how_much_to_withdraw_in_base_asset, prices[2]).unwrap() +} + +#[test] +fn withdrawing_if_health_factor_not_met() { + let suite = setup_health_check_test(); + + let env = mock_env(); + let block_time = env.block.time.seconds(); + + let max_withdraw_amount = how_much_to_withdraw(&suite, block_time); + + let HealthCheckTestSuite { + mut deps, + denoms, + withdrawer_addr, + .. + } = suite; + + // withdraw token3 with failure + // the withdraw amount needs to be a little bit greater to have health factor less than one + let withdraw_amount = max_withdraw_amount + Uint128::from(10u128); + + let err = execute( + deps.as_mut(), + env, + mock_info(withdrawer_addr.as_str(), &[]), + ExecuteMsg::Withdraw { + denom: denoms[2].to_string(), + amount: Some(withdraw_amount), + recipient: None, }, - ); + ) + .unwrap_err(); + assert_eq!(err, ContractError::InvalidHealthFactorAfterWithdraw {}); +} +#[test] +fn withdrawing_if_health_factor_met() { + let suite = setup_health_check_test(); + + let env = mock_env(); + let block_time = env.block.time.seconds(); + + let max_withdraw_amount = how_much_to_withdraw(&suite, block_time); + + let HealthCheckTestSuite { + mut deps, + denoms, + markets, + collaterals, + withdrawer_addr, + .. + } = suite; + + // withdraw token3 with success + // the withdraw amount needs to be a little bit smaller to have health factor greater than one + let withdraw_amount = max_withdraw_amount - Uint128::from(10u128); + + let res = execute( + deps.as_mut(), + env, + mock_info(withdrawer_addr.as_str(), &[]), + ExecuteMsg::Withdraw { + denom: denoms[2].to_string(), + amount: Some(withdraw_amount), + recipient: None, + }, + ) + .unwrap(); assert_eq!( res.messages, - vec![ - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: "matoken".to_string(), - msg: to_binary(&ma_token::msg::ExecuteMsg::Mint { - recipient: "protocol_rewards_collector".to_string(), - amount: compute_scaled_amount( - expected_params.protocol_rewards_to_distribute, - expected_params.liquidity_index, - ScalingOperation::Truncate - ) - .unwrap(), - }) - .unwrap(), - funds: vec![] - })), - SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: "matoken".to_string(), - msg: to_binary(&ma_token::msg::ExecuteMsg::Burn { - user: withdrawer_addr.to_string(), - amount: withdrawer_balance_scaled, - }) - .unwrap(), - funds: vec![] - })), - SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: withdrawer_addr.to_string(), - amount: coins(withdrawer_balance.u128(), "somecoin") - })), - ] - ); - assert_eq!( - res.attributes, - vec![ - attr("action", "outposts/red-bank/withdraw"), - attr("denom", "somecoin"), - attr("user", "withdrawer"), - attr("recipient", "withdrawer"), - attr("burn_amount", withdrawer_balance_scaled.to_string()), - attr("withdraw_amount", withdrawer_balance.to_string()), - ] + vec![SubMsg::new(CosmosMsg::Bank(BankMsg::Send { + to_address: withdrawer_addr.to_string(), + amount: coins(withdraw_amount.u128(), denoms[2]) + }))], ); - assert_eq!(res.events, vec![th_build_interests_updated_event("somecoin", &expected_params)]); - assert_eq!(market.borrow_rate, expected_params.borrow_rate); - assert_eq!(market.liquidity_rate, expected_params.liquidity_rate); - assert_eq!(market.liquidity_index, expected_params.liquidity_index); - assert_eq!(market.borrow_index, expected_params.borrow_index); + let expected_withdraw_amount_scaled = + get_scaled_liquidity_amount(withdraw_amount, &markets[2], block_time).unwrap(); + let expected_withdrawer_balance_after = + collaterals[2].amount_scaled - expected_withdraw_amount_scaled; + let expected_collateral_total_amount_scaled_after = + markets[2].collateral_total_scaled - expected_withdraw_amount_scaled; + + let col = COLLATERALS.load(deps.as_ref().storage, (&withdrawer_addr, denoms[2])).unwrap(); + assert_eq!(col.amount_scaled, expected_withdrawer_balance_after); - // User's collateral position should have been deleted after full withdraw - assert!(!has_collateral_position(deps.as_ref(), &withdrawer_addr, &market_initial.denom)); + let market = MARKETS.load(deps.as_ref().storage, denoms[2]).unwrap(); + assert_eq!(market.collateral_total_scaled, expected_collateral_total_amount_scaled_after); } diff --git a/packages/outpost/Cargo.toml b/packages/outpost/Cargo.toml index 8d869ef6..da7a9812 100644 --- a/packages/outpost/Cargo.toml +++ b/packages/outpost/Cargo.toml @@ -27,8 +27,6 @@ backtraces = ["cosmwasm-std/backtraces"] [dependencies] cosmwasm-std = "1.0" -cw20 = "0.14" -cw20-base = { version = "0.14", features = ["library"] } schemars = "0.8" serde = { version = "1.0.144", default-features = false, features = ["derive"] } thiserror = "1.0.33" diff --git a/packages/outpost/src/cw20_core.rs b/packages/outpost/src/cw20_core.rs deleted file mode 100644 index 0def8e09..00000000 --- a/packages/outpost/src/cw20_core.rs +++ /dev/null @@ -1,123 +0,0 @@ -/// cw20_core: Shared functionality for cw20 tokens -use cosmwasm_std::{DepsMut, StdError, Uint128}; - -use cw20::{EmbeddedLogo, Logo, LogoInfo, MarketingInfoResponse}; - -use cw20_base::msg::InstantiateMsg; -use cw20_base::state::{MinterData, TokenInfo, LOGO, MARKETING_INFO, TOKEN_INFO}; -use cw20_base::ContractError; - -pub fn instantiate_token_info_and_marketing( - deps: &mut DepsMut, - msg: InstantiateMsg, - total_supply: Uint128, -) -> Result<(), ContractError> { - if let Some(limit) = msg.get_cap() { - if total_supply > limit { - return Err(StdError::generic_err("Initial supply greater than cap").into()); - } - } - - let mint = match msg.mint { - Some(m) => Some(MinterData { - minter: deps.api.addr_validate(&m.minter)?, - cap: m.cap, - }), - None => None, - }; - - // store token info - let data = TokenInfo { - name: msg.name, - symbol: msg.symbol, - decimals: msg.decimals, - total_supply, - mint, - }; - TOKEN_INFO.save(deps.storage, &data)?; - - if let Some(marketing) = msg.marketing { - let logo = if let Some(logo) = marketing.logo { - verify_logo(&logo)?; - LOGO.save(deps.storage, &logo)?; - - match logo { - Logo::Url(url) => Some(LogoInfo::Url(url)), - Logo::Embedded(_) => Some(LogoInfo::Embedded), - } - } else { - None - }; - - let data = MarketingInfoResponse { - project: marketing.project, - description: marketing.description, - marketing: marketing.marketing.map(|addr| deps.api.addr_validate(&addr)).transpose()?, - logo, - }; - MARKETING_INFO.save(deps.storage, &data)?; - } - - Ok(()) -} - -const LOGO_SIZE_CAP: usize = 5 * 1024; - -/// Checks if data starts with XML preamble -fn verify_xml_preamble(data: &[u8]) -> Result<(), ContractError> { - // The easiest way to perform this check would be just match on regex, however regex - // compilation is heavy and probably not worth it. - - let preamble = - data.split_inclusive(|c| *c == b'>').next().ok_or(ContractError::InvalidXmlPreamble {})?; - - const PREFIX: &[u8] = b""; - - if !(preamble.starts_with(PREFIX) && preamble.ends_with(POSTFIX)) { - Err(ContractError::InvalidXmlPreamble {}) - } else { - Ok(()) - } - - // Additionally attributes format could be validated as they are well defined, as well as - // comments presence inside of preable, but it is probably not worth it. -} - -/// Validates XML logo -fn verify_xml_logo(logo: &[u8]) -> Result<(), ContractError> { - verify_xml_preamble(logo)?; - - if logo.len() > LOGO_SIZE_CAP { - Err(ContractError::LogoTooBig {}) - } else { - Ok(()) - } -} - -/// Validates png logo -fn verify_png_logo(logo: &[u8]) -> Result<(), ContractError> { - // PNG header format: - // 0x89 - magic byte, out of ASCII table to fail on 7-bit systems - // "PNG" ascii representation - // [0x0d, 0x0a] - dos style line ending - // 0x1a - dos control character, stop displaying rest of the file - // 0x0a - unix style line ending - const HEADER: [u8; 8] = [0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]; - if logo.len() > LOGO_SIZE_CAP { - Err(ContractError::LogoTooBig {}) - } else if !logo.starts_with(&HEADER) { - Err(ContractError::InvalidPngHeader {}) - } else { - Ok(()) - } -} - -/// Checks if passed logo is correct, and if not, returns an error -fn verify_logo(logo: &Logo) -> Result<(), ContractError> { - match logo { - Logo::Embedded(EmbeddedLogo::Svg(logo)) => verify_xml_logo(logo), - Logo::Embedded(EmbeddedLogo::Png(logo)) => verify_png_logo(logo), - Logo::Url(_) => Ok(()), // Any reasonable url validation would be regex based, probably not worth it - } -} diff --git a/packages/outpost/src/helpers.rs b/packages/outpost/src/helpers.rs index 5dcfd761..4213557b 100644 --- a/packages/outpost/src/helpers.rs +++ b/packages/outpost/src/helpers.rs @@ -1,35 +1,6 @@ -use cosmwasm_std::{ - coins, to_binary, Addr, Api, BankMsg, CosmosMsg, Decimal, QuerierWrapper, QueryRequest, - StdError, StdResult, Uint128, WasmQuery, -}; +use cosmwasm_std::{coins, Addr, Api, BankMsg, CosmosMsg, Decimal, StdResult, Uint128}; use crate::error::MarsError; -use cw20::{BalanceResponse, Cw20QueryMsg, TokenInfoResponse}; -use std::convert::TryInto; - -// CW20 -pub fn cw20_get_balance( - querier: &QuerierWrapper, - token_address: Addr, - balance_address: Addr, -) -> StdResult { - let query: BalanceResponse = querier.query(&QueryRequest::Wasm(WasmQuery::Smart { - contract_addr: token_address.into(), - msg: to_binary(&Cw20QueryMsg::Balance { - address: balance_address.into(), - })?, - }))?; - - Ok(query.balance) -} - -pub fn cw20_get_total_supply(querier: &QuerierWrapper, token_address: Addr) -> StdResult { - let res: TokenInfoResponse = querier.query(&QueryRequest::Wasm(WasmQuery::Smart { - contract_addr: token_address.into(), - msg: to_binary(&Cw20QueryMsg::TokenInfo {})?, - }))?; - Ok(res.total_supply) -} pub fn build_send_asset_msg(recipient_addr: &Addr, denom: &str, amount: Uint128) -> CosmosMsg { CosmosMsg::Bank(BankMsg::Send { @@ -38,23 +9,6 @@ pub fn build_send_asset_msg(recipient_addr: &Addr, denom: &str, amount: Uint128) }) } -pub fn read_be_u64(input: &[u8]) -> StdResult { - let num_of_bytes = std::mem::size_of::(); - if input.len() != num_of_bytes { - return Err(StdError::generic_err(format!( - "Expected slice length to be {}, received length of {}", - num_of_bytes, - input.len() - ))); - }; - let slice_to_array_result = input[0..num_of_bytes].try_into(); - - match slice_to_array_result { - Ok(array) => Ok(u64::from_be_bytes(array)), - Err(err) => Err(StdError::generic_err(format!("Error converting slice to array: {}", err))), - } -} - /// Used when unwrapping an optional address sent in a contract call by a user. /// Validates addreess if present, otherwise uses a given default value. pub fn option_string_to_addr( diff --git a/packages/outpost/src/lib.rs b/packages/outpost/src/lib.rs index a7410cd1..3b29cf38 100644 --- a/packages/outpost/src/lib.rs +++ b/packages/outpost/src/lib.rs @@ -1,15 +1,8 @@ -// Contracts pub mod address_provider; -pub mod cw20_core; +pub mod error; +pub mod helpers; pub mod incentives; -pub mod ma_token; +pub mod math; pub mod oracle; pub mod red_bank; pub mod rewards_collector; - -// Types -pub mod math; - -// Helpers -pub mod error; -pub mod helpers; diff --git a/packages/outpost/src/ma_token.rs b/packages/outpost/src/ma_token.rs deleted file mode 100644 index 0cf4a5ae..00000000 --- a/packages/outpost/src/ma_token.rs +++ /dev/null @@ -1,189 +0,0 @@ -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -use cosmwasm_std::Addr; - -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] -pub struct Config { - pub red_bank_address: Addr, - pub incentives_address: Addr, -} - -pub mod msg { - use cosmwasm_std::{Binary, Uint128}; - use cw20::{Cw20Coin, Expiration, Logo, MinterResponse}; - use cw20_base::msg::InstantiateMarketingInfo; - use schemars::JsonSchema; - use serde::{Deserialize, Serialize}; - - #[derive(Serialize, Deserialize, JsonSchema)] - pub struct InstantiateMsg { - // cw20_base params - pub name: String, - pub symbol: String, - pub decimals: u8, - pub initial_balances: Vec, - pub mint: Option, - pub marketing: Option, - - // custom_params - pub init_hook: Option, - pub red_bank_address: String, - pub incentives_address: String, - } - - /// Hook to be called after token initialization - #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] - pub struct InitHook { - pub msg: Binary, - pub contract_addr: String, - } - - #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] - #[serde(rename_all = "snake_case")] - pub enum ExecuteMsg { - /// Transfer is a base message to move tokens to another account. Requires to be finalized - /// by the money market. - Transfer { - recipient: String, - amount: Uint128, - }, - - /// Forced transfer called by the money market when an account is being liquidated - TransferOnLiquidation { - sender: String, - recipient: String, - amount: Uint128, - }, - - /// Burns tokens from user. Only money market can call this. - /// Used when user is being liquidated - Burn { - user: String, - amount: Uint128, - }, - - /// Send is a base message to transfer tokens to a contract and trigger an action - /// on the receiving contract. - Send { - contract: String, - amount: Uint128, - msg: Binary, - }, - - /// Only with the "mintable" extension. If authorized, creates amount new tokens - /// and adds to the recipient balance. - Mint { - recipient: String, - amount: Uint128, - }, - - /// Only with "approval" extension. Allows spender to access an additional amount tokens - /// from the owner's (env.sender) account. If expires is Some(), overwrites current allowance - /// expiration with this one. - IncreaseAllowance { - spender: String, - amount: Uint128, - expires: Option, - }, - /// Only with "approval" extension. Lowers the spender's access of tokens - /// from the owner's (env.sender) account by amount. If expires is Some(), overwrites current - /// allowance expiration with this one. - DecreaseAllowance { - spender: String, - amount: Uint128, - expires: Option, - }, - /// Only with "approval" extension. Transfers amount tokens from owner -> recipient - /// if `env.sender` has sufficient pre-approval. - TransferFrom { - owner: String, - recipient: String, - amount: Uint128, - }, - /// Only with "approval" extension. Sends amount tokens from owner -> contract - /// if `info.sender` has sufficient pre-approval. - SendFrom { - owner: String, - contract: String, - amount: Uint128, - msg: Binary, - }, - /// Only with the "marketing" extension. If authorized, updates marketing metadata. - /// Setting None/null for any of these will leave it unchanged. - /// Setting Some("") will clear this field on the contract storage - UpdateMarketing { - /// A URL pointing to the project behind this token. - project: Option, - /// A longer description of the token and it's utility. Designed for tooltips or such - description: Option, - /// The address (if any) who can update this data structure - marketing: Option, - }, - /// If set as the "marketing" role on the contract, upload a new URL, SVG, or PNG for the token - UploadLogo(Logo), - } - - #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] - #[serde(rename_all = "snake_case")] - pub enum QueryMsg { - /// Returns the current balance of the given address, 0 if unset. - /// Return type: BalanceResponse. - Balance { - address: String, - }, - /// Returns both balance (0 if unset) and total supply - /// Used by incentives contract when computing unclaimed rewards - /// Return type: BalanceAndTotalSupplyResponse - BalanceAndTotalSupply { - address: String, - }, - /// Returns metadata on the contract - name, decimals, supply, etc. - /// Return type: TokenInfoResponse. - TokenInfo {}, - Minter {}, - /// Only with "allowance" extension. - /// Returns how much spender can use from owner account, 0 if unset. - /// Return type: AllowanceResponse. - Allowance { - owner: String, - spender: String, - }, - /// Only with "enumerable" extension (and "allowances") - /// Returns all allowances this owner has approved. Supports pagination. - /// Return type: AllAllowancesResponse. - AllAllowances { - owner: String, - start_after: Option, - limit: Option, - }, - /// Only with "enumerable" extension - /// Returns all accounts that have balances. Supports pagination. - /// Return type: AllAccountsResponse. - AllAccounts { - start_after: Option, - limit: Option, - }, - /// Only with "marketing" extension - /// Returns more metadata on the contract to display in the client: - /// - description, logo, project url, etc. - /// Return type: MarketingInfoResponse - MarketingInfo {}, - /// Only with "marketing" extension - /// Downloads the mbeded logo data (if stored on chain). Errors if no logo data ftored for this - /// contract. - /// Return type: DownloadLogoResponse. - DownloadLogo {}, - /// Returns the underlying asset amount for given address. - /// Return type: BalanceResponse. - UnderlyingAssetBalance { - address: String, - }, - } - - #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] - pub struct BalanceAndTotalSupplyResponse { - pub balance: Uint128, - pub total_supply: Uint128, - } -} diff --git a/packages/outpost/src/red_bank/market.rs b/packages/outpost/src/red_bank/market.rs index 4993ae7e..248af214 100644 --- a/packages/outpost/src/red_bank/market.rs +++ b/packages/outpost/src/red_bank/market.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Addr, Decimal, StdResult, Uint128}; +use cosmwasm_std::{Decimal, StdResult, Uint128}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -10,8 +10,6 @@ use crate::red_bank::InterestRateModel; pub struct Market { /// Denom of the asset pub denom: String, - /// maToken contract address - pub ma_token_address: Addr, /// Max base asset that can be borrowed per "base asset" collateral when using the asset as collateral pub max_loan_to_value: Decimal, @@ -37,6 +35,8 @@ pub struct Market { /// Timestamp (seconds) where indexes and where last updated pub indexes_last_updated: u64, + /// Total collateral scaled for the market's currency + pub collateral_total_scaled: Uint128, /// Total debt scaled for the market's currency pub debt_total_scaled: Uint128, @@ -52,7 +52,6 @@ impl Default for Market { fn default() -> Self { Market { denom: "".to_string(), - ma_token_address: crate::helpers::zero_address(), borrow_index: Decimal::one(), liquidity_index: Decimal::one(), borrow_rate: Decimal::zero(), @@ -60,6 +59,7 @@ impl Default for Market { max_loan_to_value: Decimal::zero(), reserve_factor: Decimal::zero(), indexes_last_updated: 0, + collateral_total_scaled: Uint128::zero(), debt_total_scaled: Uint128::zero(), liquidation_threshold: Decimal::one(), liquidation_bonus: Decimal::zero(), @@ -103,4 +103,24 @@ impl Market { Ok(()) } + + pub fn increase_collateral(&mut self, amount_scaled: Uint128) -> StdResult<()> { + self.collateral_total_scaled = self.collateral_total_scaled.checked_add(amount_scaled)?; + Ok(()) + } + + pub fn increase_debt(&mut self, amount_scaled: Uint128) -> StdResult<()> { + self.debt_total_scaled = self.debt_total_scaled.checked_add(amount_scaled)?; + Ok(()) + } + + pub fn decrease_collateral(&mut self, amount_scaled: Uint128) -> StdResult<()> { + self.collateral_total_scaled = self.collateral_total_scaled.checked_sub(amount_scaled)?; + Ok(()) + } + + pub fn decrease_debt(&mut self, amount_scaled: Uint128) -> StdResult<()> { + self.debt_total_scaled = self.debt_total_scaled.checked_sub(amount_scaled)?; + Ok(()) + } } diff --git a/packages/outpost/src/red_bank/msg.rs b/packages/outpost/src/red_bank/msg.rs index 93cb99e2..7025f899 100644 --- a/packages/outpost/src/red_bank/msg.rs +++ b/packages/outpost/src/red_bank/msg.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Addr, Decimal, Uint128}; +use cosmwasm_std::{Decimal, Uint128}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -24,18 +24,7 @@ pub enum ExecuteMsg { /// Asset related info denom: String, /// Asset parameters - asset_params: InitOrUpdateAssetParams, - /// Asset symbol to be used in maToken name and description. If non is provided, - /// denom will be used for native and token symbol will be used for cw20. Mostly - /// useful for native assets since it's denom (e.g.: uluna, uusd) does not match it's - /// user facing symbol (LUNA, UST) which should be used in maToken's attributes - /// for the sake of consistency - asset_symbol: Option, - }, - - /// Callback sent from maToken contract after instantiated - InitAssetTokenCallback { - denom: String, + params: InitOrUpdateAssetParams, }, /// Update an asset on the money market (only owner can call) @@ -43,7 +32,7 @@ pub enum ExecuteMsg { /// Asset related info denom: String, /// Asset parameters - asset_params: InitOrUpdateAssetParams, + params: InitOrUpdateAssetParams, }, /// Update uncollateralized loan limit for a given user and asset. @@ -70,8 +59,7 @@ pub enum ExecuteMsg { Withdraw { /// Asset to withdraw denom: String, - /// Amount to be withdrawn. If None is specified, the full maToken balance will be - /// burned in exchange for the equivalent asset amount. + /// Amount to be withdrawn. If None is specified, the full amount will be withdrawn. amount: Option, /// The address where the withdrawn amount is sent recipient: Option, @@ -114,30 +102,12 @@ pub enum ExecuteMsg { /// Option to enable (true) / disable (false) asset as collateral enable: bool, }, - - /// Called by liquidity token (maToken). Validate liquidity token transfer is valid - /// and update collateral status - FinalizeLiquidityTokenTransfer { - /// Token sender. Address is trusted because it should have been verified in - /// the token contract - sender_address: Addr, - /// Token recipient. Address is trusted because it should have been verified in - /// the token contract - recipient_address: Addr, - /// Sender's balance before the token transfer - sender_previous_balance: Uint128, - /// Recipient's balance before the token transfer - recipient_previous_balance: Uint128, - /// Transfer amount - amount: Uint128, - }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] pub struct CreateOrUpdateConfig { pub owner: Option, pub address_provider: Option, - pub ma_token_code_id: Option, pub close_factor: Option, } @@ -244,9 +214,11 @@ pub enum QueryMsg { amount: Uint128, }, - /// Get underlying asset amount for a given maToken balance. + /// Get underlying asset amount for a given asset and scaled amount. + /// (i.e. How much underlying asset will be released if withdrawing by burning a given scaled + /// collateral amount stored in state.) UnderlyingLiquidityAmount { - ma_token_address: String, + denom: String, amount_scaled: Uint128, }, diff --git a/packages/outpost/src/red_bank/types.rs b/packages/outpost/src/red_bank/types.rs index 64a60f65..db8a4db3 100644 --- a/packages/outpost/src/red_bank/types.rs +++ b/packages/outpost/src/red_bank/types.rs @@ -12,8 +12,6 @@ pub struct Config { pub owner: Addr, /// Address provider returns addresses for all protocol contracts pub address_provider: Addr, - /// maToken code id used to instantiate new tokens - pub ma_token_code_id: u64, /// Maximum percentage of outstanding debt that can be covered by a liquidator pub close_factor: Decimal, } @@ -25,9 +23,10 @@ impl Config { } } -// TODO: Once maToken is removed, the scaled collateral amount will be stored in this struct #[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq, JsonSchema)] pub struct Collateral { + /// Scaled collateral amount + pub amount_scaled: Uint128, /// Whether this asset is enabled as collateral /// /// Set to true by default, unless the user explicitly disables it by invoking the @@ -39,11 +38,10 @@ pub struct Collateral { } /// Debt for each asset and user -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq, JsonSchema)] pub struct Debt { /// Scaled debt amount pub amount_scaled: Uint128, - /// Marker for uncollateralized debt pub uncollateralized: bool, } @@ -76,7 +74,6 @@ pub struct Position { pub struct ConfigResponse { pub owner: String, pub address_provider: String, - pub ma_token_code_id: u64, pub close_factor: Decimal, } @@ -96,13 +93,18 @@ pub struct UserDebtResponse { pub amount_scaled: Uint128, /// Underlying asset amount that is actually owed at the current block pub amount: Uint128, + /// Marker for uncollateralized debt + pub uncollateralized: bool, } -// TODO: In an upcoming PR, we will also include `amount_scaled` and `amount` in this response #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] pub struct UserCollateralResponse { /// Asset denom pub denom: String, + /// Scaled collateral amount stored in contract state + pub amount_scaled: Uint128, + /// Underlying asset amount that is actually deposited at the current block + pub amount: Uint128, /// Wether the user is using asset as collateral or not pub enabled: bool, } diff --git a/packages/testing/Cargo.toml b/packages/testing/Cargo.toml index 3e2afa52..19b85e99 100644 --- a/packages/testing/Cargo.toml +++ b/packages/testing/Cargo.toml @@ -29,8 +29,6 @@ backtraces = ["cosmwasm-std/backtraces"] mars-outpost = { path = "../outpost" } cosmwasm-std = "1.0" -cw20 = "0.14" -cw20-base = { version = "0.14", features = ["library"] } schemars = "0.8" serde = { version = "1.0.144", default-features = false, features = ["derive"] } thiserror = "1.0.33" diff --git a/packages/testing/src/cw20_querier.rs b/packages/testing/src/cw20_querier.rs deleted file mode 100644 index 64aca871..00000000 --- a/packages/testing/src/cw20_querier.rs +++ /dev/null @@ -1,199 +0,0 @@ -use std::collections::HashMap; - -use cosmwasm_std::{to_binary, Addr, QuerierResult, SystemError, Uint128}; -use cw20::{AllAccountsResponse, BalanceResponse, Cw20QueryMsg, TokenInfoResponse}; - -use mars_outpost::ma_token; - -#[derive(Clone, Debug, Default)] -pub struct Cw20Querier { - /// maps cw20 contract address to user balances - pub balances: HashMap>, - /// maps cw20 contract address to token info response - pub token_info_responses: HashMap, -} - -impl Cw20Querier { - pub fn handle_cw20_query(&self, contract_addr: &Addr, query: Cw20QueryMsg) -> QuerierResult { - match query { - Cw20QueryMsg::AllAccounts { - start_after, - limit, - } => { - if start_after.is_some() { - return Err(SystemError::InvalidRequest { - error: "mock cw20 only supports `start_after` to be `None`".to_string(), - request: Default::default(), - }) - .into(); - } - - let contract_balances = match self.balances.get(contract_addr) { - Some(balances) => balances, - None => { - return Err(SystemError::InvalidRequest { - error: format!( - "no balance available for account address {}", - contract_addr - ), - request: Default::default(), - }) - .into() - } - }; - - let mut accounts = contract_balances - .keys() - .take(limit.unwrap_or(10) as usize) - .map(|addr| addr.to_string()) - .collect::>(); - - // sort accounts alphabetically - accounts.sort(); - - Ok(to_binary(&AllAccountsResponse { - accounts, - }) - .into()) - .into() - } - - Cw20QueryMsg::Balance { - address, - } => { - let contract_balances = match self.balances.get(contract_addr) { - Some(balances) => balances, - None => { - return Err(SystemError::InvalidRequest { - error: format!( - "no balance available for account address {}", - contract_addr - ), - request: Default::default(), - }) - .into() - } - }; - - let user_balance = match contract_balances.get(&Addr::unchecked(address)) { - Some(balance) => balance, - None => { - return Err(SystemError::InvalidRequest { - error: format!( - "no balance available for account address {}", - contract_addr - ), - request: Default::default(), - }) - .into() - } - }; - - Ok(to_binary(&BalanceResponse { - balance: *user_balance, - }) - .into()) - .into() - } - - Cw20QueryMsg::TokenInfo {} => { - let token_info_response = match self.token_info_responses.get(contract_addr) { - Some(tir) => tir, - None => { - return Err(SystemError::InvalidRequest { - error: format!( - "no token_info mock for account address {}", - contract_addr - ), - request: Default::default(), - }) - .into() - } - }; - - Ok(to_binary(token_info_response).into()).into() - } - - other_query => Err(SystemError::InvalidRequest { - error: format!("[mock]: query not supported {:?}", other_query), - request: Default::default(), - }) - .into(), - } - } - - pub fn handle_ma_token_query( - &self, - contract_addr: &Addr, - query: ma_token::msg::QueryMsg, - ) -> QuerierResult { - match query { - ma_token::msg::QueryMsg::BalanceAndTotalSupply { - address, - } => { - let contract_balances = match self.balances.get(contract_addr) { - Some(balances) => balances, - None => { - return Err(SystemError::InvalidRequest { - error: format!( - "no balance available for account address {}", - contract_addr - ), - request: Default::default(), - }) - .into() - } - }; - - let user_balance = match contract_balances.get(&Addr::unchecked(address)) { - Some(balance) => balance, - None => { - return Err(SystemError::InvalidRequest { - error: format!( - "no balance available for account address {}", - contract_addr - ), - request: Default::default(), - }) - .into() - } - }; - let token_info_response = match self.token_info_responses.get(contract_addr) { - Some(tir) => tir, - None => { - return Err(SystemError::InvalidRequest { - error: format!( - "no token_info mock for account address {}", - contract_addr - ), - request: Default::default(), - }) - .into() - } - }; - - Ok(to_binary(&ma_token::msg::BalanceAndTotalSupplyResponse { - balance: *user_balance, - total_supply: token_info_response.total_supply, - }) - .into()) - .into() - } - - other_query => Err(SystemError::InvalidRequest { - error: format!("[mock]: query not supported {:?}", other_query), - request: Default::default(), - }) - .into(), - } - } -} - -pub fn mock_token_info_response() -> TokenInfoResponse { - TokenInfoResponse { - name: "".to_string(), - symbol: "".to_string(), - decimals: 0, - total_supply: Uint128::zero(), - } -} diff --git a/packages/testing/src/lib.rs b/packages/testing/src/lib.rs index daba20b7..06481e8c 100644 --- a/packages/testing/src/lib.rs +++ b/packages/testing/src/lib.rs @@ -1,7 +1,5 @@ extern crate core; -#[cfg(not(target_arch = "wasm32"))] -mod cw20_querier; /// cosmwasm_std::testing overrides and custom test helpers #[cfg(not(target_arch = "wasm32"))] mod helpers; diff --git a/packages/testing/src/mars_mock_querier.rs b/packages/testing/src/mars_mock_querier.rs index daaa3d6b..5b1deaec 100644 --- a/packages/testing/src/mars_mock_querier.rs +++ b/packages/testing/src/mars_mock_querier.rs @@ -3,15 +3,14 @@ use cosmwasm_std::{ from_binary, from_slice, Addr, Coin, Decimal, Empty, Querier, QuerierResult, QueryRequest, StdResult, SystemError, SystemResult, Uint128, WasmQuery, }; -use cw20::Cw20QueryMsg; + use osmosis_std::types::osmosis::gamm::twap::v1beta1::GetArithmeticTwapResponse; use osmosis_std::types::osmosis::gamm::v1beta1::{ QueryPoolResponse, QuerySpotPriceResponse, QuerySwapExactAmountInResponse, SwapAmountInRoute, }; -use mars_outpost::{address_provider, incentives, ma_token, oracle, red_bank}; +use mars_outpost::{address_provider, incentives, oracle, red_bank}; -use crate::cw20_querier::{mock_token_info_response, Cw20Querier}; use crate::incentives_querier::IncentivesQuerier; use crate::mock_address_provider; use crate::oracle_querier::OracleQuerier; @@ -20,7 +19,6 @@ use crate::red_bank_querier::RedBankQuerier; pub struct MarsMockQuerier { base: MockQuerier, - cw20_querier: Cw20Querier, oracle_querier: OracleQuerier, incentives_querier: IncentivesQuerier, osmosis_querier: OsmosisQuerier, @@ -47,7 +45,6 @@ impl MarsMockQuerier { pub fn new(base: MockQuerier) -> Self { MarsMockQuerier { base, - cw20_querier: Cw20Querier::default(), oracle_querier: OracleQuerier::default(), incentives_querier: IncentivesQuerier::default(), osmosis_querier: OsmosisQuerier::default(), @@ -61,36 +58,6 @@ impl MarsMockQuerier { self.base.update_balance(contract_addr.to_string(), contract_balances.to_vec()); } - /// Set mock querier balances results for a given cw20 token - pub fn set_cw20_balances(&mut self, cw20_address: Addr, balances: &[(Addr, Uint128)]) { - self.cw20_querier.balances.insert(cw20_address, balances.iter().cloned().collect()); - } - - /// Set mock querier so that it returns a specific total supply on the token info query - /// for a given cw20 token (note this will override existing token info with default - /// values for the rest of the fields) - #[allow(clippy::or_fun_call)] - pub fn set_cw20_total_supply(&mut self, cw20_address: Addr, total_supply: Uint128) { - let token_info = self - .cw20_querier - .token_info_responses - .entry(cw20_address) - .or_insert(mock_token_info_response()); - - token_info.total_supply = total_supply; - } - - #[allow(clippy::or_fun_call)] - pub fn set_cw20_symbol(&mut self, cw20_address: Addr, symbol: String) { - let token_info = self - .cw20_querier - .token_info_responses - .entry(cw20_address) - .or_insert(mock_token_info_response()); - - token_info.symbol = symbol; - } - pub fn set_oracle_price(&mut self, denom: &str, price: Decimal) { self.oracle_querier.prices.insert(denom.to_string(), price); } @@ -169,18 +136,6 @@ impl MarsMockQuerier { }) => { let contract_addr = Addr::unchecked(contract_addr); - // Cw20 Queries - let parse_cw20_query: StdResult = from_binary(msg); - if let Ok(cw20_query) = parse_cw20_query { - return self.cw20_querier.handle_cw20_query(&contract_addr, cw20_query); - } - - // MaToken Queries - let parse_ma_token_query: StdResult = from_binary(msg); - if let Ok(ma_token_query) = parse_ma_token_query { - return self.cw20_querier.handle_ma_token_query(&contract_addr, ma_token_query); - } - // Address Provider Queries let parse_address_provider_query: StdResult = from_binary(msg);