diff --git a/Cargo.lock b/Cargo.lock index 0bc53266deb..0822026fe5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4999,23 +4999,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "nym-network-statistics" -version = "1.1.34" -dependencies = [ - "dirs 4.0.0", - "log", - "nym-bin-common", - "nym-statistics-common", - "nym-task", - "pretty_env_logger", - "rocket", - "serde", - "sqlx", - "thiserror", - "tokio", -] - [[package]] name = "nym-node" version = "1.1.4" diff --git a/common/commands/src/validator/cosmwasm/generators/mixnet.rs b/common/commands/src/validator/cosmwasm/generators/mixnet.rs index f942aa52bd4..afd21fde9a8 100644 --- a/common/commands/src/validator/cosmwasm/generators/mixnet.rs +++ b/common/commands/src/validator/cosmwasm/generators/mixnet.rs @@ -1,15 +1,26 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use clap::Parser; -use log::{debug, info}; - use cosmwasm_std::Decimal; -use nym_mixnet_contract_common::{InitialRewardingParams, InstantiateMsg, Percent}; -use nym_validator_client::nyxd::AccountId; +use log::{debug, info}; +use nym_mixnet_contract_common::{ + InitialRewardingParams, InstantiateMsg, OperatingCostRange, Percent, ProfitMarginRange, +}; +use nym_network_defaults::mainnet::MIX_DENOM; +use nym_network_defaults::TOTAL_SUPPLY; +use nym_validator_client::nyxd::{AccountId, Coin}; use std::str::FromStr; use std::time::Duration; +pub fn default_maximum_operating_cost() -> Coin { + Coin::new(TOTAL_SUPPLY, MIX_DENOM.base) +} + +pub fn default_minimum_operating_cost() -> Coin { + Coin::new(0, MIX_DENOM.base) +} + #[derive(Debug, Parser)] pub struct Args { #[clap(long)] @@ -50,6 +61,18 @@ pub struct Args { #[clap(long, default_value_t = 240)] pub active_set_size: u32, + + #[clap(long, default_value_t = Percent::zero())] + pub minimum_profit_margin_percent: Percent, + + #[clap(long, default_value_t = Percent::hundred())] + pub maximum_profit_margin_percent: Percent, + + #[clap(long, default_value_t = default_minimum_operating_cost())] + pub minimum_interval_operating_cost: Coin, + + #[clap(long, default_value_t = default_maximum_operating_cost())] + pub maximum_interval_operating_cost: Coin, } pub async fn generate(args: Args) { @@ -97,6 +120,10 @@ pub async fn generate(args: Args) { .expect("Rewarding (mix) denom has to be set") }); + if args.minimum_interval_operating_cost.denom != args.maximum_interval_operating_cost.denom { + panic!("different denoms for operating cost bounds") + } + let instantiate_msg = InstantiateMsg { rewarding_validator_address: rewarding_validator_address.to_string(), vesting_contract_address: vesting_contract_address.to_string(), @@ -104,6 +131,14 @@ pub async fn generate(args: Args) { epochs_in_interval: args.epochs_in_interval, epoch_duration: Duration::from_secs(args.epoch_duration), initial_rewarding_params, + profit_margin: ProfitMarginRange { + minimum: args.minimum_profit_margin_percent, + maximum: args.maximum_profit_margin_percent, + }, + interval_operating_cost: OperatingCostRange { + minimum: args.minimum_interval_operating_cost.amount.into(), + maximum: args.maximum_interval_operating_cost.amount.into(), + }, }; debug!("instantiate_msg: {:?}", instantiate_msg); diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs index ec6e79c65f5..de322d00fc7 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs @@ -1,8 +1,9 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{EpochEventId, EpochState, IdentityKey, MixId}; +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}; use thiserror::Error; @@ -239,6 +240,19 @@ pub enum MixnetContractError { #[from] source: ApiVerifierError, }, + + #[error("the provided profit margin ({provided}) is outside the allowed range: {range}")] + ProfitMarginOutsideRange { + 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 a09d21dd2d2..3f346fad8a1 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -10,7 +10,10 @@ use crate::helpers::IntoBaseDecimal; use crate::reward_params::{NodeRewardParams, RewardingParams}; use crate::rewarding::helpers::truncate_reward; use crate::rewarding::RewardDistribution; -use crate::{Delegation, EpochEventId, EpochId, IdentityKey, MixId, Percent, SphinxKey}; +use crate::{ + Delegation, EpochEventId, EpochId, IdentityKey, MixId, OperatingCostRange, Percent, + ProfitMarginRange, SphinxKey, +}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128}; use schemars::JsonSchema; @@ -152,6 +155,16 @@ impl MixNodeRewarding { }) } + pub fn normalise_profit_margin(&mut self, allowed_range: ProfitMarginRange) { + self.cost_params.profit_margin_percent = + 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 ca154e231b6..b1d62b18321 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -1,4 +1,4 @@ -// Copyright 2021-2023 - Nym Technologies SA +// Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::delegation::{self, OwnerProxySubKey}; @@ -12,6 +12,7 @@ use crate::reward_params::{ IntervalRewardParams, IntervalRewardingParamsUpdate, Performance, RewardingParams, }; use crate::types::{ContractStateParams, LayerAssignment, MixId}; +use crate::{OperatingCostRange, ProfitMarginRange}; use contracts_common::{signing::MessageSignature, IdentityKey, Percent}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Coin, Decimal}; @@ -57,6 +58,12 @@ pub struct InstantiateMsg { pub epochs_in_interval: u32, pub epoch_duration: Duration, pub initial_rewarding_params: InitialRewardingParams, + + #[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 e1ff854bb9d..18c7fc66069 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -3,9 +3,11 @@ 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; // type aliases for better reasoning about available data @@ -15,6 +17,65 @@ pub type SphinxKeyRef<'a> = &'a str; pub type MixId = u32; pub type BlockHeight = u64; +#[cw_serde] +pub struct RangedValue { + pub minimum: T, + pub maximum: T, +} + +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) + } +} + +impl Default for ProfitMarginRange { + fn default() -> Self { + ProfitMarginRange { + minimum: Percent::zero(), + maximum: Percent::hundred(), + } + } +} + +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 value > self.maximum { + self.maximum + } else { + value + } + } + + pub fn within_range(&self, value: T) -> bool { + value >= self.minimum && value <= self.maximum + } +} + /// Specifies layer assignment for the given mixnode. #[cw_serde] pub struct LayerAssignment { @@ -154,4 +215,14 @@ pub struct ContractStateParams { /// Minimum amount a gateway must pledge to get into the system. pub minimum_gateway_pledge: Coin, + + /// Defines the allowed profit margin range of operators. + /// 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/common/types/src/error.rs b/common/types/src/error.rs index 4546731ff42..2a4595a495a 100644 --- a/common/types/src/error.rs +++ b/common/types/src/error.rs @@ -1,3 +1,4 @@ +use nym_mixnet_contract_common::ContractsCommonError; use nym_validator_client::error::TendermintRpcError; use nym_validator_client::nym_api::error::NymAPIError; use nym_validator_client::{nyxd::error::NyxdError, ValidatorClientError}; @@ -8,6 +9,9 @@ use thiserror::Error; // TODO: ask @MS why this even exists #[derive(Error, Debug)] pub enum TypesError { + #[error(transparent)] + ContractsCommon(#[from] ContractsCommonError), + #[error("{source}")] NyxdError { #[from] diff --git a/contracts/mixnet-vesting-integration-tests/src/support/fixtures.rs b/contracts/mixnet-vesting-integration-tests/src/support/fixtures.rs index 5a97c2cf7e0..9910720bb71 100644 --- a/contracts/mixnet-vesting-integration-tests/src/support/fixtures.rs +++ b/contracts/mixnet-vesting-integration-tests/src/support/fixtures.rs @@ -24,5 +24,7 @@ pub fn default_mixnet_init_msg() -> nym_mixnet_contract_common::InstantiateMsg { rewarded_set_size: 240, active_set_size: 100, }, + profit_margin: Default::default(), + interval_operating_cost: Default::default(), } } diff --git a/contracts/mixnet/schema/nym-mixnet-contract.json b/contracts/mixnet/schema/nym-mixnet-contract.json index 971cbc27fb8..1cc7b93c820 100644 --- a/contracts/mixnet/schema/nym-mixnet-contract.json +++ b/contracts/mixnet/schema/nym-mixnet-contract.json @@ -26,6 +26,28 @@ "initial_rewarding_params": { "$ref": "#/definitions/InitialRewardingParams" }, + "interval_operating_cost": { + "default": { + "maximum": "1000000000000000", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Uint128" + } + ] + }, + "profit_margin": { + "default": { + "maximum": "1", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Percent" + } + ] + }, "rewarding_denom": { "type": "string" }, @@ -112,6 +134,42 @@ "$ref": "#/definitions/Decimal" } ] + }, + "RangedValue_for_Percent": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Percent" + }, + "minimum": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "RangedValue_for_Uint128": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Uint128" + }, + "minimum": { + "$ref": "#/definitions/Uint128" + } + }, + "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" } } }, @@ -1172,6 +1230,18 @@ "minimum_mixnode_pledge" ], "properties": { + "interval_operating_cost": { + "description": "Defines the allowed interval operating cost range of operators. default: 0 - 1'000'000'000'000'000 (1 Billion native tokens - the total supply)", + "default": { + "maximum": "1000000000000000", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Uint128" + } + ] + }, "minimum_gateway_pledge": { "description": "Minimum amount a gateway must pledge to get into the system.", "allOf": [ @@ -1198,6 +1268,18 @@ "$ref": "#/definitions/Coin" } ] + }, + "profit_margin": { + "description": "Defines the allowed profit margin range of operators. default: 0% - 100%", + "default": { + "maximum": "1", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Percent" + } + ] } }, "additionalProperties": false @@ -1532,6 +1614,38 @@ } ] }, + "RangedValue_for_Percent": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Percent" + }, + "minimum": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "RangedValue_for_Uint128": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Uint128" + }, + "minimum": { + "$ref": "#/definitions/Uint128" + } + }, + "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" @@ -8063,6 +8177,18 @@ "minimum_mixnode_pledge" ], "properties": { + "interval_operating_cost": { + "description": "Defines the allowed interval operating cost range of operators. default: 0 - 1'000'000'000'000'000 (1 Billion native tokens - the total supply)", + "default": { + "maximum": "1000000000000000", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Uint128" + } + ] + }, "minimum_gateway_pledge": { "description": "Minimum amount a gateway must pledge to get into the system.", "allOf": [ @@ -8089,6 +8215,62 @@ "$ref": "#/definitions/Coin" } ] + }, + "profit_margin": { + "description": "Defines the allowed profit margin range of operators. default: 0% - 100%", + "default": { + "maximum": "1", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Percent" + } + ] + } + }, + "additionalProperties": false + }, + "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" + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "RangedValue_for_Percent": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Percent" + }, + "minimum": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "RangedValue_for_Uint128": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Uint128" + }, + "minimum": { + "$ref": "#/definitions/Uint128" } }, "additionalProperties": false @@ -8109,6 +8291,18 @@ "minimum_mixnode_pledge" ], "properties": { + "interval_operating_cost": { + "description": "Defines the allowed interval operating cost range of operators. default: 0 - 1'000'000'000'000'000 (1 Billion native tokens - the total supply)", + "default": { + "maximum": "1000000000000000", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Uint128" + } + ] + }, "minimum_gateway_pledge": { "description": "Minimum amount a gateway must pledge to get into the system.", "allOf": [ @@ -8135,6 +8329,18 @@ "$ref": "#/definitions/Coin" } ] + }, + "profit_margin": { + "description": "Defines the allowed profit margin range of operators. default: 0% - 100%", + "default": { + "maximum": "1", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Percent" + } + ] } }, "additionalProperties": false, @@ -8154,6 +8360,50 @@ } } }, + "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" + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "RangedValue_for_Percent": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Percent" + }, + "minimum": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "RangedValue_for_Uint128": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Uint128" + }, + "minimum": { + "$ref": "#/definitions/Uint128" + } + }, + "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" diff --git a/contracts/mixnet/schema/raw/execute.json b/contracts/mixnet/schema/raw/execute.json index c4f1817fc9b..a4ad3932433 100644 --- a/contracts/mixnet/schema/raw/execute.json +++ b/contracts/mixnet/schema/raw/execute.json @@ -1055,6 +1055,18 @@ "minimum_mixnode_pledge" ], "properties": { + "interval_operating_cost": { + "description": "Defines the allowed interval operating cost range of operators. default: 0 - 1'000'000'000'000'000 (1 Billion native tokens - the total supply)", + "default": { + "maximum": "1000000000000000", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Uint128" + } + ] + }, "minimum_gateway_pledge": { "description": "Minimum amount a gateway must pledge to get into the system.", "allOf": [ @@ -1081,6 +1093,18 @@ "$ref": "#/definitions/Coin" } ] + }, + "profit_margin": { + "description": "Defines the allowed profit margin range of operators. default: 0% - 100%", + "default": { + "maximum": "1", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Percent" + } + ] } }, "additionalProperties": false @@ -1415,6 +1439,38 @@ } ] }, + "RangedValue_for_Percent": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Percent" + }, + "minimum": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "RangedValue_for_Uint128": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Uint128" + }, + "minimum": { + "$ref": "#/definitions/Uint128" + } + }, + "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" diff --git a/contracts/mixnet/schema/raw/instantiate.json b/contracts/mixnet/schema/raw/instantiate.json index 60cf202f081..e025d9f11f8 100644 --- a/contracts/mixnet/schema/raw/instantiate.json +++ b/contracts/mixnet/schema/raw/instantiate.json @@ -22,6 +22,28 @@ "initial_rewarding_params": { "$ref": "#/definitions/InitialRewardingParams" }, + "interval_operating_cost": { + "default": { + "maximum": "1000000000000000", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Uint128" + } + ] + }, + "profit_margin": { + "default": { + "maximum": "1", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Percent" + } + ] + }, "rewarding_denom": { "type": "string" }, @@ -108,6 +130,42 @@ "$ref": "#/definitions/Decimal" } ] + }, + "RangedValue_for_Percent": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Percent" + }, + "minimum": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "RangedValue_for_Uint128": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Uint128" + }, + "minimum": { + "$ref": "#/definitions/Uint128" + } + }, + "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" } } } diff --git a/contracts/mixnet/schema/raw/response_to_get_state.json b/contracts/mixnet/schema/raw/response_to_get_state.json index 0888e30787b..d8c9dc5383e 100644 --- a/contracts/mixnet/schema/raw/response_to_get_state.json +++ b/contracts/mixnet/schema/raw/response_to_get_state.json @@ -77,6 +77,18 @@ "minimum_mixnode_pledge" ], "properties": { + "interval_operating_cost": { + "description": "Defines the allowed interval operating cost range of operators. default: 0 - 1'000'000'000'000'000 (1 Billion native tokens - the total supply)", + "default": { + "maximum": "1000000000000000", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Uint128" + } + ] + }, "minimum_gateway_pledge": { "description": "Minimum amount a gateway must pledge to get into the system.", "allOf": [ @@ -103,6 +115,62 @@ "$ref": "#/definitions/Coin" } ] + }, + "profit_margin": { + "description": "Defines the allowed profit margin range of operators. default: 0% - 100%", + "default": { + "maximum": "1", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Percent" + } + ] + } + }, + "additionalProperties": false + }, + "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" + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "RangedValue_for_Percent": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Percent" + }, + "minimum": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "RangedValue_for_Uint128": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Uint128" + }, + "minimum": { + "$ref": "#/definitions/Uint128" } }, "additionalProperties": false diff --git a/contracts/mixnet/schema/raw/response_to_get_state_params.json b/contracts/mixnet/schema/raw/response_to_get_state_params.json index 6e05de30351..52d01911675 100644 --- a/contracts/mixnet/schema/raw/response_to_get_state_params.json +++ b/contracts/mixnet/schema/raw/response_to_get_state_params.json @@ -8,6 +8,18 @@ "minimum_mixnode_pledge" ], "properties": { + "interval_operating_cost": { + "description": "Defines the allowed interval operating cost range of operators. default: 0 - 1'000'000'000'000'000 (1 Billion native tokens - the total supply)", + "default": { + "maximum": "1000000000000000", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Uint128" + } + ] + }, "minimum_gateway_pledge": { "description": "Minimum amount a gateway must pledge to get into the system.", "allOf": [ @@ -34,6 +46,18 @@ "$ref": "#/definitions/Coin" } ] + }, + "profit_margin": { + "description": "Defines the allowed profit margin range of operators. default: 0% - 100%", + "default": { + "maximum": "1", + "minimum": "0" + }, + "allOf": [ + { + "$ref": "#/definitions/RangedValue_for_Percent" + } + ] } }, "additionalProperties": false, @@ -53,6 +77,50 @@ } } }, + "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" + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "RangedValue_for_Percent": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Percent" + }, + "minimum": { + "$ref": "#/definitions/Percent" + } + }, + "additionalProperties": false + }, + "RangedValue_for_Uint128": { + "type": "object", + "required": [ + "maximum", + "minimum" + ], + "properties": { + "maximum": { + "$ref": "#/definitions/Uint128" + }, + "minimum": { + "$ref": "#/definitions/Uint128" + } + }, + "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" diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index e6c755d05d6..c16c6938acd 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -11,7 +11,8 @@ use cosmwasm_std::{ }; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::{ - ContractState, ContractStateParams, ExecuteMsg, InstantiateMsg, Interval, MigrateMsg, QueryMsg, + ContractState, ContractStateParams, ExecuteMsg, InstantiateMsg, Interval, MigrateMsg, + OperatingCostRange, ProfitMarginRange, QueryMsg, }; use nym_contracts_common::set_build_information; @@ -24,6 +25,8 @@ fn default_initial_state( rewarding_validator_address: Addr, rewarding_denom: String, vesting_contract_address: Addr, + profit_margin: ProfitMarginRange, + interval_operating_cost: OperatingCostRange, ) -> ContractState { ContractState { owner, @@ -40,6 +43,8 @@ fn default_initial_state( denom: rewarding_denom, amount: INITIAL_GATEWAY_PLEDGE_AMOUNT, }, + profit_margin, + interval_operating_cost, }, } } @@ -71,6 +76,8 @@ pub fn instantiate( rewarding_validator_address.clone(), 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); @@ -631,7 +638,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; @@ -657,6 +664,14 @@ mod tests { rewarded_set_size: 543, active_set_size: 123, }, + profit_margin: ProfitMarginRange { + 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", &[]); @@ -678,6 +693,14 @@ mod tests { denom: "uatom".into(), amount: INITIAL_GATEWAY_PLEDGE_AMOUNT, }, + profit_margin: ProfitMarginRange { + 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 1fbedf4d623..ca939d43342 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/queries.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/queries.rs @@ -45,6 +45,8 @@ pub(crate) mod tests { minimum_mixnode_delegation: None, 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 4bb85c1025e..278b6baacfd 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; +use mixnet_contract_common::{ContractState, OperatingCostRange, ProfitMarginRange}; pub(crate) const CONTRACT_STATE: Item<'_, ContractState> = Item::new(CONTRACT_STATE_KEY); @@ -28,6 +28,22 @@ pub(crate) fn minimum_gateway_pledge(storage: &dyn Storage) -> Result Result { + Ok(CONTRACT_STATE + .load(storage) + .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 5aed1d45eae..19b753189ec 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs @@ -121,6 +121,8 @@ pub mod tests { denom, 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 032ef29bc92..ed3ee1d013c 100644 --- a/contracts/mixnet/src/mixnodes/transactions.rs +++ b/contracts/mixnet/src/mixnodes/transactions.rs @@ -25,7 +25,8 @@ 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_proxy_match, ensure_sent_by_vesting_contract, + ensure_no_pending_pledge_changes, ensure_operating_cost_within_range, + ensure_profit_margin_within_range, ensure_proxy_match, ensure_sent_by_vesting_contract, validate_pledge, }; @@ -121,6 +122,12 @@ fn _try_add_mixnode( owner_signature: MessageSignature, proxy: Option, ) -> Result { + // 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(pledge, minimum_pledge)?; @@ -477,6 +484,12 @@ pub(crate) fn _try_update_mixnode_cost_params( ensure_proxy_match(&proxy, &existing_bond.proxy)?; ensure_bonded(&existing_bond)?; + // 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, &owner, diff --git a/contracts/mixnet/src/rewards/transactions.rs b/contracts/mixnet/src/rewards/transactions.rs index 9aa417ae7ca..b85b47dae0e 100644 --- a/contracts/mixnet/src/rewards/transactions.rs +++ b/contracts/mixnet/src/rewards/transactions.rs @@ -111,6 +111,14 @@ pub(crate) fn try_reward_mixnode( ); } + // make sure node's profit margin is within the allowed range, + // if not adjust it accordingly + 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 12c02a675a0..1ffe609d60d 100644 --- a/contracts/mixnet/src/support/helpers.rs +++ b/contracts/mixnet/src/support/helpers.rs @@ -8,6 +8,7 @@ use cosmwasm_std::{wasm_execute, Addr, BankMsg, Coin, CosmosMsg, MessageInfo, Re use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::mixnode::PendingMixNodeChanges; use mixnet_contract_common::{EpochState, EpochStatus, IdentityKeyRef, MixId, MixNodeBond}; +use nym_contracts_common::Percent; use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; // helper trait to attach `Msg` to a response if it's provided @@ -431,3 +432,34 @@ pub(crate) fn decode_ed25519_identity_key( Ok(public_key) } + +pub(crate) fn ensure_profit_margin_within_range( + storage: &dyn Storage, + profit_margin: Percent, +) -> Result<(), MixnetContractError> { + let range = mixnet_params_storage::profit_margin_range(storage)?; + if !range.within_range(profit_margin) { + return Err(MixnetContractError::ProfitMarginOutsideRange { + provided: profit_margin, + 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 2bd98eb1753..a37be6d22b7 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -1258,6 +1258,8 @@ pub mod test_helpers { epochs_in_interval: 720, 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", &[]); diff --git a/nym-wallet/nym-wallet-types/Cargo.toml b/nym-wallet/nym-wallet-types/Cargo.toml index f9cb2d4552a..9bfd3717b94 100644 --- a/nym-wallet/nym-wallet-types/Cargo.toml +++ b/nym-wallet/nym-wallet-types/Cargo.toml @@ -12,7 +12,7 @@ serde_json = "1.0" strum = { version = "0.23", features = ["derive"] } ts-rs = "7.0.0" -cosmwasm-std = "1.3.0" +cosmwasm-std = "1.4.3" cosmrs = "=0.15.0" nym-config = { path = "../../common/config" } diff --git a/nym-wallet/nym-wallet-types/src/admin.rs b/nym-wallet/nym-wallet-types/src/admin.rs index 23fc67cecbc..0aac089ee2f 100644 --- a/nym-wallet/nym-wallet-types/src/admin.rs +++ b/nym-wallet/nym-wallet-types/src/admin.rs @@ -1,7 +1,11 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_mixnet_contract_common::ContractStateParams; +use cosmwasm_std::Coin; +use nym_mixnet_contract_common::{ + ContractStateParams, OperatingCostRange as ContractOperatingCostRange, + ProfitMarginRange as ContractProfitMarginRange, +}; use nym_types::currency::{DecCoin, RegisteredCoins}; use nym_types::error::TypesError; use serde::{Deserialize, Serialize}; @@ -16,6 +20,31 @@ pub struct TauriContractStateParams { minimum_mixnode_pledge: DecCoin, minimum_gateway_pledge: DecCoin, minimum_mixnode_delegation: Option, + + operating_cost: TauriOperatingCostRange, + profit_margin: TauriProfitMarginRange, +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "nym-wallet/src/types/rust/OperatingCostRange.ts") +)] +#[derive(Serialize, Deserialize, Debug)] +pub struct TauriOperatingCostRange { + minimum: DecCoin, + maximum: DecCoin, +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "nym-wallet/src/types/rust/ProfitMarginRange.ts") +)] +#[derive(Serialize, Deserialize, Debug)] +pub struct TauriProfitMarginRange { + minimum: String, + maximum: String, } impl TauriContractStateParams { @@ -23,6 +52,16 @@ impl TauriContractStateParams { state_params: ContractStateParams, reg: &RegisteredCoins, ) -> Result { + let rewarding_denom = &state_params.minimum_mixnode_pledge.denom; + let min_operating_cost_c = Coin { + denom: rewarding_denom.into(), + amount: state_params.interval_operating_cost.minimum, + }; + let max_operating_cost_c = Coin { + denom: rewarding_denom.into(), + amount: state_params.interval_operating_cost.maximum, + }; + Ok(TauriContractStateParams { minimum_mixnode_pledge: reg .attempt_convert_to_display_dec_coin(state_params.minimum_mixnode_pledge.into())?, @@ -32,6 +71,15 @@ impl TauriContractStateParams { .minimum_mixnode_delegation .map(|min_del| reg.attempt_convert_to_display_dec_coin(min_del.into())) .transpose()?, + + operating_cost: TauriOperatingCostRange { + minimum: reg.attempt_convert_to_display_dec_coin(min_operating_cost_c.into())?, + maximum: reg.attempt_convert_to_display_dec_coin(max_operating_cost_c.into())?, + }, + profit_margin: TauriProfitMarginRange { + minimum: state_params.profit_margin.minimum.to_string(), + maximum: state_params.profit_margin.maximum.to_string(), + }, }) } @@ -39,6 +87,14 @@ impl TauriContractStateParams { self, reg: &RegisteredCoins, ) -> Result { + assert_eq!( + self.operating_cost.maximum.denom, + self.operating_cost.minimum.denom + ); + + let min_operating_cost_c = reg.attempt_convert_to_base_coin(self.operating_cost.minimum)?; + let max_operating_cost_c = reg.attempt_convert_to_base_coin(self.operating_cost.maximum)?; + Ok(ContractStateParams { minimum_mixnode_delegation: self .minimum_mixnode_delegation @@ -51,6 +107,15 @@ impl TauriContractStateParams { minimum_gateway_pledge: reg .attempt_convert_to_base_coin(self.minimum_gateway_pledge)? .into(), + + profit_margin: ContractProfitMarginRange { + minimum: self.profit_margin.minimum.parse()?, + maximum: self.profit_margin.maximum.parse()?, + }, + interval_operating_cost: ContractOperatingCostRange { + minimum: min_operating_cost_c.amount.into(), + maximum: max_operating_cost_c.amount.into(), + }, }) } }