diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs index e76a4f3e28..710ca20b16 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs @@ -1,7 +1,7 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{EpochEventId, EpochState, IdentityKey, MixId, ProfitMarginRange}; +use crate::{EpochEventId, EpochState, IdentityKey, MixId, OperatingCostRange, ProfitMarginRange}; use contracts_common::signing::verifier::ApiVerifierError; use contracts_common::Percent; use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; @@ -247,6 +247,13 @@ pub enum MixnetContractError { provided: Percent, range: ProfitMarginRange, }, + + #[error("the provided interval operating cost ({provided}{denom}) is outside the allowed range: {range}")] + OperatingCostOutsideRange { + denom: String, + provided: Uint128, + range: OperatingCostRange, + }, } impl MixnetContractError { diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index 6e774ab768..8772763150 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -11,7 +11,8 @@ use crate::reward_params::{NodeRewardParams, RewardingParams}; use crate::rewarding::helpers::truncate_reward; use crate::rewarding::RewardDistribution; use crate::{ - Delegation, EpochEventId, EpochId, IdentityKey, MixId, Percent, ProfitMarginRange, SphinxKey, + Delegation, EpochEventId, EpochId, IdentityKey, MixId, OperatingCostRange, Percent, + ProfitMarginRange, SphinxKey, }; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128}; @@ -159,6 +160,11 @@ impl MixNodeRewarding { allowed_range.normalise(self.cost_params.profit_margin_percent) } + pub fn normalise_operating_cost(&mut self, allowed_range: OperatingCostRange) { + self.cost_params.interval_operating_cost.amount = + allowed_range.normalise(self.cost_params.interval_operating_cost.amount) + } + /// Determines whether this node is still bonded. This is performed via a simple check, /// if there are no tokens left associated with the operator, it means they have unbonded /// and those params only exist for the purposes of calculating rewards for delegators that diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index 942d44f5d3..90110e0dc6 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -12,7 +12,7 @@ use crate::reward_params::{ IntervalRewardParams, IntervalRewardingParamsUpdate, Performance, RewardingParams, }; use crate::types::{ContractStateParams, LayerAssignment, MixId}; -use crate::ProfitMarginRange; +use crate::{OperatingCostRange, ProfitMarginRange}; use contracts_common::{signing::MessageSignature, IdentityKey, Percent}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Coin, Decimal}; @@ -61,6 +61,9 @@ pub struct InstantiateMsg { #[serde(default)] pub profit_margin: ProfitMarginRange, + + #[serde(default)] + pub interval_operating_cost: OperatingCostRange, } #[cw_serde] diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index ddcbb9326b..18c7fc6606 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -5,8 +5,8 @@ use crate::error::MixnetContractError; use crate::Layer; use contracts_common::Percent; use cosmwasm_schema::cw_serde; -use cosmwasm_std::Addr; use cosmwasm_std::Coin; +use cosmwasm_std::{Addr, Uint128}; use std::fmt::{Display, Formatter}; use std::ops::Index; @@ -18,13 +18,20 @@ pub type MixId = u32; pub type BlockHeight = u64; #[cw_serde] -#[derive(Copy)] -pub struct ProfitMarginRange { - pub minimum: Percent, - pub maximum: Percent, +pub struct RangedValue { + pub minimum: T, + pub maximum: T, } -impl Display for ProfitMarginRange { +impl Copy for RangedValue where T: Copy {} + +pub type ProfitMarginRange = RangedValue; +pub type OperatingCostRange = RangedValue; + +impl Display for RangedValue +where + T: Display, +{ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{} - {}", self.minimum, self.maximum) } @@ -39,19 +46,33 @@ impl Default for ProfitMarginRange { } } -impl ProfitMarginRange { - pub fn normalise(&self, profit_margin: Percent) -> Percent { - if profit_margin < self.minimum { +impl Default for OperatingCostRange { + fn default() -> Self { + OperatingCostRange { + minimum: Uint128::zero(), + + // 1 billion (native tokens, i.e. 1 billion * 1'000'000 base tokens) - the total supply + maximum: Uint128::new(1_000_000_000_000_000), + } + } +} + +impl RangedValue +where + T: Copy + PartialOrd + PartialEq, +{ + pub fn normalise(&self, value: T) -> T { + if value < self.minimum { self.minimum - } else if profit_margin > self.maximum { + } else if value > self.maximum { self.maximum } else { - profit_margin + value } } - pub fn within_range(&self, profit_margin: Percent) -> bool { - profit_margin >= self.minimum && profit_margin <= self.maximum + pub fn within_range(&self, value: T) -> bool { + value >= self.minimum && value <= self.maximum } } @@ -199,4 +220,9 @@ pub struct ContractStateParams { /// default: 0% - 100% #[serde(default)] pub profit_margin: ProfitMarginRange, + + /// Defines the allowed interval operating cost range of operators. + /// default: 0 - 1'000'000'000'000'000 (1 Billion native tokens - the total supply) + #[serde(default)] + pub interval_operating_cost: OperatingCostRange, } diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index d267094aaf..884565d142 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -12,7 +12,7 @@ use cosmwasm_std::{ use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::{ ContractState, ContractStateParams, ExecuteMsg, InstantiateMsg, Interval, MigrateMsg, - ProfitMarginRange, QueryMsg, + OperatingCostRange, ProfitMarginRange, QueryMsg, }; use nym_contracts_common::set_build_information; @@ -26,6 +26,7 @@ fn default_initial_state( rewarding_denom: String, vesting_contract_address: Addr, profit_margin: ProfitMarginRange, + interval_operating_cost: OperatingCostRange, ) -> ContractState { ContractState { owner, @@ -43,6 +44,7 @@ fn default_initial_state( amount: INITIAL_GATEWAY_PLEDGE_AMOUNT, }, profit_margin, + interval_operating_cost, }, } } @@ -75,6 +77,7 @@ pub fn instantiate( msg.rewarding_denom, vesting_contract_address, msg.profit_margin, + msg.interval_operating_cost, ); let starting_interval = Interval::init_interval(msg.epochs_in_interval, msg.epoch_duration, &env); @@ -551,7 +554,7 @@ pub fn migrate( mod tests { use super::*; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - use cosmwasm_std::Decimal; + use cosmwasm_std::{Decimal, Uint128}; use mixnet_contract_common::reward_params::{IntervalRewardParams, RewardingParams}; use mixnet_contract_common::{InitialRewardingParams, Percent}; use std::time::Duration; @@ -581,6 +584,10 @@ mod tests { minimum: "0.05".parse().unwrap(), maximum: "0.95".parse().unwrap(), }, + interval_operating_cost: OperatingCostRange { + minimum: "1000".parse().unwrap(), + maximum: "10000".parse().unwrap(), + }, }; let sender = mock_info("sender", &[]); @@ -606,6 +613,10 @@ mod tests { minimum: Percent::from_percentage_value(5).unwrap(), maximum: Percent::from_percentage_value(95).unwrap(), }, + interval_operating_cost: OperatingCostRange { + minimum: Uint128::new(1000), + maximum: Uint128::new(10000), + }, }, }; diff --git a/contracts/mixnet/src/mixnet_contract_settings/queries.rs b/contracts/mixnet/src/mixnet_contract_settings/queries.rs index adf28f72b3..ca939d4334 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/queries.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/queries.rs @@ -46,6 +46,7 @@ pub(crate) mod tests { minimum_mixnode_pledge: coin(123u128, "unym"), minimum_gateway_pledge: coin(456u128, "unym"), profit_margin: Default::default(), + interval_operating_cost: Default::default(), }, }; diff --git a/contracts/mixnet/src/mixnet_contract_settings/storage.rs b/contracts/mixnet/src/mixnet_contract_settings/storage.rs index e1f094d41c..278b6baacf 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/storage.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/storage.rs @@ -6,7 +6,7 @@ use cosmwasm_std::{Addr, Storage}; use cosmwasm_std::{Coin, StdResult}; use cw_storage_plus::Item; use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::{ContractState, ProfitMarginRange}; +use mixnet_contract_common::{ContractState, OperatingCostRange, ProfitMarginRange}; pub(crate) const CONTRACT_STATE: Item<'_, ContractState> = Item::new(CONTRACT_STATE_KEY); @@ -36,6 +36,14 @@ pub(crate) fn profit_margin_range( .map(|state| state.params.profit_margin)?) } +pub(crate) fn interval_oprating_cost_range( + storage: &dyn Storage, +) -> Result { + Ok(CONTRACT_STATE + .load(storage) + .map(|state| state.params.interval_operating_cost)?) +} + #[allow(unused)] pub(crate) fn minimum_delegation_stake( storage: &dyn Storage, diff --git a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs index efe458b94b..19b753189e 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs @@ -122,6 +122,7 @@ pub mod tests { amount: INITIAL_GATEWAY_PLEDGE_AMOUNT + Uint128::new(1234), }, profit_margin: Default::default(), + interval_operating_cost: Default::default(), }; let initial_params = storage::CONTRACT_STATE diff --git a/contracts/mixnet/src/mixnodes/transactions.rs b/contracts/mixnet/src/mixnodes/transactions.rs index ab1aa9bd86..a30d1d68b1 100644 --- a/contracts/mixnet/src/mixnodes/transactions.rs +++ b/contracts/mixnet/src/mixnodes/transactions.rs @@ -24,7 +24,9 @@ use crate::mixnodes::signature_helpers::verify_mixnode_bonding_signature; use crate::signing::storage as signing_storage; use crate::support::helpers::{ ensure_bonded, ensure_epoch_in_progress_state, ensure_is_authorized, ensure_no_existing_bond, - ensure_no_pending_pledge_changes, ensure_profit_margin_within_range, validate_pledge, + ensure_no_pending_pledge_changes, ensure_operating_cost_within_range, + ensure_profit_margin_within_range, + validate_pledge, }; use super::storage; @@ -72,6 +74,9 @@ pub(crate) fn try_add_mixnode( // ensure the profit margin is within the defined range ensure_profit_margin_within_range(deps.storage, cost_params.profit_margin_percent)?; + // ensure the operating cost is within the defined range + ensure_operating_cost_within_range(deps.storage, &cost_params.interval_operating_cost)?; + // check if the pledge contains any funds of the appropriate denomination let minimum_pledge = mixnet_params_storage::minimum_mixnode_pledge(deps.storage)?; let pledge = validate_pledge(info.funds, minimum_pledge)?; @@ -301,6 +306,9 @@ pub(crate) fn try_update_mixnode_cost_params( // ensure the profit margin is within the defined range ensure_profit_margin_within_range(deps.storage, new_costs.profit_margin_percent)?; + // ensure the operating cost is within the defined range + ensure_operating_cost_within_range(deps.storage, &new_costs.interval_operating_cost)?; + let cosmos_event = new_mixnode_pending_cost_params_update_event( existing_bond.mix_id, &info.sender, diff --git a/contracts/mixnet/src/rewards/transactions.rs b/contracts/mixnet/src/rewards/transactions.rs index a109ecad9e..9c5748ce33 100644 --- a/contracts/mixnet/src/rewards/transactions.rs +++ b/contracts/mixnet/src/rewards/transactions.rs @@ -108,8 +108,11 @@ pub(crate) fn try_reward_mixnode( // make sure node's profit margin is within the allowed range, // if not adjust it accordingly - let profit_margin_range = mixnet_params_storage::profit_margin_range(deps.storage)?; - mix_rewarding.normalise_profit_margin(profit_margin_range); + let params = mixnet_params_storage::CONTRACT_STATE + .load(deps.storage)? + .params; + mix_rewarding.normalise_profit_margin(params.profit_margin); + mix_rewarding.normalise_operating_cost(params.interval_operating_cost); let rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; let node_reward_params = NodeRewardParams::new(node_performance, node_status.is_active()); diff --git a/contracts/mixnet/src/support/helpers.rs b/contracts/mixnet/src/support/helpers.rs index 0d8b679d3f..1a22d06483 100644 --- a/contracts/mixnet/src/support/helpers.rs +++ b/contracts/mixnet/src/support/helpers.rs @@ -283,3 +283,19 @@ pub(crate) fn ensure_profit_margin_within_range( Ok(()) } + +pub fn ensure_operating_cost_within_range( + storage: &dyn Storage, + operating_cost: &Coin, +) -> Result<(), MixnetContractError> { + let range = mixnet_params_storage::interval_oprating_cost_range(storage)?; + if !range.within_range(operating_cost.amount) { + return Err(MixnetContractError::OperatingCostOutsideRange { + denom: operating_cost.denom.clone(), + provided: operating_cost.amount, + range, + }); + } + + Ok(()) +} diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index 5a07f16c77..976138fdc2 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -997,6 +997,7 @@ pub mod test_helpers { epoch_duration: Duration::from_secs(60 * 60), initial_rewarding_params: initial_rewarding_params(), profit_margin: Default::default(), + interval_operating_cost: Default::default(), }; let env = mock_env(); let info = mock_info("creator", &[]);