From 58c66907b4f3fd5b800c66f272ab5a509a0d1216 Mon Sep 17 00:00:00 2001 From: Subham2804 Date: Thu, 4 Sep 2025 09:31:59 +0530 Subject: [PATCH 01/16] Add Fee rate change --- Cargo.lock | 12 + Cargo.toml | 2 + contracts/credit-manager/src/contract.rs | 14 +- contracts/credit-manager/src/lib.rs | 1 + contracts/credit-manager/src/perp.rs | 53 +- contracts/credit-manager/src/query.rs | 65 +++ contracts/credit-manager/src/staking.rs | 118 ++++ contracts/credit-manager/src/state.rs | 7 + contracts/credit-manager/src/swap.rs | 21 +- contracts/credit-manager/src/update_config.rs | 25 +- contracts/credit-manager/tests/tests/mod.rs | 4 + .../tests/tests/test_liquidate_vault.rs | 2 +- .../credit-manager/tests/tests/test_perp.rs | 6 +- .../tests/tests/test_perps_with_discount.rs | 418 +++++++++++++++ .../tests/tests/test_staking_tiers.rs | 503 ++++++++++++++++++ .../tests/tests/test_swap_with_discount.rs | 109 ++++ .../tests/tests/test_trading_fee.rs | 185 +++++++ .../tests/tests/test_update_config.rs | 4 + contracts/mock-dao-staking/Cargo.toml | 16 + contracts/mock-dao-staking/src/lib.rs | 59 ++ contracts/perps/src/contract.rs | 18 +- contracts/perps/src/deleverage.rs | 22 +- contracts/perps/src/position_management.rs | 73 ++- contracts/perps/src/query.rs | 118 +++- contracts/perps/src/state.rs | 4 + contracts/perps/src/utils.rs | 5 + .../perps/tests/tests/helpers/contracts.rs | 45 +- .../perps/tests/tests/helpers/mock_env.rs | 60 ++- contracts/perps/tests/tests/mod.rs | 1 + .../perps/tests/tests/test_accounting.rs | 6 +- .../tests/test_accounting_with_discount.rs | 225 ++++++++ contracts/perps/tests/tests/test_position.rs | 54 +- .../tests/tests/test_risk_verification.rs | 2 +- contracts/perps/tests/tests/test_vault.rs | 8 +- packages/testing/Cargo.toml | 1 + .../src/multitest/helpers/contracts.rs | 11 + .../testing/src/multitest/helpers/mock_env.rs | 142 ++++- packages/types/src/adapters/credit_manager.rs | 56 ++ packages/types/src/adapters/dao_staking.rs | 60 +++ packages/types/src/adapters/mod.rs | 2 + packages/types/src/adapters/perps.rs | 9 +- packages/types/src/address_provider.rs | 4 + .../types/src/credit_manager/instantiate.rs | 3 + packages/types/src/credit_manager/query.rs | 36 ++ packages/types/src/fee_tiers.rs | 38 ++ packages/types/src/lib.rs | 1 + packages/types/src/perps.rs | 28 + .../generated/mars-perps/MarsPerps.client.ts | 7 +- .../mars-perps/MarsPerps.react-query.ts | 1 + 49 files changed, 2552 insertions(+), 112 deletions(-) create mode 100644 contracts/credit-manager/src/staking.rs create mode 100644 contracts/credit-manager/tests/tests/test_perps_with_discount.rs create mode 100644 contracts/credit-manager/tests/tests/test_staking_tiers.rs create mode 100644 contracts/credit-manager/tests/tests/test_swap_with_discount.rs create mode 100644 contracts/credit-manager/tests/tests/test_trading_fee.rs create mode 100644 contracts/mock-dao-staking/Cargo.toml create mode 100644 contracts/mock-dao-staking/src/lib.rs create mode 100644 contracts/perps/tests/tests/test_accounting_with_discount.rs create mode 100644 packages/types/src/adapters/credit_manager.rs create mode 100644 packages/types/src/adapters/dao_staking.rs create mode 100644 packages/types/src/fee_tiers.rs diff --git a/Cargo.lock b/Cargo.lock index d2befe2a..89f54e9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2806,6 +2806,17 @@ dependencies = [ "mars-types", ] +[[package]] +name = "mars-mock-dao-staking" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema 1.5.7", + "cosmwasm-std 1.5.7", + "cw-storage-plus 1.2.0", + "mars-types", + "thiserror", +] + [[package]] name = "mars-mock-incentives" version = "2.2.0" @@ -3240,6 +3251,7 @@ dependencies = [ "mars-credit-manager", "mars-incentives", "mars-mock-astroport-incentives", + "mars-mock-dao-staking", "mars-mock-incentives", "mars-mock-oracle", "mars-mock-pyth", diff --git a/Cargo.toml b/Cargo.toml index e0af3b12..dc16034c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ members = [ "contracts/mock-pyth", "contracts/mock-red-bank", "contracts/mock-vault", + "contracts/mock-dao-staking", # packages "packages/chains/*", @@ -151,6 +152,7 @@ mars-mock-oracle = { path = "./contracts/mock-oracle" } mars-mock-red-bank = { path = "./contracts/mock-red-bank" } mars-mock-vault = { path = "./contracts/mock-vault" } mars-mock-rover-health = { path = "./contracts/mock-health" } +mars-mock-dao-staking = { path = "./contracts/mock-dao-staking" } mars-swapper-mock = { path = "./contracts/swapper/mock" } mars-zapper-mock = { path = "./contracts/v2-zapper/mock" } diff --git a/contracts/credit-manager/src/contract.rs b/contracts/credit-manager/src/contract.rs index 9c9d5374..de0fe140 100644 --- a/contracts/credit-manager/src/contract.rs +++ b/contracts/credit-manager/src/contract.rs @@ -15,11 +15,12 @@ use crate::{ migrations, perp::update_balance_after_deleverage, query::{ - query_accounts, query_all_coin_balances, query_all_debt_shares, - query_all_total_debt_shares, query_all_trigger_orders, + query_account_tier_and_discount, query_accounts, query_all_coin_balances, + query_all_debt_shares, query_all_total_debt_shares, query_all_trigger_orders, query_all_trigger_orders_for_account, 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_trading_fee, query_vault_bindings, query_vault_position_value, + query_vault_utilization, }, repay::repay_from_wallet, state::NEXT_TRIGGER_ID, @@ -172,6 +173,13 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> ContractResult { start_after, limit, } => to_json_binary(&query_vault_bindings(deps, start_after, limit)?), + QueryMsg::GetAccountTierAndDiscount { + account_id, + } => to_json_binary(&query_account_tier_and_discount(deps, &account_id)?), + QueryMsg::TradingFee { + account_id, + market_type, + } => to_json_binary(&query_trading_fee(deps, &account_id, &market_type)?), }; res.map_err(Into::into) } diff --git a/contracts/credit-manager/src/lib.rs b/contracts/credit-manager/src/lib.rs index ca5634de..29d76dff 100644 --- a/contracts/credit-manager/src/lib.rs +++ b/contracts/credit-manager/src/lib.rs @@ -21,6 +21,7 @@ pub mod reclaim; pub mod refund; pub mod repay; pub mod stake_astro_lp; +pub mod staking; pub mod state; pub mod swap; pub mod trigger; diff --git a/contracts/credit-manager/src/perp.rs b/contracts/credit-manager/src/perp.rs index 346f0172..6a29ea8e 100644 --- a/contracts/credit-manager/src/perp.rs +++ b/contracts/credit-manager/src/perp.rs @@ -1,7 +1,8 @@ use std::cmp::min; use cosmwasm_std::{ - coin, ensure_eq, BankMsg, Coin, CosmosMsg, DepsMut, Env, Int128, MessageInfo, Response, Uint128, + coin, ensure_eq, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, Env, Int128, MessageInfo, + Response, Uint128, }; use mars_types::{ adapters::perps::Perps, @@ -12,6 +13,7 @@ use mars_types::{ use crate::{ borrow, error::{ContractError, ContractResult}, + staking::get_account_tier_and_discount, state::{COIN_BALANCES, PERPS, RED_BANK}, trigger::remove_invalid_trigger_orders, utils::{decrement_coin_balance, increment_coin_balance}, @@ -94,6 +96,10 @@ pub fn execute_perp_order( let mut response = Response::new(); + // Get staking tier discount for this account + let (tier, discount_pct, voting_power) = + get_account_tier_and_discount(deps.as_ref(), account_id)?; + // Query the perp position PnL so that we know whether funds needs to be // sent to the perps contract // @@ -114,10 +120,12 @@ pub fn execute_perp_order( position, order_size, reduce_only, + discount_pct, )?, None => { // Open new position - let opening_fee = perps.query_opening_fee(&deps.querier, denom, order_size)?; + let base_opening_fee = perps.query_opening_fee(&deps.querier, denom, order_size, None)?; + let opening_fee = perps.query_opening_fee(&deps.querier, denom, order_size, Some(discount_pct))?; let fee = opening_fee.fee; let funds = if !fee.amount.is_zero() { @@ -128,7 +136,7 @@ pub fn execute_perp_order( }; let msg = - perps.execute_perp_order(account_id, denom, order_size, reduce_only, funds)?; + perps.execute_perp_order(account_id, denom, order_size, reduce_only, funds, Some(discount_pct))?; response .add_message(msg) @@ -138,6 +146,10 @@ pub fn execute_perp_order( .add_attribute("reduce_only", reduce_only.unwrap_or(false).to_string()) .add_attribute("new_size", order_size.to_string()) .add_attribute("opening_fee", fee.to_string()) + .add_attribute("base_opening_fee", base_opening_fee.fee.to_string()) + .add_attribute("voting_power", voting_power.to_string()) + .add_attribute("tier_id", tier.id) + .add_attribute("discount_pct", discount_pct.to_string()) } }) } @@ -149,6 +161,9 @@ pub fn close_perp_position( ) -> ContractResult { let perps = PERPS.load(deps.storage)?; + // Get staking tier discount for this account + let (_, discount_pct, _) = get_account_tier_and_discount(deps.as_ref(), account_id)?; + // Query the perp position PnL so that we know whether funds needs to be // sent to the perps contract // @@ -174,6 +189,7 @@ pub fn close_perp_position( position, order_size, Some(true), + discount_pct, )?) } None => Err(ContractError::NoPerpPosition { @@ -214,15 +230,30 @@ pub fn close_all_perps( let (funds, response) = update_state_based_on_pnl(&mut deps, account_id, pnl, Some(action.clone()), response)?; let funds = funds.map_or_else(Vec::new, |c| vec![c]); + println!("funds : {:?}", funds); + + // Get staking tier discount for this account + let (tier, discount_pct, voting_power) = + get_account_tier_and_discount(deps.as_ref(), account_id)?; + + // // Get base and effective fees for logging (using a sample denom for reference) + // let sample_denom = perp_positions.first().unwrap().denom.clone(); + // let base_opening_fee = perps.query_opening_fee(&deps.querier, &sample_denom, Int128::new(1000), None)?; + // let effective_opening_fee = perps.query_opening_fee(&deps.querier, &sample_denom, Int128::new(1000), Some(discount_pct))?; // Close all perp positions at once - let close_msg = perps.close_all_msg(account_id, funds, action)?; + let close_msg = perps.close_all_msg(account_id, funds, action, Some(discount_pct))?; Ok(response .add_message(close_msg) .add_attribute("action", "close_all_perps") .add_attribute("account_id", account_id) - .add_attribute("number_of_positions", perp_positions.len().to_string())) + .add_attribute("number_of_positions", perp_positions.len().to_string()) + // .add_attribute("base_opening_fee", base_opening_fee.fee.to_string()) + // .add_attribute("effective_opening_fee", effective_opening_fee.fee.to_string()) + .add_attribute("voting_power", voting_power.to_string()) + .add_attribute("tier_id", tier.id) + .add_attribute("discount_pct", discount_pct.to_string())) } fn modify_existing_position( @@ -234,13 +265,18 @@ fn modify_existing_position( position: PerpPosition, order_size: Int128, reduce_only: Option, + discount_pct: Decimal, ) -> ContractResult { let pnl = position.unrealized_pnl.to_coins(&position.base_denom).pnl; let pnl_string = position.unrealized_pnl.pnl.to_string(); let (funds, response) = update_state_based_on_pnl(&mut deps, account_id, pnl, None, response)?; let funds = funds.map_or_else(Vec::new, |c| vec![c]); - let msg = perps.execute_perp_order(account_id, denom, order_size, reduce_only, funds)?; + // Get base and effective fees for logging + let base_opening_fee = perps.query_opening_fee(&deps.querier, denom, order_size, None)?; + let effective_opening_fee = perps.query_opening_fee(&deps.querier, denom, order_size, Some(discount_pct))?; + + let msg = perps.execute_perp_order(account_id, denom, order_size, reduce_only, funds, Some(discount_pct))?; let new_size = position.size.checked_add(order_size)?; @@ -258,7 +294,10 @@ fn modify_existing_position( .add_attribute("realized_pnl", pnl_string) .add_attribute("reduce_only", reduce_only.unwrap_or(false).to_string()) .add_attribute("order_size", order_size.to_string()) - .add_attribute("new_size", new_size.to_string())) + .add_attribute("new_size", new_size.to_string()) + .add_attribute("base_opening_fee", base_opening_fee.fee.to_string()) + .add_attribute("effective_opening_fee", effective_opening_fee.fee.to_string()) + .add_attribute("discount_pct", discount_pct.to_string())) } /// Prepare the necessary messages and funds to be sent to the perps contract based on the PnL. diff --git a/contracts/credit-manager/src/query.rs b/contracts/credit-manager/src/query.rs index db8ef4ff..d0341088 100644 --- a/contracts/credit-manager/src/query.rs +++ b/contracts/credit-manager/src/query.rs @@ -345,3 +345,68 @@ pub fn query_vault_bindings( }) }) } + +pub fn query_account_tier_and_discount( + deps: Deps, + account_id: &str, +) -> ContractResult { + use crate::staking::get_account_tier_and_discount; + + let (tier, discount_pct, voting_power) = get_account_tier_and_discount(deps, account_id)?; + + Ok(mars_types::credit_manager::AccountTierAndDiscountResponse { + tier_id: tier.id, + discount_pct, + voting_power, + }) +} + +/// Queries the trading fee for a specific account and market type. +/// For spot markets, returns a default fee structure. +/// For perp markets, calculates the opening fee based on the denom and applies any applicable discounts. +pub fn query_trading_fee( + deps: Deps, + account_id: &str, + market_type: &mars_types::credit_manager::MarketType, +) -> ContractResult { + use crate::staking::get_account_tier_and_discount; + + // Get staking tier discount for this account + let (tier, discount_pct, _) = get_account_tier_and_discount(deps, account_id)?; + + match market_type { + mars_types::credit_manager::MarketType::Spot => { + // For spot markets, use a default fee structure + // You can customize this based on your spot trading requirements + let base_fee_pct = cosmwasm_std::Decimal::percent(25); // 0.25% base fee + let effective_fee_pct = + base_fee_pct.checked_mul(cosmwasm_std::Decimal::one() - discount_pct)?; + + Ok(mars_types::credit_manager::TradingFeeResponse { + base_fee_pct, + discount_pct, + effective_fee_pct, + tier_id: tier.id, + }) + } + mars_types::credit_manager::MarketType::Perp { + denom, + } => { + // For perp markets, get the opening fee rate and apply discount + let params = crate::state::PARAMS.load(deps.storage)?; + + // Query the params contract to get the opening fee rate for this denom + let perp_params = params.query_perp_params(&deps.querier, denom)?; + let base_fee_pct = perp_params.opening_fee_rate; + let effective_fee_pct = + base_fee_pct.checked_mul(cosmwasm_std::Decimal::one() - discount_pct)?; + + Ok(mars_types::credit_manager::TradingFeeResponse { + base_fee_pct, + discount_pct, + effective_fee_pct, + tier_id: tier.id, + }) + } + } +} diff --git a/contracts/credit-manager/src/staking.rs b/contracts/credit-manager/src/staking.rs new file mode 100644 index 00000000..9ca45863 --- /dev/null +++ b/contracts/credit-manager/src/staking.rs @@ -0,0 +1,118 @@ +use crate::state::{DAO_STAKING_ADDRESS, FEE_TIER_CONFIG}; +use crate::utils::query_nft_token_owner; +use cosmwasm_std::{Decimal, Deps, StdError, StdResult, Uint128}; +use mars_types::adapters::dao_staking::DaoStaking; +use mars_types::fee_tiers::{FeeTier, FeeTierConfig}; +use std::str::FromStr; + +pub struct StakingTierManager { + pub config: FeeTierConfig, +} + +impl StakingTierManager { + pub fn new(config: FeeTierConfig) -> Self { + Self { + config, + } + } + + /// Find the applicable tier for a given voting power + /// Returns the tier with the highest min_voting_power that the user qualifies for + pub fn find_applicable_tier(&self, voting_power: Uint128) -> StdResult<&FeeTier> { + // Tiers should be sorted in descending order of min_voting_power + // So we find the first tier where user's voting power >= tier's min_voting_power + for tier in &self.config.tiers { + let min_power = Uint128::from_str(&tier.min_voting_power) + .map_err(|_| StdError::generic_err("Invalid min_voting_power in tier"))?; + + if voting_power >= min_power { + return Ok(tier); + } + } + + // If no tier found, return the last tier (lowest threshold) + self.config.tiers.last().ok_or_else(|| StdError::generic_err("No tiers configured")) + } + + /// Validate that tiers are properly ordered by min_voting_power (descending) + pub fn validate(&self) -> StdResult<()> { + if self.config.tiers.is_empty() { + return Err(StdError::generic_err("Fee tier config cannot be empty")); + } + + // Check for descending order and duplicates in one pass + for i in 1..self.config.tiers.len() { + let prev_power = Uint128::from_str(&self.config.tiers[i - 1].min_voting_power) + .map_err(|_| StdError::generic_err("Invalid min_voting_power in tier"))?; + let curr_power = Uint128::from_str(&self.config.tiers[i].min_voting_power) + .map_err(|_| StdError::generic_err("Invalid min_voting_power in tier"))?; + + if curr_power == prev_power { + return Err(StdError::generic_err("Duplicate voting power thresholds")); + } + + if curr_power >= prev_power { + return Err(StdError::generic_err("Tiers must be sorted in descending order")); + } + } + + // Validate discount percentages are reasonable (0-100%) + for tier in &self.config.tiers { + if tier.discount_pct >= Decimal::one() { + return Err(StdError::generic_err("Discount percentage must be less than 100%")); + } + } + + Ok(()) + } + + /// Get the default tier (tier with lowest min_voting_power) + pub fn get_default_tier(&self) -> StdResult<&FeeTier> { + let mut default_tier: Option<&FeeTier> = None; + let mut lowest_power = Uint128::MAX; + + for tier in &self.config.tiers { + let min_power = Uint128::from_str(&tier.min_voting_power) + .map_err(|_| StdError::generic_err("Invalid min_voting_power in tier"))?; + + if min_power < lowest_power { + default_tier = Some(tier); + lowest_power = min_power; + } + } + + default_tier.ok_or_else(|| StdError::generic_err("No tiers configured")) + } +} + +/// Get tier, discount percentage, and voting power for an account based on their staked MARS balance +/// +/// # Arguments +/// * `deps` - Contract dependencies +/// * `account_id` - The account ID to check +/// +/// # Returns +/// * `StdResult<(FeeTier, Decimal, Uint128)>` - The applicable tier, discount percentage, and voting power +pub fn get_account_tier_and_discount( + deps: Deps, + account_id: &str, +) -> StdResult<(FeeTier, Decimal, Uint128)> { + // 1. Get account owner from account_id + let account_owner = query_nft_token_owner(deps, account_id) + .map_err(|e| StdError::generic_err(e.to_string()))?; + + // 2. Get DAO staking contract address from state + let dao_staking_addr = DAO_STAKING_ADDRESS.load(deps.storage)?; + let dao_staking = DaoStaking::new(dao_staking_addr); + + // 3. Query voting power for the account owner + let voting_power_response = + dao_staking.query_voting_power_at_height(&deps.querier, &account_owner)?; + + // 4. Get fee tier config and find applicable tier + let fee_tier_config = FEE_TIER_CONFIG.load(deps.storage)?; + let manager = StakingTierManager::new(fee_tier_config); + let tier = manager.find_applicable_tier(voting_power_response.power)?; + + Ok((tier.clone(), tier.discount_pct, voting_power_response.power)) +} diff --git a/contracts/credit-manager/src/state.rs b/contracts/credit-manager/src/state.rs index 964b0a7a..2e379864 100644 --- a/contracts/credit-manager/src/state.rs +++ b/contracts/credit-manager/src/state.rs @@ -8,6 +8,7 @@ use mars_types::{ swapper::Swapper, vault::VaultPositionAmount, zapper::Zapper, }, credit_manager::{KeeperFeeConfig, TriggerOrder}, + fee_tiers::FeeTierConfig, health::AccountKind, }; use mars_utils::guard::Guard; @@ -67,3 +68,9 @@ pub const VAULTS: Map<&str, Addr> = Map::new("vaults"); pub const PERPS_LB_RATIO: Item = Item::new("perps_lb_ratio"); pub const SWAP_FEE: Item = Item::new("swap_fee"); + +// Fee tier discount configuration +pub const FEE_TIER_CONFIG: Item = Item::new("fee_tier_config"); + +// DAO staking contract address +pub const DAO_STAKING_ADDRESS: Item = Item::new("dao_staking_address"); diff --git a/contracts/credit-manager/src/swap.rs b/contracts/credit-manager/src/swap.rs index 5edba3c9..b1e8eef4 100644 --- a/contracts/credit-manager/src/swap.rs +++ b/contracts/credit-manager/src/swap.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Coin, DepsMut, Env, Response, Uint128}; +use cosmwasm_std::{Coin, Decimal, DepsMut, Env, Response, Uint128}; use mars_types::{ credit_manager::{ActionAmount, ActionCoin, ChangeExpected}, swapper::SwapperRoute, @@ -6,6 +6,7 @@ use mars_types::{ use crate::{ error::{ContractError, ContractResult}, + staking::get_account_tier_and_discount, state::{COIN_BALANCES, DUALITY_SWAPPER, REWARDS_COLLECTOR, SWAPPER, SWAP_FEE}, utils::{ assert_withdraw_enabled, decrement_coin_balance, increment_coin_balance, update_balance_msg, @@ -40,9 +41,14 @@ 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)?; + // Get staking tier discount for this account + let (tier, discount_pct, voting_power) = + get_account_tier_and_discount(deps.as_ref(), account_id)?; + + // Apply discount to swap fee + let base_swap_fee = SWAP_FEE.load(deps.storage)?; + let effective_swap_fee = base_swap_fee * (Decimal::one() - discount_pct); + let swap_fee_amount = coin_in_to_trade.amount.checked_mul_floor(effective_swap_fee)?; coin_in_to_trade.amount = coin_in_to_trade.amount.checked_sub(swap_fee_amount)?; // Send to Rewards collector @@ -74,5 +80,10 @@ pub fn swap_exact_in( .add_attribute("action", "swapper") .add_attribute("account_id", account_id) .add_attribute("coin_in", coin_in_to_trade.to_string()) - .add_attribute("denom_out", denom_out)) + .add_attribute("denom_out", denom_out) + .add_attribute("voting_power", voting_power.to_string()) + .add_attribute("tier_id", tier.id) + .add_attribute("discount_pct", discount_pct.to_string()) + .add_attribute("base_swap_fee", base_swap_fee.to_string()) + .add_attribute("effective_swap_fee", effective_swap_fee.to_string())) } diff --git a/contracts/credit-manager/src/update_config.rs b/contracts/credit-manager/src/update_config.rs index cf364aa6..b4a5194c 100644 --- a/contracts/credit-manager/src/update_config.rs +++ b/contracts/credit-manager/src/update_config.rs @@ -11,10 +11,12 @@ use mars_types::{ use crate::{ error::ContractResult, execute::create_credit_account, + staking::StakingTierManager, state::{ - ACCOUNT_NFT, DUALITY_SWAPPER, HEALTH_CONTRACT, INCENTIVES, KEEPER_FEE_CONFIG, MAX_SLIPPAGE, - MAX_TRIGGER_ORDERS, MAX_UNLOCKING_POSITIONS, ORACLE, OWNER, PARAMS, PERPS, PERPS_LB_RATIO, - RED_BANK, REWARDS_COLLECTOR, SWAPPER, SWAP_FEE, ZAPPER, + ACCOUNT_NFT, DAO_STAKING_ADDRESS, DUALITY_SWAPPER, FEE_TIER_CONFIG, HEALTH_CONTRACT, + INCENTIVES, KEEPER_FEE_CONFIG, MAX_SLIPPAGE, MAX_TRIGGER_ORDERS, MAX_UNLOCKING_POSITIONS, + ORACLE, OWNER, PARAMS, PERPS, PERPS_LB_RATIO, RED_BANK, REWARDS_COLLECTOR, SWAPPER, + SWAP_FEE, ZAPPER, }, utils::{assert_max_slippage, assert_perps_lb_ratio, assert_swap_fee}, }; @@ -137,6 +139,23 @@ pub fn update_config( response.add_attribute("key", "perps").add_attribute("value", unchecked.address()); } + // Staking tier config and DAO staking address + if let Some(cfg) = updates.fee_tier_config { + // Validate the staking tier configuration before saving + let manager = StakingTierManager::new(cfg.clone()); + manager.validate()?; + + FEE_TIER_CONFIG.save(deps.storage, &cfg)?; + response = response.add_attribute("key", "fee_tier_config"); + } + + if let Some(addr) = updates.dao_staking_address { + let checked = deps.api.addr_validate(&addr)?; + DAO_STAKING_ADDRESS.save(deps.storage, &checked)?; + response = + response.add_attribute("key", "dao_staking_address").add_attribute("value", addr); + } + if let Some(kfc) = updates.keeper_fee_config { KEEPER_FEE_CONFIG.save(deps.storage, &kfc)?; response = response.add_attributes(vec![ diff --git a/contracts/credit-manager/tests/tests/mod.rs b/contracts/credit-manager/tests/tests/mod.rs index 987afad6..a6f0abc5 100644 --- a/contracts/credit-manager/tests/tests/mod.rs +++ b/contracts/credit-manager/tests/tests/mod.rs @@ -32,6 +32,7 @@ mod test_order_relations; mod test_perp; mod test_perp_vault; mod test_perps_deleverage; +mod test_perps_with_discount; mod test_reclaim; mod test_reentrancy_guard; mod test_refund_balances; @@ -39,7 +40,10 @@ mod test_repay; mod test_repay_for_recipient; mod test_repay_from_wallet; mod test_stake_astro_lp; +mod test_staking_tiers; mod test_swap; +mod test_swap_with_discount; +mod test_trading_fee; mod test_trigger; mod test_unstake_astro_lp; mod test_update_admin; diff --git a/contracts/credit-manager/tests/tests/test_liquidate_vault.rs b/contracts/credit-manager/tests/tests/test_liquidate_vault.rs index 58c27f1f..5a199594 100644 --- a/contracts/credit-manager/tests/tests/test_liquidate_vault.rs +++ b/contracts/credit-manager/tests/tests/test_liquidate_vault.rs @@ -74,7 +74,7 @@ fn liquidatee_must_have_the_request_vault_position() { assert_err( res, ContractError::Std(NotFound { - kind: "type: mars_types::adapters::vault::amount::VaultPositionAmount; key: [00, 0F, 76, 61, 75, 6C, 74, 5F, 70, 6F, 73, 69, 74, 69, 6F, 6E, 73, 00, 01, 32, 63, 6F, 6E, 74, 72, 61, 63, 74, 31, 34]".to_string(), + kind: "type: mars_types::adapters::vault::amount::VaultPositionAmount; key: [00, 0F, 76, 61, 75, 6C, 74, 5F, 70, 6F, 73, 69, 74, 69, 6F, 6E, 73, 00, 01, 32, 63, 6F, 6E, 74, 72, 61, 63, 74, 31, 35]".to_string(), }), ) } diff --git a/contracts/credit-manager/tests/tests/test_perp.rs b/contracts/credit-manager/tests/tests/test_perp.rs index e9380a60..e9fd4da6 100644 --- a/contracts/credit-manager/tests/tests/test_perp.rs +++ b/contracts/credit-manager/tests/tests/test_perp.rs @@ -96,7 +96,7 @@ fn open_position_with_correct_payment( // Check perp data before any action let vault_usdc_balance = mock.query_balance(mock.perps.address(), &usdc_info.denom); - let opening_fee = mock.query_perp_opening_fee(&atom_info.denom, perp_size); + let opening_fee = mock.query_perp_opening_fee(&atom_info.denom, perp_size, None); assert_eq!(opening_fee.fee.amount.u128(), 49); // Open perp position @@ -313,7 +313,7 @@ fn close_perp_position_with_profit() { // check perp data before any action let vault_usdc_balance = mock.query_balance(mock.perps.address(), &usdc_info.denom); - let opening_fee = mock.query_perp_opening_fee(&atom_info.denom, perp_size); + let opening_fee = mock.query_perp_opening_fee(&atom_info.denom, perp_size, None); // open perp position mock.update_credit_account( @@ -1018,7 +1018,7 @@ fn close_perp_position() { // check perp data before any action let vault_usdc_balance = mock.query_balance(mock.perps.address(), &usdc_info.denom); - let opening_fee = mock.query_perp_opening_fee(&atom_info.denom, perp_size); + let opening_fee = mock.query_perp_opening_fee(&atom_info.denom, perp_size, None); // wrongly try to close a not existing position let res = mock.update_credit_account( diff --git a/contracts/credit-manager/tests/tests/test_perps_with_discount.rs b/contracts/credit-manager/tests/tests/test_perps_with_discount.rs new file mode 100644 index 00000000..53732d3b --- /dev/null +++ b/contracts/credit-manager/tests/tests/test_perps_with_discount.rs @@ -0,0 +1,418 @@ +use cosmwasm_std::{Addr, Coin, Decimal, Int128, Uint128}; +use cw_multi_test::AppResponse; +use mars_types::credit_manager::Action::{Deposit, ExecutePerpOrder}; +use mars_types::credit_manager::ExecutePerpOrderType; +use test_case::test_case; + +use super::helpers::{coin_info, uatom_info, AccountToFund, MockEnv}; +use crate::tests::helpers::default_perp_params; +use mars_types::params::PerpParamsUpdate; + +fn setup_env() -> (MockEnv, Addr, String) { + let atom = uatom_info(); + let usdc = coin_info("uusdc"); + let user = Addr::unchecked("user"); + + let mut mock = MockEnv::new() + .set_params(&[atom.clone(), usdc.clone()]) + .fund_account(AccountToFund { + addr: user.clone(), + funds: vec![Coin::new(100_000, usdc.denom.clone())], + }) + .build() + .unwrap(); + + let account_id = mock.create_credit_account(&user).unwrap(); + + // Setup perps params for the market and seed vault liquidity via CM + mock.update_perp_params(PerpParamsUpdate::AddOrUpdate { + params: default_perp_params(&atom.denom), + }); + // Fund the CM (rover) with USDC and deposit into perps vault so opening fees can be handled + let rover_addr = mock.rover.clone(); + let usdc_denom = usdc.denom.clone(); + mock.fund_addr(&rover_addr, vec![Coin::new(100_0000, usdc_denom.clone())]); + mock.deposit_to_perp_vault(&account_id, &Coin::new(50_0000, usdc_denom), None).unwrap(); + (mock, user, account_id) +} + +fn open_perp( + mock: &mut MockEnv, + account_id: &str, + user: &Addr, + denom: &str, + size: Int128, + usdc_to_deposit: u128, +) -> AppResponse { + // Ensure some USDC deposit to pay opening fee + mock.update_credit_account( + account_id, + user, + vec![ + Deposit(Coin::new(usdc_to_deposit, "uusdc")), + ExecutePerpOrder { + denom: denom.to_string(), + order_size: size, + reduce_only: None, + order_type: Some(ExecutePerpOrderType::Default), + }, + ], + &[Coin::new(usdc_to_deposit, "uusdc")], + ) + .unwrap() +} + +// Test fee discount across different voting power tiers and scenarios +#[test_case( + 0, + "tier_10", + Decimal::percent(0), + 200; + "tier 10: 0 power -> 0% discount, size 200" +)] +#[test_case( + 25_000, + "tier_5", + Decimal::percent(25), + 200; + "tier 5: >= 25_000 power -> 25% discount, size 200" +)] +#[test_case( + 200_000, + "tier_2", + Decimal::percent(60), + 200; + "tier 2: >= 200_000 power -> 60% discount, size 200" +)] +#[test_case( + 50_000, + "tier_4", + Decimal::percent(35), + 100; + "tier 4: >= 50_000 power -> 35% discount, size 100" +)] +#[test_case( + 150_000, + "tier_3", + Decimal::percent(45), + 500; + "tier 3: >= 150_000 power -> 45% discount, size 500" +)] +#[test_case( + 10_000, + "tier_6", + Decimal::percent(15), + 150; + "tier 6: >= 10_000 power -> 15% discount, size 150" +)] +#[test_case( + 5_000, + "tier_7", + Decimal::percent(10), + 300; + "tier 7: >= 5_000 power -> 10% discount, size 300" +)] +#[test_case( + 1_000, + "tier_8", + Decimal::percent(5), + 400; + "tier 8: >= 1_000 power -> 5% discount, size 400" +)] +#[test_case( + 100, + "tier_9", + Decimal::percent(1), + 600; + "tier 9: >= 100 power -> 1% discount, size 600" +)] +#[test_case( + 350_000, + "tier_1", + Decimal::percent(75), + 800; + "tier 1: >= 350_000 power -> 75% discount, size 800" +)] +fn test_perps_with_discount_events( + voting_power: u128, + expected_tier: &str, + expected_discount: Decimal, + position_size: i128, +) { + let (mut mock, user, account_id) = setup_env(); + let atom = uatom_info(); + + // Set voting power for this test case + mock.set_voting_power(&user, Uint128::new(voting_power)); + + let find_attrs = |res: &AppResponse| { + let evt = res + .events + .iter() + .find(|e| e.attributes.iter().any(|a| a.key == "discount_pct")) + .or_else(|| { + res.events.iter().find(|e| { + e.attributes.iter().any(|a| { + a.key == "action" + && (a.value == "open_perp_position" || a.value == "execute_perp_order") + }) + }) + }) + .expect("expected perps event with discount attributes"); + evt.attributes + .iter() + .map(|a| (a.key.clone(), a.value.clone())) + .collect::>() + }; + + // Open perp position and verify discount attributes + let res = + open_perp(&mut mock, &account_id, &user, &atom.denom, Int128::new(position_size), 10_000); + let attrs = find_attrs(&res); + + assert_eq!(attrs.get("voting_power").unwrap(), &voting_power.to_string()); + assert_eq!(attrs.get("tier_id").unwrap(), expected_tier); + assert_eq!(attrs.get("discount_pct").unwrap(), &expected_discount.to_string()); + assert_eq!(attrs.get("new_size").unwrap(), &position_size.to_string()); +} + +// Test close_perp_position with discount functionality +#[test_case( + 0, + "tier_10", + Decimal::percent(0); + "close_perp_position tier 10: 0 power -> 0% discount" +)] +#[test_case( + 25_000, + "tier_5", + Decimal::percent(25); + "close_perp_position tier 5: >= 25_000 power -> 25% discount" +)] +#[test_case( + 200_000, + "tier_2", + Decimal::percent(60); + "close_perp_position tier 2: >= 200_000 power -> 60% discount" +)] +#[test_case( + 50_000, + "tier_4", + Decimal::percent(35); + "close_perp_position tier 4: >= 50_000 power -> 35% discount" +)] +#[test_case( + 100_000, + "tier_3", + Decimal::percent(45); + "close_perp_position tier 3: >= 100_000 power -> 45% discount" +)] +#[test_case( + 350_000, + "tier_1", + Decimal::percent(75); + "close_perp_position tier 1: >= 350_000 power -> 75% discount" +)] +fn test_close_perp_position_with_discount( + voting_power: u128, + _expected_tier: &str, + expected_discount: Decimal, +) { + let (mut mock, user, account_id) = setup_env(); + let atom = uatom_info(); + + // Set voting power for this test case + mock.set_voting_power(&user, Uint128::new(voting_power)); + + // Open a position first + let res = open_perp(&mut mock, &account_id, &user, &atom.denom, Int128::new(200), 10_000); + let attrs = find_attrs(&res); + assert_eq!(attrs.get("action").unwrap(), "open_perp_position"); + assert_eq!(attrs.get("discount_pct").unwrap(), &expected_discount.to_string()); + + // Now close the position and verify discount is applied + let close_res = mock + .update_credit_account( + &account_id, + &user, + vec![mars_types::credit_manager::Action::ClosePerpPosition { + denom: atom.denom.clone(), + }], + &[], + ) + .unwrap(); + + // Find the execute_perp_order event (which is what close_perp_position actually emits) + let close_attrs = close_res + .events + .iter() + .find(|e| e.attributes.iter().any(|a| a.key == "action" && a.value == "execute_perp_order")) + .expect("expected execute_perp_order event from close_perp_position") + .attributes + .iter() + .map(|a| (a.key.clone(), a.value.clone())) + .collect::>(); + + // Verify discount attributes are present in the execute_perp_order event + assert_eq!(close_attrs.get("discount_pct").unwrap(), &expected_discount.to_string()); + assert_eq!(close_attrs.get("reduce_only").unwrap(), "true"); // close_perp_position sets reduce_only=true + assert_eq!(close_attrs.get("order_size").unwrap(), "-200"); // negative size to close position + assert_eq!(close_attrs.get("new_size").unwrap(), "0"); // position should be closed (size 0) +} + +// Test multiple perp positions with discount functionality +#[test_case( + 0, + "tier_10", + Decimal::percent(0); + "multiple positions tier 10: 0 power -> 0% discount" +)] +#[test_case( + 25_000, + "tier_5", + Decimal::percent(25); + "multiple positions tier 5: >= 25_000 power -> 25% discount" +)] +#[test_case( + 200_000, + "tier_2", + Decimal::percent(60); + "multiple positions tier 2: >= 200_000 power -> 60% discount" +)] +#[test_case( + 50_000, + "tier_4", + Decimal::percent(35); + "multiple positions tier 4: >= 50_000 power -> 35% discount" +)] +#[test_case( + 100_000, + "tier_3", + Decimal::percent(45); + "multiple positions tier 3: >= 100_000 power -> 45% discount" +)] +#[test_case( + 350_000, + "tier_1", + Decimal::percent(75); + "multiple positions tier 1: >= 350_000 power -> 75% discount" +)] +fn test_multiple_perp_positions_with_discount( + voting_power: u128, + expected_tier: &str, + expected_discount: Decimal, +) { + let (mut mock, user, account_id) = setup_env(); + let atom = uatom_info(); + + // Set voting power for this test case + mock.set_voting_power(&user, Uint128::new(voting_power)); + + // Create additional credit accounts for multiple positions + let account_id_2 = mock.create_credit_account(&user).unwrap(); + let account_id_3 = mock.create_credit_account(&user).unwrap(); + + // Fund additional accounts with USDC for perps vault + let rover_addr = mock.rover.clone(); + let usdc_denom = "uusdc".to_string(); + mock.fund_addr(&rover_addr, vec![Coin::new(100_0000, usdc_denom.clone())]); + mock.deposit_to_perp_vault(&account_id_2, &Coin::new(50_0000, usdc_denom.clone()), None) + .unwrap(); + mock.deposit_to_perp_vault(&account_id_3, &Coin::new(50_0000, usdc_denom), None).unwrap(); + + // Open first position on account_id + let res1 = open_perp(&mut mock, &account_id, &user, &atom.denom, Int128::new(200), 10_000); + let attrs1 = find_attrs(&res1); + assert_eq!(attrs1.get("action").unwrap(), "open_perp_position"); + assert_eq!(attrs1.get("discount_pct").unwrap(), &expected_discount.to_string()); + assert_eq!(attrs1.get("tier_id").unwrap(), expected_tier); + assert_eq!(attrs1.get("voting_power").unwrap(), &voting_power.to_string()); + + // Open a second position on account_id_2 + let res2 = open_perp(&mut mock, &account_id_2, &user, &atom.denom, Int128::new(100), 10_000); + let attrs2 = find_attrs(&res2); + assert_eq!(attrs2.get("action").unwrap(), "open_perp_position"); + assert_eq!(attrs2.get("discount_pct").unwrap(), &expected_discount.to_string()); + assert_eq!(attrs2.get("tier_id").unwrap(), expected_tier); + assert_eq!(attrs2.get("voting_power").unwrap(), &voting_power.to_string()); + + // Close first position and verify discount is applied + let close_res1 = mock + .update_credit_account( + &account_id, + &user, + vec![mars_types::credit_manager::Action::ClosePerpPosition { + denom: atom.denom.clone(), + }], + &[], + ) + .unwrap(); + + // Find the execute_perp_order event from closing the first position + let close_attrs1 = close_res1 + .events + .iter() + .find(|e| e.attributes.iter().any(|a| a.key == "action" && a.value == "execute_perp_order")) + .expect("expected execute_perp_order event from close_perp_position") + .attributes + .iter() + .map(|a| (a.key.clone(), a.value.clone())) + .collect::>(); + + // Verify discount attributes are present in the close event + assert_eq!(close_attrs1.get("discount_pct").unwrap(), &expected_discount.to_string()); + assert_eq!(close_attrs1.get("reduce_only").unwrap(), "true"); + assert_eq!(close_attrs1.get("order_size").unwrap(), "-200"); // negative to close 200 size + + // Close second position and verify discount is applied + let close_res2 = mock + .update_credit_account( + &account_id_2, + &user, + vec![mars_types::credit_manager::Action::ClosePerpPosition { + denom: atom.denom.clone(), + }], + &[], + ) + .unwrap(); + + // Find the execute_perp_order event from closing the second position + let close_attrs2 = close_res2 + .events + .iter() + .find(|e| e.attributes.iter().any(|a| a.key == "action" && a.value == "execute_perp_order")) + .expect("expected execute_perp_order event from close_perp_position") + .attributes + .iter() + .map(|a| (a.key.clone(), a.value.clone())) + .collect::>(); + + // Verify discount attributes are present in the second close event + assert_eq!(close_attrs2.get("discount_pct").unwrap(), &expected_discount.to_string()); + assert_eq!(close_attrs2.get("reduce_only").unwrap(), "true"); + assert_eq!(close_attrs2.get("order_size").unwrap(), "-100"); // negative to close 100 size +} + +// Helper function to find attributes (extracted to avoid duplication) +fn find_attrs(res: &AppResponse) -> std::collections::HashMap { + // First try to find any perps event with discount_pct (including "0") + let evt = res + .events + .iter() + .find(|e| e.attributes.iter().any(|a| a.key == "discount_pct")) + .or_else(|| { + // Fallback: look for perps events by action type + res.events.iter().find(|e| { + e.attributes.iter().any(|a| { + a.key == "action" + && (a.value == "open_perp_position" || a.value == "execute_perp_order") + }) + }) + }) + .expect("expected perps event with discount attributes"); + + evt.attributes + .iter() + .map(|a| (a.key.clone(), a.value.clone())) + .collect::>() +} diff --git a/contracts/credit-manager/tests/tests/test_staking_tiers.rs b/contracts/credit-manager/tests/tests/test_staking_tiers.rs new file mode 100644 index 00000000..b9c43be9 --- /dev/null +++ b/contracts/credit-manager/tests/tests/test_staking_tiers.rs @@ -0,0 +1,503 @@ +use cosmwasm_std::{Decimal, StdError, Uint128}; +use mars_credit_manager::staking::StakingTierManager; +use mars_types::fee_tiers::{FeeTier, FeeTierConfig}; +use test_case::test_case; + +// Test data based on the tier breakdown provided +fn create_test_fee_tier_config() -> FeeTierConfig { + FeeTierConfig { + tiers: vec![ + FeeTier { + id: "tier_1".to_string(), + min_voting_power: "350000".to_string(), + discount_pct: Decimal::percent(75), + }, + FeeTier { + id: "tier_2".to_string(), + min_voting_power: "200000".to_string(), + discount_pct: Decimal::percent(60), + }, + FeeTier { + id: "tier_3".to_string(), + min_voting_power: "100000".to_string(), + discount_pct: Decimal::percent(45), + }, + FeeTier { + id: "tier_4".to_string(), + min_voting_power: "50000".to_string(), + discount_pct: Decimal::percent(35), + }, + FeeTier { + id: "tier_5".to_string(), + min_voting_power: "25000".to_string(), + discount_pct: Decimal::percent(25), + }, + FeeTier { + id: "tier_6".to_string(), + min_voting_power: "10000".to_string(), + discount_pct: Decimal::percent(15), + }, + FeeTier { + id: "tier_7".to_string(), + min_voting_power: "5000".to_string(), + discount_pct: Decimal::percent(10), + }, + FeeTier { + id: "tier_8".to_string(), + min_voting_power: "1000".to_string(), + discount_pct: Decimal::percent(5), + }, + FeeTier { + id: "tier_9".to_string(), + min_voting_power: "100".to_string(), + discount_pct: Decimal::percent(1), + }, + FeeTier { + id: "tier_10".to_string(), + min_voting_power: "0".to_string(), + discount_pct: Decimal::percent(0), + }, + ], + } +} + +#[test] +fn test_staking_tier_manager_creation() { + let config = create_test_fee_tier_config(); + let manager = StakingTierManager::new(config); + + assert_eq!(manager.config.tiers.len(), 10); + assert_eq!(manager.config.tiers[0].id, "tier_1"); + assert_eq!(manager.config.tiers[9].id, "tier_10"); +} + +#[test_case( + Uint128::new(350000), + "tier_1", + Decimal::percent(75); + "exact match tier 1" +)] +#[test_case( + Uint128::new(200000), + "tier_2", + Decimal::percent(60); + "exact match tier 2" +)] +#[test_case( + Uint128::new(100000), + "tier_3", + Decimal::percent(45); + "exact match tier 3" +)] +#[test_case( + Uint128::new(50000), + "tier_4", + Decimal::percent(35); + "exact match tier 4" +)] +#[test_case( + Uint128::new(25000), + "tier_5", + Decimal::percent(25); + "exact match tier 5" +)] +#[test_case( + Uint128::new(10000), + "tier_6", + Decimal::percent(15); + "exact match tier 6" +)] +#[test_case( + Uint128::new(5000), + "tier_7", + Decimal::percent(10); + "exact match tier 7" +)] +#[test_case( + Uint128::new(1000), + "tier_8", + Decimal::percent(5); + "exact match tier 8" +)] +#[test_case( + Uint128::new(100), + "tier_9", + Decimal::percent(1); + "exact match tier 9" +)] +#[test_case( + Uint128::new(0), + "tier_10", + Decimal::percent(0); + "exact match tier 10" +)] +fn test_find_applicable_tier_exact_matches( + voting_power: Uint128, + expected_tier_id: &str, + expected_discount: Decimal, +) { + let config = create_test_fee_tier_config(); + let manager = StakingTierManager::new(config); + + let tier = manager.find_applicable_tier(voting_power).unwrap(); + assert_eq!(tier.id, expected_tier_id); + assert_eq!(tier.discount_pct, expected_discount); +} + +#[test_case( + Uint128::new(300000), + "tier_2", + Decimal::percent(60); + "between tier 1 and tier 2" +)] +#[test_case( + Uint128::new(150000), + "tier_3", + Decimal::percent(45); + "between tier 2 and tier 3" +)] +#[test_case( + Uint128::new(75000), + "tier_4", + Decimal::percent(35); + "between tier 3 and tier 4" +)] +#[test_case( + Uint128::new(30000), + "tier_5", + Decimal::percent(25); + "between tier 4 and tier 5" +)] +#[test_case( + Uint128::new(15000), + "tier_6", + Decimal::percent(15); + "between tier 5 and tier 6" +)] +#[test_case( + Uint128::new(7500), + "tier_7", + Decimal::percent(10); + "between tier 6 and tier 7" +)] +#[test_case( + Uint128::new(1500), + "tier_8", + Decimal::percent(5); + "between tier 7 and tier 8" +)] +#[test_case( + Uint128::new(500), + "tier_9", + Decimal::percent(1); + "between tier 8 and tier 9" +)] +#[test_case( + Uint128::new(50), + "tier_10", + Decimal::percent(0); + "between tier 9 and tier 10" +)] +fn test_find_applicable_tier_between_thresholds( + voting_power: Uint128, + expected_tier_id: &str, + expected_discount: Decimal, +) { + let config = create_test_fee_tier_config(); + let manager = StakingTierManager::new(config); + + let tier = manager.find_applicable_tier(voting_power).unwrap(); + assert_eq!(tier.id, expected_tier_id); + assert_eq!(tier.discount_pct, expected_discount); +} + +#[test_case( + Uint128::new(500000), + "tier_1", + Decimal::percent(75); + "above highest tier threshold" +)] +#[test_case( + Uint128::new(1000000), + "tier_1", + Decimal::percent(75); + "well above highest tier threshold" +)] +fn test_find_applicable_tier_above_highest( + voting_power: Uint128, + expected_tier_id: &str, + expected_discount: Decimal, +) { + let config = create_test_fee_tier_config(); + let manager = StakingTierManager::new(config); + + let tier = manager.find_applicable_tier(voting_power).unwrap(); + assert_eq!(tier.id, expected_tier_id); + assert_eq!(tier.discount_pct, expected_discount); +} + +#[test_case( + Uint128::new(1), + "tier_10", + Decimal::percent(0); + "edge case: minimal voting power" +)] +#[test_case( + Uint128::new(99), + "tier_10", + Decimal::percent(0); + "edge case: just below tier 9" +)] +#[test_case( + Uint128::new(101), + "tier_9", + Decimal::percent(1); + "edge case: just above tier 10" +)] +fn test_find_applicable_tier_edge_cases( + voting_power: Uint128, + expected_tier_id: &str, + expected_discount: Decimal, +) { + let config = create_test_fee_tier_config(); + let manager = StakingTierManager::new(config); + + let tier = manager.find_applicable_tier(voting_power).unwrap(); + assert_eq!(tier.id, expected_tier_id); + assert_eq!(tier.discount_pct, expected_discount); +} + +#[test] +fn test_validate_fee_tier_config_valid() { + let config = create_test_fee_tier_config(); + let manager = StakingTierManager::new(config); + + // Should not panic for valid config + let result = manager.validate(); + assert!(result.is_ok()); +} + +#[test] +fn test_validate_fee_tier_config_empty() { + let config = FeeTierConfig { + tiers: vec![], + }; + let manager = StakingTierManager::new(config); + + let result = manager.validate(); + assert!(result.is_err()); + match result.unwrap_err() { + StdError::GenericErr { msg } => { + assert!(msg.contains("Fee tier config cannot be empty")); + } + _ => panic!("Expected StdError::GenericErr"), + } +} + +#[test] +fn test_validate_fee_tier_config_unsorted() { + let config = FeeTierConfig { + tiers: vec![ + FeeTier { + id: "tier_2".to_string(), + min_voting_power: "200000".to_string(), + discount_pct: Decimal::percent(60), + }, + FeeTier { + id: "tier_1".to_string(), + min_voting_power: "350000".to_string(), + discount_pct: Decimal::percent(75), + }, + ], + }; + let manager = StakingTierManager::new(config); + + let result = manager.validate(); + assert!(result.is_err()); + match result.unwrap_err() { + StdError::GenericErr { msg } => { + assert!(msg.contains("Tiers must be sorted in descending order")); + } + _ => panic!("Expected StdError::GenericErr"), + } +} + +#[test] +fn test_validate_fee_tier_config_duplicate_thresholds() { + let config = FeeTierConfig { + tiers: vec![ + FeeTier { + id: "tier_1".to_string(), + min_voting_power: "350000".to_string(), + discount_pct: Decimal::percent(75), + }, + FeeTier { + id: "tier_2".to_string(), + min_voting_power: "350000".to_string(), + discount_pct: Decimal::percent(60), + }, + ], + }; + let manager = StakingTierManager::new(config); + + let result = manager.validate(); + assert!(result.is_err()); + match result.unwrap_err() { + StdError::GenericErr { msg } => { + assert!(msg.contains("Duplicate voting power thresholds")); + } + _ => panic!("Expected StdError::GenericErr"), + } +} + +#[test] +fn test_validate_fee_tier_config_invalid_discount() { + let config = FeeTierConfig { + tiers: vec![FeeTier { + id: "tier_1".to_string(), + min_voting_power: "350000".to_string(), + discount_pct: Decimal::percent(100), // 100% discount (invalid) + }], + }; + let manager = StakingTierManager::new(config); + + let result = manager.validate(); + assert!(result.is_err()); + match result.unwrap_err() { + StdError::GenericErr { msg } => { + assert!(msg.contains("Discount percentage must be less than 100%")); + } + _ => panic!("Expected StdError::GenericErr"), + } +} + +#[test] +fn test_get_default_tier() { + let config = create_test_fee_tier_config(); + let manager = StakingTierManager::new(config); + + let default_tier = manager.get_default_tier().unwrap(); + assert_eq!(default_tier.id, "tier_10"); + assert_eq!(default_tier.discount_pct, Decimal::percent(0)); +} + +#[test_case( + Uint128::new(400000), + Decimal::percent(75); + "tier 1: highest discount" +)] +#[test_case( + Uint128::new(250000), + Decimal::percent(60); + "tier 2: high discount" +)] +#[test_case( + Uint128::new(120000), + Decimal::percent(45); + "tier 3: medium-high discount" +)] +#[test_case( + Uint128::new(60000), + Decimal::percent(35); + "tier 4: medium discount" +)] +#[test_case( + Uint128::new(30000), + Decimal::percent(25); + "tier 5: medium-low discount" +)] +#[test_case( + Uint128::new(12000), + Decimal::percent(15); + "tier 6: low discount" +)] +#[test_case( + Uint128::new(6000), + Decimal::percent(10); + "tier 7: very low discount" +)] +#[test_case( + Uint128::new(1500), + Decimal::percent(5); + "tier 8: minimal discount" +)] +#[test_case( + Uint128::new(500), + Decimal::percent(1); + "tier 9: tiny discount" +)] +#[test_case( + Uint128::new(50), + Decimal::percent(0); + "tier 10: no discount" +)] +fn test_discount_calculation_examples( + voting_power: Uint128, + expected_discount: Decimal, +) { + let config = create_test_fee_tier_config(); + let manager = StakingTierManager::new(config); + + let tier = manager.find_applicable_tier(voting_power).unwrap(); + assert_eq!( + tier.discount_pct, + expected_discount, + "Failed for voting power: {}", + voting_power + ); +} + +#[test] +fn test_fee_tier_config_with_single_tier() { + let config = FeeTierConfig { + tiers: vec![FeeTier { + id: "single_tier".to_string(), + min_voting_power: "0".to_string(), + discount_pct: Decimal::percent(25), + }], + }; + let manager = StakingTierManager::new(config); + + // Should always return the single tier + let tier = manager.find_applicable_tier(Uint128::new(1000)).unwrap(); + assert_eq!(tier.id, "single_tier"); + assert_eq!(tier.discount_pct, Decimal::percent(25)); + + let tier = manager.find_applicable_tier(Uint128::new(0)).unwrap(); + assert_eq!(tier.id, "single_tier"); + assert_eq!(tier.discount_pct, Decimal::percent(25)); +} + +#[test] +fn test_fee_tier_config_with_two_tiers() { + let config = FeeTierConfig { + tiers: vec![ + FeeTier { + id: "high_tier".to_string(), + min_voting_power: "1000".to_string(), + discount_pct: Decimal::percent(50), + }, + FeeTier { + id: "low_tier".to_string(), + min_voting_power: "0".to_string(), + discount_pct: Decimal::percent(10), + }, + ], + }; + let manager = StakingTierManager::new(config); + + // Test high tier + let tier = manager.find_applicable_tier(Uint128::new(1500)).unwrap(); + assert_eq!(tier.id, "high_tier"); + assert_eq!(tier.discount_pct, Decimal::percent(50)); + + // Test low tier + let tier = manager.find_applicable_tier(Uint128::new(500)).unwrap(); + assert_eq!(tier.id, "low_tier"); + assert_eq!(tier.discount_pct, Decimal::percent(10)); + + // Test boundary + let tier = manager.find_applicable_tier(Uint128::new(1000)).unwrap(); + assert_eq!(tier.id, "high_tier"); + assert_eq!(tier.discount_pct, Decimal::percent(50)); +} diff --git a/contracts/credit-manager/tests/tests/test_swap_with_discount.rs b/contracts/credit-manager/tests/tests/test_swap_with_discount.rs new file mode 100644 index 00000000..fc971783 --- /dev/null +++ b/contracts/credit-manager/tests/tests/test_swap_with_discount.rs @@ -0,0 +1,109 @@ +use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; +use mars_types::credit_manager::Action::{Deposit, SwapExactIn}; +use mars_types::swapper::{OsmoRoute, OsmoSwap, SwapperRoute}; + +use super::helpers::{uatom_info, uosmo_info, AccountToFund, MockEnv}; +use cw_multi_test::AppResponse; + +fn setup_env_with_swap_fee() -> (MockEnv, Addr, String) { + let atom = uatom_info(); + let osmo = uosmo_info(); + let user = Addr::unchecked("user"); + + let mut mock = MockEnv::new() + .set_params(&[osmo.clone(), atom.clone()]) + .fund_account(AccountToFund { + addr: user.clone(), + funds: vec![Coin::new(30_000, atom.denom.clone())], + }) + .swap_fee(Decimal::percent(1)) + .build() + .unwrap(); + + let account_id = mock.create_credit_account(&user).unwrap(); + (mock, user, account_id) +} + +fn do_swap( + mock: &mut MockEnv, + account_id: &str, + user: &Addr, + amount: u128, + denom_in: &str, + denom_out: &str, +) -> AppResponse { + let route = SwapperRoute::Osmo(OsmoRoute { + swaps: vec![OsmoSwap { + pool_id: 101, + to: denom_out.to_string(), + }], + }); + let estimate = mock.query_swap_estimate(&Coin::new(amount, denom_in), denom_out, route.clone()); + let min_receive = estimate.amount - Uint128::one(); + let atom = uatom_info(); + mock.update_credit_account( + account_id, + user, + vec![ + Deposit(Coin::new(amount, denom_in)), + SwapExactIn { + coin_in: atom.to_action_coin(amount), + denom_out: denom_out.to_string(), + min_receive, + route: Some(route), + }, + ], + &[Coin::new(amount, denom_in)], + ) + .unwrap() +} + +#[test] +fn test_swap_with_discount() { + let (mut mock, user, account_id) = setup_env_with_swap_fee(); + + // Helper to extract attributes for the swapper event + let extract = |res: &AppResponse| { + res.events + .iter() + .find(|e| e.attributes.iter().any(|a| a.key == "action" && a.value == "swapper")) + .unwrap() + .attributes + .iter() + .map(|a| (a.key.clone(), a.value.clone())) + .collect::>() + }; + + // Tier 10 (min power 0) → 0% discount + mock.set_voting_power(&user, Uint128::new(0)); + let res = do_swap(&mut mock, &account_id, &user, 10_000, "uatom", "uosmo"); + let attrs = extract(&res); + assert_eq!(attrs.get("voting_power").unwrap(), "0"); + assert_eq!(attrs.get("tier_id").unwrap(), "tier_10"); + assert_eq!(attrs.get("discount_pct").unwrap(), &Decimal::percent(0).to_string()); + + // Tier 7 (>= 5_000) → 10% discount + mock.set_voting_power(&user, Uint128::new(5_000)); + let res = do_swap(&mut mock, &account_id, &user, 10_000, "uatom", "uosmo"); + let attrs = extract(&res); + assert_eq!(attrs.get("voting_power").unwrap(), "5000"); + assert_eq!(attrs.get("tier_id").unwrap(), "tier_7"); + assert_eq!(attrs.get("discount_pct").unwrap(), &Decimal::percent(10).to_string()); + + // Tier 3 (>= 100_000) → 45% discount + mock.set_voting_power(&user, Uint128::new(100_000)); + let res = do_swap(&mut mock, &account_id, &user, 10_000, "uatom", "uosmo"); + let attrs = extract(&res); + assert_eq!(attrs.get("voting_power").unwrap(), "100000"); + assert_eq!(attrs.get("tier_id").unwrap(), "tier_3"); + assert_eq!(attrs.get("discount_pct").unwrap(), &Decimal::percent(45).to_string()); + + // Assertions are based on event attributes of last swap to validate effective fee applied + // (Simple smoke check: ensure the action attribute is present and swapper ran.) + // Detailed per-fee verification can be added by inspecting event attributes similarly to existing swap tests. + // Sanity check that swapper action exists in last response + assert!(res + .events + .iter() + .any(|e| e.attributes.iter().any(|a| a.key == "action" && a.value == "swapper"))); +} diff --git a/contracts/credit-manager/tests/tests/test_trading_fee.rs b/contracts/credit-manager/tests/tests/test_trading_fee.rs new file mode 100644 index 00000000..266737a0 --- /dev/null +++ b/contracts/credit-manager/tests/tests/test_trading_fee.rs @@ -0,0 +1,185 @@ +use super::helpers::{default_perp_params, uosmo_info, MockEnv}; +use cosmwasm_std::{Addr, Decimal, Uint128}; +use mars_types::credit_manager::{MarketType, TradingFeeResponse}; +use mars_types::params::PerpParamsUpdate; +use test_case::test_case; + +#[test_case( + Uint128::new(200_000), + "tier_2", + Decimal::percent(60), + Decimal::percent(25); + "spot market tier 2: 60% discount on 0.25% base fee" +)] +#[test_case( + Uint128::new(100_000), + "tier_3", + Decimal::percent(45), + Decimal::percent(25); + "spot market tier 3: 45% discount on 0.25% base fee" +)] +#[test_case( + Uint128::new(50_000), + "tier_4", + Decimal::percent(35), + Decimal::percent(25); + "spot market tier 4: 35% discount on 0.25% base fee" +)] +fn test_trading_fee_query_spot( + voting_power: Uint128, + expected_tier_id: &str, + expected_discount: Decimal, + expected_base_fee: Decimal, +) { + let mut mock = MockEnv::new().set_params(&[uosmo_info()]).build().unwrap(); + + // Create a credit account + let user = Addr::unchecked("user"); + let account_id = mock.create_credit_account(&user).unwrap(); + + // Set voting power for the specified tier + mock.set_voting_power(&user, voting_power); + + // Query trading fee for spot market + let response: TradingFeeResponse = mock + .app + .wrap() + .query_wasm_smart( + mock.rover.clone(), + &mars_types::credit_manager::QueryMsg::TradingFee { + account_id: account_id.clone(), + market_type: MarketType::Spot, + }, + ) + .unwrap(); + + // Verify the response + assert_eq!(response.base_fee_pct, expected_base_fee); + assert_eq!(response.discount_pct, expected_discount); + + // Calculate the expected effective fee based on the actual response + let calculated_effective = response.base_fee_pct.checked_mul(Decimal::one() - expected_discount).unwrap(); + assert_eq!(response.effective_fee_pct, calculated_effective); + assert_eq!(response.tier_id, expected_tier_id); +} + +#[test_case( + Uint128::new(25_000), + "tier_5", + Decimal::percent(25), + "uosmo"; + "perp market tier 5: 25% discount on uosmo" +)] +#[test_case( + Uint128::new(10_000), + "tier_6", + Decimal::percent(15), + "uosmo"; + "perp market tier 6: 15% discount on uosmo" +)] +#[test_case( + Uint128::new(5_000), + "tier_7", + Decimal::percent(10), + "uosmo"; + "perp market tier 7: 10% discount on uosmo" +)] +fn test_trading_fee_query_perp( + voting_power: Uint128, + expected_tier_id: &str, + expected_discount: Decimal, + denom: &str, +) { + let mut mock = MockEnv::new().set_params(&[uosmo_info()]).build().unwrap(); + + // Create a credit account + let user = Addr::unchecked("user"); + let account_id = mock.create_credit_account(&user).unwrap(); + + // Set voting power for the specified tier + mock.set_voting_power(&user, voting_power); + + // Set up perp params for the specified denom + mock.update_perp_params(PerpParamsUpdate::AddOrUpdate { + params: default_perp_params(denom), + }); + + // Query trading fee for perp market + let response: TradingFeeResponse = mock + .app + .wrap() + .query_wasm_smart( + mock.rover.clone(), + &mars_types::credit_manager::QueryMsg::TradingFee { + account_id: account_id.clone(), + market_type: MarketType::Perp { + denom: denom.to_string(), + }, + }, + ) + .unwrap(); + + // Verify the response + assert_eq!(response.discount_pct, expected_discount); + assert_eq!(response.tier_id, expected_tier_id); + + // The effective fee should be base_fee * (1 - discount) + let expected_effective = response + .base_fee_pct + .checked_mul(Decimal::one() - expected_discount) + .unwrap(); + assert_eq!(response.effective_fee_pct, expected_effective); +} + +#[test] +fn test_trading_fee_query_edge_cases() { + let mut mock = MockEnv::new().set_params(&[uosmo_info()]).build().unwrap(); + + // Create a credit account + let user = Addr::unchecked("user"); + let account_id = mock.create_credit_account(&user).unwrap(); + + // Test tier 1 (highest discount - 75%) + mock.set_voting_power(&user, Uint128::new(350_000)); + + let response: TradingFeeResponse = mock + .app + .wrap() + .query_wasm_smart( + mock.rover.clone(), + &mars_types::credit_manager::QueryMsg::TradingFee { + account_id: account_id.clone(), + market_type: MarketType::Spot, + }, + ) + .unwrap(); + + assert_eq!(response.tier_id, "tier_1"); + assert_eq!(response.discount_pct, Decimal::percent(75)); + + // Calculate the expected effective fee based on the actual response + let calculated_effective = response.base_fee_pct.checked_mul(Decimal::one() - Decimal::percent(75)).unwrap(); + assert_eq!(response.effective_fee_pct, calculated_effective); + + // Test tier 10 (no discount - 0%) + mock.set_voting_power(&user, Uint128::new(0)); + + let response: TradingFeeResponse = mock + .app + .wrap() + .query_wasm_smart( + mock.rover.clone(), + &mars_types::credit_manager::QueryMsg::TradingFee { + account_id: account_id.clone(), + market_type: MarketType::Spot, + }, + ) + .unwrap(); + + assert_eq!(response.tier_id, "tier_10"); + assert_eq!(response.discount_pct, Decimal::percent(0)); + + // Calculate the expected effective fee based on the actual response + let calculated_effective = response.base_fee_pct.checked_mul(Decimal::one() - Decimal::percent(0)).unwrap(); + assert_eq!(response.effective_fee_pct, calculated_effective); +} diff --git a/contracts/credit-manager/tests/tests/test_update_config.rs b/contracts/credit-manager/tests/tests/test_update_config.rs index e7ecdf48..e62e18c1 100644 --- a/contracts/credit-manager/tests/tests/test_update_config.rs +++ b/contracts/credit-manager/tests/tests/test_update_config.rs @@ -41,6 +41,8 @@ fn only_owner_can_update_config() { keeper_fee_config: None, perps_liquidation_bonus_ratio: None, swap_fee: None, + fee_tier_config: None, + dao_staking_address: None, }, ); @@ -128,6 +130,8 @@ fn update_config_works_with_full_config() { keeper_fee_config: Some(keeper_fee_config.clone()), perps_liquidation_bonus_ratio: Some(new_perps_lb_ratio), swap_fee: Some(new_swap_fee), + fee_tier_config: None, + dao_staking_address: None, }, ) .unwrap(); diff --git a/contracts/mock-dao-staking/Cargo.toml b/contracts/mock-dao-staking/Cargo.toml new file mode 100644 index 00000000..d89ef94f --- /dev/null +++ b/contracts/mock-dao-staking/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "mars-mock-dao-staking" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +cosmwasm-schema = "1.5" +cosmwasm-std = "1.5" +cw-storage-plus = "1.2" +thiserror = "1" +mars-types = { path = "../../packages/types" } + + diff --git a/contracts/mock-dao-staking/src/lib.rs b/contracts/mock-dao-staking/src/lib.rs new file mode 100644 index 00000000..82c7e96e --- /dev/null +++ b/contracts/mock-dao-staking/src/lib.rs @@ -0,0 +1,59 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::to_json_binary; +use cosmwasm_std::{ + entry_point, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, Uint128, +}; +use cw_storage_plus::Map; +use mars_types::adapters::dao_staking::{ + DaoStakingQueryMsg, VotingPowerAtHeightQuery, VotingPowerAtHeightResponse, +}; + +const VOTING_POWER: Map<&Addr, Uint128> = Map::new("voting_power"); + +#[cw_serde] +pub enum ExecMsg { + SetVotingPower { + address: String, + power: Uint128, + }, +} + +#[entry_point] +pub fn instantiate(_deps: DepsMut, _env: Env, _info: MessageInfo, _msg: ()) -> StdResult { + Ok(Response::new()) +} + +#[entry_point] +pub fn execute(deps: DepsMut, _env: Env, _info: MessageInfo, msg: ExecMsg) -> StdResult { + match msg { + ExecMsg::SetVotingPower { + address, + power, + } => { + let addr = deps.api.addr_validate(&address)?; + VOTING_POWER.save(deps.storage, &addr, &power)?; + Ok(Response::new()) + } + } +} + +#[entry_point] +pub fn query(deps: Deps, _env: Env, msg: DaoStakingQueryMsg) -> StdResult { + match msg { + DaoStakingQueryMsg::VotingPowerAtHeight(VotingPowerAtHeightQuery { + address, + }) => { + let addr = deps.api.addr_validate(&address)?; + let power = VOTING_POWER.may_load(deps.storage, &addr)?.unwrap_or(Uint128::zero()); + to_json_binary(&VotingPowerAtHeightResponse { + power, + height: 0, + }) + } + } +} + +#[entry_point] +pub fn reply(_deps: DepsMut, _env: Env, _reply: Reply) -> StdResult { + Ok(Response::new()) +} diff --git a/contracts/perps/src/contract.rs b/contracts/perps/src/contract.rs index ada9d5ce..200f2ac3 100644 --- a/contracts/perps/src/contract.rs +++ b/contracts/perps/src/contract.rs @@ -75,15 +75,22 @@ pub fn execute( ExecuteMsg::CloseAllPositions { account_id, action, - } => { - close_all_positions(deps, env, info, account_id, action.unwrap_or(ActionKind::Default)) - } + discount_pct, + } => close_all_positions( + deps, + env, + info, + account_id, + action.unwrap_or(ActionKind::Default), + discount_pct, + ), ExecuteMsg::ExecuteOrder { account_id, denom, size, reduce_only, - } => execute_order(deps, env, info, account_id, denom, size, reduce_only), + discount_pct, + } => execute_order(deps, env, info, account_id, denom, size, reduce_only, discount_pct), ExecuteMsg::Deleverage { account_id, denom, @@ -175,7 +182,8 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> ContractResult { QueryMsg::OpeningFee { denom, size, - } => to_json_binary(&query_opening_fee(deps, &denom, size)?), + discount_pct, + } => to_json_binary(&query_opening_fee(deps, &denom, size, discount_pct)?), QueryMsg::PositionFees { account_id, denom, diff --git a/contracts/perps/src/deleverage.rs b/contracts/perps/src/deleverage.rs index 56685022..e9168b98 100644 --- a/contracts/perps/src/deleverage.rs +++ b/contracts/perps/src/deleverage.rs @@ -22,7 +22,7 @@ use crate::{ query, state::{ DeleverageRequestTempStorage, CONFIG, DELEVERAGE_REQUEST_TEMP_STORAGE, MARKET_STATES, - POSITIONS, REALIZED_PNL, TOTAL_CASH_FLOW, + POSITIONS, REALIZED_PNL, TOTAL_CASH_FLOW, ACCOUNT_OPENING_FEE_RATES, }, utils::{get_oracle_adapter, get_params_adapter, update_position_attributes}, }; @@ -114,14 +114,26 @@ pub fn deleverage( let initial_skew = ms.skew()?; ms.close_position(current_time, denom_price, base_denom_price, &position)?; + // Check if we have a stored opening fee rate for this position + let stored_opening_fee_rate = ACCOUNT_OPENING_FEE_RATES.may_load(deps.storage, (&account_id, &denom))?; + + let (opening_fee_rate, closing_fee_rate) = if let Some(stored_rate) = stored_opening_fee_rate { + // Use the stored opening fee rate (what was actually paid) for historical accuracy + // Use current closing fee rate (fair for current operations) + (stored_rate, perp_params.closing_fee_rate) + } else { + // Fallback to current rates for existing positions without stored fee rates + (perp_params.opening_fee_rate, perp_params.closing_fee_rate) + }; + // Compute the position's unrealized PnL let pnl_amounts = position.compute_pnl( &ms.funding, initial_skew, denom_price, base_denom_price, - perp_params.opening_fee_rate, - perp_params.closing_fee_rate, + opening_fee_rate, + closing_fee_rate, PositionModification::Decrease(position.size), )?; @@ -160,6 +172,10 @@ pub fn deleverage( // Save updated states POSITIONS.remove(deps.storage, (&account_id, &denom)); + + // Clean up the stored opening fee rate for this position + ACCOUNT_OPENING_FEE_RATES.remove(deps.storage, (&account_id, &denom)); + REALIZED_PNL.save(deps.storage, (&account_id, &denom), &realized_pnl)?; MARKET_STATES.save(deps.storage, &denom, &ms)?; TOTAL_CASH_FLOW.save(deps.storage, &tcf)?; diff --git a/contracts/perps/src/position_management.rs b/contracts/perps/src/position_management.rs index b18fcb87..6c8ad00b 100644 --- a/contracts/perps/src/position_management.rs +++ b/contracts/perps/src/position_management.rs @@ -18,13 +18,33 @@ use crate::{ error::{ContractError, ContractResult}, market::MarketStateExt, position::{calculate_new_size, PositionExt, PositionModification}, - state::{CONFIG, MARKET_STATES, POSITIONS, REALIZED_PNL, TOTAL_CASH_FLOW}, + state::{CONFIG, MARKET_STATES, POSITIONS, REALIZED_PNL, TOTAL_CASH_FLOW, ACCOUNT_OPENING_FEE_RATES}, utils::{ ensure_max_position, ensure_min_position, get_oracle_adapter, get_params_adapter, update_position_attributes, }, }; +/// Helper function to compute discounted fee rates +pub fn compute_discounted_fee_rates( + perp_params: &PerpParams, + discount_pct: Option, +) -> (Decimal, Decimal) { + let opening_fee_rate = if let Some(discount) = discount_pct { + perp_params.opening_fee_rate * (Decimal::one() - discount) + } else { + perp_params.opening_fee_rate + }; + + let closing_fee_rate = if let Some(discount) = discount_pct { + perp_params.closing_fee_rate * (Decimal::one() - discount) + } else { + perp_params.closing_fee_rate + }; + + (opening_fee_rate, closing_fee_rate) +} + /// Executes a perpetual order for a specific account and denom. /// /// Depending on whether a position exists and the reduce_only flag, this function either opens a new @@ -37,6 +57,7 @@ pub fn execute_order( denom: String, size: Int128, reduce_only: Option, + discount_pct: Option, ) -> ContractResult { let position = POSITIONS.may_load(deps.storage, (&account_id, &denom))?; let reduce_only_checked = reduce_only.unwrap_or(false); @@ -45,14 +66,13 @@ pub fn execute_order( None if reduce_only_checked => Err(ContractError::IllegalPositionModification { reason: "Cannot open position if reduce_only = true".to_string(), }), - None => open_position(deps, env, info, account_id, denom, size), + None => open_position(deps, env, info, account_id, denom, size, discount_pct), Some(position) => { let new_size = calculate_new_size(position.size, size, reduce_only_checked)?; - modify_position(deps, env, info, position, account_id, denom, new_size) + modify_position(deps, env, info, position, account_id, denom, new_size, discount_pct) } } } - /// Opens a new position for a specific account and denom. /// /// This function checks if the account can open a new position, validates the position parameters, @@ -64,6 +84,7 @@ fn open_position( account_id: String, denom: String, size: Int128, + discount_pct: Option, ) -> ContractResult { let cfg = CONFIG.load(deps.storage)?; @@ -120,7 +141,10 @@ fn open_position( // Params for the given market let perp_params = params.query_perp_params(&deps.querier, &denom)?; - // Find the opening fee amount + // Apply discount to fee rates if provided + let (opening_fee_rate, closing_fee_rate) = + compute_discounted_fee_rates(&perp_params, discount_pct); + let opening_fee_amt = may_pay(&info, &cfg.base_denom)?; // Query the asset's price. @@ -139,8 +163,8 @@ fn open_position( ensure_max_position(position_value, &perp_params)?; let fees = PositionModification::Increase(size).compute_fees( - perp_params.opening_fee_rate, - perp_params.closing_fee_rate, + opening_fee_rate, + closing_fee_rate, denom_price, base_denom_price, ms.skew()?, @@ -219,6 +243,9 @@ fn open_position( }, )?; + // Save the actual opening fee rate that was applied to this position + ACCOUNT_OPENING_FEE_RATES.save(deps.storage, (&account_id, &denom), &opening_fee_rate)?; + Ok(Response::new() .add_messages(msgs) .add_attribute("action", "open_position") @@ -247,6 +274,7 @@ fn modify_position( account_id: String, denom: String, new_size: Int128, + discount_pct: Option, ) -> ContractResult { // Load the contract's configuration let cfg = CONFIG.load(deps.storage)?; @@ -273,6 +301,10 @@ fn modify_position( // Query the parameters for the given market (denom) let perp_params = params.query_perp_params(&deps.querier, &denom)?; + // Apply discount to fee rates if provided + let (opening_fee_rate, closing_fee_rate) = + compute_discounted_fee_rates(&perp_params, discount_pct); + // Load relevant state variables let mut realized_pnl = REALIZED_PNL.may_load(deps.storage, (&account_id, &denom))?.unwrap_or_default(); @@ -328,14 +360,17 @@ fn modify_position( modification }; + // Check if this is a position flip to save the opening fee rate later + let is_position_flip = matches!(modification, PositionModification::Flip(_, _)); + // Compute the position's unrealized PnL let pnl_amounts = position.compute_pnl( &ms.funding, initial_skew, denom_price, base_denom_price, - perp_params.opening_fee_rate, - perp_params.closing_fee_rate, + opening_fee_rate, + closing_fee_rate, modification, )?; @@ -382,6 +417,9 @@ fn modify_position( // Delete the position if the new size is zero POSITIONS.remove(deps.storage, (&account_id, &denom)); + // Clean up the stored opening fee rate for this position + ACCOUNT_OPENING_FEE_RATES.remove(deps.storage, (&account_id, &denom)); + "close_position" } else { // Save the updated position state @@ -404,6 +442,11 @@ fn modify_position( }, )?; + // Update the opening fee rate if this was a position flip (new opening fee charged) + if is_position_flip { + ACCOUNT_OPENING_FEE_RATES.save(deps.storage, (&account_id, &denom), &opening_fee_rate)?; + } + "modify_position" }; @@ -427,6 +470,7 @@ pub fn close_all_positions( info: MessageInfo, account_id: String, action: ActionKind, + discount_pct: Option, ) -> ContractResult { let cfg = CONFIG.load(deps.storage)?; @@ -496,14 +540,18 @@ pub fn close_all_positions( // Funding rates and index is updated to the current block time (using old size). ms.close_position(env.block.time.seconds(), denom_price, base_denom_price, &position)?; + // Apply discount to fee rates if provided + let (opening_fee_rate, closing_fee_rate) = + compute_discounted_fee_rates(&perp_params, discount_pct); + // Compute the position's unrealized PnL let pnl_amounts = position.compute_pnl( &ms.funding, initial_skew, denom_price, base_denom_price, - perp_params.opening_fee_rate, - perp_params.closing_fee_rate, + opening_fee_rate, + closing_fee_rate, PositionModification::Decrease(position.size), )?; @@ -535,6 +583,9 @@ pub fn close_all_positions( // Remove the position POSITIONS.remove(deps.storage, (&account_id, &denom)); + // Clean up the stored opening fee rate for this position + ACCOUNT_OPENING_FEE_RATES.remove(deps.storage, (&account_id, &denom)); + // Save updated states REALIZED_PNL.save(deps.storage, (&account_id, &denom), &realized_pnl)?; MARKET_STATES.save(deps.storage, &denom, &ms)?; diff --git a/contracts/perps/src/query.rs b/contracts/perps/src/query.rs index f463ec99..1d612ea9 100644 --- a/contracts/perps/src/query.rs +++ b/contracts/perps/src/query.rs @@ -23,11 +23,14 @@ use crate::{ error::ContractResult, market::{compute_total_accounting_data, MarketStateExt}, position::{PositionExt, PositionModification}, + position_management::compute_discounted_fee_rates, state::{ CONFIG, DEPOSIT_SHARES, MARKET_STATES, POSITIONS, REALIZED_PNL, - TOTAL_UNLOCKING_OR_UNLOCKED_SHARES, UNLOCKS, VAULT_STATE, + TOTAL_UNLOCKING_OR_UNLOCKED_SHARES, UNLOCKS, VAULT_STATE, ACCOUNT_OPENING_FEE_RATES, + }, + utils::{ + create_user_id_key, get_credit_manager_adapter, get_oracle_adapter, get_params_adapter, }, - utils::{create_user_id_key, get_oracle_adapter, get_params_adapter}, vault::shares_to_amount, }; @@ -277,11 +280,13 @@ pub fn query_position( let addresses = query_contract_addrs( deps, &cfg.address_provider, - vec![MarsAddressType::Oracle, MarsAddressType::Params], + vec![MarsAddressType::Oracle, MarsAddressType::Params, MarsAddressType::CreditManager], )?; let oracle = get_oracle_adapter(&addresses[&MarsAddressType::Oracle]); let params = get_params_adapter(&addresses[&MarsAddressType::Params]); + let credit_manager = addresses[&MarsAddressType::CreditManager].clone(); + let credit_manager_adapter = get_credit_manager_adapter(&credit_manager); let denom_price = oracle.query_price(&deps.querier, &denom, ActionKind::Default)?.price; let base_denom_price = @@ -306,13 +311,29 @@ pub fn query_position( None => PositionModification::Decrease(position.size), }; + // Query the credit manager to get the discount for this account via adapter + let discount_pct = credit_manager_adapter.query_discount_pct(&deps.querier, &account_id)?; + + // Check if we have a stored opening fee rate for this position + let stored_opening_fee_rate = ACCOUNT_OPENING_FEE_RATES.may_load(deps.storage, (&account_id, &denom))?; + + let (discounted_opening_fee_rate, discounted_closing_fee_rate) = if let Some(stored_rate) = stored_opening_fee_rate { + // Use the stored opening fee rate (what was actually paid) for historical accuracy + // But still use current discount for closing fee rate (fair for current operations) + let current_closing_fee_rate = perp_params.closing_fee_rate * (Decimal::one() - discount_pct); + (stored_rate, current_closing_fee_rate) + } else { + // Fallback to current rates for existing positions without stored fee rates + compute_discounted_fee_rates(&perp_params, Some(discount_pct)) + }; + let pnl_amounts = position.compute_pnl( &curr_funding, ms.skew()?, denom_price, base_denom_price, - perp_params.opening_fee_rate, - perp_params.closing_fee_rate, + discounted_opening_fee_rate, + discounted_closing_fee_rate, modification, )?; @@ -323,7 +344,7 @@ pub fn query_position( account_id, position: Some(PerpPosition { denom, - base_denom: cfg.base_denom, + base_denom: cfg.base_denom.clone(), size: position.size, entry_price: position.entry_price, current_price: denom_price, @@ -351,11 +372,13 @@ pub fn query_positions( let addresses = query_contract_addrs( deps, &cfg.address_provider, - vec![MarsAddressType::Oracle, MarsAddressType::Params], + vec![MarsAddressType::Oracle, MarsAddressType::Params, MarsAddressType::CreditManager], )?; let oracle = get_oracle_adapter(&addresses[&MarsAddressType::Oracle]); let params = get_params_adapter(&addresses[&MarsAddressType::Params]); + let credit_manager = addresses[&MarsAddressType::CreditManager].clone(); + let credit_manager_adapter = get_credit_manager_adapter(&credit_manager); let start = start_after .as_ref() @@ -394,13 +417,30 @@ pub fn query_positions( (price, params, curr_funding, skew) }; + // Query the credit manager to get the discount for this account via adapter + let discount_pct = + credit_manager_adapter.query_discount_pct(&deps.querier, &account_id)?; + + // Check if we have a stored opening fee rate for this position + let stored_opening_fee_rate = ACCOUNT_OPENING_FEE_RATES.may_load(deps.storage, (&account_id, &denom))?; + + let (discounted_opening_fee_rate, discounted_closing_fee_rate) = if let Some(stored_rate) = stored_opening_fee_rate { + // Use the stored opening fee rate (what was actually paid) for historical accuracy + // But still use current discount for closing fee rate (fair for current operations) + let current_closing_fee_rate = perp_params.closing_fee_rate * (Decimal::one() - discount_pct); + (stored_rate, current_closing_fee_rate) + } else { + // Fallback to current rates for existing positions without stored fee rates + compute_discounted_fee_rates(&perp_params, Some(discount_pct)) + }; + let pnl_amounts = position.compute_pnl( &funding, skew, current_price, base_denom_price, - perp_params.opening_fee_rate, - perp_params.closing_fee_rate, + discounted_opening_fee_rate, + discounted_closing_fee_rate, PositionModification::Decrease(position.size), )?; @@ -440,7 +480,12 @@ pub fn query_positions_by_account( let addresses = query_contract_addrs( deps, &cfg.address_provider, - vec![MarsAddressType::Oracle, MarsAddressType::Params], + vec![ + MarsAddressType::Oracle, + MarsAddressType::Params, + MarsAddressType::CreditManager, + MarsAddressType::DaoStaking, + ], )?; let oracle = get_oracle_adapter(&addresses[&MarsAddressType::Oracle]); @@ -471,13 +516,25 @@ pub fn query_positions_by_account( let ms = MARKET_STATES.load(deps.storage, &denom)?; let curr_funding = ms.current_funding(current_time, denom_price, base_denom_price)?; + // Check if we have a stored opening fee rate for this position + let stored_opening_fee_rate = ACCOUNT_OPENING_FEE_RATES.may_load(deps.storage, (&account_id, &denom))?; + + let (opening_fee_rate, closing_fee_rate) = if let Some(stored_rate) = stored_opening_fee_rate { + // Use the stored opening fee rate (what was actually paid) for historical accuracy + // Use current closing fee rate (fair for current operations) + (stored_rate, perp_params.closing_fee_rate) + } else { + // Fallback to current rates for existing positions without stored fee rates + (perp_params.opening_fee_rate, perp_params.closing_fee_rate) + }; + let pnl_amounts = position.compute_pnl( &curr_funding, ms.skew()?, denom_price, base_denom_price, - perp_params.opening_fee_rate, - perp_params.closing_fee_rate, + opening_fee_rate, + closing_fee_rate, PositionModification::Decrease(position.size), )?; @@ -591,8 +648,14 @@ pub fn query_total_accounting(deps: Deps, current_time: u64) -> ContractResult ContractResult { +pub fn query_opening_fee( + deps: Deps, + denom: &str, + size: Int128, + discount_pct: Option, +) -> ContractResult { let cfg = CONFIG.load(deps.storage)?; let ms = MARKET_STATES.load(deps.storage, denom)?; @@ -610,8 +673,11 @@ pub fn query_opening_fee(deps: Deps, denom: &str, size: Int128) -> ContractResul let denom_price = oracle.query_price(&deps.querier, denom, ActionKind::Default)?.price; let perp_params = params.query_perp_params(&deps.querier, denom)?; + // Apply discount to fee rates if provided + let (opening_fee_rate, _) = compute_discounted_fee_rates(&perp_params, discount_pct); + let fees = PositionModification::Increase(size).compute_fees( - perp_params.opening_fee_rate, + opening_fee_rate, perp_params.closing_fee_rate, denom_price, base_denom_price, @@ -620,7 +686,7 @@ pub fn query_opening_fee(deps: Deps, denom: &str, size: Int128) -> ContractResul )?; Ok(TradingFee { - rate: perp_params.opening_fee_rate, + rate: opening_fee_rate, fee: coin(fees.opening_fee.unsigned_abs().u128(), cfg.base_denom), }) } @@ -640,12 +706,21 @@ pub fn query_position_fees( let addresses = query_contract_addrs( deps, &cfg.address_provider, - vec![MarsAddressType::Oracle, MarsAddressType::Params], + vec![ + MarsAddressType::Oracle, + MarsAddressType::Params, + MarsAddressType::CreditManager, + MarsAddressType::DaoStaking, + ], )?; let oracle = get_oracle_adapter(&addresses[&MarsAddressType::Oracle]); let params = get_params_adapter(&addresses[&MarsAddressType::Params]); + // Get staking tier discount for this account + let credit_manager = addresses[&MarsAddressType::CreditManager].clone(); + let credit_manager_adapter = get_credit_manager_adapter(&credit_manager); + let base_denom_price = oracle.query_price(&deps.querier, &cfg.base_denom, ActionKind::Default)?.price; let denom_price = oracle.query_price(&deps.querier, denom, ActionKind::Default)?.price; @@ -687,9 +762,16 @@ pub fn query_position_fees( PositionModification::Increase(new_size) } }; + // Query the credit manager to get the discount for this account via adapter + let discount_pct = credit_manager_adapter.query_discount_pct(&deps.querier, account_id)?; + + // Apply discount to fee rates using the helper function + let (discounted_opening_fee_rate, discounted_closing_fee_rate) = + compute_discounted_fee_rates(&perp_params, Some(discount_pct)); + let fees = modification.compute_fees( - perp_params.opening_fee_rate, - perp_params.closing_fee_rate, + discounted_opening_fee_rate, + discounted_closing_fee_rate, denom_price, base_denom_price, skew, diff --git a/contracts/perps/src/state.rs b/contracts/perps/src/state.rs index 15f1a612..6aababd9 100644 --- a/contracts/perps/src/state.rs +++ b/contracts/perps/src/state.rs @@ -6,6 +6,7 @@ use mars_types::{ keys::UserIdKey, perps::{CashFlow, Config, MarketState, PnlAmounts, Position, UnlockState, VaultState}, }; +use cosmwasm_std::Decimal; #[cw_serde] pub struct DeleverageRequestTempStorage { @@ -40,6 +41,9 @@ pub const POSITIONS: Map<(&str, &str), Position> = Map::new("positions"); // (account_id, denom) => realized PnL amounts pub const REALIZED_PNL: Map<(&str, &str), PnlAmounts> = Map::new("realized_pnls"); +// (account_id, denom) => opening fee rate that was actually applied +pub const ACCOUNT_OPENING_FEE_RATES: Map<(&str, &str), Decimal> = Map::new("account_opening_fee_rates"); + // denom => market cash flow pub const MARKET_CASH_FLOW: Map<&str, CashFlow> = Map::new("market_cf"); diff --git a/contracts/perps/src/utils.rs b/contracts/perps/src/utils.rs index e1de9f5c..8b202308 100644 --- a/contracts/perps/src/utils.rs +++ b/contracts/perps/src/utils.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use cosmwasm_std::{Addr, Attribute, Decimal, Deps, Int128, Order, SignedDecimal, Uint128}; use mars_types::{ adapters::{ + credit_manager::CreditManagerBase, oracle::{Oracle, OracleBase}, params::ParamsBase, }, @@ -99,6 +100,10 @@ pub fn get_params_adapter(address: &Addr) -> ParamsBase { ParamsBase::new(address.clone()) } +pub fn get_credit_manager_adapter(address: &Addr) -> CreditManagerBase { + CreditManagerBase::new(address.clone()) +} + // Updates the attributes vector with details of a modified position. /// /// This function is responsible for pushing key attributes related to the original diff --git a/contracts/perps/tests/tests/helpers/contracts.rs b/contracts/perps/tests/tests/helpers/contracts.rs index 4fb5f79c..87fee122 100644 --- a/contracts/perps/tests/tests/helpers/contracts.rs +++ b/contracts/perps/tests/tests/helpers/contracts.rs @@ -60,7 +60,8 @@ mod mock_credit_manager { #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult}; - use mars_types::credit_manager::{ExecuteMsg, QueryMsg}; + use mars_types::credit_manager::{ExecuteMsg, QueryMsg, Positions, Account}; + use mars_types::health::AccountKind; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -83,7 +84,45 @@ mod mock_credit_manager { } #[cfg_attr(not(feature = "library"), entry_point)] - pub fn query(_deps: Deps, _env: Env, _msg: QueryMsg) -> StdResult { - unimplemented!("query not supported") + pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::AccountKind { account_id: _ } => { + // Return a mock account kind + let account_kind = AccountKind::Default; + cosmwasm_std::to_json_binary(&account_kind) + } + QueryMsg::Positions { account_id: _, action: _ } => { + // Return empty positions + let positions = Positions { + account_id: "1".to_string(), + account_kind: AccountKind::Default, + deposits: vec![], + debts: vec![], + lends: vec![], + vaults: vec![], + staked_astro_lps: vec![], + perps: vec![], + }; + cosmwasm_std::to_json_binary(&positions) + } + QueryMsg::Accounts { owner: _, start_after: _, limit: _ } => { + // Return a mock account + let account = Account { + id: "1".to_string(), + kind: AccountKind::Default, + }; + cosmwasm_std::to_json_binary(&vec![account]) + } + QueryMsg::GetAccountTierAndDiscount { account_id: _ } => { + // Return a mock tier and discount response + let response = mars_types::credit_manager::AccountTierAndDiscountResponse { + tier_id: "default".to_string(), + discount_pct: cosmwasm_std::Decimal::zero(), + voting_power: cosmwasm_std::Uint128::zero(), + }; + cosmwasm_std::to_json_binary(&response) + } + _ => unimplemented!("query not supported: {:?}", msg), + } } } diff --git a/contracts/perps/tests/tests/helpers/mock_env.rs b/contracts/perps/tests/tests/helpers/mock_env.rs index a9ef5f91..62d45b19 100644 --- a/contracts/perps/tests/tests/helpers/mock_env.rs +++ b/contracts/perps/tests/tests/helpers/mock_env.rs @@ -58,6 +58,7 @@ pub struct MockEnvBuilder { deleverage_enabled: bool, withdraw_enabled: bool, max_unlocks: u8, + pub dao_staking_addr: Option, } #[allow(clippy::new_ret_no_self)] @@ -77,6 +78,7 @@ impl MockEnv { deleverage_enabled: true, withdraw_enabled: true, max_unlocks: 5, + dao_staking_addr: Some(Addr::unchecked("mock-dao-staking")), } } @@ -223,6 +225,7 @@ impl MockEnv { denom: denom.to_string(), size, reduce_only, + discount_pct: None, }, funds, ) @@ -240,6 +243,7 @@ impl MockEnv { &perps::ExecuteMsg::CloseAllPositions { account_id: account_id.to_string(), action: None, + discount_pct: None, }, funds, ) @@ -479,7 +483,12 @@ impl MockEnv { .unwrap() } - pub fn query_opening_fee(&self, denom: &str, size: Int128) -> TradingFee { + pub fn query_opening_fee( + &self, + denom: &str, + size: Int128, + discount_pct: Option, + ) -> TradingFee { self.app .wrap() .query_wasm_smart( @@ -487,6 +496,7 @@ impl MockEnv { &perps::QueryMsg::OpeningFee { denom: denom.to_string(), size, + discount_pct, }, ) .unwrap() @@ -514,13 +524,24 @@ impl MockEnv { 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(), + .query_wasm_smart(self.params.clone(), ¶ms::QueryMsg::PerpParams { + denom: denom.to_string(), + }) + .unwrap() + } + + pub fn set_dao_staking_address(&mut self, address: &Addr) { + self.app + .execute_contract( + self.owner.clone(), + self.address_provider.clone(), + &address_provider::ExecuteMsg::SetAddress { + address_type: MarsAddressType::DaoStaking, + address: address.to_string(), }, + &[], ) - .unwrap() + .unwrap(); } } @@ -535,6 +556,15 @@ impl MockEnvBuilder { let perps_contract = self.deploy_perps(address_provider_contract.as_str()); let incentives_contract = self.deploy_incentives(&address_provider_contract); + // Deploy dao staking if provided + if let Some(dao_staking_addr) = self.dao_staking_addr.clone() { + self.update_address_provider( + &address_provider_contract, + MarsAddressType::DaoStaking, + &dao_staking_addr, + ); + } + self.update_address_provider( &address_provider_contract, MarsAddressType::Incentives, @@ -844,4 +874,22 @@ impl MockEnvBuilder { self.max_unlocks = max_unlocks; self } + + pub fn set_dao_staking_addr(mut self, addr: &Addr) -> Self { + self.dao_staking_addr = Some(addr.clone()); + self + } + + pub fn deploy_mock_dao_staking(&mut self) -> &mut Self { + let dao_staking_addr = self.deploy_dao_staking(); + self.dao_staking_addr = Some(dao_staking_addr); + self + } + + fn deploy_dao_staking(&mut self) -> Addr { + // Create a simple mock dao staking contract address + let addr = Addr::unchecked("mock-dao-staking"); + self.set_address(MarsAddressType::DaoStaking, addr.clone()); + addr + } } diff --git a/contracts/perps/tests/tests/mod.rs b/contracts/perps/tests/tests/mod.rs index ff49f8f0..26f49abf 100644 --- a/contracts/perps/tests/tests/mod.rs +++ b/contracts/perps/tests/tests/mod.rs @@ -1,6 +1,7 @@ mod helpers; mod test_accounting; +mod test_accounting_with_discount; mod test_instantiate; mod test_managing_markets; mod test_migration_v2; diff --git a/contracts/perps/tests/tests/test_accounting.rs b/contracts/perps/tests/tests/test_accounting.rs index 4e567def..6263bc4e 100644 --- a/contracts/perps/tests/tests/test_accounting.rs +++ b/contracts/perps/tests/tests/test_accounting.rs @@ -76,7 +76,7 @@ fn accounting() { // open few positions for account 1 let osmo_size = Int128::from_str("10000000").unwrap(); - let osmo_opening_fee = mock.query_opening_fee("uosmo", osmo_size).fee; + let osmo_opening_fee = mock.query_opening_fee("uosmo", osmo_size, None).fee; let osmo_opening_protocol_fee = osmo_opening_fee.amount.checked_mul_ceil(protocol_fee_rate).unwrap(); assert!(!osmo_opening_protocol_fee.is_zero()); @@ -90,7 +90,7 @@ fn accounting() { ) .unwrap(); let atom_size = Int128::from_str("-260000").unwrap(); - let atom_opening_fee = mock.query_opening_fee("uatom", atom_size).fee; + let atom_opening_fee = mock.query_opening_fee("uatom", atom_size, None).fee; let atom_opening_protocol_fee = atom_opening_fee.amount.checked_mul_ceil(protocol_fee_rate).unwrap(); assert!(!atom_opening_protocol_fee.is_zero()); @@ -459,7 +459,7 @@ fn accounting_works_up_to_oi_limits( loop { // Query the opening fee for the given size of the position (eth_size). - let eth_opening_fee = mock.query_opening_fee("ueth", eth_size).fee; + let eth_opening_fee = mock.query_opening_fee("ueth", eth_size, None).fee; // Attempt to execute a perpetual order using the credit manager for the current position size. let res = mock.execute_perp_order( diff --git a/contracts/perps/tests/tests/test_accounting_with_discount.rs b/contracts/perps/tests/tests/test_accounting_with_discount.rs new file mode 100644 index 00000000..9f8eb663 --- /dev/null +++ b/contracts/perps/tests/tests/test_accounting_with_discount.rs @@ -0,0 +1,225 @@ +use std::str::FromStr; + +use cosmwasm_std::{coin, Decimal, Int128, Uint128}; +use mars_types::{ + params::{PerpParams, PerpParamsUpdate}, + perps::Accounting, +}; + +use super::helpers::MockEnv; +use crate::tests::helpers::default_perp_params; +use cosmwasm_std::Addr; + +#[test] +fn accounting_with_discount_fees() { + let protocol_fee_rate = Decimal::percent(2); + let mut mock = MockEnv::new() + .protocol_fee_rate(protocol_fee_rate) + .build() + .unwrap(); + + // Set up dao staking after building + let dao_staking_addr = Addr::unchecked("mock-dao-staking"); + mock.set_dao_staking_address(&dao_staking_addr); + + let owner = mock.owner.clone(); + let credit_manager = mock.credit_manager.clone(); + let user = "jake"; + + // Fund credit manager and set up prices + mock.fund_accounts(&[&credit_manager], 1_000_000_000_000_000u128, &["uosmo", "uatom", "uusdc"]); + mock.set_price(&owner, "uusdc", Decimal::from_str("0.9").unwrap()).unwrap(); + mock.set_price(&owner, "uosmo", Decimal::from_str("1.25").unwrap()).unwrap(); + mock.set_price(&owner, "uatom", Decimal::from_str("10.5").unwrap()).unwrap(); + + // Deposit USDC to vault + mock.deposit_to_vault( + &credit_manager, + Some(user), + None, + &[coin(1_000_000_000_000u128, "uusdc")], + ) + .unwrap(); + + // Set up perp markets with different fee rates + mock.update_perp_params( + &owner, + PerpParamsUpdate::AddOrUpdate { + params: PerpParams { + closing_fee_rate: Decimal::percent(1), + opening_fee_rate: Decimal::percent(2), + max_funding_velocity: Decimal::from_str("32").unwrap(), + ..default_perp_params("uosmo") + }, + }, + ); + mock.update_perp_params( + &owner, + PerpParamsUpdate::AddOrUpdate { + params: PerpParams { + closing_fee_rate: Decimal::percent(1), + opening_fee_rate: Decimal::percent(2), + max_funding_velocity: Decimal::from_str("30").unwrap(), + ..default_perp_params("uatom") + }, + }, + ); + + // Check accounting in the beginning + let osmo_accounting_before = mock.query_market_accounting("uosmo").accounting; + let atom_accounting_before = mock.query_market_accounting("uatom").accounting; + let total_accounting_before = mock.query_total_accounting().accounting; + + assert_eq!(osmo_accounting_before, Accounting::default()); + assert_eq!(atom_accounting_before, Accounting::default()); + assert_eq!(total_accounting_before, Accounting::default()); + + // Test opening fees with and without discount + let atom_size = Int128::from_str("1000000").unwrap(); + + // Query opening fee without discount + let atom_opening_fee_no_discount = mock.query_opening_fee("uatom", atom_size, None).fee; + + // Query opening fee with 50% discount + let discount_pct = Decimal::percent(50); + let atom_opening_fee_with_discount = mock.query_opening_fee("uatom", atom_size, Some(discount_pct)).fee; + + // Verify discount is applied correctly + assert!(atom_opening_fee_with_discount.amount < atom_opening_fee_no_discount.amount); + let expected_discount_amount = atom_opening_fee_no_discount.amount.checked_mul_ceil(discount_pct).unwrap(); + let expected_fee_with_discount = atom_opening_fee_no_discount.amount.checked_sub(expected_discount_amount).unwrap(); + + // Allow for small rounding differences (within 1 unit) + let difference = if expected_fee_with_discount > atom_opening_fee_with_discount.amount { + expected_fee_with_discount.checked_sub(atom_opening_fee_with_discount.amount).unwrap() + } else { + atom_opening_fee_with_discount.amount.checked_sub(expected_fee_with_discount).unwrap() + }; + assert!(difference <= Uint128::new(1), "Discount calculation difference too large: expected {}, got {}, difference {}", + expected_fee_with_discount, atom_opening_fee_with_discount.amount, difference); + + // Test different discount percentages + let discount_25 = Decimal::percent(25); + let atom_opening_fee_25_discount = mock.query_opening_fee("uatom", atom_size, Some(discount_25)).fee; + assert!(atom_opening_fee_25_discount.amount < atom_opening_fee_no_discount.amount); + assert!(atom_opening_fee_25_discount.amount > atom_opening_fee_with_discount.amount); + + let discount_75 = Decimal::percent(75); + let atom_opening_fee_75_discount = mock.query_opening_fee("uatom", atom_size, Some(discount_75)).fee; + assert!(atom_opening_fee_75_discount.amount < atom_opening_fee_25_discount.amount); + + // Test that 100% discount results in 0 fee + let discount_100 = Decimal::percent(100); + let atom_opening_fee_100_discount = mock.query_opening_fee("uatom", atom_size, Some(discount_100)).fee; + assert_eq!(atom_opening_fee_100_discount.amount, Uint128::zero()); + + // Test that 0% discount is same as no discount + let discount_0 = Decimal::zero(); + let atom_opening_fee_0_discount = mock.query_opening_fee("uatom", atom_size, Some(discount_0)).fee; + assert_eq!(atom_opening_fee_0_discount.amount, atom_opening_fee_no_discount.amount); + + // Test with different position sizes + let small_size = Int128::from_str("100000").unwrap(); + let small_fee_no_discount = mock.query_opening_fee("uatom", small_size, None).fee; + let small_fee_with_discount = mock.query_opening_fee("uatom", small_size, Some(discount_pct)).fee; + + // Verify proportional discount + let small_expected_discount = small_fee_no_discount.amount.checked_mul_ceil(discount_pct).unwrap(); + let small_expected_fee = small_fee_no_discount.amount.checked_sub(small_expected_discount).unwrap(); + let small_difference = if small_expected_fee > small_fee_with_discount.amount { + small_expected_fee.checked_sub(small_fee_with_discount.amount).unwrap() + } else { + small_fee_with_discount.amount.checked_sub(small_expected_fee).unwrap() + }; + assert!(small_difference <= Uint128::new(1)); + + // Test with negative size (short position) + let short_size = Int128::from_str("-500000").unwrap(); + let short_fee_no_discount = mock.query_opening_fee("uatom", short_size, None).fee; + let short_fee_with_discount = mock.query_opening_fee("uatom", short_size, Some(discount_pct)).fee; + + assert!(short_fee_with_discount.amount < short_fee_no_discount.amount); + let short_expected_discount = short_fee_no_discount.amount.checked_mul_ceil(discount_pct).unwrap(); + let short_expected_fee = short_fee_no_discount.amount.checked_sub(short_expected_discount).unwrap(); + let short_difference = if short_expected_fee > short_fee_with_discount.amount { + short_expected_fee.checked_sub(short_fee_with_discount.amount).unwrap() + } else { + short_fee_with_discount.amount.checked_sub(short_expected_fee).unwrap() + }; + assert!(short_difference <= Uint128::new(1)); + + // Test discount consistency across different denoms + let osmo_size = Int128::from_str("500000").unwrap(); + let osmo_opening_fee_no_discount = mock.query_opening_fee("uosmo", osmo_size, None).fee; + let osmo_opening_fee_with_discount = mock.query_opening_fee("uosmo", osmo_size, Some(discount_pct)).fee; + + // Verify discount is applied consistently + assert!(osmo_opening_fee_with_discount.amount < osmo_opening_fee_no_discount.amount); + let osmo_expected_discount = osmo_opening_fee_no_discount.amount.checked_mul_ceil(discount_pct).unwrap(); + let osmo_expected_fee = osmo_opening_fee_no_discount.amount.checked_sub(osmo_expected_discount).unwrap(); + let osmo_difference = if osmo_expected_fee > osmo_opening_fee_with_discount.amount { + osmo_expected_fee.checked_sub(osmo_opening_fee_with_discount.amount).unwrap() + } else { + osmo_opening_fee_with_discount.amount.checked_sub(osmo_expected_fee).unwrap() + }; + assert!(osmo_difference <= Uint128::new(1)); + + // Verify that accounting is still clean after all the queries + let osmo_accounting_after = mock.query_market_accounting("uosmo").accounting; + let atom_accounting_after = mock.query_market_accounting("uatom").accounting; + let total_accounting_after = mock.query_total_accounting().accounting; + + // Since we didn't execute any orders, accounting should still be default + assert_eq!(osmo_accounting_after, Accounting::default()); + assert_eq!(atom_accounting_after, Accounting::default()); + assert_eq!(total_accounting_after, Accounting::default()); +} + +#[test] +fn discount_fee_edge_cases() { + let mut mock = MockEnv::new().build().unwrap(); + let owner = mock.owner.clone(); + let credit_manager = mock.credit_manager.clone(); + let user = "alice"; + + // Set up basic environment + mock.fund_accounts(&[&credit_manager], 1_000_000_000u128, &["uusdc"]); + mock.set_price(&owner, "uusdc", Decimal::from_str("1").unwrap()).unwrap(); + mock.set_price(&owner, "uatom", Decimal::from_str("10").unwrap()).unwrap(); + + mock.deposit_to_vault( + &credit_manager, + Some(user), + None, + &[coin(1_000_000_000u128, "uusdc")], + ) + .unwrap(); + + mock.update_perp_params( + &owner, + PerpParamsUpdate::AddOrUpdate { + params: PerpParams { + opening_fee_rate: Decimal::percent(1), + closing_fee_rate: Decimal::percent(1), + ..default_perp_params("uatom") + }, + }, + ); + + let size = Int128::from_str("1000").unwrap(); + + // Test 100% discount (should result in 0 fee) + let fee_100_discount = mock.query_opening_fee("uatom", size, Some(Decimal::percent(100))).fee; + assert_eq!(fee_100_discount.amount, Uint128::zero()); + + // Test 0% discount (should be same as no discount) + let fee_0_discount = mock.query_opening_fee("uatom", size, Some(Decimal::zero())).fee; + let fee_no_discount = mock.query_opening_fee("uatom", size, None).fee; + assert_eq!(fee_0_discount.amount, fee_no_discount.amount); + + // Test very small discount + let small_discount = Decimal::from_str("0.001").unwrap(); // 0.1% + let fee_small_discount = mock.query_opening_fee("uatom", size, Some(small_discount)).fee; + assert!(fee_small_discount.amount < fee_no_discount.amount); + assert!(fee_small_discount.amount > Uint128::zero()); +} diff --git a/contracts/perps/tests/tests/test_position.rs b/contracts/perps/tests/tests/test_position.rs index b2e8fb2f..cd9d190c 100644 --- a/contracts/perps/tests/tests/test_position.rs +++ b/contracts/perps/tests/tests/test_position.rs @@ -64,7 +64,7 @@ fn random_user_cannot_modify_position() { ); let size = Int128::from_str("-125").unwrap(); - let atom_opening_fee = mock.query_opening_fee("uatom", size).fee; + let atom_opening_fee = mock.query_opening_fee("uatom", size, None).fee; mock.execute_perp_order(&credit_manager, "2", "uatom", size, None, &[atom_opening_fee]) .unwrap(); @@ -155,7 +155,7 @@ fn cannot_modify_position_for_disabled_denom() { ); let size = Int128::from_str("-125").unwrap(); - let atom_opening_fee = mock.query_opening_fee("uatom", size).fee; + let atom_opening_fee = mock.query_opening_fee("uatom", size, None).fee; mock.execute_perp_order(&credit_manager, "2", "uatom", size, None, &[atom_opening_fee]) .unwrap(); @@ -240,7 +240,7 @@ fn only_close_position_possible_for_disabled_denom() { ); let size = Int128::from_str("125").unwrap(); - let atom_opening_fee = mock.query_opening_fee("uatom", size).fee; + let atom_opening_fee = mock.query_opening_fee("uatom", size, None).fee; mock.execute_perp_order(&credit_manager, "2", "uatom", size, None, &[atom_opening_fee]) .unwrap(); @@ -466,7 +466,7 @@ fn reduced_position_cannot_be_too_small() { // create valid position let size = Int128::from_str("200").unwrap(); - let atom_opening_fee = mock.query_opening_fee("uatom", size).fee; + let atom_opening_fee = mock.query_opening_fee("uatom", size, None).fee; mock.execute_perp_order(&credit_manager, "2", "uatom", size, None, &[atom_opening_fee]) .unwrap(); @@ -572,7 +572,7 @@ fn increased_position_cannot_be_too_big() { // position size is too big // 100 * 12.5 = 1250 let size = Int128::from_str("50").unwrap(); - let atom_opening_fee = mock.query_opening_fee("uatom", size).fee; + let atom_opening_fee = mock.query_opening_fee("uatom", size, None).fee; mock.execute_perp_order(&credit_manager, "2", "uatom", size, None, &[atom_opening_fee]) .unwrap(); @@ -794,7 +794,7 @@ fn error_when_new_size_equals_old_size() { // Test with positive size let size = Int128::from_str("12").unwrap(); - let atom_opening_fee = mock.query_opening_fee("uatom", size).fee; + let atom_opening_fee = mock.query_opening_fee("uatom", size, None).fee; mock.execute_perp_order(&credit_manager, "1", "uatom", size, None, &[atom_opening_fee]) .unwrap(); // Try to modify position of 12 to 12 @@ -809,7 +809,7 @@ fn error_when_new_size_equals_old_size() { // Test with negative size let size = Int128::from_str("-3").unwrap(); - let atom_opening_fee = mock.query_opening_fee("uatom", size).fee; + let atom_opening_fee = mock.query_opening_fee("uatom", size, None).fee; mock.execute_perp_order( &credit_manager, "2", @@ -885,11 +885,11 @@ fn error_when_oi_limits_exceeded() { // Short OI = (0) -40 = -40 // Net OI = (0) + 30 - 40 = -10 let size = Int128::from_str("30").unwrap(); - let atom_opening_fee = mock.query_opening_fee("uatom", size).fee; + let atom_opening_fee = mock.query_opening_fee("uatom", size, None).fee; mock.execute_perp_order(&credit_manager, "1", "uatom", size, None, &[atom_opening_fee]) .unwrap(); let size = Int128::from_str("-40").unwrap(); - let atom_opening_fee = mock.query_opening_fee("uatom", size).fee; + let atom_opening_fee = mock.query_opening_fee("uatom", size, None).fee; mock.execute_perp_order(&credit_manager, "2", "uatom", size, None, &[atom_opening_fee]) .unwrap(); @@ -1056,7 +1056,7 @@ fn modify_position_realises_pnl() { // prepare some OI let size = Int128::from_str("300").unwrap(); - let atom_opening_fee = mock.query_opening_fee("uatom", size).fee; + let atom_opening_fee = mock.query_opening_fee("uatom", size, None).fee; mock.execute_perp_order(&credit_manager, "1", "uatom", size, None, &[atom_opening_fee.clone()]) .unwrap(); @@ -1065,7 +1065,7 @@ fn modify_position_realises_pnl() { // how much opening fee we will pay for increase from 300 to 400 let atom_opening_fee_for_increase = - mock.query_opening_fee("uatom", Int128::from_str("100").unwrap()).fee; + mock.query_opening_fee("uatom", Int128::from_str("100").unwrap(), None).fee; // modify and verify that our pnl is realized mock.execute_perp_order( @@ -1163,7 +1163,7 @@ fn shouldnt_open_when_reduce_only() { ); let size = Int128::from_str("50").unwrap(); - let atom_opening_fee = mock.query_opening_fee("uatom", size).fee; + let atom_opening_fee = mock.query_opening_fee("uatom", size, None).fee; let res = mock.execute_perp_order( &credit_manager, "2", @@ -1220,7 +1220,7 @@ fn should_open_when_reduce_only_false_or_none() { ); let size = Int128::from_str("50").unwrap(); - let atom_opening_fee = mock.query_opening_fee("uatom", size).fee; + let atom_opening_fee = mock.query_opening_fee("uatom", size, None).fee; mock.execute_perp_order( &credit_manager, "2", @@ -1286,7 +1286,7 @@ fn should_reduce_when_reduce_only_true() { let size_long_position = Int128::from_str("50").unwrap(); let size_short_position = Int128::from_str("-50").unwrap(); - let atom_opening_fee = mock.query_opening_fee(denom, size_long_position).fee; + let atom_opening_fee = mock.query_opening_fee(denom, size_long_position, None).fee; mock.execute_perp_order( &credit_manager, @@ -1419,7 +1419,7 @@ fn shouldnt_increase_when_reduce_only_true() { let size_long_position = Int128::from_str("500").unwrap(); let size_short_position = Int128::from_str("-500").unwrap(); - let atom_opening_fee = mock.query_opening_fee(denom, size_long_position).fee; + let atom_opening_fee = mock.query_opening_fee(denom, size_long_position, None).fee; mock.execute_perp_order( &credit_manager, "2", @@ -1430,7 +1430,7 @@ fn shouldnt_increase_when_reduce_only_true() { ) .unwrap(); - let atom_opening_fee = mock.query_opening_fee(denom, size_short_position).fee; + let atom_opening_fee = mock.query_opening_fee(denom, size_short_position, None).fee; mock.execute_perp_order( &credit_manager, "3", @@ -1553,7 +1553,7 @@ fn increase_when_reduce_only_false() { let size_long_position = Int128::from_str("50").unwrap(); let size_short_position = Int128::from_str("-50").unwrap(); - let atom_opening_fee = mock.query_opening_fee(denom, size_long_position).fee; + let atom_opening_fee = mock.query_opening_fee(denom, size_long_position, None).fee; mock.execute_perp_order( &credit_manager, @@ -1665,7 +1665,7 @@ fn flip_position_when_reduce_only_true() { let size_long_position = Int128::from_str("50").unwrap(); let size_short_position = Int128::from_str("-50").unwrap(); - let atom_opening_fee = mock.query_opening_fee(denom, size_long_position).fee; + let atom_opening_fee = mock.query_opening_fee(denom, size_long_position, None).fee; mock.execute_perp_order( &credit_manager, @@ -1798,7 +1798,7 @@ fn flip_position_when_reduce_only_false() { let size_long_position = Int128::from_str("50").unwrap(); let size_short_position = Int128::from_str("-50").unwrap(); - let atom_opening_fee = mock.query_opening_fee(denom, size_long_position).fee; + let atom_opening_fee = mock.query_opening_fee(denom, size_long_position, None).fee; mock.execute_perp_order( &credit_manager, @@ -2028,12 +2028,12 @@ fn query_position_fees( // open a position to change skew let size = Int128::from_str("10000").unwrap(); - let opening_fee = mock.query_opening_fee("uosmo", size).fee; + let opening_fee = mock.query_opening_fee("uosmo", size, None).fee; mock.execute_perp_order(&credit_manager, "2", "uosmo", size, None, &[opening_fee]).unwrap(); // open a position if specified if let Some(old_size) = old_size { - let opening_fee = mock.query_opening_fee("uosmo", old_size).fee; + let opening_fee = mock.query_opening_fee("uosmo", old_size, None).fee; mock.execute_perp_order(&credit_manager, "1", "uosmo", old_size, None, &[opening_fee]) .unwrap(); } @@ -2133,17 +2133,17 @@ fn close_all_positions( // open few positions let size = Int128::from_str("300").unwrap(); - let atom_opening_fee = mock.query_opening_fee("uatom", size).fee; + let atom_opening_fee = mock.query_opening_fee("uatom", size, None).fee; mock.execute_perp_order(&credit_manager, "1", "uatom", size, None, &[atom_opening_fee.clone()]) .unwrap(); let size = Int128::from_str("-500").unwrap(); - let ntrn_opening_fee = mock.query_opening_fee("untrn", size).fee; + let ntrn_opening_fee = mock.query_opening_fee("untrn", size, None).fee; mock.execute_perp_order(&credit_manager, "1", "untrn", size, None, &[ntrn_opening_fee.clone()]) .unwrap(); let size = Int128::from_str("100").unwrap(); - let osmo_opening_fee = mock.query_opening_fee("uosmo", size).fee; + let osmo_opening_fee = mock.query_opening_fee("uosmo", size, None).fee; mock.execute_perp_order(&credit_manager, "1", "uosmo", size, None, &[osmo_opening_fee.clone()]) .unwrap(); @@ -2294,7 +2294,7 @@ fn open_very_small_position_with_zero_opening_fee() { // openining fee is zero let size = Int128::from_str("1").unwrap(); - let atom_opening_fee = mock.query_opening_fee("uatom", size).fee; + let atom_opening_fee = mock.query_opening_fee("uatom", size, None).fee; assert!(atom_opening_fee.amount.is_zero()); // open a very small position where opening fee is zero but opening_fee_rate is not zero @@ -2339,7 +2339,7 @@ fn global_realized_pnl_matches_positions_realized_pnl() { // Open a LONG position let size = Int128::from_str("300000").unwrap(); - let atom_opening_fee = mock.query_opening_fee("uatom", size).fee; + let atom_opening_fee = mock.query_opening_fee("uatom", size, None).fee; mock.execute_perp_order(&credit_manager, "1", "uatom", size, None, &[atom_opening_fee.clone()]) .unwrap(); @@ -2364,7 +2364,7 @@ fn global_realized_pnl_matches_positions_realized_pnl() { // Open a SHORT position let size = Int128::from_str("-300000").unwrap(); - let atom_opening_fee = mock.query_opening_fee("uatom", size).fee; + let atom_opening_fee = mock.query_opening_fee("uatom", size, None).fee; mock.execute_perp_order(&credit_manager, "1", "uatom", size, None, &[atom_opening_fee.clone()]) .unwrap(); diff --git a/contracts/perps/tests/tests/test_risk_verification.rs b/contracts/perps/tests/tests/test_risk_verification.rs index ffb78156..d9bc4790 100644 --- a/contracts/perps/tests/tests/test_risk_verification.rs +++ b/contracts/perps/tests/tests/test_risk_verification.rs @@ -183,7 +183,7 @@ fn verify_accounting_with_input_actions() { vec![] } } else { - let opening_fee = mock.query_opening_fee(denom, *order_size).fee; + let opening_fee = mock.query_opening_fee(denom, *order_size, None).fee; vec![opening_fee] }; diff --git a/contracts/perps/tests/tests/test_vault.rs b/contracts/perps/tests/tests/test_vault.rs index 7b562dc0..75c24cb7 100644 --- a/contracts/perps/tests/tests/test_vault.rs +++ b/contracts/perps/tests/tests/test_vault.rs @@ -501,7 +501,7 @@ fn unlock_and_withdraw_if_zero_withdrawal_balance() { // open a position let size = Int128::from_str("50").unwrap(); - let atom_opening_fee = mock.query_opening_fee("uatom", size).fee; + let atom_opening_fee = mock.query_opening_fee("uatom", size, None).fee; mock.execute_perp_order(&credit_manager, "1", "uatom", size, None, &[atom_opening_fee]) .unwrap(); @@ -583,7 +583,7 @@ fn calculate_shares_correctly_after_zero_withdrawal_balance() { // open a position let size = Int128::from_str("100").unwrap(); - let atom_opening_fee = mock.query_opening_fee("uatom", size).fee; + let atom_opening_fee = mock.query_opening_fee("uatom", size, None).fee; mock.execute_perp_order(&credit_manager, "1", "uatom", size, None, &[atom_opening_fee]) .unwrap(); @@ -902,7 +902,7 @@ fn withdraw_profits_for_depositors() { // open a position let size = Int128::from_str("100").unwrap(); - let atom_opening_fee = mock.query_opening_fee("uatom", size).fee; + let atom_opening_fee = mock.query_opening_fee("uatom", size, None).fee; assert_eq!(atom_opening_fee, coin(23, "uusdc")); mock.execute_perp_order(&credit_manager, "1", "uatom", size, None, &[atom_opening_fee.clone()]) .unwrap(); @@ -1047,7 +1047,7 @@ fn cannot_withdraw_if_cr_decreases_below_threshold() { // open a position let size = Int128::from_str("100").unwrap(); - let atom_opening_fee = mock.query_opening_fee("uatom", size).fee; + let atom_opening_fee = mock.query_opening_fee("uatom", size, None).fee; mock.execute_perp_order(&credit_manager, "1", "uatom", size, None, &[atom_opening_fee.clone()]) .unwrap(); diff --git a/packages/testing/Cargo.toml b/packages/testing/Cargo.toml index 371dc8d2..373b0e14 100644 --- a/packages/testing/Cargo.toml +++ b/packages/testing/Cargo.toml @@ -44,6 +44,7 @@ mars-mock-oracle = { workspace = true } mars-mock-pyth = { workspace = true } mars-mock-red-bank = { workspace = true } mars-mock-vault = { workspace = true } +mars-mock-dao-staking = { workspace = true } mars-oracle-osmosis = { workspace = true } mars-oracle-wasm = { workspace = true } mars-owner = { workspace = true } diff --git a/packages/testing/src/multitest/helpers/contracts.rs b/packages/testing/src/multitest/helpers/contracts.rs index 33e2dd12..bc8832cd 100644 --- a/packages/testing/src/multitest/helpers/contracts.rs +++ b/packages/testing/src/multitest/helpers/contracts.rs @@ -1,5 +1,6 @@ use cosmwasm_std::Empty; use cw_multi_test::{Contract, ContractWrapper}; +use mars_mock_dao_staking as _; // ensure dependency is linked pub fn mock_rover_contract() -> Box> { let contract = ContractWrapper::new( @@ -128,3 +129,13 @@ pub fn mock_perps_contract() -> Box> { .with_reply(mars_perps::contract::reply); Box::new(contract) } + +pub fn mock_dao_staking_contract() -> Box> { + let contract = ContractWrapper::new( + mars_mock_dao_staking::execute, + mars_mock_dao_staking::instantiate, + mars_mock_dao_staking::query, + ) + .with_reply(mars_mock_dao_staking::reply); + Box::new(contract) +} diff --git a/packages/testing/src/multitest/helpers/mock_env.rs b/packages/testing/src/multitest/helpers/mock_env.rs index cebf423f..994bda98 100644 --- a/packages/testing/src/multitest/helpers/mock_env.rs +++ b/packages/testing/src/multitest/helpers/mock_env.rs @@ -13,6 +13,7 @@ use cw_vault_standard::{ extensions::lockup::{LockupQueryMsg, UnlockingPosition}, msg::{ExtensionQueryMsg, VaultStandardQueryMsg::VaultExtension}, }; +use mars_mock_dao_staking::ExecMsg as DaoStakingExecMsg; use mars_mock_oracle::msg::{ CoinPrice, ExecuteMsg as OracleExecuteMsg, InstantiateMsg as OracleInstantiateMsg, }; @@ -45,6 +46,7 @@ use mars_types::{ SharesResponseItem, TriggerOrderResponse, VaultBinding, VaultPositionResponseItem, VaultUtilizationResponse, }, + fee_tiers::{FeeTier, FeeTierConfig}, health::{ AccountKind, ExecuteMsg::UpdateConfig, HealthValuesResponse, InstantiateMsg as HealthInstantiateMsg, QueryMsg::HealthValues, @@ -87,10 +89,11 @@ use mars_zapper_mock::msg::{InstantiateMsg as ZapperInstantiateMsg, LpConfig}; use super::{ lp_token_info, mock_account_nft_contract, mock_address_provider_contract, - mock_astro_incentives_contract, mock_health_contract, mock_incentives_contract, - mock_managed_vault_contract, mock_oracle_contract, mock_params_contract, mock_perps_contract, - mock_red_bank_contract, mock_rover_contract, mock_swapper_contract, mock_v2_zapper_contract, - mock_vault_contract, AccountToFund, CoinInfo, VaultTestInfo, ASTRO_LP_DENOM, + mock_astro_incentives_contract, mock_dao_staking_contract, mock_health_contract, + mock_incentives_contract, mock_managed_vault_contract, mock_oracle_contract, + mock_params_contract, mock_perps_contract, 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::{ integration::mock_contracts::mock_rewards_collector_osmosis_contract, @@ -136,6 +139,8 @@ pub struct MockEnvBuilder { pub perps_liquidation_bonus_ratio: Option, pub perps_protocol_fee_ratio: Option, pub swap_fee: Option, + pub fee_tier_config: Option, + pub dao_staking_addr: Option, } #[allow(clippy::new_ret_no_self)] @@ -170,6 +175,8 @@ impl MockEnv { perps_liquidation_bonus_ratio: None, perps_protocol_fee_ratio: None, swap_fee: None, + fee_tier_config: None, + dao_staking_addr: None, } } @@ -1141,7 +1148,12 @@ impl MockEnv { .unwrap() } - pub fn query_perp_opening_fee(&self, denom: &str, size: Int128) -> TradingFee { + pub fn query_perp_opening_fee( + &self, + denom: &str, + size: Int128, + discount_pct: Option, + ) -> TradingFee { self.app .wrap() .query_wasm_smart( @@ -1149,6 +1161,7 @@ impl MockEnv { &perps::QueryMsg::OpeningFee { denom: denom.to_string(), size, + discount_pct, }, ) .unwrap() @@ -1192,6 +1205,21 @@ impl MockEnv { Addr::unchecked(res.address) } + + pub fn set_voting_power(&mut self, user: &Addr, power: Uint128) { + let dao = self.query_address_provider(MarsAddressType::DaoStaking); + self.app + .execute_contract( + Addr::unchecked("owner"), + dao, + &DaoStakingExecMsg::SetVotingPower { + address: user.to_string(), + power, + }, + &[], + ) + .unwrap(); + } } impl MockEnvBuilder { @@ -1212,6 +1240,8 @@ impl MockEnvBuilder { self.update_health_contract_config(&rover); self.deploy_nft_contract(&rover); + self.deploy_dao_staking(&rover); + self.set_fee_tiers(&rover); if self.deploy_nft_contract && self.set_nft_contract_minter { self.update_config( @@ -1249,6 +1279,16 @@ impl MockEnvBuilder { }) } + pub fn set_fee_tier_config(mut self, cfg: FeeTierConfig) -> Self { + self.fee_tier_config = Some(cfg); + self + } + + pub fn set_dao_staking_addr(mut self, addr: &Addr) -> Self { + self.dao_staking_addr = Some(addr.clone()); + self + } + //-------------------------------------------------------------------------------------------------- // Execute Msgs //-------------------------------------------------------------------------------------------------- @@ -1432,6 +1472,98 @@ impl MockEnvBuilder { .unwrap() } + fn deploy_dao_staking(&mut self, rover: &Addr) -> Addr { + let dao_addr = if let Some(addr) = self.dao_staking_addr.clone() { + addr + } else { + let code_id = self.app.store_code(mock_dao_staking_contract()); + self.app + .instantiate_contract(code_id, self.get_owner(), &(), &[], "mock-dao-staking", None) + .unwrap() + }; + + // Register in address provider for queries that fetch DaoStaking via AP + self.set_address(MarsAddressType::DaoStaking, dao_addr.clone()); + + // Update CM config with DAO staking address only + self.update_config( + rover, + ConfigUpdates { + dao_staking_address: Some(dao_addr.to_string()), + ..Default::default() + }, + ); + + dao_addr + } + + fn set_fee_tiers(&mut self, rover: &Addr) { + // Default full 10-tier config if none provided (descending thresholds) + let fee_cfg = self.fee_tier_config.clone().unwrap_or(FeeTierConfig { + tiers: vec![ + FeeTier { + id: "tier_1".to_string(), + min_voting_power: "350000".to_string(), + discount_pct: Decimal::percent(75), + }, + FeeTier { + id: "tier_2".to_string(), + min_voting_power: "200000".to_string(), + discount_pct: Decimal::percent(60), + }, + FeeTier { + id: "tier_3".to_string(), + min_voting_power: "100000".to_string(), + discount_pct: Decimal::percent(45), + }, + FeeTier { + id: "tier_4".to_string(), + min_voting_power: "50000".to_string(), + discount_pct: Decimal::percent(35), + }, + FeeTier { + id: "tier_5".to_string(), + min_voting_power: "25000".to_string(), + discount_pct: Decimal::percent(25), + }, + FeeTier { + id: "tier_6".to_string(), + min_voting_power: "10000".to_string(), + discount_pct: Decimal::percent(15), + }, + FeeTier { + id: "tier_7".to_string(), + min_voting_power: "5000".to_string(), + discount_pct: Decimal::percent(10), + }, + FeeTier { + id: "tier_8".to_string(), + min_voting_power: "1000".to_string(), + discount_pct: Decimal::percent(5), + }, + FeeTier { + id: "tier_9".to_string(), + min_voting_power: "100".to_string(), + discount_pct: Decimal::percent(1), + }, + FeeTier { + id: "tier_10".to_string(), + min_voting_power: "0".to_string(), + discount_pct: Decimal::percent(0), + }, + ], + }); + + // Push into Rover config + self.update_config( + rover, + ConfigUpdates { + fee_tier_config: Some(fee_cfg), + ..Default::default() + }, + ); + } + fn get_oracle(&mut self) -> Oracle { if self.oracle.is_none() { let addr = self.deploy_oracle(); diff --git a/packages/types/src/adapters/credit_manager.rs b/packages/types/src/adapters/credit_manager.rs new file mode 100644 index 00000000..347d4e75 --- /dev/null +++ b/packages/types/src/adapters/credit_manager.rs @@ -0,0 +1,56 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Api, Decimal, QuerierWrapper, StdResult}; + +use crate::credit_manager::{AccountTierAndDiscountResponse, QueryMsg}; + +#[cw_serde] +pub struct CreditManagerBase(T); + +impl CreditManagerBase { + pub fn new(address: T) -> CreditManagerBase { + CreditManagerBase(address) + } + + pub fn address(&self) -> &T { + &self.0 + } +} + +pub type CreditManagerUnchecked = CreditManagerBase; +pub type CreditManager = CreditManagerBase; + +impl From for CreditManagerUnchecked { + fn from(cm: CreditManager) -> Self { + Self(cm.address().to_string()) + } +} + +impl CreditManagerUnchecked { + pub fn check(&self, api: &dyn Api) -> StdResult { + Ok(CreditManagerBase::new(api.addr_validate(self.address())?)) + } +} + +impl CreditManager { + pub fn query_account_tier_and_discount( + &self, + querier: &QuerierWrapper, + account_id: &str, + ) -> StdResult { + let res: AccountTierAndDiscountResponse = querier.query_wasm_smart( + self.address(), + &QueryMsg::GetAccountTierAndDiscount { + account_id: account_id.to_string(), + }, + )?; + Ok(res) + } + + pub fn query_discount_pct( + &self, + querier: &QuerierWrapper, + account_id: &str, + ) -> StdResult { + Ok(self.query_account_tier_and_discount(querier, account_id)?.discount_pct) + } +} diff --git a/packages/types/src/adapters/dao_staking.rs b/packages/types/src/adapters/dao_staking.rs new file mode 100644 index 00000000..f9cb8f10 --- /dev/null +++ b/packages/types/src/adapters/dao_staking.rs @@ -0,0 +1,60 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Api, QuerierWrapper, StdResult, Uint128}; + +#[cw_serde] +pub struct VotingPowerAtHeightQuery { + pub address: String, +} + +#[cw_serde] +pub struct VotingPowerAtHeightResponse { + pub power: Uint128, + pub height: u64, +} + +#[cw_serde] +pub enum DaoStakingQueryMsg { + VotingPowerAtHeight(VotingPowerAtHeightQuery), +} + +#[cw_serde] +pub struct DaoStakingBase(T); + +impl DaoStakingBase { + pub fn new(address: T) -> DaoStakingBase { + DaoStakingBase(address) + } + + pub fn address(&self) -> &T { + &self.0 + } +} + +pub type DaoStakingUnchecked = DaoStakingBase; +pub type DaoStaking = DaoStakingBase; + +impl From for DaoStakingUnchecked { + fn from(dao_staking: DaoStaking) -> Self { + Self(dao_staking.address().to_string()) + } +} + +impl DaoStakingUnchecked { + pub fn check(&self, api: &dyn Api) -> StdResult { + Ok(DaoStakingBase::new(api.addr_validate(self.address())?)) + } +} + +impl DaoStaking { + pub fn query_voting_power_at_height( + &self, + querier: &QuerierWrapper, + address: &str, + ) -> StdResult { + let query_msg = DaoStakingQueryMsg::VotingPowerAtHeight(VotingPowerAtHeightQuery { + address: address.to_string(), + }); + + querier.query_wasm_smart(self.address().to_string(), &query_msg) + } +} diff --git a/packages/types/src/adapters/mod.rs b/packages/types/src/adapters/mod.rs index 6538389d..88296c63 100644 --- a/packages/types/src/adapters/mod.rs +++ b/packages/types/src/adapters/mod.rs @@ -1,4 +1,6 @@ pub mod account_nft; +pub mod credit_manager; +pub mod dao_staking; pub mod health; pub mod incentives; pub mod oracle; diff --git a/packages/types/src/adapters/perps.rs b/packages/types/src/adapters/perps.rs index 1f030338..9552063e 100644 --- a/packages/types/src/adapters/perps.rs +++ b/packages/types/src/adapters/perps.rs @@ -1,6 +1,7 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - to_json_binary, Addr, Api, Coin, CosmosMsg, Int128, QuerierWrapper, StdResult, Uint128, WasmMsg, + to_json_binary, Addr, Api, Coin, CosmosMsg, Decimal, Int128, QuerierWrapper, StdResult, + Uint128, WasmMsg, }; use crate::{ @@ -96,6 +97,7 @@ impl Perps { size: Int128, reduce_only: Option, funds: Vec, + discount_pct: Option, ) -> StdResult { Ok(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: self.address().into(), @@ -104,6 +106,7 @@ impl Perps { denom: denom.into(), size, reduce_only, + discount_pct, })?, funds, })) @@ -115,12 +118,14 @@ impl Perps { account_id: impl Into, funds: Vec, action: ActionKind, + discount_pct: Option, ) -> StdResult { Ok(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: self.address().into(), msg: to_json_binary(&ExecuteMsg::CloseAllPositions { account_id: account_id.into(), action: Some(action), + discount_pct, })?, funds, })) @@ -167,12 +172,14 @@ impl Perps { querier: &QuerierWrapper, denom: impl Into, size: Int128, + discount_pct: Option, ) -> StdResult { let res: TradingFee = querier.query_wasm_smart( self.address(), &QueryMsg::OpeningFee { denom: denom.into(), size, + discount_pct, }, )?; Ok(res) diff --git a/packages/types/src/address_provider.rs b/packages/types/src/address_provider.rs index 81cf4ecf..fefd4d03 100644 --- a/packages/types/src/address_provider.rs +++ b/packages/types/src/address_provider.rs @@ -45,6 +45,8 @@ pub enum MarsAddressType { Health, /// The address that shall receive the revenue share given to neutron (10%) RevenueShare, + /// Dao staking contract + DaoStaking, } impl fmt::Display for MarsAddressType { @@ -65,6 +67,7 @@ impl fmt::Display for MarsAddressType { MarsAddressType::Perps => "perps", MarsAddressType::Health => "health", MarsAddressType::RevenueShare => "revenue_share", + MarsAddressType::DaoStaking => "dao_staking", }; write!(f, "{s}") } @@ -90,6 +93,7 @@ impl FromStr for MarsAddressType { "perps" => Ok(MarsAddressType::Perps), "health" => Ok(MarsAddressType::Health), "revenue_share" => Ok(MarsAddressType::RevenueShare), + "dao_staking" => Ok(MarsAddressType::DaoStaking), _ => Err(StdError::parse_err(type_name::(), s)), } } diff --git a/packages/types/src/credit_manager/instantiate.rs b/packages/types/src/credit_manager/instantiate.rs index c607161c..6bdb4abd 100644 --- a/packages/types/src/credit_manager/instantiate.rs +++ b/packages/types/src/credit_manager/instantiate.rs @@ -76,4 +76,7 @@ pub struct ConfigUpdates { pub keeper_fee_config: Option, pub perps_liquidation_bonus_ratio: Option, pub swap_fee: Option, + // Staking-based fee tiers + pub fee_tier_config: Option, + pub dao_staking_address: Option, } diff --git a/packages/types/src/credit_manager/query.rs b/packages/types/src/credit_manager/query.rs index ce7407bd..d0c1a55f 100644 --- a/packages/types/src/credit_manager/query.rs +++ b/packages/types/src/credit_manager/query.rs @@ -114,6 +114,35 @@ pub enum QueryMsg { start_after: Option, limit: Option, }, + + /// Get the staking tier and discount percentage for an account based on their voting power + #[returns(AccountTierAndDiscountResponse)] + GetAccountTierAndDiscount { + account_id: String, + }, + + /// Query the trading fee for a specific account and market type. + #[returns(TradingFeeResponse)] + TradingFee { + account_id: String, + market_type: MarketType, + }, +} + +#[cw_serde] +pub enum MarketType { + Spot, + Perp { + denom: String, + }, +} + +#[cw_serde] +pub struct TradingFeeResponse { + pub base_fee_pct: Decimal, + pub discount_pct: Decimal, + pub effective_fee_pct: Decimal, + pub tier_id: String, } #[cw_serde] @@ -243,3 +272,10 @@ pub struct VaultBinding { pub account_id: String, pub vault_address: String, } + +#[cw_serde] +pub struct AccountTierAndDiscountResponse { + pub tier_id: String, + pub discount_pct: Decimal, + pub voting_power: Uint128, +} diff --git a/packages/types/src/fee_tiers.rs b/packages/types/src/fee_tiers.rs new file mode 100644 index 00000000..7aa61471 --- /dev/null +++ b/packages/types/src/fee_tiers.rs @@ -0,0 +1,38 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Decimal; + +#[cw_serde] +pub struct FeeTier { + pub id: String, + pub min_voting_power: String, // Uint128 as string + pub discount_pct: Decimal, // Percentage as Decimal (e.g., 0.25 for 25%) +} + +#[cw_serde] +pub struct FeeTierConfig { + pub tiers: Vec, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum FeeTierQueryMsg { + #[returns(TradingFeeResponse)] + TradingFee { + address: String, + market_type: MarketType, + }, +} + +#[cw_serde] +pub struct TradingFeeResponse { + pub base_fee_bps: u16, + pub discount_pct: Decimal, + pub effective_fee_bps: u16, + pub bucket_id: String, +} + +#[cw_serde] +pub enum MarketType { + Spot, + Perp, +} diff --git a/packages/types/src/lib.rs b/packages/types/src/lib.rs index 0a681f98..56f93d21 100644 --- a/packages/types/src/lib.rs +++ b/packages/types/src/lib.rs @@ -3,6 +3,7 @@ pub mod adapters; pub mod address_provider; pub mod credit_manager; pub mod error; +pub mod fee_tiers; pub mod health; pub mod incentives; pub mod keys; diff --git a/packages/types/src/perps.rs b/packages/types/src/perps.rs index 7949cead..e62e85b1 100644 --- a/packages/types/src/perps.rs +++ b/packages/types/src/perps.rs @@ -586,6 +586,9 @@ pub enum ExecuteMsg { // Reduce Only enforces a position size cannot increase in absolute terms, ensuring a position will never flip // from long to short or vice versa reduce_only: Option, + + // Discount percentage to apply to trading fees based on staking tier + discount_pct: Option, }, /// Close all perp positions. Use this to liquidate a user's credit account. @@ -595,6 +598,8 @@ pub enum ExecuteMsg { CloseAllPositions { account_id: String, action: Option, + // Discount percentage to apply to trading fees based on staking tier + discount_pct: Option, }, /// Deleveraging a vault by closing a position for an account. @@ -714,6 +719,7 @@ pub enum QueryMsg { OpeningFee { denom: String, size: Int128, + discount_pct: Option, }, /// Query the fees associated with modifying a specific position. @@ -797,6 +803,28 @@ pub struct PositionFeesResponse { pub closing_exec_price: Option, } +#[cw_serde] +pub enum MarketType { + Spot, + Perp { + denom: String, + }, +} + +#[cw_serde] +pub struct TradingFeeQuery { + pub account_id: String, + pub market_type: MarketType, +} + +#[cw_serde] +pub struct TradingFeeResponse { + pub base_fee_pct: Decimal, + pub discount_pct: Decimal, + pub effective_fee_pct: Decimal, + pub tier_id: String, +} + #[derive(Error, Debug, PartialEq)] pub enum PerpsError { #[error("{0}")] diff --git a/scripts/types/generated/mars-perps/MarsPerps.client.ts b/scripts/types/generated/mars-perps/MarsPerps.client.ts index e29bd76d..3464fad6 100644 --- a/scripts/types/generated/mars-perps/MarsPerps.client.ts +++ b/scripts/types/generated/mars-perps/MarsPerps.client.ts @@ -101,7 +101,7 @@ export interface MarsPerpsReadOnlyInterface { }) => Promise marketAccounting: ({ denom }: { denom: string }) => Promise totalAccounting: () => Promise - openingFee: ({ denom, size }: { denom: string; size: Int128 }) => Promise + openingFee: ({ denom, size, discountPct }: { denom: string; size: Int128; discountPct?: Decimal }) => Promise positionFees: ({ accountId, denom, @@ -267,11 +267,12 @@ export class MarsPerpsQueryClient implements MarsPerpsReadOnlyInterface { total_accounting: {}, }) } - openingFee = async ({ denom, size }: { denom: string; size: Int128 }): Promise => { + openingFee = async ({ denom, size, discountPct }: { denom: string; size: Int128; discountPct?: Decimal }) => { return this.client.queryContractSmart(this.contractAddress, { opening_fee: { denom, size, + discount_pct: discountPct, }, }) } @@ -283,7 +284,7 @@ export class MarsPerpsQueryClient implements MarsPerpsReadOnlyInterface { accountId: string denom: string newSize: Int128 - }): Promise => { + }) => Promise => { return this.client.queryContractSmart(this.contractAddress, { position_fees: { account_id: accountId, diff --git a/scripts/types/generated/mars-perps/MarsPerps.react-query.ts b/scripts/types/generated/mars-perps/MarsPerps.react-query.ts index 33576519..5cc0fc96 100644 --- a/scripts/types/generated/mars-perps/MarsPerps.react-query.ts +++ b/scripts/types/generated/mars-perps/MarsPerps.react-query.ts @@ -227,6 +227,7 @@ export interface MarsPerpsOpeningFeeQuery extends MarsPerpsReactQuery({ From 24bca38124814d1431b8bb0432aece0cfd0bfd66 Mon Sep 17 00:00:00 2001 From: Subham2804 Date: Thu, 4 Sep 2025 09:39:24 +0530 Subject: [PATCH 02/16] Remove log statements --- contracts/credit-manager/src/perp.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/contracts/credit-manager/src/perp.rs b/contracts/credit-manager/src/perp.rs index 6a29ea8e..1cb6d895 100644 --- a/contracts/credit-manager/src/perp.rs +++ b/contracts/credit-manager/src/perp.rs @@ -236,11 +236,6 @@ pub fn close_all_perps( let (tier, discount_pct, voting_power) = get_account_tier_and_discount(deps.as_ref(), account_id)?; - // // Get base and effective fees for logging (using a sample denom for reference) - // let sample_denom = perp_positions.first().unwrap().denom.clone(); - // let base_opening_fee = perps.query_opening_fee(&deps.querier, &sample_denom, Int128::new(1000), None)?; - // let effective_opening_fee = perps.query_opening_fee(&deps.querier, &sample_denom, Int128::new(1000), Some(discount_pct))?; - // Close all perp positions at once let close_msg = perps.close_all_msg(account_id, funds, action, Some(discount_pct))?; @@ -249,8 +244,6 @@ pub fn close_all_perps( .add_attribute("action", "close_all_perps") .add_attribute("account_id", account_id) .add_attribute("number_of_positions", perp_positions.len().to_string()) - // .add_attribute("base_opening_fee", base_opening_fee.fee.to_string()) - // .add_attribute("effective_opening_fee", effective_opening_fee.fee.to_string()) .add_attribute("voting_power", voting_power.to_string()) .add_attribute("tier_id", tier.id) .add_attribute("discount_pct", discount_pct.to_string())) From fc0b3be519cd35431cd98c19ab8325f0fc6feaa8 Mon Sep 17 00:00:00 2001 From: Subham2804 Date: Thu, 4 Sep 2025 10:04:23 +0530 Subject: [PATCH 03/16] format --- contracts/credit-manager/src/contract.rs | 4 +- contracts/credit-manager/src/perp.rs | 28 ++++-- contracts/credit-manager/src/staking.rs | 16 +++- .../tests/tests/test_perps_with_discount.rs | 10 +- .../tests/tests/test_staking_tiers.rs | 28 +++--- .../tests/tests/test_swap_with_discount.rs | 8 +- .../tests/tests/test_trading_fee.rs | 30 +++--- contracts/mock-dao-staking/src/lib.rs | 4 +- contracts/perps/src/deleverage.rs | 9 +- contracts/perps/src/position_management.rs | 10 +- contracts/perps/src/query.rs | 76 ++++++++------- contracts/perps/src/state.rs | 6 +- .../perps/tests/tests/helpers/contracts.rs | 25 +++-- .../perps/tests/tests/helpers/mock_env.rs | 9 +- .../tests/test_accounting_with_discount.rs | 96 +++++++++++-------- .../types/src/credit_manager/instantiate.rs | 15 +-- 16 files changed, 226 insertions(+), 148 deletions(-) diff --git a/contracts/credit-manager/src/contract.rs b/contracts/credit-manager/src/contract.rs index 8156a75d..797e29ef 100644 --- a/contracts/credit-manager/src/contract.rs +++ b/contracts/credit-manager/src/contract.rs @@ -19,8 +19,8 @@ use crate::{ query_all_debt_shares, query_all_total_debt_shares, query_all_trigger_orders, query_all_trigger_orders_for_account, query_all_vault_positions, query_all_vault_utilizations, query_config, query_positions, query_swap_fee, - query_total_debt_shares, query_trading_fee, query_vault_bindings, query_vault_position_value, - query_vault_utilization, + query_total_debt_shares, query_trading_fee, query_vault_bindings, + query_vault_position_value, query_vault_utilization, }, repay::repay_from_wallet, state::NEXT_TRIGGER_ID, diff --git a/contracts/credit-manager/src/perp.rs b/contracts/credit-manager/src/perp.rs index 1cb6d895..40941046 100644 --- a/contracts/credit-manager/src/perp.rs +++ b/contracts/credit-manager/src/perp.rs @@ -124,8 +124,10 @@ pub fn execute_perp_order( )?, None => { // Open new position - let base_opening_fee = perps.query_opening_fee(&deps.querier, denom, order_size, None)?; - let opening_fee = perps.query_opening_fee(&deps.querier, denom, order_size, Some(discount_pct))?; + let base_opening_fee = + perps.query_opening_fee(&deps.querier, denom, order_size, None)?; + let opening_fee = + perps.query_opening_fee(&deps.querier, denom, order_size, Some(discount_pct))?; let fee = opening_fee.fee; let funds = if !fee.amount.is_zero() { @@ -135,8 +137,14 @@ pub fn execute_perp_order( vec![] }; - let msg = - perps.execute_perp_order(account_id, denom, order_size, reduce_only, funds, Some(discount_pct))?; + let msg = perps.execute_perp_order( + account_id, + denom, + order_size, + reduce_only, + funds, + Some(discount_pct), + )?; response .add_message(msg) @@ -267,9 +275,17 @@ fn modify_existing_position( // Get base and effective fees for logging let base_opening_fee = perps.query_opening_fee(&deps.querier, denom, order_size, None)?; - let effective_opening_fee = perps.query_opening_fee(&deps.querier, denom, order_size, Some(discount_pct))?; + let effective_opening_fee = + perps.query_opening_fee(&deps.querier, denom, order_size, Some(discount_pct))?; - let msg = perps.execute_perp_order(account_id, denom, order_size, reduce_only, funds, Some(discount_pct))?; + let msg = perps.execute_perp_order( + account_id, + denom, + order_size, + reduce_only, + funds, + Some(discount_pct), + )?; let new_size = position.size.checked_add(order_size)?; diff --git a/contracts/credit-manager/src/staking.rs b/contracts/credit-manager/src/staking.rs index 9ca45863..5eb14df9 100644 --- a/contracts/credit-manager/src/staking.rs +++ b/contracts/credit-manager/src/staking.rs @@ -1,10 +1,16 @@ -use crate::state::{DAO_STAKING_ADDRESS, FEE_TIER_CONFIG}; -use crate::utils::query_nft_token_owner; -use cosmwasm_std::{Decimal, Deps, StdError, StdResult, Uint128}; -use mars_types::adapters::dao_staking::DaoStaking; -use mars_types::fee_tiers::{FeeTier, FeeTierConfig}; use std::str::FromStr; +use cosmwasm_std::{Decimal, Deps, StdError, StdResult, Uint128}; +use mars_types::{ + adapters::dao_staking::DaoStaking, + fee_tiers::{FeeTier, FeeTierConfig}, +}; + +use crate::{ + state::{DAO_STAKING_ADDRESS, FEE_TIER_CONFIG}, + utils::query_nft_token_owner, +}; + pub struct StakingTierManager { pub config: FeeTierConfig, } diff --git a/contracts/credit-manager/tests/tests/test_perps_with_discount.rs b/contracts/credit-manager/tests/tests/test_perps_with_discount.rs index 53732d3b..17a5928f 100644 --- a/contracts/credit-manager/tests/tests/test_perps_with_discount.rs +++ b/contracts/credit-manager/tests/tests/test_perps_with_discount.rs @@ -1,12 +1,16 @@ use cosmwasm_std::{Addr, Coin, Decimal, Int128, Uint128}; use cw_multi_test::AppResponse; -use mars_types::credit_manager::Action::{Deposit, ExecutePerpOrder}; -use mars_types::credit_manager::ExecutePerpOrderType; +use mars_types::{ + credit_manager::{ + Action::{Deposit, ExecutePerpOrder}, + ExecutePerpOrderType, + }, + params::PerpParamsUpdate, +}; use test_case::test_case; use super::helpers::{coin_info, uatom_info, AccountToFund, MockEnv}; use crate::tests::helpers::default_perp_params; -use mars_types::params::PerpParamsUpdate; fn setup_env() -> (MockEnv, Addr, String) { let atom = uatom_info(); diff --git a/contracts/credit-manager/tests/tests/test_staking_tiers.rs b/contracts/credit-manager/tests/tests/test_staking_tiers.rs index b9c43be9..031957e2 100644 --- a/contracts/credit-manager/tests/tests/test_staking_tiers.rs +++ b/contracts/credit-manager/tests/tests/test_staking_tiers.rs @@ -287,7 +287,9 @@ fn test_validate_fee_tier_config_empty() { let result = manager.validate(); assert!(result.is_err()); match result.unwrap_err() { - StdError::GenericErr { msg } => { + StdError::GenericErr { + msg, + } => { assert!(msg.contains("Fee tier config cannot be empty")); } _ => panic!("Expected StdError::GenericErr"), @@ -315,7 +317,9 @@ fn test_validate_fee_tier_config_unsorted() { let result = manager.validate(); assert!(result.is_err()); match result.unwrap_err() { - StdError::GenericErr { msg } => { + StdError::GenericErr { + msg, + } => { assert!(msg.contains("Tiers must be sorted in descending order")); } _ => panic!("Expected StdError::GenericErr"), @@ -343,7 +347,9 @@ fn test_validate_fee_tier_config_duplicate_thresholds() { let result = manager.validate(); assert!(result.is_err()); match result.unwrap_err() { - StdError::GenericErr { msg } => { + StdError::GenericErr { + msg, + } => { assert!(msg.contains("Duplicate voting power thresholds")); } _ => panic!("Expected StdError::GenericErr"), @@ -364,7 +370,9 @@ fn test_validate_fee_tier_config_invalid_discount() { let result = manager.validate(); assert!(result.is_err()); match result.unwrap_err() { - StdError::GenericErr { msg } => { + StdError::GenericErr { + msg, + } => { assert!(msg.contains("Discount percentage must be less than 100%")); } _ => panic!("Expected StdError::GenericErr"), @@ -431,20 +439,12 @@ fn test_get_default_tier() { Decimal::percent(0); "tier 10: no discount" )] -fn test_discount_calculation_examples( - voting_power: Uint128, - expected_discount: Decimal, -) { +fn test_discount_calculation_examples(voting_power: Uint128, expected_discount: Decimal) { let config = create_test_fee_tier_config(); let manager = StakingTierManager::new(config); let tier = manager.find_applicable_tier(voting_power).unwrap(); - assert_eq!( - tier.discount_pct, - expected_discount, - "Failed for voting power: {}", - voting_power - ); + assert_eq!(tier.discount_pct, expected_discount, "Failed for voting power: {}", voting_power); } #[test] diff --git a/contracts/credit-manager/tests/tests/test_swap_with_discount.rs b/contracts/credit-manager/tests/tests/test_swap_with_discount.rs index fc971783..59a80abc 100644 --- a/contracts/credit-manager/tests/tests/test_swap_with_discount.rs +++ b/contracts/credit-manager/tests/tests/test_swap_with_discount.rs @@ -1,9 +1,11 @@ use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; -use mars_types::credit_manager::Action::{Deposit, SwapExactIn}; -use mars_types::swapper::{OsmoRoute, OsmoSwap, SwapperRoute}; +use cw_multi_test::AppResponse; +use mars_types::{ + credit_manager::Action::{Deposit, SwapExactIn}, + swapper::{OsmoRoute, OsmoSwap, SwapperRoute}, +}; use super::helpers::{uatom_info, uosmo_info, AccountToFund, MockEnv}; -use cw_multi_test::AppResponse; fn setup_env_with_swap_fee() -> (MockEnv, Addr, String) { let atom = uatom_info(); diff --git a/contracts/credit-manager/tests/tests/test_trading_fee.rs b/contracts/credit-manager/tests/tests/test_trading_fee.rs index 266737a0..6028ff0a 100644 --- a/contracts/credit-manager/tests/tests/test_trading_fee.rs +++ b/contracts/credit-manager/tests/tests/test_trading_fee.rs @@ -1,9 +1,12 @@ -use super::helpers::{default_perp_params, uosmo_info, MockEnv}; use cosmwasm_std::{Addr, Decimal, Uint128}; -use mars_types::credit_manager::{MarketType, TradingFeeResponse}; -use mars_types::params::PerpParamsUpdate; +use mars_types::{ + credit_manager::{MarketType, TradingFeeResponse}, + params::PerpParamsUpdate, +}; use test_case::test_case; +use super::helpers::{default_perp_params, uosmo_info, MockEnv}; + #[test_case( Uint128::new(200_000), "tier_2", @@ -56,9 +59,10 @@ fn test_trading_fee_query_spot( // Verify the response assert_eq!(response.base_fee_pct, expected_base_fee); assert_eq!(response.discount_pct, expected_discount); - + // Calculate the expected effective fee based on the actual response - let calculated_effective = response.base_fee_pct.checked_mul(Decimal::one() - expected_discount).unwrap(); + let calculated_effective = + response.base_fee_pct.checked_mul(Decimal::one() - expected_discount).unwrap(); assert_eq!(response.effective_fee_pct, calculated_effective); assert_eq!(response.tier_id, expected_tier_id); } @@ -124,10 +128,8 @@ fn test_trading_fee_query_perp( assert_eq!(response.tier_id, expected_tier_id); // The effective fee should be base_fee * (1 - discount) - let expected_effective = response - .base_fee_pct - .checked_mul(Decimal::one() - expected_discount) - .unwrap(); + let expected_effective = + response.base_fee_pct.checked_mul(Decimal::one() - expected_discount).unwrap(); assert_eq!(response.effective_fee_pct, expected_effective); } @@ -156,9 +158,10 @@ fn test_trading_fee_query_edge_cases() { assert_eq!(response.tier_id, "tier_1"); assert_eq!(response.discount_pct, Decimal::percent(75)); - + // Calculate the expected effective fee based on the actual response - let calculated_effective = response.base_fee_pct.checked_mul(Decimal::one() - Decimal::percent(75)).unwrap(); + let calculated_effective = + response.base_fee_pct.checked_mul(Decimal::one() - Decimal::percent(75)).unwrap(); assert_eq!(response.effective_fee_pct, calculated_effective); // Test tier 10 (no discount - 0%) @@ -178,8 +181,9 @@ fn test_trading_fee_query_edge_cases() { assert_eq!(response.tier_id, "tier_10"); assert_eq!(response.discount_pct, Decimal::percent(0)); - + // Calculate the expected effective fee based on the actual response - let calculated_effective = response.base_fee_pct.checked_mul(Decimal::one() - Decimal::percent(0)).unwrap(); + let calculated_effective = + response.base_fee_pct.checked_mul(Decimal::one() - Decimal::percent(0)).unwrap(); assert_eq!(response.effective_fee_pct, calculated_effective); } diff --git a/contracts/mock-dao-staking/src/lib.rs b/contracts/mock-dao-staking/src/lib.rs index 82c7e96e..d8bb0dfa 100644 --- a/contracts/mock-dao-staking/src/lib.rs +++ b/contracts/mock-dao-staking/src/lib.rs @@ -1,7 +1,7 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::to_json_binary; use cosmwasm_std::{ - entry_point, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, Uint128, + entry_point, to_json_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, + StdResult, Uint128, }; use cw_storage_plus::Map; use mars_types::adapters::dao_staking::{ diff --git a/contracts/perps/src/deleverage.rs b/contracts/perps/src/deleverage.rs index e9168b98..f14478c2 100644 --- a/contracts/perps/src/deleverage.rs +++ b/contracts/perps/src/deleverage.rs @@ -21,8 +21,8 @@ use crate::{ position_management::apply_pnl_and_fees, query, state::{ - DeleverageRequestTempStorage, CONFIG, DELEVERAGE_REQUEST_TEMP_STORAGE, MARKET_STATES, - POSITIONS, REALIZED_PNL, TOTAL_CASH_FLOW, ACCOUNT_OPENING_FEE_RATES, + DeleverageRequestTempStorage, ACCOUNT_OPENING_FEE_RATES, CONFIG, + DELEVERAGE_REQUEST_TEMP_STORAGE, MARKET_STATES, POSITIONS, REALIZED_PNL, TOTAL_CASH_FLOW, }, utils::{get_oracle_adapter, get_params_adapter, update_position_attributes}, }; @@ -115,8 +115,9 @@ pub fn deleverage( ms.close_position(current_time, denom_price, base_denom_price, &position)?; // Check if we have a stored opening fee rate for this position - let stored_opening_fee_rate = ACCOUNT_OPENING_FEE_RATES.may_load(deps.storage, (&account_id, &denom))?; - + let stored_opening_fee_rate = + ACCOUNT_OPENING_FEE_RATES.may_load(deps.storage, (&account_id, &denom))?; + let (opening_fee_rate, closing_fee_rate) = if let Some(stored_rate) = stored_opening_fee_rate { // Use the stored opening fee rate (what was actually paid) for historical accuracy // Use current closing fee rate (fair for current operations) diff --git a/contracts/perps/src/position_management.rs b/contracts/perps/src/position_management.rs index 6c8ad00b..a53b8629 100644 --- a/contracts/perps/src/position_management.rs +++ b/contracts/perps/src/position_management.rs @@ -18,7 +18,9 @@ use crate::{ error::{ContractError, ContractResult}, market::MarketStateExt, position::{calculate_new_size, PositionExt, PositionModification}, - state::{CONFIG, MARKET_STATES, POSITIONS, REALIZED_PNL, TOTAL_CASH_FLOW, ACCOUNT_OPENING_FEE_RATES}, + state::{ + ACCOUNT_OPENING_FEE_RATES, CONFIG, MARKET_STATES, POSITIONS, REALIZED_PNL, TOTAL_CASH_FLOW, + }, utils::{ ensure_max_position, ensure_min_position, get_oracle_adapter, get_params_adapter, update_position_attributes, @@ -444,7 +446,11 @@ fn modify_position( // Update the opening fee rate if this was a position flip (new opening fee charged) if is_position_flip { - ACCOUNT_OPENING_FEE_RATES.save(deps.storage, (&account_id, &denom), &opening_fee_rate)?; + ACCOUNT_OPENING_FEE_RATES.save( + deps.storage, + (&account_id, &denom), + &opening_fee_rate, + )?; } "modify_position" diff --git a/contracts/perps/src/query.rs b/contracts/perps/src/query.rs index 1d612ea9..3a35170d 100644 --- a/contracts/perps/src/query.rs +++ b/contracts/perps/src/query.rs @@ -25,8 +25,8 @@ use crate::{ position::{PositionExt, PositionModification}, position_management::compute_discounted_fee_rates, state::{ - CONFIG, DEPOSIT_SHARES, MARKET_STATES, POSITIONS, REALIZED_PNL, - TOTAL_UNLOCKING_OR_UNLOCKED_SHARES, UNLOCKS, VAULT_STATE, ACCOUNT_OPENING_FEE_RATES, + ACCOUNT_OPENING_FEE_RATES, CONFIG, DEPOSIT_SHARES, MARKET_STATES, POSITIONS, REALIZED_PNL, + TOTAL_UNLOCKING_OR_UNLOCKED_SHARES, UNLOCKS, VAULT_STATE, }, utils::{ create_user_id_key, get_credit_manager_adapter, get_oracle_adapter, get_params_adapter, @@ -315,17 +315,20 @@ pub fn query_position( let discount_pct = credit_manager_adapter.query_discount_pct(&deps.querier, &account_id)?; // Check if we have a stored opening fee rate for this position - let stored_opening_fee_rate = ACCOUNT_OPENING_FEE_RATES.may_load(deps.storage, (&account_id, &denom))?; - - let (discounted_opening_fee_rate, discounted_closing_fee_rate) = if let Some(stored_rate) = stored_opening_fee_rate { - // Use the stored opening fee rate (what was actually paid) for historical accuracy - // But still use current discount for closing fee rate (fair for current operations) - let current_closing_fee_rate = perp_params.closing_fee_rate * (Decimal::one() - discount_pct); - (stored_rate, current_closing_fee_rate) - } else { - // Fallback to current rates for existing positions without stored fee rates - compute_discounted_fee_rates(&perp_params, Some(discount_pct)) - }; + let stored_opening_fee_rate = + ACCOUNT_OPENING_FEE_RATES.may_load(deps.storage, (&account_id, &denom))?; + + let (discounted_opening_fee_rate, discounted_closing_fee_rate) = + if let Some(stored_rate) = stored_opening_fee_rate { + // Use the stored opening fee rate (what was actually paid) for historical accuracy + // But still use current discount for closing fee rate (fair for current operations) + let current_closing_fee_rate = + perp_params.closing_fee_rate * (Decimal::one() - discount_pct); + (stored_rate, current_closing_fee_rate) + } else { + // Fallback to current rates for existing positions without stored fee rates + compute_discounted_fee_rates(&perp_params, Some(discount_pct)) + }; let pnl_amounts = position.compute_pnl( &curr_funding, @@ -422,17 +425,20 @@ pub fn query_positions( credit_manager_adapter.query_discount_pct(&deps.querier, &account_id)?; // Check if we have a stored opening fee rate for this position - let stored_opening_fee_rate = ACCOUNT_OPENING_FEE_RATES.may_load(deps.storage, (&account_id, &denom))?; - - let (discounted_opening_fee_rate, discounted_closing_fee_rate) = if let Some(stored_rate) = stored_opening_fee_rate { - // Use the stored opening fee rate (what was actually paid) for historical accuracy - // But still use current discount for closing fee rate (fair for current operations) - let current_closing_fee_rate = perp_params.closing_fee_rate * (Decimal::one() - discount_pct); - (stored_rate, current_closing_fee_rate) - } else { - // Fallback to current rates for existing positions without stored fee rates - compute_discounted_fee_rates(&perp_params, Some(discount_pct)) - }; + let stored_opening_fee_rate = + ACCOUNT_OPENING_FEE_RATES.may_load(deps.storage, (&account_id, &denom))?; + + let (discounted_opening_fee_rate, discounted_closing_fee_rate) = + if let Some(stored_rate) = stored_opening_fee_rate { + // Use the stored opening fee rate (what was actually paid) for historical accuracy + // But still use current discount for closing fee rate (fair for current operations) + let current_closing_fee_rate = + perp_params.closing_fee_rate * (Decimal::one() - discount_pct); + (stored_rate, current_closing_fee_rate) + } else { + // Fallback to current rates for existing positions without stored fee rates + compute_discounted_fee_rates(&perp_params, Some(discount_pct)) + }; let pnl_amounts = position.compute_pnl( &funding, @@ -517,16 +523,18 @@ pub fn query_positions_by_account( let curr_funding = ms.current_funding(current_time, denom_price, base_denom_price)?; // Check if we have a stored opening fee rate for this position - let stored_opening_fee_rate = ACCOUNT_OPENING_FEE_RATES.may_load(deps.storage, (&account_id, &denom))?; - - let (opening_fee_rate, closing_fee_rate) = if let Some(stored_rate) = stored_opening_fee_rate { - // Use the stored opening fee rate (what was actually paid) for historical accuracy - // Use current closing fee rate (fair for current operations) - (stored_rate, perp_params.closing_fee_rate) - } else { - // Fallback to current rates for existing positions without stored fee rates - (perp_params.opening_fee_rate, perp_params.closing_fee_rate) - }; + let stored_opening_fee_rate = + ACCOUNT_OPENING_FEE_RATES.may_load(deps.storage, (&account_id, &denom))?; + + let (opening_fee_rate, closing_fee_rate) = + if let Some(stored_rate) = stored_opening_fee_rate { + // Use the stored opening fee rate (what was actually paid) for historical accuracy + // Use current closing fee rate (fair for current operations) + (stored_rate, perp_params.closing_fee_rate) + } else { + // Fallback to current rates for existing positions without stored fee rates + (perp_params.opening_fee_rate, perp_params.closing_fee_rate) + }; let pnl_amounts = position.compute_pnl( &curr_funding, diff --git a/contracts/perps/src/state.rs b/contracts/perps/src/state.rs index 6aababd9..a4fa5eed 100644 --- a/contracts/perps/src/state.rs +++ b/contracts/perps/src/state.rs @@ -1,12 +1,11 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, StdError, StdResult, Storage, Uint128}; +use cosmwasm_std::{Addr, Decimal, StdError, StdResult, Storage, Uint128}; use cw_storage_plus::{Item, Map}; use mars_owner::Owner; use mars_types::{ keys::UserIdKey, perps::{CashFlow, Config, MarketState, PnlAmounts, Position, UnlockState, VaultState}, }; -use cosmwasm_std::Decimal; #[cw_serde] pub struct DeleverageRequestTempStorage { @@ -42,7 +41,8 @@ pub const POSITIONS: Map<(&str, &str), Position> = Map::new("positions"); pub const REALIZED_PNL: Map<(&str, &str), PnlAmounts> = Map::new("realized_pnls"); // (account_id, denom) => opening fee rate that was actually applied -pub const ACCOUNT_OPENING_FEE_RATES: Map<(&str, &str), Decimal> = Map::new("account_opening_fee_rates"); +pub const ACCOUNT_OPENING_FEE_RATES: Map<(&str, &str), Decimal> = + Map::new("account_opening_fee_rates"); // denom => market cash flow pub const MARKET_CASH_FLOW: Map<&str, CashFlow> = Map::new("market_cf"); diff --git a/contracts/perps/tests/tests/helpers/contracts.rs b/contracts/perps/tests/tests/helpers/contracts.rs index 87fee122..d848855e 100644 --- a/contracts/perps/tests/tests/helpers/contracts.rs +++ b/contracts/perps/tests/tests/helpers/contracts.rs @@ -60,8 +60,10 @@ mod mock_credit_manager { #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult}; - use mars_types::credit_manager::{ExecuteMsg, QueryMsg, Positions, Account}; - use mars_types::health::AccountKind; + use mars_types::{ + credit_manager::{Account, ExecuteMsg, Positions, QueryMsg}, + health::AccountKind, + }; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -86,12 +88,17 @@ mod mock_credit_manager { #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::AccountKind { account_id: _ } => { + QueryMsg::AccountKind { + account_id: _, + } => { // Return a mock account kind let account_kind = AccountKind::Default; cosmwasm_std::to_json_binary(&account_kind) } - QueryMsg::Positions { account_id: _, action: _ } => { + QueryMsg::Positions { + account_id: _, + action: _, + } => { // Return empty positions let positions = Positions { account_id: "1".to_string(), @@ -105,7 +112,11 @@ mod mock_credit_manager { }; cosmwasm_std::to_json_binary(&positions) } - QueryMsg::Accounts { owner: _, start_after: _, limit: _ } => { + QueryMsg::Accounts { + owner: _, + start_after: _, + limit: _, + } => { // Return a mock account let account = Account { id: "1".to_string(), @@ -113,7 +124,9 @@ mod mock_credit_manager { }; cosmwasm_std::to_json_binary(&vec![account]) } - QueryMsg::GetAccountTierAndDiscount { account_id: _ } => { + QueryMsg::GetAccountTierAndDiscount { + account_id: _, + } => { // Return a mock tier and discount response let response = mars_types::credit_manager::AccountTierAndDiscountResponse { tier_id: "default".to_string(), diff --git a/contracts/perps/tests/tests/helpers/mock_env.rs b/contracts/perps/tests/tests/helpers/mock_env.rs index 1789333a..d31d891d 100644 --- a/contracts/perps/tests/tests/helpers/mock_env.rs +++ b/contracts/perps/tests/tests/helpers/mock_env.rs @@ -524,9 +524,12 @@ impl MockEnv { 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(), - }) + .query_wasm_smart( + self.params.clone(), + ¶ms::QueryMsg::PerpParams { + denom: denom.to_string(), + }, + ) .unwrap() } diff --git a/contracts/perps/tests/tests/test_accounting_with_discount.rs b/contracts/perps/tests/tests/test_accounting_with_discount.rs index 9f8eb663..9eff6c8a 100644 --- a/contracts/perps/tests/tests/test_accounting_with_discount.rs +++ b/contracts/perps/tests/tests/test_accounting_with_discount.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -use cosmwasm_std::{coin, Decimal, Int128, Uint128}; +use cosmwasm_std::{coin, Addr, Decimal, Int128, Uint128}; use mars_types::{ params::{PerpParams, PerpParamsUpdate}, perps::Accounting, @@ -8,15 +8,11 @@ use mars_types::{ use super::helpers::MockEnv; use crate::tests::helpers::default_perp_params; -use cosmwasm_std::Addr; #[test] fn accounting_with_discount_fees() { let protocol_fee_rate = Decimal::percent(2); - let mut mock = MockEnv::new() - .protocol_fee_rate(protocol_fee_rate) - .build() - .unwrap(); + let mut mock = MockEnv::new().protocol_fee_rate(protocol_fee_rate).build().unwrap(); // Set up dao staking after building let dao_staking_addr = Addr::unchecked("mock-dao-staking"); @@ -69,63 +65,78 @@ fn accounting_with_discount_fees() { let osmo_accounting_before = mock.query_market_accounting("uosmo").accounting; let atom_accounting_before = mock.query_market_accounting("uatom").accounting; let total_accounting_before = mock.query_total_accounting().accounting; - + assert_eq!(osmo_accounting_before, Accounting::default()); assert_eq!(atom_accounting_before, Accounting::default()); assert_eq!(total_accounting_before, Accounting::default()); // Test opening fees with and without discount let atom_size = Int128::from_str("1000000").unwrap(); - + // Query opening fee without discount let atom_opening_fee_no_discount = mock.query_opening_fee("uatom", atom_size, None).fee; - + // Query opening fee with 50% discount let discount_pct = Decimal::percent(50); - let atom_opening_fee_with_discount = mock.query_opening_fee("uatom", atom_size, Some(discount_pct)).fee; - + let atom_opening_fee_with_discount = + mock.query_opening_fee("uatom", atom_size, Some(discount_pct)).fee; + // Verify discount is applied correctly assert!(atom_opening_fee_with_discount.amount < atom_opening_fee_no_discount.amount); - let expected_discount_amount = atom_opening_fee_no_discount.amount.checked_mul_ceil(discount_pct).unwrap(); - let expected_fee_with_discount = atom_opening_fee_no_discount.amount.checked_sub(expected_discount_amount).unwrap(); - + let expected_discount_amount = + atom_opening_fee_no_discount.amount.checked_mul_ceil(discount_pct).unwrap(); + let expected_fee_with_discount = + atom_opening_fee_no_discount.amount.checked_sub(expected_discount_amount).unwrap(); + // Allow for small rounding differences (within 1 unit) let difference = if expected_fee_with_discount > atom_opening_fee_with_discount.amount { expected_fee_with_discount.checked_sub(atom_opening_fee_with_discount.amount).unwrap() } else { atom_opening_fee_with_discount.amount.checked_sub(expected_fee_with_discount).unwrap() }; - assert!(difference <= Uint128::new(1), "Discount calculation difference too large: expected {}, got {}, difference {}", - expected_fee_with_discount, atom_opening_fee_with_discount.amount, difference); + assert!( + difference <= Uint128::new(1), + "Discount calculation difference too large: expected {}, got {}, difference {}", + expected_fee_with_discount, + atom_opening_fee_with_discount.amount, + difference + ); // Test different discount percentages let discount_25 = Decimal::percent(25); - let atom_opening_fee_25_discount = mock.query_opening_fee("uatom", atom_size, Some(discount_25)).fee; + let atom_opening_fee_25_discount = + mock.query_opening_fee("uatom", atom_size, Some(discount_25)).fee; assert!(atom_opening_fee_25_discount.amount < atom_opening_fee_no_discount.amount); assert!(atom_opening_fee_25_discount.amount > atom_opening_fee_with_discount.amount); let discount_75 = Decimal::percent(75); - let atom_opening_fee_75_discount = mock.query_opening_fee("uatom", atom_size, Some(discount_75)).fee; + let atom_opening_fee_75_discount = + mock.query_opening_fee("uatom", atom_size, Some(discount_75)).fee; assert!(atom_opening_fee_75_discount.amount < atom_opening_fee_25_discount.amount); // Test that 100% discount results in 0 fee let discount_100 = Decimal::percent(100); - let atom_opening_fee_100_discount = mock.query_opening_fee("uatom", atom_size, Some(discount_100)).fee; + let atom_opening_fee_100_discount = + mock.query_opening_fee("uatom", atom_size, Some(discount_100)).fee; assert_eq!(atom_opening_fee_100_discount.amount, Uint128::zero()); // Test that 0% discount is same as no discount let discount_0 = Decimal::zero(); - let atom_opening_fee_0_discount = mock.query_opening_fee("uatom", atom_size, Some(discount_0)).fee; + let atom_opening_fee_0_discount = + mock.query_opening_fee("uatom", atom_size, Some(discount_0)).fee; assert_eq!(atom_opening_fee_0_discount.amount, atom_opening_fee_no_discount.amount); // Test with different position sizes let small_size = Int128::from_str("100000").unwrap(); let small_fee_no_discount = mock.query_opening_fee("uatom", small_size, None).fee; - let small_fee_with_discount = mock.query_opening_fee("uatom", small_size, Some(discount_pct)).fee; - + let small_fee_with_discount = + mock.query_opening_fee("uatom", small_size, Some(discount_pct)).fee; + // Verify proportional discount - let small_expected_discount = small_fee_no_discount.amount.checked_mul_ceil(discount_pct).unwrap(); - let small_expected_fee = small_fee_no_discount.amount.checked_sub(small_expected_discount).unwrap(); + let small_expected_discount = + small_fee_no_discount.amount.checked_mul_ceil(discount_pct).unwrap(); + let small_expected_fee = + small_fee_no_discount.amount.checked_sub(small_expected_discount).unwrap(); let small_difference = if small_expected_fee > small_fee_with_discount.amount { small_expected_fee.checked_sub(small_fee_with_discount.amount).unwrap() } else { @@ -136,11 +147,14 @@ fn accounting_with_discount_fees() { // Test with negative size (short position) let short_size = Int128::from_str("-500000").unwrap(); let short_fee_no_discount = mock.query_opening_fee("uatom", short_size, None).fee; - let short_fee_with_discount = mock.query_opening_fee("uatom", short_size, Some(discount_pct)).fee; - + let short_fee_with_discount = + mock.query_opening_fee("uatom", short_size, Some(discount_pct)).fee; + assert!(short_fee_with_discount.amount < short_fee_no_discount.amount); - let short_expected_discount = short_fee_no_discount.amount.checked_mul_ceil(discount_pct).unwrap(); - let short_expected_fee = short_fee_no_discount.amount.checked_sub(short_expected_discount).unwrap(); + let short_expected_discount = + short_fee_no_discount.amount.checked_mul_ceil(discount_pct).unwrap(); + let short_expected_fee = + short_fee_no_discount.amount.checked_sub(short_expected_discount).unwrap(); let short_difference = if short_expected_fee > short_fee_with_discount.amount { short_expected_fee.checked_sub(short_fee_with_discount.amount).unwrap() } else { @@ -151,12 +165,15 @@ fn accounting_with_discount_fees() { // Test discount consistency across different denoms let osmo_size = Int128::from_str("500000").unwrap(); let osmo_opening_fee_no_discount = mock.query_opening_fee("uosmo", osmo_size, None).fee; - let osmo_opening_fee_with_discount = mock.query_opening_fee("uosmo", osmo_size, Some(discount_pct)).fee; - + let osmo_opening_fee_with_discount = + mock.query_opening_fee("uosmo", osmo_size, Some(discount_pct)).fee; + // Verify discount is applied consistently assert!(osmo_opening_fee_with_discount.amount < osmo_opening_fee_no_discount.amount); - let osmo_expected_discount = osmo_opening_fee_no_discount.amount.checked_mul_ceil(discount_pct).unwrap(); - let osmo_expected_fee = osmo_opening_fee_no_discount.amount.checked_sub(osmo_expected_discount).unwrap(); + let osmo_expected_discount = + osmo_opening_fee_no_discount.amount.checked_mul_ceil(discount_pct).unwrap(); + let osmo_expected_fee = + osmo_opening_fee_no_discount.amount.checked_sub(osmo_expected_discount).unwrap(); let osmo_difference = if osmo_expected_fee > osmo_opening_fee_with_discount.amount { osmo_expected_fee.checked_sub(osmo_opening_fee_with_discount.amount).unwrap() } else { @@ -168,7 +185,7 @@ fn accounting_with_discount_fees() { let osmo_accounting_after = mock.query_market_accounting("uosmo").accounting; let atom_accounting_after = mock.query_market_accounting("uatom").accounting; let total_accounting_after = mock.query_total_accounting().accounting; - + // Since we didn't execute any orders, accounting should still be default assert_eq!(osmo_accounting_after, Accounting::default()); assert_eq!(atom_accounting_after, Accounting::default()); @@ -186,14 +203,9 @@ fn discount_fee_edge_cases() { mock.fund_accounts(&[&credit_manager], 1_000_000_000u128, &["uusdc"]); mock.set_price(&owner, "uusdc", Decimal::from_str("1").unwrap()).unwrap(); mock.set_price(&owner, "uatom", Decimal::from_str("10").unwrap()).unwrap(); - - mock.deposit_to_vault( - &credit_manager, - Some(user), - None, - &[coin(1_000_000_000u128, "uusdc")], - ) - .unwrap(); + + mock.deposit_to_vault(&credit_manager, Some(user), None, &[coin(1_000_000_000u128, "uusdc")]) + .unwrap(); mock.update_perp_params( &owner, @@ -207,7 +219,7 @@ fn discount_fee_edge_cases() { ); let size = Int128::from_str("1000").unwrap(); - + // Test 100% discount (should result in 0 fee) let fee_100_discount = mock.query_opening_fee("uatom", size, Some(Decimal::percent(100))).fee; assert_eq!(fee_100_discount.amount, Uint128::zero()); diff --git a/packages/types/src/credit_manager/instantiate.rs b/packages/types/src/credit_manager/instantiate.rs index 6bdb4abd..38686ec6 100644 --- a/packages/types/src/credit_manager/instantiate.rs +++ b/packages/types/src/credit_manager/instantiate.rs @@ -2,11 +2,14 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Decimal, Uint128}; use super::KeeperFeeConfig; -use crate::adapters::{ - account_nft::AccountNftUnchecked, health::HealthContractUnchecked, - incentives::IncentivesUnchecked, oracle::OracleUnchecked, params::ParamsUnchecked, - perps::PerpsUnchecked, red_bank::RedBankUnchecked, swapper::SwapperUnchecked, - zapper::ZapperUnchecked, +use crate::{ + adapters::{ + account_nft::AccountNftUnchecked, health::HealthContractUnchecked, + incentives::IncentivesUnchecked, oracle::OracleUnchecked, params::ParamsUnchecked, + perps::PerpsUnchecked, red_bank::RedBankUnchecked, swapper::SwapperUnchecked, + zapper::ZapperUnchecked, + }, + fee_tiers::FeeTierConfig, }; #[cw_serde] @@ -77,6 +80,6 @@ pub struct ConfigUpdates { pub perps_liquidation_bonus_ratio: Option, pub swap_fee: Option, // Staking-based fee tiers - pub fee_tier_config: Option, + pub fee_tier_config: Option, pub dao_staking_address: Option, } From 42ec1e06eb2458abe2416961b26e90e8cd1b0af1 Mon Sep 17 00:00:00 2001 From: Subham2804 Date: Thu, 4 Sep 2025 15:03:07 +0530 Subject: [PATCH 04/16] add validation tests and remove unwanted comments --- contracts/credit-manager/src/perp.rs | 1 - contracts/credit-manager/src/query.rs | 12 +- contracts/credit-manager/src/staking.rs | 55 ++-- .../tests/tests/test_staking_tiers.rs | 253 ++++++++++++++++++ .../tests/tests/test_swap_with_discount.rs | 27 +- contracts/vault/tests/tests/test_redeem.rs | 2 +- .../mars-address-provider.json | 35 +++ .../mars-credit-manager.json | 194 ++++++++++++++ schemas/mars-perps/mars-perps.json | 34 +++ scripts/deploy/base/deployer.ts | 2 + scripts/deploy/neutron/mainnet-config.ts | 4 + scripts/deploy/neutron/testnet-config.ts | 4 + scripts/types/config.ts | 11 + .../MarsAddressProvider.types.ts | 1 + .../MarsCreditManager.client.ts | 44 +++ .../MarsCreditManager.react-query.ts | 74 +++++ .../MarsCreditManager.types.ts | 39 +++ .../generated/mars-perps/MarsPerps.client.ts | 34 ++- .../mars-perps/MarsPerps.react-query.ts | 5 +- .../generated/mars-perps/MarsPerps.types.ts | 3 + 20 files changed, 794 insertions(+), 40 deletions(-) diff --git a/contracts/credit-manager/src/perp.rs b/contracts/credit-manager/src/perp.rs index 40941046..987605e1 100644 --- a/contracts/credit-manager/src/perp.rs +++ b/contracts/credit-manager/src/perp.rs @@ -238,7 +238,6 @@ pub fn close_all_perps( let (funds, response) = update_state_based_on_pnl(&mut deps, account_id, pnl, Some(action.clone()), response)?; let funds = funds.map_or_else(Vec::new, |c| vec![c]); - println!("funds : {:?}", funds); // Get staking tier discount for this account let (tier, discount_pct, voting_power) = diff --git a/contracts/credit-manager/src/query.rs b/contracts/credit-manager/src/query.rs index cf948b52..55baa6f9 100644 --- a/contracts/credit-manager/src/query.rs +++ b/contracts/credit-manager/src/query.rs @@ -364,9 +364,6 @@ pub fn query_account_tier_and_discount( }) } -/// Queries the trading fee for a specific account and market type. -/// For spot markets, returns a default fee structure. -/// For perp markets, calculates the opening fee based on the denom and applies any applicable discounts. pub fn query_trading_fee( deps: Deps, account_id: &str, @@ -374,14 +371,11 @@ pub fn query_trading_fee( ) -> ContractResult { use crate::staking::get_account_tier_and_discount; - // Get staking tier discount for this account let (tier, discount_pct, _) = get_account_tier_and_discount(deps, account_id)?; match market_type { mars_types::credit_manager::MarketType::Spot => { - // For spot markets, use a default fee structure - // You can customize this based on your spot trading requirements - let base_fee_pct = cosmwasm_std::Decimal::percent(25); // 0.25% base fee + let base_fee_pct = SWAP_FEE.load(deps.storage)?; let effective_fee_pct = base_fee_pct.checked_mul(cosmwasm_std::Decimal::one() - discount_pct)?; @@ -395,10 +389,8 @@ pub fn query_trading_fee( mars_types::credit_manager::MarketType::Perp { denom, } => { - // For perp markets, get the opening fee rate and apply discount - let params = crate::state::PARAMS.load(deps.storage)?; + let params = PARAMS.load(deps.storage)?; - // Query the params contract to get the opening fee rate for this denom let perp_params = params.query_perp_params(&deps.querier, denom)?; let base_fee_pct = perp_params.opening_fee_rate; let effective_fee_pct = diff --git a/contracts/credit-manager/src/staking.rs b/contracts/credit-manager/src/staking.rs index 5eb14df9..55e550e8 100644 --- a/contracts/credit-manager/src/staking.rs +++ b/contracts/credit-manager/src/staking.rs @@ -25,19 +25,39 @@ impl StakingTierManager { /// Find the applicable tier for a given voting power /// Returns the tier with the highest min_voting_power that the user qualifies for pub fn find_applicable_tier(&self, voting_power: Uint128) -> StdResult<&FeeTier> { - // Tiers should be sorted in descending order of min_voting_power - // So we find the first tier where user's voting power >= tier's min_voting_power - for tier in &self.config.tiers { + // Ensure tiers are sorted in descending order of min_voting_power + if self.config.tiers.is_empty() { + return Err(StdError::generic_err("No tiers configured")); + } + + // Binary search for the applicable tier + let mut left = 0; + let mut right = self.config.tiers.len() - 1; + let mut result = 0; // Default to first tier (highest threshold) + + while left <= right { + let mid = left + (right - left) / 2; + let tier = &self.config.tiers[mid]; + + // Parse min_voting_power once per tier let min_power = Uint128::from_str(&tier.min_voting_power) .map_err(|_| StdError::generic_err("Invalid min_voting_power in tier"))?; if voting_power >= min_power { - return Ok(tier); + // User qualifies for this tier, but there might be a better one + result = mid; + // Look for higher tiers (lower indices) but don't go below 0 + if mid == 0 { + break; // We found the highest tier + } + right = mid - 1; + } else { + // User doesn't qualify for this tier, look at lower tiers + left = mid + 1; } } - // If no tier found, return the last tier (lowest threshold) - self.config.tiers.last().ok_or_else(|| StdError::generic_err("No tiers configured")) + Ok(&self.config.tiers[result]) } /// Validate that tiers are properly ordered by min_voting_power (descending) @@ -46,10 +66,12 @@ impl StakingTierManager { return Err(StdError::generic_err("Fee tier config cannot be empty")); } + // Parse first tier once + let mut prev_power = Uint128::from_str(&self.config.tiers[0].min_voting_power) + .map_err(|_| StdError::generic_err("Invalid min_voting_power in tier"))?; + // Check for descending order and duplicates in one pass for i in 1..self.config.tiers.len() { - let prev_power = Uint128::from_str(&self.config.tiers[i - 1].min_voting_power) - .map_err(|_| StdError::generic_err("Invalid min_voting_power in tier"))?; let curr_power = Uint128::from_str(&self.config.tiers[i].min_voting_power) .map_err(|_| StdError::generic_err("Invalid min_voting_power in tier"))?; @@ -60,6 +82,8 @@ impl StakingTierManager { if curr_power >= prev_power { return Err(StdError::generic_err("Tiers must be sorted in descending order")); } + + prev_power = curr_power; } // Validate discount percentages are reasonable (0-100%) @@ -92,30 +116,23 @@ impl StakingTierManager { } /// Get tier, discount percentage, and voting power for an account based on their staked MARS balance -/// -/// # Arguments -/// * `deps` - Contract dependencies -/// * `account_id` - The account ID to check -/// -/// # Returns -/// * `StdResult<(FeeTier, Decimal, Uint128)>` - The applicable tier, discount percentage, and voting power pub fn get_account_tier_and_discount( deps: Deps, account_id: &str, ) -> StdResult<(FeeTier, Decimal, Uint128)> { - // 1. Get account owner from account_id + // Get account owner from account_id let account_owner = query_nft_token_owner(deps, account_id) .map_err(|e| StdError::generic_err(e.to_string()))?; - // 2. Get DAO staking contract address from state + // Get DAO staking contract address from state let dao_staking_addr = DAO_STAKING_ADDRESS.load(deps.storage)?; let dao_staking = DaoStaking::new(dao_staking_addr); - // 3. Query voting power for the account owner + // Query voting power for the account owner let voting_power_response = dao_staking.query_voting_power_at_height(&deps.querier, &account_owner)?; - // 4. Get fee tier config and find applicable tier + // Get fee tier config and find applicable tier let fee_tier_config = FEE_TIER_CONFIG.load(deps.storage)?; let manager = StakingTierManager::new(fee_tier_config); let tier = manager.find_applicable_tier(voting_power_response.power)?; diff --git a/contracts/credit-manager/tests/tests/test_staking_tiers.rs b/contracts/credit-manager/tests/tests/test_staking_tiers.rs index 031957e2..6c62fffd 100644 --- a/contracts/credit-manager/tests/tests/test_staking_tiers.rs +++ b/contracts/credit-manager/tests/tests/test_staking_tiers.rs @@ -501,3 +501,256 @@ fn test_fee_tier_config_with_two_tiers() { assert_eq!(tier.id, "high_tier"); assert_eq!(tier.discount_pct, Decimal::percent(50)); } + +// ===== VALIDATION TEST CASES ===== + +#[test] +fn test_validation_empty_tiers() { + let config = FeeTierConfig { + tiers: vec![], + }; + let manager = StakingTierManager::new(config); + + let result = manager.validate(); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), StdError::generic_err("Fee tier config cannot be empty")); +} + +#[test] +fn test_validation_single_tier_valid() { + let config = FeeTierConfig { + tiers: vec![FeeTier { + id: "single".to_string(), + min_voting_power: "1000".to_string(), + discount_pct: Decimal::percent(25), + }], + }; + let manager = StakingTierManager::new(config); + + let result = manager.validate(); + assert!(result.is_ok()); +} + +#[test] +fn test_validation_multiple_tiers_valid() { + let config = create_test_fee_tier_config(); + let manager = StakingTierManager::new(config); + + let result = manager.validate(); + assert!(result.is_ok()); +} + +#[test] +fn test_validation_duplicate_voting_power() { + let config = FeeTierConfig { + tiers: vec![ + FeeTier { + id: "tier_1".to_string(), + min_voting_power: "1000".to_string(), + discount_pct: Decimal::percent(50), + }, + FeeTier { + id: "tier_2".to_string(), + min_voting_power: "1000".to_string(), // Duplicate! + discount_pct: Decimal::percent(25), + }, + ], + }; + let manager = StakingTierManager::new(config); + + let result = manager.validate(); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), StdError::generic_err("Duplicate voting power thresholds")); +} + +#[test] +fn test_validation_not_descending_order() { + let config = FeeTierConfig { + tiers: vec![ + FeeTier { + id: "tier_1".to_string(), + min_voting_power: "1000".to_string(), + discount_pct: Decimal::percent(50), + }, + FeeTier { + id: "tier_2".to_string(), + min_voting_power: "2000".to_string(), // Higher than previous! + discount_pct: Decimal::percent(25), + }, + ], + }; + let manager = StakingTierManager::new(config); + + let result = manager.validate(); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + StdError::generic_err("Tiers must be sorted in descending order") + ); +} + +#[test] +fn test_validation_equal_voting_power() { + let config = FeeTierConfig { + tiers: vec![ + FeeTier { + id: "tier_1".to_string(), + min_voting_power: "1000".to_string(), + discount_pct: Decimal::percent(50), + }, + FeeTier { + id: "tier_2".to_string(), + min_voting_power: "1000".to_string(), // Equal to previous! + discount_pct: Decimal::percent(25), + }, + ], + }; + let manager = StakingTierManager::new(config); + + let result = manager.validate(); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), StdError::generic_err("Duplicate voting power thresholds")); +} + +#[test] +fn test_validation_invalid_voting_power_format() { + let config = FeeTierConfig { + tiers: vec![FeeTier { + id: "tier_1".to_string(), + min_voting_power: "invalid_number".to_string(), // Invalid format! + discount_pct: Decimal::percent(50), + }], + }; + let manager = StakingTierManager::new(config); + + let result = manager.validate(); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), StdError::generic_err("Invalid min_voting_power in tier")); +} + +#[test] +fn test_validation_discount_100_percent() { + let config = FeeTierConfig { + tiers: vec![FeeTier { + id: "tier_1".to_string(), + min_voting_power: "1000".to_string(), + discount_pct: Decimal::one(), // 100% discount! + }], + }; + let manager = StakingTierManager::new(config); + + let result = manager.validate(); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + StdError::generic_err("Discount percentage must be less than 100%") + ); +} + +#[test] +fn test_validation_discount_over_100_percent() { + let config = FeeTierConfig { + tiers: vec![FeeTier { + id: "tier_1".to_string(), + min_voting_power: "1000".to_string(), + discount_pct: Decimal::percent(150), // 150% discount! + }], + }; + let manager = StakingTierManager::new(config); + + let result = manager.validate(); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + StdError::generic_err("Discount percentage must be less than 100%") + ); +} + +#[test] +fn test_validation_discount_99_percent_valid() { + let config = FeeTierConfig { + tiers: vec![FeeTier { + id: "tier_1".to_string(), + min_voting_power: "1000".to_string(), + discount_pct: Decimal::percent(99), // 99% discount - valid! + }], + }; + let manager = StakingTierManager::new(config); + + let result = manager.validate(); + assert!(result.is_ok()); +} + +#[test] +fn test_validation_zero_discount_valid() { + let config = FeeTierConfig { + tiers: vec![FeeTier { + id: "tier_1".to_string(), + min_voting_power: "1000".to_string(), + discount_pct: Decimal::zero(), // 0% discount - valid! + }], + }; + let manager = StakingTierManager::new(config); + + let result = manager.validate(); + assert!(result.is_ok()); +} + +#[test] +fn test_validation_complex_scenario() { + let config = FeeTierConfig { + tiers: vec![ + FeeTier { + id: "platinum".to_string(), + min_voting_power: "1000000".to_string(), + discount_pct: Decimal::percent(90), + }, + FeeTier { + id: "gold".to_string(), + min_voting_power: "500000".to_string(), + discount_pct: Decimal::percent(75), + }, + FeeTier { + id: "silver".to_string(), + min_voting_power: "100000".to_string(), + discount_pct: Decimal::percent(50), + }, + FeeTier { + id: "bronze".to_string(), + min_voting_power: "10000".to_string(), + discount_pct: Decimal::percent(25), + }, + FeeTier { + id: "basic".to_string(), + min_voting_power: "0".to_string(), + discount_pct: Decimal::zero(), + }, + ], + }; + let manager = StakingTierManager::new(config); + + let result = manager.validate(); + assert!(result.is_ok()); +} + +#[test] +fn test_validation_edge_case_single_digit() { + let config = FeeTierConfig { + tiers: vec![ + FeeTier { + id: "tier_1".to_string(), + min_voting_power: "1".to_string(), + discount_pct: Decimal::percent(10), + }, + FeeTier { + id: "tier_2".to_string(), + min_voting_power: "0".to_string(), + discount_pct: Decimal::zero(), + }, + ], + }; + let manager = StakingTierManager::new(config); + + let result = manager.validate(); + assert!(result.is_ok()); +} diff --git a/contracts/credit-manager/tests/tests/test_swap_with_discount.rs b/contracts/credit-manager/tests/tests/test_swap_with_discount.rs index 59a80abc..01b9506e 100644 --- a/contracts/credit-manager/tests/tests/test_swap_with_discount.rs +++ b/contracts/credit-manager/tests/tests/test_swap_with_discount.rs @@ -76,6 +76,14 @@ fn test_swap_with_discount() { .collect::>() }; + // Helper to extract and parse fee values + let extract_fees = |res: &AppResponse| { + let attrs = extract(res); + let base_fee = attrs.get("base_swap_fee").unwrap().parse::().unwrap(); + let effective_fee = attrs.get("effective_swap_fee").unwrap().parse::().unwrap(); + (base_fee, effective_fee) + }; + // Tier 10 (min power 0) → 0% discount mock.set_voting_power(&user, Uint128::new(0)); let res = do_swap(&mut mock, &account_id, &user, 10_000, "uatom", "uosmo"); @@ -84,6 +92,11 @@ fn test_swap_with_discount() { assert_eq!(attrs.get("tier_id").unwrap(), "tier_10"); assert_eq!(attrs.get("discount_pct").unwrap(), &Decimal::percent(0).to_string()); + // Verify fees: no discount means base_fee == effective_fee + let (base_fee, effective_fee) = extract_fees(&res); + assert_eq!(base_fee, Decimal::percent(1)); // 1% base fee + assert_eq!(effective_fee, Decimal::percent(1)); // No discount applied + // Tier 7 (>= 5_000) → 10% discount mock.set_voting_power(&user, Uint128::new(5_000)); let res = do_swap(&mut mock, &account_id, &user, 10_000, "uatom", "uosmo"); @@ -92,6 +105,11 @@ fn test_swap_with_discount() { assert_eq!(attrs.get("tier_id").unwrap(), "tier_7"); assert_eq!(attrs.get("discount_pct").unwrap(), &Decimal::percent(10).to_string()); + // Verify fees: 10% discount means effective_fee = base_fee * (1 - 0.1) = base_fee * 0.9 + let (base_fee, effective_fee) = extract_fees(&res); + assert_eq!(base_fee, Decimal::percent(1)); // 1% base fee + assert_eq!(effective_fee, Decimal::percent(1) * (Decimal::one() - Decimal::percent(10))); // 0.9% effective fee + // Tier 3 (>= 100_000) → 45% discount mock.set_voting_power(&user, Uint128::new(100_000)); let res = do_swap(&mut mock, &account_id, &user, 10_000, "uatom", "uosmo"); @@ -100,10 +118,11 @@ fn test_swap_with_discount() { assert_eq!(attrs.get("tier_id").unwrap(), "tier_3"); assert_eq!(attrs.get("discount_pct").unwrap(), &Decimal::percent(45).to_string()); - // Assertions are based on event attributes of last swap to validate effective fee applied - // (Simple smoke check: ensure the action attribute is present and swapper ran.) - // Detailed per-fee verification can be added by inspecting event attributes similarly to existing swap tests. - // Sanity check that swapper action exists in last response + // Verify fees: 45% discount means effective_fee = base_fee * (1 - 0.45) = base_fee * 0.55 + let (base_fee, effective_fee) = extract_fees(&res); + assert_eq!(base_fee, Decimal::percent(1)); // 1% base fee + assert_eq!(effective_fee, Decimal::percent(1) * (Decimal::one() - Decimal::percent(45))); // 0.55% effective fee + assert!(res .events .iter() diff --git a/contracts/vault/tests/tests/test_redeem.rs b/contracts/vault/tests/tests/test_redeem.rs index 2690d9e5..ce364fef 100644 --- a/contracts/vault/tests/tests/test_redeem.rs +++ b/contracts/vault/tests/tests/test_redeem.rs @@ -90,7 +90,7 @@ fn redeem_invalid_funds() { ); assert_vault_err( res, - ContractError::Payment(PaymentError::MissingDenom("factory/contract14/vault".to_string())), + ContractError::Payment(PaymentError::MissingDenom("factory/contract15/vault".to_string())), ); } diff --git a/schemas/mars-address-provider/mars-address-provider.json b/schemas/mars-address-provider/mars-address-provider.json index 220ac77a..5bcb04fc 100644 --- a/schemas/mars-address-provider/mars-address-provider.json +++ b/schemas/mars-address-provider/mars-address-provider.json @@ -143,6 +143,13 @@ "enum": [ "revenue_share" ] + }, + { + "description": "Dao staking contract", + "type": "string", + "enum": [ + "dao_staking" + ] } ] }, @@ -382,6 +389,13 @@ "enum": [ "revenue_share" ] + }, + { + "description": "Dao staking contract", + "type": "string", + "enum": [ + "dao_staking" + ] } ] } @@ -489,6 +503,13 @@ "enum": [ "revenue_share" ] + }, + { + "description": "Dao staking contract", + "type": "string", + "enum": [ + "dao_staking" + ] } ] } @@ -599,6 +620,13 @@ "enum": [ "revenue_share" ] + }, + { + "description": "Dao staking contract", + "type": "string", + "enum": [ + "dao_staking" + ] } ] } @@ -709,6 +737,13 @@ "enum": [ "revenue_share" ] + }, + { + "description": "Dao staking contract", + "type": "string", + "enum": [ + "dao_staking" + ] } ] } diff --git a/schemas/mars-credit-manager/mars-credit-manager.json b/schemas/mars-credit-manager/mars-credit-manager.json index 9cf33360..7798bc8c 100644 --- a/schemas/mars-credit-manager/mars-credit-manager.json +++ b/schemas/mars-credit-manager/mars-credit-manager.json @@ -2430,6 +2430,12 @@ } ] }, + "dao_staking_address": { + "type": [ + "string", + "null" + ] + }, "duality_swapper": { "anyOf": [ { @@ -2440,6 +2446,16 @@ } ] }, + "fee_tier_config": { + "anyOf": [ + { + "$ref": "#/definitions/FeeTierConfig" + }, + { + "type": "null" + } + ] + }, "health_contract": { "anyOf": [ { @@ -2708,6 +2724,41 @@ } ] }, + "FeeTier": { + "type": "object", + "required": [ + "discount_pct", + "id", + "min_voting_power" + ], + "properties": { + "discount_pct": { + "$ref": "#/definitions/Decimal" + }, + "id": { + "type": "string" + }, + "min_voting_power": { + "type": "string" + } + }, + "additionalProperties": false + }, + "FeeTierConfig": { + "type": "object", + "required": [ + "tiers" + ], + "properties": { + "tiers": { + "type": "array", + "items": { + "$ref": "#/definitions/FeeTier" + } + } + }, + "additionalProperties": false + }, "HealthContractBase_for_String": { "type": "string" }, @@ -3744,6 +3795,54 @@ }, "additionalProperties": false }, + { + "description": "Get the staking tier and discount percentage for an account based on their voting power", + "type": "object", + "required": [ + "get_account_tier_and_discount" + ], + "properties": { + "get_account_tier_and_discount": { + "type": "object", + "required": [ + "account_id" + ], + "properties": { + "account_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Query the trading fee for a specific account and market type.", + "type": "object", + "required": [ + "trading_fee" + ], + "properties": { + "trading_fee": { + "type": "object", + "required": [ + "account_id", + "market_type" + ], + "properties": { + "account_id": { + "type": "string" + }, + "market_type": { + "$ref": "#/definitions/MarketType" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -3802,6 +3901,37 @@ }, "additionalProperties": false }, + "MarketType": { + "oneOf": [ + { + "type": "string", + "enum": [ + "spot" + ] + }, + { + "type": "object", + "required": [ + "perp" + ], + "properties": { + "perp": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" @@ -6967,6 +7097,38 @@ } } }, + "get_account_tier_and_discount": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AccountTierAndDiscountResponse", + "type": "object", + "required": [ + "discount_pct", + "tier_id", + "voting_power" + ], + "properties": { + "discount_pct": { + "$ref": "#/definitions/Decimal" + }, + "tier_id": { + "type": "string" + }, + "voting_power": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false, + "definitions": { + "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" + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, "positions": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Positions", @@ -7338,6 +7500,38 @@ } } }, + "trading_fee": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TradingFeeResponse", + "type": "object", + "required": [ + "base_fee_pct", + "discount_pct", + "effective_fee_pct", + "tier_id" + ], + "properties": { + "base_fee_pct": { + "$ref": "#/definitions/Decimal" + }, + "discount_pct": { + "$ref": "#/definitions/Decimal" + }, + "effective_fee_pct": { + "$ref": "#/definitions/Decimal" + }, + "tier_id": { + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "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" + } + } + }, "vault_bindings": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Array_of_VaultBinding", diff --git a/schemas/mars-perps/mars-perps.json b/schemas/mars-perps/mars-perps.json index cf993f0f..44862529 100644 --- a/schemas/mars-perps/mars-perps.json +++ b/schemas/mars-perps/mars-perps.json @@ -217,6 +217,16 @@ "denom": { "type": "string" }, + "discount_pct": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, "reduce_only": { "type": [ "boolean", @@ -257,6 +267,16 @@ "type": "null" } ] + }, + "discount_pct": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false @@ -993,6 +1013,16 @@ "denom": { "type": "string" }, + "discount_pct": { + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, "size": { "$ref": "#/definitions/Int128" } @@ -1042,6 +1072,10 @@ "liquidation" ] }, + "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" + }, "Int128": { "description": "An implementation of i128 that is using strings for JSON encoding/decoding, such that the full i128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `i128` to get the value out:\n\n``` # use cosmwasm_std::Int128; let a = Int128::from(258i128); assert_eq!(a.i128(), 258); ```", "type": "string" diff --git a/scripts/deploy/base/deployer.ts b/scripts/deploy/base/deployer.ts index 2f1a4e95..e2a8bd18 100644 --- a/scripts/deploy/base/deployer.ts +++ b/scripts/deploy/base/deployer.ts @@ -210,6 +210,8 @@ export class Deployer { keeper_fee_config: this.config.keeperFeeConfig, perps_liquidation_bonus_ratio: this.config.perpsLiquidationBonusRatio, swap_fee: this.config.swapFee, + fee_tier_config: this.config.feeTierConfig, + dao_staking_address: this.config.daoStakingAddress, } await this.instantiate('creditManager', this.storage.codeIds.creditManager!, msg) diff --git a/scripts/deploy/neutron/mainnet-config.ts b/scripts/deploy/neutron/mainnet-config.ts index e06f7244..a81f7d06 100644 --- a/scripts/deploy/neutron/mainnet-config.ts +++ b/scripts/deploy/neutron/mainnet-config.ts @@ -412,4 +412,8 @@ export const neutronMainnetConfig: DeploymentConfig = { maxPerpParams: 20, perpsLiquidationBonusRatio: '0.6', swapFee: '0.0005', + feeTierConfig: { + tiers: [], + }, + daoStakingAddress: '', } diff --git a/scripts/deploy/neutron/testnet-config.ts b/scripts/deploy/neutron/testnet-config.ts index 23340b14..5d58ae6b 100644 --- a/scripts/deploy/neutron/testnet-config.ts +++ b/scripts/deploy/neutron/testnet-config.ts @@ -878,4 +878,8 @@ export const neutronTestnetConfig: DeploymentConfig = { maxPerpParams: 20, perpsLiquidationBonusRatio: '0.6', swapFee: '0.0005', + feeTierConfig: { + tiers: [], + }, + daoStakingAddress: '', } diff --git a/scripts/types/config.ts b/scripts/types/config.ts index 64d4c8da..c2aced6e 100644 --- a/scripts/types/config.ts +++ b/scripts/types/config.ts @@ -58,6 +58,15 @@ export interface AstroportConfig { incentives: string } +export interface FeeTierConfig { + tiers: FeeTier[] +} +export interface FeeTier { + discount_pct: Decimal + id: string + min_voting_power: string +} + export interface DeploymentConfig { mainnet: boolean deployerMnemonic: string @@ -130,6 +139,8 @@ export interface DeploymentConfig { maxPerpParams: number perpsLiquidationBonusRatio: Decimal swapFee: Decimal + feeTierConfig: FeeTierConfig + daoStakingAddress: string } export interface AssetConfig { diff --git a/scripts/types/generated/mars-address-provider/MarsAddressProvider.types.ts b/scripts/types/generated/mars-address-provider/MarsAddressProvider.types.ts index 264d5d44..64358e5a 100644 --- a/scripts/types/generated/mars-address-provider/MarsAddressProvider.types.ts +++ b/scripts/types/generated/mars-address-provider/MarsAddressProvider.types.ts @@ -30,6 +30,7 @@ export type MarsAddressType = | 'perps' | 'health' | 'revenue_share' + | 'dao_staking' export type OwnerUpdate = | { propose_new_owner: { diff --git a/scripts/types/generated/mars-credit-manager/MarsCreditManager.client.ts b/scripts/types/generated/mars-credit-manager/MarsCreditManager.client.ts index fc7324f0..70ddf9a0 100644 --- a/scripts/types/generated/mars-credit-manager/MarsCreditManager.client.ts +++ b/scripts/types/generated/mars-credit-manager/MarsCreditManager.client.ts @@ -53,6 +53,8 @@ import { OsmoRoute, OsmoSwap, ConfigUpdates, + FeeTierConfig, + FeeTier, NftConfigUpdates, VaultBaseForAddr, HealthValuesResponse, @@ -62,6 +64,7 @@ import { VaultAmount, VaultAmount1, UnlockingPositions, + MarketType, VaultPosition, LockingVaultAmount, VaultUnlockingPosition, @@ -85,10 +88,12 @@ import { OwnerResponse, RewardsCollector, ArrayOfCoin, + AccountTierAndDiscountResponse, Positions, DebtAmount, PerpPosition, PnlAmounts, + TradingFeeResponse, ArrayOfVaultBinding, VaultBinding, VaultPositionValue, @@ -187,6 +192,18 @@ export interface MarsCreditManagerReadOnlyInterface { limit?: number startAfter?: string }) => Promise + getAccountTierAndDiscount: ({ + accountId, + }: { + accountId: string + }) => Promise + tradingFee: ({ + accountId, + marketType, + }: { + accountId: string + marketType: MarketType + }) => Promise swapFeeRate: () => Promise } export class MarsCreditManagerQueryClient implements MarsCreditManagerReadOnlyInterface { @@ -212,6 +229,8 @@ export class MarsCreditManagerQueryClient implements MarsCreditManagerReadOnlyIn this.allTriggerOrders = this.allTriggerOrders.bind(this) this.allAccountTriggerOrders = this.allAccountTriggerOrders.bind(this) this.vaultBindings = this.vaultBindings.bind(this) + this.getAccountTierAndDiscount = this.getAccountTierAndDiscount.bind(this) + this.tradingFee = this.tradingFee.bind(this) this.swapFeeRate = this.swapFeeRate.bind(this) } accountKind = async ({ accountId }: { accountId: string }): Promise => { @@ -420,6 +439,31 @@ export class MarsCreditManagerQueryClient implements MarsCreditManagerReadOnlyIn }, }) } + getAccountTierAndDiscount = async ({ + accountId, + }: { + accountId: string + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + get_account_tier_and_discount: { + account_id: accountId, + }, + }) + } + tradingFee = async ({ + accountId, + marketType, + }: { + accountId: string + marketType: MarketType + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + trading_fee: { + account_id: accountId, + market_type: marketType, + }, + }) + } swapFeeRate = async (): Promise => { return this.client.queryContractSmart(this.contractAddress, { swap_fee_rate: {}, 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 ffda35e6..84234495 100644 --- a/scripts/types/generated/mars-credit-manager/MarsCreditManager.react-query.ts +++ b/scripts/types/generated/mars-credit-manager/MarsCreditManager.react-query.ts @@ -54,6 +54,8 @@ import { OsmoRoute, OsmoSwap, ConfigUpdates, + FeeTierConfig, + FeeTier, NftConfigUpdates, VaultBaseForAddr, HealthValuesResponse, @@ -63,6 +65,7 @@ import { VaultAmount, VaultAmount1, UnlockingPositions, + MarketType, VaultPosition, LockingVaultAmount, VaultUnlockingPosition, @@ -86,10 +89,12 @@ import { OwnerResponse, RewardsCollector, ArrayOfCoin, + AccountTierAndDiscountResponse, Positions, DebtAmount, PerpPosition, PnlAmounts, + TradingFeeResponse, ArrayOfVaultBinding, VaultBinding, VaultPositionValue, @@ -248,6 +253,25 @@ export const marsCreditManagerQueryKeys = { args, }, ] as const, + getAccountTierAndDiscount: ( + contractAddress: string | undefined, + args?: Record, + ) => + [ + { + ...marsCreditManagerQueryKeys.address(contractAddress)[0], + method: 'get_account_tier_and_discount', + args, + }, + ] as const, + tradingFee: (contractAddress: string | undefined, args?: Record) => + [ + { + ...marsCreditManagerQueryKeys.address(contractAddress)[0], + method: 'trading_fee', + args, + }, + ] as const, swapFeeRate: (contractAddress: string | undefined, args?: Record) => [ { @@ -281,6 +305,56 @@ export function useMarsCreditManagerSwapFeeRateQuery({ }, ) } +export interface MarsCreditManagerTradingFeeQuery + extends MarsCreditManagerReactQuery { + args: { + accountId: string + marketType: MarketType + } +} +export function useMarsCreditManagerTradingFeeQuery({ + client, + args, + options, +}: MarsCreditManagerTradingFeeQuery) { + return useQuery( + marsCreditManagerQueryKeys.tradingFee(client?.contractAddress, args), + () => + client + ? client.tradingFee({ + accountId: args.accountId, + marketType: args.marketType, + }) + : Promise.reject(new Error('Invalid client')), + { + ...options, + enabled: !!client && (options?.enabled != undefined ? options.enabled : true), + }, + ) +} +export interface MarsCreditManagerGetAccountTierAndDiscountQuery + extends MarsCreditManagerReactQuery { + args: { + accountId: string + } +} +export function useMarsCreditManagerGetAccountTierAndDiscountQuery< + TData = AccountTierAndDiscountResponse, +>({ client, args, options }: MarsCreditManagerGetAccountTierAndDiscountQuery) { + return useQuery( + marsCreditManagerQueryKeys.getAccountTierAndDiscount(client?.contractAddress, args), + () => + client + ? client.getAccountTierAndDiscount({ + accountId: args.accountId, + }) + : 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 bb3ee74b..71506ddc 100644 --- a/scripts/types/generated/mars-credit-manager/MarsCreditManager.types.ts +++ b/scripts/types/generated/mars-credit-manager/MarsCreditManager.types.ts @@ -619,7 +619,9 @@ export interface OsmoSwap { } export interface ConfigUpdates { account_nft?: AccountNftBaseForString | null + dao_staking_address?: string | null duality_swapper?: SwapperBaseForString | null + fee_tier_config?: FeeTierConfig | null health_contract?: HealthContractBaseForString | null incentives?: IncentivesUnchecked | null keeper_fee_config?: KeeperFeeConfig | null @@ -636,6 +638,14 @@ export interface ConfigUpdates { swapper?: SwapperBaseForString | null zapper?: ZapperBaseForString | null } +export interface FeeTierConfig { + tiers: FeeTier[] +} +export interface FeeTier { + discount_pct: Decimal + id: string + min_voting_power: string +} export interface NftConfigUpdates { address_provider_contract_addr?: string | null max_value_for_burn?: Uint128 | null @@ -751,6 +761,17 @@ export type QueryMsg = start_after?: string | null } } + | { + get_account_tier_and_discount: { + account_id: string + } + } + | { + trading_fee: { + account_id: string + market_type: MarketType + } + } | { swap_fee_rate: {} } @@ -765,6 +786,13 @@ export type VaultPositionAmount = export type VaultAmount = string export type VaultAmount1 = string export type UnlockingPositions = VaultUnlockingPosition[] +export type MarketType = + | 'spot' + | { + perp: { + denom: string + } + } export interface VaultPosition { amount: VaultPositionAmount vault: VaultBaseForAddr @@ -858,6 +886,11 @@ export interface RewardsCollector { address: string } export type ArrayOfCoin = Coin[] +export interface AccountTierAndDiscountResponse { + discount_pct: Decimal + tier_id: string + voting_power: Uint128 +} export interface Positions { account_id: string account_kind: AccountKind @@ -891,6 +924,12 @@ export interface PnlAmounts { pnl: Int128 price_pnl: Int128 } +export interface TradingFeeResponse { + base_fee_pct: Decimal + discount_pct: Decimal + effective_fee_pct: Decimal + tier_id: string +} export type ArrayOfVaultBinding = VaultBinding[] export interface VaultBinding { account_id: string diff --git a/scripts/types/generated/mars-perps/MarsPerps.client.ts b/scripts/types/generated/mars-perps/MarsPerps.client.ts index 3464fad6..df90094f 100644 --- a/scripts/types/generated/mars-perps/MarsPerps.client.ts +++ b/scripts/types/generated/mars-perps/MarsPerps.client.ts @@ -101,7 +101,15 @@ export interface MarsPerpsReadOnlyInterface { }) => Promise marketAccounting: ({ denom }: { denom: string }) => Promise totalAccounting: () => Promise - openingFee: ({ denom, size, discountPct }: { denom: string; size: Int128; discountPct?: Decimal }) => Promise + openingFee: ({ + denom, + discountPct, + size, + }: { + denom: string + discountPct?: Decimal + size: Int128 + }) => Promise positionFees: ({ accountId, denom, @@ -267,12 +275,20 @@ export class MarsPerpsQueryClient implements MarsPerpsReadOnlyInterface { total_accounting: {}, }) } - openingFee = async ({ denom, size, discountPct }: { denom: string; size: Int128; discountPct?: Decimal }) => { + openingFee = async ({ + denom, + discountPct, + size, + }: { + denom: string + discountPct?: Decimal + size: Int128 + }): Promise => { return this.client.queryContractSmart(this.contractAddress, { opening_fee: { denom, - size, discount_pct: discountPct, + size, }, }) } @@ -284,7 +300,7 @@ export class MarsPerpsQueryClient implements MarsPerpsReadOnlyInterface { accountId: string denom: string newSize: Int128 - }) => Promise => { + }): Promise => { return this.client.queryContractSmart(this.contractAddress, { position_fees: { account_id: accountId, @@ -343,11 +359,13 @@ export interface MarsPerpsInterface extends MarsPerpsReadOnlyInterface { { accountId, denom, + discountPct, reduceOnly, size, }: { accountId: string denom: string + discountPct?: Decimal reduceOnly?: boolean size: Int128 }, @@ -359,9 +377,11 @@ export interface MarsPerpsInterface extends MarsPerpsReadOnlyInterface { { accountId, action, + discountPct, }: { accountId: string action?: ActionKind + discountPct?: Decimal }, fee?: number | StdFee | 'auto', memo?: string, @@ -518,11 +538,13 @@ export class MarsPerpsClient extends MarsPerpsQueryClient implements MarsPerpsIn { accountId, denom, + discountPct, reduceOnly, size, }: { accountId: string denom: string + discountPct?: Decimal reduceOnly?: boolean size: Int128 }, @@ -537,6 +559,7 @@ export class MarsPerpsClient extends MarsPerpsQueryClient implements MarsPerpsIn execute_order: { account_id: accountId, denom, + discount_pct: discountPct, reduce_only: reduceOnly, size, }, @@ -550,9 +573,11 @@ export class MarsPerpsClient extends MarsPerpsQueryClient implements MarsPerpsIn { accountId, action, + discountPct, }: { accountId: string action?: ActionKind + discountPct?: Decimal }, fee: number | StdFee | 'auto' = 'auto', memo?: string, @@ -565,6 +590,7 @@ export class MarsPerpsClient extends MarsPerpsQueryClient implements MarsPerpsIn close_all_positions: { account_id: accountId, action, + discount_pct: discountPct, }, }, fee, diff --git a/scripts/types/generated/mars-perps/MarsPerps.react-query.ts b/scripts/types/generated/mars-perps/MarsPerps.react-query.ts index 5cc0fc96..5c0b1086 100644 --- a/scripts/types/generated/mars-perps/MarsPerps.react-query.ts +++ b/scripts/types/generated/mars-perps/MarsPerps.react-query.ts @@ -226,8 +226,8 @@ export function useMarsPerpsPositionFeesQuery({ export interface MarsPerpsOpeningFeeQuery extends MarsPerpsReactQuery { args: { denom: string - size: Int128 discountPct?: Decimal + size: Int128 } } export function useMarsPerpsOpeningFeeQuery({ @@ -241,6 +241,7 @@ export function useMarsPerpsOpeningFeeQuery({ client ? client.openingFee({ denom: args.denom, + discountPct: args.discountPct, size: args.size, }) : Promise.reject(new Error('Invalid client')), @@ -631,6 +632,7 @@ export interface MarsPerpsCloseAllPositionsMutation { msg: { accountId: string action?: ActionKind + discountPct?: Decimal } args?: { fee?: number | StdFee | 'auto' @@ -655,6 +657,7 @@ export interface MarsPerpsExecuteOrderMutation { msg: { accountId: string denom: string + discountPct?: Decimal reduceOnly?: boolean size: Int128 } diff --git a/scripts/types/generated/mars-perps/MarsPerps.types.ts b/scripts/types/generated/mars-perps/MarsPerps.types.ts index 511ca899..e60a1e41 100644 --- a/scripts/types/generated/mars-perps/MarsPerps.types.ts +++ b/scripts/types/generated/mars-perps/MarsPerps.types.ts @@ -43,6 +43,7 @@ export type ExecuteMsg = execute_order: { account_id: string denom: string + discount_pct?: Decimal | null reduce_only?: boolean | null size: Int128 } @@ -51,6 +52,7 @@ export type ExecuteMsg = close_all_positions: { account_id: string action?: ActionKind | null + discount_pct?: Decimal | null } } | { @@ -185,6 +187,7 @@ export type QueryMsg = | { opening_fee: { denom: string + discount_pct?: Decimal | null size: Int128 } } From 650170d34152970b626ec98d1d5eca292d107028 Mon Sep 17 00:00:00 2001 From: Subham2804 Date: Fri, 5 Sep 2025 13:24:04 +0530 Subject: [PATCH 05/16] update lints check and tier config --- contracts/credit-manager/src/instantiate.rs | 8 +- contracts/credit-manager/src/update_config.rs | 9 +- .../tests/tests/test_perps_with_discount.rs | 148 +++++----- .../tests/tests/test_staking_tiers.rs | 266 ++++++++---------- .../tests/tests/test_swap_with_discount.rs | 20 +- .../tests/tests/test_trading_fee.rs | 69 ++--- .../testing/src/multitest/helpers/mock_env.rs | 74 ++--- .../types/src/credit_manager/instantiate.rs | 14 +- .../mars-credit-manager.json | 49 ++++ scripts/deploy/neutron/devnet-config.ts | 45 +++ scripts/deploy/osmosis/mainnet-config.ts | 4 + scripts/deploy/osmosis/testnet-config.ts | 4 + scripts/health/pkg-web/index_bg.wasm | Bin 459822 -> 462566 bytes scripts/health/pkg-web/package.json | 1 + .../MarsCreditManager.client.ts | 6 +- .../MarsCreditManager.react-query.ts | 6 +- .../MarsCreditManager.types.ts | 20 +- 17 files changed, 405 insertions(+), 338 deletions(-) diff --git a/contracts/credit-manager/src/instantiate.rs b/contracts/credit-manager/src/instantiate.rs index 041a17bf..198fbed4 100644 --- a/contracts/credit-manager/src/instantiate.rs +++ b/contracts/credit-manager/src/instantiate.rs @@ -5,9 +5,9 @@ use mars_types::credit_manager::InstantiateMsg; use crate::{ error::ContractResult, state::{ - DUALITY_SWAPPER, HEALTH_CONTRACT, INCENTIVES, KEEPER_FEE_CONFIG, MAX_SLIPPAGE, - MAX_TRIGGER_ORDERS, MAX_UNLOCKING_POSITIONS, ORACLE, OWNER, PARAMS, PERPS_LB_RATIO, - RED_BANK, SWAPPER, SWAP_FEE, ZAPPER, + DAO_STAKING_ADDRESS, DUALITY_SWAPPER, FEE_TIER_CONFIG, HEALTH_CONTRACT, INCENTIVES, + KEEPER_FEE_CONFIG, MAX_SLIPPAGE, MAX_TRIGGER_ORDERS, MAX_UNLOCKING_POSITIONS, ORACLE, + OWNER, PARAMS, PERPS_LB_RATIO, RED_BANK, SWAPPER, SWAP_FEE, ZAPPER, }, utils::{assert_max_slippage, assert_perps_lb_ratio}, }; @@ -40,6 +40,8 @@ pub fn store_config(deps: DepsMut, env: Env, msg: &InstantiateMsg) -> ContractRe INCENTIVES.save(deps.storage, &msg.incentives.check(deps.api, env.contract.address)?)?; KEEPER_FEE_CONFIG.save(deps.storage, &msg.keeper_fee_config)?; SWAP_FEE.save(deps.storage, &msg.swap_fee)?; + FEE_TIER_CONFIG.save(deps.storage, &msg.fee_tier_config)?; + DAO_STAKING_ADDRESS.save(deps.storage, msg.dao_staking_address.check(deps.api)?.address())?; Ok(()) } diff --git a/contracts/credit-manager/src/update_config.rs b/contracts/credit-manager/src/update_config.rs index b4a5194c..c1984962 100644 --- a/contracts/credit-manager/src/update_config.rs +++ b/contracts/credit-manager/src/update_config.rs @@ -150,10 +150,11 @@ pub fn update_config( } if let Some(addr) = updates.dao_staking_address { - let checked = deps.api.addr_validate(&addr)?; - DAO_STAKING_ADDRESS.save(deps.storage, &checked)?; - response = - response.add_attribute("key", "dao_staking_address").add_attribute("value", addr); + let checked = addr.check(deps.api)?; + DAO_STAKING_ADDRESS.save(deps.storage, checked.address())?; + response = response + .add_attribute("key", "dao_staking_address") + .add_attribute("value", checked.address()); } if let Some(kfc) = updates.keeper_fee_config { diff --git a/contracts/credit-manager/tests/tests/test_perps_with_discount.rs b/contracts/credit-manager/tests/tests/test_perps_with_discount.rs index 17a5928f..964fe2a5 100644 --- a/contracts/credit-manager/tests/tests/test_perps_with_discount.rs +++ b/contracts/credit-manager/tests/tests/test_perps_with_discount.rs @@ -69,73 +69,59 @@ fn open_perp( // Test fee discount across different voting power tiers and scenarios #[test_case( 0, - "tier_10", + "tier_1", Decimal::percent(0), 200; - "tier 10: 0 power -> 0% discount, size 200" + "tier 1: 0 power -> 0% discount, size 200" )] #[test_case( - 25_000, + 250_000_000_000, "tier_5", - Decimal::percent(25), + Decimal::percent(45), 200; - "tier 5: >= 25_000 power -> 25% discount, size 200" + "tier 5: >= 250_000 MARS power -> 45% discount, size 200" )] #[test_case( - 200_000, - "tier_2", - Decimal::percent(60), + 1_000_000_000_000, + "tier_7", + Decimal::percent(70), 200; - "tier 2: >= 200_000 power -> 60% discount, size 200" + "tier 7: >= 1_000_000 MARS power -> 70% discount, size 200" )] #[test_case( - 50_000, + 100_000_000_000, "tier_4", - Decimal::percent(35), + Decimal::percent(30), 100; - "tier 4: >= 50_000 power -> 35% discount, size 100" + "tier 4: >= 100_000 MARS power -> 30% discount, size 100" )] #[test_case( - 150_000, - "tier_3", - Decimal::percent(45), + 500_000_000_000, + "tier_6", + Decimal::percent(60), 500; - "tier 3: >= 150_000 power -> 45% discount, size 500" + "tier 6: >= 500_000 MARS power -> 60% discount, size 500" )] #[test_case( - 10_000, - "tier_6", - Decimal::percent(15), + 10_000_000_000, + "tier_2", + Decimal::percent(10), 150; - "tier 6: >= 10_000 power -> 15% discount, size 150" + "tier 2: >= 10_000 MARS power -> 10% discount, size 150" )] #[test_case( - 5_000, - "tier_7", - Decimal::percent(10), + 50_000_000_000, + "tier_3", + Decimal::percent(20), 300; - "tier 7: >= 5_000 power -> 10% discount, size 300" + "tier 3: >= 50_000 MARS power -> 20% discount, size 300" )] #[test_case( - 1_000, + 1_500_000_000_000, "tier_8", - Decimal::percent(5), + Decimal::percent(80), 400; - "tier 8: >= 1_000 power -> 5% discount, size 400" -)] -#[test_case( - 100, - "tier_9", - Decimal::percent(1), - 600; - "tier 9: >= 100 power -> 1% discount, size 600" -)] -#[test_case( - 350_000, - "tier_1", - Decimal::percent(75), - 800; - "tier 1: >= 350_000 power -> 75% discount, size 800" + "tier 8: >= 1_500_000 MARS power -> 80% discount, size 400" )] fn test_perps_with_discount_events( voting_power: u128, @@ -183,39 +169,39 @@ fn test_perps_with_discount_events( // Test close_perp_position with discount functionality #[test_case( 0, - "tier_10", + "tier_1", Decimal::percent(0); - "close_perp_position tier 10: 0 power -> 0% discount" + "close_perp_position tier 1: 0 power -> 0% discount" )] #[test_case( - 25_000, + 250_000_000_000, "tier_5", - Decimal::percent(25); - "close_perp_position tier 5: >= 25_000 power -> 25% discount" + Decimal::percent(45); + "close_perp_position tier 5: >= 250_000 MARS power -> 45% discount" )] #[test_case( - 200_000, - "tier_2", - Decimal::percent(60); - "close_perp_position tier 2: >= 200_000 power -> 60% discount" + 1_000_000_000_000, + "tier_7", + Decimal::percent(70); + "close_perp_position tier 7: >= 1_000_000 MARS power -> 70% discount" )] #[test_case( - 50_000, + 100_000_000_000, "tier_4", - Decimal::percent(35); - "close_perp_position tier 4: >= 50_000 power -> 35% discount" + Decimal::percent(30); + "close_perp_position tier 4: >= 100_000 MARS power -> 30% discount" )] #[test_case( - 100_000, - "tier_3", - Decimal::percent(45); - "close_perp_position tier 3: >= 100_000 power -> 45% discount" + 500_000_000_000, + "tier_6", + Decimal::percent(60); + "close_perp_position tier 6: >= 500_000 MARS power -> 60% discount" )] #[test_case( - 350_000, - "tier_1", - Decimal::percent(75); - "close_perp_position tier 1: >= 350_000 power -> 75% discount" + 1_500_000_000_000, + "tier_8", + Decimal::percent(80); + "close_perp_position tier 8: >= 1_500_000 MARS power -> 80% discount" )] fn test_close_perp_position_with_discount( voting_power: u128, @@ -267,39 +253,39 @@ fn test_close_perp_position_with_discount( // Test multiple perp positions with discount functionality #[test_case( 0, - "tier_10", + "tier_1", Decimal::percent(0); - "multiple positions tier 10: 0 power -> 0% discount" + "multiple positions tier 1: 0 power -> 0% discount" )] #[test_case( - 25_000, + 250_000_000_000, "tier_5", - Decimal::percent(25); - "multiple positions tier 5: >= 25_000 power -> 25% discount" + Decimal::percent(45); + "multiple positions tier 5: >= 250_000 MARS power -> 45% discount" )] #[test_case( - 200_000, - "tier_2", - Decimal::percent(60); - "multiple positions tier 2: >= 200_000 power -> 60% discount" + 1_000_000_000_000, + "tier_7", + Decimal::percent(70); + "multiple positions tier 7: >= 1_000_000 MARS power -> 70% discount" )] #[test_case( - 50_000, + 100_000_000_000, "tier_4", - Decimal::percent(35); - "multiple positions tier 4: >= 50_000 power -> 35% discount" + Decimal::percent(30); + "multiple positions tier 4: >= 100_000 MARS power -> 30% discount" )] #[test_case( - 100_000, - "tier_3", - Decimal::percent(45); - "multiple positions tier 3: >= 100_000 power -> 45% discount" + 500_000_000_000, + "tier_6", + Decimal::percent(60); + "multiple positions tier 6: >= 500_000 MARS power -> 60% discount" )] #[test_case( - 350_000, - "tier_1", - Decimal::percent(75); - "multiple positions tier 1: >= 350_000 power -> 75% discount" + 1_500_000_000_000, + "tier_8", + Decimal::percent(80); + "multiple positions tier 8: >= 1_500_000 MARS power -> 80% discount" )] fn test_multiple_perp_positions_with_discount( voting_power: u128, diff --git a/contracts/credit-manager/tests/tests/test_staking_tiers.rs b/contracts/credit-manager/tests/tests/test_staking_tiers.rs index 6c62fffd..88c921ab 100644 --- a/contracts/credit-manager/tests/tests/test_staking_tiers.rs +++ b/contracts/credit-manager/tests/tests/test_staking_tiers.rs @@ -8,52 +8,42 @@ fn create_test_fee_tier_config() -> FeeTierConfig { FeeTierConfig { tiers: vec![ FeeTier { - id: "tier_1".to_string(), - min_voting_power: "350000".to_string(), - discount_pct: Decimal::percent(75), + id: "tier_8".to_string(), + min_voting_power: "1500000000000".to_string(), // 1,500,000 MARS = 1,500,000,000,000 uMARS + discount_pct: Decimal::percent(80), }, FeeTier { - id: "tier_2".to_string(), - min_voting_power: "200000".to_string(), + id: "tier_7".to_string(), + min_voting_power: "1000000000000".to_string(), // 1,000,000 MARS = 1,000,000,000,000 uMARS + discount_pct: Decimal::percent(70), + }, + FeeTier { + id: "tier_6".to_string(), + min_voting_power: "500000000000".to_string(), // 500,000 MARS = 500,000,000,000 uMARS discount_pct: Decimal::percent(60), }, FeeTier { - id: "tier_3".to_string(), - min_voting_power: "100000".to_string(), + id: "tier_5".to_string(), + min_voting_power: "250000000000".to_string(), // 250,000 MARS = 250,000,000,000 uMARS discount_pct: Decimal::percent(45), }, FeeTier { id: "tier_4".to_string(), - min_voting_power: "50000".to_string(), - discount_pct: Decimal::percent(35), - }, - FeeTier { - id: "tier_5".to_string(), - min_voting_power: "25000".to_string(), - discount_pct: Decimal::percent(25), + min_voting_power: "100000000000".to_string(), // 100,000 MARS = 100,000,000,000 uMARS + discount_pct: Decimal::percent(30), }, FeeTier { - id: "tier_6".to_string(), - min_voting_power: "10000".to_string(), - discount_pct: Decimal::percent(15), + id: "tier_3".to_string(), + min_voting_power: "50000000000".to_string(), // 50,000 MARS = 50,000,000,000 uMARS + discount_pct: Decimal::percent(20), }, FeeTier { - id: "tier_7".to_string(), - min_voting_power: "5000".to_string(), + id: "tier_2".to_string(), + min_voting_power: "10000000000".to_string(), // 10,000 MARS = 10,000,000,000 uMARS discount_pct: Decimal::percent(10), }, FeeTier { - id: "tier_8".to_string(), - min_voting_power: "1000".to_string(), - discount_pct: Decimal::percent(5), - }, - FeeTier { - id: "tier_9".to_string(), - min_voting_power: "100".to_string(), - discount_pct: Decimal::percent(1), - }, - FeeTier { - id: "tier_10".to_string(), + id: "tier_1".to_string(), min_voting_power: "0".to_string(), discount_pct: Decimal::percent(0), }, @@ -66,70 +56,58 @@ fn test_staking_tier_manager_creation() { let config = create_test_fee_tier_config(); let manager = StakingTierManager::new(config); - assert_eq!(manager.config.tiers.len(), 10); - assert_eq!(manager.config.tiers[0].id, "tier_1"); - assert_eq!(manager.config.tiers[9].id, "tier_10"); + assert_eq!(manager.config.tiers.len(), 8); + assert_eq!(manager.config.tiers[0].id, "tier_8"); + assert_eq!(manager.config.tiers[7].id, "tier_1"); } #[test_case( - Uint128::new(350000), - "tier_1", - Decimal::percent(75); - "exact match tier 1" + Uint128::new(1500000000000), + "tier_8", + Decimal::percent(80); + "exact match tier 8" )] #[test_case( - Uint128::new(200000), - "tier_2", + Uint128::new(1000000000000), + "tier_7", + Decimal::percent(70); + "exact match tier 7" +)] +#[test_case( + Uint128::new(500000000000), + "tier_6", Decimal::percent(60); - "exact match tier 2" + "exact match tier 6" )] #[test_case( - Uint128::new(100000), - "tier_3", + Uint128::new(250000000000), + "tier_5", Decimal::percent(45); - "exact match tier 3" + "exact match tier 5" )] #[test_case( - Uint128::new(50000), + Uint128::new(100000000000), "tier_4", - Decimal::percent(35); + Decimal::percent(30); "exact match tier 4" )] #[test_case( - Uint128::new(25000), - "tier_5", - Decimal::percent(25); - "exact match tier 5" -)] -#[test_case( - Uint128::new(10000), - "tier_6", - Decimal::percent(15); - "exact match tier 6" + Uint128::new(50000000000), + "tier_3", + Decimal::percent(20); + "exact match tier 3" )] #[test_case( - Uint128::new(5000), - "tier_7", + Uint128::new(10000000000), + "tier_2", Decimal::percent(10); - "exact match tier 7" -)] -#[test_case( - Uint128::new(1000), - "tier_8", - Decimal::percent(5); - "exact match tier 8" -)] -#[test_case( - Uint128::new(100), - "tier_9", - Decimal::percent(1); - "exact match tier 9" + "exact match tier 2" )] #[test_case( Uint128::new(0), - "tier_10", + "tier_1", Decimal::percent(0); - "exact match tier 10" + "exact match tier 1" )] fn test_find_applicable_tier_exact_matches( voting_power: Uint128, @@ -145,58 +123,46 @@ fn test_find_applicable_tier_exact_matches( } #[test_case( - Uint128::new(300000), - "tier_2", + Uint128::new(1200000000000), + "tier_7", + Decimal::percent(70); + "between tier 6 and tier 7" +)] +#[test_case( + Uint128::new(750000000000), + "tier_6", Decimal::percent(60); - "between tier 1 and tier 2" + "between tier 5 and tier 6" )] #[test_case( - Uint128::new(150000), - "tier_3", + Uint128::new(300000000000), + "tier_5", Decimal::percent(45); - "between tier 2 and tier 3" + "between tier 4 and tier 5" )] #[test_case( - Uint128::new(75000), + Uint128::new(150000000000), "tier_4", - Decimal::percent(35); + Decimal::percent(30); "between tier 3 and tier 4" )] #[test_case( - Uint128::new(30000), - "tier_5", - Decimal::percent(25); - "between tier 4 and tier 5" -)] -#[test_case( - Uint128::new(15000), - "tier_6", - Decimal::percent(15); - "between tier 5 and tier 6" + Uint128::new(75000000000), + "tier_3", + Decimal::percent(20); + "between tier 2 and tier 3" )] #[test_case( - Uint128::new(7500), - "tier_7", + Uint128::new(15000000000), + "tier_2", Decimal::percent(10); - "between tier 6 and tier 7" -)] -#[test_case( - Uint128::new(1500), - "tier_8", - Decimal::percent(5); - "between tier 7 and tier 8" -)] -#[test_case( - Uint128::new(500), - "tier_9", - Decimal::percent(1); - "between tier 8 and tier 9" + "between tier 1 and tier 2" )] #[test_case( - Uint128::new(50), - "tier_10", + Uint128::new(5000000000), + "tier_1", Decimal::percent(0); - "between tier 9 and tier 10" + "between tier 0 and tier 1" )] fn test_find_applicable_tier_between_thresholds( voting_power: Uint128, @@ -212,15 +178,15 @@ fn test_find_applicable_tier_between_thresholds( } #[test_case( - Uint128::new(500000), - "tier_1", - Decimal::percent(75); + Uint128::new(2000000000000), + "tier_8", + Decimal::percent(80); "above highest tier threshold" )] #[test_case( - Uint128::new(1000000), - "tier_1", - Decimal::percent(75); + Uint128::new(3000000000000), + "tier_8", + Decimal::percent(80); "well above highest tier threshold" )] fn test_find_applicable_tier_above_highest( @@ -238,21 +204,21 @@ fn test_find_applicable_tier_above_highest( #[test_case( Uint128::new(1), - "tier_10", + "tier_1", Decimal::percent(0); "edge case: minimal voting power" )] #[test_case( - Uint128::new(99), - "tier_10", + Uint128::new(9999000000), + "tier_1", Decimal::percent(0); - "edge case: just below tier 9" + "edge case: just below tier 2" )] #[test_case( - Uint128::new(101), - "tier_9", - Decimal::percent(1); - "edge case: just above tier 10" + Uint128::new(10001000000), + "tier_2", + Decimal::percent(10); + "edge case: just above tier 1" )] fn test_find_applicable_tier_edge_cases( voting_power: Uint128, @@ -385,59 +351,49 @@ fn test_get_default_tier() { let manager = StakingTierManager::new(config); let default_tier = manager.get_default_tier().unwrap(); - assert_eq!(default_tier.id, "tier_10"); + assert_eq!(default_tier.id, "tier_1"); assert_eq!(default_tier.discount_pct, Decimal::percent(0)); } #[test_case( - Uint128::new(400000), - Decimal::percent(75); - "tier 1: highest discount" + Uint128::new(1500000000000), + Decimal::percent(80); + "tier 8: highest discount" )] #[test_case( - Uint128::new(250000), - Decimal::percent(60); - "tier 2: high discount" + Uint128::new(1000000000000), + Decimal::percent(70); + "tier 7: high discount" )] #[test_case( - Uint128::new(120000), - Decimal::percent(45); - "tier 3: medium-high discount" + Uint128::new(500000000000), + Decimal::percent(60); + "tier 6: medium-high discount" )] #[test_case( - Uint128::new(60000), - Decimal::percent(35); - "tier 4: medium discount" + Uint128::new(250000000000), + Decimal::percent(45); + "tier 5: medium discount" )] #[test_case( - Uint128::new(30000), - Decimal::percent(25); - "tier 5: medium-low discount" + Uint128::new(100000000000), + Decimal::percent(30); + "tier 4: medium-low discount" )] #[test_case( - Uint128::new(12000), - Decimal::percent(15); - "tier 6: low discount" + Uint128::new(50000000000), + Decimal::percent(20); + "tier 3: low discount" )] #[test_case( - Uint128::new(6000), + Uint128::new(10000000000), Decimal::percent(10); - "tier 7: very low discount" -)] -#[test_case( - Uint128::new(1500), - Decimal::percent(5); - "tier 8: minimal discount" + "tier 2: very low discount" )] #[test_case( - Uint128::new(500), - Decimal::percent(1); - "tier 9: tiny discount" -)] -#[test_case( - Uint128::new(50), + Uint128::new(0), Decimal::percent(0); - "tier 10: no discount" + "tier 1: no discount" )] fn test_discount_calculation_examples(voting_power: Uint128, expected_discount: Decimal) { let config = create_test_fee_tier_config(); diff --git a/contracts/credit-manager/tests/tests/test_swap_with_discount.rs b/contracts/credit-manager/tests/tests/test_swap_with_discount.rs index 01b9506e..fdb08169 100644 --- a/contracts/credit-manager/tests/tests/test_swap_with_discount.rs +++ b/contracts/credit-manager/tests/tests/test_swap_with_discount.rs @@ -84,12 +84,12 @@ fn test_swap_with_discount() { (base_fee, effective_fee) }; - // Tier 10 (min power 0) → 0% discount + // Tier 1 (min power 0) → 0% discount mock.set_voting_power(&user, Uint128::new(0)); let res = do_swap(&mut mock, &account_id, &user, 10_000, "uatom", "uosmo"); let attrs = extract(&res); assert_eq!(attrs.get("voting_power").unwrap(), "0"); - assert_eq!(attrs.get("tier_id").unwrap(), "tier_10"); + assert_eq!(attrs.get("tier_id").unwrap(), "tier_1"); assert_eq!(attrs.get("discount_pct").unwrap(), &Decimal::percent(0).to_string()); // Verify fees: no discount means base_fee == effective_fee @@ -97,12 +97,12 @@ fn test_swap_with_discount() { assert_eq!(base_fee, Decimal::percent(1)); // 1% base fee assert_eq!(effective_fee, Decimal::percent(1)); // No discount applied - // Tier 7 (>= 5_000) → 10% discount - mock.set_voting_power(&user, Uint128::new(5_000)); + // Tier 2 (>= 10_000 MARS) → 10% discount + mock.set_voting_power(&user, Uint128::new(10_000_000_000)); let res = do_swap(&mut mock, &account_id, &user, 10_000, "uatom", "uosmo"); let attrs = extract(&res); - assert_eq!(attrs.get("voting_power").unwrap(), "5000"); - assert_eq!(attrs.get("tier_id").unwrap(), "tier_7"); + assert_eq!(attrs.get("voting_power").unwrap(), "10000000000"); + assert_eq!(attrs.get("tier_id").unwrap(), "tier_2"); assert_eq!(attrs.get("discount_pct").unwrap(), &Decimal::percent(10).to_string()); // Verify fees: 10% discount means effective_fee = base_fee * (1 - 0.1) = base_fee * 0.9 @@ -110,12 +110,12 @@ fn test_swap_with_discount() { assert_eq!(base_fee, Decimal::percent(1)); // 1% base fee assert_eq!(effective_fee, Decimal::percent(1) * (Decimal::one() - Decimal::percent(10))); // 0.9% effective fee - // Tier 3 (>= 100_000) → 45% discount - mock.set_voting_power(&user, Uint128::new(100_000)); + // Tier 5 (>= 250_000 MARS) → 45% discount + mock.set_voting_power(&user, Uint128::new(250_000_000_000)); let res = do_swap(&mut mock, &account_id, &user, 10_000, "uatom", "uosmo"); let attrs = extract(&res); - assert_eq!(attrs.get("voting_power").unwrap(), "100000"); - assert_eq!(attrs.get("tier_id").unwrap(), "tier_3"); + assert_eq!(attrs.get("voting_power").unwrap(), "250000000000"); + assert_eq!(attrs.get("tier_id").unwrap(), "tier_5"); assert_eq!(attrs.get("discount_pct").unwrap(), &Decimal::percent(45).to_string()); // Verify fees: 45% discount means effective_fee = base_fee * (1 - 0.45) = base_fee * 0.55 diff --git a/contracts/credit-manager/tests/tests/test_trading_fee.rs b/contracts/credit-manager/tests/tests/test_trading_fee.rs index 6028ff0a..af14edd8 100644 --- a/contracts/credit-manager/tests/tests/test_trading_fee.rs +++ b/contracts/credit-manager/tests/tests/test_trading_fee.rs @@ -8,25 +8,25 @@ use test_case::test_case; use super::helpers::{default_perp_params, uosmo_info, MockEnv}; #[test_case( - Uint128::new(200_000), - "tier_2", - Decimal::percent(60), - Decimal::percent(25); - "spot market tier 2: 60% discount on 0.25% base fee" + Uint128::new(100_000_000_000), + "tier_4", + Decimal::percent(30), + Decimal::percent(1); + "spot market tier 4: 30% discount on 1% base fee" )] #[test_case( - Uint128::new(100_000), + Uint128::new(50_000_000_000), "tier_3", - Decimal::percent(45), - Decimal::percent(25); - "spot market tier 3: 45% discount on 0.25% base fee" + Decimal::percent(20), + Decimal::percent(1); + "spot market tier 3: 20% discount on 1% base fee" )] #[test_case( - Uint128::new(50_000), - "tier_4", - Decimal::percent(35), - Decimal::percent(25); - "spot market tier 4: 35% discount on 0.25% base fee" + Uint128::new(10_000_000_000), + "tier_2", + Decimal::percent(10), + Decimal::percent(1); + "spot market tier 2: 10% discount on 1% base fee" )] fn test_trading_fee_query_spot( voting_power: Uint128, @@ -34,7 +34,8 @@ fn test_trading_fee_query_spot( expected_discount: Decimal, expected_base_fee: Decimal, ) { - let mut mock = MockEnv::new().set_params(&[uosmo_info()]).build().unwrap(); + let mut mock = + MockEnv::new().set_params(&[uosmo_info()]).swap_fee(Decimal::percent(1)).build().unwrap(); // Create a credit account let user = Addr::unchecked("user"); @@ -68,25 +69,25 @@ fn test_trading_fee_query_spot( } #[test_case( - Uint128::new(25_000), + Uint128::new(250_000_000_000), "tier_5", - Decimal::percent(25), + Decimal::percent(45), "uosmo"; - "perp market tier 5: 25% discount on uosmo" + "perp market tier 5: 45% discount on uosmo" )] #[test_case( - Uint128::new(10_000), + Uint128::new(500_000_000_000), "tier_6", - Decimal::percent(15), + Decimal::percent(60), "uosmo"; - "perp market tier 6: 15% discount on uosmo" + "perp market tier 6: 60% discount on uosmo" )] #[test_case( - Uint128::new(5_000), + Uint128::new(1_000_000_000_000), "tier_7", - Decimal::percent(10), + Decimal::percent(70), "uosmo"; - "perp market tier 7: 10% discount on uosmo" + "perp market tier 7: 70% discount on uosmo" )] fn test_trading_fee_query_perp( voting_power: Uint128, @@ -94,7 +95,8 @@ fn test_trading_fee_query_perp( expected_discount: Decimal, denom: &str, ) { - let mut mock = MockEnv::new().set_params(&[uosmo_info()]).build().unwrap(); + let mut mock = + MockEnv::new().set_params(&[uosmo_info()]).swap_fee(Decimal::percent(1)).build().unwrap(); // Create a credit account let user = Addr::unchecked("user"); @@ -135,14 +137,15 @@ fn test_trading_fee_query_perp( #[test] fn test_trading_fee_query_edge_cases() { - let mut mock = MockEnv::new().set_params(&[uosmo_info()]).build().unwrap(); + let mut mock = + MockEnv::new().set_params(&[uosmo_info()]).swap_fee(Decimal::percent(1)).build().unwrap(); // Create a credit account let user = Addr::unchecked("user"); let account_id = mock.create_credit_account(&user).unwrap(); - // Test tier 1 (highest discount - 75%) - mock.set_voting_power(&user, Uint128::new(350_000)); + // Test tier 8 (highest discount - 80%) + mock.set_voting_power(&user, Uint128::new(1_500_000_000_000)); let response: TradingFeeResponse = mock .app @@ -156,15 +159,15 @@ fn test_trading_fee_query_edge_cases() { ) .unwrap(); - assert_eq!(response.tier_id, "tier_1"); - assert_eq!(response.discount_pct, Decimal::percent(75)); + assert_eq!(response.tier_id, "tier_8"); + assert_eq!(response.discount_pct, Decimal::percent(80)); // Calculate the expected effective fee based on the actual response let calculated_effective = - response.base_fee_pct.checked_mul(Decimal::one() - Decimal::percent(75)).unwrap(); + response.base_fee_pct.checked_mul(Decimal::one() - Decimal::percent(80)).unwrap(); assert_eq!(response.effective_fee_pct, calculated_effective); - // Test tier 10 (no discount - 0%) + // Test tier 1 (no discount - 0%) mock.set_voting_power(&user, Uint128::new(0)); let response: TradingFeeResponse = mock @@ -179,7 +182,7 @@ fn test_trading_fee_query_edge_cases() { ) .unwrap(); - assert_eq!(response.tier_id, "tier_10"); + assert_eq!(response.tier_id, "tier_1"); assert_eq!(response.discount_pct, Decimal::percent(0)); // Calculate the expected effective fee based on the actual response diff --git a/packages/testing/src/multitest/helpers/mock_env.rs b/packages/testing/src/multitest/helpers/mock_env.rs index 51804e82..fddcb9ec 100644 --- a/packages/testing/src/multitest/helpers/mock_env.rs +++ b/packages/testing/src/multitest/helpers/mock_env.rs @@ -28,6 +28,7 @@ use mars_types::{ }, adapters::{ account_nft::AccountNftUnchecked, + dao_staking::DaoStakingUnchecked, health::HealthContract, incentives::{Incentives, IncentivesUnchecked}, oracle::{Oracle, OracleBase, OracleUnchecked}, @@ -1289,6 +1290,10 @@ impl MockEnvBuilder { self } + pub fn set_swap_fee(mut self, fee: Decimal) -> Self { + self.swap_fee = Some(fee); + self + } //-------------------------------------------------------------------------------------------------- // Execute Msgs //-------------------------------------------------------------------------------------------------- @@ -1404,6 +1409,19 @@ impl MockEnvBuilder { let params = self.get_params_contract().into(); let keeper_fee_config = self.get_keeper_fee_config(); let swap_fee = self.get_swap_fee(); + let fee_tier_config = self.fee_tier_config.clone().unwrap_or(FeeTierConfig { + tiers: vec![FeeTier { + id: "tier_1".to_string(), + min_voting_power: "0".to_string(), + discount_pct: Decimal::percent(0), + }], + }); + let dao_staking_address = DaoStakingUnchecked::new( + self.dao_staking_addr + .clone() + .unwrap_or_else(|| Addr::unchecked("mock-dao-staking")) + .to_string(), + ); self.deploy_rewards_collector(); self.deploy_astroport_incentives(); @@ -1429,6 +1447,8 @@ impl MockEnvBuilder { keeper_fee_config, perps_liquidation_bonus_ratio, swap_fee, + fee_tier_config, + dao_staking_address, }, &[], "mock-rover-contract", @@ -1489,7 +1509,7 @@ impl MockEnvBuilder { self.update_config( rover, ConfigUpdates { - dao_staking_address: Some(dao_addr.to_string()), + dao_staking_address: Some(DaoStakingUnchecked::new(dao_addr.to_string())), ..Default::default() }, ); @@ -1498,56 +1518,46 @@ impl MockEnvBuilder { } fn set_fee_tiers(&mut self, rover: &Addr) { - // Default full 10-tier config if none provided (descending thresholds) + // Default 8-tier config if none provided (descending thresholds) let fee_cfg = self.fee_tier_config.clone().unwrap_or(FeeTierConfig { tiers: vec![ FeeTier { - id: "tier_1".to_string(), - min_voting_power: "350000".to_string(), - discount_pct: Decimal::percent(75), + id: "tier_8".to_string(), + min_voting_power: "1500000000000".to_string(), // 1,500,000 MARS = 1,500,000,000,000 uMARS + discount_pct: Decimal::percent(80), }, FeeTier { - id: "tier_2".to_string(), - min_voting_power: "200000".to_string(), + id: "tier_7".to_string(), + min_voting_power: "1000000000000".to_string(), // 1,000,000 MARS = 1,000,000,000,000 uMARS + discount_pct: Decimal::percent(70), + }, + FeeTier { + id: "tier_6".to_string(), + min_voting_power: "500000000000".to_string(), // 500,000 MARS discount_pct: Decimal::percent(60), }, FeeTier { - id: "tier_3".to_string(), - min_voting_power: "100000".to_string(), + id: "tier_5".to_string(), + min_voting_power: "250000000000".to_string(), // 250,000 MARS discount_pct: Decimal::percent(45), }, FeeTier { id: "tier_4".to_string(), - min_voting_power: "50000".to_string(), - discount_pct: Decimal::percent(35), + min_voting_power: "100000000000".to_string(), // 100,000 MARS + discount_pct: Decimal::percent(30), }, FeeTier { - id: "tier_5".to_string(), - min_voting_power: "25000".to_string(), - discount_pct: Decimal::percent(25), - }, - FeeTier { - id: "tier_6".to_string(), - min_voting_power: "10000".to_string(), - discount_pct: Decimal::percent(15), + id: "tier_3".to_string(), + min_voting_power: "50000000000".to_string(), // 50,000 MARS + discount_pct: Decimal::percent(20), }, FeeTier { - id: "tier_7".to_string(), - min_voting_power: "5000".to_string(), + id: "tier_2".to_string(), + min_voting_power: "10000000000".to_string(), // 10,000 MARS discount_pct: Decimal::percent(10), }, FeeTier { - id: "tier_8".to_string(), - min_voting_power: "1000".to_string(), - discount_pct: Decimal::percent(5), - }, - FeeTier { - id: "tier_9".to_string(), - min_voting_power: "100".to_string(), - discount_pct: Decimal::percent(1), - }, - FeeTier { - id: "tier_10".to_string(), + id: "tier_1".to_string(), min_voting_power: "0".to_string(), discount_pct: Decimal::percent(0), }, diff --git a/packages/types/src/credit_manager/instantiate.rs b/packages/types/src/credit_manager/instantiate.rs index 38686ec6..8dea31f0 100644 --- a/packages/types/src/credit_manager/instantiate.rs +++ b/packages/types/src/credit_manager/instantiate.rs @@ -4,10 +4,10 @@ use cosmwasm_std::{Decimal, Uint128}; use super::KeeperFeeConfig; use crate::{ adapters::{ - account_nft::AccountNftUnchecked, health::HealthContractUnchecked, - incentives::IncentivesUnchecked, oracle::OracleUnchecked, params::ParamsUnchecked, - perps::PerpsUnchecked, red_bank::RedBankUnchecked, swapper::SwapperUnchecked, - zapper::ZapperUnchecked, + account_nft::AccountNftUnchecked, dao_staking::DaoStakingUnchecked, + health::HealthContractUnchecked, incentives::IncentivesUnchecked, oracle::OracleUnchecked, + params::ParamsUnchecked, perps::PerpsUnchecked, red_bank::RedBankUnchecked, + swapper::SwapperUnchecked, zapper::ZapperUnchecked, }, fee_tiers::FeeTierConfig, }; @@ -55,6 +55,10 @@ pub struct InstantiateMsg { /// 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, + /// Configuration for fee tiers based on staking + pub fee_tier_config: FeeTierConfig, + /// Address of the DAO staking contract + pub dao_staking_address: DaoStakingUnchecked, } /// Used when you want to update fields on Instantiate config @@ -81,5 +85,5 @@ pub struct ConfigUpdates { pub swap_fee: Option, // Staking-based fee tiers pub fee_tier_config: Option, - pub dao_staking_address: Option, + pub dao_staking_address: Option, } diff --git a/schemas/mars-credit-manager/mars-credit-manager.json b/schemas/mars-credit-manager/mars-credit-manager.json index 7798bc8c..3bf57270 100644 --- a/schemas/mars-credit-manager/mars-credit-manager.json +++ b/schemas/mars-credit-manager/mars-credit-manager.json @@ -7,7 +7,9 @@ "title": "InstantiateMsg", "type": "object", "required": [ + "dao_staking_address", "duality_swapper", + "fee_tier_config", "health_contract", "incentives", "keeper_fee_config", @@ -24,6 +26,10 @@ "zapper" ], "properties": { + "dao_staking_address": { + "description": "Address of the DAO staking contract", + "type": "string" + }, "duality_swapper": { "description": "Helper contract for making swaps", "allOf": [ @@ -32,6 +38,14 @@ } ] }, + "fee_tier_config": { + "description": "Configuration for fee tiers based on staking", + "allOf": [ + { + "$ref": "#/definitions/FeeTierConfig" + } + ] + }, "health_contract": { "description": "Helper contract for calculating health factor", "allOf": [ @@ -160,6 +174,41 @@ "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" }, + "FeeTier": { + "type": "object", + "required": [ + "discount_pct", + "id", + "min_voting_power" + ], + "properties": { + "discount_pct": { + "$ref": "#/definitions/Decimal" + }, + "id": { + "type": "string" + }, + "min_voting_power": { + "type": "string" + } + }, + "additionalProperties": false + }, + "FeeTierConfig": { + "type": "object", + "required": [ + "tiers" + ], + "properties": { + "tiers": { + "type": "array", + "items": { + "$ref": "#/definitions/FeeTier" + } + } + }, + "additionalProperties": false + }, "HealthContractBase_for_String": { "type": "string" }, diff --git a/scripts/deploy/neutron/devnet-config.ts b/scripts/deploy/neutron/devnet-config.ts index f4144b59..b00287de 100644 --- a/scripts/deploy/neutron/devnet-config.ts +++ b/scripts/deploy/neutron/devnet-config.ts @@ -510,4 +510,49 @@ export const neutronDevnetConfig: DeploymentConfig = { maxPerpParams: 20, perpsLiquidationBonusRatio: '0.6', swapFee: '0.0005', + feeTierConfig: { + tiers: [ + { + id: 'tier_8', + min_voting_power: '1500000000000', // 1,500,000 MARS + discount_pct: '0.80', // 80% discount + }, + { + id: 'tier_7', + min_voting_power: '1000000000000', // 1,000,000 MARS + discount_pct: '0.70', // 70% discount + }, + { + id: 'tier_6', + min_voting_power: '500000000000', // 500,000 MARS + discount_pct: '0.60', // 60% discount + }, + { + id: 'tier_5', + min_voting_power: '250000000000', // 250,000 MARS + discount_pct: '0.45', // 45% discount + }, + { + id: 'tier_4', + min_voting_power: '100000000000', // 100,000 MARS + discount_pct: '0.30', // 30% discount + }, + { + id: 'tier_3', + min_voting_power: '50000000000', // 50,000 MARS + discount_pct: '0.20', // 20% discount + }, + { + id: 'tier_2', + min_voting_power: '10000000000', // 10,000 MARS + discount_pct: '0.10', // 10% discount + }, + { + id: 'tier_1', + min_voting_power: '0', + discount_pct: '0.00', // 0% discount + }, + ], + }, + daoStakingAddress: 'neutron1pxjszcmmdxwtw9kv533u3hcudl6qahsa42chcs24gervf4ge40usaw3pcr', } diff --git a/scripts/deploy/osmosis/mainnet-config.ts b/scripts/deploy/osmosis/mainnet-config.ts index 81288a27..65bd8edb 100644 --- a/scripts/deploy/osmosis/mainnet-config.ts +++ b/scripts/deploy/osmosis/mainnet-config.ts @@ -1117,4 +1117,8 @@ export const osmosisMainnetConfig: DeploymentConfig = { maxPerpParams: 20, perpsLiquidationBonusRatio: '0.6', swapFee: '0.0005', + feeTierConfig: { + tiers: [], + }, + daoStakingAddress: '', } diff --git a/scripts/deploy/osmosis/testnet-config.ts b/scripts/deploy/osmosis/testnet-config.ts index 2994d433..ae917e69 100644 --- a/scripts/deploy/osmosis/testnet-config.ts +++ b/scripts/deploy/osmosis/testnet-config.ts @@ -309,4 +309,8 @@ export const osmosisTestnetConfig: DeploymentConfig = { maxPerpParams: 20, perpsLiquidationBonusRatio: '0.6', swapFee: '0.0005', + feeTierConfig: { + tiers: [], + }, + daoStakingAddress: '', } diff --git a/scripts/health/pkg-web/index_bg.wasm b/scripts/health/pkg-web/index_bg.wasm index a464c99bbd92fbd9136cfe9cf9c7994e41aed2e3..8a894dd400cc55738d7441c61452335de4c1e201 100644 GIT binary patch literal 462566 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

tvD z$L2DaO#UrV+2XobTr@J6s46Z?7{+7w8OA)A(tR($faUil=`-oWP@nXX2bhj$bHlJq zL)`YvjX$q&`nE6(_4{8o6UQJnMdBwcnFY6*Eld-{u~NbWkqiz?EWutu8&SW0fDny8 zss2QtC_{*5_D~4XbsJIWBDy|R6#m|av2a}l(&s>^0pS`5l_A^>A(AMOFfiCc@n}G! zB!a<>>)VP59ydt<(^JR;aS+xw2BfEGc!WhV5>TJAL=zqa1d9*zm6-l)E(jTxK#03G z43?jxQ@m|5QJ(F@*qUgI2}SXHs(Qy=EO1xl^_%|BpMqL zFu-^zVInGpEsSCE89g+lCk-H^yds3C9Bl|u|H9hE7ov566CvQh@IWJi6M;u{ge#;$ zJorI~#{5(W(Hxi|3R7a(LKX)UD;6_`1LHbZlwMqR5zuG_X%dZRahrZ`yEvaXJ(>e7 zfq--$@}n^VV+hvQBrXbpRnjX_{qG?}D;N<2;z$%PKal8SWe8DzJg^}Yk3I3KX$*=goxKeqVS*79zl;X0gV<0C)Q@Iu+$|kfUd^{ktqb} z`v~d&?#LJ^MdNUu&SV^dS_}#Xt}3Ll1`=g8Ee)uLHDZA6ge^v zp)ldZU{d__&;5-d&~lu^L#S~laTEdM3p6Vs zahJvpB9o>=y)eM>Ecxd~M@Kq zUPq4yJ(14k3F#3mY-=(V$he+=r+p%06dITZ^`cpN01h+`J)u?X1P(ir(AC5i4oOy# zOj3n-ar!Dax9&r)f^!=<{vH04dTx*_@P+ta2M6k7(A1J5g>)y7uynL&(JkpLLirSs zY9TF}x#)*hOmt7tiZ06I!{s<&vq(VXwC>GGB7KLvdXSbu7qAkOSlmchm2pizpep+y zBMuZEBHx(4`7Fs~(%?RIpwtXLBVJU8Mc+!oA+0{7B~}ZxW|2q*a1E^zqS|~}Oh?c% z{8&P?#EJ?T*{8x%NROl!Hm|~XNnlp86B0mQg=^O!4y`nIAVlpKmFdxIp*SLqg=mFU zZ=oL{{jlEj;?{~1j_yflKp|{iQMt}wNDv)Ll-QR|k_V(k{V85S#VfifuPaADbdw`m z>4={G<=XE>u7&apdU&+vq}00`H`#>)hEvHh&5(^upU{kF!+J7_xlZiJXXqNlEr_^i*Lyv)shna zIxox%JlOox5?HpjNb>#0*slq2;r}s3ukh>{uB5k6xA~uZ|3%S%@@+^X+6>uy2 zkadOD6r^3jvdIH+hkr;eqeC4IKOxhLCv*XC6%$)6$Z|n(NTWnQaattTgZN;(!UO_i z9efEQk3tm6iemyny8t2m&!{U<2l(yGz&l1b6<EIxYh83-EQqo#c+FcML z%ZiyqEZ!_}$#AU};?Uaud)?xJ8xC==L}|ozPK-|wrnwOnJA(B<;TjXMOieVv>O{AWr!Q5se)vAXd9XS6jjSCtEaKlluo^(jQh|;<6dGUXgLx*^0 z!d36f;6}41rbZ%Loe7tF%0TND3^U*ijVCdOC`E4?w2u3+qruJfSEYe46KC^cMT<|T z!Jb3kGLYVY`⁢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 yeO0Vt! diff --git a/scripts/health/pkg-web/package.json b/scripts/health/pkg-web/package.json index add497a0..7d4dbc22 100644 --- a/scripts/health/pkg-web/package.json +++ b/scripts/health/pkg-web/package.json @@ -7,6 +7,7 @@ "Gabe R. ", "Larry Engineer ", "Spike Spiegel ", + "Subham Singh ", "Brianna M. ", "Ahmad Kaouk", "Harry Scholes" diff --git a/scripts/types/generated/mars-credit-manager/MarsCreditManager.client.ts b/scripts/types/generated/mars-credit-manager/MarsCreditManager.client.ts index 70ddf9a0..2381c83a 100644 --- a/scripts/types/generated/mars-credit-manager/MarsCreditManager.client.ts +++ b/scripts/types/generated/mars-credit-manager/MarsCreditManager.client.ts @@ -9,15 +9,17 @@ import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from '@cosmjs/co import { StdFee } from '@cosmjs/amino' import { SwapperBaseForString, + Decimal, HealthContractBaseForString, IncentivesUnchecked, Uint128, - Decimal, OracleBaseForString, ParamsBaseForString, RedBankUnchecked, ZapperBaseForString, InstantiateMsg, + FeeTierConfig, + FeeTier, KeeperFeeConfig, Coin, ExecuteMsg, @@ -53,8 +55,6 @@ import { OsmoRoute, OsmoSwap, ConfigUpdates, - FeeTierConfig, - FeeTier, NftConfigUpdates, VaultBaseForAddr, HealthValuesResponse, 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 84234495..b11376ab 100644 --- a/scripts/types/generated/mars-credit-manager/MarsCreditManager.react-query.ts +++ b/scripts/types/generated/mars-credit-manager/MarsCreditManager.react-query.ts @@ -10,15 +10,17 @@ import { ExecuteResult } from '@cosmjs/cosmwasm-stargate' import { StdFee } from '@cosmjs/amino' import { SwapperBaseForString, + Decimal, HealthContractBaseForString, IncentivesUnchecked, Uint128, - Decimal, OracleBaseForString, ParamsBaseForString, RedBankUnchecked, ZapperBaseForString, InstantiateMsg, + FeeTierConfig, + FeeTier, KeeperFeeConfig, Coin, ExecuteMsg, @@ -54,8 +56,6 @@ import { OsmoRoute, OsmoSwap, ConfigUpdates, - FeeTierConfig, - FeeTier, NftConfigUpdates, VaultBaseForAddr, HealthValuesResponse, diff --git a/scripts/types/generated/mars-credit-manager/MarsCreditManager.types.ts b/scripts/types/generated/mars-credit-manager/MarsCreditManager.types.ts index 71506ddc..bba03954 100644 --- a/scripts/types/generated/mars-credit-manager/MarsCreditManager.types.ts +++ b/scripts/types/generated/mars-credit-manager/MarsCreditManager.types.ts @@ -6,16 +6,18 @@ */ export type SwapperBaseForString = string +export type Decimal = string export type HealthContractBaseForString = string export type IncentivesUnchecked = string export type Uint128 = string -export type Decimal = string export type OracleBaseForString = string export type ParamsBaseForString = string export type RedBankUnchecked = string export type ZapperBaseForString = string export interface InstantiateMsg { + dao_staking_address: string duality_swapper: SwapperBaseForString + fee_tier_config: FeeTierConfig health_contract: HealthContractBaseForString incentives: IncentivesUnchecked keeper_fee_config: KeeperFeeConfig @@ -31,6 +33,14 @@ export interface InstantiateMsg { swapper: SwapperBaseForString zapper: ZapperBaseForString } +export interface FeeTierConfig { + tiers: FeeTier[] +} +export interface FeeTier { + discount_pct: Decimal + id: string + min_voting_power: string +} export interface KeeperFeeConfig { min_fee: Coin } @@ -638,14 +648,6 @@ export interface ConfigUpdates { swapper?: SwapperBaseForString | null zapper?: ZapperBaseForString | null } -export interface FeeTierConfig { - tiers: FeeTier[] -} -export interface FeeTier { - discount_pct: Decimal - id: string - min_voting_power: string -} export interface NftConfigUpdates { address_provider_contract_addr?: string | null max_value_for_burn?: Uint128 | null From 96531d097c3486b2794cdae2011480d6e145363f Mon Sep 17 00:00:00 2001 From: Subham2804 Date: Fri, 5 Sep 2025 13:40:52 +0530 Subject: [PATCH 06/16] update types --- .../mars-credit-manager/MarsCreditManager.client.ts | 1 + .../mars-credit-manager/MarsCreditManager.react-query.ts | 1 + .../generated/mars-credit-manager/MarsCreditManager.types.ts | 5 +++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/types/generated/mars-credit-manager/MarsCreditManager.client.ts b/scripts/types/generated/mars-credit-manager/MarsCreditManager.client.ts index 2381c83a..0045f31e 100644 --- a/scripts/types/generated/mars-credit-manager/MarsCreditManager.client.ts +++ b/scripts/types/generated/mars-credit-manager/MarsCreditManager.client.ts @@ -8,6 +8,7 @@ import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from '@cosmjs/cosmwasm-stargate' import { StdFee } from '@cosmjs/amino' import { + DaoStakingBaseForString, SwapperBaseForString, Decimal, HealthContractBaseForString, 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 b11376ab..abd21573 100644 --- a/scripts/types/generated/mars-credit-manager/MarsCreditManager.react-query.ts +++ b/scripts/types/generated/mars-credit-manager/MarsCreditManager.react-query.ts @@ -9,6 +9,7 @@ import { UseQueryOptions, useQuery, useMutation, UseMutationOptions } from '@tan import { ExecuteResult } from '@cosmjs/cosmwasm-stargate' import { StdFee } from '@cosmjs/amino' import { + DaoStakingBaseForString, SwapperBaseForString, Decimal, HealthContractBaseForString, diff --git a/scripts/types/generated/mars-credit-manager/MarsCreditManager.types.ts b/scripts/types/generated/mars-credit-manager/MarsCreditManager.types.ts index bba03954..4d37c3ab 100644 --- a/scripts/types/generated/mars-credit-manager/MarsCreditManager.types.ts +++ b/scripts/types/generated/mars-credit-manager/MarsCreditManager.types.ts @@ -5,6 +5,7 @@ * and run the @cosmwasm/ts-codegen generate command to regenerate this file. */ +export type DaoStakingBaseForString = string export type SwapperBaseForString = string export type Decimal = string export type HealthContractBaseForString = string @@ -15,7 +16,7 @@ export type ParamsBaseForString = string export type RedBankUnchecked = string export type ZapperBaseForString = string export interface InstantiateMsg { - dao_staking_address: string + dao_staking_address: DaoStakingBaseForString duality_swapper: SwapperBaseForString fee_tier_config: FeeTierConfig health_contract: HealthContractBaseForString @@ -629,7 +630,7 @@ export interface OsmoSwap { } export interface ConfigUpdates { account_nft?: AccountNftBaseForString | null - dao_staking_address?: string | null + dao_staking_address?: DaoStakingBaseForString | null duality_swapper?: SwapperBaseForString | null fee_tier_config?: FeeTierConfig | null health_contract?: HealthContractBaseForString | null From 9cdeeac70d2f887af54380b5fec577c3f212f38b Mon Sep 17 00:00:00 2001 From: Subham2804 Date: Fri, 5 Sep 2025 13:57:21 +0530 Subject: [PATCH 07/16] update schema --- .../mars-credit-manager.json | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/schemas/mars-credit-manager/mars-credit-manager.json b/schemas/mars-credit-manager/mars-credit-manager.json index 3bf57270..f15b6dfc 100644 --- a/schemas/mars-credit-manager/mars-credit-manager.json +++ b/schemas/mars-credit-manager/mars-credit-manager.json @@ -28,7 +28,11 @@ "properties": { "dao_staking_address": { "description": "Address of the DAO staking contract", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/DaoStakingBase_for_String" + } + ] }, "duality_swapper": { "description": "Helper contract for making swaps", @@ -170,6 +174,9 @@ } } }, + "DaoStakingBase_for_String": { + "type": "string" + }, "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" @@ -2480,9 +2487,13 @@ ] }, "dao_staking_address": { - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/DaoStakingBase_for_String" + }, + { + "type": "null" + } ] }, "duality_swapper": { @@ -2678,6 +2689,9 @@ } ] }, + "DaoStakingBase_for_String": { + "type": "string" + }, "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" From 6e91432d690f4f800708bf4bf93c3fb4073188eb Mon Sep 17 00:00:00 2001 From: Subham2804 Date: Sat, 6 Sep 2025 08:48:12 +0530 Subject: [PATCH 08/16] remove unwanted comments --- contracts/credit-manager/src/query.rs | 25 +++++++++---------- contracts/credit-manager/src/staking.rs | 3 +-- .../tests/tests/test_staking_tiers.rs | 14 +++++------ 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/contracts/credit-manager/src/query.rs b/contracts/credit-manager/src/query.rs index 55baa6f9..c957e5e6 100644 --- a/contracts/credit-manager/src/query.rs +++ b/contracts/credit-manager/src/query.rs @@ -7,9 +7,9 @@ use cw_storage_plus::Bound; use mars_types::{ adapters::vault::{Vault, VaultBase, VaultPosition, VaultPositionValue, VaultUnchecked}, credit_manager::{ - Account, CoinBalanceResponseItem, ConfigResponse, DebtAmount, DebtShares, Positions, - SharesResponseItem, TriggerOrderResponse, VaultBinding, VaultPositionResponseItem, - VaultUtilizationResponse, + Account, AccountTierAndDiscountResponse, CoinBalanceResponseItem, ConfigResponse, + DebtAmount, DebtShares, MarketType, Positions, SharesResponseItem, TradingFeeResponse, + TriggerOrderResponse, VaultBinding, VaultPositionResponseItem, VaultUtilizationResponse, }, health::AccountKind, oracle::ActionKind, @@ -17,6 +17,7 @@ use mars_types::{ use crate::{ error::ContractResult, + staking::get_account_tier_and_discount, state::{ ACCOUNT_KINDS, ACCOUNT_NFT, COIN_BALANCES, DEBT_SHARES, HEALTH_CONTRACT, INCENTIVES, KEEPER_FEE_CONFIG, MAX_SLIPPAGE, MAX_UNLOCKING_POSITIONS, ORACLE, OWNER, PARAMS, PERPS, @@ -352,12 +353,12 @@ pub fn query_vault_bindings( pub fn query_account_tier_and_discount( deps: Deps, account_id: &str, -) -> ContractResult { +) -> ContractResult { use crate::staking::get_account_tier_and_discount; let (tier, discount_pct, voting_power) = get_account_tier_and_discount(deps, account_id)?; - Ok(mars_types::credit_manager::AccountTierAndDiscountResponse { + Ok(AccountTierAndDiscountResponse { tier_id: tier.id, discount_pct, voting_power, @@ -367,26 +368,24 @@ pub fn query_account_tier_and_discount( pub fn query_trading_fee( deps: Deps, account_id: &str, - market_type: &mars_types::credit_manager::MarketType, -) -> ContractResult { - use crate::staking::get_account_tier_and_discount; - + market_type: &MarketType, +) -> ContractResult { let (tier, discount_pct, _) = get_account_tier_and_discount(deps, account_id)?; match market_type { - mars_types::credit_manager::MarketType::Spot => { + MarketType::Spot => { let base_fee_pct = SWAP_FEE.load(deps.storage)?; let effective_fee_pct = base_fee_pct.checked_mul(cosmwasm_std::Decimal::one() - discount_pct)?; - Ok(mars_types::credit_manager::TradingFeeResponse { + Ok(TradingFeeResponse { base_fee_pct, discount_pct, effective_fee_pct, tier_id: tier.id, }) } - mars_types::credit_manager::MarketType::Perp { + MarketType::Perp { denom, } => { let params = PARAMS.load(deps.storage)?; @@ -396,7 +395,7 @@ pub fn query_trading_fee( let effective_fee_pct = base_fee_pct.checked_mul(cosmwasm_std::Decimal::one() - discount_pct)?; - Ok(mars_types::credit_manager::TradingFeeResponse { + Ok(TradingFeeResponse { base_fee_pct, discount_pct, effective_fee_pct, diff --git a/contracts/credit-manager/src/staking.rs b/contracts/credit-manager/src/staking.rs index 55e550e8..108a261a 100644 --- a/contracts/credit-manager/src/staking.rs +++ b/contracts/credit-manager/src/staking.rs @@ -48,7 +48,7 @@ impl StakingTierManager { result = mid; // Look for higher tiers (lower indices) but don't go below 0 if mid == 0 { - break; // We found the highest tier + break; } right = mid - 1; } else { @@ -66,7 +66,6 @@ impl StakingTierManager { return Err(StdError::generic_err("Fee tier config cannot be empty")); } - // Parse first tier once let mut prev_power = Uint128::from_str(&self.config.tiers[0].min_voting_power) .map_err(|_| StdError::generic_err("Invalid min_voting_power in tier"))?; diff --git a/contracts/credit-manager/tests/tests/test_staking_tiers.rs b/contracts/credit-manager/tests/tests/test_staking_tiers.rs index 88c921ab..f13e02dc 100644 --- a/contracts/credit-manager/tests/tests/test_staking_tiers.rs +++ b/contracts/credit-manager/tests/tests/test_staking_tiers.rs @@ -9,37 +9,37 @@ fn create_test_fee_tier_config() -> FeeTierConfig { tiers: vec![ FeeTier { id: "tier_8".to_string(), - min_voting_power: "1500000000000".to_string(), // 1,500,000 MARS = 1,500,000,000,000 uMARS + min_voting_power: "1500000000000".to_string(), // 1,500,000 MARS discount_pct: Decimal::percent(80), }, FeeTier { id: "tier_7".to_string(), - min_voting_power: "1000000000000".to_string(), // 1,000,000 MARS = 1,000,000,000,000 uMARS + min_voting_power: "1000000000000".to_string(), // 1,000,000 MARS discount_pct: Decimal::percent(70), }, FeeTier { id: "tier_6".to_string(), - min_voting_power: "500000000000".to_string(), // 500,000 MARS = 500,000,000,000 uMARS + min_voting_power: "500000000000".to_string(), // 500,000 MARS discount_pct: Decimal::percent(60), }, FeeTier { id: "tier_5".to_string(), - min_voting_power: "250000000000".to_string(), // 250,000 MARS = 250,000,000,000 uMARS + min_voting_power: "250000000000".to_string(), // 250,000 MARS discount_pct: Decimal::percent(45), }, FeeTier { id: "tier_4".to_string(), - min_voting_power: "100000000000".to_string(), // 100,000 MARS = 100,000,000,000 uMARS + min_voting_power: "100000000000".to_string(), // 100,000 MARS discount_pct: Decimal::percent(30), }, FeeTier { id: "tier_3".to_string(), - min_voting_power: "50000000000".to_string(), // 50,000 MARS = 50,000,000,000 uMARS + min_voting_power: "50000000000".to_string(), // 50,000 MARS discount_pct: Decimal::percent(20), }, FeeTier { id: "tier_2".to_string(), - min_voting_power: "10000000000".to_string(), // 10,000 MARS = 10,000,000,000 uMARS + min_voting_power: "10000000000".to_string(), // 10,000 MARS discount_pct: Decimal::percent(10), }, FeeTier { From 4fca71702ef838611cba90eaf66ecb691cca3596 Mon Sep 17 00:00:00 2001 From: Subham2804 Date: Tue, 16 Sep 2025 12:21:30 +0530 Subject: [PATCH 09/16] comment fixes --- Cargo.lock | 4 +- Cargo.toml | 4 +- contracts/credit-manager/src/error.rs | 31 ++ contracts/credit-manager/src/instantiate.rs | 4 +- contracts/credit-manager/src/perp.rs | 16 +- contracts/credit-manager/src/staking.rs | 103 ++-- contracts/credit-manager/src/state.rs | 4 +- contracts/credit-manager/src/swap.rs | 3 +- contracts/credit-manager/src/update_config.rs | 17 +- contracts/credit-manager/src/utils.rs | 23 + .../tests/tests/test_perps_with_discount.rs | 474 +++++++++++++++++- .../tests/tests/test_staking_tiers.rs | 143 ++---- .../tests/tests/test_update_config.rs | 4 +- .../Cargo.toml | 2 +- .../src/lib.rs | 8 +- contracts/perps/src/deleverage.rs | 33 +- contracts/perps/src/position_management.rs | 56 +-- contracts/perps/src/query.rs | 61 +-- contracts/perps/src/state.rs | 6 +- .../perps/tests/tests/helpers/mock_env.rs | 34 +- .../tests/test_accounting_with_discount.rs | 7 +- contracts/perps/tests/tests/test_position.rs | 93 ++++ packages/testing/Cargo.toml | 2 +- .../src/multitest/helpers/contracts.rs | 11 +- .../testing/src/multitest/helpers/mock_env.rs | 66 +-- .../{dao_staking.rs => governance.rs} | 30 +- packages/types/src/adapters/mod.rs | 2 +- packages/types/src/address_provider.rs | 8 +- .../types/src/credit_manager/instantiate.rs | 8 +- packages/types/src/fee_tiers.rs | 6 +- .../mars-address-provider.json | 20 +- .../mars-credit-manager.json | 46 +- scripts/deploy/base/deployer.ts | 2 +- scripts/deploy/neutron/devnet-config.ts | 2 +- scripts/deploy/neutron/mainnet-config.ts | 2 +- scripts/deploy/neutron/testnet-config.ts | 2 +- scripts/deploy/osmosis/mainnet-config.ts | 2 +- scripts/deploy/osmosis/testnet-config.ts | 2 +- scripts/types/config.ts | 2 +- .../MarsAddressProvider.types.ts | 2 +- .../MarsCreditManager.client.ts | 4 +- .../MarsCreditManager.react-query.ts | 4 +- .../MarsCreditManager.types.ts | 10 +- 43 files changed, 928 insertions(+), 435 deletions(-) rename contracts/{mock-dao-staking => mock-governance}/Cargo.toml (88%) rename contracts/{mock-dao-staking => mock-governance}/src/lib.rs (86%) rename packages/types/src/adapters/{dao_staking.rs => governance.rs} (54%) diff --git a/Cargo.lock b/Cargo.lock index 89f54e9d..9ac6912c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2807,7 +2807,7 @@ dependencies = [ ] [[package]] -name = "mars-mock-dao-staking" +name = "mars-mock-governance" version = "0.1.0" dependencies = [ "cosmwasm-schema 1.5.7", @@ -3251,7 +3251,7 @@ dependencies = [ "mars-credit-manager", "mars-incentives", "mars-mock-astroport-incentives", - "mars-mock-dao-staking", + "mars-mock-governance", "mars-mock-incentives", "mars-mock-oracle", "mars-mock-pyth", diff --git a/Cargo.toml b/Cargo.toml index dc16034c..777b7bcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ members = [ "contracts/mock-pyth", "contracts/mock-red-bank", "contracts/mock-vault", - "contracts/mock-dao-staking", + "contracts/mock-governance", # packages "packages/chains/*", @@ -152,7 +152,7 @@ mars-mock-oracle = { path = "./contracts/mock-oracle" } mars-mock-red-bank = { path = "./contracts/mock-red-bank" } mars-mock-vault = { path = "./contracts/mock-vault" } mars-mock-rover-health = { path = "./contracts/mock-health" } -mars-mock-dao-staking = { path = "./contracts/mock-dao-staking" } +mars-mock-governance = { path = "./contracts/mock-governance" } mars-swapper-mock = { path = "./contracts/swapper/mock" } mars-zapper-mock = { path = "./contracts/v2-zapper/mock" } diff --git a/contracts/credit-manager/src/error.rs b/contracts/credit-manager/src/error.rs index 6ce499cb..a3555f04 100644 --- a/contracts/credit-manager/src/error.rs +++ b/contracts/credit-manager/src/error.rs @@ -118,6 +118,37 @@ pub enum ContractError { reason: String, }, + #[error("Fee tier config cannot be empty")] + EmptyFeeTierConfig, + + #[error("No tiers present")] + NoTiersPresent, + + #[error("Invalid min_voting_power in tier: {voting_power:?}")] + InvalidVotingPower { + voting_power: String, + }, + + #[error("Tiers must be sorted in descending order")] + TiersNotSortedDescending, + + #[error("Duplicate voting power thresholds")] + DuplicateVotingPowerThresholds, + + #[error("Discount percentage must be less than or equal to 100%")] + InvalidDiscountPercentage, + + #[error("Failed to load governance address from storage")] + FailedToLoadGovernanceAddress, + + #[error("Failed to load fee tier config from storage")] + FailedToLoadFeeTierConfig, + + #[error("Failed to query voting power: {error:?}")] + FailedToQueryVotingPower { + error: String, + }, + #[error("Paying down {debt_coin:?} for {request_coin:?} does not result in a profit for the liquidator")] LiquidationNotProfitable { debt_coin: Coin, diff --git a/contracts/credit-manager/src/instantiate.rs b/contracts/credit-manager/src/instantiate.rs index 198fbed4..ea8f3cad 100644 --- a/contracts/credit-manager/src/instantiate.rs +++ b/contracts/credit-manager/src/instantiate.rs @@ -5,7 +5,7 @@ use mars_types::credit_manager::InstantiateMsg; use crate::{ error::ContractResult, state::{ - DAO_STAKING_ADDRESS, DUALITY_SWAPPER, FEE_TIER_CONFIG, HEALTH_CONTRACT, INCENTIVES, + DUALITY_SWAPPER, FEE_TIER_CONFIG, GOVERNANCE, HEALTH_CONTRACT, INCENTIVES, KEEPER_FEE_CONFIG, MAX_SLIPPAGE, MAX_TRIGGER_ORDERS, MAX_UNLOCKING_POSITIONS, ORACLE, OWNER, PARAMS, PERPS_LB_RATIO, RED_BANK, SWAPPER, SWAP_FEE, ZAPPER, }, @@ -41,7 +41,7 @@ pub fn store_config(deps: DepsMut, env: Env, msg: &InstantiateMsg) -> ContractRe KEEPER_FEE_CONFIG.save(deps.storage, &msg.keeper_fee_config)?; SWAP_FEE.save(deps.storage, &msg.swap_fee)?; FEE_TIER_CONFIG.save(deps.storage, &msg.fee_tier_config)?; - DAO_STAKING_ADDRESS.save(deps.storage, msg.dao_staking_address.check(deps.api)?.address())?; + GOVERNANCE.save(deps.storage, msg.governance_address.check(deps.api)?.address())?; Ok(()) } diff --git a/contracts/credit-manager/src/perp.rs b/contracts/credit-manager/src/perp.rs index 987605e1..1342474d 100644 --- a/contracts/credit-manager/src/perp.rs +++ b/contracts/credit-manager/src/perp.rs @@ -121,11 +121,10 @@ pub fn execute_perp_order( order_size, reduce_only, discount_pct, + &tier.id, )?, None => { // Open new position - let base_opening_fee = - perps.query_opening_fee(&deps.querier, denom, order_size, None)?; let opening_fee = perps.query_opening_fee(&deps.querier, denom, order_size, Some(discount_pct))?; let fee = opening_fee.fee; @@ -154,7 +153,6 @@ pub fn execute_perp_order( .add_attribute("reduce_only", reduce_only.unwrap_or(false).to_string()) .add_attribute("new_size", order_size.to_string()) .add_attribute("opening_fee", fee.to_string()) - .add_attribute("base_opening_fee", base_opening_fee.fee.to_string()) .add_attribute("voting_power", voting_power.to_string()) .add_attribute("tier_id", tier.id) .add_attribute("discount_pct", discount_pct.to_string()) @@ -170,7 +168,7 @@ pub fn close_perp_position( let perps = PERPS.load(deps.storage)?; // Get staking tier discount for this account - let (_, discount_pct, _) = get_account_tier_and_discount(deps.as_ref(), account_id)?; + let (tier, discount_pct, _) = get_account_tier_and_discount(deps.as_ref(), account_id)?; // Query the perp position PnL so that we know whether funds needs to be // sent to the perps contract @@ -198,6 +196,7 @@ pub fn close_perp_position( order_size, Some(true), discount_pct, + &tier.id, )?) } None => Err(ContractError::NoPerpPosition { @@ -266,17 +265,16 @@ fn modify_existing_position( order_size: Int128, reduce_only: Option, discount_pct: Decimal, + tier: &str, ) -> ContractResult { let pnl = position.unrealized_pnl.to_coins(&position.base_denom).pnl; let pnl_string = position.unrealized_pnl.pnl.to_string(); let (funds, response) = update_state_based_on_pnl(&mut deps, account_id, pnl, None, response)?; let funds = funds.map_or_else(Vec::new, |c| vec![c]); - // Get base and effective fees for logging - let base_opening_fee = perps.query_opening_fee(&deps.querier, denom, order_size, None)?; + // Get effective fee let effective_opening_fee = perps.query_opening_fee(&deps.querier, denom, order_size, Some(discount_pct))?; - let msg = perps.execute_perp_order( account_id, denom, @@ -303,9 +301,9 @@ fn modify_existing_position( .add_attribute("reduce_only", reduce_only.unwrap_or(false).to_string()) .add_attribute("order_size", order_size.to_string()) .add_attribute("new_size", new_size.to_string()) - .add_attribute("base_opening_fee", base_opening_fee.fee.to_string()) .add_attribute("effective_opening_fee", effective_opening_fee.fee.to_string()) - .add_attribute("discount_pct", discount_pct.to_string())) + .add_attribute("discount_pct", discount_pct.to_string()) + .add_attribute("tier_id", tier)) } /// Prepare the necessary messages and funds to be sent to the perps contract based on the PnL. diff --git a/contracts/credit-manager/src/staking.rs b/contracts/credit-manager/src/staking.rs index 108a261a..6d0a689e 100644 --- a/contracts/credit-manager/src/staking.rs +++ b/contracts/credit-manager/src/staking.rs @@ -1,14 +1,16 @@ -use std::str::FromStr; - -use cosmwasm_std::{Decimal, Deps, StdError, StdResult, Uint128}; +use cosmwasm_std::{Decimal, Deps, Uint128}; use mars_types::{ - adapters::dao_staking::DaoStaking, + adapters::governance::Governance, fee_tiers::{FeeTier, FeeTierConfig}, }; use crate::{ - state::{DAO_STAKING_ADDRESS, FEE_TIER_CONFIG}, - utils::query_nft_token_owner, + error::{ContractError, ContractResult}, + state::{FEE_TIER_CONFIG, GOVERNANCE}, + utils::{ + assert_discount_pct, assert_tiers_not_empty, assert_tiers_sorted_descending, + query_nft_token_owner, + }, }; pub struct StakingTierManager { @@ -24,11 +26,9 @@ impl StakingTierManager { /// Find the applicable tier for a given voting power /// Returns the tier with the highest min_voting_power that the user qualifies for - pub fn find_applicable_tier(&self, voting_power: Uint128) -> StdResult<&FeeTier> { + pub fn find_applicable_tier(&self, voting_power: Uint128) -> ContractResult<&FeeTier> { // Ensure tiers are sorted in descending order of min_voting_power - if self.config.tiers.is_empty() { - return Err(StdError::generic_err("No tiers configured")); - } + assert_tiers_not_empty(&self.config.tiers)?; // Binary search for the applicable tier let mut left = 0; @@ -39,9 +39,7 @@ impl StakingTierManager { let mid = left + (right - left) / 2; let tier = &self.config.tiers[mid]; - // Parse min_voting_power once per tier - let min_power = Uint128::from_str(&tier.min_voting_power) - .map_err(|_| StdError::generic_err("Invalid min_voting_power in tier"))?; + let min_power = tier.min_voting_power; if voting_power >= min_power { // User qualifies for this tier, but there might be a better one @@ -61,56 +59,37 @@ impl StakingTierManager { } /// Validate that tiers are properly ordered by min_voting_power (descending) - pub fn validate(&self) -> StdResult<()> { - if self.config.tiers.is_empty() { - return Err(StdError::generic_err("Fee tier config cannot be empty")); - } - - let mut prev_power = Uint128::from_str(&self.config.tiers[0].min_voting_power) - .map_err(|_| StdError::generic_err("Invalid min_voting_power in tier"))?; - - // Check for descending order and duplicates in one pass - for i in 1..self.config.tiers.len() { - let curr_power = Uint128::from_str(&self.config.tiers[i].min_voting_power) - .map_err(|_| StdError::generic_err("Invalid min_voting_power in tier"))?; + pub fn validate(&self) -> ContractResult<()> { + assert_tiers_not_empty(&self.config.tiers)?; - if curr_power == prev_power { - return Err(StdError::generic_err("Duplicate voting power thresholds")); - } + // Extract all voting powers + let mut voting_powers = Vec::new(); + for tier in &self.config.tiers { + voting_powers.push(tier.min_voting_power); + } - if curr_power >= prev_power { - return Err(StdError::generic_err("Tiers must be sorted in descending order")); + // Check for duplicates + for i in 1..voting_powers.len() { + if voting_powers[i] == voting_powers[i - 1] { + return Err(ContractError::DuplicateVotingPowerThresholds); } - - prev_power = curr_power; } - // Validate discount percentages are reasonable (0-100%) + // Check for descending order + assert_tiers_sorted_descending(&voting_powers)?; + + // Validate discount percentages are reasonable (0-100% inclusive) for tier in &self.config.tiers { - if tier.discount_pct >= Decimal::one() { - return Err(StdError::generic_err("Discount percentage must be less than 100%")); - } + assert_discount_pct(tier.discount_pct)?; } Ok(()) } /// Get the default tier (tier with lowest min_voting_power) - pub fn get_default_tier(&self) -> StdResult<&FeeTier> { - let mut default_tier: Option<&FeeTier> = None; - let mut lowest_power = Uint128::MAX; - - for tier in &self.config.tiers { - let min_power = Uint128::from_str(&tier.min_voting_power) - .map_err(|_| StdError::generic_err("Invalid min_voting_power in tier"))?; - - if min_power < lowest_power { - default_tier = Some(tier); - lowest_power = min_power; - } - } - - default_tier.ok_or_else(|| StdError::generic_err("No tiers configured")) + /// the default tier (lowest voting power requirement) is the last element. + pub fn get_default_tier(&self) -> ContractResult<&FeeTier> { + self.config.tiers.last().ok_or(ContractError::NoTiersPresent) } } @@ -118,21 +97,25 @@ impl StakingTierManager { pub fn get_account_tier_and_discount( deps: Deps, account_id: &str, -) -> StdResult<(FeeTier, Decimal, Uint128)> { +) -> ContractResult<(FeeTier, Decimal, Uint128)> { // Get account owner from account_id - let account_owner = query_nft_token_owner(deps, account_id) - .map_err(|e| StdError::generic_err(e.to_string()))?; + let account_owner = query_nft_token_owner(deps, account_id)?; - // Get DAO staking contract address from state - let dao_staking_addr = DAO_STAKING_ADDRESS.load(deps.storage)?; - let dao_staking = DaoStaking::new(dao_staking_addr); + // Get governance contract address from state + let governance_addr = + GOVERNANCE.load(deps.storage).map_err(|_| ContractError::FailedToLoadGovernanceAddress)?; + let governance = Governance::new(governance_addr); // Query voting power for the account owner - let voting_power_response = - dao_staking.query_voting_power_at_height(&deps.querier, &account_owner)?; + let voting_power_response = governance + .query_voting_power_at_height(&deps.querier, &account_owner) + .map_err(|e| ContractError::FailedToQueryVotingPower { + error: e.to_string(), + })?; // Get fee tier config and find applicable tier - let fee_tier_config = FEE_TIER_CONFIG.load(deps.storage)?; + let fee_tier_config = + FEE_TIER_CONFIG.load(deps.storage).map_err(|_| ContractError::FailedToLoadFeeTierConfig)?; let manager = StakingTierManager::new(fee_tier_config); let tier = manager.find_applicable_tier(voting_power_response.power)?; diff --git a/contracts/credit-manager/src/state.rs b/contracts/credit-manager/src/state.rs index 2e379864..0097efd4 100644 --- a/contracts/credit-manager/src/state.rs +++ b/contracts/credit-manager/src/state.rs @@ -72,5 +72,5 @@ pub const SWAP_FEE: Item = Item::new("swap_fee"); // Fee tier discount configuration pub const FEE_TIER_CONFIG: Item = Item::new("fee_tier_config"); -// DAO staking contract address -pub const DAO_STAKING_ADDRESS: Item = Item::new("dao_staking_address"); +// Governance contract address +pub const GOVERNANCE: Item = Item::new("governance"); diff --git a/contracts/credit-manager/src/swap.rs b/contracts/credit-manager/src/swap.rs index b1e8eef4..843ceb83 100644 --- a/contracts/credit-manager/src/swap.rs +++ b/contracts/credit-manager/src/swap.rs @@ -47,7 +47,8 @@ pub fn swap_exact_in( // Apply discount to swap fee let base_swap_fee = SWAP_FEE.load(deps.storage)?; - let effective_swap_fee = base_swap_fee * (Decimal::one() - discount_pct); + let effective_swap_fee = + base_swap_fee.checked_mul(Decimal::one().checked_sub(discount_pct)?)?; let swap_fee_amount = coin_in_to_trade.amount.checked_mul_floor(effective_swap_fee)?; coin_in_to_trade.amount = coin_in_to_trade.amount.checked_sub(swap_fee_amount)?; diff --git a/contracts/credit-manager/src/update_config.rs b/contracts/credit-manager/src/update_config.rs index c1984962..54bc43be 100644 --- a/contracts/credit-manager/src/update_config.rs +++ b/contracts/credit-manager/src/update_config.rs @@ -13,10 +13,10 @@ use crate::{ execute::create_credit_account, staking::StakingTierManager, state::{ - ACCOUNT_NFT, DAO_STAKING_ADDRESS, DUALITY_SWAPPER, FEE_TIER_CONFIG, HEALTH_CONTRACT, - INCENTIVES, KEEPER_FEE_CONFIG, MAX_SLIPPAGE, MAX_TRIGGER_ORDERS, MAX_UNLOCKING_POSITIONS, - ORACLE, OWNER, PARAMS, PERPS, PERPS_LB_RATIO, RED_BANK, REWARDS_COLLECTOR, SWAPPER, - SWAP_FEE, ZAPPER, + ACCOUNT_NFT, DUALITY_SWAPPER, FEE_TIER_CONFIG, GOVERNANCE, HEALTH_CONTRACT, INCENTIVES, + KEEPER_FEE_CONFIG, MAX_SLIPPAGE, MAX_TRIGGER_ORDERS, MAX_UNLOCKING_POSITIONS, ORACLE, + OWNER, PARAMS, PERPS, PERPS_LB_RATIO, RED_BANK, REWARDS_COLLECTOR, SWAPPER, SWAP_FEE, + ZAPPER, }, utils::{assert_max_slippage, assert_perps_lb_ratio, assert_swap_fee}, }; @@ -149,12 +149,11 @@ pub fn update_config( response = response.add_attribute("key", "fee_tier_config"); } - if let Some(addr) = updates.dao_staking_address { + if let Some(addr) = updates.governance_address { let checked = addr.check(deps.api)?; - DAO_STAKING_ADDRESS.save(deps.storage, checked.address())?; - response = response - .add_attribute("key", "dao_staking_address") - .add_attribute("value", checked.address()); + GOVERNANCE.save(deps.storage, checked.address())?; + response = + response.add_attribute("key", "governance").add_attribute("value", checked.address()); } if let Some(kfc) = updates.keeper_fee_config { diff --git a/contracts/credit-manager/src/utils.rs b/contracts/credit-manager/src/utils.rs index 8724341e..14faacc8 100644 --- a/contracts/credit-manager/src/utils.rs +++ b/contracts/credit-manager/src/utils.rs @@ -110,6 +110,29 @@ pub fn assert_swap_fee(swap_fee: Decimal) -> ContractResult<()> { Ok(()) } +pub fn assert_discount_pct(discount_pct: Decimal) -> ContractResult<()> { + if discount_pct > Decimal::one() { + return Err(ContractError::InvalidDiscountPercentage); + } + Ok(()) +} + +pub fn assert_tiers_not_empty(tiers: &[impl std::fmt::Debug]) -> ContractResult<()> { + if tiers.is_empty() { + return Err(ContractError::EmptyFeeTierConfig); + } + Ok(()) +} + +pub fn assert_tiers_sorted_descending(voting_powers: &[Uint128]) -> ContractResult<()> { + for i in 1..voting_powers.len() { + if voting_powers[i] >= voting_powers[i - 1] { + return Err(ContractError::TiersNotSortedDescending); + } + } + Ok(()) +} + pub fn assert_withdraw_enabled( storage: &dyn Storage, querier: &QuerierWrapper, diff --git a/contracts/credit-manager/tests/tests/test_perps_with_discount.rs b/contracts/credit-manager/tests/tests/test_perps_with_discount.rs index 964fe2a5..57d57685 100644 --- a/contracts/credit-manager/tests/tests/test_perps_with_discount.rs +++ b/contracts/credit-manager/tests/tests/test_perps_with_discount.rs @@ -1,8 +1,10 @@ +use std::str::FromStr; + use cosmwasm_std::{Addr, Coin, Decimal, Int128, Uint128}; use cw_multi_test::AppResponse; use mars_types::{ credit_manager::{ - Action::{Deposit, ExecutePerpOrder}, + Action::{ClosePerpPosition, Deposit, ExecutePerpOrder}, ExecutePerpOrderType, }, params::PerpParamsUpdate, @@ -10,7 +12,7 @@ use mars_types::{ use test_case::test_case; use super::helpers::{coin_info, uatom_info, AccountToFund, MockEnv}; -use crate::tests::helpers::default_perp_params; +use crate::tests::helpers::{default_perp_params, get_coin}; fn setup_env() -> (MockEnv, Addr, String) { let atom = uatom_info(); @@ -406,3 +408,471 @@ fn find_attrs(res: &AppResponse) -> std::collections::HashMap { .map(|a| (a.key.clone(), a.value.clone())) .collect::>() } + +// Helper function to execute perp order with tier validation +fn execute_perp_order_with_tier_validation( + mock: &mut MockEnv, + account_id: &str, + user: &Addr, + denom: &str, + size: Int128, + expected_tier: &str, + expected_discount: Decimal, + expected_action: &str, +) -> AppResponse { + let res = mock + .update_credit_account( + account_id, + user, + vec![ExecutePerpOrder { + denom: denom.to_string(), + order_size: size, + reduce_only: None, + order_type: Some(ExecutePerpOrderType::Default), + }], + &[], + ) + .unwrap(); + + let attrs = find_attrs(&res); + assert_eq!(attrs.get("action").unwrap(), expected_action); + assert_eq!(attrs.get("discount_pct").unwrap(), &expected_discount.to_string()); + assert_eq!(attrs.get("tier_id").unwrap(), expected_tier); + + res +} + +// Helper function to assert vault balance increase +fn assert_vault_balance_increase( + mock: &MockEnv, + previous_balance: Uint128, + expected_fee: Uint128, +) -> Uint128 { + let current_balance = mock.query_balance(mock.perps.address(), "uusdc"); + let expected_balance = previous_balance + expected_fee; + assert_eq!(current_balance.amount, expected_balance); + current_balance.amount +} + +// Helper function to assert position size +fn assert_position_size(mock: &MockEnv, account_id: &str, denom: &str, expected_size: Int128) { + let position = mock.query_perp_position(account_id, denom); + assert!(position.position.is_some()); + assert_eq!(position.position.unwrap().size, expected_size); +} + +// Helper function to query and validate opening fee +fn query_and_validate_opening_fee( + mock: &MockEnv, + denom: &str, + size: Int128, + discount_pct: Decimal, + expected_rate: Decimal, + expected_fee: Uint128, +) -> Uint128 { + let opening_fee = mock.query_perp_opening_fee(denom, size, Some(discount_pct)); + assert_eq!(opening_fee.rate, expected_rate); + assert_eq!(opening_fee.fee.amount, expected_fee); + opening_fee.fee.amount +} + +#[test] +fn test_perp_position_modification_with_tier_changes() { + // Test scenario: Open position with no discount, then modify with different discount tiers + // This validates that discount tiers are correctly applied during position lifecycle + + let (mut mock, user, account_id) = setup_env(); + let atom = uatom_info(); + + // Setup: Deposit USDC for position health + mock.update_credit_account( + &account_id, + &user, + vec![Deposit(Coin::new(50_000, "uusdc"))], + &[Coin::new(50_000, "uusdc")], + ) + .unwrap(); + + let initial_vault_balance = mock.query_balance(mock.perps.address(), "uusdc").amount; + + // Step 1: Open initial position with Tier 1 (0% discount) + mock.set_voting_power(&user, Uint128::new(0)); + + // Check user balance before opening position + let position_before_1 = mock.query_positions(&account_id); + let user_usdc_balance_before_1 = get_coin("uusdc", &position_before_1.deposits).amount; + + let opening_fee_1 = query_and_validate_opening_fee( + &mock, + &atom.denom, + Int128::new(200), + Decimal::percent(0), + Decimal::percent(1), + Uint128::new(9), + ); + + execute_perp_order_with_tier_validation( + &mut mock, + &account_id, + &user, + &atom.denom, + Int128::new(200), + "tier_1", + Decimal::percent(0), + "open_perp_position", + ); + + // Verify user paid the correct fee (balance should decrease by opening fee) + let position_after_1 = mock.query_positions(&account_id); + let user_usdc_balance_after_1 = get_coin("uusdc", &position_after_1.deposits).amount; + assert_eq!(user_usdc_balance_after_1, user_usdc_balance_before_1 - opening_fee_1); + + assert_position_size(&mock, &account_id, &atom.denom, Int128::new(200)); + let vault_balance_1 = + assert_vault_balance_increase(&mock, initial_vault_balance, opening_fee_1); + + // Step 2: Increase position with Tier 2 (10% discount) + mock.set_voting_power(&user, Uint128::new(10_000_000_000)); // 10,000 MARS + + // Check user balance before increasing position + let position_before_2 = mock.query_positions(&account_id); + let user_usdc_balance_before_2 = get_coin("uusdc", &position_before_2.deposits).amount; + + let opening_fee_2 = query_and_validate_opening_fee( + &mock, + &atom.denom, + Int128::new(200), + Decimal::percent(10), + Decimal::from_str("0.009").unwrap(), + Uint128::new(8), + ); + + execute_perp_order_with_tier_validation( + &mut mock, + &account_id, + &user, + &atom.denom, + Int128::new(200), + "tier_2", + Decimal::percent(10), + "execute_perp_order", + ); + + // Verify user paid the correct discounted fee (balance should decrease by discounted opening fee) + let position_after_2 = mock.query_positions(&account_id); + let user_usdc_balance_after_2 = get_coin("uusdc", &position_after_2.deposits).amount; + assert_eq!(user_usdc_balance_after_2, user_usdc_balance_before_2 - opening_fee_2); + + let expected_total_size = Int128::new(200 + 200); + assert_position_size(&mock, &account_id, &atom.denom, expected_total_size); + let vault_balance_2 = assert_vault_balance_increase(&mock, vault_balance_1, opening_fee_2); + + // Step 3: Increase position with Tier 5 (45% discount) + mock.set_voting_power(&user, Uint128::new(250_000_000_000)); // 250,000 MARS + + // Check user balance before increasing position + let position_before_3 = mock.query_positions(&account_id); + let user_usdc_balance_before_3 = get_coin("uusdc", &position_before_3.deposits).amount; + + let opening_fee_3 = query_and_validate_opening_fee( + &mock, + &atom.denom, + Int128::new(200), + Decimal::percent(45), + Decimal::from_str("0.0055").unwrap(), + Uint128::new(5), + ); + + execute_perp_order_with_tier_validation( + &mut mock, + &account_id, + &user, + &atom.denom, + Int128::new(200), + "tier_5", + Decimal::percent(45), + "execute_perp_order", + ); + + // Verify user paid the correct discounted fee (balance should decrease by discounted opening fee) + let position_after_3 = mock.query_positions(&account_id); + let user_usdc_balance_after_3 = get_coin("uusdc", &position_after_3.deposits).amount; + assert_eq!(user_usdc_balance_after_3, user_usdc_balance_before_3 - opening_fee_3); + + let final_expected_size = Int128::new(200 + 200 + 200); + assert_position_size(&mock, &account_id, &atom.denom, final_expected_size); + let vault_balance_3 = assert_vault_balance_increase(&mock, vault_balance_2, opening_fee_3); + + // Step 4: Validate total fees collected and user balance changes + let total_fees_collected = vault_balance_3 - initial_vault_balance; + let expected_total_fees = opening_fee_1 + opening_fee_2 + opening_fee_3; + assert_eq!(total_fees_collected, expected_total_fees); + + // Verify total user balance decrease equals total fees paid + let total_user_balance_decrease = user_usdc_balance_before_1 - user_usdc_balance_after_3; + assert_eq!(total_user_balance_decrease, expected_total_fees); + + // Step 5: Close position with current tier (Tier 5) + let close_res = mock + .update_credit_account( + &account_id, + &user, + vec![ClosePerpPosition { + denom: atom.denom.clone(), + }], + &[], + ) + .unwrap(); + + let close_attrs = find_attrs(&close_res); + assert_eq!(close_attrs.get("action").unwrap(), "execute_perp_order"); + assert_eq!(close_attrs.get("discount_pct").unwrap(), &Decimal::percent(45).to_string()); + assert_eq!(close_attrs.get("tier_id").unwrap(), "tier_5"); + + // Verify position is closed + let position_after_close = mock.query_perp_position(&account_id, &atom.denom); + assert!(position_after_close.position.is_none()); +} + +#[test] +fn test_perp_fee_discount_comparison_two_users() { + // Test scenario: Compare fee discounts between two users with different voting power tiers + // User 1: 0 voting power (tier 1, 0% discount) + // User 2: 250,000 MARS voting power (tier 5, 45% discount) + // Both users will: create position, increase position, reduce position, close position + // After each operation, we'll query their balances to verify fee changes + + let atom = uatom_info(); + let usdc = coin_info("uusdc"); + + // Setup two users + let user1 = Addr::unchecked("user1"); + let user2 = Addr::unchecked("user2"); + + let mut mock = MockEnv::new() + .set_params(&[atom.clone(), usdc.clone()]) + .fund_account(AccountToFund { + addr: user1.clone(), + funds: vec![Coin::new(10_000_000, usdc.denom.clone())], + }) + .fund_account(AccountToFund { + addr: user2.clone(), + funds: vec![Coin::new(10_000_000, usdc.denom.clone())], + }) + .build() + .unwrap(); + + // Create credit accounts for both users + let account_id_1 = mock.create_credit_account(&user1).unwrap(); + let account_id_2 = mock.create_credit_account(&user2).unwrap(); + + // Setup perps params and fund vault + mock.update_perp_params(PerpParamsUpdate::AddOrUpdate { + params: default_perp_params(&atom.denom), + }); + let rover_addr = mock.rover.clone(); + let usdc_denom = usdc.denom.clone(); + mock.fund_addr(&rover_addr, vec![Coin::new(200_000_000, usdc_denom.clone())]); + mock.deposit_to_perp_vault(&account_id_1, &Coin::new(50_000_000, usdc_denom.clone()), None) + .unwrap(); + mock.deposit_to_perp_vault(&account_id_2, &Coin::new(50_000_000, usdc_denom), None).unwrap(); + + // Set voting power for both users + mock.set_voting_power(&user1, Uint128::new(0)); // Tier 1: 0% discount + mock.set_voting_power(&user2, Uint128::new(250_000_000_000)); // Tier 5: 45% discount + + // Deposit USDC into both users' credit accounts for position health + mock.update_credit_account( + &account_id_1, + &user1, + vec![Deposit(Coin::new(5_000_000, usdc.denom.clone()))], + &[Coin::new(5_000_000, usdc.denom.clone())], + ) + .unwrap(); + + mock.update_credit_account( + &account_id_2, + &user2, + vec![Deposit(Coin::new(5_000_000, usdc.denom.clone()))], + &[Coin::new(5_000_000, usdc.denom.clone())], + ) + .unwrap(); + + let initial_vault_balance = mock.query_balance(mock.perps.address(), "uusdc").amount; + + // Helper function to get user USDC balance + let get_user_usdc_balance = |mock: &MockEnv, account_id: &str| -> Uint128 { + let position = mock.query_positions(account_id); + get_coin("uusdc", &position.deposits).amount + }; + + // Helper function to execute perp order and return fee paid + let execute_perp_and_track_fee = |mock: &mut MockEnv, + account_id: &str, + user: &Addr, + size: i128| + -> (AppResponse, Uint128, Uint128) { + let balance_before = get_user_usdc_balance(mock, account_id); + let res = mock + .update_credit_account( + account_id, + user, + vec![ExecutePerpOrder { + denom: atom.denom.clone(), + order_size: Int128::new(size), + reduce_only: None, + order_type: Some(ExecutePerpOrderType::Default), + }], + &[], + ) + .unwrap(); + let balance_after = get_user_usdc_balance(mock, account_id); + let fee_paid = balance_before - balance_after; + (res, balance_before, fee_paid) + }; + + // Helper function to close position and return fee paid + let close_position_and_track_fee = + |mock: &mut MockEnv, account_id: &str, user: &Addr| -> (AppResponse, Uint128, Uint128) { + let balance_before = get_user_usdc_balance(mock, account_id); + let res = mock + .update_credit_account( + account_id, + user, + vec![ClosePerpPosition { + denom: atom.denom.clone(), + }], + &[], + ) + .unwrap(); + let balance_after = get_user_usdc_balance(mock, account_id); + let fee_paid = balance_before - balance_after; + (res, balance_before, fee_paid) + }; + + // Helper function to extract discount from response + let extract_discount = |res: &AppResponse| -> Decimal { + let attrs = find_attrs(res); + Decimal::from_str(attrs.get("discount_pct").unwrap()).unwrap() + }; + + // OPERATION 1: Create initial positions (size: 200,000) + // User 1 creates position with 0% discount (Tier 1) + let (res1_create, _, fee_1_create) = + execute_perp_and_track_fee(&mut mock, &account_id_1, &user1, 200000); + let discount_1_create = extract_discount(&res1_create); + assert_eq!(discount_1_create, Decimal::percent(0)); // Verify 0% discount for Tier 1 + + // User 2 creates position with 45% discount (Tier 5) + let (res2_create, _, fee_2_create) = + execute_perp_and_track_fee(&mut mock, &account_id_2, &user2, 200000); + let discount_2_create = extract_discount(&res2_create); + assert_eq!(discount_2_create, Decimal::percent(45)); // Verify 45% discount for Tier 5 + + // Verify User 2 paid significantly less fee due to discount + assert!(fee_2_create < fee_1_create, "User 2 should pay less fee due to 45% discount"); + + // Verify positions were created with correct sizes + assert_position_size(&mock, &account_id_1, &atom.denom, Int128::new(200000)); + assert_position_size(&mock, &account_id_2, &atom.denom, Int128::new(200000)); + + // OPERATION 2: Increase positions (size: +100,000) + // User 1 increases position with 0% discount (Tier 1) + let (res1_increase, _, fee_1_increase) = + execute_perp_and_track_fee(&mut mock, &account_id_1, &user1, 100000); + let discount_1_increase = extract_discount(&res1_increase); + assert_eq!(discount_1_increase, Decimal::percent(0)); // Verify 0% discount maintained + + // User 2 increases position with 45% discount (Tier 5) + let (res2_increase, _, fee_2_increase) = + execute_perp_and_track_fee(&mut mock, &account_id_2, &user2, 100000); + let discount_2_increase = extract_discount(&res2_increase); + assert_eq!(discount_2_increase, Decimal::percent(45)); // Verify 45% discount maintained + + // Verify User 2 continues to pay less fee due to discount + assert!(fee_2_increase < fee_1_increase, "User 2 should pay less fee due to 45% discount"); + + // Verify positions were increased to correct total sizes (200k + 100k = 300k) + assert_position_size(&mock, &account_id_1, &atom.denom, Int128::new(300000)); + assert_position_size(&mock, &account_id_2, &atom.denom, Int128::new(300000)); + + // OPERATION 3: Reduce positions (size: -50,000) + // User 1 reduces position with 0% discount (Tier 1) + let (res1_reduce, _, fee_1_reduce) = + execute_perp_and_track_fee(&mut mock, &account_id_1, &user1, -50000); + let discount_1_reduce = extract_discount(&res1_reduce); + assert_eq!(discount_1_reduce, Decimal::percent(0)); // Verify 0% discount maintained + + // User 2 reduces position with 45% discount (Tier 5) + let (res2_reduce, _, fee_2_reduce) = + execute_perp_and_track_fee(&mut mock, &account_id_2, &user2, -50000); + let discount_2_reduce = extract_discount(&res2_reduce); + assert_eq!(discount_2_reduce, Decimal::percent(45)); // Verify 45% discount maintained + + // Verify User 2 continues to pay less fee due to discount + assert!(fee_2_reduce < fee_1_reduce, "User 2 should pay less fee due to 45% discount"); + + // Verify positions were reduced to correct total sizes (300k - 50k = 250k) + assert_position_size(&mock, &account_id_1, &atom.denom, Int128::new(250000)); + assert_position_size(&mock, &account_id_2, &atom.denom, Int128::new(250000)); + + // OPERATION 4: Close positions (remaining 250,000) + // User 1 closes position with 0% discount (Tier 1) + let (res1_close, _, fee_1_close) = + close_position_and_track_fee(&mut mock, &account_id_1, &user1); + let discount_1_close = extract_discount(&res1_close); + assert_eq!(discount_1_close, Decimal::percent(0)); // Verify 0% discount maintained + + // User 2 closes position with 45% discount (Tier 5) + let (res2_close, _, fee_2_close) = + close_position_and_track_fee(&mut mock, &account_id_2, &user2); + let discount_2_close = extract_discount(&res2_close); + assert_eq!(discount_2_close, Decimal::percent(45)); // Verify 45% discount maintained + + // Verify User 2 continues to pay less fee due to discount + assert!(fee_2_close < fee_1_close, "User 2 should pay less fee due to 45% discount"); + + // Verify positions were completely closed (size 0) + let position_1_after_close = mock.query_perp_position(&account_id_1, &atom.denom); + let position_2_after_close = mock.query_perp_position(&account_id_2, &atom.denom); + assert!(position_1_after_close.position.is_none()); // User 1 position closed + assert!(position_2_after_close.position.is_none()); // User 2 position closed + + // SUMMARY: Calculate and verify total fee differences + // Calculate total fees paid by each user across all operations + let total_fees_user1 = fee_1_create + fee_1_increase + fee_1_reduce + fee_1_close; + let total_fees_user2 = fee_2_create + fee_2_increase + fee_2_reduce + fee_2_close; + + // Verify total fees collected by vault matches sum of both users' fees + let final_vault_balance = mock.query_balance(mock.perps.address(), "uusdc").amount; + let total_fees_collected = final_vault_balance - initial_vault_balance; + let expected_total_fees = total_fees_user1 + total_fees_user2; + assert_eq!( + total_fees_collected, expected_total_fees, + "Vault should have collected the sum of both users' fees" + ); + + // Verify that User 2's total fees are approximately 45% less than User 1's + // (allowing for rounding differences due to fee calculations) + let expected_user2_fees = total_fees_user1 * Decimal::percent(55); // 100% - 45% = 55% + let fee_difference = if total_fees_user2 > expected_user2_fees { + total_fees_user2 - expected_user2_fees + } else { + expected_user2_fees - total_fees_user2 + }; + + // Allow for small rounding differences (within 2 units due to larger discount) + assert!(fee_difference <= Uint128::new(2), + "User 2's total fees should be approximately 45% less than User 1's. Expected: {}, Actual: {}, Difference: {}", + expected_user2_fees, total_fees_user2, fee_difference); + + // Verify the discount is working correctly by checking the percentage saved + let actual_discount_percentage = + ((total_fees_user1 - total_fees_user2) * Uint128::new(100)) / total_fees_user1; + assert!( + actual_discount_percentage >= Uint128::new(40) + && actual_discount_percentage <= Uint128::new(50), + "Actual discount should be close to 45%. Got: {}%", + actual_discount_percentage + ); +} diff --git a/contracts/credit-manager/tests/tests/test_staking_tiers.rs b/contracts/credit-manager/tests/tests/test_staking_tiers.rs index f13e02dc..7cf01d95 100644 --- a/contracts/credit-manager/tests/tests/test_staking_tiers.rs +++ b/contracts/credit-manager/tests/tests/test_staking_tiers.rs @@ -1,5 +1,5 @@ -use cosmwasm_std::{Decimal, StdError, Uint128}; -use mars_credit_manager::staking::StakingTierManager; +use cosmwasm_std::{Decimal, Uint128}; +use mars_credit_manager::{error::ContractError, staking::StakingTierManager}; use mars_types::fee_tiers::{FeeTier, FeeTierConfig}; use test_case::test_case; @@ -9,42 +9,42 @@ fn create_test_fee_tier_config() -> FeeTierConfig { tiers: vec![ FeeTier { id: "tier_8".to_string(), - min_voting_power: "1500000000000".to_string(), // 1,500,000 MARS + min_voting_power: Uint128::new(1500000000000), // 1,500,000 MARS discount_pct: Decimal::percent(80), }, FeeTier { id: "tier_7".to_string(), - min_voting_power: "1000000000000".to_string(), // 1,000,000 MARS + min_voting_power: Uint128::new(1000000000000), // 1,000,000 MARS discount_pct: Decimal::percent(70), }, FeeTier { id: "tier_6".to_string(), - min_voting_power: "500000000000".to_string(), // 500,000 MARS + min_voting_power: Uint128::new(500000000000), // 500,000 MARS discount_pct: Decimal::percent(60), }, FeeTier { id: "tier_5".to_string(), - min_voting_power: "250000000000".to_string(), // 250,000 MARS + min_voting_power: Uint128::new(250000000000), // 250,000 MARS discount_pct: Decimal::percent(45), }, FeeTier { id: "tier_4".to_string(), - min_voting_power: "100000000000".to_string(), // 100,000 MARS + min_voting_power: Uint128::new(100000000000), // 100,000 MARS discount_pct: Decimal::percent(30), }, FeeTier { id: "tier_3".to_string(), - min_voting_power: "50000000000".to_string(), // 50,000 MARS + min_voting_power: Uint128::new(50000000000), // 50,000 MARS discount_pct: Decimal::percent(20), }, FeeTier { id: "tier_2".to_string(), - min_voting_power: "10000000000".to_string(), // 10,000 MARS + min_voting_power: Uint128::new(10000000000), // 10,000 MARS discount_pct: Decimal::percent(10), }, FeeTier { id: "tier_1".to_string(), - min_voting_power: "0".to_string(), + min_voting_power: Uint128::zero(), discount_pct: Decimal::percent(0), }, ], @@ -252,14 +252,7 @@ fn test_validate_fee_tier_config_empty() { let result = manager.validate(); assert!(result.is_err()); - match result.unwrap_err() { - StdError::GenericErr { - msg, - } => { - assert!(msg.contains("Fee tier config cannot be empty")); - } - _ => panic!("Expected StdError::GenericErr"), - } + assert_eq!(result.unwrap_err(), ContractError::EmptyFeeTierConfig); } #[test] @@ -268,12 +261,12 @@ fn test_validate_fee_tier_config_unsorted() { tiers: vec![ FeeTier { id: "tier_2".to_string(), - min_voting_power: "200000".to_string(), + min_voting_power: Uint128::new(200000), discount_pct: Decimal::percent(60), }, FeeTier { id: "tier_1".to_string(), - min_voting_power: "350000".to_string(), + min_voting_power: Uint128::new(350000), discount_pct: Decimal::percent(75), }, ], @@ -282,14 +275,7 @@ fn test_validate_fee_tier_config_unsorted() { let result = manager.validate(); assert!(result.is_err()); - match result.unwrap_err() { - StdError::GenericErr { - msg, - } => { - assert!(msg.contains("Tiers must be sorted in descending order")); - } - _ => panic!("Expected StdError::GenericErr"), - } + assert_eq!(result.unwrap_err(), ContractError::TiersNotSortedDescending); } #[test] @@ -298,12 +284,12 @@ fn test_validate_fee_tier_config_duplicate_thresholds() { tiers: vec![ FeeTier { id: "tier_1".to_string(), - min_voting_power: "350000".to_string(), + min_voting_power: Uint128::new(350000), discount_pct: Decimal::percent(75), }, FeeTier { id: "tier_2".to_string(), - min_voting_power: "350000".to_string(), + min_voting_power: Uint128::new(350000), discount_pct: Decimal::percent(60), }, ], @@ -312,14 +298,7 @@ fn test_validate_fee_tier_config_duplicate_thresholds() { let result = manager.validate(); assert!(result.is_err()); - match result.unwrap_err() { - StdError::GenericErr { - msg, - } => { - assert!(msg.contains("Duplicate voting power thresholds")); - } - _ => panic!("Expected StdError::GenericErr"), - } + assert_eq!(result.unwrap_err(), ContractError::DuplicateVotingPowerThresholds); } #[test] @@ -327,22 +306,15 @@ fn test_validate_fee_tier_config_invalid_discount() { let config = FeeTierConfig { tiers: vec![FeeTier { id: "tier_1".to_string(), - min_voting_power: "350000".to_string(), - discount_pct: Decimal::percent(100), // 100% discount (invalid) + min_voting_power: Uint128::new(350000), + discount_pct: Decimal::percent(150), // 150% discount (invalid - over 100%) }], }; let manager = StakingTierManager::new(config); let result = manager.validate(); assert!(result.is_err()); - match result.unwrap_err() { - StdError::GenericErr { - msg, - } => { - assert!(msg.contains("Discount percentage must be less than 100%")); - } - _ => panic!("Expected StdError::GenericErr"), - } + assert_eq!(result.unwrap_err(), ContractError::InvalidDiscountPercentage); } #[test] @@ -408,7 +380,7 @@ fn test_fee_tier_config_with_single_tier() { let config = FeeTierConfig { tiers: vec![FeeTier { id: "single_tier".to_string(), - min_voting_power: "0".to_string(), + min_voting_power: Uint128::zero(), discount_pct: Decimal::percent(25), }], }; @@ -430,12 +402,12 @@ fn test_fee_tier_config_with_two_tiers() { tiers: vec![ FeeTier { id: "high_tier".to_string(), - min_voting_power: "1000".to_string(), + min_voting_power: Uint128::new(1000), discount_pct: Decimal::percent(50), }, FeeTier { id: "low_tier".to_string(), - min_voting_power: "0".to_string(), + min_voting_power: Uint128::zero(), discount_pct: Decimal::percent(10), }, ], @@ -469,7 +441,7 @@ fn test_validation_empty_tiers() { let result = manager.validate(); assert!(result.is_err()); - assert_eq!(result.unwrap_err(), StdError::generic_err("Fee tier config cannot be empty")); + assert_eq!(result.unwrap_err(), ContractError::EmptyFeeTierConfig); } #[test] @@ -477,7 +449,7 @@ fn test_validation_single_tier_valid() { let config = FeeTierConfig { tiers: vec![FeeTier { id: "single".to_string(), - min_voting_power: "1000".to_string(), + min_voting_power: Uint128::new(1000), discount_pct: Decimal::percent(25), }], }; @@ -502,12 +474,12 @@ fn test_validation_duplicate_voting_power() { tiers: vec![ FeeTier { id: "tier_1".to_string(), - min_voting_power: "1000".to_string(), + min_voting_power: Uint128::new(1000), discount_pct: Decimal::percent(50), }, FeeTier { id: "tier_2".to_string(), - min_voting_power: "1000".to_string(), // Duplicate! + min_voting_power: Uint128::new(1000), // Duplicate! discount_pct: Decimal::percent(25), }, ], @@ -516,7 +488,7 @@ fn test_validation_duplicate_voting_power() { let result = manager.validate(); assert!(result.is_err()); - assert_eq!(result.unwrap_err(), StdError::generic_err("Duplicate voting power thresholds")); + assert_eq!(result.unwrap_err(), ContractError::DuplicateVotingPowerThresholds); } #[test] @@ -525,12 +497,12 @@ fn test_validation_not_descending_order() { tiers: vec![ FeeTier { id: "tier_1".to_string(), - min_voting_power: "1000".to_string(), + min_voting_power: Uint128::new(1000), discount_pct: Decimal::percent(50), }, FeeTier { id: "tier_2".to_string(), - min_voting_power: "2000".to_string(), // Higher than previous! + min_voting_power: Uint128::new(2000), // Higher than previous! discount_pct: Decimal::percent(25), }, ], @@ -539,10 +511,7 @@ fn test_validation_not_descending_order() { let result = manager.validate(); assert!(result.is_err()); - assert_eq!( - result.unwrap_err(), - StdError::generic_err("Tiers must be sorted in descending order") - ); + assert_eq!(result.unwrap_err(), ContractError::TiersNotSortedDescending); } #[test] @@ -551,12 +520,12 @@ fn test_validation_equal_voting_power() { tiers: vec![ FeeTier { id: "tier_1".to_string(), - min_voting_power: "1000".to_string(), + min_voting_power: Uint128::new(1000), discount_pct: Decimal::percent(50), }, FeeTier { id: "tier_2".to_string(), - min_voting_power: "1000".to_string(), // Equal to previous! + min_voting_power: Uint128::new(1000), // Equal to previous! discount_pct: Decimal::percent(25), }, ], @@ -565,23 +534,24 @@ fn test_validation_equal_voting_power() { let result = manager.validate(); assert!(result.is_err()); - assert_eq!(result.unwrap_err(), StdError::generic_err("Duplicate voting power thresholds")); + assert_eq!(result.unwrap_err(), ContractError::DuplicateVotingPowerThresholds); } #[test] -fn test_validation_invalid_voting_power_format() { +fn test_validation_valid_voting_power_format() { + // This test is no longer needed since Uint128 type ensures valid format + // But we can test that valid Uint128 values work correctly let config = FeeTierConfig { tiers: vec![FeeTier { id: "tier_1".to_string(), - min_voting_power: "invalid_number".to_string(), // Invalid format! + min_voting_power: Uint128::new(1000), discount_pct: Decimal::percent(50), }], }; let manager = StakingTierManager::new(config); let result = manager.validate(); - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), StdError::generic_err("Invalid min_voting_power in tier")); + assert!(result.is_ok()); } #[test] @@ -589,18 +559,14 @@ fn test_validation_discount_100_percent() { let config = FeeTierConfig { tiers: vec![FeeTier { id: "tier_1".to_string(), - min_voting_power: "1000".to_string(), - discount_pct: Decimal::one(), // 100% discount! + min_voting_power: Uint128::new(1000), + discount_pct: Decimal::one(), // 100% discount - now valid! }], }; let manager = StakingTierManager::new(config); let result = manager.validate(); - assert!(result.is_err()); - assert_eq!( - result.unwrap_err(), - StdError::generic_err("Discount percentage must be less than 100%") - ); + assert!(result.is_ok()); // 100% discount should now be valid } #[test] @@ -608,7 +574,7 @@ fn test_validation_discount_over_100_percent() { let config = FeeTierConfig { tiers: vec![FeeTier { id: "tier_1".to_string(), - min_voting_power: "1000".to_string(), + min_voting_power: Uint128::new(1000), discount_pct: Decimal::percent(150), // 150% discount! }], }; @@ -616,10 +582,7 @@ fn test_validation_discount_over_100_percent() { let result = manager.validate(); assert!(result.is_err()); - assert_eq!( - result.unwrap_err(), - StdError::generic_err("Discount percentage must be less than 100%") - ); + assert_eq!(result.unwrap_err(), ContractError::InvalidDiscountPercentage); } #[test] @@ -627,7 +590,7 @@ fn test_validation_discount_99_percent_valid() { let config = FeeTierConfig { tiers: vec![FeeTier { id: "tier_1".to_string(), - min_voting_power: "1000".to_string(), + min_voting_power: Uint128::new(1000), discount_pct: Decimal::percent(99), // 99% discount - valid! }], }; @@ -642,7 +605,7 @@ fn test_validation_zero_discount_valid() { let config = FeeTierConfig { tiers: vec![FeeTier { id: "tier_1".to_string(), - min_voting_power: "1000".to_string(), + min_voting_power: Uint128::new(1000), discount_pct: Decimal::zero(), // 0% discount - valid! }], }; @@ -658,27 +621,27 @@ fn test_validation_complex_scenario() { tiers: vec![ FeeTier { id: "platinum".to_string(), - min_voting_power: "1000000".to_string(), + min_voting_power: Uint128::new(1000000), discount_pct: Decimal::percent(90), }, FeeTier { id: "gold".to_string(), - min_voting_power: "500000".to_string(), + min_voting_power: Uint128::new(500000), discount_pct: Decimal::percent(75), }, FeeTier { id: "silver".to_string(), - min_voting_power: "100000".to_string(), + min_voting_power: Uint128::new(100000), discount_pct: Decimal::percent(50), }, FeeTier { id: "bronze".to_string(), - min_voting_power: "10000".to_string(), + min_voting_power: Uint128::new(10000), discount_pct: Decimal::percent(25), }, FeeTier { id: "basic".to_string(), - min_voting_power: "0".to_string(), + min_voting_power: Uint128::zero(), discount_pct: Decimal::zero(), }, ], @@ -695,12 +658,12 @@ fn test_validation_edge_case_single_digit() { tiers: vec![ FeeTier { id: "tier_1".to_string(), - min_voting_power: "1".to_string(), + min_voting_power: Uint128::new(1), discount_pct: Decimal::percent(10), }, FeeTier { id: "tier_2".to_string(), - min_voting_power: "0".to_string(), + min_voting_power: Uint128::zero(), discount_pct: Decimal::zero(), }, ], diff --git a/contracts/credit-manager/tests/tests/test_update_config.rs b/contracts/credit-manager/tests/tests/test_update_config.rs index 018d60a7..3d792fd8 100644 --- a/contracts/credit-manager/tests/tests/test_update_config.rs +++ b/contracts/credit-manager/tests/tests/test_update_config.rs @@ -42,7 +42,7 @@ fn only_owner_can_update_config() { perps_liquidation_bonus_ratio: None, swap_fee: None, fee_tier_config: None, - dao_staking_address: None, + governance_address: None, }, ); @@ -131,7 +131,7 @@ fn update_config_works_with_full_config() { perps_liquidation_bonus_ratio: Some(new_perps_lb_ratio), swap_fee: Some(new_swap_fee), fee_tier_config: None, - dao_staking_address: None, + governance_address: None, }, ) .unwrap(); diff --git a/contracts/mock-dao-staking/Cargo.toml b/contracts/mock-governance/Cargo.toml similarity index 88% rename from contracts/mock-dao-staking/Cargo.toml rename to contracts/mock-governance/Cargo.toml index d89ef94f..7baa9b8e 100644 --- a/contracts/mock-dao-staking/Cargo.toml +++ b/contracts/mock-governance/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "mars-mock-dao-staking" +name = "mars-mock-governance" version = "0.1.0" edition = "2021" diff --git a/contracts/mock-dao-staking/src/lib.rs b/contracts/mock-governance/src/lib.rs similarity index 86% rename from contracts/mock-dao-staking/src/lib.rs rename to contracts/mock-governance/src/lib.rs index d8bb0dfa..811f844c 100644 --- a/contracts/mock-dao-staking/src/lib.rs +++ b/contracts/mock-governance/src/lib.rs @@ -4,8 +4,8 @@ use cosmwasm_std::{ StdResult, Uint128, }; use cw_storage_plus::Map; -use mars_types::adapters::dao_staking::{ - DaoStakingQueryMsg, VotingPowerAtHeightQuery, VotingPowerAtHeightResponse, +use mars_types::adapters::governance::{ + GovernanceQueryMsg, VotingPowerAtHeightQuery, VotingPowerAtHeightResponse, }; const VOTING_POWER: Map<&Addr, Uint128> = Map::new("voting_power"); @@ -38,9 +38,9 @@ pub fn execute(deps: DepsMut, _env: Env, _info: MessageInfo, msg: ExecMsg) -> St } #[entry_point] -pub fn query(deps: Deps, _env: Env, msg: DaoStakingQueryMsg) -> StdResult { +pub fn query(deps: Deps, _env: Env, msg: GovernanceQueryMsg) -> StdResult { match msg { - DaoStakingQueryMsg::VotingPowerAtHeight(VotingPowerAtHeightQuery { + GovernanceQueryMsg::VotingPowerAtHeight(VotingPowerAtHeightQuery { address, }) => { let addr = deps.api.addr_validate(&address)?; diff --git a/contracts/perps/src/deleverage.rs b/contracts/perps/src/deleverage.rs index f14478c2..9ad9f260 100644 --- a/contracts/perps/src/deleverage.rs +++ b/contracts/perps/src/deleverage.rs @@ -18,13 +18,16 @@ use crate::{ error::{ContractError, ContractResult}, market::MarketStateExt, position::{PositionExt, PositionModification}, - position_management::apply_pnl_and_fees, + position_management::{apply_pnl_and_fees, compute_discounted_fee_rates}, query, state::{ - DeleverageRequestTempStorage, ACCOUNT_OPENING_FEE_RATES, CONFIG, - DELEVERAGE_REQUEST_TEMP_STORAGE, MARKET_STATES, POSITIONS, REALIZED_PNL, TOTAL_CASH_FLOW, + DeleverageRequestTempStorage, CONFIG, DELEVERAGE_REQUEST_TEMP_STORAGE, MARKET_STATES, + POSITIONS, REALIZED_PNL, TOTAL_CASH_FLOW, + }, + utils::{ + get_credit_manager_adapter, get_oracle_adapter, get_params_adapter, + update_position_attributes, }, - utils::{get_oracle_adapter, get_params_adapter, update_position_attributes}, }; pub const DELEVERAGE_REQUEST_REPLY_ID: u64 = 10_001; @@ -87,11 +90,12 @@ pub fn deleverage( let addresses = query_contract_addrs( deps.as_ref(), &cfg.address_provider, - vec![MarsAddressType::Oracle, MarsAddressType::Params], + vec![MarsAddressType::Oracle, MarsAddressType::Params, MarsAddressType::CreditManager], )?; let oracle = get_oracle_adapter(&addresses[&MarsAddressType::Oracle]); let params = get_params_adapter(&addresses[&MarsAddressType::Params]); + let credit_manager = get_credit_manager_adapter(&addresses[&MarsAddressType::CreditManager]); // Query prices and parameters let base_denom_price = @@ -114,19 +118,9 @@ pub fn deleverage( let initial_skew = ms.skew()?; ms.close_position(current_time, denom_price, base_denom_price, &position)?; - // Check if we have a stored opening fee rate for this position - let stored_opening_fee_rate = - ACCOUNT_OPENING_FEE_RATES.may_load(deps.storage, (&account_id, &denom))?; - - let (opening_fee_rate, closing_fee_rate) = if let Some(stored_rate) = stored_opening_fee_rate { - // Use the stored opening fee rate (what was actually paid) for historical accuracy - // Use current closing fee rate (fair for current operations) - (stored_rate, perp_params.closing_fee_rate) - } else { - // Fallback to current rates for existing positions without stored fee rates - (perp_params.opening_fee_rate, perp_params.closing_fee_rate) - }; - + let discount_pct = credit_manager.query_discount_pct(&deps.querier, &account_id)?; + let (opening_fee_rate, closing_fee_rate) = + compute_discounted_fee_rates(&perp_params, Some(discount_pct))?; // Compute the position's unrealized PnL let pnl_amounts = position.compute_pnl( &ms.funding, @@ -174,9 +168,6 @@ pub fn deleverage( // Save updated states POSITIONS.remove(deps.storage, (&account_id, &denom)); - // Clean up the stored opening fee rate for this position - ACCOUNT_OPENING_FEE_RATES.remove(deps.storage, (&account_id, &denom)); - REALIZED_PNL.save(deps.storage, (&account_id, &denom), &realized_pnl)?; MARKET_STATES.save(deps.storage, &denom, &ms)?; TOTAL_CASH_FLOW.save(deps.storage, &tcf)?; diff --git a/contracts/perps/src/position_management.rs b/contracts/perps/src/position_management.rs index a53b8629..064d4cc9 100644 --- a/contracts/perps/src/position_management.rs +++ b/contracts/perps/src/position_management.rs @@ -18,9 +18,7 @@ use crate::{ error::{ContractError, ContractResult}, market::MarketStateExt, position::{calculate_new_size, PositionExt, PositionModification}, - state::{ - ACCOUNT_OPENING_FEE_RATES, CONFIG, MARKET_STATES, POSITIONS, REALIZED_PNL, TOTAL_CASH_FLOW, - }, + state::{CONFIG, MARKET_STATES, POSITIONS, REALIZED_PNL, TOTAL_CASH_FLOW}, utils::{ ensure_max_position, ensure_min_position, get_oracle_adapter, get_params_adapter, update_position_attributes, @@ -31,20 +29,16 @@ use crate::{ pub fn compute_discounted_fee_rates( perp_params: &PerpParams, discount_pct: Option, -) -> (Decimal, Decimal) { - let opening_fee_rate = if let Some(discount) = discount_pct { - perp_params.opening_fee_rate * (Decimal::one() - discount) - } else { - perp_params.opening_fee_rate - }; - - let closing_fee_rate = if let Some(discount) = discount_pct { - perp_params.closing_fee_rate * (Decimal::one() - discount) +) -> Result<(Decimal, Decimal), ContractError> { + if let Some(discount) = discount_pct { + let discount_multiplier = Decimal::one().checked_sub(discount)?; + Ok(( + perp_params.opening_fee_rate.checked_mul(discount_multiplier)?, + perp_params.closing_fee_rate.checked_mul(discount_multiplier)?, + )) } else { - perp_params.closing_fee_rate - }; - - (opening_fee_rate, closing_fee_rate) + Ok((perp_params.opening_fee_rate, perp_params.closing_fee_rate)) + } } /// Executes a perpetual order for a specific account and denom. @@ -145,7 +139,7 @@ fn open_position( // Apply discount to fee rates if provided let (opening_fee_rate, closing_fee_rate) = - compute_discounted_fee_rates(&perp_params, discount_pct); + compute_discounted_fee_rates(&perp_params, discount_pct)?; let opening_fee_amt = may_pay(&info, &cfg.base_denom)?; @@ -245,9 +239,6 @@ fn open_position( }, )?; - // Save the actual opening fee rate that was applied to this position - ACCOUNT_OPENING_FEE_RATES.save(deps.storage, (&account_id, &denom), &opening_fee_rate)?; - Ok(Response::new() .add_messages(msgs) .add_attribute("action", "open_position") @@ -305,7 +296,7 @@ fn modify_position( // Apply discount to fee rates if provided let (opening_fee_rate, closing_fee_rate) = - compute_discounted_fee_rates(&perp_params, discount_pct); + compute_discounted_fee_rates(&perp_params, discount_pct)?; // Load relevant state variables let mut realized_pnl = @@ -362,9 +353,6 @@ fn modify_position( modification }; - // Check if this is a position flip to save the opening fee rate later - let is_position_flip = matches!(modification, PositionModification::Flip(_, _)); - // Compute the position's unrealized PnL let pnl_amounts = position.compute_pnl( &ms.funding, @@ -378,9 +366,7 @@ fn modify_position( // Convert PnL amounts to coins let pnl = pnl_amounts.to_coins(&cfg.base_denom).pnl; - let mut msgs = vec![]; - // Apply the payment to the credit manager if necessary apply_payment_to_cm_if_needed(&cfg, cm_address, &mut msgs, paid_amount, &pnl)?; @@ -401,7 +387,6 @@ fn modify_position( entry_accrued_funding_per_unit_in_base_denom, &pnl_amounts, ); - // Update the realized PnL, market state, and total cash flow based on the new amounts apply_pnl_and_fees( &cfg, @@ -419,9 +404,6 @@ fn modify_position( // Delete the position if the new size is zero POSITIONS.remove(deps.storage, (&account_id, &denom)); - // Clean up the stored opening fee rate for this position - ACCOUNT_OPENING_FEE_RATES.remove(deps.storage, (&account_id, &denom)); - "close_position" } else { // Save the updated position state @@ -444,15 +426,6 @@ fn modify_position( }, )?; - // Update the opening fee rate if this was a position flip (new opening fee charged) - if is_position_flip { - ACCOUNT_OPENING_FEE_RATES.save( - deps.storage, - (&account_id, &denom), - &opening_fee_rate, - )?; - } - "modify_position" }; @@ -548,7 +521,7 @@ pub fn close_all_positions( // Apply discount to fee rates if provided let (opening_fee_rate, closing_fee_rate) = - compute_discounted_fee_rates(&perp_params, discount_pct); + compute_discounted_fee_rates(&perp_params, discount_pct)?; // Compute the position's unrealized PnL let pnl_amounts = position.compute_pnl( @@ -589,9 +562,6 @@ pub fn close_all_positions( // Remove the position POSITIONS.remove(deps.storage, (&account_id, &denom)); - // Clean up the stored opening fee rate for this position - ACCOUNT_OPENING_FEE_RATES.remove(deps.storage, (&account_id, &denom)); - // Save updated states REALIZED_PNL.save(deps.storage, (&account_id, &denom), &realized_pnl)?; MARKET_STATES.save(deps.storage, &denom, &ms)?; diff --git a/contracts/perps/src/query.rs b/contracts/perps/src/query.rs index 3a35170d..52ff6d1a 100644 --- a/contracts/perps/src/query.rs +++ b/contracts/perps/src/query.rs @@ -25,7 +25,7 @@ use crate::{ position::{PositionExt, PositionModification}, position_management::compute_discounted_fee_rates, state::{ - ACCOUNT_OPENING_FEE_RATES, CONFIG, DEPOSIT_SHARES, MARKET_STATES, POSITIONS, REALIZED_PNL, + CONFIG, DEPOSIT_SHARES, MARKET_STATES, POSITIONS, REALIZED_PNL, TOTAL_UNLOCKING_OR_UNLOCKED_SHARES, UNLOCKS, VAULT_STATE, }, utils::{ @@ -314,22 +314,18 @@ pub fn query_position( // Query the credit manager to get the discount for this account via adapter let discount_pct = credit_manager_adapter.query_discount_pct(&deps.querier, &account_id)?; - // Check if we have a stored opening fee rate for this position - let stored_opening_fee_rate = - ACCOUNT_OPENING_FEE_RATES.may_load(deps.storage, (&account_id, &denom))?; + // Get the effective opening fee rate (volume-weighted average or fallback) + // let fallback_opening_fee_rate = perp_params.opening_fee_rate * (Decimal::one() - discount_pct); + // let discounted_opening_fee_rate = get_effective_opening_fee_rate( + // deps.storage, + // &account_id, + // &denom, + // fallback_opening_fee_rate, + // )?; + // Use current discount for closing fee rate (fair for current operations) let (discounted_opening_fee_rate, discounted_closing_fee_rate) = - if let Some(stored_rate) = stored_opening_fee_rate { - // Use the stored opening fee rate (what was actually paid) for historical accuracy - // But still use current discount for closing fee rate (fair for current operations) - let current_closing_fee_rate = - perp_params.closing_fee_rate * (Decimal::one() - discount_pct); - (stored_rate, current_closing_fee_rate) - } else { - // Fallback to current rates for existing positions without stored fee rates - compute_discounted_fee_rates(&perp_params, Some(discount_pct)) - }; - + compute_discounted_fee_rates(&perp_params, Some(discount_pct))?; let pnl_amounts = position.compute_pnl( &curr_funding, ms.skew()?, @@ -425,21 +421,9 @@ pub fn query_positions( credit_manager_adapter.query_discount_pct(&deps.querier, &account_id)?; // Check if we have a stored opening fee rate for this position - let stored_opening_fee_rate = - ACCOUNT_OPENING_FEE_RATES.may_load(deps.storage, (&account_id, &denom))?; let (discounted_opening_fee_rate, discounted_closing_fee_rate) = - if let Some(stored_rate) = stored_opening_fee_rate { - // Use the stored opening fee rate (what was actually paid) for historical accuracy - // But still use current discount for closing fee rate (fair for current operations) - let current_closing_fee_rate = - perp_params.closing_fee_rate * (Decimal::one() - discount_pct); - (stored_rate, current_closing_fee_rate) - } else { - // Fallback to current rates for existing positions without stored fee rates - compute_discounted_fee_rates(&perp_params, Some(discount_pct)) - }; - + compute_discounted_fee_rates(&perp_params, Some(discount_pct))?; let pnl_amounts = position.compute_pnl( &funding, skew, @@ -490,12 +474,13 @@ pub fn query_positions_by_account( MarsAddressType::Oracle, MarsAddressType::Params, MarsAddressType::CreditManager, - MarsAddressType::DaoStaking, + MarsAddressType::Governance, ], )?; let oracle = get_oracle_adapter(&addresses[&MarsAddressType::Oracle]); let params = get_params_adapter(&addresses[&MarsAddressType::Params]); + let credit_manager = get_credit_manager_adapter(&addresses[&MarsAddressType::CreditManager]); // Don't query the price if there are no positions. This is important during liquidation as // the price query might fail (if Default pricing is pased in). @@ -523,18 +508,10 @@ pub fn query_positions_by_account( let curr_funding = ms.current_funding(current_time, denom_price, base_denom_price)?; // Check if we have a stored opening fee rate for this position - let stored_opening_fee_rate = - ACCOUNT_OPENING_FEE_RATES.may_load(deps.storage, (&account_id, &denom))?; + let discount_pct = credit_manager.query_discount_pct(&deps.querier, &account_id)?; let (opening_fee_rate, closing_fee_rate) = - if let Some(stored_rate) = stored_opening_fee_rate { - // Use the stored opening fee rate (what was actually paid) for historical accuracy - // Use current closing fee rate (fair for current operations) - (stored_rate, perp_params.closing_fee_rate) - } else { - // Fallback to current rates for existing positions without stored fee rates - (perp_params.opening_fee_rate, perp_params.closing_fee_rate) - }; + compute_discounted_fee_rates(&perp_params, Some(discount_pct))?; let pnl_amounts = position.compute_pnl( &curr_funding, @@ -682,7 +659,7 @@ pub fn query_opening_fee( let perp_params = params.query_perp_params(&deps.querier, denom)?; // Apply discount to fee rates if provided - let (opening_fee_rate, _) = compute_discounted_fee_rates(&perp_params, discount_pct); + let (opening_fee_rate, _) = compute_discounted_fee_rates(&perp_params, discount_pct)?; let fees = PositionModification::Increase(size).compute_fees( opening_fee_rate, @@ -718,7 +695,7 @@ pub fn query_position_fees( MarsAddressType::Oracle, MarsAddressType::Params, MarsAddressType::CreditManager, - MarsAddressType::DaoStaking, + MarsAddressType::Governance, ], )?; @@ -775,7 +752,7 @@ pub fn query_position_fees( // Apply discount to fee rates using the helper function let (discounted_opening_fee_rate, discounted_closing_fee_rate) = - compute_discounted_fee_rates(&perp_params, Some(discount_pct)); + compute_discounted_fee_rates(&perp_params, Some(discount_pct))?; let fees = modification.compute_fees( discounted_opening_fee_rate, diff --git a/contracts/perps/src/state.rs b/contracts/perps/src/state.rs index a4fa5eed..15f1a612 100644 --- a/contracts/perps/src/state.rs +++ b/contracts/perps/src/state.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Decimal, StdError, StdResult, Storage, Uint128}; +use cosmwasm_std::{Addr, StdError, StdResult, Storage, Uint128}; use cw_storage_plus::{Item, Map}; use mars_owner::Owner; use mars_types::{ @@ -40,10 +40,6 @@ pub const POSITIONS: Map<(&str, &str), Position> = Map::new("positions"); // (account_id, denom) => realized PnL amounts pub const REALIZED_PNL: Map<(&str, &str), PnlAmounts> = Map::new("realized_pnls"); -// (account_id, denom) => opening fee rate that was actually applied -pub const ACCOUNT_OPENING_FEE_RATES: Map<(&str, &str), Decimal> = - Map::new("account_opening_fee_rates"); - // denom => market cash flow pub const MARKET_CASH_FLOW: Map<&str, CashFlow> = Map::new("market_cf"); diff --git a/contracts/perps/tests/tests/helpers/mock_env.rs b/contracts/perps/tests/tests/helpers/mock_env.rs index d31d891d..069b0460 100644 --- a/contracts/perps/tests/tests/helpers/mock_env.rs +++ b/contracts/perps/tests/tests/helpers/mock_env.rs @@ -58,7 +58,7 @@ pub struct MockEnvBuilder { deleverage_enabled: bool, withdraw_enabled: bool, max_unlocks: u8, - pub dao_staking_addr: Option, + pub governance_addr: Option, } #[allow(clippy::new_ret_no_self)] @@ -78,7 +78,7 @@ impl MockEnv { deleverage_enabled: true, withdraw_enabled: true, max_unlocks: 5, - dao_staking_addr: Some(Addr::unchecked("mock-dao-staking")), + governance_addr: Some(Addr::unchecked("mock-governance")), } } @@ -533,13 +533,13 @@ impl MockEnv { .unwrap() } - pub fn set_dao_staking_address(&mut self, address: &Addr) { + pub fn set_governance_address(&mut self, address: &Addr) { self.app .execute_contract( self.owner.clone(), self.address_provider.clone(), &address_provider::ExecuteMsg::SetAddress { - address_type: MarsAddressType::DaoStaking, + address_type: MarsAddressType::Governance, address: address.to_string(), }, &[], @@ -559,12 +559,12 @@ impl MockEnvBuilder { let perps_contract = self.deploy_perps(address_provider_contract.as_str()); let incentives_contract = self.deploy_incentives(&address_provider_contract); - // Deploy dao staking if provided - if let Some(dao_staking_addr) = self.dao_staking_addr.clone() { + // Deploy governance if provided + if let Some(governance_addr) = self.governance_addr.clone() { self.update_address_provider( &address_provider_contract, - MarsAddressType::DaoStaking, - &dao_staking_addr, + MarsAddressType::Governance, + &governance_addr, ); } @@ -878,21 +878,21 @@ impl MockEnvBuilder { self } - pub fn set_dao_staking_addr(mut self, addr: &Addr) -> Self { - self.dao_staking_addr = Some(addr.clone()); + pub fn set_governance_addr(mut self, addr: &Addr) -> Self { + self.governance_addr = Some(addr.clone()); self } - pub fn deploy_mock_dao_staking(&mut self) -> &mut Self { - let dao_staking_addr = self.deploy_dao_staking(); - self.dao_staking_addr = Some(dao_staking_addr); + pub fn deploy_mock_governance(&mut self) -> &mut Self { + let governance_addr = self.deploy_governance(); + self.governance_addr = Some(governance_addr); self } - fn deploy_dao_staking(&mut self) -> Addr { - // Create a simple mock dao staking contract address - let addr = Addr::unchecked("mock-dao-staking"); - self.set_address(MarsAddressType::DaoStaking, addr.clone()); + fn deploy_governance(&mut self) -> Addr { + // Create a simple mock governance contract address + let addr = Addr::unchecked("mock-governance"); + self.set_address(MarsAddressType::Governance, addr.clone()); addr } } diff --git a/contracts/perps/tests/tests/test_accounting_with_discount.rs b/contracts/perps/tests/tests/test_accounting_with_discount.rs index 9eff6c8a..19b3560c 100644 --- a/contracts/perps/tests/tests/test_accounting_with_discount.rs +++ b/contracts/perps/tests/tests/test_accounting_with_discount.rs @@ -14,14 +14,13 @@ fn accounting_with_discount_fees() { let protocol_fee_rate = Decimal::percent(2); let mut mock = MockEnv::new().protocol_fee_rate(protocol_fee_rate).build().unwrap(); - // Set up dao staking after building - let dao_staking_addr = Addr::unchecked("mock-dao-staking"); - mock.set_dao_staking_address(&dao_staking_addr); + // Set up governance after building + let governance_addr = Addr::unchecked("mock-governance"); + mock.set_governance_address(&governance_addr); let owner = mock.owner.clone(); let credit_manager = mock.credit_manager.clone(); let user = "jake"; - // Fund credit manager and set up prices mock.fund_accounts(&[&credit_manager], 1_000_000_000_000_000u128, &["uosmo", "uatom", "uusdc"]); mock.set_price(&owner, "uusdc", Decimal::from_str("0.9").unwrap()).unwrap(); diff --git a/contracts/perps/tests/tests/test_position.rs b/contracts/perps/tests/tests/test_position.rs index cd9d190c..a0106c8d 100644 --- a/contracts/perps/tests/tests/test_position.rs +++ b/contracts/perps/tests/tests/test_position.rs @@ -2043,6 +2043,99 @@ fn query_position_fees( assert_eq!(position_fees, expected_fees); } +#[test] +fn query_opening_fee_with_voting_power_discounts() { + let mut mock = MockEnv::new().build().unwrap(); + + let owner = mock.owner.clone(); + let credit_manager = mock.credit_manager.clone(); + let user = "jake"; + + // set prices + mock.set_price(&owner, "uusdc", Decimal::from_str("0.9").unwrap()).unwrap(); + mock.set_price(&owner, "uosmo", Decimal::from_str("1.25").unwrap()).unwrap(); + + // credit manager is calling the perps contract, so we need to fund it + mock.fund_accounts(&[&credit_manager], 1_000_000_000_000_000u128, &["uosmo", "uusdc"]); + + // deposit some big number of uusdc to vault + mock.deposit_to_vault( + &credit_manager, + Some(user), + None, + &[coin(1_000_000_000_000u128, "uusdc")], + ) + .unwrap(); + + // init denoms + mock.update_perp_params( + &owner, + PerpParamsUpdate::AddOrUpdate { + params: PerpParams { + opening_fee_rate: Decimal::from_str("0.004").unwrap(), + closing_fee_rate: Decimal::from_str("0.006").unwrap(), + ..default_perp_params("uosmo") + }, + }, + ); + + // open a position to change skew + let size = Int128::from_str("10000").unwrap(); + let opening_fee = mock.query_opening_fee("uosmo", size, None).fee; + mock.execute_perp_order(&credit_manager, "2", "uosmo", size, None, &[opening_fee]).unwrap(); + + let test_size = Int128::from_str("2500").unwrap(); + + // Test different discount tiers + let test_cases = vec![ + (Decimal::percent(0), "Tier 1: 0% discount"), + (Decimal::percent(10), "Tier 2: 10% discount"), + (Decimal::percent(20), "Tier 3: 20% discount"), + (Decimal::percent(30), "Tier 4: 30% discount"), + (Decimal::percent(45), "Tier 5: 45% discount"), + ]; + + // Get base fee without discount + let base_fee = mock.query_opening_fee("uosmo", test_size, None).fee; + + for (discount_pct, description) in test_cases { + // Query fee with discount + let discounted_fee = mock.query_opening_fee("uosmo", test_size, Some(discount_pct)).fee; + + // Calculate expected fee with discount + let discount_multiplier = Decimal::one().checked_sub(discount_pct).unwrap(); + let expected_fee_amount = base_fee.amount.checked_mul_floor(discount_multiplier).unwrap(); + + // Verify discount is applied correctly + assert_eq!( + discounted_fee.amount, expected_fee_amount, + "{}: Expected fee {} but got {}", + description, expected_fee_amount, discounted_fee.amount + ); + + // Verify that higher discounts result in lower fees + if discount_pct > Decimal::percent(0) { + assert!( + discounted_fee.amount < base_fee.amount, + "{}: Discounted fee should be less than base fee", + description + ); + } + } + + // Test edge cases + // 100% discount should result in 0 fee + let zero_fee = mock.query_opening_fee("uosmo", test_size, Some(Decimal::percent(100))).fee; + assert_eq!(zero_fee.amount, Uint128::zero(), "100% discount should result in 0 fee"); + + // 0% discount should be same as no discount + let no_discount_fee = mock.query_opening_fee("uosmo", test_size, Some(Decimal::percent(0))).fee; + assert_eq!( + no_discount_fee.amount, base_fee.amount, + "0% discount should be same as no discount" + ); +} + #[test] fn random_user_cannot_close_all_positions() { let mut mock = MockEnv::new().build().unwrap(); diff --git a/packages/testing/Cargo.toml b/packages/testing/Cargo.toml index 373b0e14..92129c25 100644 --- a/packages/testing/Cargo.toml +++ b/packages/testing/Cargo.toml @@ -44,7 +44,7 @@ mars-mock-oracle = { workspace = true } mars-mock-pyth = { workspace = true } mars-mock-red-bank = { workspace = true } mars-mock-vault = { workspace = true } -mars-mock-dao-staking = { workspace = true } +mars-mock-governance = { workspace = true } mars-oracle-osmosis = { workspace = true } mars-oracle-wasm = { workspace = true } mars-owner = { workspace = true } diff --git a/packages/testing/src/multitest/helpers/contracts.rs b/packages/testing/src/multitest/helpers/contracts.rs index bc8832cd..b209f387 100644 --- a/packages/testing/src/multitest/helpers/contracts.rs +++ b/packages/testing/src/multitest/helpers/contracts.rs @@ -1,6 +1,5 @@ use cosmwasm_std::Empty; use cw_multi_test::{Contract, ContractWrapper}; -use mars_mock_dao_staking as _; // ensure dependency is linked pub fn mock_rover_contract() -> Box> { let contract = ContractWrapper::new( @@ -130,12 +129,12 @@ pub fn mock_perps_contract() -> Box> { Box::new(contract) } -pub fn mock_dao_staking_contract() -> Box> { +pub fn mock_governance_contract() -> Box> { let contract = ContractWrapper::new( - mars_mock_dao_staking::execute, - mars_mock_dao_staking::instantiate, - mars_mock_dao_staking::query, + mars_mock_governance::execute, + mars_mock_governance::instantiate, + mars_mock_governance::query, ) - .with_reply(mars_mock_dao_staking::reply); + .with_reply(mars_mock_governance::reply); Box::new(contract) } diff --git a/packages/testing/src/multitest/helpers/mock_env.rs b/packages/testing/src/multitest/helpers/mock_env.rs index fddcb9ec..4c3b1e81 100644 --- a/packages/testing/src/multitest/helpers/mock_env.rs +++ b/packages/testing/src/multitest/helpers/mock_env.rs @@ -13,7 +13,7 @@ use cw_vault_standard::{ extensions::lockup::{LockupQueryMsg, UnlockingPosition}, msg::{ExtensionQueryMsg, VaultStandardQueryMsg::VaultExtension}, }; -use mars_mock_dao_staking::ExecMsg as DaoStakingExecMsg; +use mars_mock_governance::ExecMsg as GovernanceExecMsg; use mars_mock_oracle::msg::{ CoinPrice, ExecuteMsg as OracleExecuteMsg, InstantiateMsg as OracleInstantiateMsg, }; @@ -28,7 +28,7 @@ use mars_types::{ }, adapters::{ account_nft::AccountNftUnchecked, - dao_staking::DaoStakingUnchecked, + governance::GovernanceUnchecked, health::HealthContract, incentives::{Incentives, IncentivesUnchecked}, oracle::{Oracle, OracleBase, OracleUnchecked}, @@ -90,7 +90,7 @@ use mars_zapper_mock::msg::{InstantiateMsg as ZapperInstantiateMsg, LpConfig}; use super::{ lp_token_info, mock_account_nft_contract, mock_address_provider_contract, - mock_astro_incentives_contract, mock_dao_staking_contract, mock_health_contract, + mock_astro_incentives_contract, mock_governance_contract, mock_health_contract, mock_incentives_contract, mock_managed_vault_contract, mock_oracle_contract, mock_params_contract, mock_perps_contract, mock_red_bank_contract, mock_rover_contract, mock_swapper_contract, mock_v2_zapper_contract, mock_vault_contract, AccountToFund, CoinInfo, @@ -141,7 +141,7 @@ pub struct MockEnvBuilder { pub perps_protocol_fee_ratio: Option, pub swap_fee: Option, pub fee_tier_config: Option, - pub dao_staking_addr: Option, + pub governance_addr: Option, } #[allow(clippy::new_ret_no_self)] @@ -177,7 +177,7 @@ impl MockEnv { perps_protocol_fee_ratio: None, swap_fee: None, fee_tier_config: None, - dao_staking_addr: None, + governance_addr: None, } } @@ -1208,12 +1208,12 @@ impl MockEnv { } pub fn set_voting_power(&mut self, user: &Addr, power: Uint128) { - let dao = self.query_address_provider(MarsAddressType::DaoStaking); + let governance = self.query_address_provider(MarsAddressType::Governance); self.app .execute_contract( Addr::unchecked("owner"), - dao, - &DaoStakingExecMsg::SetVotingPower { + governance, + &GovernanceExecMsg::SetVotingPower { address: user.to_string(), power, }, @@ -1241,7 +1241,7 @@ impl MockEnvBuilder { self.update_health_contract_config(&rover); self.deploy_nft_contract(&rover); - self.deploy_dao_staking(&rover); + self.deploy_governance(&rover); self.set_fee_tiers(&rover); if self.deploy_nft_contract && self.set_nft_contract_minter { @@ -1285,8 +1285,8 @@ impl MockEnvBuilder { self } - pub fn set_dao_staking_addr(mut self, addr: &Addr) -> Self { - self.dao_staking_addr = Some(addr.clone()); + pub fn set_governance_addr(mut self, addr: &Addr) -> Self { + self.governance_addr = Some(addr.clone()); self } @@ -1412,14 +1412,14 @@ impl MockEnvBuilder { let fee_tier_config = self.fee_tier_config.clone().unwrap_or(FeeTierConfig { tiers: vec![FeeTier { id: "tier_1".to_string(), - min_voting_power: "0".to_string(), + min_voting_power: Uint128::zero(), discount_pct: Decimal::percent(0), }], }); - let dao_staking_address = DaoStakingUnchecked::new( - self.dao_staking_addr + let governance = GovernanceUnchecked::new( + self.governance_addr .clone() - .unwrap_or_else(|| Addr::unchecked("mock-dao-staking")) + .unwrap_or_else(|| Addr::unchecked("mock-governance")) .to_string(), ); @@ -1448,7 +1448,7 @@ impl MockEnvBuilder { perps_liquidation_bonus_ratio, swap_fee, fee_tier_config, - dao_staking_address, + governance_address: governance, }, &[], "mock-rover-contract", @@ -1492,29 +1492,29 @@ impl MockEnvBuilder { .unwrap() } - fn deploy_dao_staking(&mut self, rover: &Addr) -> Addr { - let dao_addr = if let Some(addr) = self.dao_staking_addr.clone() { + fn deploy_governance(&mut self, rover: &Addr) -> Addr { + let governance_addr = if let Some(addr) = self.governance_addr.clone() { addr } else { - let code_id = self.app.store_code(mock_dao_staking_contract()); + let code_id = self.app.store_code(mock_governance_contract()); self.app - .instantiate_contract(code_id, self.get_owner(), &(), &[], "mock-dao-staking", None) + .instantiate_contract(code_id, self.get_owner(), &(), &[], "mock-governance", None) .unwrap() }; - // Register in address provider for queries that fetch DaoStaking via AP - self.set_address(MarsAddressType::DaoStaking, dao_addr.clone()); + // Register in address provider for queries that fetch Governance via AP + self.set_address(MarsAddressType::Governance, governance_addr.clone()); - // Update CM config with DAO staking address only + // Update CM config with governance address only self.update_config( rover, ConfigUpdates { - dao_staking_address: Some(DaoStakingUnchecked::new(dao_addr.to_string())), + governance_address: Some(GovernanceUnchecked::new(governance_addr.to_string())), ..Default::default() }, ); - dao_addr + governance_addr } fn set_fee_tiers(&mut self, rover: &Addr) { @@ -1523,42 +1523,42 @@ impl MockEnvBuilder { tiers: vec![ FeeTier { id: "tier_8".to_string(), - min_voting_power: "1500000000000".to_string(), // 1,500,000 MARS = 1,500,000,000,000 uMARS + min_voting_power: Uint128::new(1500000000000), // 1,500,000 MARS = 1,500,000,000,000 uMARS discount_pct: Decimal::percent(80), }, FeeTier { id: "tier_7".to_string(), - min_voting_power: "1000000000000".to_string(), // 1,000,000 MARS = 1,000,000,000,000 uMARS + min_voting_power: Uint128::new(1000000000000), // 1,000,000 MARS = 1,000,000,000,000 uMARS discount_pct: Decimal::percent(70), }, FeeTier { id: "tier_6".to_string(), - min_voting_power: "500000000000".to_string(), // 500,000 MARS + min_voting_power: Uint128::new(500000000000), // 500,000 MARS discount_pct: Decimal::percent(60), }, FeeTier { id: "tier_5".to_string(), - min_voting_power: "250000000000".to_string(), // 250,000 MARS + min_voting_power: Uint128::new(250000000000), // 250,000 MARS discount_pct: Decimal::percent(45), }, FeeTier { id: "tier_4".to_string(), - min_voting_power: "100000000000".to_string(), // 100,000 MARS + min_voting_power: Uint128::new(100000000000), // 100,000 MARS discount_pct: Decimal::percent(30), }, FeeTier { id: "tier_3".to_string(), - min_voting_power: "50000000000".to_string(), // 50,000 MARS + min_voting_power: Uint128::new(50000000000), // 50,000 MARS discount_pct: Decimal::percent(20), }, FeeTier { id: "tier_2".to_string(), - min_voting_power: "10000000000".to_string(), // 10,000 MARS + min_voting_power: Uint128::new(10000000000), // 10,000 MARS discount_pct: Decimal::percent(10), }, FeeTier { id: "tier_1".to_string(), - min_voting_power: "0".to_string(), + min_voting_power: Uint128::zero(), discount_pct: Decimal::percent(0), }, ], diff --git a/packages/types/src/adapters/dao_staking.rs b/packages/types/src/adapters/governance.rs similarity index 54% rename from packages/types/src/adapters/dao_staking.rs rename to packages/types/src/adapters/governance.rs index f9cb8f10..33918b66 100644 --- a/packages/types/src/adapters/dao_staking.rs +++ b/packages/types/src/adapters/governance.rs @@ -13,16 +13,16 @@ pub struct VotingPowerAtHeightResponse { } #[cw_serde] -pub enum DaoStakingQueryMsg { +pub enum GovernanceQueryMsg { VotingPowerAtHeight(VotingPowerAtHeightQuery), } #[cw_serde] -pub struct DaoStakingBase(T); +pub struct GovernanceBase(T); -impl DaoStakingBase { - pub fn new(address: T) -> DaoStakingBase { - DaoStakingBase(address) +impl GovernanceBase { + pub fn new(address: T) -> GovernanceBase { + GovernanceBase(address) } pub fn address(&self) -> &T { @@ -30,28 +30,28 @@ impl DaoStakingBase { } } -pub type DaoStakingUnchecked = DaoStakingBase; -pub type DaoStaking = DaoStakingBase; +pub type GovernanceUnchecked = GovernanceBase; +pub type Governance = GovernanceBase; -impl From for DaoStakingUnchecked { - fn from(dao_staking: DaoStaking) -> Self { - Self(dao_staking.address().to_string()) +impl From for GovernanceUnchecked { + fn from(governance: Governance) -> Self { + Self(governance.address().to_string()) } } -impl DaoStakingUnchecked { - pub fn check(&self, api: &dyn Api) -> StdResult { - Ok(DaoStakingBase::new(api.addr_validate(self.address())?)) +impl GovernanceUnchecked { + pub fn check(&self, api: &dyn Api) -> StdResult { + Ok(GovernanceBase::new(api.addr_validate(self.address())?)) } } -impl DaoStaking { +impl Governance { pub fn query_voting_power_at_height( &self, querier: &QuerierWrapper, address: &str, ) -> StdResult { - let query_msg = DaoStakingQueryMsg::VotingPowerAtHeight(VotingPowerAtHeightQuery { + let query_msg = GovernanceQueryMsg::VotingPowerAtHeight(VotingPowerAtHeightQuery { address: address.to_string(), }); diff --git a/packages/types/src/adapters/mod.rs b/packages/types/src/adapters/mod.rs index 88296c63..b535c9c6 100644 --- a/packages/types/src/adapters/mod.rs +++ b/packages/types/src/adapters/mod.rs @@ -1,6 +1,6 @@ pub mod account_nft; pub mod credit_manager; -pub mod dao_staking; +pub mod governance; pub mod health; pub mod incentives; pub mod oracle; diff --git a/packages/types/src/address_provider.rs b/packages/types/src/address_provider.rs index fefd4d03..73dd491d 100644 --- a/packages/types/src/address_provider.rs +++ b/packages/types/src/address_provider.rs @@ -45,8 +45,8 @@ pub enum MarsAddressType { Health, /// The address that shall receive the revenue share given to neutron (10%) RevenueShare, - /// Dao staking contract - DaoStaking, + /// Governance contract + Governance, } impl fmt::Display for MarsAddressType { @@ -67,7 +67,7 @@ impl fmt::Display for MarsAddressType { MarsAddressType::Perps => "perps", MarsAddressType::Health => "health", MarsAddressType::RevenueShare => "revenue_share", - MarsAddressType::DaoStaking => "dao_staking", + MarsAddressType::Governance => "governance", }; write!(f, "{s}") } @@ -93,7 +93,7 @@ impl FromStr for MarsAddressType { "perps" => Ok(MarsAddressType::Perps), "health" => Ok(MarsAddressType::Health), "revenue_share" => Ok(MarsAddressType::RevenueShare), - "dao_staking" => Ok(MarsAddressType::DaoStaking), + "governance" => Ok(MarsAddressType::Governance), _ => Err(StdError::parse_err(type_name::(), s)), } } diff --git a/packages/types/src/credit_manager/instantiate.rs b/packages/types/src/credit_manager/instantiate.rs index 8dea31f0..e190747a 100644 --- a/packages/types/src/credit_manager/instantiate.rs +++ b/packages/types/src/credit_manager/instantiate.rs @@ -4,7 +4,7 @@ use cosmwasm_std::{Decimal, Uint128}; use super::KeeperFeeConfig; use crate::{ adapters::{ - account_nft::AccountNftUnchecked, dao_staking::DaoStakingUnchecked, + account_nft::AccountNftUnchecked, governance::GovernanceUnchecked, health::HealthContractUnchecked, incentives::IncentivesUnchecked, oracle::OracleUnchecked, params::ParamsUnchecked, perps::PerpsUnchecked, red_bank::RedBankUnchecked, swapper::SwapperUnchecked, zapper::ZapperUnchecked, @@ -57,8 +57,8 @@ pub struct InstantiateMsg { pub swap_fee: Decimal, /// Configuration for fee tiers based on staking pub fee_tier_config: FeeTierConfig, - /// Address of the DAO staking contract - pub dao_staking_address: DaoStakingUnchecked, + /// Address of the governance contract + pub governance_address: GovernanceUnchecked, } /// Used when you want to update fields on Instantiate config @@ -85,5 +85,5 @@ pub struct ConfigUpdates { pub swap_fee: Option, // Staking-based fee tiers pub fee_tier_config: Option, - pub dao_staking_address: Option, + pub governance_address: Option, } diff --git a/packages/types/src/fee_tiers.rs b/packages/types/src/fee_tiers.rs index 7aa61471..82309310 100644 --- a/packages/types/src/fee_tiers.rs +++ b/packages/types/src/fee_tiers.rs @@ -1,11 +1,11 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::Decimal; +use cosmwasm_std::{Decimal, Uint128}; #[cw_serde] pub struct FeeTier { pub id: String, - pub min_voting_power: String, // Uint128 as string - pub discount_pct: Decimal, // Percentage as Decimal (e.g., 0.25 for 25%) + pub min_voting_power: Uint128, + pub discount_pct: Decimal, // Percentage as Decimal (e.g., 0.25 for 25%) } #[cw_serde] diff --git a/schemas/mars-address-provider/mars-address-provider.json b/schemas/mars-address-provider/mars-address-provider.json index 5bcb04fc..49ecc4be 100644 --- a/schemas/mars-address-provider/mars-address-provider.json +++ b/schemas/mars-address-provider/mars-address-provider.json @@ -145,10 +145,10 @@ ] }, { - "description": "Dao staking contract", + "description": "Governance contract", "type": "string", "enum": [ - "dao_staking" + "governance" ] } ] @@ -391,10 +391,10 @@ ] }, { - "description": "Dao staking contract", + "description": "Governance contract", "type": "string", "enum": [ - "dao_staking" + "governance" ] } ] @@ -505,10 +505,10 @@ ] }, { - "description": "Dao staking contract", + "description": "Governance contract", "type": "string", "enum": [ - "dao_staking" + "governance" ] } ] @@ -622,10 +622,10 @@ ] }, { - "description": "Dao staking contract", + "description": "Governance contract", "type": "string", "enum": [ - "dao_staking" + "governance" ] } ] @@ -739,10 +739,10 @@ ] }, { - "description": "Dao staking contract", + "description": "Governance contract", "type": "string", "enum": [ - "dao_staking" + "governance" ] } ] diff --git a/schemas/mars-credit-manager/mars-credit-manager.json b/schemas/mars-credit-manager/mars-credit-manager.json index f15b6dfc..fc5c9dd8 100644 --- a/schemas/mars-credit-manager/mars-credit-manager.json +++ b/schemas/mars-credit-manager/mars-credit-manager.json @@ -7,9 +7,9 @@ "title": "InstantiateMsg", "type": "object", "required": [ - "dao_staking_address", "duality_swapper", "fee_tier_config", + "governance_address", "health_contract", "incentives", "keeper_fee_config", @@ -26,14 +26,6 @@ "zapper" ], "properties": { - "dao_staking_address": { - "description": "Address of the DAO staking contract", - "allOf": [ - { - "$ref": "#/definitions/DaoStakingBase_for_String" - } - ] - }, "duality_swapper": { "description": "Helper contract for making swaps", "allOf": [ @@ -50,6 +42,14 @@ } ] }, + "governance_address": { + "description": "Address of the governance contract", + "allOf": [ + { + "$ref": "#/definitions/GovernanceBase_for_String" + } + ] + }, "health_contract": { "description": "Helper contract for calculating health factor", "allOf": [ @@ -174,9 +174,6 @@ } } }, - "DaoStakingBase_for_String": { - "type": "string" - }, "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" @@ -196,7 +193,7 @@ "type": "string" }, "min_voting_power": { - "type": "string" + "$ref": "#/definitions/Uint128" } }, "additionalProperties": false @@ -216,6 +213,9 @@ }, "additionalProperties": false }, + "GovernanceBase_for_String": { + "type": "string" + }, "HealthContractBase_for_String": { "type": "string" }, @@ -2486,30 +2486,30 @@ } ] }, - "dao_staking_address": { + "duality_swapper": { "anyOf": [ { - "$ref": "#/definitions/DaoStakingBase_for_String" + "$ref": "#/definitions/SwapperBase_for_String" }, { "type": "null" } ] }, - "duality_swapper": { + "fee_tier_config": { "anyOf": [ { - "$ref": "#/definitions/SwapperBase_for_String" + "$ref": "#/definitions/FeeTierConfig" }, { "type": "null" } ] }, - "fee_tier_config": { + "governance_address": { "anyOf": [ { - "$ref": "#/definitions/FeeTierConfig" + "$ref": "#/definitions/GovernanceBase_for_String" }, { "type": "null" @@ -2689,9 +2689,6 @@ } ] }, - "DaoStakingBase_for_String": { - "type": "string" - }, "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" @@ -2802,7 +2799,7 @@ "type": "string" }, "min_voting_power": { - "type": "string" + "$ref": "#/definitions/Uint128" } }, "additionalProperties": false @@ -2822,6 +2819,9 @@ }, "additionalProperties": false }, + "GovernanceBase_for_String": { + "type": "string" + }, "HealthContractBase_for_String": { "type": "string" }, diff --git a/scripts/deploy/base/deployer.ts b/scripts/deploy/base/deployer.ts index e2a8bd18..1cf3452f 100644 --- a/scripts/deploy/base/deployer.ts +++ b/scripts/deploy/base/deployer.ts @@ -211,7 +211,7 @@ export class Deployer { perps_liquidation_bonus_ratio: this.config.perpsLiquidationBonusRatio, swap_fee: this.config.swapFee, fee_tier_config: this.config.feeTierConfig, - dao_staking_address: this.config.daoStakingAddress, + governance_address: this.config.governance, } await this.instantiate('creditManager', this.storage.codeIds.creditManager!, msg) diff --git a/scripts/deploy/neutron/devnet-config.ts b/scripts/deploy/neutron/devnet-config.ts index b00287de..52fecadf 100644 --- a/scripts/deploy/neutron/devnet-config.ts +++ b/scripts/deploy/neutron/devnet-config.ts @@ -554,5 +554,5 @@ export const neutronDevnetConfig: DeploymentConfig = { }, ], }, - daoStakingAddress: 'neutron1pxjszcmmdxwtw9kv533u3hcudl6qahsa42chcs24gervf4ge40usaw3pcr', + governance: 'neutron1pxjszcmmdxwtw9kv533u3hcudl6qahsa42chcs24gervf4ge40usaw3pcr', } diff --git a/scripts/deploy/neutron/mainnet-config.ts b/scripts/deploy/neutron/mainnet-config.ts index a81f7d06..29ff387a 100644 --- a/scripts/deploy/neutron/mainnet-config.ts +++ b/scripts/deploy/neutron/mainnet-config.ts @@ -415,5 +415,5 @@ export const neutronMainnetConfig: DeploymentConfig = { feeTierConfig: { tiers: [], }, - daoStakingAddress: '', + governance: '', } diff --git a/scripts/deploy/neutron/testnet-config.ts b/scripts/deploy/neutron/testnet-config.ts index 5d58ae6b..8b7147ed 100644 --- a/scripts/deploy/neutron/testnet-config.ts +++ b/scripts/deploy/neutron/testnet-config.ts @@ -881,5 +881,5 @@ export const neutronTestnetConfig: DeploymentConfig = { feeTierConfig: { tiers: [], }, - daoStakingAddress: '', + governance: '', } diff --git a/scripts/deploy/osmosis/mainnet-config.ts b/scripts/deploy/osmosis/mainnet-config.ts index 65bd8edb..eabee4eb 100644 --- a/scripts/deploy/osmosis/mainnet-config.ts +++ b/scripts/deploy/osmosis/mainnet-config.ts @@ -1120,5 +1120,5 @@ export const osmosisMainnetConfig: DeploymentConfig = { feeTierConfig: { tiers: [], }, - daoStakingAddress: '', + governance: '', } diff --git a/scripts/deploy/osmosis/testnet-config.ts b/scripts/deploy/osmosis/testnet-config.ts index ae917e69..1ee5dbcc 100644 --- a/scripts/deploy/osmosis/testnet-config.ts +++ b/scripts/deploy/osmosis/testnet-config.ts @@ -312,5 +312,5 @@ export const osmosisTestnetConfig: DeploymentConfig = { feeTierConfig: { tiers: [], }, - daoStakingAddress: '', + governance: '', } diff --git a/scripts/types/config.ts b/scripts/types/config.ts index c2aced6e..48413b3c 100644 --- a/scripts/types/config.ts +++ b/scripts/types/config.ts @@ -140,7 +140,7 @@ export interface DeploymentConfig { perpsLiquidationBonusRatio: Decimal swapFee: Decimal feeTierConfig: FeeTierConfig - daoStakingAddress: string + governance: string } export interface AssetConfig { diff --git a/scripts/types/generated/mars-address-provider/MarsAddressProvider.types.ts b/scripts/types/generated/mars-address-provider/MarsAddressProvider.types.ts index 64358e5a..33455b2a 100644 --- a/scripts/types/generated/mars-address-provider/MarsAddressProvider.types.ts +++ b/scripts/types/generated/mars-address-provider/MarsAddressProvider.types.ts @@ -30,7 +30,7 @@ export type MarsAddressType = | 'perps' | 'health' | 'revenue_share' - | 'dao_staking' + | 'governance' export type OwnerUpdate = | { propose_new_owner: { diff --git a/scripts/types/generated/mars-credit-manager/MarsCreditManager.client.ts b/scripts/types/generated/mars-credit-manager/MarsCreditManager.client.ts index 0045f31e..513fa101 100644 --- a/scripts/types/generated/mars-credit-manager/MarsCreditManager.client.ts +++ b/scripts/types/generated/mars-credit-manager/MarsCreditManager.client.ts @@ -8,12 +8,12 @@ import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from '@cosmjs/cosmwasm-stargate' import { StdFee } from '@cosmjs/amino' import { - DaoStakingBaseForString, SwapperBaseForString, Decimal, + Uint128, + GovernanceBaseForString, HealthContractBaseForString, IncentivesUnchecked, - Uint128, OracleBaseForString, ParamsBaseForString, RedBankUnchecked, 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 abd21573..971adcf0 100644 --- a/scripts/types/generated/mars-credit-manager/MarsCreditManager.react-query.ts +++ b/scripts/types/generated/mars-credit-manager/MarsCreditManager.react-query.ts @@ -9,12 +9,12 @@ import { UseQueryOptions, useQuery, useMutation, UseMutationOptions } from '@tan import { ExecuteResult } from '@cosmjs/cosmwasm-stargate' import { StdFee } from '@cosmjs/amino' import { - DaoStakingBaseForString, SwapperBaseForString, Decimal, + Uint128, + GovernanceBaseForString, HealthContractBaseForString, IncentivesUnchecked, - Uint128, OracleBaseForString, ParamsBaseForString, RedBankUnchecked, diff --git a/scripts/types/generated/mars-credit-manager/MarsCreditManager.types.ts b/scripts/types/generated/mars-credit-manager/MarsCreditManager.types.ts index 4d37c3ab..85d5ca10 100644 --- a/scripts/types/generated/mars-credit-manager/MarsCreditManager.types.ts +++ b/scripts/types/generated/mars-credit-manager/MarsCreditManager.types.ts @@ -5,20 +5,20 @@ * and run the @cosmwasm/ts-codegen generate command to regenerate this file. */ -export type DaoStakingBaseForString = string export type SwapperBaseForString = string export type Decimal = string +export type Uint128 = string +export type GovernanceBaseForString = string export type HealthContractBaseForString = string export type IncentivesUnchecked = string -export type Uint128 = string export type OracleBaseForString = string export type ParamsBaseForString = string export type RedBankUnchecked = string export type ZapperBaseForString = string export interface InstantiateMsg { - dao_staking_address: DaoStakingBaseForString duality_swapper: SwapperBaseForString fee_tier_config: FeeTierConfig + governance_address: GovernanceBaseForString health_contract: HealthContractBaseForString incentives: IncentivesUnchecked keeper_fee_config: KeeperFeeConfig @@ -40,7 +40,7 @@ export interface FeeTierConfig { export interface FeeTier { discount_pct: Decimal id: string - min_voting_power: string + min_voting_power: Uint128 } export interface KeeperFeeConfig { min_fee: Coin @@ -630,9 +630,9 @@ export interface OsmoSwap { } export interface ConfigUpdates { account_nft?: AccountNftBaseForString | null - dao_staking_address?: DaoStakingBaseForString | null duality_swapper?: SwapperBaseForString | null fee_tier_config?: FeeTierConfig | null + governance_address?: GovernanceBaseForString | null health_contract?: HealthContractBaseForString | null incentives?: IncentivesUnchecked | null keeper_fee_config?: KeeperFeeConfig | null From dbf7395d645d5f706276691b78a49970353d02a3 Mon Sep 17 00:00:00 2001 From: Subham2804 Date: Tue, 16 Sep 2025 13:02:06 +0530 Subject: [PATCH 10/16] update fmt and add swap account tests --- contracts/credit-manager/src/perp.rs | 4 - .../tests/tests/test_swap_with_discount.rs | 74 +++++++++++++++++++ contracts/perps/src/position_management.rs | 3 + contracts/perps/src/query.rs | 26 ++----- 4 files changed, 83 insertions(+), 24 deletions(-) diff --git a/contracts/credit-manager/src/perp.rs b/contracts/credit-manager/src/perp.rs index 1342474d..e23e37bc 100644 --- a/contracts/credit-manager/src/perp.rs +++ b/contracts/credit-manager/src/perp.rs @@ -272,9 +272,6 @@ fn modify_existing_position( let (funds, response) = update_state_based_on_pnl(&mut deps, account_id, pnl, None, response)?; let funds = funds.map_or_else(Vec::new, |c| vec![c]); - // Get effective fee - let effective_opening_fee = - perps.query_opening_fee(&deps.querier, denom, order_size, Some(discount_pct))?; let msg = perps.execute_perp_order( account_id, denom, @@ -301,7 +298,6 @@ fn modify_existing_position( .add_attribute("reduce_only", reduce_only.unwrap_or(false).to_string()) .add_attribute("order_size", order_size.to_string()) .add_attribute("new_size", new_size.to_string()) - .add_attribute("effective_opening_fee", effective_opening_fee.fee.to_string()) .add_attribute("discount_pct", discount_pct.to_string()) .add_attribute("tier_id", tier)) } diff --git a/contracts/credit-manager/tests/tests/test_swap_with_discount.rs b/contracts/credit-manager/tests/tests/test_swap_with_discount.rs index fdb08169..57ce0c07 100644 --- a/contracts/credit-manager/tests/tests/test_swap_with_discount.rs +++ b/contracts/credit-manager/tests/tests/test_swap_with_discount.rs @@ -97,6 +97,30 @@ fn test_swap_with_discount() { assert_eq!(base_fee, Decimal::percent(1)); // 1% base fee assert_eq!(effective_fee, Decimal::percent(1)); // No discount applied + // Verify account balances for tier 1 (no discount) + let positions = mock.query_positions(&account_id); + assert_eq!(positions.deposits.len(), 1); + let osmo_deposit = positions.deposits.iter().find(|d| d.denom == "uosmo").unwrap(); + assert!(osmo_deposit.amount > Uint128::zero()); + + let atom_deposit = positions.deposits.iter().find(|d| d.denom == "uatom"); + assert!(atom_deposit.is_none()); + + // Verify Credit Manager contract balances + let rover_atom_balance = mock.query_balance(&mock.rover, "uatom"); + // Rover retains the effective swap fee in input denom (1% of 10,000 = 100) + assert_eq!(rover_atom_balance.amount, Uint128::new(100)); + + let rover_osmo_balance = mock.query_balance(&mock.rover, "uosmo"); + assert_eq!(rover_osmo_balance.amount, osmo_deposit.amount); + + // Verify user wallet balances - check that swap amount is correctly debited + let user_atom_balance = mock.query_balance(&user, "uatom"); + assert_eq!(user_atom_balance.amount, Uint128::new(30_000 - 10_000)); // 20,000 ATOM remaining (10,000 debited for swap) + + let user_osmo_balance = mock.query_balance(&user, "uosmo"); + assert_eq!(user_osmo_balance.amount, Uint128::zero()); + // Tier 2 (>= 10_000 MARS) → 10% discount mock.set_voting_power(&user, Uint128::new(10_000_000_000)); let res = do_swap(&mut mock, &account_id, &user, 10_000, "uatom", "uosmo"); @@ -110,6 +134,30 @@ fn test_swap_with_discount() { assert_eq!(base_fee, Decimal::percent(1)); // 1% base fee assert_eq!(effective_fee, Decimal::percent(1) * (Decimal::one() - Decimal::percent(10))); // 0.9% effective fee + // Verify account balances for tier 2 (10% discount) + let positions = mock.query_positions(&account_id); + assert_eq!(positions.deposits.len(), 1); + let osmo_deposit = positions.deposits.iter().find(|d| d.denom == "uosmo").unwrap(); + assert!(osmo_deposit.amount > Uint128::zero()); + + let atom_deposit = positions.deposits.iter().find(|d| d.denom == "uatom"); + assert!(atom_deposit.is_none()); + + // Verify Credit Manager contract balances + let rover_atom_balance = mock.query_balance(&mock.rover, "uatom"); + // Cumulative retained fee: 100 (first) + 90 (second with 0.9%) = 190 + assert_eq!(rover_atom_balance.amount, Uint128::new(190)); + + let rover_osmo_balance = mock.query_balance(&mock.rover, "uosmo"); + assert_eq!(rover_osmo_balance.amount, osmo_deposit.amount); + + // Verify user wallet balances - check that swap amount is correctly debited + let user_atom_balance = mock.query_balance(&user, "uatom"); + assert_eq!(user_atom_balance.amount, Uint128::new(30_000 - 20_000)); // 10,000 ATOM remaining (20,000 debited for 2 swaps) + + let user_osmo_balance = mock.query_balance(&user, "uosmo"); + assert_eq!(user_osmo_balance.amount, Uint128::zero()); + // Tier 5 (>= 250_000 MARS) → 45% discount mock.set_voting_power(&user, Uint128::new(250_000_000_000)); let res = do_swap(&mut mock, &account_id, &user, 10_000, "uatom", "uosmo"); @@ -123,6 +171,32 @@ fn test_swap_with_discount() { assert_eq!(base_fee, Decimal::percent(1)); // 1% base fee assert_eq!(effective_fee, Decimal::percent(1) * (Decimal::one() - Decimal::percent(45))); // 0.55% effective fee + // Verify account balances to ensure internal accounting is correct + let positions = mock.query_positions(&account_id); + assert_eq!(positions.deposits.len(), 1); + let osmo_deposit = positions.deposits.iter().find(|d| d.denom == "uosmo").unwrap(); + // Should have received OSMO from the swap (minus fees) + assert!(osmo_deposit.amount > Uint128::zero()); + + // Verify no ATOM remains in account (it was swapped) + let atom_deposit = positions.deposits.iter().find(|d| d.denom == "uatom"); + assert!(atom_deposit.is_none()); + + // Verify Credit Manager contract balances + let rover_atom_balance = mock.query_balance(&mock.rover, "uatom"); + // Cumulative retained fee: 190 + 55 (third with 0.55%) = 245 + assert_eq!(rover_atom_balance.amount, Uint128::new(245)); + + let rover_osmo_balance = mock.query_balance(&mock.rover, "uosmo"); + assert_eq!(rover_osmo_balance.amount, osmo_deposit.amount); // OSMO should match account deposit + + // Verify user wallet balances - check that swap amount is correctly debited + let user_atom_balance = mock.query_balance(&user, "uatom"); + assert_eq!(user_atom_balance.amount, Uint128::new(30_000 - 30_000)); // 0 ATOM remaining (30,000 debited for 3 swaps) + + let user_osmo_balance = mock.query_balance(&user, "uosmo"); + assert_eq!(user_osmo_balance.amount, Uint128::zero()); // No OSMO in user wallet + assert!(res .events .iter() diff --git a/contracts/perps/src/position_management.rs b/contracts/perps/src/position_management.rs index 064d4cc9..4ca47091 100644 --- a/contracts/perps/src/position_management.rs +++ b/contracts/perps/src/position_management.rs @@ -366,7 +366,9 @@ fn modify_position( // Convert PnL amounts to coins let pnl = pnl_amounts.to_coins(&cfg.base_denom).pnl; + let mut msgs = vec![]; + // Apply the payment to the credit manager if necessary apply_payment_to_cm_if_needed(&cfg, cm_address, &mut msgs, paid_amount, &pnl)?; @@ -387,6 +389,7 @@ fn modify_position( entry_accrued_funding_per_unit_in_base_denom, &pnl_amounts, ); + // Update the realized PnL, market state, and total cash flow based on the new amounts apply_pnl_and_fees( &cfg, diff --git a/contracts/perps/src/query.rs b/contracts/perps/src/query.rs index 52ff6d1a..482abfd1 100644 --- a/contracts/perps/src/query.rs +++ b/contracts/perps/src/query.rs @@ -313,19 +313,9 @@ pub fn query_position( // Query the credit manager to get the discount for this account via adapter let discount_pct = credit_manager_adapter.query_discount_pct(&deps.querier, &account_id)?; - - // Get the effective opening fee rate (volume-weighted average or fallback) - // let fallback_opening_fee_rate = perp_params.opening_fee_rate * (Decimal::one() - discount_pct); - // let discounted_opening_fee_rate = get_effective_opening_fee_rate( - // deps.storage, - // &account_id, - // &denom, - // fallback_opening_fee_rate, - // )?; - - // Use current discount for closing fee rate (fair for current operations) let (discounted_opening_fee_rate, discounted_closing_fee_rate) = compute_discounted_fee_rates(&perp_params, Some(discount_pct))?; + let pnl_amounts = position.compute_pnl( &curr_funding, ms.skew()?, @@ -419,11 +409,9 @@ pub fn query_positions( // Query the credit manager to get the discount for this account via adapter let discount_pct = credit_manager_adapter.query_discount_pct(&deps.querier, &account_id)?; - - // Check if we have a stored opening fee rate for this position - let (discounted_opening_fee_rate, discounted_closing_fee_rate) = compute_discounted_fee_rates(&perp_params, Some(discount_pct))?; + let pnl_amounts = position.compute_pnl( &funding, skew, @@ -507,8 +495,7 @@ pub fn query_positions_by_account( let ms = MARKET_STATES.load(deps.storage, &denom)?; let curr_funding = ms.current_funding(current_time, denom_price, base_denom_price)?; - // Check if we have a stored opening fee rate for this position - + // Query the credit manager to get the discount for this account via adapter let discount_pct = credit_manager.query_discount_pct(&deps.querier, &account_id)?; let (opening_fee_rate, closing_fee_rate) = compute_discounted_fee_rates(&perp_params, Some(discount_pct))?; @@ -659,11 +646,12 @@ pub fn query_opening_fee( let perp_params = params.query_perp_params(&deps.querier, denom)?; // Apply discount to fee rates if provided - let (opening_fee_rate, _) = compute_discounted_fee_rates(&perp_params, discount_pct)?; + let (opening_fee_rate, closing_fee_rate) = + compute_discounted_fee_rates(&perp_params, discount_pct)?; let fees = PositionModification::Increase(size).compute_fees( opening_fee_rate, - perp_params.closing_fee_rate, + closing_fee_rate, denom_price, base_denom_price, ms.skew()?, @@ -749,8 +737,6 @@ pub fn query_position_fees( }; // Query the credit manager to get the discount for this account via adapter let discount_pct = credit_manager_adapter.query_discount_pct(&deps.querier, account_id)?; - - // Apply discount to fee rates using the helper function let (discounted_opening_fee_rate, discounted_closing_fee_rate) = compute_discounted_fee_rates(&perp_params, Some(discount_pct))?; From bbb7b09b5e6d038642723f9af9142b29e418ede0 Mon Sep 17 00:00:00 2001 From: Subham2804 Date: Tue, 16 Sep 2025 14:47:08 +0530 Subject: [PATCH 11/16] update account tests --- .../tests/tests/test_perps_with_discount.rs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/contracts/credit-manager/tests/tests/test_perps_with_discount.rs b/contracts/credit-manager/tests/tests/test_perps_with_discount.rs index 57d57685..c7567fa2 100644 --- a/contracts/credit-manager/tests/tests/test_perps_with_discount.rs +++ b/contracts/credit-manager/tests/tests/test_perps_with_discount.rs @@ -494,6 +494,17 @@ fn test_perp_position_modification_with_tier_changes() { .unwrap(); let initial_vault_balance = mock.query_balance(mock.perps.address(), "uusdc").amount; + let query_market_acct = |mock: &MockEnv| -> mars_types::perps::AccountingResponse { + mock.app + .wrap() + .query_wasm_smart( + mock.perps.address(), + &mars_types::perps::QueryMsg::MarketAccounting { + denom: atom.denom.clone(), + }, + ) + .unwrap() + }; // Step 1: Open initial position with Tier 1 (0% discount) mock.set_voting_power(&user, Uint128::new(0)); @@ -531,6 +542,15 @@ fn test_perp_position_modification_with_tier_changes() { let vault_balance_1 = assert_vault_balance_increase(&mock, initial_vault_balance, opening_fee_1); + // Market accounting after step 1 + let acct_after_1 = query_market_acct(&mock); + assert_eq!( + acct_after_1.accounting.cash_flow.opening_fee, + Int128::try_from(opening_fee_1).unwrap() + ); + assert_eq!(acct_after_1.accounting.cash_flow.price_pnl, Int128::zero()); + assert_eq!(acct_after_1.accounting.cash_flow.accrued_funding, Int128::zero()); + // Step 2: Increase position with Tier 2 (10% discount) mock.set_voting_power(&user, Uint128::new(10_000_000_000)); // 10,000 MARS @@ -567,6 +587,16 @@ fn test_perp_position_modification_with_tier_changes() { assert_position_size(&mock, &account_id, &atom.denom, expected_total_size); let vault_balance_2 = assert_vault_balance_increase(&mock, vault_balance_1, opening_fee_2); + // Market accounting after step 2 (opening fees accumulate) + let acct_after_2 = query_market_acct(&mock); + let expected_opening_fees_1_2 = opening_fee_1 + opening_fee_2; + assert_eq!( + acct_after_2.accounting.cash_flow.opening_fee, + Int128::try_from(expected_opening_fees_1_2).unwrap() + ); + assert_eq!(acct_after_2.accounting.cash_flow.price_pnl, Int128::zero()); + assert_eq!(acct_after_2.accounting.cash_flow.accrued_funding, Int128::zero()); + // Step 3: Increase position with Tier 5 (45% discount) mock.set_voting_power(&user, Uint128::new(250_000_000_000)); // 250,000 MARS @@ -603,6 +633,16 @@ fn test_perp_position_modification_with_tier_changes() { assert_position_size(&mock, &account_id, &atom.denom, final_expected_size); let vault_balance_3 = assert_vault_balance_increase(&mock, vault_balance_2, opening_fee_3); + // Market accounting after step 3 (opening fees accumulate) + let acct_after_3 = query_market_acct(&mock); + let expected_opening_fees_total = opening_fee_1 + opening_fee_2 + opening_fee_3; + assert_eq!( + acct_after_3.accounting.cash_flow.opening_fee, + Int128::try_from(expected_opening_fees_total).unwrap() + ); + assert_eq!(acct_after_3.accounting.cash_flow.price_pnl, Int128::zero()); + assert_eq!(acct_after_3.accounting.cash_flow.accrued_funding, Int128::zero()); + // Step 4: Validate total fees collected and user balance changes let total_fees_collected = vault_balance_3 - initial_vault_balance; let expected_total_fees = opening_fee_1 + opening_fee_2 + opening_fee_3; @@ -613,6 +653,19 @@ fn test_perp_position_modification_with_tier_changes() { assert_eq!(total_user_balance_decrease, expected_total_fees); // Step 5: Close position with current tier (Tier 5) + // Pre-calc expected closing fee from query + let closing_fee_estimate: mars_types::perps::PositionFeesResponse = mock + .app + .wrap() + .query_wasm_smart( + mock.perps.address(), + &mars_types::perps::QueryMsg::PositionFees { + account_id: account_id.clone(), + denom: atom.denom.clone(), + new_size: Int128::zero(), + }, + ) + .unwrap(); let close_res = mock .update_credit_account( &account_id, @@ -632,6 +685,18 @@ fn test_perp_position_modification_with_tier_changes() { // Verify position is closed let position_after_close = mock.query_perp_position(&account_id, &atom.denom); assert!(position_after_close.position.is_none()); + + // Market accounting after close: opening fees + closing fee must be realized, no unrealized pnl + let acct_after_close = query_market_acct(&mock); + let expected_opening_total_i = Int128::try_from(expected_total_fees).unwrap(); + assert_eq!(acct_after_close.accounting.cash_flow.opening_fee, expected_opening_total_i); + assert_eq!(acct_after_close.accounting.cash_flow.price_pnl, Int128::zero()); + assert_eq!(acct_after_close.accounting.cash_flow.accrued_funding, Int128::zero()); + // closing fee equals estimate + assert_eq!( + acct_after_close.accounting.cash_flow.closing_fee, + Int128::try_from(closing_fee_estimate.closing_fee).unwrap() + ); } #[test] From 0c6410c1a4f89e5d4426688d4d09ac3bf4b78e79 Mon Sep 17 00:00:00 2001 From: Subham2804 Date: Wed, 24 Sep 2025 12:53:38 +0530 Subject: [PATCH 12/16] add fee tier config query --- .../tests/tests/helpers/mock_env_builder.rs | 1 + contracts/credit-manager/src/contract.rs | 5 +- contracts/credit-manager/src/query.rs | 47 +++-- .../tests/tests/test_trading_fee.rs | 95 ++++++---- .../tests/tests/helpers/mock_env_builder.rs | 1 + packages/types/src/credit_manager/query.rs | 36 +++- .../mars-credit-manager.json | 176 ++++++++++++++++-- scripts/health/pkg-web/index_bg.wasm | Bin 462566 -> 462566 bytes .../MarsCreditManager.client.ts | 10 + .../MarsCreditManager.react-query.ts | 26 +++ .../MarsCreditManager.types.ts | 24 ++- 11 files changed, 340 insertions(+), 81 deletions(-) diff --git a/contracts/account-nft/tests/tests/helpers/mock_env_builder.rs b/contracts/account-nft/tests/tests/helpers/mock_env_builder.rs index 23d87265..054b98b1 100644 --- a/contracts/account-nft/tests/tests/helpers/mock_env_builder.rs +++ b/contracts/account-nft/tests/tests/helpers/mock_env_builder.rs @@ -161,6 +161,7 @@ impl MockEnvBuilder { perps: "n/a".to_string(), keeper_fee_config: Default::default(), perps_liquidation_bonus_ratio: Decimal::percent(60), + governance: "n/a".to_string(), }, }, &[], diff --git a/contracts/credit-manager/src/contract.rs b/contracts/credit-manager/src/contract.rs index 797e29ef..7c724044 100644 --- a/contracts/credit-manager/src/contract.rs +++ b/contracts/credit-manager/src/contract.rs @@ -18,8 +18,8 @@ use crate::{ query_account_tier_and_discount, query_accounts, query_all_coin_balances, query_all_debt_shares, query_all_total_debt_shares, query_all_trigger_orders, query_all_trigger_orders_for_account, query_all_vault_positions, - query_all_vault_utilizations, query_config, query_positions, query_swap_fee, - query_total_debt_shares, query_trading_fee, query_vault_bindings, + query_all_vault_utilizations, query_config, query_fee_tier_config, query_positions, + query_swap_fee, query_total_debt_shares, query_trading_fee, query_vault_bindings, query_vault_position_value, query_vault_utilization, }, repay::repay_from_wallet, @@ -181,6 +181,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> ContractResult { market_type, } => to_json_binary(&query_trading_fee(deps, &account_id, &market_type)?), QueryMsg::SwapFeeRate {} => to_json_binary(&query_swap_fee(deps)?), + QueryMsg::FeeTierConfig {} => to_json_binary(&query_fee_tier_config(deps)?), }; res.map_err(Into::into) } diff --git a/contracts/credit-manager/src/query.rs b/contracts/credit-manager/src/query.rs index c957e5e6..5ed9ea8a 100644 --- a/contracts/credit-manager/src/query.rs +++ b/contracts/credit-manager/src/query.rs @@ -8,21 +8,23 @@ use mars_types::{ adapters::vault::{Vault, VaultBase, VaultPosition, VaultPositionValue, VaultUnchecked}, credit_manager::{ Account, AccountTierAndDiscountResponse, CoinBalanceResponseItem, ConfigResponse, - DebtAmount, DebtShares, MarketType, Positions, SharesResponseItem, TradingFeeResponse, - TriggerOrderResponse, VaultBinding, VaultPositionResponseItem, VaultUtilizationResponse, + DebtAmount, DebtShares, FeeTierConfigResponse, PerpTradingFeeResponse, Positions, + SharesResponseItem, SpotTradingFeeResponse, TradingFeeResponse, TriggerOrderResponse, + VaultBinding, VaultPositionResponseItem, VaultUtilizationResponse, }, health::AccountKind, oracle::ActionKind, + perps::MarketType, }; use crate::{ error::ContractResult, staking::get_account_tier_and_discount, state::{ - ACCOUNT_KINDS, ACCOUNT_NFT, COIN_BALANCES, DEBT_SHARES, HEALTH_CONTRACT, INCENTIVES, - KEEPER_FEE_CONFIG, MAX_SLIPPAGE, MAX_UNLOCKING_POSITIONS, ORACLE, OWNER, PARAMS, PERPS, - PERPS_LB_RATIO, RED_BANK, REWARDS_COLLECTOR, SWAPPER, SWAP_FEE, TOTAL_DEBT_SHARES, - TRIGGER_ORDERS, VAULTS, VAULT_POSITIONS, ZAPPER, + ACCOUNT_KINDS, ACCOUNT_NFT, COIN_BALANCES, DEBT_SHARES, FEE_TIER_CONFIG, GOVERNANCE, + HEALTH_CONTRACT, INCENTIVES, KEEPER_FEE_CONFIG, MAX_SLIPPAGE, MAX_UNLOCKING_POSITIONS, + ORACLE, OWNER, PARAMS, PERPS, PERPS_LB_RATIO, RED_BANK, REWARDS_COLLECTOR, SWAPPER, + SWAP_FEE, TOTAL_DEBT_SHARES, TRIGGER_ORDERS, VAULTS, VAULT_POSITIONS, ZAPPER, }, utils::debt_shares_to_amount, vault::vault_utilization_in_deposit_cap_denom, @@ -68,6 +70,7 @@ pub fn query_config(deps: Deps) -> ContractResult { rewards_collector: REWARDS_COLLECTOR.may_load(deps.storage)?, keeper_fee_config: KEEPER_FEE_CONFIG.load(deps.storage)?, perps_liquidation_bonus_ratio: PERPS_LB_RATIO.load(deps.storage)?, + governance: GOVERNANCE.load(deps.storage)?.into(), }) } @@ -378,29 +381,41 @@ pub fn query_trading_fee( let effective_fee_pct = base_fee_pct.checked_mul(cosmwasm_std::Decimal::one() - discount_pct)?; - Ok(TradingFeeResponse { + Ok(TradingFeeResponse::Spot(SpotTradingFeeResponse { base_fee_pct, discount_pct, effective_fee_pct, tier_id: tier.id, - }) + })) } MarketType::Perp { denom, } => { let params = PARAMS.load(deps.storage)?; - let perp_params = params.query_perp_params(&deps.querier, denom)?; - let base_fee_pct = perp_params.opening_fee_rate; - let effective_fee_pct = - base_fee_pct.checked_mul(cosmwasm_std::Decimal::one() - discount_pct)?; - Ok(TradingFeeResponse { - base_fee_pct, + let opening_fee_pct = perp_params.opening_fee_rate; + let closing_fee_pct = perp_params.closing_fee_rate; + + let effective_opening_fee_pct = + opening_fee_pct.checked_mul(cosmwasm_std::Decimal::one() - discount_pct)?; + let effective_closing_fee_pct = + closing_fee_pct.checked_mul(cosmwasm_std::Decimal::one() - discount_pct)?; + + Ok(TradingFeeResponse::Perp(PerpTradingFeeResponse { + opening_fee_pct, + closing_fee_pct, discount_pct, - effective_fee_pct, + effective_opening_fee_pct, + effective_closing_fee_pct, tier_id: tier.id, - }) + })) } } } + +pub fn query_fee_tier_config(deps: Deps) -> ContractResult { + Ok(FeeTierConfigResponse { + fee_tier_config: FEE_TIER_CONFIG.load(deps.storage)?, + }) +} diff --git a/contracts/credit-manager/tests/tests/test_trading_fee.rs b/contracts/credit-manager/tests/tests/test_trading_fee.rs index af14edd8..a5fe2d11 100644 --- a/contracts/credit-manager/tests/tests/test_trading_fee.rs +++ b/contracts/credit-manager/tests/tests/test_trading_fee.rs @@ -1,8 +1,5 @@ use cosmwasm_std::{Addr, Decimal, Uint128}; -use mars_types::{ - credit_manager::{MarketType, TradingFeeResponse}, - params::PerpParamsUpdate, -}; +use mars_types::{credit_manager::TradingFeeResponse, params::PerpParamsUpdate, perps::MarketType}; use test_case::test_case; use super::helpers::{default_perp_params, uosmo_info, MockEnv}; @@ -58,14 +55,19 @@ fn test_trading_fee_query_spot( .unwrap(); // Verify the response - assert_eq!(response.base_fee_pct, expected_base_fee); - assert_eq!(response.discount_pct, expected_discount); - - // Calculate the expected effective fee based on the actual response - let calculated_effective = - response.base_fee_pct.checked_mul(Decimal::one() - expected_discount).unwrap(); - assert_eq!(response.effective_fee_pct, calculated_effective); - assert_eq!(response.tier_id, expected_tier_id); + match response { + TradingFeeResponse::Spot(spot_response) => { + assert_eq!(spot_response.base_fee_pct, expected_base_fee); + assert_eq!(spot_response.discount_pct, expected_discount); + + // Calculate the expected effective fee based on the actual response + let calculated_effective = + spot_response.base_fee_pct.checked_mul(Decimal::one() - expected_discount).unwrap(); + assert_eq!(spot_response.effective_fee_pct, calculated_effective); + assert_eq!(spot_response.tier_id, expected_tier_id); + } + _ => panic!("Expected Spot response"), + } } #[test_case( @@ -126,13 +128,26 @@ fn test_trading_fee_query_perp( .unwrap(); // Verify the response - assert_eq!(response.discount_pct, expected_discount); - assert_eq!(response.tier_id, expected_tier_id); - - // The effective fee should be base_fee * (1 - discount) - let expected_effective = - response.base_fee_pct.checked_mul(Decimal::one() - expected_discount).unwrap(); - assert_eq!(response.effective_fee_pct, expected_effective); + match response { + TradingFeeResponse::Perp(perp_response) => { + assert_eq!(perp_response.discount_pct, expected_discount); + assert_eq!(perp_response.tier_id, expected_tier_id); + + // The effective fees should be base_fee * (1 - discount) + let expected_opening_effective = perp_response + .opening_fee_pct + .checked_mul(Decimal::one() - expected_discount) + .unwrap(); + let expected_closing_effective = perp_response + .closing_fee_pct + .checked_mul(Decimal::one() - expected_discount) + .unwrap(); + + assert_eq!(perp_response.effective_opening_fee_pct, expected_opening_effective); + assert_eq!(perp_response.effective_closing_fee_pct, expected_closing_effective); + } + _ => panic!("Expected Perp response"), + } } #[test] @@ -159,13 +174,20 @@ fn test_trading_fee_query_edge_cases() { ) .unwrap(); - assert_eq!(response.tier_id, "tier_8"); - assert_eq!(response.discount_pct, Decimal::percent(80)); - - // Calculate the expected effective fee based on the actual response - let calculated_effective = - response.base_fee_pct.checked_mul(Decimal::one() - Decimal::percent(80)).unwrap(); - assert_eq!(response.effective_fee_pct, calculated_effective); + match response { + TradingFeeResponse::Spot(spot_response) => { + assert_eq!(spot_response.tier_id, "tier_8"); + assert_eq!(spot_response.discount_pct, Decimal::percent(80)); + + // Calculate the expected effective fee based on the actual response + let calculated_effective = spot_response + .base_fee_pct + .checked_mul(Decimal::one() - Decimal::percent(80)) + .unwrap(); + assert_eq!(spot_response.effective_fee_pct, calculated_effective); + } + _ => panic!("Expected Spot response"), + } // Test tier 1 (no discount - 0%) mock.set_voting_power(&user, Uint128::new(0)); @@ -182,11 +204,18 @@ fn test_trading_fee_query_edge_cases() { ) .unwrap(); - assert_eq!(response.tier_id, "tier_1"); - assert_eq!(response.discount_pct, Decimal::percent(0)); - - // Calculate the expected effective fee based on the actual response - let calculated_effective = - response.base_fee_pct.checked_mul(Decimal::one() - Decimal::percent(0)).unwrap(); - assert_eq!(response.effective_fee_pct, calculated_effective); + match response { + TradingFeeResponse::Spot(spot_response) => { + assert_eq!(spot_response.tier_id, "tier_1"); + assert_eq!(spot_response.discount_pct, Decimal::percent(0)); + + // Calculate the expected effective fee based on the actual response + let calculated_effective = spot_response + .base_fee_pct + .checked_mul(Decimal::one() - Decimal::percent(0)) + .unwrap(); + assert_eq!(spot_response.effective_fee_pct, calculated_effective); + } + _ => panic!("Expected Spot response"), + } } diff --git a/contracts/health/tests/tests/helpers/mock_env_builder.rs b/contracts/health/tests/tests/helpers/mock_env_builder.rs index 96d7c0f7..bab04e2a 100644 --- a/contracts/health/tests/tests/helpers/mock_env_builder.rs +++ b/contracts/health/tests/tests/helpers/mock_env_builder.rs @@ -188,6 +188,7 @@ impl MockEnvBuilder { perps: "n/a".to_string(), keeper_fee_config: Default::default(), perps_liquidation_bonus_ratio: Decimal::percent(60), + governance: "n/a".to_string(), }, }, &[], diff --git a/packages/types/src/credit_manager/query.rs b/packages/types/src/credit_manager/query.rs index 534b5719..35c85444 100644 --- a/packages/types/src/credit_manager/query.rs +++ b/packages/types/src/credit_manager/query.rs @@ -8,9 +8,10 @@ use crate::{ rewards_collector::RewardsCollector, vault::{Vault, VaultPosition, VaultUnchecked}, }, + fee_tiers::FeeTierConfig, health::AccountKind, oracle::ActionKind, - perps::PerpPosition, + perps::{MarketType, PerpPosition}, traits::Coins, }; @@ -130,24 +131,35 @@ pub enum QueryMsg { #[returns(Decimal)] SwapFeeRate {}, -} -#[cw_serde] -pub enum MarketType { - Spot, - Perp { - denom: String, - }, + #[returns(FeeTierConfigResponse)] + FeeTierConfig {}, } #[cw_serde] -pub struct TradingFeeResponse { +pub struct SpotTradingFeeResponse { pub base_fee_pct: Decimal, pub discount_pct: Decimal, pub effective_fee_pct: Decimal, pub tier_id: String, } +#[cw_serde] +pub struct PerpTradingFeeResponse { + pub opening_fee_pct: Decimal, + pub closing_fee_pct: Decimal, + pub discount_pct: Decimal, + pub effective_opening_fee_pct: Decimal, + pub effective_closing_fee_pct: Decimal, + pub tier_id: String, +} + +#[cw_serde] +pub enum TradingFeeResponse { + Spot(SpotTradingFeeResponse), + Perp(PerpTradingFeeResponse), +} + #[cw_serde] pub struct VaultUtilizationResponse { pub vault: VaultUnchecked, @@ -261,6 +273,7 @@ pub struct ConfigResponse { pub rewards_collector: Option, pub keeper_fee_config: KeeperFeeConfig, pub perps_liquidation_bonus_ratio: Decimal, + pub governance: String, } #[cw_serde] @@ -281,3 +294,8 @@ pub struct AccountTierAndDiscountResponse { pub discount_pct: Decimal, pub voting_power: Uint128, } + +#[cw_serde] +pub struct FeeTierConfigResponse { + pub fee_tier_config: FeeTierConfig, +} diff --git a/schemas/mars-credit-manager/mars-credit-manager.json b/schemas/mars-credit-manager/mars-credit-manager.json index fc5c9dd8..317aa9d4 100644 --- a/schemas/mars-credit-manager/mars-credit-manager.json +++ b/schemas/mars-credit-manager/mars-credit-manager.json @@ -3918,6 +3918,19 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "fee_tier_config" + ], + "properties": { + "fee_tier_config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": { @@ -6964,6 +6977,7 @@ "title": "ConfigResponse", "type": "object", "required": [ + "governance", "health_contract", "incentives", "keeper_fee_config", @@ -6985,6 +6999,9 @@ "null" ] }, + "governance": { + "type": "string" + }, "health_contract": { "type": "string" }, @@ -7160,6 +7177,65 @@ } } }, + "fee_tier_config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FeeTierConfigResponse", + "type": "object", + "required": [ + "fee_tier_config" + ], + "properties": { + "fee_tier_config": { + "$ref": "#/definitions/FeeTierConfig" + } + }, + "additionalProperties": false, + "definitions": { + "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" + }, + "FeeTier": { + "type": "object", + "required": [ + "discount_pct", + "id", + "min_voting_power" + ], + "properties": { + "discount_pct": { + "$ref": "#/definitions/Decimal" + }, + "id": { + "type": "string" + }, + "min_voting_power": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, + "FeeTierConfig": { + "type": "object", + "required": [ + "tiers" + ], + "properties": { + "tiers": { + "type": "array", + "items": { + "$ref": "#/definitions/FeeTier" + } + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, "get_account_tier_and_discount": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "AccountTierAndDiscountResponse", @@ -7566,32 +7642,92 @@ "trading_fee": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "TradingFeeResponse", - "type": "object", - "required": [ - "base_fee_pct", - "discount_pct", - "effective_fee_pct", - "tier_id" - ], - "properties": { - "base_fee_pct": { - "$ref": "#/definitions/Decimal" - }, - "discount_pct": { - "$ref": "#/definitions/Decimal" - }, - "effective_fee_pct": { - "$ref": "#/definitions/Decimal" + "oneOf": [ + { + "type": "object", + "required": [ + "spot" + ], + "properties": { + "spot": { + "$ref": "#/definitions/SpotTradingFeeResponse" + } + }, + "additionalProperties": false }, - "tier_id": { - "type": "string" + { + "type": "object", + "required": [ + "perp" + ], + "properties": { + "perp": { + "$ref": "#/definitions/PerpTradingFeeResponse" + } + }, + "additionalProperties": false } - }, - "additionalProperties": false, + ], "definitions": { "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" + }, + "PerpTradingFeeResponse": { + "type": "object", + "required": [ + "closing_fee_pct", + "discount_pct", + "effective_closing_fee_pct", + "effective_opening_fee_pct", + "opening_fee_pct", + "tier_id" + ], + "properties": { + "closing_fee_pct": { + "$ref": "#/definitions/Decimal" + }, + "discount_pct": { + "$ref": "#/definitions/Decimal" + }, + "effective_closing_fee_pct": { + "$ref": "#/definitions/Decimal" + }, + "effective_opening_fee_pct": { + "$ref": "#/definitions/Decimal" + }, + "opening_fee_pct": { + "$ref": "#/definitions/Decimal" + }, + "tier_id": { + "type": "string" + } + }, + "additionalProperties": false + }, + "SpotTradingFeeResponse": { + "type": "object", + "required": [ + "base_fee_pct", + "discount_pct", + "effective_fee_pct", + "tier_id" + ], + "properties": { + "base_fee_pct": { + "$ref": "#/definitions/Decimal" + }, + "discount_pct": { + "$ref": "#/definitions/Decimal" + }, + "effective_fee_pct": { + "$ref": "#/definitions/Decimal" + }, + "tier_id": { + "type": "string" + } + }, + "additionalProperties": false } } }, diff --git a/scripts/health/pkg-web/index_bg.wasm b/scripts/health/pkg-web/index_bg.wasm index 8a894dd400cc55738d7441c61452335de4c1e201..a80ee2df340583dba1e7667506f8f3077524db01 100644 GIT binary patch delta 213 zcmaDhSLWGVnGGy#jBh8ivIY9TXa2(cj`=W{d4AJe$D)k`496e=Ksv6nNKl)U_Q(Ii}@S#5$39^@KOxU8PGn%sLPVY2fck=sZrkj}_bhI&U*xsGSc*2?S!}gF>j9to%3%1`s%P6nS z_+b10AB;;}7$>wZv1I~cW*}zSzQmUG={L6C%Lf=71*bQduq97lX3EAt{jmv~?sR!m Nw)X8?P1y`@0ss)zTIc`( diff --git a/scripts/types/generated/mars-credit-manager/MarsCreditManager.client.ts b/scripts/types/generated/mars-credit-manager/MarsCreditManager.client.ts index 513fa101..5585468f 100644 --- a/scripts/types/generated/mars-credit-manager/MarsCreditManager.client.ts +++ b/scripts/types/generated/mars-credit-manager/MarsCreditManager.client.ts @@ -89,12 +89,15 @@ import { OwnerResponse, RewardsCollector, ArrayOfCoin, + FeeTierConfigResponse, AccountTierAndDiscountResponse, Positions, DebtAmount, PerpPosition, PnlAmounts, TradingFeeResponse, + SpotTradingFeeResponse, + PerpTradingFeeResponse, ArrayOfVaultBinding, VaultBinding, VaultPositionValue, @@ -206,6 +209,7 @@ export interface MarsCreditManagerReadOnlyInterface { marketType: MarketType }) => Promise swapFeeRate: () => Promise + feeTierConfig: () => Promise } export class MarsCreditManagerQueryClient implements MarsCreditManagerReadOnlyInterface { client: CosmWasmClient @@ -233,6 +237,7 @@ export class MarsCreditManagerQueryClient implements MarsCreditManagerReadOnlyIn this.getAccountTierAndDiscount = this.getAccountTierAndDiscount.bind(this) this.tradingFee = this.tradingFee.bind(this) this.swapFeeRate = this.swapFeeRate.bind(this) + this.feeTierConfig = this.feeTierConfig.bind(this) } accountKind = async ({ accountId }: { accountId: string }): Promise => { return this.client.queryContractSmart(this.contractAddress, { @@ -470,6 +475,11 @@ export class MarsCreditManagerQueryClient implements MarsCreditManagerReadOnlyIn swap_fee_rate: {}, }) } + feeTierConfig = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + fee_tier_config: {}, + }) + } } 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 971adcf0..d3eaeb70 100644 --- a/scripts/types/generated/mars-credit-manager/MarsCreditManager.react-query.ts +++ b/scripts/types/generated/mars-credit-manager/MarsCreditManager.react-query.ts @@ -90,12 +90,15 @@ import { OwnerResponse, RewardsCollector, ArrayOfCoin, + FeeTierConfigResponse, AccountTierAndDiscountResponse, Positions, DebtAmount, PerpPosition, PnlAmounts, TradingFeeResponse, + SpotTradingFeeResponse, + PerpTradingFeeResponse, ArrayOfVaultBinding, VaultBinding, VaultPositionValue, @@ -281,6 +284,14 @@ export const marsCreditManagerQueryKeys = { args, }, ] as const, + feeTierConfig: (contractAddress: string | undefined, args?: Record) => + [ + { + ...marsCreditManagerQueryKeys.address(contractAddress)[0], + method: 'fee_tier_config', + args, + }, + ] as const, } export interface MarsCreditManagerReactQuery { client: MarsCreditManagerQueryClient | undefined @@ -291,6 +302,21 @@ export interface MarsCreditManagerReactQuery { initialData?: undefined } } +export interface MarsCreditManagerFeeTierConfigQuery + extends MarsCreditManagerReactQuery {} +export function useMarsCreditManagerFeeTierConfigQuery({ + client, + options, +}: MarsCreditManagerFeeTierConfigQuery) { + return useQuery( + marsCreditManagerQueryKeys.feeTierConfig(client?.contractAddress), + () => (client ? client.feeTierConfig() : Promise.reject(new Error('Invalid client'))), + { + ...options, + enabled: !!client && (options?.enabled != undefined ? options.enabled : true), + }, + ) +} export interface MarsCreditManagerSwapFeeRateQuery extends MarsCreditManagerReactQuery {} export function useMarsCreditManagerSwapFeeRateQuery({ diff --git a/scripts/types/generated/mars-credit-manager/MarsCreditManager.types.ts b/scripts/types/generated/mars-credit-manager/MarsCreditManager.types.ts index 85d5ca10..9c514189 100644 --- a/scripts/types/generated/mars-credit-manager/MarsCreditManager.types.ts +++ b/scripts/types/generated/mars-credit-manager/MarsCreditManager.types.ts @@ -778,6 +778,9 @@ export type QueryMsg = | { swap_fee_rate: {} } + | { + fee_tier_config: {} + } export type ActionKind = 'default' | 'liquidation' export type VaultPositionAmount = | { @@ -862,6 +865,7 @@ export interface VaultUtilizationResponse { } export interface ConfigResponse { account_nft?: string | null + governance: string health_contract: string incentives: string keeper_fee_config: KeeperFeeConfig @@ -889,6 +893,9 @@ export interface RewardsCollector { address: string } export type ArrayOfCoin = Coin[] +export interface FeeTierConfigResponse { + fee_tier_config: FeeTierConfig +} export interface AccountTierAndDiscountResponse { discount_pct: Decimal tier_id: string @@ -927,12 +934,27 @@ export interface PnlAmounts { pnl: Int128 price_pnl: Int128 } -export interface TradingFeeResponse { +export type TradingFeeResponse = + | { + spot: SpotTradingFeeResponse + } + | { + perp: PerpTradingFeeResponse + } +export interface SpotTradingFeeResponse { base_fee_pct: Decimal discount_pct: Decimal effective_fee_pct: Decimal tier_id: string } +export interface PerpTradingFeeResponse { + closing_fee_pct: Decimal + discount_pct: Decimal + effective_closing_fee_pct: Decimal + effective_opening_fee_pct: Decimal + opening_fee_pct: Decimal + tier_id: string +} export type ArrayOfVaultBinding = VaultBinding[] export interface VaultBinding { account_id: string From f16a13db9fc183f3b6bfc3788985874b690675c3 Mon Sep 17 00:00:00 2001 From: Subham2804 Date: Tue, 30 Sep 2025 18:03:14 +0530 Subject: [PATCH 13/16] add migration --- Cargo.lock | 6 +- contracts/address-provider/Cargo.toml | 2 +- contracts/address-provider/src/contract.rs | 1 + .../address-provider/src/migrations/mod.rs | 1 + .../address-provider/src/migrations/v2_4_0.rs | 22 +++ contracts/address-provider/tests/tests/mod.rs | 1 + .../tests/tests/test_migration_v2_4_0.rs | 73 ++++++++ contracts/credit-manager/Cargo.toml | 2 +- contracts/credit-manager/src/contract.rs | 4 + .../credit-manager/src/migrations/mod.rs | 1 + .../credit-manager/src/migrations/v2_4_0.rs | 37 ++++ contracts/credit-manager/tests/tests/mod.rs | 1 + .../tests/tests/test_migration_v2_4_0.rs | 172 ++++++++++++++++++ contracts/perps/Cargo.toml | 2 +- contracts/perps/src/contract.rs | 2 +- contracts/perps/src/migrations/mod.rs | 1 + contracts/perps/src/migrations/v2_4_0.rs | 22 +++ contracts/perps/tests/tests/mod.rs | 1 + .../tests/tests/test_migration_v2_4_0.rs | 70 +++++++ packages/types/src/address_provider.rs | 1 + packages/types/src/credit_manager/migrate.rs | 7 +- 21 files changed, 421 insertions(+), 8 deletions(-) create mode 100644 contracts/address-provider/src/migrations/v2_4_0.rs create mode 100644 contracts/address-provider/tests/tests/test_migration_v2_4_0.rs create mode 100644 contracts/credit-manager/src/migrations/v2_4_0.rs create mode 100644 contracts/credit-manager/tests/tests/test_migration_v2_4_0.rs create mode 100644 contracts/perps/src/migrations/v2_4_0.rs create mode 100644 contracts/perps/tests/tests/test_migration_v2_4_0.rs diff --git a/Cargo.lock b/Cargo.lock index 181f96cd..f3c90cd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2656,7 +2656,7 @@ dependencies = [ [[package]] name = "mars-address-provider" -version = "2.3.2" +version = "2.4.0" dependencies = [ "bech32 0.11.0", "cosmwasm-schema 1.5.7", @@ -2672,7 +2672,7 @@ dependencies = [ [[package]] name = "mars-credit-manager" -version = "2.3.1" +version = "2.4.0" dependencies = [ "anyhow", "cosmwasm-schema 1.5.7", @@ -3008,7 +3008,7 @@ dependencies = [ [[package]] name = "mars-perps" -version = "2.3.0" +version = "2.4.0" dependencies = [ "anyhow", "cosmwasm-schema 1.5.7", diff --git a/contracts/address-provider/Cargo.toml b/contracts/address-provider/Cargo.toml index 32b624d6..8cfbb3b3 100644 --- a/contracts/address-provider/Cargo.toml +++ b/contracts/address-provider/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "mars-address-provider" description = "A smart contract that holds addresses of Mars Red Bank contracts" -version = "2.3.2" +version = "2.4.0" authors = { workspace = true } edition = { workspace = true } license = { workspace = true } diff --git a/contracts/address-provider/src/contract.rs b/contracts/address-provider/src/contract.rs index 70fc79f6..b655b9d6 100644 --- a/contracts/address-provider/src/contract.rs +++ b/contracts/address-provider/src/contract.rs @@ -176,5 +176,6 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result migrations::v2_2_2::migrate(deps), MigrateMsg::V2_2_2ToV2_3_2 {} => migrations::v2_3_2::migrate(deps), + MigrateMsg::V2_3_2ToV2_4_0 {} => migrations::v2_4_0::migrate(deps), } } diff --git a/contracts/address-provider/src/migrations/mod.rs b/contracts/address-provider/src/migrations/mod.rs index e7bee920..a4ad49c9 100644 --- a/contracts/address-provider/src/migrations/mod.rs +++ b/contracts/address-provider/src/migrations/mod.rs @@ -1,2 +1,3 @@ pub mod v2_2_2; pub mod v2_3_2; +pub mod v2_4_0; diff --git a/contracts/address-provider/src/migrations/v2_4_0.rs b/contracts/address-provider/src/migrations/v2_4_0.rs new file mode 100644 index 00000000..62369466 --- /dev/null +++ b/contracts/address-provider/src/migrations/v2_4_0.rs @@ -0,0 +1,22 @@ +use cosmwasm_std::{DepsMut, Response}; +use cw2::{assert_contract_version, set_contract_version}; + +use crate::{ + contract::{CONTRACT_NAME, CONTRACT_VERSION}, + error::ContractError, +}; + +pub const FROM_VERSION: &str = "2.3.2"; + +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)?; + + // this is a standard migration with no state changes + 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/address-provider/tests/tests/mod.rs b/contracts/address-provider/tests/tests/mod.rs index edb731f6..619c3141 100644 --- a/contracts/address-provider/tests/tests/mod.rs +++ b/contracts/address-provider/tests/tests/mod.rs @@ -4,4 +4,5 @@ mod test_addresses; mod test_instantiate; mod test_migration_v2; mod test_migration_v2_3_2; +mod test_migration_v2_4_0; mod test_update_owner; diff --git a/contracts/address-provider/tests/tests/test_migration_v2_4_0.rs b/contracts/address-provider/tests/tests/test_migration_v2_4_0.rs new file mode 100644 index 00000000..eb031b85 --- /dev/null +++ b/contracts/address-provider/tests/tests/test_migration_v2_4_0.rs @@ -0,0 +1,73 @@ +use cosmwasm_std::{attr, testing::mock_env, Event}; +use cw2::{ContractVersion, VersionError}; +use mars_address_provider::{ + contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}, + error::ContractError, + migrations::v2_4_0::FROM_VERSION, +}; +use mars_testing::mock_dependencies; +use mars_types::address_provider::MigrateMsg; + +#[test] +fn wrong_contract_name() { + let mut deps = mock_dependencies(&[]); + cw2::set_contract_version(deps.as_mut().storage, "contract_xyz", FROM_VERSION).unwrap(); + + let err = migrate(deps.as_mut(), mock_env(), MigrateMsg::V2_3_2ToV2_4_0 {}).unwrap_err(); + + assert_eq!( + err, + ContractError::Version(VersionError::WrongContract { + expected: format!("crates.io:{CONTRACT_NAME}"), + found: "contract_xyz".to_string() + }) + ); +} + +#[test] +fn wrong_contract_version() { + let mut deps = mock_dependencies(&[]); + cw2::set_contract_version(deps.as_mut().storage, format!("crates.io:{CONTRACT_NAME}"), "4.1.0") + .unwrap(); + + let err = migrate(deps.as_mut(), mock_env(), MigrateMsg::V2_3_2ToV2_4_0 {}).unwrap_err(); + + assert_eq!( + err, + ContractError::Version(VersionError::WrongVersion { + expected: FROM_VERSION.to_string(), + found: "4.1.0".to_string() + }) + ); +} + +#[test] +fn successful_migration() { + let mut deps = mock_dependencies(&[]); + cw2::set_contract_version( + deps.as_mut().storage, + format!("crates.io:{CONTRACT_NAME}"), + FROM_VERSION, + ) + .unwrap(); + + let res = migrate(deps.as_mut(), mock_env(), MigrateMsg::V2_3_2ToV2_4_0 {}).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", FROM_VERSION), + attr("to_version", CONTRACT_VERSION) + ] + ); + + let new_contract_version = ContractVersion { + contract: format!("crates.io:{CONTRACT_NAME}"), + version: CONTRACT_VERSION.to_string(), + }; + assert_eq!(cw2::get_contract_version(deps.as_ref().storage).unwrap(), new_contract_version); +} diff --git a/contracts/credit-manager/Cargo.toml b/contracts/credit-manager/Cargo.toml index cabfc4ba..2bb1497c 100644 --- a/contracts/credit-manager/Cargo.toml +++ b/contracts/credit-manager/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mars-credit-manager" -version = "2.3.1" +version = "2.4.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 34d7d0e4..efb66c25 100644 --- a/contracts/credit-manager/src/contract.rs +++ b/contracts/credit-manager/src/contract.rs @@ -196,5 +196,9 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result migrations::v2_3_0::migrate(deps, max_trigger_orders), MigrateMsg::V2_2_0ToV2_2_3 {} => migrations::v2_2_3::migrate(deps), + MigrateMsg::V2_3_1ToV2_4_0 { + fee_tier_config, + governance_address, + } => migrations::v2_4_0::migrate(deps, fee_tier_config, governance_address), } } diff --git a/contracts/credit-manager/src/migrations/mod.rs b/contracts/credit-manager/src/migrations/mod.rs index 22d21463..56fc13f9 100644 --- a/contracts/credit-manager/src/migrations/mod.rs +++ b/contracts/credit-manager/src/migrations/mod.rs @@ -2,3 +2,4 @@ pub mod v2_2_0; pub mod v2_2_3; pub mod v2_3_0; pub mod v2_3_1; +pub mod v2_4_0; diff --git a/contracts/credit-manager/src/migrations/v2_4_0.rs b/contracts/credit-manager/src/migrations/v2_4_0.rs new file mode 100644 index 00000000..7ab24b1e --- /dev/null +++ b/contracts/credit-manager/src/migrations/v2_4_0.rs @@ -0,0 +1,37 @@ +use cosmwasm_std::{Addr, DepsMut, Response}; +use cw2::{assert_contract_version, set_contract_version}; +use mars_types::fee_tiers::FeeTierConfig; + +use crate::{ + contract::CONTRACT_NAME, + error::ContractError, + staking::StakingTierManager, + state::{FEE_TIER_CONFIG, GOVERNANCE}, +}; + +const FROM_VERSION: &str = "2.3.1"; +const TO_VERSION: &str = "2.4.0"; + +pub fn migrate( + deps: DepsMut, + fee_tier_config: FeeTierConfig, + governance_address: Addr, +) -> 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)?; + + let manager = StakingTierManager::new(fee_tier_config.clone()); + manager.validate()?; + // Set the new state items + FEE_TIER_CONFIG.save(deps.storage, &fee_tier_config)?; + GOVERNANCE.save(deps.storage, &governance_address)?; + + 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", TO_VERSION) + .add_attribute("fee_tier_config", "set") + .add_attribute("governance", governance_address)) +} diff --git a/contracts/credit-manager/tests/tests/mod.rs b/contracts/credit-manager/tests/tests/mod.rs index bf680038..04e95247 100644 --- a/contracts/credit-manager/tests/tests/mod.rs +++ b/contracts/credit-manager/tests/tests/mod.rs @@ -29,6 +29,7 @@ mod test_migration_v2; mod test_migration_v2_2_3; mod test_migration_v2_3_0; mod test_migration_v2_3_1; +mod test_migration_v2_4_0; mod test_no_health_check; mod test_order_relations; mod test_perp; diff --git a/contracts/credit-manager/tests/tests/test_migration_v2_4_0.rs b/contracts/credit-manager/tests/tests/test_migration_v2_4_0.rs new file mode 100644 index 00000000..4253cd68 --- /dev/null +++ b/contracts/credit-manager/tests/tests/test_migration_v2_4_0.rs @@ -0,0 +1,172 @@ +use cosmwasm_std::{attr, testing::mock_env, Addr, Decimal, Uint128}; +use cw2::{ContractVersion, VersionError}; +use mars_credit_manager::{ + contract::migrate, + error::ContractError, + state::{FEE_TIER_CONFIG, GOVERNANCE}, +}; +use mars_testing::mock_dependencies; +use mars_types::{ + credit_manager::MigrateMsg, + fee_tiers::{FeeTier, FeeTierConfig}, +}; + +const CONTRACT_NAME: &str = "crates.io:mars-credit-manager"; +const CONTRACT_VERSION: &str = "2.4.0"; + +fn create_test_fee_tier_config() -> FeeTierConfig { + FeeTierConfig { + tiers: vec![ + FeeTier { + id: "tier_3".to_string(), + min_voting_power: Uint128::new(100000000000), // 100,000 MARS + discount_pct: Decimal::percent(30), + }, + FeeTier { + id: "tier_2".to_string(), + min_voting_power: Uint128::new(10000000000), // 10,000 MARS + discount_pct: Decimal::percent(10), + }, + FeeTier { + id: "tier_1".to_string(), + min_voting_power: Uint128::zero(), + discount_pct: Decimal::percent(0), + }, + ], + } +} + +#[test] +fn wrong_contract_name() { + let mut deps = mock_dependencies(&[]); + cw2::set_contract_version(deps.as_mut().storage, "contract_xyz", "2.3.1").unwrap(); + + let fee_tier_config = create_test_fee_tier_config(); + let governance_address = Addr::unchecked("governance"); + + let err = migrate( + deps.as_mut(), + mock_env(), + MigrateMsg::V2_3_1ToV2_4_0 { + fee_tier_config, + governance_address, + }, + ) + .unwrap_err(); + + assert_eq!( + err, + ContractError::Version(VersionError::WrongContract { + expected: CONTRACT_NAME.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, CONTRACT_NAME, "2.1.0").unwrap(); + + let fee_tier_config = create_test_fee_tier_config(); + let governance_address = Addr::unchecked("governance"); + + let err = migrate( + deps.as_mut(), + mock_env(), + MigrateMsg::V2_3_1ToV2_4_0 { + fee_tier_config, + governance_address, + }, + ) + .unwrap_err(); + + assert_eq!( + err, + ContractError::Version(VersionError::WrongVersion { + expected: "2.3.1".to_string(), + found: "2.1.0".to_string() + }) + ); +} + +#[test] +fn successful_migration() { + let mut deps = mock_dependencies(&[]); + cw2::set_contract_version(deps.as_mut().storage, CONTRACT_NAME, "2.3.1").unwrap(); + + let fee_tier_config = create_test_fee_tier_config(); + let governance_address = Addr::unchecked("governance"); + + let res = migrate( + deps.as_mut(), + mock_env(), + MigrateMsg::V2_3_1ToV2_4_0 { + fee_tier_config: fee_tier_config.clone(), + governance_address: governance_address.clone(), + }, + ) + .unwrap(); + + // Verify that the state was set correctly + let stored_fee_tier_config = FEE_TIER_CONFIG.load(deps.as_ref().storage).unwrap(); + let stored_governance = GOVERNANCE.load(deps.as_ref().storage).unwrap(); + + assert_eq!(stored_fee_tier_config, fee_tier_config); + assert_eq!(stored_governance, governance_address); + + // Verify response attributes + assert_eq!( + res.attributes, + vec![ + attr("action", "migrate"), + attr("from_version", "2.3.1"), + attr("to_version", "2.4.0"), + attr("fee_tier_config", "set"), + attr("governance", governance_address) + ] + ); + + // Verify contract version was updated + let new_contract_version = ContractVersion { + contract: CONTRACT_NAME.to_string(), + version: CONTRACT_VERSION.to_string(), + }; + assert_eq!(cw2::get_contract_version(deps.as_ref().storage).unwrap(), new_contract_version); +} + +#[test] +fn migration_with_invalid_fee_tier_config() { + let mut deps = mock_dependencies(&[]); + cw2::set_contract_version(deps.as_mut().storage, CONTRACT_NAME, "2.3.1").unwrap(); + + // Create invalid fee tier config (tiers not sorted by min_voting_power descending) + let invalid_fee_tier_config = FeeTierConfig { + tiers: vec![ + FeeTier { + id: "tier_1".to_string(), + min_voting_power: Uint128::new(10000000000), // Lower threshold first + discount_pct: Decimal::percent(10), + }, + FeeTier { + id: "tier_2".to_string(), + min_voting_power: Uint128::new(100000000000), // Higher threshold second + discount_pct: Decimal::percent(30), + }, + ], + }; + let governance_address = Addr::unchecked("governance"); + + let err = migrate( + deps.as_mut(), + mock_env(), + MigrateMsg::V2_3_1ToV2_4_0 { + fee_tier_config: invalid_fee_tier_config, + governance_address, + }, + ) + .unwrap_err(); + + // Should fail validation due to incorrect tier ordering + assert!(matches!(err, ContractError::TiersNotSortedDescending)); +} diff --git a/contracts/perps/Cargo.toml b/contracts/perps/Cargo.toml index 1c329e9a..c6832f4f 100644 --- a/contracts/perps/Cargo.toml +++ b/contracts/perps/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mars-perps" -version = "2.3.0" +version = "2.4.0" authors = { workspace = true } license = { workspace = true } edition = { workspace = true } diff --git a/contracts/perps/src/contract.rs b/contracts/perps/src/contract.rs index 200f2ac3..2b3b9662 100644 --- a/contracts/perps/src/contract.rs +++ b/contracts/perps/src/contract.rs @@ -198,5 +198,5 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> ContractResult { #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate(deps: DepsMut, _env: Env, _msg: Empty) -> Result { - migrations::v2_3_0::migrate(deps) + migrations::v2_4_0::migrate(deps) } diff --git a/contracts/perps/src/migrations/mod.rs b/contracts/perps/src/migrations/mod.rs index 2602415b..21426045 100644 --- a/contracts/perps/src/migrations/mod.rs +++ b/contracts/perps/src/migrations/mod.rs @@ -1,3 +1,4 @@ pub mod v2_2_1; pub mod v2_2_3; pub mod v2_3_0; +pub mod v2_4_0; diff --git a/contracts/perps/src/migrations/v2_4_0.rs b/contracts/perps/src/migrations/v2_4_0.rs new file mode 100644 index 00000000..7b6def77 --- /dev/null +++ b/contracts/perps/src/migrations/v2_4_0.rs @@ -0,0 +1,22 @@ +use cosmwasm_std::{DepsMut, Response}; +use cw2::{assert_contract_version, set_contract_version}; + +use crate::{ + contract::{CONTRACT_NAME, CONTRACT_VERSION}, + error::ContractError, +}; + +const FROM_VERSION: &str = "2.3.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)?; + + // Update contract version + 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/perps/tests/tests/mod.rs b/contracts/perps/tests/tests/mod.rs index 26f49abf..d495b2e8 100644 --- a/contracts/perps/tests/tests/mod.rs +++ b/contracts/perps/tests/tests/mod.rs @@ -6,6 +6,7 @@ mod test_instantiate; mod test_managing_markets; mod test_migration_v2; mod test_migration_v2_3_0; +mod test_migration_v2_4_0; mod test_position; mod test_protocol_fees; mod test_query; diff --git a/contracts/perps/tests/tests/test_migration_v2_4_0.rs b/contracts/perps/tests/tests/test_migration_v2_4_0.rs new file mode 100644 index 00000000..95e52a19 --- /dev/null +++ b/contracts/perps/tests/tests/test_migration_v2_4_0.rs @@ -0,0 +1,70 @@ +use cosmwasm_std::{attr, testing::mock_env, Empty}; +use cw2::{ContractVersion, VersionError}; +use mars_perps::{contract::migrate, error::ContractError}; +use mars_testing::mock_dependencies; + +const CONTRACT_NAME: &str = "mars-perps"; +const CONTRACT_VERSION: &str = "2.4.0"; + +#[test] +fn wrong_contract_name() { + let mut deps = mock_dependencies(&[]); + cw2::set_contract_version(deps.as_mut().storage, "contract_xyz", "2.3.0").unwrap(); + + let err = migrate(deps.as_mut(), mock_env(), Empty {}).unwrap_err(); + + assert_eq!( + err, + ContractError::Version(VersionError::WrongContract { + expected: format!("crates.io:{CONTRACT_NAME}"), + found: "contract_xyz".to_string() + }) + ); +} + +#[test] +fn wrong_contract_version() { + let mut deps = mock_dependencies(&[]); + cw2::set_contract_version(deps.as_mut().storage, format!("crates.io:{CONTRACT_NAME}"), "1.0.0") + .unwrap(); + + let err = migrate(deps.as_mut(), mock_env(), Empty {}).unwrap_err(); + + assert_eq!( + err, + ContractError::Version(VersionError::WrongVersion { + expected: "2.3.0".to_string(), + found: "1.0.0".to_string() + }) + ); +} + +#[test] +fn successful_migration_from_2_3_0() { + let mut deps = mock_dependencies(&[]); + cw2::set_contract_version(deps.as_mut().storage, format!("crates.io:{CONTRACT_NAME}"), "2.3.0") + .unwrap(); + + let res = migrate(deps.as_mut(), mock_env(), Empty {}).unwrap(); + + assert_eq!(res.messages, vec![]); + assert_eq!( + res.attributes, + vec![ + attr("action", "migrate"), + attr("from_version", "2.3.0"), + attr("to_version", CONTRACT_VERSION), + ] + ); + assert!(res.data.is_none()); + + // Verify the contract version was updated + let new_contract_version = cw2::get_contract_version(deps.as_ref().storage).unwrap(); + assert_eq!( + new_contract_version, + ContractVersion { + contract: format!("crates.io:{CONTRACT_NAME}"), + version: CONTRACT_VERSION.to_string(), + } + ); +} diff --git a/packages/types/src/address_provider.rs b/packages/types/src/address_provider.rs index 932cf784..b07f98c5 100644 --- a/packages/types/src/address_provider.rs +++ b/packages/types/src/address_provider.rs @@ -176,6 +176,7 @@ pub struct AddressResponseItem { #[cw_serde] pub enum MigrateMsg { + V2_3_2ToV2_4_0 {}, V2_2_2ToV2_3_2 {}, V2_2_0ToV2_2_2 {}, } diff --git a/packages/types/src/credit_manager/migrate.rs b/packages/types/src/credit_manager/migrate.rs index 985cf316..ab2423ef 100644 --- a/packages/types/src/credit_manager/migrate.rs +++ b/packages/types/src/credit_manager/migrate.rs @@ -1,5 +1,6 @@ +use crate::fee_tiers::FeeTierConfig; use cosmwasm_schema::cw_serde; -use cosmwasm_std::Decimal; +use cosmwasm_std::{Addr, Decimal}; #[cw_serde] pub enum MigrateMsg { @@ -10,4 +11,8 @@ pub enum MigrateMsg { V2_3_0ToV2_3_1 { swap_fee: Decimal, }, + V2_3_1ToV2_4_0 { + fee_tier_config: FeeTierConfig, + governance_address: Addr, + }, } From cb20139ab3627905dfa7f31eac5f101bcd8afac6 Mon Sep 17 00:00:00 2001 From: Subham2804 Date: Tue, 30 Sep 2025 18:16:47 +0530 Subject: [PATCH 14/16] fmt --- packages/types/src/credit_manager/migrate.rs | 3 ++- .../mars-address-provider.json | 2 +- .../mars-credit-manager.json | 2 +- schemas/mars-perps/mars-perps.json | 2 +- scripts/health/pkg-web/index_bg.wasm | Bin 462566 -> 462566 bytes 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/types/src/credit_manager/migrate.rs b/packages/types/src/credit_manager/migrate.rs index ab2423ef..5cdb69f5 100644 --- a/packages/types/src/credit_manager/migrate.rs +++ b/packages/types/src/credit_manager/migrate.rs @@ -1,7 +1,8 @@ -use crate::fee_tiers::FeeTierConfig; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Decimal}; +use crate::fee_tiers::FeeTierConfig; + #[cw_serde] pub enum MigrateMsg { V2_2_0ToV2_2_3 {}, diff --git a/schemas/mars-address-provider/mars-address-provider.json b/schemas/mars-address-provider/mars-address-provider.json index b806bce6..faf979e4 100644 --- a/schemas/mars-address-provider/mars-address-provider.json +++ b/schemas/mars-address-provider/mars-address-provider.json @@ -1,6 +1,6 @@ { "contract_name": "mars-address-provider", - "contract_version": "2.3.2", + "contract_version": "2.4.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-credit-manager/mars-credit-manager.json b/schemas/mars-credit-manager/mars-credit-manager.json index e6977d7c..a6428d87 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.3.1", + "contract_version": "2.4.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/schemas/mars-perps/mars-perps.json b/schemas/mars-perps/mars-perps.json index 44862529..76ff81ef 100644 --- a/schemas/mars-perps/mars-perps.json +++ b/schemas/mars-perps/mars-perps.json @@ -1,6 +1,6 @@ { "contract_name": "mars-perps", - "contract_version": "2.3.0", + "contract_version": "2.4.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/scripts/health/pkg-web/index_bg.wasm b/scripts/health/pkg-web/index_bg.wasm index a80ee2df340583dba1e7667506f8f3077524db01..789a7db3c66fd05b7a77fd37dec552cd74abaab3 100644 GIT binary patch delta 7608 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 Date: Tue, 30 Sep 2025 19:42:44 +0530 Subject: [PATCH 15/16] fix migration tests --- contracts/perps/src/contract.rs | 12 ++++++++---- contracts/perps/src/migrations/v2_2_1.rs | 9 ++++----- contracts/perps/src/migrations/v2_2_3.rs | 8 ++++---- contracts/perps/src/migrations/v2_3_0.rs | 8 ++++---- contracts/perps/tests/tests/test_migration_v2_3_0.rs | 9 +++++---- contracts/perps/tests/tests/test_migration_v2_4_0.rs | 9 +++++---- packages/types/src/perps.rs | 8 ++++++++ 7 files changed, 38 insertions(+), 25 deletions(-) diff --git a/contracts/perps/src/contract.rs b/contracts/perps/src/contract.rs index 2b3b9662..e05cc961 100644 --- a/contracts/perps/src/contract.rs +++ b/contracts/perps/src/contract.rs @@ -1,10 +1,10 @@ use cosmwasm_std::{ - entry_point, to_json_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Reply, Response, + entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, }; use mars_owner::OwnerInit; use mars_types::{ oracle::ActionKind, - perps::{ExecuteMsg, InstantiateMsg, QueryMsg}, + perps::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, }; use crate::{ @@ -197,6 +197,10 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> ContractResult { } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(deps: DepsMut, _env: Env, _msg: Empty) -> Result { - migrations::v2_4_0::migrate(deps) +pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { + match msg { + MigrateMsg::V2_2_1ToV2_2_3 {} => migrations::v2_2_3::migrate(deps), + MigrateMsg::V2_2_3ToV2_3_0 {} => migrations::v2_3_0::migrate(deps), + MigrateMsg::V2_3_0ToV2_4_0 {} => migrations::v2_4_0::migrate(deps), + } } diff --git a/contracts/perps/src/migrations/v2_2_1.rs b/contracts/perps/src/migrations/v2_2_1.rs index b7f741b6..b737d9f4 100644 --- a/contracts/perps/src/migrations/v2_2_1.rs +++ b/contracts/perps/src/migrations/v2_2_1.rs @@ -1,14 +1,13 @@ use cosmwasm_std::{DepsMut, Response}; use cw2::{assert_contract_version, set_contract_version}; -use crate::{ - contract::{CONTRACT_NAME, CONTRACT_VERSION}, - error::ContractError, - state::MARKET_STATES, -}; +use crate::{contract::CONTRACT_NAME, error::ContractError, state::MARKET_STATES}; const FROM_VERSION: &str = "2.2.0"; +// Hardcode the contract version so we don't break the tests +const CONTRACT_VERSION: &str = "2.3.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, CONTRACT_NAME, FROM_VERSION)?; diff --git a/contracts/perps/src/migrations/v2_2_3.rs b/contracts/perps/src/migrations/v2_2_3.rs index b40fe055..99263fd9 100644 --- a/contracts/perps/src/migrations/v2_2_3.rs +++ b/contracts/perps/src/migrations/v2_2_3.rs @@ -1,13 +1,13 @@ 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.2.1"; +// Hardcode the contract version so we don't break the tests +const CONTRACT_VERSION: &str = "2.2.3"; + 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)?; diff --git a/contracts/perps/src/migrations/v2_3_0.rs b/contracts/perps/src/migrations/v2_3_0.rs index 60b74eac..37d67c45 100644 --- a/contracts/perps/src/migrations/v2_3_0.rs +++ b/contracts/perps/src/migrations/v2_3_0.rs @@ -1,13 +1,13 @@ 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.2.3"; +// Hardcode the contract version so we don't break the tests +const CONTRACT_VERSION: &str = "2.3.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)?; diff --git a/contracts/perps/tests/tests/test_migration_v2_3_0.rs b/contracts/perps/tests/tests/test_migration_v2_3_0.rs index 9d300cf9..25f6b19b 100644 --- a/contracts/perps/tests/tests/test_migration_v2_3_0.rs +++ b/contracts/perps/tests/tests/test_migration_v2_3_0.rs @@ -1,7 +1,8 @@ -use cosmwasm_std::{attr, testing::mock_env, Empty}; +use cosmwasm_std::{attr, testing::mock_env}; use cw2::{ContractVersion, VersionError}; use mars_perps::{contract::migrate, error::ContractError}; use mars_testing::mock_dependencies; +use mars_types::perps::MigrateMsg; const CONTRACT_NAME: &str = "mars-perps"; const CONTRACT_VERSION: &str = "2.3.0"; @@ -11,7 +12,7 @@ fn wrong_contract_name() { let mut deps = mock_dependencies(&[]); cw2::set_contract_version(deps.as_mut().storage, "contract_xyz", "2.2.3").unwrap(); - let err = migrate(deps.as_mut(), mock_env(), Empty {}).unwrap_err(); + let err = migrate(deps.as_mut(), mock_env(), MigrateMsg::V2_2_3ToV2_3_0 {}).unwrap_err(); assert_eq!( err, @@ -28,7 +29,7 @@ fn wrong_contract_version() { cw2::set_contract_version(deps.as_mut().storage, format!("crates.io:{CONTRACT_NAME}"), "1.0.0") .unwrap(); - let err = migrate(deps.as_mut(), mock_env(), Empty {}).unwrap_err(); + let err = migrate(deps.as_mut(), mock_env(), MigrateMsg::V2_2_3ToV2_3_0 {}).unwrap_err(); assert_eq!( err, @@ -45,7 +46,7 @@ fn successful_migration_from_2_2_1() { cw2::set_contract_version(deps.as_mut().storage, format!("crates.io:{CONTRACT_NAME}"), "2.2.3") .unwrap(); - let res = migrate(deps.as_mut(), mock_env(), Empty {}).unwrap(); + let res = migrate(deps.as_mut(), mock_env(), MigrateMsg::V2_2_3ToV2_3_0 {}).unwrap(); assert_eq!(res.messages, vec![]); assert_eq!( diff --git a/contracts/perps/tests/tests/test_migration_v2_4_0.rs b/contracts/perps/tests/tests/test_migration_v2_4_0.rs index 95e52a19..d072abbe 100644 --- a/contracts/perps/tests/tests/test_migration_v2_4_0.rs +++ b/contracts/perps/tests/tests/test_migration_v2_4_0.rs @@ -1,7 +1,8 @@ -use cosmwasm_std::{attr, testing::mock_env, Empty}; +use cosmwasm_std::{attr, testing::mock_env}; use cw2::{ContractVersion, VersionError}; use mars_perps::{contract::migrate, error::ContractError}; use mars_testing::mock_dependencies; +use mars_types::perps::MigrateMsg; const CONTRACT_NAME: &str = "mars-perps"; const CONTRACT_VERSION: &str = "2.4.0"; @@ -11,7 +12,7 @@ fn wrong_contract_name() { let mut deps = mock_dependencies(&[]); cw2::set_contract_version(deps.as_mut().storage, "contract_xyz", "2.3.0").unwrap(); - let err = migrate(deps.as_mut(), mock_env(), Empty {}).unwrap_err(); + let err = migrate(deps.as_mut(), mock_env(), MigrateMsg::V2_3_0ToV2_4_0 {}).unwrap_err(); assert_eq!( err, @@ -28,7 +29,7 @@ fn wrong_contract_version() { cw2::set_contract_version(deps.as_mut().storage, format!("crates.io:{CONTRACT_NAME}"), "1.0.0") .unwrap(); - let err = migrate(deps.as_mut(), mock_env(), Empty {}).unwrap_err(); + let err = migrate(deps.as_mut(), mock_env(), MigrateMsg::V2_3_0ToV2_4_0 {}).unwrap_err(); assert_eq!( err, @@ -45,7 +46,7 @@ fn successful_migration_from_2_3_0() { cw2::set_contract_version(deps.as_mut().storage, format!("crates.io:{CONTRACT_NAME}"), "2.3.0") .unwrap(); - let res = migrate(deps.as_mut(), mock_env(), Empty {}).unwrap(); + let res = migrate(deps.as_mut(), mock_env(), MigrateMsg::V2_3_0ToV2_4_0 {}).unwrap(); assert_eq!(res.messages, vec![]); assert_eq!( diff --git a/packages/types/src/perps.rs b/packages/types/src/perps.rs index e62e85b1..04c4e262 100644 --- a/packages/types/src/perps.rs +++ b/packages/types/src/perps.rs @@ -62,6 +62,14 @@ pub struct Config { /// The maximum number of unlocks that can be requested by a single user pub max_unlocks: u8, } +// ------------------------------- migrate messages ------------------------------- + +#[cw_serde] +pub enum MigrateMsg { + V2_2_1ToV2_2_3 {}, + V2_2_3ToV2_3_0 {}, + V2_3_0ToV2_4_0 {}, +} impl Config { pub fn check(self, api: &dyn Api) -> StdResult> { From 83c8391633a6fbc5f7d8b95f859321c6cc00907c Mon Sep 17 00:00:00 2001 From: Subham2804 Date: Wed, 1 Oct 2025 10:03:36 +0530 Subject: [PATCH 16/16] Fix PR Comments --- contracts/credit-manager/src/error.rs | 6 +++ contracts/credit-manager/src/staking.rs | 26 +++++----- contracts/credit-manager/src/utils.rs | 13 +++++ .../tests/tests/test_staking_tiers.rs | 51 +++++++++++++++++++ 4 files changed, 83 insertions(+), 13 deletions(-) diff --git a/contracts/credit-manager/src/error.rs b/contracts/credit-manager/src/error.rs index a3555f04..aa2b2902 100644 --- a/contracts/credit-manager/src/error.rs +++ b/contracts/credit-manager/src/error.rs @@ -328,4 +328,10 @@ pub enum ContractError { }, #[error("Vault has an admin; vaults cannot be managed with an admin set.")] VaultHasAdmin {}, + + #[error("Too many tiers. Maximum allowed: {max_tiers:?}, provided: {provided_tiers:?}")] + TooManyTiers { + max_tiers: usize, + provided_tiers: usize, + }, } diff --git a/contracts/credit-manager/src/staking.rs b/contracts/credit-manager/src/staking.rs index 6d0a689e..4b2ac3b4 100644 --- a/contracts/credit-manager/src/staking.rs +++ b/contracts/credit-manager/src/staking.rs @@ -4,12 +4,14 @@ use mars_types::{ fee_tiers::{FeeTier, FeeTierConfig}, }; +const MAX_TIER_SIZE: usize = 20; + use crate::{ error::{ContractError, ContractResult}, state::{FEE_TIER_CONFIG, GOVERNANCE}, utils::{ - assert_discount_pct, assert_tiers_not_empty, assert_tiers_sorted_descending, - query_nft_token_owner, + assert_discount_pct, assert_tiers_max_size, assert_tiers_not_empty, + assert_tiers_sorted_descending, query_nft_token_owner, }, }; @@ -61,16 +63,19 @@ impl StakingTierManager { /// Validate that tiers are properly ordered by min_voting_power (descending) pub fn validate(&self) -> ContractResult<()> { assert_tiers_not_empty(&self.config.tiers)?; + assert_tiers_max_size(&self.config.tiers, MAX_TIER_SIZE)?; - // Extract all voting powers + // Check duplicates, descending order, and discount percentages let mut voting_powers = Vec::new(); - for tier in &self.config.tiers { + for (i, tier) in self.config.tiers.iter().enumerate() { + // Validate discount percentage + assert_discount_pct(tier.discount_pct)?; + + // Collect voting power for later validation voting_powers.push(tier.min_voting_power); - } - // Check for duplicates - for i in 1..voting_powers.len() { - if voting_powers[i] == voting_powers[i - 1] { + // Check for duplicates (compare with previous tier) + if i > 0 && voting_powers[i] == voting_powers[i - 1] { return Err(ContractError::DuplicateVotingPowerThresholds); } } @@ -78,11 +83,6 @@ impl StakingTierManager { // Check for descending order assert_tiers_sorted_descending(&voting_powers)?; - // Validate discount percentages are reasonable (0-100% inclusive) - for tier in &self.config.tiers { - assert_discount_pct(tier.discount_pct)?; - } - Ok(()) } diff --git a/contracts/credit-manager/src/utils.rs b/contracts/credit-manager/src/utils.rs index 14faacc8..3ac64e06 100644 --- a/contracts/credit-manager/src/utils.rs +++ b/contracts/credit-manager/src/utils.rs @@ -133,6 +133,19 @@ pub fn assert_tiers_sorted_descending(voting_powers: &[Uint128]) -> ContractResu Ok(()) } +pub fn assert_tiers_max_size( + tiers: &[impl std::fmt::Debug], + max_tiers: usize, +) -> ContractResult<()> { + if tiers.len() > max_tiers { + return Err(ContractError::TooManyTiers { + max_tiers, + provided_tiers: tiers.len(), + }); + } + Ok(()) +} + pub fn assert_withdraw_enabled( storage: &dyn Storage, querier: &QuerierWrapper, diff --git a/contracts/credit-manager/tests/tests/test_staking_tiers.rs b/contracts/credit-manager/tests/tests/test_staking_tiers.rs index 7cf01d95..7cea0fad 100644 --- a/contracts/credit-manager/tests/tests/test_staking_tiers.rs +++ b/contracts/credit-manager/tests/tests/test_staking_tiers.rs @@ -673,3 +673,54 @@ fn test_validation_edge_case_single_digit() { let result = manager.validate(); assert!(result.is_ok()); } + +#[test_case( + 20, true, None; + "exactly 20 tiers (max allowed)" +)] +#[test_case( + 21, false, Some(ContractError::TooManyTiers { max_tiers: 20, provided_tiers: 21 }); + "21 tiers (exceeds maximum)" +)] +#[test_case( + 19, true, None; + "19 tiers (just under maximum)" +)] +#[test_case( + 1, true, None; + "single tier (well under maximum)" +)] +#[test_case( + 8, true, None; + "original test config (8 tiers)" +)] +fn test_validation_max_tier_size( + tier_count: usize, + should_pass: bool, + expected_error: Option, +) { + let mut tiers = Vec::new(); + for i in 0..tier_count { + tiers.push(FeeTier { + id: format!("tier_{}", i), + min_voting_power: Uint128::new(((tier_count - i) * 1000) as u128), // Descending order + discount_pct: Decimal::percent((i * 5) as u64), // 0% to max + }); + } + + let config = FeeTierConfig { + tiers, + }; + let manager = StakingTierManager::new(config); + + let result = manager.validate(); + + if should_pass { + assert!(result.is_ok()); + } else { + assert!(result.is_err()); + if let Some(expected_err) = expected_error { + assert_eq!(result.unwrap_err(), expected_err); + } + } +}