diff --git a/Cargo.lock b/Cargo.lock index 4af1e947f..c2106f3ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2147,7 +2147,7 @@ dependencies = [ [[package]] name = "mars-credit-manager" -version = "2.1.0" +version = "2.2.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -2530,7 +2530,7 @@ dependencies = [ [[package]] name = "mars-rewards-collector-osmosis" -version = "2.1.1" +version = "2.2.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", diff --git a/Makefile.toml b/Makefile.toml index d9758408a..456931601 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -13,7 +13,7 @@ ARTIFACTS_DIR_PATH = "target/wasm32-unknown-unknown/release" RUST_OPTIMIZER_VERSION = "0.16.0" # Use rust version from rust-optimizer Dockerfile (see https://github.com/CosmWasm/optimizer/blob/v0.16.0/Dockerfile#L1) # to be sure that we compile / test against the same version -RUST_VERSION = "1.78.0" +RUST_VERSION = "1.81.0" [tasks.install-stable] script = ''' diff --git a/contracts/account-nft/tests/tests/helpers/mock_env.rs b/contracts/account-nft/tests/tests/helpers/mock_env.rs index 882963125..d0e06bfdf 100644 --- a/contracts/account-nft/tests/tests/helpers/mock_env.rs +++ b/contracts/account-nft/tests/tests/helpers/mock_env.rs @@ -18,6 +18,7 @@ use mars_types::{ use super::MockEnvBuilder; +#[allow(dead_code)] pub struct MockEnv { pub app: BasicApp, pub minter: Addr, diff --git a/contracts/credit-manager/Cargo.toml b/contracts/credit-manager/Cargo.toml index 6180d1add..e7dd9efc4 100644 --- a/contracts/credit-manager/Cargo.toml +++ b/contracts/credit-manager/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mars-credit-manager" -version = { workspace = true } +version = "2.2.0" authors = { workspace = true } license = { workspace = true } edition = { workspace = true } diff --git a/contracts/credit-manager/src/contract.rs b/contracts/credit-manager/src/contract.rs index 8fafa9125..e74d395df 100644 --- a/contracts/credit-manager/src/contract.rs +++ b/contracts/credit-manager/src/contract.rs @@ -15,8 +15,8 @@ use crate::{ query::{ query_accounts, query_all_coin_balances, query_all_debt_shares, query_all_total_debt_shares, query_all_vault_positions, query_all_vault_utilizations, - query_config, query_positions, query_total_debt_shares, query_vault_bindings, - query_vault_position_value, query_vault_utilization, + query_config, query_positions, query_swap_fee, query_total_debt_shares, + query_vault_bindings, query_vault_position_value, query_vault_utilization, }, repay::repay_from_wallet, update_config::{update_config, update_nft_config, update_owner}, @@ -132,11 +132,12 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> ContractResult { start_after, limit, } => to_json_binary(&query_vault_bindings(deps, start_after, limit)?), + QueryMsg::SwapFeeRate {} => to_json_binary(&query_swap_fee(deps)?), }; res.map_err(Into::into) } #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate(deps: DepsMut, _env: Env, _msg: Empty) -> Result { - migrations::v2_1_0::migrate(deps) + migrations::v2_2_0::migrate(deps) } diff --git a/contracts/credit-manager/src/instantiate.rs b/contracts/credit-manager/src/instantiate.rs index 3a4ec1f99..3697e1ee5 100644 --- a/contracts/credit-manager/src/instantiate.rs +++ b/contracts/credit-manager/src/instantiate.rs @@ -6,9 +6,9 @@ use crate::{ error::ContractResult, state::{ HEALTH_CONTRACT, INCENTIVES, MAX_SLIPPAGE, MAX_UNLOCKING_POSITIONS, ORACLE, OWNER, PARAMS, - RED_BANK, SWAPPER, ZAPPER, + RED_BANK, SWAPPER, SWAP_FEE, ZAPPER, }, - utils::assert_max_slippage, + utils::{assert_max_slippage, assert_swap_fee}, }; pub fn store_config(deps: DepsMut, env: Env, msg: &InstantiateMsg) -> ContractResult<()> { @@ -32,6 +32,8 @@ pub fn store_config(deps: DepsMut, env: Env, msg: &InstantiateMsg) -> ContractRe HEALTH_CONTRACT.save(deps.storage, &msg.health_contract.check(deps.api)?)?; PARAMS.save(deps.storage, &msg.params.check(deps.api)?)?; INCENTIVES.save(deps.storage, &msg.incentives.check(deps.api, env.contract.address)?)?; + assert_swap_fee(msg.swap_fee)?; + SWAP_FEE.save(deps.storage, &msg.swap_fee)?; Ok(()) } diff --git a/contracts/credit-manager/src/liquidate.rs b/contracts/credit-manager/src/liquidate.rs index 9c3c20b2c..d809facb8 100644 --- a/contracts/credit-manager/src/liquidate.rs +++ b/contracts/credit-manager/src/liquidate.rs @@ -14,6 +14,7 @@ use crate::{ /// - Exceeds liquidatee's total debt for denom /// - Not enough liquidatee request coin balance to match /// - The value of the debt repaid exceeds the Maximum Debt Repayable (MDR) +/// /// Returns -> (Debt Coin, Liquidator Request Coin, Liquidatee Request Coin) /// Difference between Liquidator Request Coin and Liquidatee Request Coin goes to rewards-collector account as protocol fee. pub fn calculate_liquidation( diff --git a/contracts/credit-manager/src/migrations/mod.rs b/contracts/credit-manager/src/migrations/mod.rs index 78e0d72f0..569d5a2f0 100644 --- a/contracts/credit-manager/src/migrations/mod.rs +++ b/contracts/credit-manager/src/migrations/mod.rs @@ -1 +1,2 @@ pub mod v2_1_0; +pub mod v2_2_0; diff --git a/contracts/credit-manager/src/migrations/v2_1_0.rs b/contracts/credit-manager/src/migrations/v2_1_0.rs index 67cc959f8..73ac80af0 100644 --- a/contracts/credit-manager/src/migrations/v2_1_0.rs +++ b/contracts/credit-manager/src/migrations/v2_1_0.rs @@ -1,21 +1,19 @@ use cosmwasm_std::{DepsMut, Response}; use cw2::{assert_contract_version, set_contract_version}; -use crate::{ - contract::{CONTRACT_NAME, CONTRACT_VERSION}, - error::ContractError, -}; +use crate::{contract::CONTRACT_NAME, error::ContractError}; const FROM_VERSION: &str = "2.0.3"; +const TO_VERSION: &str = "2.1.0"; pub fn migrate(deps: DepsMut) -> Result { // make sure we're migrating the correct contract and from the correct version assert_contract_version(deps.storage, &format!("crates.io:{CONTRACT_NAME}"), FROM_VERSION)?; - set_contract_version(deps.storage, format!("crates.io:{CONTRACT_NAME}"), CONTRACT_VERSION)?; + set_contract_version(deps.storage, format!("crates.io:{CONTRACT_NAME}"), TO_VERSION)?; Ok(Response::new() .add_attribute("action", "migrate") .add_attribute("from_version", FROM_VERSION) - .add_attribute("to_version", CONTRACT_VERSION)) + .add_attribute("to_version", TO_VERSION)) } diff --git a/contracts/credit-manager/src/migrations/v2_2_0.rs b/contracts/credit-manager/src/migrations/v2_2_0.rs new file mode 100644 index 000000000..7b2206cd1 --- /dev/null +++ b/contracts/credit-manager/src/migrations/v2_2_0.rs @@ -0,0 +1,43 @@ +use cosmwasm_std::{Decimal, DepsMut, Response}; +use cw2::{assert_contract_version, get_contract_version, set_contract_version, VersionError}; + +use crate::{ + contract::{CONTRACT_NAME, CONTRACT_VERSION}, + error::ContractError, + state::SWAP_FEE, +}; + +const FROM_VERSION: &str = "2.1.0"; + +pub fn migrate(deps: DepsMut) -> Result { + let contract = format!("crates.io:{CONTRACT_NAME}"); + let version = get_contract_version(deps.storage)?; + let from_version = version.version; + + if version.contract != contract { + return Err(ContractError::Version(VersionError::WrongContract { + expected: contract, + found: version.contract, + })); + } + + if from_version != FROM_VERSION { + return Err(ContractError::Version(VersionError::WrongVersion { + expected: FROM_VERSION.to_string(), + found: from_version, + })); + } + + assert_contract_version(deps.storage, &contract, FROM_VERSION)?; + + if SWAP_FEE.may_load(deps.storage)?.is_none() { + SWAP_FEE.save(deps.storage, &Decimal::zero())?; + } + + set_contract_version(deps.storage, format!("crates.io:{CONTRACT_NAME}"), CONTRACT_VERSION)?; + + Ok(Response::new() + .add_attribute("action", "migrate") + .add_attribute("from_version", from_version) + .add_attribute("to_version", CONTRACT_VERSION)) +} diff --git a/contracts/credit-manager/src/query.rs b/contracts/credit-manager/src/query.rs index 3a2bd97dc..09d353a57 100644 --- a/contracts/credit-manager/src/query.rs +++ b/contracts/credit-manager/src/query.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Coin, Deps, Env, Order, StdResult}; +use cosmwasm_std::{Coin, Decimal, Deps, Env, Order, StdResult}; use cw_paginate::{paginate_map, paginate_map_query, PaginationResponse, DEFAULT_LIMIT, MAX_LIMIT}; use cw_storage_plus::Bound; use mars_types::{ @@ -16,7 +16,7 @@ use crate::{ state::{ ACCOUNT_KINDS, ACCOUNT_NFT, COIN_BALANCES, DEBT_SHARES, HEALTH_CONTRACT, INCENTIVES, MAX_SLIPPAGE, MAX_UNLOCKING_POSITIONS, ORACLE, OWNER, PARAMS, RED_BANK, REWARDS_COLLECTOR, - SWAPPER, TOTAL_DEBT_SHARES, VAULTS, VAULT_POSITIONS, ZAPPER, + SWAPPER, SWAP_FEE, TOTAL_DEBT_SHARES, VAULTS, VAULT_POSITIONS, ZAPPER, }, utils::debt_shares_to_amount, vault::vault_utilization_in_deposit_cap_denom, @@ -62,6 +62,10 @@ pub fn query_config(deps: Deps) -> ContractResult { }) } +pub fn query_swap_fee(deps: Deps) -> ContractResult { + Ok(SWAP_FEE.load(deps.storage)?) +} + pub fn query_positions(deps: Deps, account_id: &str) -> ContractResult { Ok(Positions { account_id: account_id.to_string(), diff --git a/contracts/credit-manager/src/state.rs b/contracts/credit-manager/src/state.rs index 7931494ba..9f20b2d75 100644 --- a/contracts/credit-manager/src/state.rs +++ b/contracts/credit-manager/src/state.rs @@ -46,3 +46,6 @@ pub const REWARDS_COLLECTOR: Item = Item::new("rewards_collect // (account id, vault addr) bindings between account and vault pub const VAULTS: Map<&str, Addr> = Map::new("vaults"); + +// Swap fee +pub const SWAP_FEE: Item = Item::new("swap_fee"); diff --git a/contracts/credit-manager/src/swap.rs b/contracts/credit-manager/src/swap.rs index af8b2f69e..4a4c315ab 100644 --- a/contracts/credit-manager/src/swap.rs +++ b/contracts/credit-manager/src/swap.rs @@ -6,8 +6,8 @@ use mars_types::{ use crate::{ error::{ContractError, ContractResult}, - state::{COIN_BALANCES, SWAPPER}, - utils::{decrement_coin_balance, update_balance_msg}, + state::{COIN_BALANCES, REWARDS_COLLECTOR, SWAPPER, SWAP_FEE}, + utils::{decrement_coin_balance, increment_coin_balance, update_balance_msg}, }; pub fn swap_exact_in( @@ -19,7 +19,7 @@ pub fn swap_exact_in( min_receive: Uint128, route: Option, ) -> ContractResult { - let coin_in_to_trade = Coin { + let mut coin_in_to_trade = Coin { denom: coin_in.denom.clone(), amount: match coin_in.amount { ActionAmount::Exact(a) => a, @@ -35,6 +35,19 @@ pub fn swap_exact_in( decrement_coin_balance(deps.storage, account_id, &coin_in_to_trade)?; + // Deduct the swap fee + let swap_fee = SWAP_FEE.load(deps.storage)?; + let swap_fee_amount = coin_in_to_trade.amount.checked_mul_floor(swap_fee)?; + coin_in_to_trade.amount = coin_in_to_trade.amount.checked_sub(swap_fee_amount)?; + + // Send to Rewards collector + let rc_coin = Coin { + denom: coin_in.denom.clone(), + amount: swap_fee_amount, + }; + let rewards_collector_account = REWARDS_COLLECTOR.load(deps.storage)?.account_id; + increment_coin_balance(deps.storage, &rewards_collector_account, &rc_coin)?; + // Updates coin balances for account after the swap has taken place let update_coin_balance_msg = update_balance_msg( &deps.querier, diff --git a/contracts/credit-manager/src/update_config.rs b/contracts/credit-manager/src/update_config.rs index 0382cbd4c..5e61d4761 100644 --- a/contracts/credit-manager/src/update_config.rs +++ b/contracts/credit-manager/src/update_config.rs @@ -13,9 +13,9 @@ use crate::{ execute::create_credit_account, state::{ ACCOUNT_NFT, HEALTH_CONTRACT, INCENTIVES, MAX_SLIPPAGE, MAX_UNLOCKING_POSITIONS, ORACLE, - OWNER, RED_BANK, REWARDS_COLLECTOR, SWAPPER, ZAPPER, + OWNER, RED_BANK, REWARDS_COLLECTOR, SWAPPER, SWAP_FEE, ZAPPER, }, - utils::assert_max_slippage, + utils::{assert_max_slippage, assert_swap_fee}, }; pub fn update_config( @@ -83,6 +83,13 @@ pub fn update_config( response.add_attribute("key", "max_slippage").add_attribute("value", num.to_string()); } + if let Some(num) = updates.swap_fee { + assert_swap_fee(num)?; + SWAP_FEE.save(deps.storage, &num)?; + response = + response.add_attribute("key", "swap_fee").add_attribute("value", num.to_string()); + } + if let Some(unchecked) = updates.health_contract { HEALTH_CONTRACT.save(deps.storage, &unchecked.check(deps.api)?)?; response = response diff --git a/contracts/credit-manager/src/utils.rs b/contracts/credit-manager/src/utils.rs index f605f6c44..423c4c446 100644 --- a/contracts/credit-manager/src/utils.rs +++ b/contracts/credit-manager/src/utils.rs @@ -49,6 +49,15 @@ pub fn assert_slippage(storage: &dyn Storage, slippage: Decimal) -> ContractResu Ok(()) } +pub fn assert_swap_fee(swap_fee: Decimal) -> ContractResult<()> { + if swap_fee >= Decimal::one() { + return Err(ContractError::InvalidConfig { + reason: "Swap fee must be less than 1".to_string(), + }); + } + Ok(()) +} + pub fn query_nft_token_owner(deps: Deps, account_id: &str) -> ContractResult { Ok(ACCOUNT_NFT.load(deps.storage)?.query_nft_token_owner(&deps.querier, account_id)?) } diff --git a/contracts/credit-manager/tests/tests/mod.rs b/contracts/credit-manager/tests/tests/mod.rs index 72ac5ff12..defcc4614 100644 --- a/contracts/credit-manager/tests/tests/mod.rs +++ b/contracts/credit-manager/tests/tests/mod.rs @@ -24,7 +24,7 @@ mod test_liquidate_lend; mod test_liquidate_staked_astro_lp; mod test_liquidate_vault; mod test_liquidation_pricing; -mod test_migration_v2; +mod test_migration_v2_2_0; mod test_no_health_check; mod test_reclaim; mod test_reentrancy_guard; @@ -32,6 +32,7 @@ mod test_refund_balances; mod test_repay; mod test_repay_for_recipient; mod test_repay_from_wallet; +mod test_rewards_collector_whitelist; mod test_stake_astro_lp; mod test_swap; mod test_unstake_astro_lp; diff --git a/contracts/credit-manager/tests/tests/test_instantiate.rs b/contracts/credit-manager/tests/tests/test_instantiate.rs index 242858a57..816a1f2dd 100644 --- a/contracts/credit-manager/tests/tests/test_instantiate.rs +++ b/contracts/credit-manager/tests/tests/test_instantiate.rs @@ -65,3 +65,11 @@ fn params_set_on_instantiate() { fn raises_on_invalid_params_addr() { MockEnv::new().params("%%%INVALID%%%").build().unwrap(); } + +#[test] +#[should_panic] +fn raises_on_invalid_swap_fee() { + use cosmwasm_std::Decimal; + + MockEnv::new().swap_fee(Decimal::percent(100)).build().unwrap(); +} diff --git a/contracts/credit-manager/tests/tests/test_migration_v2.rs b/contracts/credit-manager/tests/tests/test_migration_v2_2_0.rs similarity index 59% rename from contracts/credit-manager/tests/tests/test_migration_v2.rs rename to contracts/credit-manager/tests/tests/test_migration_v2_2_0.rs index 28db67d18..21742f2ae 100644 --- a/contracts/credit-manager/tests/tests/test_migration_v2.rs +++ b/contracts/credit-manager/tests/tests/test_migration_v2_2_0.rs @@ -1,22 +1,25 @@ -use cosmwasm_std::{attr, testing::mock_env, Empty, Event}; +use cosmwasm_std::{attr, testing::mock_env, Decimal, Empty, Event}; use cw2::{ContractVersion, VersionError}; -use mars_credit_manager::{contract::migrate, error::ContractError}; +use mars_credit_manager::{contract::migrate, error::ContractError, state::SWAP_FEE}; use mars_testing::mock_dependencies; #[test] fn wrong_contract_name() { let mut deps = mock_dependencies(&[]); - cw2::set_contract_version(deps.as_mut().storage, "contract_xyz", "2.0.3").unwrap(); + cw2::set_contract_version(deps.as_mut().storage, "contract_xyz", "2.1.0").unwrap(); let err = migrate(deps.as_mut(), mock_env(), Empty {}).unwrap_err(); - assert_eq!( - err, + match err { ContractError::Version(VersionError::WrongContract { - expected: "crates.io:mars-credit-manager".to_string(), - found: "contract_xyz".to_string() - }) - ); + expected, + found, + }) => { + assert_eq!(expected, "crates.io:mars-credit-manager".to_string()); + assert_eq!(found, "contract_xyz".to_string()); + } + other => panic!("unexpected error: {other:?}"), + } } #[test] @@ -27,19 +30,21 @@ fn wrong_contract_version() { let err = migrate(deps.as_mut(), mock_env(), Empty {}).unwrap_err(); - assert_eq!( - err, + match err { ContractError::Version(VersionError::WrongVersion { - expected: "2.0.3".to_string(), - found: "4.1.0".to_string() - }) - ); + found, + .. + }) => { + assert_eq!(found, "4.1.0".to_string()); + } + other => panic!("unexpected error: {other:?}"), + } } #[test] -fn successful_migration() { +fn successful_migration_from_2_1_0() { let mut deps = mock_dependencies(&[]); - cw2::set_contract_version(deps.as_mut().storage, "crates.io:mars-credit-manager", "2.0.3") + cw2::set_contract_version(deps.as_mut().storage, "crates.io:mars-credit-manager", "2.1.0") .unwrap(); let res = migrate(deps.as_mut(), mock_env(), Empty {}).unwrap(); @@ -49,12 +54,16 @@ fn successful_migration() { assert!(res.data.is_none()); assert_eq!( res.attributes, - vec![attr("action", "migrate"), attr("from_version", "2.0.3"), attr("to_version", "2.1.0")] + vec![attr("action", "migrate"), attr("from_version", "2.1.0"), attr("to_version", "2.2.0")] ); let new_contract_version = ContractVersion { contract: "crates.io:mars-credit-manager".to_string(), - version: "2.1.0".to_string(), + version: "2.2.0".to_string(), }; assert_eq!(cw2::get_contract_version(deps.as_ref().storage).unwrap(), new_contract_version); + + // Ensure swap fee exists post-migration (zero by default if absent) + let swap_fee = SWAP_FEE.may_load(deps.as_ref().storage).unwrap().unwrap_or_else(Decimal::zero); + assert!(swap_fee <= Decimal::one()); } diff --git a/contracts/credit-manager/tests/tests/test_rewards_collector_whitelist.rs b/contracts/credit-manager/tests/tests/test_rewards_collector_whitelist.rs new file mode 100644 index 000000000..a0839f942 --- /dev/null +++ b/contracts/credit-manager/tests/tests/test_rewards_collector_whitelist.rs @@ -0,0 +1,64 @@ +use cosmwasm_std::{coin, Addr}; +use cw_multi_test::{BankSudo, Executor, SudoMsg}; +use mars_testing::multitest::helpers; +use mars_types::rewards_collector::{ + ExecuteMsg as RcExecuteMsg, UpdateConfig as RcUpdateConfig, WhitelistAction, +}; + +#[test] +fn rewards_collector_whitelist_enforced() { + let mut mock = helpers::MockEnv::new().build().unwrap(); + let config = mock.query_config(); + let rewards_collector_info = config.rewards_collector.expect("rewards collector configured"); + let rewards_collector_addr = Addr::unchecked(rewards_collector_info.address.clone()); + + // fund the rewards collector with uusdc so distribution can execute + mock.app + .sudo(SudoMsg::Bank(BankSudo::Mint { + to_address: rewards_collector_addr.to_string(), + amount: vec![coin(1_000, "uusdc")], + })) + .unwrap(); + + // non-whitelisted address cannot distribute rewards + assert!(mock + .app + .execute_contract( + Addr::unchecked("not_whitelisted"), + rewards_collector_addr.clone(), + &RcExecuteMsg::DistributeRewards { + denom: "uusdc".to_string(), + }, + &[], + ) + .is_err()); + + // whitelist alice + mock.app + .execute_contract( + Addr::unchecked("owner"), + rewards_collector_addr.clone(), + &RcExecuteMsg::UpdateConfig { + new_cfg: RcUpdateConfig { + whitelist_actions: Some(vec![WhitelistAction::AddAddress { + address: "alice".to_string(), + }]), + ..Default::default() + }, + }, + &[], + ) + .unwrap(); + + // whitelisted address succeeds + mock.app + .execute_contract( + Addr::unchecked("alice"), + rewards_collector_addr, + &RcExecuteMsg::DistributeRewards { + denom: "uusdc".to_string(), + }, + &[], + ) + .unwrap(); +} diff --git a/contracts/credit-manager/tests/tests/test_swap.rs b/contracts/credit-manager/tests/tests/test_swap.rs index c319c201e..c2dc7509b 100644 --- a/contracts/credit-manager/tests/tests/test_swap.rs +++ b/contracts/credit-manager/tests/tests/test_swap.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{coins, Addr, Coin, OverflowError, OverflowOperation::Sub, Uint128}; +use cosmwasm_std::{coins, Addr, Coin, Decimal, OverflowError, OverflowOperation::Sub, Uint128}; use mars_credit_manager::error::ContractError; use mars_swapper_mock::contract::MOCK_SWAP_RESULT; use mars_types::{ @@ -362,3 +362,70 @@ fn swap_success_with_amount_none() { assert_eq!(position.deposits.first().unwrap().denom, osmo_info.denom); assert_eq!(position.deposits.first().unwrap().amount, MOCK_SWAP_RESULT); } + +#[test] +fn swap_fee_deducted() { + let coin_in = uatom_info(); + let coin_out = uosmo_info(); + + let user = Addr::unchecked("user"); + let mut mock = MockEnv::new() + .fund_account(AccountToFund { + addr: user.clone(), + funds: coins(10_000, coin_in.denom.clone()), + }) + .set_params(&[coin_out.clone(), coin_in.clone()]) + .swap_fee(Decimal::percent(1)) + .build() + .unwrap(); + + let route = SwapperRoute::Osmo(OsmoRoute { + swaps: vec![OsmoSwap { + pool_id: 101, + to: coin_out.denom.clone(), + }], + }); + + let account_id = mock.create_credit_account(&user).unwrap(); + let res = mock + .update_credit_account( + &account_id, + &user, + vec![ + Deposit(coin_in.to_coin(10_000)), + SwapExactIn { + coin_in: coin_in.to_action_coin_full_balance(), + denom_out: coin_out.denom.clone(), + min_receive: MOCK_SWAP_RESULT - Uint128::one(), + route: Some(route), + }, + ], + &[coin_in.to_coin(10_000)], + ) + .unwrap(); + + // Prove we swapped amount in with the fee deducted + let event = &res.events[3]; + assert_eq!(event.attributes[0].key, "_contract_address"); + assert_eq!(event.attributes[0].value, "contract9"); + assert_eq!(event.attributes[1].key, "action"); + assert_eq!(event.attributes[1].value, "swapper"); + assert_eq!(event.attributes[2].key, "account_id"); + assert_eq!(event.attributes[2].value, "2"); + assert_eq!(event.attributes[3].key, "coin_in"); + assert_eq!(event.attributes[3].value, "9900uatom"); // 10_000 - (10_000 * 0.01) = 10_000 - 100 = 9_900 + assert_eq!(event.attributes[4].key, "denom_out"); + assert_eq!(event.attributes[4].value, "uosmo"); + + // assert rewards balance (rewards collector account id is created on deployment, so its #1) + let rewards_positions = mock.query_positions("1"); + assert_eq!(rewards_positions.deposits.len(), 1); + assert_eq!(rewards_positions.deposits.first().unwrap().denom, coin_in.denom); + assert_eq!(rewards_positions.deposits.first().unwrap().amount, Uint128::from(100u128)); + + // User balance will still be mock_swap_result because it is mocked + let user_positions = mock.query_positions(&account_id); + assert_eq!(user_positions.deposits.len(), 1); + assert_eq!(user_positions.deposits.first().unwrap().denom, coin_out.denom); + assert_eq!(user_positions.deposits.first().unwrap().amount, MOCK_SWAP_RESULT); +} diff --git a/contracts/credit-manager/tests/tests/test_update_config.rs b/contracts/credit-manager/tests/tests/test_update_config.rs index cabd900e2..d1da7b736 100644 --- a/contracts/credit-manager/tests/tests/test_update_config.rs +++ b/contracts/credit-manager/tests/tests/test_update_config.rs @@ -34,6 +34,7 @@ fn only_owner_can_update_config() { zapper: None, health_contract: None, rewards_collector: None, + swap_fee: None, }, ); @@ -91,6 +92,7 @@ fn update_config_works_with_full_config() { let new_swapper = SwapperBase::new("new_swapper".to_string()); let new_health_contract = HealthContractUnchecked::new("new_health_contract".to_string()); let new_rewards_collector = "rewards_collector_contract_new".to_string(); + let new_swap_fee = Decimal::percent(1); mock.update_config( &Addr::unchecked(original_config.ownership.owner.clone().unwrap()), @@ -105,6 +107,7 @@ fn update_config_works_with_full_config() { zapper: Some(new_zapper.clone()), health_contract: Some(new_health_contract.clone()), rewards_collector: Some(new_rewards_collector.clone()), + swap_fee: Some(new_swap_fee), }, ) .unwrap(); diff --git a/contracts/incentives/src/helpers.rs b/contracts/incentives/src/helpers.rs index 3171d5b25..ed278b768 100644 --- a/contracts/incentives/src/helpers.rs +++ b/contracts/incentives/src/helpers.rs @@ -59,7 +59,7 @@ impl MaybeMutStorage<'_> { /// - duration is a multiple of epoch duration /// - enough tokens are sent to cover the entire duration /// - start_time is a multiple of epoch duration away from any other existing incentive -/// for the same collateral denom and incentive denom tuple +/// for the same collateral denom and incentive denom tuple pub fn validate_incentive_schedule( storage: &dyn Storage, info: &MessageInfo, diff --git a/contracts/perps/tests/tests/helpers/mock_env.rs b/contracts/perps/tests/tests/helpers/mock_env.rs new file mode 100644 index 000000000..59a957367 --- /dev/null +++ b/contracts/perps/tests/tests/helpers/mock_env.rs @@ -0,0 +1,847 @@ +#![allow(dead_code)] +use std::mem::take; + +use anyhow::Result as AnyResult; +use cosmwasm_std::{coin, Addr, Coin, Decimal, Empty, Int128, Timestamp, Uint128}; +use cw_multi_test::{App, AppResponse, BankSudo, BasicApp, Executor, SudoMsg}; +use cw_paginate::PaginationResponse; +use mars_oracle_osmosis::OsmosisPriceSourceUnchecked; +use mars_owner::{OwnerResponse, OwnerUpdate}; +use mars_testing::integration::mock_contracts::mock_rewards_collector_osmosis_contract; +use mars_types::{ + address_provider::{self, MarsAddressType}, + incentives, + oracle::{self, ActionKind}, + params::{ + self, EmergencyUpdate, + ExecuteMsg::{self, UpdatePerpParams}, + PerpParams, PerpParamsUpdate, + }, + perps::{ + self, AccountingResponse, Config, ConfigUpdates, MarketResponse, MarketStateResponse, + PnlAmounts, PositionFeesResponse, PositionResponse, PositionsByAccountResponse, TradingFee, + VaultPositionResponse, VaultResponse, + }, + rewards_collector::{self, RewardConfig, TransferType}, +}; + +use super::{ + contracts::{mock_oracle_contract, mock_perps_contract}, + mock_address_provider_contract, mock_credit_manager_contract, mock_incentives_contract, + mock_params_contract, +}; + +pub const ONE_HOUR_SEC: u64 = 3600u64; + +pub struct MockEnv { + app: BasicApp, + pub owner: Addr, + pub perps: Addr, + pub oracle: Addr, + pub params: Addr, + pub credit_manager: Addr, + pub address_provider: Addr, + pub rewards_collector: Addr, +} + +pub struct MockEnvBuilder { + app: BasicApp, + deployer: Addr, + oracle_base_denom: String, + perps_base_denom: String, + cooldown_period: u64, + max_positions: u8, + protocol_fee_rate: Decimal, + pub address_provider: Option, + target_vault_collateralization_ratio: Decimal, + pub emergency_owner: Option, + deleverage_enabled: bool, + withdraw_enabled: bool, + max_unlocks: u8, +} + +#[allow(clippy::new_ret_no_self)] +impl MockEnv { + pub fn new() -> MockEnvBuilder { + MockEnvBuilder { + app: App::default(), + deployer: Addr::unchecked("deployer"), + oracle_base_denom: "uusd".to_string(), + perps_base_denom: "uusdc".to_string(), + cooldown_period: 3600, + max_positions: 4, + protocol_fee_rate: Decimal::percent(0), + address_provider: None, + target_vault_collateralization_ratio: Decimal::percent(125), + emergency_owner: None, + deleverage_enabled: true, + withdraw_enabled: true, + max_unlocks: 5, + } + } + + pub fn fund_accounts(&mut self, addrs: &[&Addr], amount: u128, denoms: &[&str]) { + for addr in addrs { + let coins: Vec<_> = denoms.iter().map(|&d| coin(amount, d)).collect(); + self.fund_account(addr, &coins); + } + } + + pub fn fund_account(&mut self, addr: &Addr, coins: &[Coin]) { + self.app + .sudo(SudoMsg::Bank(BankSudo::Mint { + to_address: addr.to_string(), + amount: coins.to_vec(), + })) + .unwrap(); + } + + pub fn query_balance(&self, addr: &Addr, denom: &str) -> Coin { + self.app.wrap().query_balance(addr.clone(), denom).unwrap() + } + + pub fn increment_by_blocks(&mut self, num_of_blocks: u64) { + self.app.update_block(|block| { + block.height += num_of_blocks; + // assume block time = 6 sec + block.time = block.time.plus_seconds(num_of_blocks * 6); + }) + } + + pub fn increment_by_time(&mut self, seconds: u64) { + self.app.update_block(|block| { + block.height += seconds / 6; + // assume block time = 6 sec + block.time = block.time.plus_seconds(seconds); + }) + } + + pub fn set_block_time(&mut self, seconds: u64) { + self.app.update_block(|block| { + block.time = Timestamp::from_seconds(seconds); + }) + } + + pub fn query_block_time(&self) -> u64 { + self.app.block_info().time.seconds() + } + + //-------------------------------------------------------------------------------------------------- + // Execute Msgs + //-------------------------------------------------------------------------------------------------- + + pub fn update_owner(&mut self, sender: &Addr, update: OwnerUpdate) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.perps.clone(), + &perps::ExecuteMsg::UpdateOwner(update), + &[], + ) + } + + pub fn update_config( + &mut self, + sender: &Addr, + updates: ConfigUpdates, + ) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.perps.clone(), + &perps::ExecuteMsg::UpdateConfig { + updates, + }, + &[], + ) + } + + pub fn deposit_to_vault( + &mut self, + sender: &Addr, + account_id: Option<&str>, + max_shares_receivable: Option, + funds: &[Coin], + ) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.perps.clone(), + &perps::ExecuteMsg::Deposit { + account_id: account_id.map(|s| s.to_string()), + max_shares_receivable, + }, + funds, + ) + } + + pub fn unlock_from_vault( + &mut self, + sender: &Addr, + account_id: Option<&str>, + shares: Uint128, + ) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.perps.clone(), + &perps::ExecuteMsg::Unlock { + account_id: account_id.map(|s| s.to_string()), + shares, + }, + &[], + ) + } + + pub fn withdraw_from_vault( + &mut self, + sender: &Addr, + account_id: Option<&str>, + min_receive: Option, + ) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.perps.clone(), + &perps::ExecuteMsg::Withdraw { + account_id: account_id.map(|s| s.to_string()), + min_receive, + }, + &[], + ) + } + + pub fn execute_perp_order( + &mut self, + sender: &Addr, + account_id: &str, + denom: &str, + size: Int128, + reduce_only: Option, + funds: &[Coin], + ) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.perps.clone(), + &perps::ExecuteMsg::ExecuteOrder { + account_id: account_id.to_string(), + denom: denom.to_string(), + size, + reduce_only, + }, + funds, + ) + } + + pub fn close_all_positions( + &mut self, + sender: &Addr, + account_id: &str, + funds: &[Coin], + ) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.perps.clone(), + &perps::ExecuteMsg::CloseAllPositions { + account_id: account_id.to_string(), + action: None, + }, + funds, + ) + } + + pub fn set_price( + &mut self, + sender: &Addr, + denom: &str, + price: Decimal, + ) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.oracle.clone(), + &oracle::ExecuteMsg::::SetPriceSource { + denom: denom.to_string(), + price_source: OsmosisPriceSourceUnchecked::Fixed { + price, + }, + }, + &[], + ) + } + + pub fn update_perp_params(&mut self, sender: &Addr, update: PerpParamsUpdate) { + self.app + .execute_contract(sender.clone(), self.params.clone(), &UpdatePerpParams(update), &[]) + .unwrap(); + } + + pub fn update_market(&mut self, sender: &Addr, params: PerpParams) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.perps.clone(), + &perps::ExecuteMsg::UpdateMarket { + params, + }, + &[], + ) + } + + pub fn emergency_params_update( + &mut self, + sender: &Addr, + update: EmergencyUpdate, + ) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.params.clone(), + &mars_types::params::ExecuteMsg::EmergencyUpdate(update), + &[], + ) + } + + //-------------------------------------------------------------------------------------------------- + // Queries + //-------------------------------------------------------------------------------------------------- + + pub fn query_owner(&self) -> Addr { + let res = self.query_ownership(); + Addr::unchecked(res.owner.unwrap()) + } + + pub fn query_ownership(&self) -> OwnerResponse { + self.app.wrap().query_wasm_smart(self.perps.clone(), &perps::QueryMsg::Owner {}).unwrap() + } + + pub fn query_config(&self) -> Config { + self.app.wrap().query_wasm_smart(self.perps.clone(), &perps::QueryMsg::Config {}).unwrap() + } + + pub fn query_vault(&self) -> VaultResponse { + self.app + .wrap() + .query_wasm_smart( + self.perps.clone(), + &perps::QueryMsg::Vault { + action: None, + }, + ) + .unwrap() + } + + pub fn query_market_state(&self, denom: &str) -> MarketStateResponse { + self.app + .wrap() + .query_wasm_smart( + self.perps.clone(), + &perps::QueryMsg::MarketState { + denom: denom.to_string(), + }, + ) + .unwrap() + } + + pub fn query_market(&self, denom: &str) -> MarketResponse { + self.app + .wrap() + .query_wasm_smart( + self.perps.clone(), + &perps::QueryMsg::Market { + denom: denom.to_string(), + }, + ) + .unwrap() + } + + pub fn query_markets( + &self, + start_after: Option, + limit: Option, + ) -> PaginationResponse { + self.app + .wrap() + .query_wasm_smart( + self.perps.clone(), + &perps::QueryMsg::Markets { + start_after, + limit, + }, + ) + .unwrap() + } + + pub fn query_cm_vault_position(&self, account_id: &str) -> Option { + self.query_vault_position(self.credit_manager.as_str(), Some(account_id)) + } + + pub fn query_vault_position( + &self, + user_address: &str, + account_id: Option<&str>, + ) -> Option { + self.app + .wrap() + .query_wasm_smart( + self.perps.clone(), + &perps::QueryMsg::VaultPosition { + user_address: user_address.to_string(), + account_id: account_id.map(|s| s.to_string()), + }, + ) + .unwrap() + } + + pub fn query_position(&self, account_id: &str, denom: &str) -> PositionResponse { + self.query_position_with_order_size(account_id, denom, None) + } + + pub fn query_position_with_order_size( + &self, + account_id: &str, + denom: &str, + order_size: Option, + ) -> PositionResponse { + self.app + .wrap() + .query_wasm_smart( + self.perps.clone(), + &perps::QueryMsg::Position { + account_id: account_id.to_string(), + denom: denom.to_string(), + order_size, + reduce_only: None, + }, + ) + .unwrap() + } + + pub fn query_positions( + &self, + start_after: Option<(String, String)>, + limit: Option, + ) -> Vec { + self.app + .wrap() + .query_wasm_smart( + self.perps.clone(), + &perps::QueryMsg::Positions { + start_after, + limit, + }, + ) + .unwrap() + } + + pub fn query_positions_by_account_id( + &self, + account_id: &str, + action: ActionKind, + ) -> PositionsByAccountResponse { + self.app + .wrap() + .query_wasm_smart( + self.perps.clone(), + &perps::QueryMsg::PositionsByAccount { + account_id: account_id.to_string(), + action: Some(action), + }, + ) + .unwrap() + } + + pub fn query_market_accounting(&self, denom: &str) -> AccountingResponse { + self.app + .wrap() + .query_wasm_smart( + self.perps.clone(), + &perps::QueryMsg::MarketAccounting { + denom: denom.to_string(), + }, + ) + .unwrap() + } + + pub fn query_total_accounting(&self) -> AccountingResponse { + self.app + .wrap() + .query_wasm_smart(self.perps.clone(), &perps::QueryMsg::TotalAccounting {}) + .unwrap() + } + + pub fn query_realized_pnl_by_account_and_market( + &self, + account_id: &str, + denom: &str, + ) -> PnlAmounts { + self.app + .wrap() + .query_wasm_smart( + self.perps.clone(), + &perps::QueryMsg::RealizedPnlByAccountAndMarket { + account_id: account_id.to_string(), + denom: denom.to_string(), + }, + ) + .unwrap() + } + + pub fn query_opening_fee(&self, denom: &str, size: Int128) -> TradingFee { + self.app + .wrap() + .query_wasm_smart( + self.perps.clone(), + &perps::QueryMsg::OpeningFee { + denom: denom.to_string(), + size, + }, + ) + .unwrap() + } + + pub fn query_position_fees( + &self, + account_id: &str, + denom: &str, + new_size: Int128, + ) -> PositionFeesResponse { + self.app + .wrap() + .query_wasm_smart( + self.perps.clone(), + &perps::QueryMsg::PositionFees { + account_id: account_id.to_string(), + denom: denom.to_string(), + new_size, + }, + ) + .unwrap() + } + + pub fn query_perp_params(&self, denom: &str) -> PerpParams { + self.app + .wrap() + .query_wasm_smart( + self.params.clone(), + ¶ms::QueryMsg::PerpParams { + denom: denom.to_string(), + }, + ) + .unwrap() + } +} + +impl MockEnvBuilder { + pub fn build(&mut self) -> AnyResult { + let address_provider_contract = self.get_address_provider(); + let oracle_contract = self.deploy_oracle(); + let params_contract = self.deploy_params(address_provider_contract.as_str()); + let credit_manager_contract = self.deploy_credit_manager(); + let rewards_collector_contract = + self.deploy_rewards_collector(address_provider_contract.as_str()); + let perps_contract = self.deploy_perps(address_provider_contract.as_str()); + let incentives_contract = self.deploy_incentives(&address_provider_contract); + + self.update_address_provider( + &address_provider_contract, + MarsAddressType::Incentives, + &incentives_contract, + ); + self.update_address_provider( + &address_provider_contract, + MarsAddressType::Perps, + &perps_contract, + ); + + if self.emergency_owner.is_some() { + self.set_emergency_owner(¶ms_contract, &self.emergency_owner.clone().unwrap()); + } + + Ok(MockEnv { + app: take(&mut self.app), + owner: self.deployer.clone(), + perps: perps_contract, + oracle: oracle_contract, + params: params_contract, + credit_manager: credit_manager_contract, + address_provider: address_provider_contract, + rewards_collector: rewards_collector_contract, + }) + } + + fn deploy_address_provider(&mut self) -> Addr { + let contract = mock_address_provider_contract(); + let code_id = self.app.store_code(contract); + + self.app + .instantiate_contract( + code_id, + self.deployer.clone(), + &address_provider::InstantiateMsg { + owner: self.deployer.clone().to_string(), + prefix: "".to_string(), + }, + &[], + "mock-address-provider", + None, + ) + .unwrap() + } + + fn deploy_oracle(&mut self) -> Addr { + let contract = mock_oracle_contract(); + let code_id = self.app.store_code(contract); + + let addr = self + .app + .instantiate_contract( + code_id, + self.deployer.clone(), + &oracle::InstantiateMsg:: { + owner: self.deployer.clone().to_string(), + base_denom: self.oracle_base_denom.clone(), + custom_init: None, + }, + &[], + "mock-oracle", + None, + ) + .unwrap(); + + self.set_address(MarsAddressType::Oracle, addr.clone()); + + addr + } + + fn deploy_params(&mut self, address_provider: &str) -> Addr { + let contract = mock_params_contract(); + let code_id = self.app.store_code(contract); + + let addr = self + .app + .instantiate_contract( + code_id, + self.deployer.clone(), + ¶ms::InstantiateMsg { + owner: self.deployer.clone().to_string(), + risk_manager: None, + address_provider: address_provider.to_string(), + max_perp_params: 40, + }, + &[], + "mock-params", + None, + ) + .unwrap(); + + self.set_address(MarsAddressType::Params, addr.clone()); + + addr + } + + fn deploy_incentives(&mut self, address_provider_addr: &Addr) -> Addr { + let code_id = self.app.store_code(mock_incentives_contract()); + + self.app + .instantiate_contract( + code_id, + self.deployer.clone(), + &incentives::InstantiateMsg { + owner: self.deployer.to_string(), + address_provider: address_provider_addr.to_string(), + epoch_duration: 604800, + max_whitelisted_denoms: 10, + }, + &[], + "incentives", + None, + ) + .unwrap() + } + + fn deploy_perps(&mut self, address_provider: &str) -> Addr { + let code_id = self.app.store_code(mock_perps_contract()); + + self.app + .instantiate_contract( + code_id, + self.deployer.clone(), + &perps::InstantiateMsg { + address_provider: address_provider.to_string(), + base_denom: self.perps_base_denom.clone(), + cooldown_period: self.cooldown_period, + max_positions: self.max_positions, + protocol_fee_rate: self.protocol_fee_rate, + target_vault_collateralization_ratio: self.target_vault_collateralization_ratio, + deleverage_enabled: self.deleverage_enabled, + vault_withdraw_enabled: self.withdraw_enabled, + max_unlocks: self.max_unlocks, + }, + &[], + "mock-perps", + None, + ) + .unwrap() + } + + fn deploy_credit_manager(&mut self) -> Addr { + let contract = mock_credit_manager_contract(); + let code_id = self.app.store_code(contract); + + let addr = self + .app + .instantiate_contract( + code_id, + self.deployer.clone(), + &Empty {}, + &[], + "mock-credit-manager", + None, + ) + .unwrap(); + + self.set_address(MarsAddressType::CreditManager, addr.clone()); + + addr + } + + fn deploy_rewards_collector(&mut self, address_provider: &str) -> Addr { + let contract = mock_rewards_collector_osmosis_contract(); + let code_id = self.app.store_code(contract); + + let addr = self + .app + .instantiate_contract( + code_id, + self.deployer.clone(), + &rewards_collector::InstantiateMsg { + owner: self.deployer.clone().to_string(), + address_provider: address_provider.to_string(), + safety_tax_rate: Default::default(), + revenue_share_tax_rate: Default::default(), + safety_fund_config: RewardConfig { + target_denom: "uusdc".to_string(), + transfer_type: TransferType::Bank, + }, + revenue_share_config: RewardConfig { + target_denom: "uusdc".to_string(), + transfer_type: TransferType::Bank, + }, + fee_collector_config: RewardConfig { + target_denom: "umars".to_string(), + transfer_type: TransferType::Ibc, + }, + channel_id: "".to_string(), + timeout_seconds: 1, + whitelisted_distributors: vec![], + }, + &[], + "mock-rewards-collector", + None, + ) + .unwrap(); + + self.set_address(MarsAddressType::RewardsCollector, addr.clone()); + + addr + } + + fn set_address(&mut self, address_type: MarsAddressType, address: Addr) { + let address_provider_addr = self.get_address_provider(); + + self.app + .execute_contract( + self.deployer.clone(), + address_provider_addr, + &address_provider::ExecuteMsg::SetAddress { + address_type, + address: address.into(), + }, + &[], + ) + .unwrap(); + } + + fn get_address_provider(&mut self) -> Addr { + if self.address_provider.is_none() { + let addr = self.deploy_address_provider(); + + self.address_provider = Some(addr); + } + self.address_provider.clone().unwrap() + } + + fn update_address_provider( + &mut self, + address_provider_addr: &Addr, + address_type: MarsAddressType, + addr: &Addr, + ) { + self.app + .execute_contract( + self.deployer.clone(), + address_provider_addr.clone(), + &address_provider::ExecuteMsg::SetAddress { + address_type, + address: addr.to_string(), + }, + &[], + ) + .unwrap(); + } + + fn set_emergency_owner(&mut self, params_contract: &Addr, eo: &str) { + self.app + .execute_contract( + self.deployer.clone(), + params_contract.clone(), + &ExecuteMsg::UpdateOwner(OwnerUpdate::SetEmergencyOwner { + emergency_owner: eo.to_string(), + }), + &[], + ) + .unwrap(); + } + + //-------------------------------------------------------------------------------------------------- + // Setter functions + //-------------------------------------------------------------------------------------------------- + + pub fn oracle_base_denom(&mut self, denom: &str) -> &mut Self { + self.oracle_base_denom = denom.to_string(); + self + } + + pub fn perps_base_denom(&mut self, denom: &str) -> &mut Self { + self.perps_base_denom = denom.to_string(); + self + } + + pub fn cooldown_period(&mut self, cp: u64) -> &mut Self { + self.cooldown_period = cp; + self + } + + pub fn max_positions(&mut self, max_positions: u8) -> &mut Self { + self.max_positions = max_positions; + self + } + + pub fn protocol_fee_rate(&mut self, rate: Decimal) -> &mut Self { + self.protocol_fee_rate = rate; + self + } + + pub fn target_vault_collaterization_ratio(&mut self, ratio: Decimal) -> &mut Self { + self.target_vault_collateralization_ratio = ratio; + self + } + + pub fn withdraw_enabled(&mut self, enabled: bool) -> &mut Self { + self.withdraw_enabled = enabled; + self + } + + pub fn emergency_owner(&mut self, eo: &str) -> &mut Self { + self.emergency_owner = Some(eo.to_string()); + self + } + + pub fn max_unlocks(&mut self, max_unlocks: u8) -> &mut Self { + self.max_unlocks = max_unlocks; + self + } +} diff --git a/contracts/red-bank/src/interest_rates.rs b/contracts/red-bank/src/interest_rates.rs index 3d65c6774..1a0b39b31 100644 --- a/contracts/red-bank/src/interest_rates.rs +++ b/contracts/red-bank/src/interest_rates.rs @@ -15,6 +15,7 @@ use crate::{error::ContractError, user::User}; /// 1. Updates market borrow and liquidity indices. /// 2. If there are any protocol rewards, builds a mint to the rewards collector and adds it /// to the returned response +/// /// NOTE: it does not save the market to store /// WARNING: For a given block, this function should be called before updating interest rates /// as it would apply the new interest rates instead of the ones that were valid during diff --git a/contracts/rewards-collector/base/src/contract.rs b/contracts/rewards-collector/base/src/contract.rs index 9845be597..9e177c0d5 100644 --- a/contracts/rewards-collector/base/src/contract.rs +++ b/contracts/rewards-collector/base/src/contract.rs @@ -7,9 +7,7 @@ use mars_owner::{Owner, OwnerInit::SetInitialOwner, OwnerUpdate}; use mars_types::{ address_provider::{self, AddressResponseItem, MarsAddressType}, credit_manager::{self, Action}, - incentives, - oracle::ActionKind, - red_bank, + incentives, red_bank, rewards_collector::{ Config, ConfigResponse, ExecuteMsg, InstantiateMsg, QueryMsg, UpdateConfig, }, @@ -18,7 +16,7 @@ use mars_types::{ use mars_utils::helpers::option_string_to_addr; use crate::{ - helpers::{stringify_option_amount, unwrap_option_amount}, + helpers::{ensure_distributor_whitelisted, stringify_option_amount, unwrap_option_amount}, ContractError, ContractResult, TransferMsg, }; @@ -96,7 +94,7 @@ where } => self.withdraw_from_credit_manager(deps, account_id, actions), ExecuteMsg::DistributeRewards { denom, - } => self.distribute_rewards(deps, &env, &denom), + } => self.distribute_rewards(deps, &env, &denom, info.sender), ExecuteMsg::SwapAsset { denom, amount, @@ -108,6 +106,7 @@ where deps, env, &denom, + info.sender, amount, safety_fund_route, fee_collector_route, @@ -161,7 +160,7 @@ where fee_collector_config, channel_id, timeout_seconds, - slippage_tolerance, + whitelist_actions, } = new_cfg; cfg.address_provider = @@ -173,7 +172,34 @@ where cfg.fee_collector_config = fee_collector_config.unwrap_or(cfg.fee_collector_config); cfg.channel_id = channel_id.unwrap_or(cfg.channel_id); cfg.timeout_seconds = timeout_seconds.unwrap_or(cfg.timeout_seconds); - cfg.slippage_tolerance = slippage_tolerance.unwrap_or(cfg.slippage_tolerance); + + // Process whitelist actions if provided + if let Some(actions) = whitelist_actions { + for action in actions { + match action { + mars_types::rewards_collector::WhitelistAction::AddAddress { + address, + } => { + // Validate the address + let validated_addr = deps.api.addr_validate(&address)?; + + // Only add if not already in the list + if !cfg.whitelisted_distributors.contains(&validated_addr) { + cfg.whitelisted_distributors.push(validated_addr); + } + } + mars_types::rewards_collector::WhitelistAction::RemoveAddress { + address, + } => { + // Validate the address for consistency + let validated_addr = deps.api.addr_validate(&address)?; + + // Remove the address if it exists in the list + cfg.whitelisted_distributors.retain(|addr| addr != validated_addr); + } + } + } + } cfg.validate()?; @@ -289,6 +315,7 @@ where deps: DepsMut, env: Env, denom: &str, + sender: Addr, amount: Option, safety_fund_route: Option, fee_collector_route: Option, @@ -296,6 +323,7 @@ where fee_collector_min_receive: Option, ) -> ContractResult> { let cfg = self.config.load(deps.storage)?; + ensure_distributor_whitelisted(deps.as_ref(), &cfg, &self.owner, &sender)?; // if amount is None, swap the total balance let amount_to_swap = @@ -317,42 +345,22 @@ where )?; let swapper_addr = &addresses[0].address; - let oracle_addr = &addresses[1].address; - - let asset_in_price = deps - .querier - .query_wasm_smart::( - oracle_addr.to_string(), - &mars_types::oracle::QueryMsg::Price { - denom: denom.to_string(), - kind: Some(ActionKind::Default), - }, - )? - .price; - - // apply slippage to asset in price. Creating this variable means we only need to apply - // slippage tolerance calculation once, instead of for each denom - let slippage_adjusted_asset_in_price = - asset_in_price.checked_mul(Decimal::one().checked_sub(cfg.slippage_tolerance)?)?; // execute the swap to safety fund denom, if the amount to swap is non-zero, // and if the denom is not already the safety fund denom // Note that revenue share is included in this swap as they are the same denom if !rf_and_sf_combined.is_zero() && denom != cfg.safety_fund_config.target_denom { - let swap_msg = self.swap_asset_to_reward( - &deps, - oracle_addr, - &denom.to_string(), + let swap_msg = self.generate_swap_msg( + swapper_addr, + denom, rf_and_sf_combined, - slippage_adjusted_asset_in_price, + &cfg.safety_fund_config.target_denom, safety_fund_min_receive.ok_or( ContractError::InvalidMinReceive { reason: "required to pass 'safety_fund_min_receive' when swapping safety fund amount".to_string() } )?, - &cfg.safety_fund_config.target_denom, safety_fund_route, - swapper_addr, )?; messages.push(swap_msg); @@ -361,20 +369,17 @@ where // execute the swap to fee collector denom, if the amount to swap is non-zero, // and if the denom is not already the fee collector denom if !fc_amount.is_zero() && denom != cfg.fee_collector_config.target_denom { - let swap_msg = self.swap_asset_to_reward( - &deps, - oracle_addr, - &denom.to_string(), + let swap_msg = self.generate_swap_msg( + swapper_addr, + denom, fc_amount, - slippage_adjusted_asset_in_price, + &cfg.fee_collector_config.target_denom, fee_collector_min_receive.ok_or( ContractError::InvalidMinReceive { reason: "required to pass 'fee_collector_min_receive' when swapping to fee collector".to_string() } )?, - &cfg.fee_collector_config.target_denom, fee_collector_route, - swapper_addr, )?; messages.push(swap_msg); @@ -388,48 +393,6 @@ where .add_attribute("amount_fee_collector", fc_amount)) } - fn swap_asset_to_reward( - &self, - deps: &DepsMut, - oracle_addr: &str, - asset_in_denom: &String, - asset_in_amount: Uint128, - slippage_adjusted_asset_in_price: Decimal, - min_receive: Uint128, - target_reward_denom: &String, - target_route: Option, - swapper_addr: &str, - ) -> Result { - let target_fund_price = deps - .querier - .query_wasm_smart::( - oracle_addr.to_string(), - &mars_types::oracle::QueryMsg::Price { - denom: target_reward_denom.to_string(), - kind: Some(ActionKind::Default), - }, - )? - .price; - - self.ensure_min_receive_within_slippage_tolerance( - asset_in_denom.to_string(), - target_reward_denom.to_string(), - asset_in_amount, - slippage_adjusted_asset_in_price, - target_fund_price, - min_receive, - )?; - - self.generate_swap_msg( - swapper_addr, - asset_in_denom, - asset_in_amount, - target_reward_denom, - min_receive, - target_route, - ) - } - fn generate_swap_msg( &self, swapper_addr: &str, @@ -451,50 +414,20 @@ where }) } - /// Ensure the slippage is not greater than what is tolerated in contract config - /// We do this by calculating the minimum_price and applying that to the min receive - /// Calculation is as follows: - /// Safety_denom price in oracle is 2 - /// slippage_adjusted_asset_in_price (calculated via oracle) is 9.5 - /// pair price = 9.5 / 2 = 4.75 - /// minimum_tolerated = 4.75 * amount_in - fn ensure_min_receive_within_slippage_tolerance( - &self, - asset_in_denom: String, - asset_out_denom: String, - amount_in: Uint128, - slippage_adjusted_asset_in_price: Decimal, - asset_out_price: Decimal, - min_receive: Uint128, - ) -> Result<(), ContractError> { - // The price of the asset to be swapped, denominated in the output asset denom - let asset_out_denominated_price = - slippage_adjusted_asset_in_price.checked_div(asset_out_price)?; - let min_receive_lower_limit = amount_in.checked_mul_floor(asset_out_denominated_price)?; - - if min_receive_lower_limit > min_receive { - return Err(ContractError::SlippageLimitExceeded { - denom_in: asset_in_denom, - denom_out: asset_out_denom, - min_receive_minimum: min_receive_lower_limit, - min_receive_given: min_receive, - }); - } - - Ok(()) - } - pub fn distribute_rewards( &self, deps: DepsMut, env: &Env, denom: &str, + sender: Addr, ) -> ContractResult> { let mut res = Response::new().add_attribute("action", "distribute_rewards"); let mut msgs: Vec> = vec![]; // Configs let cfg = &self.config.load(deps.storage)?; + ensure_distributor_whitelisted(deps.as_ref(), cfg, &self.owner, &sender)?; + let safety_fund_config = &cfg.safety_fund_config; let revenue_share_config = &cfg.revenue_share_config; let fee_collector_config = &cfg.fee_collector_config; @@ -612,7 +545,11 @@ where fee_collector_config: cfg.fee_collector_config, channel_id: cfg.channel_id, timeout_seconds: cfg.timeout_seconds, - slippage_tolerance: cfg.slippage_tolerance, + whitelisted_distributors: cfg + .whitelisted_distributors + .iter() + .map(|addr| addr.to_string()) + .collect(), }) } } diff --git a/contracts/rewards-collector/base/src/error.rs b/contracts/rewards-collector/base/src/error.rs index 71f9c16f8..56bc68f96 100644 --- a/contracts/rewards-collector/base/src/error.rs +++ b/contracts/rewards-collector/base/src/error.rs @@ -72,6 +72,11 @@ pub enum ContractError { UnsupportedTransferType { transfer_type: String, }, + + #[error("Unauthorized: sender {sender} is not allowed to distribute rewards")] + UnauthorizedDistributor { + sender: String, + }, } pub type ContractResult = Result; diff --git a/contracts/rewards-collector/base/src/helpers.rs b/contracts/rewards-collector/base/src/helpers.rs index e0997c024..e9f7fa29f 100644 --- a/contracts/rewards-collector/base/src/helpers.rs +++ b/contracts/rewards-collector/base/src/helpers.rs @@ -1,4 +1,6 @@ -use cosmwasm_std::{Addr, QuerierWrapper, Uint128}; +use cosmwasm_std::{Addr, Deps, QuerierWrapper, Uint128}; +use mars_owner::Owner; +use mars_types::rewards_collector::Config; use crate::{ContractError, ContractResult}; @@ -25,6 +27,26 @@ pub(crate) fn unwrap_option_amount( } } +pub(crate) fn ensure_distributor_whitelisted( + deps: Deps, + cfg: &Config, + owner: &Owner, + sender: &Addr, +) -> ContractResult<()> { + // Owner can always distribute rewards + if owner.is_owner(deps.storage, sender)? { + return Ok(()); + } + + if cfg.whitelisted_distributors.is_empty() || !cfg.whitelisted_distributors.contains(sender) { + return Err(ContractError::UnauthorizedDistributor { + sender: sender.to_string(), + }); + } + + Ok(()) +} + /// Convert an optional Uint128 amount to string. If the amount is undefined, return `undefined` pub(crate) fn stringify_option_amount(amount: Option) -> String { amount.map_or_else(|| "undefined".to_string(), |amount| amount.to_string()) diff --git a/contracts/rewards-collector/osmosis/Cargo.toml b/contracts/rewards-collector/osmosis/Cargo.toml index 38c1a92b4..fd7c049e3 100644 --- a/contracts/rewards-collector/osmosis/Cargo.toml +++ b/contracts/rewards-collector/osmosis/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mars-rewards-collector-osmosis" -version = "2.1.1" +version = "2.2.0" authors = { workspace = true } edition = { workspace = true } license = { workspace = true } diff --git a/contracts/rewards-collector/osmosis/src/lib.rs b/contracts/rewards-collector/osmosis/src/lib.rs index f580b36d1..5f22dc500 100644 --- a/contracts/rewards-collector/osmosis/src/lib.rs +++ b/contracts/rewards-collector/osmosis/src/lib.rs @@ -78,6 +78,6 @@ pub mod entry { #[entry_point] pub fn migrate(deps: DepsMut, _env: Env, _msg: Empty) -> Result { - migrations::v2_1_1::migrate(deps) + migrations::v2_2_0::migrate(deps) } } diff --git a/contracts/rewards-collector/osmosis/src/migrations/mod.rs b/contracts/rewards-collector/osmosis/src/migrations/mod.rs index dfd90f0b6..d82c4436e 100644 --- a/contracts/rewards-collector/osmosis/src/migrations/mod.rs +++ b/contracts/rewards-collector/osmosis/src/migrations/mod.rs @@ -1,2 +1,3 @@ pub mod v2_1_0; pub mod v2_1_1; +pub mod v2_2_0; diff --git a/contracts/rewards-collector/osmosis/src/migrations/v2_1_0.rs b/contracts/rewards-collector/osmosis/src/migrations/v2_1_0.rs index 0e6cedc46..0f89338c5 100644 --- a/contracts/rewards-collector/osmosis/src/migrations/v2_1_0.rs +++ b/contracts/rewards-collector/osmosis/src/migrations/v2_1_0.rs @@ -2,18 +2,19 @@ use cosmwasm_std::{DepsMut, Response}; use cw2::{assert_contract_version, set_contract_version}; use mars_rewards_collector_base::ContractError; -use crate::entry::{CONTRACT_NAME, CONTRACT_VERSION}; +use crate::entry::CONTRACT_NAME; const FROM_VERSION: &str = "2.0.1"; +const TO_VERSION: &str = "2.1.0"; pub fn migrate(deps: DepsMut) -> Result { // make sure we're migrating the correct contract and from the correct version assert_contract_version(deps.storage, &format!("crates.io:{CONTRACT_NAME}"), FROM_VERSION)?; - set_contract_version(deps.storage, format!("crates.io:{CONTRACT_NAME}"), CONTRACT_VERSION)?; + set_contract_version(deps.storage, format!("crates.io:{CONTRACT_NAME}"), TO_VERSION)?; Ok(Response::new() .add_attribute("action", "migrate") .add_attribute("from_version", FROM_VERSION) - .add_attribute("to_version", CONTRACT_VERSION)) + .add_attribute("to_version", TO_VERSION)) } diff --git a/contracts/rewards-collector/osmosis/src/migrations/v2_1_1.rs b/contracts/rewards-collector/osmosis/src/migrations/v2_1_1.rs index 09538c267..28c2dd0c3 100644 --- a/contracts/rewards-collector/osmosis/src/migrations/v2_1_1.rs +++ b/contracts/rewards-collector/osmosis/src/migrations/v2_1_1.rs @@ -3,10 +3,7 @@ use cw2::{assert_contract_version, set_contract_version}; use mars_rewards_collector_base::ContractError; use mars_types::rewards_collector::{Config, RewardConfig, TransferType}; -use crate::{ - entry::{CONTRACT_NAME, CONTRACT_VERSION}, - OsmosisCollector, -}; +use crate::{entry::CONTRACT_NAME, OsmosisCollector}; pub mod previous_state { use cosmwasm_schema::cw_serde; @@ -44,6 +41,7 @@ pub mod previous_state { } const FROM_VERSION: &str = "2.1.0"; +const TO_VERSION: &str = "2.1.1"; pub fn migrate(deps: DepsMut) -> Result { let storage: &mut dyn Storage = deps.storage; @@ -59,7 +57,6 @@ pub fn migrate(deps: DepsMut) -> Result { let new_config = Config { // old, unchanged values address_provider: old_config.address_provider, - slippage_tolerance: old_config.slippage_tolerance, timeout_seconds: old_config.timeout_seconds, // source channel on osmosis-1 for neutron-1 is channel-874. Proof below @@ -90,6 +87,8 @@ pub fn migrate(deps: DepsMut) -> Result { target_denom: old_config.fee_collector_denom, transfer_type: TransferType::Ibc, }, + // empty initially + whitelisted_distributors: vec![], }; // ensure our new config is legal @@ -98,10 +97,10 @@ pub fn migrate(deps: DepsMut) -> Result { let collector = OsmosisCollector::default(); collector.config.save(storage, &new_config)?; - set_contract_version(deps.storage, format!("crates.io:{CONTRACT_NAME}"), CONTRACT_VERSION)?; + set_contract_version(deps.storage, format!("crates.io:{CONTRACT_NAME}"), TO_VERSION)?; Ok(Response::new() .add_attribute("action", "migrate") .add_attribute("from_version", FROM_VERSION) - .add_attribute("to_version", CONTRACT_VERSION)) + .add_attribute("to_version", TO_VERSION)) } diff --git a/contracts/rewards-collector/osmosis/src/migrations/v2_2_0.rs b/contracts/rewards-collector/osmosis/src/migrations/v2_2_0.rs new file mode 100644 index 000000000..51da486d3 --- /dev/null +++ b/contracts/rewards-collector/osmosis/src/migrations/v2_2_0.rs @@ -0,0 +1,81 @@ +use cosmwasm_std::{DepsMut, Response, Storage}; +use cw2::{assert_contract_version, get_contract_version, set_contract_version, VersionError}; +use mars_rewards_collector_base::ContractError; +use mars_types::rewards_collector::Config; + +use crate::{ + entry::{CONTRACT_NAME, CONTRACT_VERSION}, + OsmosisCollector, +}; + +mod previous_state { + use cosmwasm_schema::cw_serde; + use cosmwasm_std::{Addr, Decimal}; + use cw_storage_plus::Item; + use mars_types::rewards_collector::RewardConfig; + + #[cw_serde] + pub struct Config { + pub address_provider: Addr, + pub safety_tax_rate: Decimal, + pub revenue_share_tax_rate: Decimal, + pub slippage_tolerance: Decimal, + pub safety_fund_config: RewardConfig, + pub revenue_share_config: RewardConfig, + pub fee_collector_config: RewardConfig, + pub channel_id: String, + pub timeout_seconds: u64, + } + + pub const CONFIG: Item = Item::new("config"); +} + +const FROM_VERSION: &str = "2.1.1"; + +pub fn migrate(deps: DepsMut) -> Result { + let contract = format!("crates.io:{CONTRACT_NAME}"); + let version = get_contract_version(deps.storage)?; + + if version.contract != contract { + return Err(ContractError::Version(VersionError::WrongContract { + expected: contract, + found: version.contract, + })); + } + + if version.version != FROM_VERSION { + return Err(ContractError::Version(VersionError::WrongVersion { + expected: FROM_VERSION.to_string(), + found: version.version, + })); + } + + assert_contract_version(deps.storage, &contract, FROM_VERSION)?; + + let storage: &mut dyn Storage = deps.storage; + let collector = OsmosisCollector::default(); + + let old_config = previous_state::CONFIG.load(storage)?; + + let new_config = Config { + address_provider: old_config.address_provider, + safety_tax_rate: old_config.safety_tax_rate, + revenue_share_tax_rate: old_config.revenue_share_tax_rate, + safety_fund_config: old_config.safety_fund_config, + revenue_share_config: old_config.revenue_share_config, + fee_collector_config: old_config.fee_collector_config, + channel_id: old_config.channel_id, + timeout_seconds: old_config.timeout_seconds, + whitelisted_distributors: vec![], + }; + + new_config.validate()?; + collector.config.save(storage, &new_config)?; + + set_contract_version(deps.storage, contract, CONTRACT_VERSION)?; + + Ok(Response::new() + .add_attribute("action", "migrate") + .add_attribute("from_version", FROM_VERSION) + .add_attribute("to_version", CONTRACT_VERSION)) +} diff --git a/contracts/rewards-collector/osmosis/tests/tests/helpers/mod.rs b/contracts/rewards-collector/osmosis/tests/tests/helpers/mod.rs index d764ff34f..a6e73fde8 100644 --- a/contracts/rewards-collector/osmosis/tests/tests/helpers/mod.rs +++ b/contracts/rewards-collector/osmosis/tests/tests/helpers/mod.rs @@ -31,7 +31,7 @@ pub fn mock_instantiate_msg() -> InstantiateMsg { }, channel_id: "channel-69".to_string(), timeout_seconds: 300, - slippage_tolerance: Decimal::percent(3), + whitelisted_distributors: vec!["owner".to_string(), "jake".to_string()], } } diff --git a/contracts/rewards-collector/osmosis/tests/tests/mod.rs b/contracts/rewards-collector/osmosis/tests/tests/mod.rs index 8f39526ac..3d17ad33c 100644 --- a/contracts/rewards-collector/osmosis/tests/tests/mod.rs +++ b/contracts/rewards-collector/osmosis/tests/tests/mod.rs @@ -2,7 +2,8 @@ mod helpers; mod test_admin; mod test_distribute_rewards; -mod test_migration_v2_1_0_to_v2_1_1; +mod test_migration_v2_2_0; mod test_swap; mod test_update_owner; +mod test_whitelist_distributors; mod test_withdraw; diff --git a/contracts/rewards-collector/osmosis/tests/tests/test_admin.rs b/contracts/rewards-collector/osmosis/tests/tests/test_admin.rs index e7fb8581d..6be8db417 100644 --- a/contracts/rewards-collector/osmosis/tests/tests/test_admin.rs +++ b/contracts/rewards-collector/osmosis/tests/tests/test_admin.rs @@ -33,7 +33,7 @@ fn instantiating() { fee_collector_config: config.fee_collector_config, channel_id: config.channel_id, timeout_seconds: config.timeout_seconds, - slippage_tolerance: config.slippage_tolerance, + whitelisted_distributors: vec!["owner".to_string(), "jake".to_string()], } ); @@ -52,30 +52,6 @@ fn instantiating() { ); } -#[test] -fn updating_config_if_invalid_slippage() { - let mut deps = helpers::setup_test(); - - let invalid_cfg = UpdateConfig { - slippage_tolerance: Some(Decimal::percent(51u64)), - ..Default::default() - }; - - let info = mock_info("owner"); - let msg = ExecuteMsg::UpdateConfig { - new_cfg: invalid_cfg, - }; - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!( - err, - ContractError::Validation(ValidationError::InvalidParam { - param_name: "slippage_tolerance".to_string(), - invalid_value: "0.51".to_string(), - predicate: "<= 0.5".to_string(), - }) - ); -} - #[test] fn updating_config() { let mut deps = helpers::setup_test(); diff --git a/contracts/rewards-collector/osmosis/tests/tests/test_distribute_rewards.rs b/contracts/rewards-collector/osmosis/tests/tests/test_distribute_rewards.rs index b7e30e75e..273260406 100644 --- a/contracts/rewards-collector/osmosis/tests/tests/test_distribute_rewards.rs +++ b/contracts/rewards-collector/osmosis/tests/tests/test_distribute_rewards.rs @@ -106,7 +106,7 @@ fn assert_rewards_distribution( let res = execute( deps.as_mut(), env.clone(), - mock_info("jake"), + mock_info("owner"), ExecuteMsg::DistributeRewards { denom: denom_to_distribute, }, diff --git a/contracts/rewards-collector/osmosis/tests/tests/test_migration_v2_1_0_to_v2_1_1.rs b/contracts/rewards-collector/osmosis/tests/tests/test_migration_v2_1_0_to_v2_1_1.rs deleted file mode 100644 index f4f1fcb5c..000000000 --- a/contracts/rewards-collector/osmosis/tests/tests/test_migration_v2_1_0_to_v2_1_1.rs +++ /dev/null @@ -1,100 +0,0 @@ -use cosmwasm_std::{attr, testing::mock_env, Addr, Decimal, Empty, Event}; -use cw2::{ContractVersion, VersionError}; -use mars_rewards_collector_base::ContractError; -use mars_rewards_collector_osmosis::{ - entry::migrate, migrations::v2_1_1::previous_state, OsmosisCollector, -}; -use mars_testing::mock_dependencies; -use mars_types::rewards_collector::TransferType; - -#[test] -fn wrong_contract_name() { - let mut deps = mock_dependencies(&[]); - cw2::set_contract_version(deps.as_mut().storage, "contract_xyz", "2.1.0").unwrap(); - - let err = migrate(deps.as_mut(), mock_env(), Empty {}).unwrap_err(); - - assert_eq!( - err, - ContractError::Version(VersionError::WrongContract { - expected: "crates.io:mars-rewards-collector-osmosis".to_string(), - found: "contract_xyz".to_string() - }) - ); -} - -#[test] -fn wrong_contract_version() { - let mut deps = mock_dependencies(&[]); - cw2::set_contract_version( - deps.as_mut().storage, - "crates.io:mars-rewards-collector-osmosis", - "4.1.0", - ) - .unwrap(); - - let err = migrate(deps.as_mut(), mock_env(), Empty {}).unwrap_err(); - - assert_eq!( - err, - ContractError::Version(VersionError::WrongVersion { - expected: "2.1.0".to_string(), - found: "4.1.0".to_string() - }) - ); -} - -#[test] -fn successful_migration_to_v2_1_1() { - let mut deps = mock_dependencies(&[]); - cw2::set_contract_version( - deps.as_mut().storage, - "crates.io:mars-rewards-collector-osmosis", - "2.1.0", - ) - .unwrap(); - - let v1_config = previous_state::Config { - address_provider: Addr::unchecked("address_provider"), - safety_tax_rate: Decimal::percent(50), - safety_fund_denom: "ibc/6F34E1BD664C36CE49ACC28E60D62559A5F96C4F9A6CCE4FC5A67B2852E24CFE" - .to_string(), - fee_collector_denom: "ibc/2E7368A14AC9AB7870F32CFEA687551C5064FA861868EDF7437BC877358A81F9" - .to_string(), - channel_id: "channel-2083".to_string(), - timeout_seconds: 600, - slippage_tolerance: Decimal::percent(1), - neutron_ibc_config: None, - }; - previous_state::CONFIG.save(deps.as_mut().storage, &v1_config).unwrap(); - - let res = migrate(deps.as_mut(), mock_env(), Empty {}).unwrap(); - - assert_eq!(res.messages, vec![]); - assert_eq!(res.events, vec![] as Vec); - assert!(res.data.is_none()); - assert_eq!( - res.attributes, - vec![attr("action", "migrate"), attr("from_version", "2.1.0"), attr("to_version", "2.1.1")] - ); - - let new_contract_version = ContractVersion { - contract: "crates.io:mars-rewards-collector-osmosis".to_string(), - version: "2.1.1".to_string(), - }; - assert_eq!(cw2::get_contract_version(deps.as_ref().storage).unwrap(), new_contract_version); - - // ensure state is correct - let collector = OsmosisCollector::default(); - let updated_config = collector.config.load(deps.as_ref().storage).unwrap(); - - assert_eq!(updated_config.channel_id, "channel-874".to_string()); - assert_eq!(updated_config.safety_tax_rate, Decimal::percent(45)); - assert_eq!(updated_config.revenue_share_tax_rate, Decimal::percent(10)); - assert_eq!(updated_config.safety_fund_config.target_denom, v1_config.safety_fund_denom); - assert_eq!(updated_config.safety_fund_config.transfer_type, TransferType::Bank); - assert_eq!(updated_config.revenue_share_config.target_denom, v1_config.safety_fund_denom); - assert_eq!(updated_config.revenue_share_config.transfer_type, TransferType::Bank); - assert_eq!(updated_config.fee_collector_config.target_denom, v1_config.fee_collector_denom); - assert_eq!(updated_config.fee_collector_config.transfer_type, TransferType::Ibc); -} diff --git a/contracts/rewards-collector/osmosis/tests/tests/test_migration_v2_2_0.rs b/contracts/rewards-collector/osmosis/tests/tests/test_migration_v2_2_0.rs new file mode 100644 index 000000000..3e4db6d29 --- /dev/null +++ b/contracts/rewards-collector/osmosis/tests/tests/test_migration_v2_2_0.rs @@ -0,0 +1,127 @@ +use cosmwasm_std::{attr, testing::mock_env, Decimal, Empty, Event}; +use cw2::{ContractVersion, VersionError}; +use mars_rewards_collector_base::ContractError; +use mars_rewards_collector_osmosis::{entry::migrate, OsmosisCollector}; +use mars_testing::mock_dependencies; +use mars_types::rewards_collector::{Config, RewardConfig, TransferType}; + +const CONTRACT: &str = "crates.io:mars-rewards-collector-osmosis"; + +mod previous_state { + use cosmwasm_schema::cw_serde; + use cosmwasm_std::{Addr, Decimal}; + use cw_storage_plus::Item; + use mars_types::rewards_collector::RewardConfig; + + #[cw_serde] + pub struct Config { + pub address_provider: Addr, + pub safety_tax_rate: Decimal, + pub revenue_share_tax_rate: Decimal, + pub slippage_tolerance: Decimal, + pub safety_fund_config: RewardConfig, + pub revenue_share_config: RewardConfig, + pub fee_collector_config: RewardConfig, + pub channel_id: String, + pub timeout_seconds: u64, + } + + pub const CONFIG: Item = Item::new("config"); +} + +#[test] +fn wrong_contract_name() { + let mut deps = mock_dependencies(&[]); + cw2::set_contract_version(deps.as_mut().storage, "contract_xyz", "2.1.1").unwrap(); + + let err = migrate(deps.as_mut(), mock_env(), Empty {}).unwrap_err(); + + match err { + ContractError::Version(VersionError::WrongContract { + expected, + found, + }) => { + assert_eq!(expected, CONTRACT.to_string()); + assert_eq!(found, "contract_xyz".to_string()); + } + other => panic!("unexpected error: {other:?}"), + } +} + +#[test] +fn wrong_contract_version() { + let mut deps = mock_dependencies(&[]); + cw2::set_contract_version(deps.as_mut().storage, CONTRACT, "4.1.0").unwrap(); + + let err = migrate(deps.as_mut(), mock_env(), Empty {}).unwrap_err(); + + match err { + ContractError::Version(VersionError::WrongVersion { + found, + .. + }) => { + assert_eq!(found, "4.1.0".to_string()); + } + other => panic!("unexpected error: {other:?}"), + } +} + +#[test] +fn successful_migration_from_2_1_1() { + let mut deps = mock_dependencies(&[]); + cw2::set_contract_version(deps.as_mut().storage, CONTRACT, "2.1.1").unwrap(); + + let reward_cfg = |denom: &str| RewardConfig { + target_denom: denom.to_string(), + transfer_type: TransferType::Bank, + }; + + let addr_provider = deps.as_ref().api.addr_validate("addr_provider").unwrap(); + let old_config = previous_state::Config { + address_provider: addr_provider.clone(), + safety_tax_rate: Decimal::percent(5), + revenue_share_tax_rate: Decimal::percent(10), + slippage_tolerance: Decimal::percent(1), + safety_fund_config: reward_cfg("usdc"), + revenue_share_config: reward_cfg("usdc"), + fee_collector_config: reward_cfg("mars"), + channel_id: "channel-1".to_string(), + timeout_seconds: 600, + }; + + previous_state::CONFIG.save(deps.as_mut().storage, &old_config).unwrap(); + + let res = migrate(deps.as_mut(), mock_env(), Empty {}).unwrap(); + + assert_eq!(res.messages, vec![]); + assert_eq!(res.events, vec![] as Vec); + assert!(res.data.is_none()); + assert_eq!( + res.attributes, + vec![attr("action", "migrate"), attr("from_version", "2.1.1"), attr("to_version", "2.2.0")] + ); + + let new_contract_version = ContractVersion { + contract: CONTRACT.to_string(), + version: "2.2.0".to_string(), + }; + assert_eq!(cw2::get_contract_version(deps.as_ref().storage).unwrap(), new_contract_version); + + let collector = OsmosisCollector::default(); + let stored_config = collector.config.load(deps.as_ref().storage).unwrap(); + + assert_eq!( + stored_config, + Config { + address_provider: addr_provider, + safety_tax_rate: Decimal::percent(5), + revenue_share_tax_rate: Decimal::percent(10), + safety_fund_config: reward_cfg("usdc"), + revenue_share_config: reward_cfg("usdc"), + fee_collector_config: reward_cfg("mars"), + channel_id: "channel-1".to_string(), + timeout_seconds: 600, + whitelisted_distributors: vec![], + } + ); +} diff --git a/contracts/rewards-collector/osmosis/tests/tests/test_swap.rs b/contracts/rewards-collector/osmosis/tests/tests/test_swap.rs index cb402f024..579e17ec5 100644 --- a/contracts/rewards-collector/osmosis/tests/tests/test_swap.rs +++ b/contracts/rewards-collector/osmosis/tests/tests/test_swap.rs @@ -1,7 +1,6 @@ use cosmwasm_std::{ coin, testing::mock_env, to_json_binary, CosmosMsg, Decimal, Empty, SubMsg, Uint128, WasmMsg, }; -use mars_rewards_collector_base::ContractError; use mars_rewards_collector_osmosis::entry::execute; use mars_testing::mock_info; use mars_types::{ @@ -195,107 +194,3 @@ fn skipping_swap_if_denom_matches() { .into(); assert_eq!(res.messages[0], SubMsg::new(swap_msg)); } - -#[test] -fn swap_fails_if_slippage_limit_exceeded() { - let mut deps = helpers::setup_test(); - - let usdc_denom = "uusdc".to_string(); - let mars_denom = "umars".to_string(); - let atom_denom = "uatom".to_string(); - - let uusdc_usd_price = Decimal::one(); - let umars_uusdc_price = Decimal::from_ratio(5u128, 10u128); // 0.5 uusdc = 1 umars - let uatom_uusdc_price = Decimal::from_ratio(125u128, 10u128); // 12.5 uusd = 1 uatom - - deps.querier.set_oracle_price(&usdc_denom, uusdc_usd_price); - - deps.querier.set_oracle_price(&mars_denom, umars_uusdc_price); - - deps.querier.set_oracle_price(&atom_denom, uatom_uusdc_price); - - deps.querier.set_swapper_estimate_price(&mars_denom, umars_uusdc_price); - deps.querier.set_swapper_estimate_price(&atom_denom, uatom_uusdc_price); - deps.querier.set_swapper_estimate_price(&usdc_denom, uusdc_usd_price); - - // Here we test the slippage limits for each of the target reward denoms - // - // uatom for revenue share = 88888 * 0.1 = 8888 - // uatom for safety fund = 88888 * 0.25 = 22222 - // uatom for Fee collector = 88888 - 8888 - 22222 = 57778 - // worst price (atom -> mars ) = 12.5 / 0/5 = 25 * (1-0.03) = 24.25 - // worst price (atom -> usdc) = 12.5 = * (1-0.03) = 12.125 - // minimum revenue share (atom -> usdc) = 8888 * 12.125 = 107767 - // minimum safety fund (atom -> usdc) = 31110 * 12.125 = 377208 - // minimum fee collector (atom -> mars) = 57778 * 24.25= 1401116 - - // Safety Fund fail - let res = execute( - deps.as_mut(), - mock_env(), - mock_info("jake"), - ExecuteMsg::SwapAsset { - denom: atom_denom.to_string(), - amount: None, - safety_fund_route: Some(SwapperRoute::Osmo(OsmoRoute { - swaps: vec![OsmoSwap { - pool_id: 12, - to: usdc_denom.to_string(), - }], - })), - fee_collector_route: Some(SwapperRoute::Osmo(OsmoRoute { - swaps: vec![OsmoSwap { - pool_id: 69, - to: mars_denom.to_string(), - }], - })), - safety_fund_min_receive: Some(Uint128::new(377207)), // 377207 < 377208 -> error - fee_collector_min_receive: Some(Uint128::new(1401116)), // pass - }, - ); - - assert_eq!( - res.unwrap_err(), - ContractError::SlippageLimitExceeded { - denom_in: atom_denom.clone(), - denom_out: usdc_denom.clone(), - min_receive_minimum: Uint128::new(377208), - min_receive_given: Uint128::new(377207), - } - ); - - // Fee Collector fail - let res = execute( - deps.as_mut(), - mock_env(), - mock_info("jake"), - ExecuteMsg::SwapAsset { - denom: atom_denom.to_string(), - amount: None, - safety_fund_route: Some(SwapperRoute::Osmo(OsmoRoute { - swaps: vec![OsmoSwap { - pool_id: 12, - to: usdc_denom.to_string(), - }], - })), - fee_collector_route: Some(SwapperRoute::Osmo(OsmoRoute { - swaps: vec![OsmoSwap { - pool_id: 69, - to: mars_denom.to_string(), - }], - })), - safety_fund_min_receive: Some(Uint128::new(377208)), // pass - fee_collector_min_receive: Some(Uint128::new(1401115)), // 1401115 < 1401116 -> error - }, - ); - - assert_eq!( - res.unwrap_err(), - ContractError::SlippageLimitExceeded { - denom_in: atom_denom.clone(), - denom_out: mars_denom.clone(), - min_receive_minimum: Uint128::new(1401116), - min_receive_given: Uint128::new(1401115), - } - ); -} diff --git a/contracts/rewards-collector/osmosis/tests/tests/test_whitelist_distributors.rs b/contracts/rewards-collector/osmosis/tests/tests/test_whitelist_distributors.rs new file mode 100644 index 000000000..1692e25e2 --- /dev/null +++ b/contracts/rewards-collector/osmosis/tests/tests/test_whitelist_distributors.rs @@ -0,0 +1,390 @@ +use cosmwasm_std::{testing::mock_env, Uint128}; +use mars_owner::OwnerError::NotOwner; +use mars_rewards_collector_base::ContractError; +use mars_rewards_collector_osmosis::entry::execute; +use mars_testing::mock_info; +use mars_types::rewards_collector::{ + ConfigResponse, ExecuteMsg, QueryMsg, UpdateConfig, WhitelistAction, +}; + +use super::helpers; + +#[test] +fn owner_can_add_to_whitelist() { + let mut deps = helpers::setup_test(); + // Owner adds alice + let info = mock_info("owner"); + let msg = ExecuteMsg::UpdateConfig { + new_cfg: UpdateConfig { + whitelist_actions: Some(vec![WhitelistAction::AddAddress { + address: "alice".to_string(), + }]), + ..Default::default() + }, + }; + execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + let cfg: ConfigResponse = helpers::query(deps.as_ref(), QueryMsg::Config {}); + assert!(cfg.whitelisted_distributors.contains(&"alice".to_string())); +} + +#[test] +fn non_owner_cannot_add_to_whitelist() { + let mut deps = helpers::setup_test(); + let info = mock_info("not_owner"); + let msg = ExecuteMsg::UpdateConfig { + new_cfg: UpdateConfig { + whitelist_actions: Some(vec![WhitelistAction::AddAddress { + address: "alice".to_string(), + }]), + ..Default::default() + }, + }; + let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + assert_eq!(err, ContractError::Owner(NotOwner {})); +} + +#[test] +fn owner_can_remove_from_whitelist() { + let mut deps = helpers::setup_test(); + // Owner adds alice + let info = mock_info("owner"); + let msg = ExecuteMsg::UpdateConfig { + new_cfg: UpdateConfig { + whitelist_actions: Some(vec![WhitelistAction::AddAddress { + address: "alice".to_string(), + }]), + ..Default::default() + }, + }; + execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); + // Owner removes alice + let msg = ExecuteMsg::UpdateConfig { + new_cfg: UpdateConfig { + whitelist_actions: Some(vec![WhitelistAction::RemoveAddress { + address: "alice".to_string(), + }]), + ..Default::default() + }, + }; + execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + let cfg: ConfigResponse = helpers::query(deps.as_ref(), QueryMsg::Config {}); + assert!(!cfg.whitelisted_distributors.contains(&"alice".to_string())); +} + +#[test] +fn whitelisted_can_distribute_rewards() { + let mut deps = helpers::setup_test(); + // Owner adds alice + let info = mock_info("owner"); + let msg = ExecuteMsg::UpdateConfig { + new_cfg: UpdateConfig { + whitelist_actions: Some(vec![WhitelistAction::AddAddress { + address: "alice".to_string(), + }]), + ..Default::default() + }, + }; + execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + deps.querier.set_contract_balances(&[cosmwasm_std::coin(1000, "umars")]); + // Alice can distribute + let info = mock_info("alice"); + let msg = ExecuteMsg::DistributeRewards { + denom: "umars".to_string(), + }; + let result = execute(deps.as_mut(), mock_env(), info, msg); + assert!(result.is_ok()); +} + +#[test] +fn owner_can_distribute_rewards() { + let mut deps = helpers::setup_test(); + deps.querier.set_contract_balances(&[cosmwasm_std::coin(1000, "umars")]); + let info = mock_info("owner"); + let msg = ExecuteMsg::DistributeRewards { + denom: "umars".to_string(), + }; + let result = execute(deps.as_mut(), mock_env(), info, msg); + assert!(result.is_ok()); +} + +#[test] +fn non_whitelisted_cannot_distribute_rewards() { + let mut deps = helpers::setup_test(); + deps.querier.set_contract_balances(&[cosmwasm_std::coin(1000, "umars")]); + let info = mock_info("bob"); + let msg = ExecuteMsg::DistributeRewards { + denom: "umars".to_string(), + }; + let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + assert!(matches!( + err, + ContractError::UnauthorizedDistributor { + sender: _ + } + )); + if let ContractError::UnauthorizedDistributor { + sender, + } = err + { + assert_eq!(sender, "bob"); + } +} + +#[test] +fn removed_account_cannot_distribute_rewards() { + let mut deps = helpers::setup_test(); + // Owner adds alice + let info = mock_info("owner"); + let msg = ExecuteMsg::UpdateConfig { + new_cfg: UpdateConfig { + whitelist_actions: Some(vec![WhitelistAction::AddAddress { + address: "alice".to_string(), + }]), + ..Default::default() + }, + }; + execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); + // Owner removes alice + let msg = ExecuteMsg::UpdateConfig { + new_cfg: UpdateConfig { + whitelist_actions: Some(vec![WhitelistAction::RemoveAddress { + address: "alice".to_string(), + }]), + ..Default::default() + }, + }; + execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + // Alice can no longer distribute + let info = mock_info("alice"); + let msg = ExecuteMsg::DistributeRewards { + denom: "umars".to_string(), + }; + let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + assert!(matches!( + err, + ContractError::UnauthorizedDistributor { + sender: _ + } + )); +} + +#[test] +fn whitelisted_can_swap_asset() { + let mut deps = helpers::setup_test(); + // Owner adds alice + let info = mock_info("owner"); + let msg = ExecuteMsg::UpdateConfig { + new_cfg: UpdateConfig { + whitelist_actions: Some(vec![WhitelistAction::AddAddress { + address: "alice".to_string(), + }]), + ..Default::default() + }, + }; + execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + deps.querier.set_contract_balances(&[cosmwasm_std::coin(1000, "umars")]); + // Alice can swap + let info = mock_info("alice"); + let msg = ExecuteMsg::SwapAsset { + denom: "umars".to_string(), + amount: None, + safety_fund_route: None, + fee_collector_route: None, + safety_fund_min_receive: Some(Uint128::from(1000u128)), + + fee_collector_min_receive: None, + }; + let result = execute(deps.as_mut(), mock_env(), info, msg); + assert!(result.is_ok()); +} + +#[test] +fn non_whitelisted_cannot_swap_asset() { + let mut deps = helpers::setup_test(); + deps.querier.set_contract_balances(&[cosmwasm_std::coin(1000, "umars")]); + let info = mock_info("bob"); + let msg = ExecuteMsg::SwapAsset { + denom: "umars".to_string(), + amount: None, + safety_fund_route: None, + fee_collector_route: None, + safety_fund_min_receive: None, + fee_collector_min_receive: None, + }; + let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + assert!(matches!( + err, + ContractError::UnauthorizedDistributor { + sender: _ + } + )); + if let ContractError::UnauthorizedDistributor { + sender, + } = err + { + assert_eq!(sender, "bob"); + } +} + +#[test] +fn removed_account_cannot_swap_asset() { + let mut deps = helpers::setup_test(); + // Owner adds alice + let info = mock_info("owner"); + let msg = ExecuteMsg::UpdateConfig { + new_cfg: UpdateConfig { + whitelist_actions: Some(vec![WhitelistAction::AddAddress { + address: "alice".to_string(), + }]), + ..Default::default() + }, + }; + execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); + // Owner removes alice + let msg = ExecuteMsg::UpdateConfig { + new_cfg: UpdateConfig { + whitelist_actions: Some(vec![WhitelistAction::RemoveAddress { + address: "alice".to_string(), + }]), + ..Default::default() + }, + }; + execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + // Alice can no longer swap + let info = mock_info("alice"); + let msg = ExecuteMsg::SwapAsset { + denom: "umars".to_string(), + amount: None, + safety_fund_route: None, + fee_collector_route: None, + safety_fund_min_receive: Some(Uint128::from(1000u128)), + fee_collector_min_receive: None, + }; + let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + assert!(matches!( + err, + ContractError::UnauthorizedDistributor { + sender: _ + } + )); +} + +#[test] +fn owner_can_swap_asset() { + let mut deps = helpers::setup_test(); + deps.querier.set_contract_balances(&[cosmwasm_std::coin(1000, "umars")]); + let info = mock_info("owner"); + let msg = ExecuteMsg::SwapAsset { + denom: "umars".to_string(), + amount: None, + safety_fund_route: None, + fee_collector_route: None, + safety_fund_min_receive: Some(Uint128::from(1000u128)), + fee_collector_min_receive: None, + }; + let result = execute(deps.as_mut(), mock_env(), info, msg); + assert!(result.is_ok()); +} + +#[test] +fn owner_can_add_multiple_to_whitelist() { + let mut deps = helpers::setup_test(); + // Owner adds alice and bob + let info = mock_info("owner"); + let msg = ExecuteMsg::UpdateConfig { + new_cfg: UpdateConfig { + whitelist_actions: Some(vec![ + WhitelistAction::AddAddress { + address: "alice".to_string(), + }, + WhitelistAction::AddAddress { + address: "bob".to_string(), + }, + ]), + ..Default::default() + }, + }; + execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + let cfg: ConfigResponse = helpers::query(deps.as_ref(), QueryMsg::Config {}); + assert!(cfg.whitelisted_distributors.contains(&"alice".to_string())); + assert!(cfg.whitelisted_distributors.contains(&"bob".to_string())); +} + +#[test] +fn owner_can_remove_multiple_from_whitelist() { + let mut deps = helpers::setup_test(); + // Owner adds alice and bob + let info = mock_info("owner"); + let msg = ExecuteMsg::UpdateConfig { + new_cfg: UpdateConfig { + whitelist_actions: Some(vec![ + WhitelistAction::AddAddress { + address: "alice".to_string(), + }, + WhitelistAction::AddAddress { + address: "bob".to_string(), + }, + ]), + ..Default::default() + }, + }; + execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); + + // Owner removes alice and bob + let msg = ExecuteMsg::UpdateConfig { + new_cfg: UpdateConfig { + whitelist_actions: Some(vec![ + WhitelistAction::RemoveAddress { + address: "alice".to_string(), + }, + WhitelistAction::RemoveAddress { + address: "bob".to_string(), + }, + ]), + ..Default::default() + }, + }; + execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + let cfg: ConfigResponse = helpers::query(deps.as_ref(), QueryMsg::Config {}); + assert!(!cfg.whitelisted_distributors.contains(&"alice".to_string())); + assert!(!cfg.whitelisted_distributors.contains(&"bob".to_string())); +} + +#[test] +fn owner_can_add_and_remove_in_same_tx() { + let mut deps = helpers::setup_test(); + // Owner adds alice + let info = mock_info("owner"); + let msg = ExecuteMsg::UpdateConfig { + new_cfg: UpdateConfig { + whitelist_actions: Some(vec![WhitelistAction::AddAddress { + address: "alice".to_string(), + }]), + ..Default::default() + }, + }; + execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); + + // Ensure alice is there + let cfg: ConfigResponse = helpers::query(deps.as_ref(), QueryMsg::Config {}); + assert!(cfg.whitelisted_distributors.contains(&"alice".to_string())); + + // Owner adds bob and removes alice in the same tx + let msg = ExecuteMsg::UpdateConfig { + new_cfg: UpdateConfig { + whitelist_actions: Some(vec![ + WhitelistAction::AddAddress { + address: "bob".to_string(), + }, + WhitelistAction::RemoveAddress { + address: "alice".to_string(), + }, + ]), + ..Default::default() + }, + }; + execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + let cfg: ConfigResponse = helpers::query(deps.as_ref(), QueryMsg::Config {}); + assert!(cfg.whitelisted_distributors.contains(&"bob".to_string())); + assert!(!cfg.whitelisted_distributors.contains(&"alice".to_string())); +} diff --git a/contracts/vault/tests/tests/test_redeem.rs b/contracts/vault/tests/tests/test_redeem.rs index 1fc2aa2e9..316dad3c2 100644 --- a/contracts/vault/tests/tests/test_redeem.rs +++ b/contracts/vault/tests/tests/test_redeem.rs @@ -70,7 +70,7 @@ fn redeem_invalid_funds() { ); assert_vault_err( res, - ContractError::Payment(PaymentError::MissingDenom("factory/contract11/vault".to_string())), + ContractError::Payment(PaymentError::MissingDenom("factory/contract12/vault".to_string())), ); } diff --git a/coverage_grcov.Makefile.toml b/coverage_grcov.Makefile.toml index 883872a13..5fdf7714f 100644 --- a/coverage_grcov.Makefile.toml +++ b/coverage_grcov.Makefile.toml @@ -29,7 +29,7 @@ LLVM_PROFILE_FILE = "${COVERAGE_PROF_OUTPUT}/coverage-%p-%m.profraw" condition = { env_not_set = ["SKIP_INSTALL_GRCOV"] } private = true command = "cargo" -args = ["install", "grcov", "--locked"] +args = ["install", "grcov", "--version=0.9.1", "--locked"] # NOTE: ignore coverage for swapper and zapper contracts because their tests are based on `osmosis-testing` which don't work for grcov [tasks.coverage-grcov] diff --git a/integration-tests/tests/test_oracles.rs b/integration-tests/tests/test_oracles.rs index 60bafc155..a08ad4956 100644 --- a/integration-tests/tests/test_oracles.rs +++ b/integration-tests/tests/test_oracles.rs @@ -1294,7 +1294,7 @@ fn setup_redbank(wasm: &Wasm, signer: &SigningAccount) -> (Strin }, channel_id: "channel-1".to_string(), timeout_seconds: 60, - slippage_tolerance: Decimal::new(Uint128::from(1u128)), + whitelisted_distributors: vec![signer.address()], }, ); diff --git a/integration-tests/tests/test_rewards_collector.rs b/integration-tests/tests/test_rewards_collector.rs index 3d5eb321e..ec29a90a4 100644 --- a/integration-tests/tests/test_rewards_collector.rs +++ b/integration-tests/tests/test_rewards_collector.rs @@ -86,7 +86,7 @@ fn swapping_rewards() { }, channel_id: "channel-1".to_string(), timeout_seconds: 60, - slippage_tolerance: Decimal::percent(5), + whitelisted_distributors: vec![signer.address()], }, ); @@ -469,7 +469,7 @@ fn distribute_rewards_if_ibc_channel_invalid() { }, channel_id: "".to_string(), timeout_seconds: 60, - slippage_tolerance: Decimal::percent(1), + whitelisted_distributors: vec![signer.address()], }, ); @@ -510,7 +510,7 @@ fn distribute_rewards_if_ibc_channel_invalid() { fee_collector_config: None, channel_id: Some("channel-1".to_string()), timeout_seconds: None, - slippage_tolerance: None, + whitelist_actions: None, }, }, &[], diff --git a/packages/testing/src/integration/mock_env.rs b/packages/testing/src/integration/mock_env.rs index 31dae64f8..7c5023fb3 100644 --- a/packages/testing/src/integration/mock_env.rs +++ b/packages/testing/src/integration/mock_env.rs @@ -840,7 +840,6 @@ pub struct MockEnvBuilder { safety_fund_config: RewardConfig, revenue_share_config: RewardConfig, fee_collector_config: RewardConfig, - slippage_tolerance: Decimal, pyth_contract_addr: String, @@ -872,7 +871,6 @@ impl MockEnvBuilder { target_denom: "umars".to_string(), transfer_type: rewards_collector::TransferType::Ibc, }, - slippage_tolerance: Decimal::percent(5), pyth_contract_addr: "osmo1svg55quy7jjee6dn0qx85qxxvx5cafkkw4tmqpcjr9dx99l0zrhs4usft5" .to_string(), // correct bech32 addr to pass validation credit_manager_contract_addr: @@ -915,11 +913,6 @@ impl MockEnvBuilder { self } - pub fn slippage_tolerance(&mut self, percentage: Decimal) -> &mut Self { - self.slippage_tolerance = percentage; - self - } - pub fn pyth_contract_addr(&mut self, pyth_contract_addr: Addr) -> &mut Self { self.pyth_contract_addr = pyth_contract_addr.to_string(); self @@ -1087,7 +1080,7 @@ impl MockEnvBuilder { fee_collector_config: self.fee_collector_config.clone(), channel_id: "0".to_string(), timeout_seconds: 900, - slippage_tolerance: self.slippage_tolerance, + whitelisted_distributors: vec![], }, &[], "rewards-collector", diff --git a/packages/testing/src/multitest/helpers/mock_env.rs b/packages/testing/src/multitest/helpers/mock_env.rs index e3afe0944..b3c562a2a 100644 --- a/packages/testing/src/multitest/helpers/mock_env.rs +++ b/packages/testing/src/multitest/helpers/mock_env.rs @@ -64,6 +64,7 @@ use mars_types::{ QueryMsg::{UserCollateral, UserDebt}, UserCollateralResponse, UserDebtResponse, }, + rewards_collector::{self, RewardConfig, TransferType}, swapper::{ EstimateExactInSwapResponse, InstantiateMsg as SwapperInstantiateMsg, QueryMsg::EstimateExactInSwap, SwapperRoute, @@ -81,7 +82,10 @@ use super::{ mock_red_bank_contract, mock_rover_contract, mock_swapper_contract, mock_v2_zapper_contract, mock_vault_contract, AccountToFund, CoinInfo, VaultTestInfo, ASTRO_LP_DENOM, }; -use crate::multitest::modules::token_factory::{CustomApp, TokenFactory}; +use crate::{ + integration::mock_contracts::mock_rewards_collector_osmosis_contract, + multitest::modules::token_factory::{CustomApp, TokenFactory}, +}; pub const DEFAULT_RED_BANK_COIN_BALANCE: Uint128 = Uint128::new(1_000_000); @@ -113,6 +117,8 @@ pub struct MockEnvBuilder { pub max_slippage: Option, pub health_contract: Option, pub evil_vault: Option, + pub swap_fee: Option, + pub rewards_collector: Option, } #[allow(clippy::new_ret_no_self)] @@ -140,6 +146,8 @@ impl MockEnv { max_slippage: None, health_contract: None, evil_vault: None, + swap_fee: None, + rewards_collector: None, } } @@ -937,21 +945,22 @@ impl MockEnvBuilder { self.update_health_contract_config(&rover); self.deploy_nft_contract(&rover); + self.fund_users(); + + self.deploy_vaults(); if self.deploy_nft_contract && self.set_nft_contract_minter { + let rewards_collector_addr = self.deploy_rewards_collector(); + self.rewards_collector = Some(rewards_collector_addr.clone()); self.update_config( &rover, ConfigUpdates { - rewards_collector: Some("rewards_collector_contract".to_string()), + rewards_collector: Some(rewards_collector_addr.to_string()), ..Default::default() }, ); } - self.fund_users(); - - self.deploy_vaults(); - Ok(MockEnv { app: self.app, rover, @@ -1085,11 +1094,11 @@ impl MockEnvBuilder { let swapper = self.deploy_swapper().into(); let max_unlocking_positions = self.get_max_unlocking_positions(); let max_slippage = self.get_max_slippage(); - let oracle = self.get_oracle().into(); let zapper = self.deploy_zapper(&oracle)?.into(); let health_contract = self.get_health_contract().into(); let params = self.get_params_contract().into(); + let swap_fee = self.get_swap_fee(); self.deploy_astroport_incentives(); @@ -1109,6 +1118,7 @@ impl MockEnvBuilder { health_contract, params, incentives, + swap_fee, }, &[], "mock-rover-contract", @@ -1417,6 +1427,56 @@ impl MockEnvBuilder { vault_addr } + fn deploy_rewards_collector(&mut self) -> Addr { + if let Some(addr) = &self.rewards_collector { + return addr.clone(); + } + + let code_id = self.app.store_code(mock_rewards_collector_osmosis_contract()); + let owner = self.get_owner(); + let address_provider = self.get_address_provider(); + + let addr = self + .app + .instantiate_contract( + code_id, + owner.clone(), + &rewards_collector::InstantiateMsg { + owner: owner.clone().to_string(), + address_provider: address_provider.to_string(), + safety_tax_rate: Default::default(), + revenue_share_tax_rate: Default::default(), + safety_fund_config: RewardConfig { + target_denom: "uusdc".to_string(), + transfer_type: TransferType::Bank, + }, + revenue_share_config: RewardConfig { + target_denom: "uusdc".to_string(), + transfer_type: TransferType::Bank, + }, + fee_collector_config: RewardConfig { + target_denom: "umars".to_string(), + transfer_type: TransferType::Bank, + }, + channel_id: "".to_string(), + timeout_seconds: 1, + whitelisted_distributors: vec![], + }, + &[], + "mock-rewards-collector", + None, + ) + .unwrap(); + + self.set_address(MarsAddressType::RewardsCollector, addr.clone()); + self.set_address(MarsAddressType::SafetyFund, Addr::unchecked("safety_fund")); + self.set_address(MarsAddressType::RevenueShare, Addr::unchecked("revenue_share")); + self.set_address(MarsAddressType::FeeCollector, Addr::unchecked("fee_collector")); + + self.rewards_collector = Some(addr.clone()); + addr + } + fn deploy_swapper(&mut self) -> Swapper { let code_id = self.app.store_code(mock_swapper_contract()); let addr = self @@ -1510,6 +1570,10 @@ impl MockEnvBuilder { self.max_slippage.unwrap_or_else(|| Decimal::percent(99)) } + fn get_swap_fee(&self) -> Decimal { + self.swap_fee.unwrap_or_else(|| Decimal::percent(0)) + } + //-------------------------------------------------------------------------------------------------- // Setter functions //-------------------------------------------------------------------------------------------------- @@ -1588,6 +1652,11 @@ impl MockEnvBuilder { self.evil_vault = Some(credit_account.to_string()); self } + + pub fn swap_fee(mut self, ratio: Decimal) -> Self { + self.swap_fee = Some(ratio); + self + } } //-------------------------------------------------------------------------------------------------- diff --git a/packages/types/src/credit_manager/instantiate.rs b/packages/types/src/credit_manager/instantiate.rs index 73a766fea..8a4eb16b5 100644 --- a/packages/types/src/credit_manager/instantiate.rs +++ b/packages/types/src/credit_manager/instantiate.rs @@ -31,6 +31,10 @@ pub struct InstantiateMsg { pub params: ParamsUnchecked, /// Contract that handles lending incentive rewards pub incentives: IncentivesUnchecked, + /// The swap fee applied to each swap. This is a percentage of the swap amount. + /// For example, if set to 0.0001, 0.01% of the swap amount will be taken as a fee. + /// This fee is applied once, no matter how many hops in the route + pub swap_fee: Decimal, } /// Used when you want to update fields on Instantiate config @@ -48,4 +52,5 @@ pub struct ConfigUpdates { pub health_contract: Option, /// The Mars Protocol rewards-collector contract. We collect protocol fee for its account. pub rewards_collector: Option, + pub swap_fee: Option, } diff --git a/packages/types/src/credit_manager/query.rs b/packages/types/src/credit_manager/query.rs index ee30f35a4..efea793d0 100644 --- a/packages/types/src/credit_manager/query.rs +++ b/packages/types/src/credit_manager/query.rs @@ -96,6 +96,8 @@ pub enum QueryMsg { start_after: Option, limit: Option, }, + #[returns(Decimal)] + SwapFeeRate {}, } #[cw_serde] diff --git a/packages/types/src/rewards_collector.rs b/packages/types/src/rewards_collector.rs index e05d47540..1062d64de 100644 --- a/packages/types/src/rewards_collector.rs +++ b/packages/types/src/rewards_collector.rs @@ -10,8 +10,6 @@ use mars_utils::{ use crate::{credit_manager::Action, swapper::SwapperRoute}; -const MAX_SLIPPAGE_TOLERANCE_PERCENTAGE: u64 = 50; - #[cw_serde] pub struct InstantiateMsg { /// The contract's owner @@ -32,8 +30,8 @@ pub struct InstantiateMsg { pub channel_id: String, /// Number of seconds after which an IBC transfer is to be considered failed, if no acknowledgement is received pub timeout_seconds: u64, - /// Maximum percentage of price movement (minimum amount you accept to receive during swap) - pub slippage_tolerance: Decimal, + /// List of addresses that are allowed to execute the rewards distribution + pub whitelisted_distributors: Vec, } #[cw_serde] pub enum TransferType { @@ -78,8 +76,8 @@ pub struct Config { pub channel_id: String, /// Number of seconds after which an IBC transfer is to be considered failed, if no acknowledgement is received pub timeout_seconds: u64, - /// Maximum percentage of price movement (minimum amount you accept to receive during swap) - pub slippage_tolerance: Decimal, + /// List of addresses that are allowed to execute the rewards distribution + pub whitelisted_distributors: Vec, } impl Config { @@ -89,14 +87,6 @@ impl Config { integer_param_gt_zero(self.timeout_seconds, "timeout_seconds")?; - if self.slippage_tolerance > Decimal::percent(MAX_SLIPPAGE_TOLERANCE_PERCENTAGE) { - return Err(ValidationError::InvalidParam { - param_name: "slippage_tolerance".to_string(), - invalid_value: self.slippage_tolerance.to_string(), - predicate: format!("<= {}", Decimal::percent(MAX_SLIPPAGE_TOLERANCE_PERCENTAGE)), - }); - } - // There is an assumption that revenue share and safety fund are swapped to the same denom assert_eq!(self.safety_fund_config.target_denom, self.revenue_share_config.target_denom); @@ -113,6 +103,13 @@ impl Config { impl Config { pub fn checked(api: &dyn Api, msg: InstantiateMsg) -> StdResult { + // Validate all addresses in the whitelist + let whitelisted_distributors = msg + .whitelisted_distributors + .iter() + .map(|addr| api.addr_validate(addr)) + .collect::>>()?; + Ok(Config { address_provider: api.addr_validate(&msg.address_provider)?, safety_tax_rate: msg.safety_tax_rate, @@ -122,11 +119,23 @@ impl Config { fee_collector_config: msg.fee_collector_config, channel_id: msg.channel_id, timeout_seconds: msg.timeout_seconds, - slippage_tolerance: msg.slippage_tolerance, + whitelisted_distributors, }) } } +#[cw_serde] +pub enum WhitelistAction { + /// Add an address to the whitelist of distributors + AddAddress { + address: String, + }, + /// Remove an address from the whitelist of distributors + RemoveAddress { + address: String, + }, +} + #[cw_serde] #[derive(Default)] pub struct UpdateConfig { @@ -146,8 +155,8 @@ pub struct UpdateConfig { pub channel_id: Option, /// Number of seconds after which an IBC transfer is to be considered failed, if no acknowledgement is received pub timeout_seconds: Option, - /// Maximum percentage of price movement (minimum amount you accept to receive during swap) - pub slippage_tolerance: Option, + /// Actions to modify the whitelist of distributors + pub whitelist_actions: Option>, } #[cw_serde] @@ -227,8 +236,8 @@ pub struct ConfigResponse { pub channel_id: String, /// Number of seconds after which an IBC transfer is to be considered failed, if no acknowledgement is received pub timeout_seconds: u64, - /// Maximum percentage of price movement (minimum amount you accept to receive during swap) - pub slippage_tolerance: Decimal, + /// List of addresses that are allowed to execute the rewards distribution + pub whitelisted_distributors: Vec, } #[cw_serde] diff --git a/schemas/mars-credit-manager/mars-credit-manager.json b/schemas/mars-credit-manager/mars-credit-manager.json index cde8c9d37..daba3146d 100644 --- a/schemas/mars-credit-manager/mars-credit-manager.json +++ b/schemas/mars-credit-manager/mars-credit-manager.json @@ -1,6 +1,6 @@ { "contract_name": "mars-credit-manager", - "contract_version": "2.1.0", + "contract_version": "2.2.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -15,6 +15,7 @@ "owner", "params", "red_bank", + "swap_fee", "swapper", "zapper" ], @@ -79,6 +80,14 @@ } ] }, + "swap_fee": { + "description": "The swap fee applied to each swap. This is a percentage of the swap amount. For example, if set to 0.0001, 0.01% of the swap amount will be taken as a fee. This fee is applied once, no matter how many hops in the route", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, "swapper": { "description": "Helper contract for making swaps", "allOf": [ @@ -1795,6 +1804,16 @@ "null" ] }, + "swap_fee": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, "swapper": { "anyOf": [ { @@ -2687,6 +2706,19 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "swap_fee_rate" + ], + "properties": { + "swap_fee_rate": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": { @@ -3671,6 +3703,12 @@ } } }, + "swap_fee_rate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Decimal", + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, "total_debt_shares": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "DebtShares", diff --git a/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json b/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json index 0b1bfe28b..f59feb169 100644 --- a/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json +++ b/schemas/mars-rewards-collector-base/mars-rewards-collector-base.json @@ -15,8 +15,8 @@ "revenue_share_tax_rate", "safety_fund_config", "safety_tax_rate", - "slippage_tolerance", - "timeout_seconds" + "timeout_seconds", + "whitelisted_distributors" ], "properties": { "address_provider": { @@ -71,19 +71,18 @@ } ] }, - "slippage_tolerance": { - "description": "Maximum percentage of price movement (minimum amount you accept to receive during swap)", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, "timeout_seconds": { "description": "Number of seconds after which an IBC transfer is to be considered failed, if no acknowledgement is received", "type": "integer", "format": "uint64", "minimum": 0.0 + }, + "whitelisted_distributors": { + "description": "List of addresses that are allowed to execute the rewards distribution", + "type": "array", + "items": { + "type": "string" + } } }, "additionalProperties": false, @@ -1227,17 +1226,6 @@ } ] }, - "slippage_tolerance": { - "description": "Maximum percentage of price movement (minimum amount you accept to receive during swap)", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" - }, - { - "type": "null" - } - ] - }, "timeout_seconds": { "description": "Number of seconds after which an IBC transfer is to be considered failed, if no acknowledgement is received", "type": [ @@ -1246,6 +1234,16 @@ ], "format": "uint64", "minimum": 0.0 + }, + "whitelist_actions": { + "description": "Actions to modify the whitelist of distributors", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/WhitelistAction" + } } }, "additionalProperties": false @@ -1269,6 +1267,54 @@ "l_o_c_k_e_d", "u_n_l_o_c_k_i_n_g" ] + }, + "WhitelistAction": { + "oneOf": [ + { + "description": "Add an address to the whitelist of distributors", + "type": "object", + "required": [ + "add_address" + ], + "properties": { + "add_address": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Remove an address from the whitelist of distributors", + "type": "object", + "required": [ + "remove_address" + ], + "properties": { + "remove_address": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] } } }, @@ -1307,8 +1353,8 @@ "revenue_share_tax_rate", "safety_fund_config", "safety_tax_rate", - "slippage_tolerance", - "timeout_seconds" + "timeout_seconds", + "whitelisted_distributors" ], "properties": { "address_provider": { @@ -1373,19 +1419,18 @@ } ] }, - "slippage_tolerance": { - "description": "Maximum percentage of price movement (minimum amount you accept to receive during swap)", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, "timeout_seconds": { "description": "Number of seconds after which an IBC transfer is to be considered failed, if no acknowledgement is received", "type": "integer", "format": "uint64", "minimum": 0.0 + }, + "whitelisted_distributors": { + "description": "List of addresses that are allowed to execute the rewards distribution", + "type": "array", + "items": { + "type": "string" + } } }, "additionalProperties": false, diff --git a/scripts/deploy/base/deployer.ts b/scripts/deploy/base/deployer.ts index f7390014c..6ace2773c 100644 --- a/scripts/deploy/base/deployer.ts +++ b/scripts/deploy/base/deployer.ts @@ -201,6 +201,7 @@ export class Deployer { zapper: this.storage.addresses.zapper!, health_contract: this.storage.addresses.health!, incentives: this.storage.addresses.incentives!, + swap_fee: this.config.swapFee, } await this.instantiate('creditManager', this.storage.codeIds.creditManager!, msg) @@ -399,7 +400,7 @@ export class Deployer { fee_collector_config: this.config.rewardsCollector.feeCollectorConfig, channel_id: this.config.rewardsCollector.channelId, timeout_seconds: this.config.rewardsCollector.timeoutSeconds, - slippage_tolerance: this.config.rewardsCollector.slippageTolerance, + whitelisted_distributors: this.config.rewardsCollector.whitelistedDistributors, } await this.instantiate('rewardsCollector', this.storage.codeIds['rewardsCollector']!, msg) } diff --git a/scripts/deploy/neutron/devnet-config.ts b/scripts/deploy/neutron/devnet-config.ts index 04a16db97..614287817 100644 --- a/scripts/deploy/neutron/devnet-config.ts +++ b/scripts/deploy/neutron/devnet-config.ts @@ -437,7 +437,7 @@ export const neutronDevnetConfig: DeploymentConfig = { target_denom: marsDenom, transfer_type: 'ibc', }, - slippageTolerance: '0.01', + whitelistedDistributors: [], }, incentives: { epochDuration: 604800, // 1 week @@ -481,4 +481,5 @@ export const neutronDevnetConfig: DeploymentConfig = { router: astroportRouter, incentives: astroportIncentives, }, + swapFee: '0.0005', } diff --git a/scripts/deploy/neutron/mainnet-config.ts b/scripts/deploy/neutron/mainnet-config.ts index 8b2d322ce..5b210d9f1 100644 --- a/scripts/deploy/neutron/mainnet-config.ts +++ b/scripts/deploy/neutron/mainnet-config.ts @@ -377,7 +377,7 @@ export const neutronMainnetConfig: DeploymentConfig = { target_denom: marsDenom, transfer_type: 'ibc', }, - slippageTolerance: '0.01', + whitelistedDistributors: [], }, incentives: { epochDuration: 604800, // 1 week @@ -397,4 +397,5 @@ export const neutronMainnetConfig: DeploymentConfig = { assets: [ntrnAsset, atomAsset, axlUSDCAsset], vaults: [], oracleConfigs: [usdOracle, axlUSDCOracle, marsOracle, atomOracle, ntrnOracle], + swapFee: '0.0005', } diff --git a/scripts/deploy/neutron/testnet-config.ts b/scripts/deploy/neutron/testnet-config.ts index 56e3766ea..20ce1e169 100644 --- a/scripts/deploy/neutron/testnet-config.ts +++ b/scripts/deploy/neutron/testnet-config.ts @@ -456,7 +456,7 @@ export const neutronTestnetConfig: DeploymentConfig = { target_denom: marsDenom, transfer_type: 'ibc', }, - slippageTolerance: '0.01', + whitelistedDistributors: [], }, incentives: { epochDuration: 604800, // 1 week @@ -496,4 +496,5 @@ export const neutronTestnetConfig: DeploymentConfig = { router: astroportRouter, incentives: astroportIncentives, }, + swapFee: '0.0005', } diff --git a/scripts/deploy/osmosis/mainnet-config.ts b/scripts/deploy/osmosis/mainnet-config.ts index ec44a7c5b..31d0d0790 100644 --- a/scripts/deploy/osmosis/mainnet-config.ts +++ b/scripts/deploy/osmosis/mainnet-config.ts @@ -1030,7 +1030,7 @@ export const osmosisMainnetConfig: DeploymentConfig = { target_denom: axlUSDC, transfer_type: 'ibc', }, - slippageTolerance: '0.01', + whitelistedDistributors: [], }, incentives: { epochDuration: 604800, // 1 week @@ -1084,5 +1084,5 @@ export const osmosisMainnetConfig: DeploymentConfig = { wbtcOsmoOracle, atomStAtomOracle, ], - // oracleConfigs: [osmoOracleTwap, atomOracleTwap, axlOracleTwap, stAtomOracleTwap, wbtcOracleTwap, axlUSDCOracleTwap, ethOracleTwap, atomOsmoOracle, usdcOsmoOracle, ethOsmoOracle, wbtcOsmoOracle, atomStAtomOracle], + swapFee: '0.0005', } diff --git a/scripts/deploy/osmosis/testnet-config.ts b/scripts/deploy/osmosis/testnet-config.ts index 5d6ef9952..9391a4921 100644 --- a/scripts/deploy/osmosis/testnet-config.ts +++ b/scripts/deploy/osmosis/testnet-config.ts @@ -271,7 +271,7 @@ export const osmosisTestnetConfig: DeploymentConfig = { target_denom: aUSDC, transfer_type: 'bank', }, - slippageTolerance: '0.01', + whitelistedDistributors: [], }, incentives: { epochDuration: 604800, // 1 week @@ -301,4 +301,5 @@ export const osmosisTestnetConfig: DeploymentConfig = { assets: [osmoAsset, atomAsset, USDCAsset], vaults: [usdcOsmoVault, atomOsmoVault], oracleConfigs: [osmoOracle, atomOracle, USDCOracle, atomOsmoOracle, usdcOsmoOracle], + swapFee: '0.0005', } diff --git a/scripts/types/config.ts b/scripts/types/config.ts index ebc7cbc32..e41547359 100644 --- a/scripts/types/config.ts +++ b/scripts/types/config.ts @@ -69,7 +69,7 @@ export interface DeploymentConfig { revenueShareConfig: RewardConfig safetyFundConfig: RewardConfig feeCollectorConfig: RewardConfig - slippageTolerance: string + whitelistedDistributors: string[] } incentives: { epochDuration: number @@ -92,6 +92,7 @@ export interface DeploymentConfig { vaults: VaultConfig[] oracleConfigs: OracleConfig[] astroportConfig?: AstroportConfig + swapFee: Decimal } export interface AssetConfig { diff --git a/scripts/types/generated/mars-credit-manager/MarsCreditManager.client.ts b/scripts/types/generated/mars-credit-manager/MarsCreditManager.client.ts index 43d834e74..4720802ce 100644 --- a/scripts/types/generated/mars-credit-manager/MarsCreditManager.client.ts +++ b/scripts/types/generated/mars-credit-manager/MarsCreditManager.client.ts @@ -149,6 +149,7 @@ export interface MarsCreditManagerReadOnlyInterface { limit?: number startAfter?: string }) => Promise + swapFeeRate: () => Promise } export class MarsCreditManagerQueryClient implements MarsCreditManagerReadOnlyInterface { client: CosmWasmClient @@ -171,6 +172,7 @@ export class MarsCreditManagerQueryClient implements MarsCreditManagerReadOnlyIn this.estimateWithdrawLiquidity = this.estimateWithdrawLiquidity.bind(this) this.vaultPositionValue = this.vaultPositionValue.bind(this) this.vaultBindings = this.vaultBindings.bind(this) + this.swapFeeRate = this.swapFeeRate.bind(this) } accountKind = async ({ accountId }: { accountId: string }): Promise => { return this.client.queryContractSmart(this.contractAddress, { @@ -340,6 +342,11 @@ export class MarsCreditManagerQueryClient implements MarsCreditManagerReadOnlyIn }, }) } + swapFeeRate = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + swap_fee_rate: {}, + }) + } } export interface MarsCreditManagerInterface extends MarsCreditManagerReadOnlyInterface { contractAddress: string diff --git a/scripts/types/generated/mars-credit-manager/MarsCreditManager.react-query.ts b/scripts/types/generated/mars-credit-manager/MarsCreditManager.react-query.ts index 3ab24d9a6..f557ceeab 100644 --- a/scripts/types/generated/mars-credit-manager/MarsCreditManager.react-query.ts +++ b/scripts/types/generated/mars-credit-manager/MarsCreditManager.react-query.ts @@ -216,6 +216,14 @@ export const marsCreditManagerQueryKeys = { args, }, ] as const, + swapFeeRate: (contractAddress: string | undefined, args?: Record) => + [ + { + ...marsCreditManagerQueryKeys.address(contractAddress)[0], + method: 'swap_fee_rate', + args, + }, + ] as const, } export interface MarsCreditManagerReactQuery { client: MarsCreditManagerQueryClient | undefined @@ -226,6 +234,21 @@ export interface MarsCreditManagerReactQuery { initialData?: undefined } } +export interface MarsCreditManagerSwapFeeRateQuery + extends MarsCreditManagerReactQuery {} +export function useMarsCreditManagerSwapFeeRateQuery({ + client, + options, +}: MarsCreditManagerSwapFeeRateQuery) { + return useQuery( + marsCreditManagerQueryKeys.swapFeeRate(client?.contractAddress), + () => (client ? client.swapFeeRate() : Promise.reject(new Error('Invalid client'))), + { + ...options, + enabled: !!client && (options?.enabled != undefined ? options.enabled : true), + }, + ) +} export interface MarsCreditManagerVaultBindingsQuery extends MarsCreditManagerReactQuery { args: { diff --git a/scripts/types/generated/mars-credit-manager/MarsCreditManager.types.ts b/scripts/types/generated/mars-credit-manager/MarsCreditManager.types.ts index 16aac04c9..2a2ae9eef 100644 --- a/scripts/types/generated/mars-credit-manager/MarsCreditManager.types.ts +++ b/scripts/types/generated/mars-credit-manager/MarsCreditManager.types.ts @@ -23,6 +23,7 @@ export interface InstantiateMsg { owner: string params: ParamsBaseForString red_bank: RedBankUnchecked + swap_fee: Decimal swapper: SwapperBaseForString zapper: ZapperBaseForString } @@ -462,6 +463,7 @@ export interface ConfigUpdates { oracle?: OracleBaseForString | null red_bank?: RedBankUnchecked | null rewards_collector?: string | null + swap_fee?: Decimal | null swapper?: SwapperBaseForString | null zapper?: ZapperBaseForString | null } @@ -554,6 +556,9 @@ export type QueryMsg = start_after?: string | null } } + | { + swap_fee_rate: {} + } export type VaultPositionAmount = | { unlocked: VaultAmount diff --git a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.client.ts b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.client.ts index e307ffd9d..d95e49275 100644 --- a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.client.ts +++ b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.client.ts @@ -14,6 +14,7 @@ import { RewardConfig, ExecuteMsg, OwnerUpdate, + WhitelistAction, Uint128, Action, ActionAmount, diff --git a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.react-query.ts b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.react-query.ts index 7f2e68e0b..6fcfe1127 100644 --- a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.react-query.ts +++ b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.react-query.ts @@ -15,6 +15,7 @@ import { RewardConfig, ExecuteMsg, OwnerUpdate, + WhitelistAction, Uint128, Action, ActionAmount, diff --git a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.types.ts b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.types.ts index 81987ce3d..88fcedc9a 100644 --- a/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.types.ts +++ b/scripts/types/generated/mars-rewards-collector-base/MarsRewardsCollectorBase.types.ts @@ -16,8 +16,8 @@ export interface InstantiateMsg { revenue_share_tax_rate: Decimal safety_fund_config: RewardConfig safety_tax_rate: Decimal - slippage_tolerance: Decimal timeout_seconds: number + whitelisted_distributors: string[] } export interface RewardConfig { target_denom: string @@ -81,6 +81,17 @@ export type OwnerUpdate = } } | 'clear_emergency_owner' +export type WhitelistAction = + | { + add_address: { + address: string + } + } + | { + remove_address: { + address: string + } + } export type Uint128 = string export type Action = | { @@ -220,8 +231,8 @@ export interface UpdateConfig { revenue_share_tax_rate?: Decimal | null safety_fund_config?: RewardConfig | null safety_tax_rate?: Decimal | null - slippage_tolerance?: Decimal | null timeout_seconds?: number | null + whitelist_actions?: WhitelistAction[] | null } export interface Coin { amount: Uint128 @@ -262,6 +273,6 @@ export interface ConfigResponse { revenue_share_tax_rate: Decimal safety_fund_config: RewardConfig safety_tax_rate: Decimal - slippage_tolerance: Decimal timeout_seconds: number + whitelisted_distributors: string[] }