diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs index 9b5761bed63..26a766a1908 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs @@ -683,6 +683,24 @@ pub trait MixnetSigningClient { .await } + async fn migrate_vested_mixnode(&self, fee: Option) -> Result { + self.execute_mixnet_contract(fee, MixnetExecuteMsg::MigrateVestedMixNode {}, vec![]) + .await + } + + async fn migrate_vested_delegation( + &self, + mix_id: MixId, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::MigrateVestedDelegation { mix_id }, + vec![], + ) + .await + } + #[cfg(feature = "contract-testing")] async fn testing_resolve_all_pending_events( &self, @@ -928,6 +946,12 @@ mod tests { MixnetExecuteMsg::WithdrawDelegatorRewardOnBehalf { mix_id, owner } => client .withdraw_delegator_reward_on_behalf(owner.parse().unwrap(), mix_id, None) .ignore(), + MixnetExecuteMsg::MigrateVestedMixNode { .. } => { + client.migrate_vested_mixnode(None).ignore() + } + MixnetExecuteMsg::MigrateVestedDelegation { mix_id } => { + client.migrate_vested_delegation(mix_id, None).ignore() + } #[cfg(feature = "contract-testing")] MixnetExecuteMsg::TestingResolveAllPendingEvents { .. } => { diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs index 11c449fccd1..07472c4262b 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs @@ -437,6 +437,7 @@ where mod tests { use super::*; use crate::nyxd::contract_traits::tests::{mock_coin, IgnoreValue}; + use nym_vesting_contract_common::ExecuteMsg; // it's enough that this compiles and clippy is happy about it #[allow(dead_code)] @@ -560,6 +561,9 @@ mod tests { VestingExecuteMsg::UpdateLockedPledgeCap { address, cap } => client .update_locked_pledge_cap(address.parse().unwrap(), cap, None) .ignore(), + // those will never be manually called by clients + ExecuteMsg::TrackMigratedMixnode { .. } => "explicitly_ignored".ignore(), + ExecuteMsg::TrackMigratedDelegation { .. } => "explicitly_ignored".ignore(), }; } } 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/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs b/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs new file mode 100644 index 00000000000..8523e63639d --- /dev/null +++ b/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs @@ -0,0 +1,42 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use nym_mixnet_contract_common::MixId; +use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub mix_id: Option, + + #[clap(long)] + pub identity_key: Option, +} + +pub async fn migrate_vested_delegation(args: Args, client: SigningClient) { + let mix_id = match args.mix_id { + Some(mix_id) => mix_id, + None => { + let identity_key = args + .identity_key + .expect("either mix_id or mix_identity has to be specified"); + let node_details = client + .get_mixnode_details_by_identity(identity_key) + .await + .expect("contract query failed") + .mixnode_details + .expect("mixnode with the specified identity doesnt exist"); + node_details.mix_id() + } + }; + + let res = client + .migrate_vested_delegation(mix_id, None) + .await + .expect("failed to migrate delegation!"); + + info!("migration result: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/delegators/mod.rs b/common/commands/src/validator/mixnet/delegators/mod.rs index f2cf156c0cb..17a5f9fa5c4 100644 --- a/common/commands/src/validator/mixnet/delegators/mod.rs +++ b/common/commands/src/validator/mixnet/delegators/mod.rs @@ -7,6 +7,7 @@ pub mod rewards; pub mod delegate_to_mixnode; pub mod delegate_to_multiple_mixnodes; +pub mod migrate_vested_delegation; pub mod query_for_delegations; pub mod undelegate_from_mixnode; pub mod vesting_delegate_to_mixnode; @@ -35,4 +36,6 @@ pub enum MixnetDelegatorsCommands { DelegateVesting(vesting_delegate_to_mixnode::Args), /// Undelegate from a mixnode (when originally using locked tokens) UndelegateVesting(vesting_undelegate_from_mixnode::Args), + /// Migrate the delegation to use liquid tokens + MigrateVestedDelegation(migrate_vested_delegation::Args), } diff --git a/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs b/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs index f29dae6db3b..b1a223b6033 100644 --- a/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs +++ b/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs @@ -96,6 +96,7 @@ async fn print_delegation_events(events: Vec, client: &Signin mix_id, amount, proxy, + .. } => { if owner.as_str() == client.nyxd.address().as_ref() { table.add_row(vec![ @@ -111,6 +112,7 @@ async fn print_delegation_events(events: Vec, client: &Signin owner, mix_id, proxy, + .. } => { if owner.as_str() == client.nyxd.address().as_ref() { table.add_row(vec![ diff --git a/common/commands/src/validator/mixnet/operators/gateway/gateway_bonding_sign_payload.rs b/common/commands/src/validator/mixnet/operators/gateway/gateway_bonding_sign_payload.rs index 3845a6c7efe..ba19f618674 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/gateway_bonding_sign_payload.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/gateway_bonding_sign_payload.rs @@ -8,7 +8,7 @@ use cosmwasm_std::Coin; use nym_bin_common::output_format::OutputFormat; use nym_mixnet_contract_common::construct_gateway_bonding_sign_payload; use nym_network_defaults::{DEFAULT_CLIENT_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT}; -use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, NymContractsProvider}; +use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; #[derive(Debug, Parser)] pub struct Args { @@ -39,10 +39,6 @@ pub struct Args { )] pub amount: u128, - /// Indicates whether the gateway is going to get bonded via a vesting account - #[arg(long)] - pub with_vesting_account: bool, - #[clap(short, long, default_value_t = OutputFormat::default())] output: OutputFormat, } @@ -74,15 +70,8 @@ pub async fn create_payload(args: Args, client: SigningClient) { }; let address = account_id_to_cw_addr(&client.address()); - let proxy = if args.with_vesting_account { - Some(account_id_to_cw_addr( - client.vesting_contract_address().unwrap(), - )) - } else { - None - }; - let payload = construct_gateway_bonding_sign_payload(nonce, address, proxy, coin, gateway); + let payload = construct_gateway_bonding_sign_payload(nonce, address, coin, gateway); let wrapper = DataWrapper::new(payload.to_base58_string().unwrap()); println!("{}", args.output.format(&wrapper)) } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/create_family.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/create_family.rs index 6acaca80d28..6e2c664a35b 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/create_family.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/families/create_family.rs @@ -5,33 +5,21 @@ use crate::context::SigningClient; use clap::Parser; use log::info; use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; -use nym_validator_client::nyxd::contract_traits::VestingSigningClient; #[derive(Debug, Parser)] pub struct Args { /// Label that is going to be used for creating the family #[arg(long)] pub family_label: String, - - /// Indicates whether the family is going to get created via a vesting account - #[arg(long)] - pub with_vesting_account: bool, } pub async fn create_family(args: Args, client: SigningClient) { info!("Create family"); - let res = if args.with_vesting_account { - client - .vesting_create_family(args.family_label, None) - .await - .expect("failed to create family with vesting account") - } else { - client - .create_family(args.family_label, None) - .await - .expect("failed to create family") - }; + let res = client + .create_family(args.family_label, None) + .await + .expect("failed to create family"); info!("Family creation result: {:?}", res); } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/create_family_join_permit_sign_payload.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/create_family_join_permit_sign_payload.rs index c81dc35f690..ac397150ea7 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/create_family_join_permit_sign_payload.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/families/create_family_join_permit_sign_payload.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::context::QueryClient; -use crate::utils::{account_id_to_cw_addr, DataWrapper}; +use crate::utils::DataWrapper; use clap::Parser; use cosmrs::AccountId; use log::info; @@ -10,7 +10,7 @@ use nym_bin_common::output_format::OutputFormat; use nym_crypto::asymmetric::identity; use nym_mixnet_contract_common::construct_family_join_permit; use nym_mixnet_contract_common::families::FamilyHead; -use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, NymContractsProvider}; +use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; #[derive(Debug, Parser)] pub struct Args { @@ -18,10 +18,6 @@ pub struct Args { #[arg(long)] pub address: AccountId, - /// Indicates whether the member joining the family is going to use the vesting account for joining. - #[arg(long)] - pub with_vesting_account: bool, - // might as well validate the value when parsing the arguments /// Identity of the member for whom we're issuing the permit #[arg(long)] @@ -68,18 +64,9 @@ pub async fn create_family_join_permit_sign_payload(args: Args, client: QueryCli } }; - // let address = account_id_to_cw_addr(&args.address); - let proxy = if args.with_vesting_account { - Some(account_id_to_cw_addr( - client.vesting_contract_address().unwrap(), - )) - } else { - None - }; - let head = FamilyHead::new(mixnode.bond_information.identity()); - let payload = construct_family_join_permit(nonce, head, proxy, args.member.to_base58_string()); + let payload = construct_family_join_permit(nonce, head, args.member.to_base58_string()); let wrapper = DataWrapper::new(payload.to_base58_string().unwrap()); println!("{}", args.output.format(&wrapper)) } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/join_family.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/join_family.rs index 411a8412b70..08b4c471c06 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/join_family.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/families/join_family.rs @@ -8,7 +8,6 @@ use nym_contracts_common::signing::MessageSignature; use nym_crypto::asymmetric::identity; use nym_mixnet_contract_common::families::FamilyHead; use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; -use nym_validator_client::nyxd::contract_traits::VestingSigningClient; #[derive(Debug, Parser)] pub struct Args { @@ -16,10 +15,6 @@ pub struct Args { #[arg(long)] pub family_head: identity::PublicKey, - /// Indicates whether the member joining the family is going to do so via the vesting contract - #[arg(long)] - pub with_vesting_account: bool, - /// Permission, as provided by the family head, for joining the family #[arg(long)] pub join_permit: MessageSignature, @@ -30,17 +25,10 @@ pub async fn join_family(args: Args, client: SigningClient) { let family_head = FamilyHead::new(args.family_head.to_base58_string()); - let res = if args.with_vesting_account { - client - .vesting_join_family(args.join_permit, family_head, None) - .await - .expect("failed to join family with vesting account") - } else { - client - .join_family(args.join_permit, family_head, None) - .await - .expect("failed to join family") - }; + let res = client + .join_family(args.join_permit, family_head, None) + .await + .expect("failed to join family"); info!("Family join result: {:?}", res); } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/leave_family.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/leave_family.rs index 0673be508ae..d9c31e39330 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/leave_family.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/families/leave_family.rs @@ -7,17 +7,12 @@ use log::info; use nym_crypto::asymmetric::identity; use nym_mixnet_contract_common::families::FamilyHead; use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; -use nym_validator_client::nyxd::contract_traits::VestingSigningClient; #[derive(Debug, Parser)] pub struct Args { /// The head of the family that we intend to leave #[arg(long)] pub family_head: identity::PublicKey, - - /// Indicates whether we joined the family via the vesting contract - #[arg(long)] - pub with_vesting_account: bool, } pub async fn leave_family(args: Args, client: SigningClient) { @@ -25,17 +20,10 @@ pub async fn leave_family(args: Args, client: SigningClient) { let family_head = FamilyHead::new(args.family_head.to_base58_string()); - let res = if args.with_vesting_account { - client - .vesting_leave_family(family_head, None) - .await - .expect("failed to leave family with vesting account") - } else { - client - .leave_family(family_head, None) - .await - .expect("failed to leave family") - }; + let res = client + .leave_family(family_head, None) + .await + .expect("failed to leave family"); info!("Family leave result: {:?}", res); } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/migrate_vested_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/migrate_vested_mixnode.rs new file mode 100644 index 00000000000..95bd9d0573c --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/migrate_vested_mixnode.rs @@ -0,0 +1,19 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; + +#[derive(Debug, Parser)] +pub struct Args {} + +pub async fn migrate_vested_mixnode(_args: Args, client: SigningClient) { + let res = client + .migrate_vested_mixnode(None) + .await + .expect("failed to migrate mixnode!"); + + info!("migration result: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/mixnode_bonding_sign_payload.rs b/common/commands/src/validator/mixnet/operators/mixnode/mixnode_bonding_sign_payload.rs index 332de7614eb..a492b3a0b6d 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/mixnode_bonding_sign_payload.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/mixnode_bonding_sign_payload.rs @@ -11,7 +11,7 @@ use nym_mixnet_contract_common::{construct_mixnode_bonding_sign_payload, MixNode use nym_network_defaults::{ DEFAULT_HTTP_API_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT, }; -use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, NymContractsProvider}; +use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; use nym_validator_client::nyxd::CosmWasmCoin; #[derive(Debug, Parser)] @@ -52,10 +52,6 @@ pub struct Args { )] pub amount: u128, - /// Indicates whether the mixnode is going to get bonded via a vesting account - #[arg(long)] - pub with_vesting_account: bool, - #[clap(short, long, default_value_t = OutputFormat::default())] output: OutputFormat, } @@ -100,16 +96,9 @@ pub async fn create_payload(args: Args, client: SigningClient) { }; let address = account_id_to_cw_addr(&client.address()); - let proxy = if args.with_vesting_account { - Some(account_id_to_cw_addr( - client.vesting_contract_address().unwrap(), - )) - } else { - None - }; let payload = - construct_mixnode_bonding_sign_payload(nonce, address, proxy, coin, mixnode, cost_params); + construct_mixnode_bonding_sign_payload(nonce, address, coin, mixnode, cost_params); let wrapper = DataWrapper::new(payload.to_base58_string().unwrap()); println!("{}", args.output.format(&wrapper)) } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/mod.rs b/common/commands/src/validator/mixnet/operators/mixnode/mod.rs index 6e5283d77e1..abb5060e9b2 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/mod.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/mod.rs @@ -7,6 +7,7 @@ pub mod bond_mixnode; pub mod decrease_pledge; pub mod families; pub mod keys; +pub mod migrate_vested_mixnode; pub mod mixnode_bonding_sign_payload; pub mod pledge_more; pub mod rewards; @@ -52,4 +53,6 @@ pub enum MixnetOperatorsMixnodeCommands { DecreasePledge(decrease_pledge::Args), /// Decrease pledge with locked tokens DecreasePledgeVesting(vesting_decrease_pledge::Args), + /// Migrate the mixnode to use liquid tokens + MigrateVestedNode(migrate_vested_mixnode::Args), } diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/signing/mod.rs b/common/cosmwasm-smart-contracts/contracts-common/src/signing/mod.rs index 23018c92492..d2722b2f597 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/signing/mod.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/signing/mod.rs @@ -218,7 +218,6 @@ where #[derive(Serialize)] pub struct ContractMessageContent { pub sender: Addr, - pub proxy: Option, pub funds: Vec, pub data: T, } @@ -233,25 +232,17 @@ where } impl ContractMessageContent { - pub fn new(sender: Addr, proxy: Option, funds: Vec, data: T) -> Self { + pub fn new(sender: Addr, funds: Vec, data: T) -> Self { ContractMessageContent { sender, - proxy, funds, data, } } pub fn new_with_info(info: MessageInfo, signer: Addr, data: T) -> Self { - let proxy = if info.sender == signer { - None - } else { - Some(info.sender) - }; - ContractMessageContent { sender: signer, - proxy, funds: info.funds, data, } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs index 3a7cdd77025..f30dc63a73f 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs @@ -65,7 +65,6 @@ impl Delegation { cumulative_reward_ratio: Decimal, amount: Coin, height: u64, - proxy: Option, ) -> Self { assert!( amount.amount <= TOKEN_SUPPLY, @@ -78,7 +77,7 @@ impl Delegation { cumulative_reward_ratio, amount, height, - proxy, + proxy: None, } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs index ec6e79c65f5..710ca20b160 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; @@ -76,21 +77,11 @@ pub enum MixnetContractError { #[error("Received multiple coin types during staking")] MultipleDenoms, - #[error("Proxy address mismatch, expected {existing}, got {incoming}")] - ProxyMismatch { existing: String, incoming: String }, - #[error("Proxy address ({received}) is not set to the vesting contract ({vesting_contract})")] ProxyIsNotVestingContract { received: Addr, vesting_contract: Addr, }, - #[error( - "Sender of this message ({received}) is not the vesting contract ({vesting_contract})" - )] - SenderIsNotVestingContract { - received: Addr, - vesting_contract: Addr, - }, #[error("Failed to recover ed25519 public key from its base58 representation - {0}")] MalformedEd25519IdentityKey(String), @@ -239,6 +230,30 @@ pub enum MixnetContractError { #[from] source: ApiVerifierError, }, + + #[error("this operation is no longer allowed to be performed with vesting tokens. please move them to your liquid balance and try again")] + DisabledVestingOperation, + + #[error( + "this mixnode has not been bonded with the vesting tokens or has already been migrated" + )] + NotAVestingMixnode, + + #[error("this delegation has not been performed with the vesting tokens or has already been migrated")] + NotAVestingDelegation, + + #[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/events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs index 365adf9df3f..7ebb604225f 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs @@ -103,7 +103,6 @@ impl Display for MixnetEventType { // attributes that are used in multiple places pub const OWNER_KEY: &str = "owner"; pub const AMOUNT_KEY: &str = "amount"; -pub const PROXY_KEY: &str = "proxy"; // event-specific attributes @@ -163,7 +162,6 @@ pub const NEW_EPOCHS_IN_INTERVAL: &str = "new_epochs_in_interval"; pub fn new_delegation_event( created_at: BlockHeight, delegator: &Addr, - proxy: &Option, amount: &Coin, mix_id: MixId, unit_reward: Decimal, @@ -171,58 +169,34 @@ pub fn new_delegation_event( Event::new(MixnetEventType::Delegation) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) .add_attribute(DELEGATOR_KEY, delegator) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(AMOUNT_KEY, amount.to_string()) .add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string()) .add_attribute(UNIT_REWARD_KEY, unit_reward.to_string()) } -pub fn new_delegation_on_unbonded_node_event( - delegator: &Addr, - proxy: &Option, - mix_id: MixId, -) -> Event { +pub fn new_delegation_on_unbonded_node_event(delegator: &Addr, mix_id: MixId) -> Event { Event::new(MixnetEventType::Delegation) .add_attribute(DELEGATOR_KEY, delegator) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string()) } -pub fn new_pending_delegation_event( - delegator: &Addr, - proxy: &Option, - amount: &Coin, - mix_id: MixId, -) -> Event { +pub fn new_pending_delegation_event(delegator: &Addr, amount: &Coin, mix_id: MixId) -> Event { Event::new(MixnetEventType::PendingDelegation) .add_attribute(DELEGATOR_KEY, delegator) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(AMOUNT_KEY, amount.to_string()) .add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string()) } -pub fn new_withdraw_operator_reward_event( - owner: &Addr, - proxy: &Option, - amount: Coin, - mix_id: MixId, -) -> Event { +pub fn new_withdraw_operator_reward_event(owner: &Addr, amount: Coin, mix_id: MixId) -> Event { Event::new(MixnetEventType::WithdrawOperatorReward) .add_attribute(OWNER_KEY, owner.as_str()) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(AMOUNT_KEY, amount.to_string()) .add_attribute(MIX_ID_KEY, mix_id.to_string()) } -pub fn new_withdraw_delegator_reward_event( - delegator: &Addr, - proxy: &Option, - amount: Coin, - mix_id: MixId, -) -> Event { +pub fn new_withdraw_delegator_reward_event(delegator: &Addr, amount: Coin, mix_id: MixId) -> Event { Event::new(MixnetEventType::WithdrawDelegatorReward) .add_attribute(DELEGATOR_KEY, delegator) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(AMOUNT_KEY, amount.to_string()) .add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string()) } @@ -278,59 +252,43 @@ pub fn new_pending_rewarding_params_update_event( ) } -pub fn new_undelegation_event( - created_at: BlockHeight, - delegator: &Addr, - proxy: &Option, - mix_id: MixId, -) -> Event { +pub fn new_undelegation_event(created_at: BlockHeight, delegator: &Addr, mix_id: MixId) -> Event { Event::new(MixnetEventType::Undelegation) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) .add_attribute(DELEGATOR_KEY, delegator) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(MIX_ID_KEY, mix_id.to_string()) } -pub fn new_pending_undelegation_event( - delegator: &Addr, - proxy: &Option, - mix_id: MixId, -) -> Event { +pub fn new_pending_undelegation_event(delegator: &Addr, mix_id: MixId) -> Event { Event::new(MixnetEventType::PendingUndelegation) .add_attribute(DELEGATOR_KEY, delegator) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(MIX_ID_KEY, mix_id.to_string()) } pub fn new_gateway_bonding_event( owner: &Addr, - proxy: &Option, amount: &Coin, identity: IdentityKeyRef<'_>, ) -> Event { Event::new(MixnetEventType::GatewayBonding) .add_attribute(OWNER_KEY, owner) .add_attribute(NODE_IDENTITY_KEY, identity) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(AMOUNT_KEY, amount.to_string()) } pub fn new_gateway_unbonding_event( owner: &Addr, - proxy: &Option, amount: &Coin, identity: IdentityKeyRef<'_>, ) -> Event { Event::new(MixnetEventType::GatewayUnbonding) .add_attribute(OWNER_KEY, owner) .add_attribute(NODE_IDENTITY_KEY, identity) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(AMOUNT_KEY, amount.to_string()) } pub fn new_mixnode_bonding_event( owner: &Addr, - proxy: &Option, amount: &Coin, identity: IdentityKeyRef<'_>, mix_id: MixId, @@ -341,7 +299,6 @@ pub fn new_mixnode_bonding_event( .add_attribute(MIX_ID_KEY, mix_id.to_string()) .add_attribute(NODE_IDENTITY_KEY, identity) .add_attribute(OWNER_KEY, owner) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(ASSIGNED_LAYER_KEY, assigned_layer) .add_attribute(AMOUNT_KEY, amount.to_string()) } @@ -380,7 +337,6 @@ pub fn new_mixnode_unbonding_event(created_at: BlockHeight, mix_id: MixId) -> Ev pub fn new_pending_mixnode_unbonding_event( owner: &Addr, - proxy: &Option, identity: IdentityKeyRef<'_>, mix_id: MixId, ) -> Event { @@ -388,43 +344,33 @@ pub fn new_pending_mixnode_unbonding_event( .add_attribute(MIX_ID_KEY, mix_id.to_string()) .add_attribute(NODE_IDENTITY_KEY, identity) .add_attribute(OWNER_KEY, owner) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) } pub fn new_mixnode_config_update_event( mix_id: MixId, owner: &Addr, - proxy: &Option, update: &MixNodeConfigUpdate, ) -> Event { Event::new(MixnetEventType::MixnodeConfigUpdate) .add_attribute(MIX_ID_KEY, mix_id.to_string()) .add_attribute(OWNER_KEY, owner) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(UPDATED_MIXNODE_CONFIG_KEY, update.to_inline_json()) } -pub fn new_gateway_config_update_event( - owner: &Addr, - proxy: &Option, - update: &GatewayConfigUpdate, -) -> Event { +pub fn new_gateway_config_update_event(owner: &Addr, update: &GatewayConfigUpdate) -> Event { Event::new(MixnetEventType::GatewayConfigUpdate) .add_attribute(OWNER_KEY, owner) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(UPDATED_GATEWAY_CONFIG_KEY, update.to_inline_json()) } pub fn new_mixnode_pending_cost_params_update_event( mix_id: MixId, owner: &Addr, - proxy: &Option, new_costs: &MixNodeCostParams, ) -> Event { Event::new(MixnetEventType::PendingMixnodeCostParamsUpdate) .add_attribute(MIX_ID_KEY, mix_id.to_string()) .add_attribute(OWNER_KEY, owner) - .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(UPDATED_MIXNODE_COST_PARAMS_KEY, new_costs.to_inline_json()) } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/families.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/families.rs index 5b44483c830..58f74f4e087 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/families.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/families.rs @@ -3,7 +3,6 @@ use crate::{IdentityKey, IdentityKeyRef}; use cosmwasm_schema::cw_serde; -use cosmwasm_std::Addr; use schemars::JsonSchema; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::fmt::{Display, Formatter}; @@ -84,10 +83,10 @@ impl FamilyHead { } impl Family { - pub fn new(head: FamilyHead, proxy: Option, label: String) -> Self { + pub fn new(head: FamilyHead, label: String) -> Self { Family { head, - proxy: proxy.map(|p| p.to_string()), + proxy: None, label, } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs index fd29090374c..ff991853fb5 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs @@ -55,19 +55,13 @@ pub struct GatewayBond { } impl GatewayBond { - pub fn new( - pledge_amount: Coin, - owner: Addr, - block_height: u64, - gateway: Gateway, - proxy: Option, - ) -> Self { + pub fn new(pledge_amount: Coin, owner: Addr, block_height: u64, gateway: Gateway) -> Self { GatewayBond { pledge_amount, owner, block_height, gateway, - proxy, + proxy: None, } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index a09d21dd2d2..87727631506 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 @@ -518,7 +531,6 @@ impl MixNodeBond { original_pledge: Coin, layer: Layer, mix_node: MixNode, - proxy: Option, bonding_height: u64, ) -> Self { MixNodeBond { @@ -527,7 +539,7 @@ impl MixNodeBond { original_pledge, layer, mix_node, - proxy, + proxy: None, bonding_height, is_unbonding: false, } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index ca154e231b6..90110e0dc6b 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] @@ -269,6 +276,12 @@ pub enum ExecuteMsg { owner: String, }, + // vesting migration: + MigrateVestedMixNode {}, + MigrateVestedDelegation { + mix_id: MixId, + }, + // testing-only #[cfg(feature = "contract-testing")] TestingResolveAllPendingEvents { @@ -381,6 +394,9 @@ impl ExecuteMsg { ExecuteMsg::WithdrawDelegatorRewardOnBehalf { mix_id, .. } => { format!("withdrawing delegator reward from mixnode {mix_id} on behalf") } + ExecuteMsg::MigrateVestedMixNode { .. } => "migrate vested mixnode".into(), + ExecuteMsg::MigrateVestedDelegation { .. } => "migrate vested delegation".to_string(), + #[cfg(feature = "contract-testing")] ExecuteMsg::TestingResolveAllPendingEvents { .. } => { "resolving all pending events".into() diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs index 8a60af94a48..8d97e204944 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs @@ -38,6 +38,7 @@ pub enum PendingEpochEventKind { /// Request to create a delegation towards particular mixnode. /// Note that if such delegation already exists, it will get updated with the provided token amount. #[serde(alias = "Delegate")] + #[non_exhaustive] Delegate { /// The address of the owner of the delegation. owner: Addr, @@ -55,6 +56,7 @@ pub enum PendingEpochEventKind { /// Request to remove delegation from particular mixnode. #[serde(alias = "Undelegate")] + #[non_exhaustive] Undelegate { /// The address of the owner of the delegation. owner: Addr, @@ -109,6 +111,23 @@ impl PendingEpochEventKind { kind: self, } } + + pub fn new_delegate(owner: Addr, mix_id: MixId, amount: Coin) -> Self { + PendingEpochEventKind::Delegate { + owner, + mix_id, + amount, + proxy: None, + } + } + + pub fn new_undelegate(owner: Addr, mix_id: MixId) -> Self { + PendingEpochEventKind::Undelegate { + owner, + mix_id, + proxy: None, + } + } } impl From<(EpochEventId, PendingEpochEventData)> for PendingEpochEvent { diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/simulated_node.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/simulated_node.rs index 5af272366e6..12a9e967bd1 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/simulated_node.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/simulated_node.rs @@ -47,7 +47,6 @@ impl SimulatedNode { self.rewarding_details.total_unit_reward, delegation, 42, - None, ); self.delegations.insert(delegator, delegation); diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs index 57ad49dc96b..84c9b8b4aaf 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs @@ -37,13 +37,12 @@ impl SigningPurpose for MixnodeBondingPayload { pub fn construct_mixnode_bonding_sign_payload( nonce: Nonce, sender: Addr, - proxy: Option, pledge: Coin, mix_node: MixNode, cost_params: MixNodeCostParams, ) -> SignableMixNodeBondingMsg { let payload = MixnodeBondingPayload::new(mix_node, cost_params); - let content = ContractMessageContent::new(sender, proxy, vec![pledge], payload); + let content = ContractMessageContent::new(sender, vec![pledge], payload); SignableMessage::new(nonce, content) } @@ -68,12 +67,11 @@ impl SigningPurpose for GatewayBondingPayload { pub fn construct_gateway_bonding_sign_payload( nonce: Nonce, sender: Addr, - proxy: Option, pledge: Coin, gateway: Gateway, ) -> SignableGatewayBondingMsg { let payload = GatewayBondingPayload::new(gateway); - let content = ContractMessageContent::new(sender, proxy, vec![pledge], payload); + let content = ContractMessageContent::new(sender, vec![pledge], payload); SignableMessage::new(nonce, content) } @@ -82,17 +80,14 @@ pub fn construct_gateway_bonding_sign_payload( pub struct FamilyJoinPermit { // the granter of this permit family_head: FamilyHead, - // whether the **member** will want to join via the proxy (i.e. vesting contract) - proxy: Option, // the actual member we want to permit to join member_node: IdentityKey, } impl FamilyJoinPermit { - pub fn new(family_head: FamilyHead, proxy: Option, member_node: IdentityKey) -> Self { + pub fn new(family_head: FamilyHead, member_node: IdentityKey) -> Self { Self { family_head, - proxy, member_node, } } @@ -107,10 +102,9 @@ impl SigningPurpose for FamilyJoinPermit { pub fn construct_family_join_permit( nonce: Nonce, family_head: FamilyHead, - proxy: Option, member_node: IdentityKey, ) -> SignableFamilyJoinPermitMsg { - let payload = FamilyJoinPermit::new(family_head, proxy, member_node); + let payload = FamilyJoinPermit::new(family_head, member_node); // note: we're NOT wrapping it in `ContractMessageContent` because the family head is not going to be the one // sending the message to the contract 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/cosmwasm-smart-contracts/vesting-contract/src/events.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs index d7dc757956d..b957adbe10c 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs @@ -167,3 +167,11 @@ pub fn new_track_undelegation_event() -> Event { pub fn new_track_reward_event() -> Event { Event::new(TRACK_REWARD_EVENT_TYPE) } + +pub fn new_track_migrate_mixnode_event() -> Event { + Event::new("track_migrate_vesting_mixnode") +} + +pub fn new_track_migrate_delegation_event() -> Event { + Event::new("track_migrate_vesting_delegation") +} diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs index 6593926445f..eefe07e9f15 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs @@ -136,6 +136,14 @@ pub enum ExecuteMsg { address: String, cap: PledgeCap, }, + TrackMigratedMixnode { + owner: String, + }, + // no need to track migrated gateways as there are no vesting gateways on mainnet + TrackMigratedDelegation { + owner: String, + mix_id: MixId, + }, } impl ExecuteMsg { @@ -171,6 +179,10 @@ impl ExecuteMsg { ExecuteMsg::TransferOwnership { .. } => "VestingExecuteMsg::TransferOwnership", ExecuteMsg::UpdateStakingAddress { .. } => "VestingExecuteMsg::UpdateStakingAddress", ExecuteMsg::UpdateLockedPledgeCap { .. } => "VestingExecuteMsg::UpdateLockedPledgeCap", + ExecuteMsg::TrackMigratedMixnode { .. } => "VestingExecuteMsg::TrackMigratedMixnode", + ExecuteMsg::TrackMigratedDelegation { .. } => { + "VestingExecuteMsg::TrackMigratedDelegation" + } } } } 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/common/types/src/pending_events.rs b/common/types/src/pending_events.rs index f5789890bf8..d36e0b978cd 100644 --- a/common/types/src/pending_events.rs +++ b/common/types/src/pending_events.rs @@ -84,6 +84,7 @@ impl PendingEpochEventData { mix_id, amount, proxy, + .. } => Ok(PendingEpochEventData::Delegate { owner: owner.into_string(), mix_id, @@ -94,6 +95,7 @@ impl PendingEpochEventData { owner, mix_id, proxy, + .. } => Ok(PendingEpochEventData::Undelegate { owner: owner.into_string(), mix_id, diff --git a/contracts/mixnet-vesting-integration-tests/src/decrease_mixnode_pledge.rs b/contracts/mixnet-vesting-integration-tests/src/decrease_mixnode_pledge.rs deleted file mode 100644 index a9930e23f79..00000000000 --- a/contracts/mixnet-vesting-integration-tests/src/decrease_mixnode_pledge.rs +++ /dev/null @@ -1,239 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::support::helpers::{mix_coin, mix_coins, vesting_owner}; -use crate::support::setup::{TestSetup, MIX_DENOM}; -use cosmwasm_std::Addr; -use cw_multi_test::Executor; -use nym_contracts_common::Percent; -use nym_mixnet_contract_common::error::MixnetContractError; -use nym_mixnet_contract_common::{ContractStateParams, MixNodeCostParams}; -use nym_mixnet_contract_common::{MixOwnershipResponse, QueryMsg as MixnetQueryMsg}; -use nym_vesting_contract_common::{ExecuteMsg as VestingExecuteMsg, VestingContractError}; - -#[test] -fn decrease_mixnode_pledge_from_vesting_account_with_minimum_pledge() { - let mut test = TestSetup::new_simple(); - let vesting_account = "vesting-account"; - - // 0. get the minimum pledge amount - let state_params: ContractStateParams = test - .app - .wrap() - .query_wasm_smart(test.mixnet_contract(), &MixnetQueryMsg::GetStateParams {}) - .unwrap(); - let minimum_pledge = state_params.minimum_mixnode_pledge; - - // 1. create vesting account - let create_msg = VestingExecuteMsg::CreateAccount { - owner_address: vesting_account.to_string(), - staking_address: None, - vesting_spec: None, - cap: None, - }; - - test.app - .execute_contract( - vesting_owner(), - test.vesting_contract(), - &create_msg, - &mix_coins(1_000_000_000), - ) - .unwrap(); - - // 2. bond mixnode with the vesting account - let pledge = minimum_pledge.clone(); - - let cost_params = MixNodeCostParams { - profit_margin_percent: Percent::from_percentage_value(10).unwrap(), - interval_operating_cost: mix_coin(40_000_000), - }; - - let (mix_node, owner_signature) = test.valid_mixnode_with_sig( - vesting_account, - Some(test.vesting_contract()), - cost_params.clone(), - pledge.clone(), - ); - - let bond_msg = VestingExecuteMsg::BondMixnode { - mix_node, - cost_params, - owner_signature, - amount: pledge.clone(), - }; - test.app - .execute_contract( - Addr::unchecked(vesting_account), - test.vesting_contract(), - &bond_msg, - &[], - ) - .unwrap(); - - // 3. try to decrease the pledge - - // trying to decrease by a zero amount - not valid - let decrease_pledge_msg = VestingExecuteMsg::DecreasePledge { - amount: mix_coin(0), - }; - let res_zero = test - .app - .execute_contract( - Addr::unchecked(vesting_account), - test.vesting_contract(), - &decrease_pledge_msg, - &[], - ) - .unwrap_err(); - - assert_eq!( - VestingContractError::EmptyFunds, - res_zero.downcast().unwrap() - ); - - // trying to go below the cap - also not valid - let amount = mix_coin(50_000); - let decrease_pledge_msg = VestingExecuteMsg::DecreasePledge { - amount: amount.clone(), - }; - let res_below = test - .app - .execute_contract( - Addr::unchecked(vesting_account), - test.vesting_contract(), - &decrease_pledge_msg, - &[], - ) - .unwrap_err(); - assert_eq!( - MixnetContractError::InvalidPledgeReduction { - current: pledge.amount, - decrease_by: amount.amount, - minimum: minimum_pledge.amount, - denom: minimum_pledge.denom - }, - res_below.downcast().unwrap() - ) -} - -#[test] -fn decrease_mixnode_pledge_from_vesting_account_with_sufficient_pledge() { - let mut test = TestSetup::new_simple(); - let vesting_account = "vesting-account"; - - // 1. create vesting account - let create_msg = VestingExecuteMsg::CreateAccount { - owner_address: vesting_account.to_string(), - staking_address: None, - vesting_spec: None, - cap: None, - }; - - test.app - .execute_contract( - vesting_owner(), - test.vesting_contract(), - &create_msg, - &mix_coins(10_000_000_000), - ) - .unwrap(); - - // 2. bond mixnode with the vesting account - let pledge = mix_coin(150_000_000); - - let cost_params = MixNodeCostParams { - profit_margin_percent: Percent::from_percentage_value(10).unwrap(), - interval_operating_cost: mix_coin(40_000_000), - }; - - let (mix_node, owner_signature) = test.valid_mixnode_with_sig( - vesting_account, - Some(test.vesting_contract()), - cost_params.clone(), - pledge.clone(), - ); - - let bond_msg = VestingExecuteMsg::BondMixnode { - mix_node, - cost_params, - owner_signature, - amount: pledge, - }; - test.app - .execute_contract( - Addr::unchecked(vesting_account), - test.vesting_contract(), - &bond_msg, - &[], - ) - .unwrap(); - - // 3. try to decrease the pledge - let before: MixOwnershipResponse = test - .app - .wrap() - .query_wasm_smart( - test.mixnet_contract(), - &MixnetQueryMsg::GetOwnedMixnode { - address: vesting_account.to_string(), - }, - ) - .unwrap(); - let balance_before = test - .app - .wrap() - .query_balance(test.vesting_contract(), MIX_DENOM) - .unwrap(); - assert_eq!(balance_before.amount.u128(), 9_850_000_000); - - let decrease_pledge_msg = VestingExecuteMsg::DecreasePledge { - amount: mix_coin(50_000_000), - }; - test.app - .execute_contract( - Addr::unchecked(vesting_account), - test.vesting_contract(), - &decrease_pledge_msg, - &[], - ) - .unwrap(); - - let after_decrease: MixOwnershipResponse = test - .app - .wrap() - .query_wasm_smart( - test.mixnet_contract(), - &MixnetQueryMsg::GetOwnedMixnode { - address: vesting_account.to_string(), - }, - ) - .unwrap(); - - // note: nothing has changed with the pledge because the event hasn't been resolved yet! - assert_eq!(before.address, after_decrease.address); - let before_details = before.mixnode_details.unwrap(); - let after_details = after_decrease.mixnode_details.unwrap(); - assert_eq!( - before_details.rewarding_details, - after_details.rewarding_details - ); - assert_eq!( - before_details.bond_information, - after_details.bond_information - ); - - // but we have the pending change saved now! - assert!(before_details.pending_changes.pledge_change.is_none()); - assert_eq!(Some(1), after_details.pending_changes.pledge_change); - - // 4. resolve events - test.advance_mixnet_epoch(); - - let balance_after = test - .app - .wrap() - .query_balance(test.vesting_contract(), MIX_DENOM) - .unwrap(); - assert_eq!(balance_after.amount.u128(), 9_900_000_000); -} 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-vesting-integration-tests/src/support/helpers.rs b/contracts/mixnet-vesting-integration-tests/src/support/helpers.rs index f739d8556a0..a714ccf493a 100644 --- a/contracts/mixnet-vesting-integration-tests/src/support/helpers.rs +++ b/contracts/mixnet-vesting-integration-tests/src/support/helpers.rs @@ -12,27 +12,33 @@ pub fn mixnet_owner() -> Addr { Addr::unchecked(MIXNET_OWNER) } +#[allow(unused)] pub fn vesting_owner() -> Addr { Addr::unchecked(VESTING_OWNER) } +#[allow(unused)] pub fn rewarding_validator() -> Addr { Addr::unchecked(REWARDING_VALIDATOR) } +#[allow(unused)] pub fn mix_coins(amount: u128) -> Vec { coins(amount, MIX_DENOM) } +#[allow(unused)] pub fn mix_coin(amount: u128) -> Coin { coin(amount, MIX_DENOM) } +#[allow(unused)] pub fn test_rng() -> ChaCha20Rng { let dummy_seed = [42u8; 32]; ChaCha20Rng::from_seed(dummy_seed) } +#[allow(unused)] pub fn mixnet_contract_wrapper() -> Box> { Box::new( ContractWrapper::new( diff --git a/contracts/mixnet-vesting-integration-tests/src/support/setup.rs b/contracts/mixnet-vesting-integration-tests/src/support/setup.rs index 4f1a4d3bec7..4ef1b04cbac 100644 --- a/contracts/mixnet-vesting-integration-tests/src/support/setup.rs +++ b/contracts/mixnet-vesting-integration-tests/src/support/setup.rs @@ -26,6 +26,7 @@ pub const VESTING_OWNER: &str = "vesting-owner"; pub const REWARDING_VALIDATOR: &str = "rewarding-validator"; pub const MIX_DENOM: &str = "unym"; +#[allow(unused)] pub struct ContractInstantiationResult { mixnet_contract_address: Addr, vesting_contract_address: Addr, @@ -69,14 +70,15 @@ impl TestSetupBuilder { } } +#[allow(unused)] pub struct TestSetup { pub app: App, pub rng: ChaCha20Rng, pub mixnet_contract: Addr, - pub vesting_contract: Addr, } +#[allow(unused)] impl TestSetup { pub fn new_simple() -> Self { TestSetup::new(Default::default(), fixtures::default_mixnet_init_msg()) @@ -91,7 +93,6 @@ impl TestSetup { app, rng: test_rng(), mixnet_contract: contracts.mixnet_contract_address, - vesting_contract: contracts.vesting_contract_address, } } @@ -99,10 +100,6 @@ impl TestSetup { self.mixnet_contract.clone() } - pub fn vesting_contract(&self) -> Addr { - self.vesting_contract.clone() - } - pub fn skip_to_current_epoch_end(&mut self) { let current_interval: CurrentIntervalResponse = self .app @@ -209,7 +206,6 @@ impl TestSetup { pub fn valid_mixnode_with_sig( &mut self, owner: &str, - proxy: Option, cost_params: MixNodeCostParams, stake: Coin, ) -> (MixNode, MessageSignature) { @@ -239,8 +235,7 @@ impl TestSetup { }; let payload = MixnodeBondingPayload::new(mixnode.clone(), cost_params); - let content = - ContractMessageContent::new(Addr::unchecked(owner), proxy, vec![stake], payload); + let content = ContractMessageContent::new(Addr::unchecked(owner), vec![stake], payload); let sign_payload = SignableMixNodeBondingMsg::new(signing_nonce, content); let plaintext = sign_payload.to_plaintext().unwrap(); let signature = keypair.private_key().sign(plaintext); diff --git a/contracts/mixnet-vesting-integration-tests/src/tests.rs b/contracts/mixnet-vesting-integration-tests/src/tests.rs index d560e38394b..826adcfd53c 100644 --- a/contracts/mixnet-vesting-integration-tests/src/tests.rs +++ b/contracts/mixnet-vesting-integration-tests/src/tests.rs @@ -1,5 +1,4 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -mod decrease_mixnode_pledge; mod support; diff --git a/contracts/mixnet/schema/nym-mixnet-contract.json b/contracts/mixnet/schema/nym-mixnet-contract.json index 971cbc27fb8..dc3e521bd2c 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" } } }, @@ -1146,6 +1204,42 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "migrate_vested_mix_node" + ], + "properties": { + "migrate_vested_mix_node": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "migrate_vested_delegation" + ], + "properties": { + "migrate_vested_delegation": { + "type": "object", + "required": [ + "mix_id" + ], + "properties": { + "mix_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": { @@ -1172,6 +1266,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 +1304,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 +1650,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 +8213,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 +8251,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 +8327,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 +8365,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 +8396,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..688b57db0ba 100644 --- a/contracts/mixnet/schema/raw/execute.json +++ b/contracts/mixnet/schema/raw/execute.json @@ -1029,6 +1029,42 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "migrate_vested_mix_node" + ], + "properties": { + "migrate_vested_mix_node": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "migrate_vested_delegation" + ], + "properties": { + "migrate_vested_delegation": { + "type": "object", + "required": [ + "mix_id" + ], + "properties": { + "mix_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": { @@ -1055,6 +1091,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 +1129,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 +1475,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..884565d142c 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); @@ -118,44 +125,6 @@ pub fn execute( ExecuteMsg::KickFamilyMember { member } => { crate::families::transactions::try_head_kick_member(deps, info, member) } - ExecuteMsg::CreateFamilyOnBehalf { - owner_address, - label, - } => crate::families::transactions::try_create_family_on_behalf( - deps, - info, - owner_address, - label, - ), - ExecuteMsg::JoinFamilyOnBehalf { - member_address, - join_permit, - family_head, - } => crate::families::transactions::try_join_family_on_behalf( - deps, - info, - member_address, - join_permit, - family_head, - ), - ExecuteMsg::LeaveFamilyOnBehalf { - member_address, - family_head, - } => crate::families::transactions::try_leave_family_on_behalf( - deps, - info, - member_address, - family_head, - ), - ExecuteMsg::KickFamilyMemberOnBehalf { - head_address, - member, - } => crate::families::transactions::try_head_kick_member_on_behalf( - deps, - info, - head_address, - member, - ), // state/sys-params-related ExecuteMsg::UpdateRewardingValidatorAddress { address } => { crate::mixnet_contract_settings::transactions::try_update_rewarding_validator_address( @@ -232,62 +201,23 @@ pub fn execute( cost_params, owner_signature, ), - ExecuteMsg::BondMixnodeOnBehalf { - mix_node, - cost_params, - owner, - owner_signature, - } => crate::mixnodes::transactions::try_add_mixnode_on_behalf( - deps, - env, - info, - mix_node, - cost_params, - owner, - owner_signature, - ), ExecuteMsg::PledgeMore {} => { crate::mixnodes::transactions::try_increase_pledge(deps, env, info) } - ExecuteMsg::PledgeMoreOnBehalf { owner } => { - crate::mixnodes::transactions::try_increase_pledge_on_behalf(deps, env, info, owner) - } ExecuteMsg::DecreasePledge { decrease_by } => { crate::mixnodes::transactions::try_decrease_pledge(deps, env, info, decrease_by) } - ExecuteMsg::DecreasePledgeOnBehalf { owner, decrease_by } => { - crate::mixnodes::transactions::try_decrease_pledge_on_behalf( - deps, - env, - info, - decrease_by, - owner, - ) - } ExecuteMsg::UnbondMixnode {} => { crate::mixnodes::transactions::try_remove_mixnode(deps, env, info) } - ExecuteMsg::UnbondMixnodeOnBehalf { owner } => { - crate::mixnodes::transactions::try_remove_mixnode_on_behalf(deps, env, info, owner) - } ExecuteMsg::UpdateMixnodeCostParams { new_costs } => { crate::mixnodes::transactions::try_update_mixnode_cost_params( deps, env, info, new_costs, ) } - ExecuteMsg::UpdateMixnodeCostParamsOnBehalf { new_costs, owner } => { - crate::mixnodes::transactions::try_update_mixnode_cost_params_on_behalf( - deps, env, info, new_costs, owner, - ) - } ExecuteMsg::UpdateMixnodeConfig { new_config } => { crate::mixnodes::transactions::try_update_mixnode_config(deps, info, new_config) } - ExecuteMsg::UpdateMixnodeConfigOnBehalf { new_config, owner } => { - crate::mixnodes::transactions::try_update_mixnode_config_on_behalf( - deps, info, new_config, owner, - ) - } // gateway-related: ExecuteMsg::BondGateway { @@ -300,52 +230,22 @@ pub fn execute( gateway, owner_signature, ), - ExecuteMsg::BondGatewayOnBehalf { - gateway, - owner, - owner_signature, - } => crate::gateways::transactions::try_add_gateway_on_behalf( - deps, - env, - info, - gateway, - owner, - owner_signature, - ), ExecuteMsg::UnbondGateway {} => { crate::gateways::transactions::try_remove_gateway(deps, info) } - ExecuteMsg::UnbondGatewayOnBehalf { owner } => { - crate::gateways::transactions::try_remove_gateway_on_behalf(deps, info, owner) - } ExecuteMsg::UpdateGatewayConfig { new_config } => { crate::gateways::transactions::try_update_gateway_config(deps, info, new_config) } - ExecuteMsg::UpdateGatewayConfigOnBehalf { new_config, owner } => { - crate::gateways::transactions::try_update_gateway_config_on_behalf( - deps, info, new_config, owner, - ) - } // delegation-related: ExecuteMsg::DelegateToMixnode { mix_id } => { crate::delegations::transactions::try_delegate_to_mixnode(deps, env, info, mix_id) } - ExecuteMsg::DelegateToMixnodeOnBehalf { mix_id, delegate } => { - crate::delegations::transactions::try_delegate_to_mixnode_on_behalf( - deps, env, info, mix_id, delegate, - ) - } ExecuteMsg::UndelegateFromMixnode { mix_id } => { crate::delegations::transactions::try_remove_delegation_from_mixnode( deps, env, info, mix_id, ) } - ExecuteMsg::UndelegateFromMixnodeOnBehalf { mix_id, delegate } => { - crate::delegations::transactions::try_remove_delegation_from_mixnode_on_behalf( - deps, env, info, mix_id, delegate, - ) - } // reward-related ExecuteMsg::RewardMixnode { @@ -356,16 +256,37 @@ pub fn execute( ExecuteMsg::WithdrawOperatorReward {} => { crate::rewards::transactions::try_withdraw_operator_reward(deps, info) } - ExecuteMsg::WithdrawOperatorRewardOnBehalf { owner } => { - crate::rewards::transactions::try_withdraw_operator_reward_on_behalf(deps, info, owner) - } ExecuteMsg::WithdrawDelegatorReward { mix_id } => { crate::rewards::transactions::try_withdraw_delegator_reward(deps, info, mix_id) } - ExecuteMsg::WithdrawDelegatorRewardOnBehalf { mix_id, owner } => { - crate::rewards::transactions::try_withdraw_delegator_reward_on_behalf( - deps, info, mix_id, owner, - ) + + // vesting migration: + ExecuteMsg::MigrateVestedMixNode { .. } => { + crate::vesting_migration::try_migrate_vested_mixnode(deps, info) + } + ExecuteMsg::MigrateVestedDelegation { mix_id } => { + crate::vesting_migration::try_migrate_vested_delegation(deps, info, mix_id) + } + + // legacy vesting + ExecuteMsg::CreateFamilyOnBehalf { .. } + | ExecuteMsg::JoinFamilyOnBehalf { .. } + | ExecuteMsg::LeaveFamilyOnBehalf { .. } + | ExecuteMsg::KickFamilyMemberOnBehalf { .. } + | ExecuteMsg::BondMixnodeOnBehalf { .. } + | ExecuteMsg::PledgeMoreOnBehalf { .. } + | ExecuteMsg::DecreasePledgeOnBehalf { .. } + | ExecuteMsg::UnbondMixnodeOnBehalf { .. } + | ExecuteMsg::UpdateMixnodeCostParamsOnBehalf { .. } + | ExecuteMsg::UpdateMixnodeConfigOnBehalf { .. } + | ExecuteMsg::BondGatewayOnBehalf { .. } + | ExecuteMsg::UnbondGatewayOnBehalf { .. } + | ExecuteMsg::UpdateGatewayConfigOnBehalf { .. } + | ExecuteMsg::DelegateToMixnodeOnBehalf { .. } + | ExecuteMsg::UndelegateFromMixnodeOnBehalf { .. } + | ExecuteMsg::WithdrawOperatorRewardOnBehalf { .. } + | ExecuteMsg::WithdrawDelegatorRewardOnBehalf { .. } => { + Err(MixnetContractError::DisabledVestingOperation) } // testing-only @@ -607,13 +528,15 @@ pub fn query( #[entry_point] pub fn migrate( - deps: DepsMut<'_>, + mut deps: DepsMut<'_>, _env: Env, msg: MigrateMsg, ) -> Result { set_build_information!(deps.storage)?; cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + crate::queued_migrations::vesting_purge(deps.branch())?; + // due to circular dependency on contract addresses (i.e. mixnet contract requiring vesting contract address // and vesting contract requiring the mixnet contract address), if we ever want to deploy any new fresh // environment, one of the contracts will HAVE TO go through a migration @@ -631,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; @@ -657,6 +580,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 +609,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/delegations/queries.rs b/contracts/mixnet/src/delegations/queries.rs index 5a5d07c32ff..01e7b656486 100644 --- a/contracts/mixnet/src/delegations/queries.rs +++ b/contracts/mixnet/src/delegations/queries.rs @@ -302,10 +302,7 @@ mod tests { mod delegator_delegations { use super::*; - use crate::delegations::transactions::try_delegate_to_mixnode_on_behalf; - use crate::support::tests::fixtures::TEST_COIN_DENOM; - use cosmwasm_std::testing::mock_info; - use cosmwasm_std::{coin, Addr}; + use cosmwasm_std::Addr; #[test] fn obeys_limits() { @@ -453,25 +450,10 @@ mod tests { #[test] fn all_retrieved_delegations_are_from_the_specified_delegator() { let mut test = TestSetup::new(); - let env = test.env(); + // it means we have, for example, delegation from "delegator1" towards mix1, mix2, ...., from "delegator2" towards mix1, mix2, ...., etc add_dummy_mixes_with_delegations(&mut test, 50, 100); - // add some proxies while we're at it to make sure they're queried for separately - let with_proxy = "delegator42"; - let vesting_contract = test.vesting_contract(); - for mix_id in 1..=25 { - try_delegate_to_mixnode_on_behalf( - test.deps_mut(), - env.clone(), - mock_info(vesting_contract.as_ref(), &[coin(100_000, TEST_COIN_DENOM)]), - mix_id, - with_proxy.into(), - ) - .unwrap(); - } - test.execute_all_pending_events(); - // make few queries let res1 = query_delegator_delegations_paged(test.deps(), "delegator2".into(), None, None) @@ -490,59 +472,6 @@ mod tests { .delegations .into_iter() .all(|d| d.owner == Addr::unchecked("delegator35"))); - - let with_proxy_full = - query_delegator_delegations_paged(test.deps(), with_proxy.into(), None, None) - .unwrap(); - assert_eq!(with_proxy_full.delegations.len(), 125); - - // all delegations have correct owner - assert!(with_proxy_full - .delegations - .iter() - .all(|d| d.owner == Addr::unchecked(with_proxy))); - - // and we have 100 delegations without proxy and 25 with - let no_proxy = with_proxy_full - .delegations - .iter() - .filter(|d| d.proxy.is_none()) - .count(); - assert_eq!(no_proxy, 100); - let proxy = with_proxy_full - .delegations - .iter() - .filter(|d| d.proxy.is_some()) - .count(); - assert_eq!(proxy, 25); - - assert!(with_proxy_full - .delegations - .iter() - .filter(|d| d.proxy.is_some()) - .all(|d| d.proxy.as_ref().unwrap() == vesting_contract)); - - // now make sure that if we do it in paged manner, we'll get exactly the same result - let per_page = Some(15); - let mut delegations = Vec::new(); - let mut start_after = None; - loop { - let mut paged_response = query_delegator_delegations_paged( - test.deps(), - with_proxy.into(), - start_after, - per_page, - ) - .unwrap(); - delegations.append(&mut paged_response.delegations); - - if let Some(start_after_res) = paged_response.start_next_after { - start_after = Some(start_after_res) - } else { - break; - } - } - assert_eq!(with_proxy_full.delegations, delegations) } } diff --git a/contracts/mixnet/src/delegations/transactions.rs b/contracts/mixnet/src/delegations/transactions.rs index adc70c54cea..b91c6c85823 100644 --- a/contracts/mixnet/src/delegations/transactions.rs +++ b/contracts/mixnet/src/delegations/transactions.rs @@ -1,14 +1,12 @@ -// Copyright 2021-2023 - Nym Technologies SA +// Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use super::storage; use crate::interval::storage as interval_storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::mixnodes::storage as mixnodes_storage; -use crate::support::helpers::{ - ensure_epoch_in_progress_state, ensure_sent_by_vesting_contract, validate_delegation_stake, -}; -use cosmwasm_std::{Addr, Coin, DepsMut, Env, MessageInfo, Response}; +use crate::support::helpers::{ensure_epoch_in_progress_state, validate_delegation_stake}; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ new_pending_delegation_event, new_pending_undelegation_event, @@ -21,30 +19,6 @@ pub(crate) fn try_delegate_to_mixnode( env: Env, info: MessageInfo, mix_id: MixId, -) -> Result { - _try_delegate_to_mixnode(deps, env, mix_id, info.sender, info.funds, None) -} - -pub(crate) fn try_delegate_to_mixnode_on_behalf( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - mix_id: MixId, - delegate: String, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let delegate = deps.api.addr_validate(&delegate)?; - _try_delegate_to_mixnode(deps, env, mix_id, delegate, info.funds, Some(info.sender)) -} - -pub(crate) fn _try_delegate_to_mixnode( - deps: DepsMut<'_>, - env: Env, - mix_id: MixId, - delegate: Addr, - amount: Vec, - proxy: Option, ) -> Result { // delegation is only allowed if the epoch is currently not in the process of being advanced ensure_epoch_in_progress_state(deps.storage)?; @@ -52,7 +26,7 @@ pub(crate) fn _try_delegate_to_mixnode( // check if the delegation contains any funds of the appropriate denomination let contract_state = mixnet_params_storage::CONTRACT_STATE.load(deps.storage)?; let delegation = validate_delegation_stake( - amount, + info.funds, contract_state.params.minimum_mixnode_delegation, contract_state.rewarding_denom, )?; @@ -67,14 +41,9 @@ pub(crate) fn _try_delegate_to_mixnode( } // push the event onto the queue and wait for it to be picked up at the end of the epoch - let cosmos_event = new_pending_delegation_event(&delegate, &proxy, &delegation, mix_id); - - let epoch_event = PendingEpochEventKind::Delegate { - owner: delegate, - mix_id, - amount: delegation, - proxy, - }; + let cosmos_event = new_pending_delegation_event(&info.sender, &delegation, mix_id); + + let epoch_event = PendingEpochEventKind::new_delegate(info.sender, mix_id, delegation); interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?; Ok(Response::new().add_event(cosmos_event)) @@ -85,35 +54,12 @@ pub(crate) fn try_remove_delegation_from_mixnode( env: Env, info: MessageInfo, mix_id: MixId, -) -> Result { - _try_remove_delegation_from_mixnode(deps, env, mix_id, info.sender, None) -} - -pub(crate) fn try_remove_delegation_from_mixnode_on_behalf( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - mix_id: MixId, - delegate: String, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let delegate = deps.api.addr_validate(&delegate)?; - _try_remove_delegation_from_mixnode(deps, env, mix_id, delegate, Some(info.sender)) -} - -pub(crate) fn _try_remove_delegation_from_mixnode( - deps: DepsMut<'_>, - env: Env, - mix_id: MixId, - delegate: Addr, - proxy: Option, ) -> Result { // undelegation is only allowed if the epoch is currently not in the process of being advanced ensure_epoch_in_progress_state(deps.storage)?; // see if the delegation even exists - let storage_key = Delegation::generate_storage_key(mix_id, &delegate, proxy.as_ref()); + let storage_key = Delegation::generate_storage_key(mix_id, &info.sender, None); if storage::delegations() .may_load(deps.storage, storage_key)? @@ -121,19 +67,15 @@ pub(crate) fn _try_remove_delegation_from_mixnode( { return Err(MixnetContractError::NoMixnodeDelegationFound { mix_id, - address: delegate.into_string(), - proxy: proxy.map(Addr::into_string), + address: info.sender.into_string(), + proxy: None, }); } // push the event onto the queue and wait for it to be picked up at the end of the epoch - let cosmos_event = new_pending_undelegation_event(&delegate, &proxy, mix_id); + let cosmos_event = new_pending_undelegation_event(&info.sender, mix_id); - let epoch_event = PendingEpochEventKind::Undelegate { - owner: delegate, - mix_id, - proxy, - }; + let epoch_event = PendingEpochEventKind::new_undelegate(info.sender, mix_id); interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?; Ok(Response::new().add_event(cosmos_event)) @@ -151,7 +93,7 @@ mod tests { use crate::support::tests::fixtures::TEST_COIN_DENOM; use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::testing::mock_info; - use cosmwasm_std::{coin, Decimal}; + use cosmwasm_std::{coin, Addr, Decimal}; use mixnet_contract_common::{EpochState, EpochStatus}; #[test] @@ -368,66 +310,18 @@ mod tests { let mix_id = test.add_dummy_mixnode("mix-owner", None); let amount1 = coin(100_000_000, TEST_COIN_DENOM); - let amount2 = coin(50_000_000, TEST_COIN_DENOM); let sender1 = mock_info(owner, &[amount1.clone()]); - let sender2 = mock_info(test.vesting_contract().as_str(), &[amount2.clone()]); try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id).unwrap(); - try_delegate_to_mixnode_on_behalf(test.deps_mut(), env, sender2, mix_id, owner.into()) - .unwrap(); let events = test.pending_epoch_events(); assert_eq!( events[0].kind, - PendingEpochEventKind::Delegate { - owner: Addr::unchecked(owner), - mix_id, - amount: amount1, - proxy: None - } - ); - - assert_eq!( - events[1].kind, - PendingEpochEventKind::Delegate { - owner: Addr::unchecked(owner), - mix_id, - amount: amount2, - proxy: Some(test.vesting_contract()) - } + PendingEpochEventKind::new_delegate(Addr::unchecked(owner), mix_id, amount1,) ); } - - #[test] - fn fails_for_illegal_proxy() { - let mut test = TestSetup::new(); - let env = test.env(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let owner = "delegator"; - let mix_id = test.add_dummy_mixnode("mix-owner", None); - - let res = try_delegate_to_mixnode_on_behalf( - test.deps_mut(), - env, - mock_info(illegal_proxy.as_ref(), &[coin(123, TEST_COIN_DENOM)]), - mix_id, - owner.into(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract - } - ) - } } #[cfg(test)] @@ -573,40 +467,5 @@ mod tests { ); assert!(res.is_ok()); } - - #[test] - fn fails_for_illegal_proxy() { - let mut test = TestSetup::new(); - let env = test.env(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let owner = "delegator"; - let mix_id = test.add_dummy_mixnode("mix-owner", None); - test.add_immediate_delegation_with_illegal_proxy( - owner, - 10000u32, - mix_id, - illegal_proxy.clone(), - ); - - let res = try_remove_delegation_from_mixnode_on_behalf( - test.deps_mut(), - env, - mock_info(illegal_proxy.as_ref(), &[coin(123, TEST_COIN_DENOM)]), - mix_id, - owner.into(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract - } - ) - } } } diff --git a/contracts/mixnet/src/families/signature_helpers.rs b/contracts/mixnet/src/families/signature_helpers.rs index 222c7548cd2..3c6bb796267 100644 --- a/contracts/mixnet/src/families/signature_helpers.rs +++ b/contracts/mixnet/src/families/signature_helpers.rs @@ -4,7 +4,7 @@ use crate::mixnodes::storage as mixnodes_storage; use crate::signing::storage as signing_storage; use crate::support::helpers::decode_ed25519_identity_key; -use cosmwasm_std::{Addr, Deps}; +use cosmwasm_std::Deps; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::families::FamilyHead; use mixnet_contract_common::{construct_family_join_permit, IdentityKeyRef}; @@ -13,7 +13,6 @@ use nym_contracts_common::signing::{MessageSignature, Verifier}; pub(crate) fn verify_family_join_permit( deps: Deps<'_>, granter: FamilyHead, - proxy: Option, member: IdentityKeyRef, signature: MessageSignature, ) -> Result<(), MixnetContractError> { @@ -32,7 +31,7 @@ pub(crate) fn verify_family_join_permit( }); }; let nonce = signing_storage::get_signing_nonce(deps.storage, head_mixnode.owner)?; - let msg = construct_family_join_permit(nonce, granter, proxy, member.to_owned()); + let msg = construct_family_join_permit(nonce, granter, member.to_owned()); if deps.api.verify_message(msg, signature, &public_key)? { Ok(()) diff --git a/contracts/mixnet/src/families/transactions.rs b/contracts/mixnet/src/families/transactions.rs index 732acddb43b..ee88f98e635 100644 --- a/contracts/mixnet/src/families/transactions.rs +++ b/contracts/mixnet/src/families/transactions.rs @@ -1,4 +1,4 @@ -// Copyright 2022-2023 - Nym Technologies SA +// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use super::storage::{ @@ -7,41 +7,20 @@ use super::storage::{ }; use crate::families::queries::get_family_by_label; use crate::families::signature_helpers::verify_family_join_permit; -use crate::support::helpers::{ensure_bonded, ensure_sent_by_vesting_contract}; -use cosmwasm_std::{Addr, DepsMut, MessageInfo, Response}; +use crate::support::helpers::ensure_bonded; +use cosmwasm_std::{DepsMut, MessageInfo, Response}; use mixnet_contract_common::families::{Family, FamilyHead}; use mixnet_contract_common::{error::MixnetContractError, IdentityKey}; use nym_contracts_common::signing::MessageSignature; /// Creates a new MixNode family with senders node as head -pub fn try_create_family( +pub(crate) fn try_create_family( deps: DepsMut, info: MessageInfo, label: String, -) -> Result { - _try_create_family(deps, &info.sender, label, None) -} - -pub fn try_create_family_on_behalf( - deps: DepsMut, - info: MessageInfo, - owner_address: String, - label: String, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let owner_address = deps.api.addr_validate(&owner_address)?; - _try_create_family(deps, &owner_address, label, Some(info.sender)) -} - -fn _try_create_family( - deps: DepsMut, - owner: &Addr, - label: String, - proxy: Option, ) -> Result { let existing_bond = - crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, owner)?; + crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; ensure_bonded(&existing_bond)?; @@ -60,43 +39,19 @@ fn _try_create_family( return Err(MixnetContractError::FamilyWithLabelExists(label)); } - let family = Family::new(family_head, proxy, label); + let family = Family::new(family_head, label); save_family(&family, deps.storage)?; Ok(Response::default()) } -pub fn try_join_family( - deps: DepsMut, - info: MessageInfo, - join_permit: MessageSignature, - family_head: FamilyHead, -) -> Result { - _try_join_family(deps, &info.sender, join_permit, family_head, None) -} - -pub fn try_join_family_on_behalf( +pub(crate) fn try_join_family( deps: DepsMut, info: MessageInfo, - member_address: String, - join_permit: MessageSignature, - family_head: FamilyHead, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let member_address = deps.api.addr_validate(&member_address)?; - let proxy = Some(info.sender); - _try_join_family(deps, &member_address, join_permit, family_head, proxy) -} - -fn _try_join_family( - deps: DepsMut, - owner: &Addr, join_permit: MessageSignature, family_head: FamilyHead, - proxy: Option, ) -> Result { let existing_bond = - crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, owner)?; + crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; ensure_bonded(&existing_bond)?; @@ -116,7 +71,6 @@ fn _try_join_family( verify_family_join_permit( deps.as_ref(), family_head.clone(), - proxy, existing_bond.identity(), join_permit, )?; @@ -128,33 +82,13 @@ fn _try_join_family( Ok(Response::default()) } -pub fn try_leave_family( - deps: DepsMut, - info: MessageInfo, - family_head: FamilyHead, -) -> Result { - _try_leave_family(deps, &info.sender, family_head) -} - -pub fn try_leave_family_on_behalf( +pub(crate) fn try_leave_family( deps: DepsMut, info: MessageInfo, - member_address: String, - family_head: FamilyHead, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let member_address = deps.api.addr_validate(&member_address)?; - _try_leave_family(deps, &member_address, family_head) -} - -fn _try_leave_family( - deps: DepsMut, - owner: &Addr, family_head: FamilyHead, ) -> Result { let existing_bond = - crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, owner)?; + crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; ensure_bonded(&existing_bond)?; @@ -178,32 +112,13 @@ fn _try_leave_family( Ok(Response::default()) } -pub fn try_head_kick_member( +pub(crate) fn try_head_kick_member( deps: DepsMut, info: MessageInfo, member: IdentityKey, ) -> Result { - _try_head_kick_member(deps, &info.sender, member) -} - -pub fn try_head_kick_member_on_behalf( - deps: DepsMut, - info: MessageInfo, - head_address: String, - member: IdentityKey, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let head_address = deps.api.addr_validate(&head_address)?; - _try_head_kick_member(deps, &head_address, member) -} - -fn _try_head_kick_member( - deps: DepsMut, - owner: &Addr, - member: IdentityKey, -) -> Result { - let head_bond = crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, owner)?; + let head_bond = + crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; // make sure we're still in the mixnet ensure_bonded(&head_bond)?; @@ -321,7 +236,7 @@ mod test { assert_eq!(family.head_identity(), family_head.identity()); let join_permit = - test.generate_family_join_permit(&head_keypair, &member_mixnode.identity_key, false); + test.generate_family_join_permit(&head_keypair, &member_mixnode.identity_key); try_join_family( test.deps_mut(), @@ -345,7 +260,7 @@ mod test { ); let new_join_permit = - test.generate_family_join_permit(&head_keypair, &member_mixnode.identity_key, false); + test.generate_family_join_permit(&head_keypair, &member_mixnode.identity_key); try_join_family( test.deps_mut(), @@ -373,189 +288,4 @@ mod test { !is_family_member(test.deps().storage, &family, &member_mixnode.identity_key).unwrap() ); } - - #[cfg(test)] - mod creating_family { - use super::*; - - #[test] - fn fails_for_illegal_proxy() { - let mut test = TestSetup::new(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let head = "alice"; - - test.add_dummy_mixnode(head, None); - - let res = try_create_family_on_behalf( - test.deps_mut(), - mock_info(illegal_proxy.as_ref(), &[]), - head.to_string(), - "label".to_string(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract - } - ) - } - } - - #[cfg(test)] - mod joining_family { - use super::*; - - #[test] - fn fails_for_illegal_proxy() { - let mut test = TestSetup::new(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let head = "alice"; - let label = "family"; - let new_member = "vin-diesel"; - - let (_, head_keys) = test.create_dummy_mixnode_with_new_family(head, label); - let (_, member_keys) = test.add_dummy_mixnode_with_keypair(new_member, None); - - let join_permit = test.generate_family_join_permit( - &head_keys, - &member_keys.public_key().to_base58_string(), - false, - ); - - let head_identity = head_keys.public_key().to_base58_string(); - let family_head = FamilyHead::new(head_identity); - let res = try_join_family_on_behalf( - test.deps_mut(), - mock_info(illegal_proxy.as_ref(), &[]), - new_member.to_string(), - join_permit, - family_head, - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract - } - ) - } - } - - #[cfg(test)] - mod leaving_family { - use super::*; - - #[test] - fn fails_for_illegal_proxy() { - let mut test = TestSetup::new(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let head = "alice"; - let label = "family"; - let new_member = "vin-diesel"; - - let (_, head_keys) = test.create_dummy_mixnode_with_new_family(head, label); - let (_, member_keys) = test.add_dummy_mixnode_with_keypair(new_member, None); - - let join_permit = test.generate_family_join_permit( - &head_keys, - &member_keys.public_key().to_base58_string(), - true, - ); - - let head_identity = head_keys.public_key().to_base58_string(); - let family_head = FamilyHead::new(head_identity); - try_join_family_on_behalf( - test.deps_mut(), - mock_info(vesting_contract.as_ref(), &[]), - new_member.to_string(), - join_permit, - family_head.clone(), - ) - .unwrap(); - - let res = try_leave_family_on_behalf( - test.deps_mut(), - mock_info(illegal_proxy.as_ref(), &[]), - new_member.to_string(), - family_head, - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract - } - ) - } - } - - #[cfg(test)] - mod kicking_family_member { - use super::*; - - #[test] - fn fails_for_illegal_proxy() { - let mut test = TestSetup::new(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let head = "alice"; - let label = "family"; - let new_member = "vin-diesel"; - - let (_, head_keys) = test.create_dummy_mixnode_with_new_family(head, label); - let (_, member_keys) = test.add_dummy_mixnode_with_keypair(new_member, None); - - let join_permit = test.generate_family_join_permit( - &head_keys, - &member_keys.public_key().to_base58_string(), - true, - ); - - let head_identity = head_keys.public_key().to_base58_string(); - let family_head = FamilyHead::new(head_identity); - - try_join_family_on_behalf( - test.deps_mut(), - mock_info(vesting_contract.as_ref(), &[]), - new_member.to_string(), - join_permit, - family_head, - ) - .unwrap(); - - let res = try_head_kick_member_on_behalf( - test.deps_mut(), - mock_info(illegal_proxy.as_ref(), &[]), - head.to_string(), - member_keys.public_key().to_base58_string(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract - } - ) - } - } } diff --git a/contracts/mixnet/src/gateways/signature_helpers.rs b/contracts/mixnet/src/gateways/signature_helpers.rs index e6ce7f5ad50..46d37c60731 100644 --- a/contracts/mixnet/src/gateways/signature_helpers.rs +++ b/contracts/mixnet/src/gateways/signature_helpers.rs @@ -12,7 +12,6 @@ use nym_contracts_common::signing::Verifier; pub(crate) fn verify_gateway_bonding_signature( deps: Deps<'_>, sender: Addr, - proxy: Option, pledge: Coin, gateway: Gateway, signature: MessageSignature, @@ -22,7 +21,7 @@ pub(crate) fn verify_gateway_bonding_signature( // reconstruct the payload let nonce = signing_storage::get_signing_nonce(deps.storage, sender.clone())?; - let msg = construct_gateway_bonding_sign_payload(nonce, sender, proxy, pledge, gateway); + let msg = construct_gateway_bonding_sign_payload(nonce, sender, pledge, gateway); if deps.api.verify_message(msg, signature, &public_key)? { Ok(()) diff --git a/contracts/mixnet/src/gateways/transactions.rs b/contracts/mixnet/src/gateways/transactions.rs index f747c4c7ba2..6075c258941 100644 --- a/contracts/mixnet/src/gateways/transactions.rs +++ b/contracts/mixnet/src/gateways/transactions.rs @@ -1,4 +1,4 @@ -// Copyright 2021-2023 - Nym Technologies SA +// Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use super::helpers::must_get_gateway_bond_by_owner; @@ -6,10 +6,8 @@ use super::storage; use crate::gateways::signature_helpers::verify_gateway_bonding_signature; use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::signing::storage as signing_storage; -use crate::support::helpers::{ - ensure_no_existing_bond, ensure_proxy_match, ensure_sent_by_vesting_contract, validate_pledge, -}; -use cosmwasm_std::{wasm_execute, Addr, BankMsg, Coin, DepsMut, Env, MessageInfo, Response}; +use crate::support::helpers::{ensure_no_existing_bond, validate_pledge}; +use cosmwasm_std::{BankMsg, DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ new_gateway_bonding_event, new_gateway_config_update_event, new_gateway_unbonding_event, @@ -17,72 +15,28 @@ use mixnet_contract_common::events::{ use mixnet_contract_common::gateway::GatewayConfigUpdate; use mixnet_contract_common::{Gateway, GatewayBond}; use nym_contracts_common::signing::MessageSignature; -use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; - -pub fn try_add_gateway( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - gateway: Gateway, - owner_signature: MessageSignature, -) -> Result { - _try_add_gateway( - deps, - env, - gateway, - info.funds, - info.sender, - owner_signature, - None, - ) -} - -pub fn try_add_gateway_on_behalf( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - gateway: Gateway, - owner: String, - owner_signature: MessageSignature, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let proxy = info.sender; - let owner = deps.api.addr_validate(&owner)?; - _try_add_gateway( - deps, - env, - gateway, - info.funds, - owner, - owner_signature, - Some(proxy), - ) -} // TODO: perhaps also require the user to explicitly provide what it thinks is the current nonce // so that we could return a better error message if it doesn't match? -pub(crate) fn _try_add_gateway( +pub(crate) fn try_add_gateway( deps: DepsMut<'_>, env: Env, + info: MessageInfo, gateway: Gateway, - pledge: Vec, - owner: Addr, owner_signature: MessageSignature, - proxy: Option, ) -> Result { // check if the pledge contains any funds of the appropriate denomination let minimum_pledge = mixnet_params_storage::minimum_gateway_pledge(deps.storage)?; - let pledge = validate_pledge(pledge, minimum_pledge)?; + let pledge = validate_pledge(info.funds, minimum_pledge)?; // if the client has an active bonded mixnode or gateway, don't allow bonding - ensure_no_existing_bond(&owner, deps.storage)?; + ensure_no_existing_bond(&info.sender, deps.storage)?; // check if somebody else has already bonded a gateway with this identity if let Some(existing_bond) = storage::gateways().may_load(deps.storage, &gateway.identity_key)? { - if existing_bond.owner != owner { + if existing_bond.owner != info.sender { return Err(MixnetContractError::DuplicateGateway { owner: existing_bond.owner, }); @@ -92,105 +46,62 @@ pub(crate) fn _try_add_gateway( // check if this sender actually owns the gateway by checking the signature verify_gateway_bonding_signature( deps.as_ref(), - owner.clone(), - proxy.clone(), + info.sender.clone(), pledge.clone(), gateway.clone(), owner_signature, )?; // update the signing nonce associated with this sender so that the future signature would be made on the new value - signing_storage::increment_signing_nonce(deps.storage, owner.clone())?; + signing_storage::increment_signing_nonce(deps.storage, info.sender.clone())?; let gateway_identity = gateway.identity_key.clone(); let bond = GatewayBond::new( pledge.clone(), - owner.clone(), + info.sender.clone(), env.block.height, gateway, - proxy.clone(), ); storage::gateways().save(deps.storage, bond.identity(), &bond)?; Ok(Response::new().add_event(new_gateway_bonding_event( - &owner, - &proxy, + &info.sender, &pledge, &gateway_identity, ))) } -pub fn try_remove_gateway_on_behalf( - deps: DepsMut<'_>, - info: MessageInfo, - owner: String, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let proxy = info.sender; - let owner = deps.api.addr_validate(&owner)?; - _try_remove_gateway(deps, owner, Some(proxy)) -} - -pub fn try_remove_gateway( +pub(crate) fn try_remove_gateway( deps: DepsMut<'_>, info: MessageInfo, -) -> Result { - _try_remove_gateway(deps, info.sender, None) -} - -pub(crate) fn _try_remove_gateway( - deps: DepsMut<'_>, - owner: Addr, - proxy: Option, ) -> Result { // try to find the node of the sender let gateway_bond = match storage::gateways() .idx .owner - .item(deps.storage, owner.clone())? + .item(deps.storage, info.sender.clone())? { Some(record) => record.1, - None => return Err(MixnetContractError::NoAssociatedGatewayBond { owner }), + None => return Err(MixnetContractError::NoAssociatedGatewayBond { owner: info.sender }), }; - if proxy != gateway_bond.proxy { - return Err(MixnetContractError::ProxyMismatch { - existing: gateway_bond - .proxy - .map_or_else(|| "None".to_string(), |a| a.as_str().to_string()), - incoming: proxy.map_or_else(|| "None".to_string(), |a| a.as_str().to_string()), - }); - } - // send bonded funds back to the bond owner let return_tokens = BankMsg::Send { - to_address: proxy.as_ref().unwrap_or(&owner).to_string(), + to_address: info.sender.to_string(), amount: vec![gateway_bond.pledge_amount()], }; // remove the bond storage::gateways().remove(deps.storage, gateway_bond.identity())?; - let mut response = Response::new().add_message(return_tokens); - - if let Some(proxy) = &proxy { - let msg = VestingContractExecuteMsg::TrackUnbondGateway { - owner: owner.as_str().to_string(), - amount: gateway_bond.pledge_amount(), - }; - - let track_unbond_message = wasm_execute(proxy, &msg, vec![])?; - response = response.add_message(track_unbond_message); - } - - Ok(response.add_event(new_gateway_unbonding_event( - &owner, - &proxy, - &gateway_bond.pledge_amount, - gateway_bond.identity(), - ))) + Ok(Response::new() + .add_message(return_tokens) + .add_event(new_gateway_unbonding_event( + &info.sender, + &gateway_bond.pledge_amount, + gateway_bond.identity(), + ))) } pub(crate) fn try_update_gateway_config( @@ -198,36 +109,9 @@ pub(crate) fn try_update_gateway_config( info: MessageInfo, new_config: GatewayConfigUpdate, ) -> Result { - let owner = info.sender; - _try_update_gateway_config(deps, new_config, owner, None) -} - -pub(crate) fn try_update_gateway_config_on_behalf( - deps: DepsMut, - info: MessageInfo, - new_config: GatewayConfigUpdate, - owner: String, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let owner = deps.api.addr_validate(&owner)?; - let proxy = info.sender; - _try_update_gateway_config(deps, new_config, owner, Some(proxy)) -} - -pub(crate) fn _try_update_gateway_config( - deps: DepsMut, - new_config: GatewayConfigUpdate, - owner: Addr, - proxy: Option, -) -> Result { - let existing_bond = must_get_gateway_bond_by_owner(deps.storage, &owner)?; - ensure_proxy_match(&proxy, &existing_bond.proxy)?; - - let cfg_update_event = new_gateway_config_update_event(&owner, &proxy, &new_config); + let existing_bond = must_get_gateway_bond_by_owner(deps.storage, &info.sender)?; + let cfg_update_event = new_gateway_config_update_event(&info.sender, &new_config); - // clippy beta 1.70.0-beta.1 false positive - #[allow(clippy::redundant_clone)] let mut updated_bond = existing_bond.clone(); updated_bond.gateway.host = new_config.host; updated_bond.gateway.mix_port = new_config.mix_port; @@ -254,10 +138,10 @@ pub mod tests { use crate::mixnet_contract_settings::storage::minimum_gateway_pledge; use crate::support::tests; use crate::support::tests::fixtures; - use crate::support::tests::fixtures::{good_gateway_pledge, good_mixnode_pledge}; + use crate::support::tests::fixtures::good_mixnode_pledge; use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::testing::mock_info; - use cosmwasm_std::Uint128; + use cosmwasm_std::{Addr, Uint128}; use mixnet_contract_common::ExecuteMsg; #[test] @@ -392,42 +276,12 @@ pub mod tests { .unwrap(); assert_eq!(1, updated_nonce); - _try_remove_gateway(test.deps_mut(), Addr::unchecked(sender), None).unwrap(); + try_remove_gateway(test.deps_mut(), info.clone()).unwrap(); let res = try_add_gateway(test.deps_mut(), env, info, gateway, signature); assert_eq!(res, Err(MixnetContractError::InvalidEd25519Signature)); } - #[test] - fn gateway_add_with_illegal_proxy() { - let mut test = TestSetup::new(); - let env = test.env(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let owner = "alice"; - let (gateway, sig) = test.gateway_with_signature(owner, None); - - let res = try_add_gateway_on_behalf( - test.deps_mut(), - env, - mock_info(illegal_proxy.as_ref(), &good_gateway_pledge()), - gateway, - owner.to_string(), - sig, - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract - } - ) - } - #[test] fn gateway_remove() { let mut test = TestSetup::new(); @@ -495,7 +349,6 @@ pub mod tests { .add_message(expected_message) .add_event(new_gateway_unbonding_event( &Addr::unchecked("fred"), - &None, &tests::fixtures::good_gateway_pledge()[0], &fred_identity, )); @@ -510,33 +363,6 @@ pub mod tests { assert_eq!(&Addr::unchecked("bob"), nodes[0].owner()); } - #[test] - fn gateway_remove_with_illegal_proxy() { - let mut test = TestSetup::new(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let owner = "alice"; - - test.add_dummy_gateway_with_illegal_proxy(owner, None, illegal_proxy.clone()); - - let res = try_remove_gateway_on_behalf( - test.deps_mut(), - mock_info(illegal_proxy.as_ref(), &good_gateway_pledge()), - owner.to_string(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract - } - ) - } - #[test] fn update_gateway_config() { let mut test = TestSetup::new(); @@ -561,22 +387,6 @@ pub mod tests { ); test.add_dummy_gateway(owner, None); - let vesting_contract = test.vesting_contract(); - - // attempted to remove on behalf with invalid proxy (current is `None`) - let res = try_update_gateway_config_on_behalf( - test.deps_mut(), - mock_info(vesting_contract.as_ref(), &[]), - update.clone(), - owner.to_string(), - ); - assert_eq!( - res, - Err(MixnetContractError::ProxyMismatch { - existing: "None".to_string(), - incoming: vesting_contract.into_string() - }) - ); // "normal" update succeeds let res = try_update_gateway_config(test.deps_mut(), info, update.clone()); @@ -591,39 +401,4 @@ pub mod tests { assert_eq!(bond.gateway.location, update.location); assert_eq!(bond.gateway.version, update.version); } - - #[test] - fn updating_gateway_config_with_illegal_proxy() { - let mut test = TestSetup::new(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let owner = "alice"; - - test.add_dummy_gateway_with_illegal_proxy(owner, None, illegal_proxy.clone()); - let update = GatewayConfigUpdate { - host: "1.1.1.1:1234".to_string(), - mix_port: 1234, - clients_port: 1235, - location: "at home".to_string(), - version: "v1.2.3".to_string(), - }; - - let res = try_update_gateway_config_on_behalf( - test.deps_mut(), - mock_info(illegal_proxy.as_ref(), &[]), - update, - owner.to_string(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract - } - ) - } } diff --git a/contracts/mixnet/src/interval/pending_events.rs b/contracts/mixnet/src/interval/pending_events.rs index e0c5077a8f9..fa44e36ec3a 100644 --- a/contracts/mixnet/src/interval/pending_events.rs +++ b/contracts/mixnet/src/interval/pending_events.rs @@ -24,7 +24,7 @@ use crate::interval::storage; use crate::mixnodes::helpers::{cleanup_post_unbond_mixnode_storage, get_mixnode_details_by_id}; use crate::mixnodes::storage as mixnodes_storage; use crate::rewards::storage as rewards_storage; -use crate::support::helpers::{send_to_proxy_or_owner, VestingTracking}; +use crate::support::helpers::AttachSendTokens; pub(crate) trait ContractExecutableEvent { // note: the error only means a HARD error like we failed to read from storage. @@ -40,7 +40,6 @@ pub(crate) fn delegate( owner: Addr, mix_id: MixId, amount: Coin, - proxy: Option, ) -> Result { // check if the target node still exists (it might have unbonded between this event getting created // and being executed). Do note that it's absolutely possible for a mixnode to get immediately @@ -56,20 +55,9 @@ pub(crate) fn delegate( _ => { // if mixnode is no longer bonded or in the process of unbonding, return the tokens back to the // delegator; - // (read the notes regarding possible epoch progressiong halting behaviour in `maybe_add_track_undelegation_message`) - let return_tokens = send_to_proxy_or_owner(&proxy, &owner, vec![amount.clone()]); let response = Response::new() - .add_message(return_tokens) - .add_event(new_delegation_on_unbonded_node_event( - &owner, &proxy, mix_id, - )) - .maybe_add_track_vesting_undelegation_message( - deps.storage, - proxy, - owner.to_string(), - mix_id, - amount, - )?; + .send_tokens(&owner, amount.clone()) + .add_event(new_delegation_on_unbonded_node_event(&owner, mix_id)); return Ok(response); } @@ -84,7 +72,7 @@ pub(crate) fn delegate( // if there's an existing delegation, then withdraw the full reward and create a new delegation // with the sum of both - let storage_key = Delegation::generate_storage_key(mix_id, &owner, proxy.as_ref()); + let storage_key = Delegation::generate_storage_key(mix_id, &owner, None); let old_delegation = if let Some(existing_delegation) = delegations_storage::delegations().may_load(deps.storage, storage_key.clone())? { @@ -106,7 +94,6 @@ pub(crate) fn delegate( let cosmos_event = new_delegation_event( created_at, &owner, - &proxy, &new_delegation_amount, mix_id, mix_rewarding.total_unit_reward, @@ -118,7 +105,6 @@ pub(crate) fn delegate( mix_rewarding.total_unit_reward, stored_delegation_amount, env.block.height, - proxy, ); // save on reading since `.save()` would have attempted to read old data that we already have on hand @@ -138,11 +124,10 @@ pub(crate) fn undelegate( created_at: BlockHeight, owner: Addr, mix_id: MixId, - proxy: Option, ) -> Result { // see if the delegation still exists (in case of impatient user who decided to send multiple // undelegation requests in an epoch) - let storage_key = Delegation::generate_storage_key(mix_id, &owner, proxy.as_ref()); + let storage_key = Delegation::generate_storage_key(mix_id, &owner, None); let delegation = match delegations_storage::delegations().may_load(deps.storage, storage_key)? { None => return Ok(Response::default()), Some(delegation) => delegation, @@ -155,18 +140,9 @@ pub(crate) fn undelegate( let tokens_to_return = delegations::helpers::undelegate(deps.storage, delegation, mix_rewarding)?; - // (read the notes regarding possible epoch progressiong halting behaviour in `maybe_add_track_undelegation_message`) - let return_tokens = send_to_proxy_or_owner(&proxy, &owner, vec![tokens_to_return.clone()]); let response = Response::new() - .add_message(return_tokens) - .add_event(new_undelegation_event(created_at, &owner, &proxy, mix_id)) - .maybe_add_track_vesting_undelegation_message( - deps.storage, - proxy, - owner.to_string(), - mix_id, - tokens_to_return, - )?; + .send_tokens(&owner, tokens_to_return.clone()) + .add_event(new_undelegation_event(created_at, &owner, mix_id)); Ok(response) } @@ -197,25 +173,15 @@ pub(crate) fn unbond_mixnode( .rewarding_details .operator_pledge_with_reward(rewarding_denom); - let proxy = &node_details.bond_information.proxy; let owner = &node_details.bond_information.owner; - // send bonded funds (alongside all earned rewards) to the bond owner - let return_tokens = send_to_proxy_or_owner(proxy, owner, vec![tokens.clone()]); - // remove the bond and if there are no delegations left, also the rewarding information // decrement the associated layer count cleanup_post_unbond_mixnode_storage(deps.storage, env, &node_details)?; let response = Response::new() - .add_message(return_tokens) - .add_event(new_mixnode_unbonding_event(created_at, mix_id)) - .maybe_add_track_vesting_unbond_mixnode_message( - deps.storage, - proxy.clone(), - owner.clone().into_string(), - tokens, - )?; + .send_tokens(owner, tokens.clone()) + .add_event(new_mixnode_unbonding_event(created_at, mix_id)); Ok(response) } @@ -311,12 +277,8 @@ pub(crate) fn decrease_pledge( updated_bond.original_pledge.amount -= decrease_by.amount; updated_rewarding.decrease_operator_uint128(decrease_by.amount)?; - let proxy = &mix_details.bond_information.proxy; let owner = &mix_details.bond_information.owner; - // send the removed tokens back to the operator - let return_tokens = send_to_proxy_or_owner(proxy, owner, vec![decrease_by.clone()]); - // update all: bond information, rewarding details and pending pledge changes mixnodes_storage::mixnode_bonds().replace( deps.storage, @@ -328,14 +290,8 @@ pub(crate) fn decrease_pledge( mixnodes_storage::PENDING_MIXNODE_CHANGES.save(deps.storage, mix_id, &pending_changes)?; let response = Response::new() - .add_message(return_tokens) - .add_event(new_pledge_decrease_event(created_at, mix_id, &decrease_by)) - .maybe_add_track_vesting_decrease_mixnode_pledge( - deps.storage, - proxy.clone(), - owner.clone().to_string(), - decrease_by, - )?; + .send_tokens(owner, decrease_by.clone()) + .add_event(new_pledge_decrease_event(created_at, mix_id, &decrease_by)); Ok(response) } @@ -349,13 +305,11 @@ impl ContractExecutableEvent for PendingEpochEventData { owner, mix_id, amount, - proxy, - } => delegate(deps, env, self.created_at, owner, mix_id, amount, proxy), - PendingEpochEventKind::Undelegate { - owner, - mix_id, - proxy, - } => undelegate(deps, self.created_at, owner, mix_id, proxy), + .. + } => delegate(deps, env, self.created_at, owner, mix_id, amount), + PendingEpochEventKind::Undelegate { owner, mix_id, .. } => { + undelegate(deps, self.created_at, owner, mix_id) + } PendingEpochEventKind::PledgeMore { mix_id, amount } => { increase_pledge(deps, self.created_at, mix_id, amount) } @@ -472,33 +426,25 @@ impl ContractExecutableEvent for PendingIntervalEventData { #[cfg(test)] mod tests { - use std::time::Duration; - - use cosmwasm_std::Decimal; - - use mixnet_contract_common::Percent; - use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; - + use super::*; use crate::support::tests::test_helpers; use crate::support::tests::test_helpers::{assert_decimals, TestSetup}; - - use super::*; + use cosmwasm_std::Decimal; + use mixnet_contract_common::Percent; + use std::time::Duration; // note that authorization and basic validation has already been performed for all of those // before being pushed onto the event queues #[cfg(test)] mod delegating { - use cosmwasm_std::testing::mock_info; - use cosmwasm_std::{coin, to_binary, CosmosMsg, WasmMsg}; - - use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; - + use super::*; use crate::mixnodes::transactions::try_remove_mixnode; use crate::support::tests::fixtures::TEST_COIN_DENOM; use crate::support::tests::test_helpers::get_bank_send_msg; - - use super::*; + use cosmwasm_std::coin; + use cosmwasm_std::testing::mock_info; + use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; #[test] fn returns_the_tokens_if_mixnode_has_unbonded() { @@ -523,7 +469,6 @@ mod tests { Addr::unchecked(owner1), mix_id, delegation_coin.clone(), - None, ) .unwrap(); @@ -549,7 +494,6 @@ mod tests { Addr::unchecked(owner2), mix_id, delegation_coin.clone(), - None, ) .unwrap(); let storage_key = @@ -588,7 +532,6 @@ mod tests { Addr::unchecked(owner1), mix_id, delegation_coin.clone(), - None, ) .unwrap(); @@ -614,7 +557,6 @@ mod tests { Addr::unchecked(owner2), mix_id, delegation_coin.clone(), - None, ) .unwrap(); let storage_key = @@ -650,7 +592,6 @@ mod tests { Addr::unchecked(owner), mix_id, delegation_coin_new, - None, ) .unwrap(); @@ -725,7 +666,6 @@ mod tests { Addr::unchecked(owner), mix_id, delegation_coin_new, - None, ) .unwrap(); @@ -797,7 +737,6 @@ mod tests { Addr::unchecked(owner), mix_id, delegation_coin.clone(), - None, ) .unwrap(); assert!(get_bank_send_msg(&res).is_none()); @@ -816,117 +755,13 @@ mod tests { Decimal::from_atomics(delegation, 0).unwrap() ) } - - #[test] - fn attaches_vesting_contract_track_message_if_tokens_are_returned() { - let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - - let delegation = 120_000_000u128; - let delegation_coin = coin(delegation, TEST_COIN_DENOM); - let owner = "delegator"; - - let env = test.env(); - unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap(); - - let vesting_contract = test.vesting_contract(); - - // for a fresh delegation, nothing was added to the storage either - let res_vesting = delegate( - test.deps_mut(), - &env, - 123, - Addr::unchecked(owner), - mix_id, - delegation_coin.clone(), - Some(vesting_contract.clone()), - ) - .unwrap(); - let storage_key = Delegation::generate_storage_key( - mix_id, - &Addr::unchecked(owner), - Some(vesting_contract.clone()).as_ref(), - ); - assert!(delegations_storage::delegations() - .may_load(test.deps().storage, storage_key) - .unwrap() - .is_none()); - // and all tokens are returned back to the proxy - let (receiver, sent_amount) = get_bank_send_msg(&res_vesting).unwrap(); - assert_eq!(receiver, vesting_contract.as_str()); - assert_eq!(sent_amount[0], delegation_coin); - - // and we get appropriate track message - let mut found_track = true; - for msg in &res_vesting.messages { - if let CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr, - msg, - funds, - }) = &msg.msg - { - found_track = true; - assert_eq!(contract_addr, vesting_contract.as_str()); - let expected_msg = to_binary(&VestingContractExecuteMsg::TrackUndelegation { - owner: owner.to_string(), - mix_id, - amount: delegation_coin.clone(), - }) - .unwrap(); - assert_eq!(&expected_msg, msg); - assert!(funds.is_empty()) - } - } - assert!(found_track); - } - - #[test] - fn returns_error_for_illegal_proxy() { - let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - - let delegation = 120_000_000u128; - let delegation_coin = coin(delegation, TEST_COIN_DENOM); - let owner = "delegator"; - let dummy_proxy = Addr::unchecked("not-vesting-contract"); - - let env = test.env(); - unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap(); - - let vesting_contract = test.vesting_contract(); - - // try to add illegal delegation (with invalid proxy) - let res_other_proxy = delegate( - test.deps_mut(), - &env, - 123, - Addr::unchecked(owner), - mix_id, - delegation_coin, - Some(dummy_proxy.clone()), - ) - .unwrap_err(); - - assert_eq!( - res_other_proxy, - MixnetContractError::ProxyIsNotVestingContract { - received: dummy_proxy, - vesting_contract, - } - ); - } } #[cfg(test)] mod undelegating { - use cosmwasm_std::{coin, to_binary, CosmosMsg, WasmMsg}; - - use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; - - use crate::support::tests::fixtures::TEST_COIN_DENOM; - use crate::support::tests::test_helpers::get_bank_send_msg; - use super::*; + use crate::support::tests::test_helpers::get_bank_send_msg; + use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; #[test] fn doesnt_return_any_tokens_if_it_doesnt_exist() { @@ -935,7 +770,7 @@ mod tests { let owner = Addr::unchecked("delegator"); - let res = undelegate(test.deps_mut(), 123, owner, mix_id, None).unwrap(); + let res = undelegate(test.deps_mut(), 123, owner, mix_id).unwrap(); assert!(get_bank_send_msg(&res).is_none()); } @@ -950,7 +785,7 @@ mod tests { // this should never happen in actual code, but if we manually messed something up, // lets make sure this throws an error rewards_storage::MIXNODE_REWARDING.remove(test.deps_mut().storage, mix_id); - let res = undelegate(test.deps_mut(), 123, owner, mix_id, None); + let res = undelegate(test.deps_mut(), 123, owner, mix_id); assert!(matches!( res, Err(MixnetContractError::InconsistentState { .. }) @@ -996,8 +831,7 @@ mod tests { let expected_return = delegation + truncated_reward.u128(); - let res = - undelegate(test.deps_mut(), 123, Addr::unchecked(owner), mix_id, None).unwrap(); + let res = undelegate(test.deps_mut(), 123, Addr::unchecked(owner), mix_id).unwrap(); let (receiver, sent_amount) = get_bank_send_msg(&res).unwrap(); assert_eq!(receiver, owner); assert_eq!(sent_amount[0].amount.u128(), expected_return); @@ -1015,116 +849,18 @@ mod tests { assert!(rewarding.delegates.is_zero()); assert_eq!(rewarding.unique_delegations, 0); } - - #[test] - fn attaches_vesting_contract_track_message_if_tokens_are_returned() { - let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - - let delegation = 120_000_000u128; - let delegation_coin = coin(delegation, TEST_COIN_DENOM); - let owner = "delegator"; - - let vesting_contract = test.vesting_contract(); - - test.add_immediate_delegation_with_legal_proxy(owner, delegation, mix_id); - - let res_vesting = undelegate( - test.deps_mut(), - 123, - Addr::unchecked(owner), - mix_id, - Some(vesting_contract.clone()), - ) - .unwrap(); - let storage_key = Delegation::generate_storage_key( - mix_id, - &Addr::unchecked(owner), - Some(vesting_contract.clone()).as_ref(), - ); - assert!(delegations_storage::delegations() - .may_load(test.deps().storage, storage_key) - .unwrap() - .is_none()); - - // and all tokens are returned back to the proxy - let (receiver, sent_amount) = get_bank_send_msg(&res_vesting).unwrap(); - assert_eq!(receiver, vesting_contract.as_str()); - assert_eq!(sent_amount[0], delegation_coin); - - // and we get appropriate track message - let mut found_track = true; - for msg in &res_vesting.messages { - if let CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr, - msg, - funds, - }) = &msg.msg - { - found_track = true; - assert_eq!(contract_addr, vesting_contract.as_str()); - let expected_msg = to_binary(&VestingContractExecuteMsg::TrackUndelegation { - owner: owner.to_string(), - mix_id, - amount: delegation_coin.clone(), - }) - .unwrap(); - assert_eq!(&expected_msg, msg); - assert!(funds.is_empty()) - } - } - assert!(found_track); - } - - #[test] - fn returns_error_for_illegal_proxy() { - let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - - let delegation = 120_000_000u128; - let owner = "delegator1"; - - let vesting_contract = test.vesting_contract(); - let dummy_proxy = Addr::unchecked("not-vesting-contract"); - - test.add_immediate_delegation_with_illegal_proxy( - owner, - delegation, - mix_id, - dummy_proxy.clone(), - ); - - let res_other_proxy = undelegate( - test.deps_mut(), - 123, - Addr::unchecked(owner), - mix_id, - Some(dummy_proxy.clone()), - ) - .unwrap_err(); - assert_eq!( - res_other_proxy, - MixnetContractError::ProxyIsNotVestingContract { - received: dummy_proxy, - vesting_contract, - } - ); - } } #[cfg(test)] mod mixnode_unbonding { - use cosmwasm_std::{coin, to_binary, CosmosMsg, Uint128, WasmMsg}; - - use mixnet_contract_common::mixnode::{PendingMixNodeChanges, UnbondedMixnode}; - use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; - + use super::*; use crate::mixnodes::storage as mixnodes_storage; - use crate::mixnodes::transactions::{_try_decrease_pledge, _try_increase_pledge}; - use crate::support::tests::fixtures::TEST_COIN_DENOM; + use crate::mixnodes::transactions::{try_decrease_pledge, try_increase_pledge}; use crate::support::tests::test_helpers::get_bank_send_msg; - - use super::*; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::Uint128; + use mixnet_contract_common::mixnode::{PendingMixNodeChanges, UnbondedMixnode}; + use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; #[test] fn returns_hard_error_if_mixnode_doesnt_exist() { @@ -1150,12 +886,10 @@ mod tests { let pledge = Uint128::new(250_000_000); let mix_id = test.add_dummy_mixnode(owner, Some(pledge)); - _try_increase_pledge( + try_increase_pledge( test.deps_mut(), env.clone(), - change.clone(), - Addr::unchecked(owner), - None, + mock_info(owner, &change.clone()), ) .unwrap(); @@ -1170,12 +904,11 @@ mod tests { let pledge = Uint128::new(250_000_000); let mix_id = test.add_dummy_mixnode(owner, Some(pledge)); - _try_decrease_pledge( + try_decrease_pledge( test.deps_mut(), env.clone(), + mock_info(owner, &[]), change[0].clone(), - Addr::unchecked(owner), - None, ) .unwrap(); @@ -1263,79 +996,6 @@ mod tests { 0 ) } - - #[test] - fn attaches_vesting_contract_track_message_if_tokens_are_returned() { - let mut test = TestSetup::new(); - - let vesting_contract = test.vesting_contract(); - - let pledge = Uint128::new(250_000_000); - let pledge_coin = coin(250_000_000, TEST_COIN_DENOM); - let owner = "mix-owner1"; - let mix_id_vesting = test.add_dummy_mixnode_with_legal_proxy(owner, Some(pledge)); - - let env = test.env(); - let res = unbond_mixnode(test.deps_mut(), &env, 123, mix_id_vesting).unwrap(); - - assert!(mixnodes_storage::mixnode_bonds() - .may_load(test.deps().storage, mix_id_vesting) - .unwrap() - .is_none()); - - // and all tokens are returned back to the proxy - let (receiver, sent_amount) = get_bank_send_msg(&res).unwrap(); - assert_eq!(receiver, vesting_contract.as_str()); - assert_eq!(sent_amount[0], pledge_coin); - - // and we get appropriate track message - let mut found_track = true; - for msg in &res.messages { - if let CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr, - msg, - funds, - }) = &msg.msg - { - found_track = true; - assert_eq!(contract_addr, vesting_contract.as_str()); - let expected_msg = to_binary(&VestingContractExecuteMsg::TrackUnbondMixnode { - owner: owner.to_string(), - amount: pledge_coin.clone(), - }) - .unwrap(); - assert_eq!(&expected_msg, msg); - assert!(funds.is_empty()) - } - } - assert!(found_track); - } - - #[test] - fn returns_error_for_illegal_proxy() { - let mut test = TestSetup::new(); - - let dummy_proxy = Addr::unchecked("not-vesting-contract"); - let env = test.env(); - - let vesting_contract = test.vesting_contract(); - let owner = "mix-owner"; - let pledge = Uint128::new(250_000_000); - - let mix_id_illegal_proxy = - test.add_dummy_mixnode_with_illegal_proxy(owner, Some(pledge), dummy_proxy.clone()); - - // this is the halting issue that should have never occurred - let res_other_proxy = - unbond_mixnode(test.deps_mut(), &env, 123, mix_id_illegal_proxy).unwrap_err(); - assert_eq!( - res_other_proxy, - MixnetContractError::ProxyIsNotVestingContract { - received: dummy_proxy, - vesting_contract, - } - ); - } } #[cfg(test)] @@ -1615,11 +1275,9 @@ mod tests { #[cfg(test)] mod decreasing_pledge { - use cosmwasm_std::{to_binary, BankMsg, CosmosMsg, Uint128, WasmMsg}; - - use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; - use super::*; + use cosmwasm_std::{BankMsg, CosmosMsg, Uint128}; + use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; #[test] fn returns_hard_error_if_mixnode_doesnt_exist() { @@ -1699,64 +1357,6 @@ mod tests { ) } - #[test] - fn returns_tokens_back_to_the_proxy_if_bonded_with_vesting() { - let mut test = TestSetup::new(); - let owner = "mix-owner"; - let mix_id = test.add_dummy_mixnode_with_legal_proxy(owner, None); - test.set_pending_pledge_change(mix_id, None); - - let vesting_contract = test.vesting_contract(); - - let amount = test.coin(12345); - let res = decrease_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap(); - - assert_eq!(res.messages.len(), 2); - assert_eq!( - res.messages[0].msg, - CosmosMsg::Bank(BankMsg::Send { - to_address: vesting_contract.to_string(), - amount: vec![amount], - }) - ) - } - - #[test] - fn attaches_vesting_track_message() { - let mut test = TestSetup::new(); - let mix_id_no_proxy = test.add_dummy_mixnode("mix-owner1", None); - test.set_pending_pledge_change(mix_id_no_proxy, None); - - let mix_id_proxy = test.add_dummy_mixnode_with_legal_proxy("mix-owner2", None); - test.set_pending_pledge_change(mix_id_proxy, None); - - let vesting_contract = test.vesting_contract(); - - let amount = test.coin(12345); - let res_no_proxy = - decrease_pledge(test.deps_mut(), 123, mix_id_no_proxy, amount.clone()).unwrap(); - - // nothing was attached (apart from bank message tested in `returns_tokens_back_to_the_owner`) - // because it wasn't done with proxy! - assert_eq!(res_no_proxy.messages.len(), 1); - - let res_proxy = - decrease_pledge(test.deps_mut(), 123, mix_id_proxy, amount.clone()).unwrap(); - assert_eq!(res_proxy.messages.len(), 2); - assert_eq!( - res_proxy.messages[1].msg, - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: vesting_contract.to_string(), - msg: to_binary(&VestingContractExecuteMsg::TrackDecreasePledge { - owner: "mix-owner2".to_string(), - amount, - }) - .unwrap(), - funds: vec![], - }) - ); - } - #[test] fn without_any_events_in_between_is_equivalent_to_pledging_the_same_amount_immediately() { let mut test = TestSetup::new(); diff --git a/contracts/mixnet/src/interval/queries.rs b/contracts/mixnet/src/interval/queries.rs index d0b480848f4..423d9a90ae0 100644 --- a/contracts/mixnet/src/interval/queries.rs +++ b/contracts/mixnet/src/interval/queries.rs @@ -159,11 +159,8 @@ mod tests { } fn push_dummy_epoch_action(test: &mut TestSetup) { - let dummy_action = PendingEpochEventKind::Undelegate { - owner: Addr::unchecked("foomp"), - mix_id: test.rng.next_u32(), - proxy: None, - }; + let dummy_action = + PendingEpochEventKind::new_undelegate(Addr::unchecked("foomp"), test.rng.next_u32()); let env = test.env(); storage::push_new_epoch_event(test.deps_mut().storage, &env, dummy_action).unwrap(); } @@ -571,11 +568,8 @@ mod tests { ); // it exists - let dummy_action = PendingEpochEventKind::Undelegate { - owner: Addr::unchecked("foomp"), - mix_id: test.rng.next_u32(), - proxy: None, - }; + let dummy_action = + PendingEpochEventKind::new_undelegate(Addr::unchecked("foomp"), test.rng.next_u32()); let env = test.env(); storage::push_new_epoch_event(test.deps_mut().storage, &env, dummy_action.clone()).unwrap(); let expected = PendingEpochEventResponse { diff --git a/contracts/mixnet/src/interval/storage.rs b/contracts/mixnet/src/interval/storage.rs index 24666db431a..33eddd4d4e8 100644 --- a/contracts/mixnet/src/interval/storage.rs +++ b/contracts/mixnet/src/interval/storage.rs @@ -222,11 +222,10 @@ mod tests { let env = test.env(); for _ in 0..500 { - let dummy_action = PendingEpochEventKind::Undelegate { - owner: Addr::unchecked("foomp"), - mix_id: test.rng.next_u32(), - proxy: None, - }; + let dummy_action = PendingEpochEventKind::new_undelegate( + Addr::unchecked("foomp"), + test.rng.next_u32(), + ); let id = push_new_epoch_event(test.deps_mut().storage, &env, dummy_action).unwrap(); let expected = EPOCH_EVENT_ID_COUNTER.load(test.deps().storage).unwrap(); assert_eq!(expected, id); @@ -235,11 +234,10 @@ mod tests { test.execute_all_pending_events(); for _ in 0..10 { - let dummy_action = PendingEpochEventKind::Undelegate { - owner: Addr::unchecked("foomp"), - mix_id: test.rng.next_u32(), - proxy: None, - }; + let dummy_action = PendingEpochEventKind::new_undelegate( + Addr::unchecked("foomp"), + test.rng.next_u32(), + ); let id = push_new_epoch_event(test.deps_mut().storage, &env, dummy_action).unwrap(); let expected = EPOCH_EVENT_ID_COUNTER.load(test.deps().storage).unwrap(); assert_eq!(expected, id); diff --git a/contracts/mixnet/src/interval/transactions.rs b/contracts/mixnet/src/interval/transactions.rs index 0eae4fed7b7..349545984f8 100644 --- a/contracts/mixnet/src/interval/transactions.rs +++ b/contracts/mixnet/src/interval/transactions.rs @@ -373,18 +373,14 @@ mod tests { use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::Addr; use mixnet_contract_common::pending_events::PendingEpochEventKind; - use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; fn push_n_dummy_epoch_actions(test: &mut TestSetup, n: usize) { // if you attempt to undelegate non-existent delegation, // it will return an empty response, but will not fail let env = test.env(); for i in 0..n { - let dummy_action = PendingEpochEventKind::Undelegate { - owner: Addr::unchecked("foomp"), - mix_id: i as MixId, - proxy: None, - }; + let dummy_action = + PendingEpochEventKind::new_undelegate(Addr::unchecked("foomp"), i as MixId); storage::push_new_epoch_event(test.deps_mut().storage, &env, dummy_action).unwrap(); } } @@ -406,7 +402,7 @@ mod tests { mod performing_pending_epoch_actions { use super::*; use crate::support::tests::fixtures::TEST_COIN_DENOM; - use cosmwasm_std::{coin, coins, wasm_execute, BankMsg, Empty, SubMsg}; + use cosmwasm_std::{coin, coins, BankMsg, Empty, SubMsg}; use mixnet_contract_common::events::{ new_active_set_update_event, new_delegation_on_unbonded_node_event, new_undelegation_event, @@ -495,7 +491,6 @@ mod tests { #[test] fn catches_all_events_and_messages_from_executed_actions() { let mut test = TestSetup::new(); - let vesting_contract = test.vesting_contract(); let env = test.env(); let legit_mix = test.add_dummy_mixnode("mix-owner", None); @@ -509,17 +504,15 @@ mod tests { // delegate to node that doesn't exist, // we expect to receive BankMsg with tokens being returned, // and event regarding delegation - let non_existent_delegation = PendingEpochEventKind::Delegate { - owner: Addr::unchecked("foomp"), - mix_id: 123, - amount: coin(123, TEST_COIN_DENOM), - proxy: None, - }; + let non_existent_delegation = PendingEpochEventKind::new_delegate( + Addr::unchecked("foomp"), + 123, + coin(123, TEST_COIN_DENOM), + ); storage::push_new_epoch_event(test.deps_mut().storage, &env, non_existent_delegation) .unwrap(); expected_events.push(new_delegation_on_unbonded_node_event( &Addr::unchecked("foomp"), - &None, 123, )); expected_messages.push(SubMsg::new(BankMsg::Send { @@ -527,33 +520,6 @@ mod tests { amount: coins(123, TEST_COIN_DENOM), })); - // delegation to node that doesn't exist with vesting contract - // we expect the same as above PLUS TrackUndelegation message - let non_existent_delegation = PendingEpochEventKind::Delegate { - owner: Addr::unchecked("foomp2"), - mix_id: 123, - amount: coin(123, TEST_COIN_DENOM), - proxy: Some(vesting_contract.clone()), - }; - storage::push_new_epoch_event(test.deps_mut().storage, &env, non_existent_delegation) - .unwrap(); - expected_events.push(new_delegation_on_unbonded_node_event( - &Addr::unchecked("foomp2"), - &Some(vesting_contract.clone()), - 123, - )); - expected_messages.push(SubMsg::new(BankMsg::Send { - to_address: vesting_contract.clone().into_string(), - amount: coins(123, TEST_COIN_DENOM), - })); - let msg = VestingContractExecuteMsg::TrackUndelegation { - owner: "foomp2".to_string(), - mix_id: 123, - amount: coin(123, TEST_COIN_DENOM), - }; - let track_undelegate_message = wasm_execute(vesting_contract, &msg, vec![]).unwrap(); - expected_messages.push(SubMsg::new(track_undelegate_message)); - // updating active set should only emit events and no cosmos messages let action_with_event = PendingEpochEventKind::UpdateActiveSetSize { new_size: 50 }; storage::push_new_epoch_event(test.deps_mut().storage, &env, action_with_event) @@ -561,16 +527,12 @@ mod tests { expected_events.push(new_active_set_update_event(env.block.height, 50)); // undelegation just returns tokens and emits event - let legit_undelegate = PendingEpochEventKind::Undelegate { - owner: delegator.clone(), - mix_id: legit_mix, - proxy: None, - }; + let legit_undelegate = + PendingEpochEventKind::new_undelegate(delegator.clone(), legit_mix); storage::push_new_epoch_event(test.deps_mut().storage, &env, legit_undelegate).unwrap(); expected_events.push(new_undelegation_event( env.block.height, &delegator, - &None, legit_mix, )); expected_messages.push(SubMsg::new(BankMsg::Send { @@ -583,9 +545,9 @@ mod tests { let mut expected = Response::new().add_events(expected_events); expected.messages = expected_messages; assert_eq!(res, expected); - assert_eq!(executed, 4); + assert_eq!(executed, 3); assert_eq!( - 4, + 3, storage::LAST_PROCESSED_EPOCH_EVENT .load(test.deps().storage) .unwrap() @@ -1330,17 +1292,15 @@ mod tests { let mut expected_messages: Vec> = Vec::new(); // epoch event - let non_existent_delegation = PendingEpochEventKind::Delegate { - owner: Addr::unchecked("foomp"), - mix_id: 123, - amount: coin(123, TEST_COIN_DENOM), - proxy: None, - }; + let non_existent_delegation = PendingEpochEventKind::new_delegate( + Addr::unchecked("foomp"), + 123, + coin(123, TEST_COIN_DENOM), + ); storage::push_new_epoch_event(test.deps_mut().storage, &env, non_existent_delegation) .unwrap(); expected_events.push(new_delegation_on_unbonded_node_event( &Addr::unchecked("foomp"), - &None, 123, )); expected_messages.push(SubMsg::new(BankMsg::Send { diff --git a/contracts/mixnet/src/lib.rs b/contracts/mixnet/src/lib.rs index 80e6822f896..985cffda57b 100644 --- a/contracts/mixnet/src/lib.rs +++ b/contracts/mixnet/src/lib.rs @@ -19,3 +19,4 @@ mod support; #[cfg(feature = "contract-testing")] mod testing; +mod vesting_migration; 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/helpers.rs b/contracts/mixnet/src/mixnodes/helpers.rs index 2650283b051..b8dd15ae6e4 100644 --- a/contracts/mixnet/src/mixnodes/helpers.rs +++ b/contracts/mixnet/src/mixnodes/helpers.rs @@ -97,7 +97,6 @@ pub(crate) fn save_new_mixnode( mixnode: MixNode, cost_params: MixNodeCostParams, owner: Addr, - proxy: Option, pledge: Coin, ) -> Result<(MixId, Layer), MixnetContractError> { let layer = assign_layer(storage)?; @@ -105,15 +104,7 @@ pub(crate) fn save_new_mixnode( let current_epoch = interval_storage::current_interval(storage)?.current_epoch_absolute_id(); let mixnode_rewarding = MixNodeRewarding::initialise_new(cost_params, &pledge, current_epoch)?; - let mixnode_bond = MixNodeBond::new( - mix_id, - owner, - pledge, - layer, - mixnode, - proxy, - env.block.height, - ); + let mixnode_bond = MixNodeBond::new(mix_id, owner, pledge, layer, mixnode, env.block.height); // save mixnode bond data // note that this implicitly checks for uniqueness on identity key, sphinx key and owner @@ -411,7 +402,6 @@ pub(crate) mod tests { mixnode, cost_params.clone(), owner.clone(), - None, pledge.clone(), ) .unwrap(); @@ -444,7 +434,6 @@ pub(crate) mod tests { mixnode, cost_params.clone(), Addr::unchecked("different-owner"), - None, pledge.clone(), ); assert!(res.is_err()); @@ -457,7 +446,6 @@ pub(crate) mod tests { mixnode, cost_params.clone(), owner, - None, pledge.clone(), ); assert!(res.is_err()); @@ -471,7 +459,6 @@ pub(crate) mod tests { mixnode, cost_params, Addr::unchecked("different-owner"), - None, pledge, ); assert!(res.is_err()); diff --git a/contracts/mixnet/src/mixnodes/signature_helpers.rs b/contracts/mixnet/src/mixnodes/signature_helpers.rs index 2ccec88d810..975bb556693 100644 --- a/contracts/mixnet/src/mixnodes/signature_helpers.rs +++ b/contracts/mixnet/src/mixnodes/signature_helpers.rs @@ -12,7 +12,6 @@ use nym_contracts_common::signing::Verifier; pub(crate) fn verify_mixnode_bonding_signature( deps: Deps<'_>, sender: Addr, - proxy: Option, pledge: Coin, mixnode: MixNode, cost_params: MixNodeCostParams, @@ -23,8 +22,7 @@ pub(crate) fn verify_mixnode_bonding_signature( // reconstruct the payload let nonce = signing_storage::get_signing_nonce(deps.storage, sender.clone())?; - let msg = - construct_mixnode_bonding_sign_payload(nonce, sender, proxy, pledge, mixnode, cost_params); + let msg = construct_mixnode_bonding_sign_payload(nonce, sender, pledge, mixnode, cost_params); if deps.api.verify_message(msg, signature, &public_key)? { Ok(()) diff --git a/contracts/mixnet/src/mixnodes/transactions.rs b/contracts/mixnet/src/mixnodes/transactions.rs index 032ef29bc92..59ebc432c07 100644 --- a/contracts/mixnet/src/mixnodes/transactions.rs +++ b/contracts/mixnet/src/mixnodes/transactions.rs @@ -1,8 +1,7 @@ -// Copyright 2021-2023 - Nym Technologies SA +// Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{coin, Addr, Coin, DepsMut, Env, MessageInfo, Response, Storage}; - +use cosmwasm_std::{coin, Coin, DepsMut, Env, MessageInfo, Response, Storage}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ new_mixnode_bonding_event, new_mixnode_config_update_event, @@ -25,8 +24,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, - validate_pledge, + ensure_no_pending_pledge_changes, ensure_operating_cost_within_range, + ensure_profit_margin_within_range, validate_pledge, }; use super::storage; @@ -61,74 +60,30 @@ pub fn assign_mixnode_layer( Ok(Response::default()) } -pub fn try_add_mixnode( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - mix_node: MixNode, - cost_params: MixNodeCostParams, - owner_signature: MessageSignature, -) -> Result { - _try_add_mixnode( - deps, - env, - mix_node, - cost_params, - info.funds, - info.sender, - owner_signature, - None, - ) -} - -pub fn try_add_mixnode_on_behalf( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - mix_node: MixNode, - cost_params: MixNodeCostParams, - owner: String, - owner_signature: MessageSignature, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let proxy = info.sender; - let owner = deps.api.addr_validate(&owner)?; - _try_add_mixnode( - deps, - env, - mix_node, - cost_params, - info.funds, - owner, - owner_signature, - Some(proxy), - ) -} - -// I'm not entirely sure how to deal with this warning at the current moment -// // TODO: perhaps also require the user to explicitly provide what it thinks is the current nonce // so that we could return a better error message if it doesn't match? -#[allow(clippy::too_many_arguments)] -fn _try_add_mixnode( +pub(crate) fn try_add_mixnode( deps: DepsMut<'_>, env: Env, + info: MessageInfo, mixnode: MixNode, cost_params: MixNodeCostParams, - pledge: Vec, - owner: Addr, 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)?; + let pledge = validate_pledge(info.funds, minimum_pledge)?; // if the client has an active bonded mixnode or gateway, don't allow bonding // note that this has to be done explicitly as `UniqueIndex` constraint would not protect us // against attempting to use different node types (i.e. gateways and mixnodes) - ensure_no_existing_bond(&owner, deps.storage)?; + ensure_no_existing_bond(&info.sender, deps.storage)?; // there's no need to explicitly check whether there already exists mixnode with the same // identity or sphinx keys as this is going to be done implicitly when attempting to save @@ -137,8 +92,7 @@ fn _try_add_mixnode( // check if this sender actually owns the mixnode by checking the signature verify_mixnode_bonding_signature( deps.as_ref(), - owner.clone(), - proxy.clone(), + info.sender.clone(), pledge.clone(), mixnode.clone(), cost_params.clone(), @@ -146,7 +100,7 @@ fn _try_add_mixnode( )?; // update the signing nonce associated with this sender so that the future signature would be made on the new value - signing_storage::increment_signing_nonce(deps.storage, owner.clone())?; + signing_storage::increment_signing_nonce(deps.storage, info.sender.clone())?; let node_identity = mixnode.identity_key.clone(); let (node_id, layer) = save_new_mixnode( @@ -154,14 +108,12 @@ fn _try_add_mixnode( env, mixnode, cost_params, - owner.clone(), - proxy.clone(), + info.sender.clone(), pledge.clone(), )?; Ok(Response::new().add_event(new_mixnode_bonding_event( - &owner, - &proxy, + &info.sender, &pledge, &node_identity, node_id, @@ -174,43 +126,19 @@ pub fn try_increase_pledge( env: Env, info: MessageInfo, ) -> Result { - _try_increase_pledge(deps, env, info.funds, info.sender, None) -} - -pub fn try_increase_pledge_on_behalf( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - owner: String, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let proxy = info.sender; - let owner = deps.api.addr_validate(&owner)?; - _try_increase_pledge(deps, env, info.funds, owner, Some(proxy)) -} - -pub fn _try_increase_pledge( - deps: DepsMut<'_>, - env: Env, - increase: Vec, - owner: Addr, - proxy: Option, -) -> Result { - let mix_details = get_mixnode_details_by_owner(deps.storage, owner.clone())? - .ok_or(MixnetContractError::NoAssociatedMixNodeBond { owner })?; + let mix_details = get_mixnode_details_by_owner(deps.storage, info.sender.clone())? + .ok_or(MixnetContractError::NoAssociatedMixNodeBond { owner: info.sender })?; let mut pending_changes = mix_details.pending_changes; let mix_id = mix_details.mix_id(); // increasing pledge is only allowed if the epoch is currently not in the process of being advanced ensure_epoch_in_progress_state(deps.storage)?; - ensure_proxy_match(&proxy, &mix_details.bond_information.proxy)?; ensure_bonded(&mix_details.bond_information)?; ensure_no_pending_pledge_changes(&pending_changes)?; let rewarding_denom = rewarding_denom(deps.storage)?; - let pledge_increase = validate_pledge(increase, coin(1, rewarding_denom))?; + let pledge_increase = validate_pledge(info.funds, coin(1, rewarding_denom))?; let cosmos_event = new_pending_pledge_increase_event(mix_id, &pledge_increase); @@ -232,39 +160,14 @@ pub fn try_decrease_pledge( info: MessageInfo, decrease_by: Coin, ) -> Result { - _try_decrease_pledge(deps, env, decrease_by, info.sender, None) -} - -pub fn try_decrease_pledge_on_behalf( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - decrease_by: Coin, - owner: String, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let proxy = info.sender; - let owner = deps.api.addr_validate(&owner)?; - _try_decrease_pledge(deps, env, decrease_by, owner, Some(proxy)) -} - -pub fn _try_decrease_pledge( - deps: DepsMut<'_>, - env: Env, - decrease_by: Coin, - owner: Addr, - proxy: Option, -) -> Result { - let mix_details = get_mixnode_details_by_owner(deps.storage, owner.clone())? - .ok_or(MixnetContractError::NoAssociatedMixNodeBond { owner })?; + let mix_details = get_mixnode_details_by_owner(deps.storage, info.sender.clone())? + .ok_or(MixnetContractError::NoAssociatedMixNodeBond { owner: info.sender })?; let mut pending_changes = mix_details.pending_changes; let mix_id = mix_details.mix_id(); // decreasing pledge is only allowed if the epoch is currently not in the process of being advanced ensure_epoch_in_progress_state(deps.storage)?; - ensure_proxy_match(&proxy, &mix_details.bond_information.proxy)?; ensure_bonded(&mix_details.bond_information)?; ensure_no_pending_pledge_changes(&pending_changes)?; @@ -312,34 +215,12 @@ pub fn _try_decrease_pledge( Ok(Response::new().add_event(cosmos_event)) } -pub fn try_remove_mixnode_on_behalf( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - owner: String, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let proxy = info.sender; - let owner = deps.api.addr_validate(&owner)?; - _try_remove_mixnode(deps, env, owner, Some(proxy)) -} - -pub fn try_remove_mixnode( +pub(crate) fn try_remove_mixnode( deps: DepsMut<'_>, env: Env, info: MessageInfo, ) -> Result { - _try_remove_mixnode(deps, env, info.sender, None) -} - -pub(crate) fn _try_remove_mixnode( - deps: DepsMut<'_>, - env: Env, - owner: Addr, - proxy: Option, -) -> Result { - let existing_bond = must_get_mixnode_bond_by_owner(deps.storage, &owner)?; + let existing_bond = must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; let pending_changes = storage::PENDING_MIXNODE_CHANGES .may_load(deps.storage, existing_bond.mix_id)? .unwrap_or_default(); @@ -348,15 +229,12 @@ pub(crate) fn _try_remove_mixnode( ensure_epoch_in_progress_state(deps.storage)?; // see if the proxy matches - ensure_proxy_match(&proxy, &existing_bond.proxy)?; ensure_bonded(&existing_bond)?; // if there are any pending requests to change the pledge, wait for them to resolve before allowing the unbonding ensure_no_pending_pledge_changes(&pending_changes)?; // set `is_unbonding` field - // clippy beta 1.70.0-beta.1 false positive - #[allow(clippy::redundant_clone)] let mut updated_bond = existing_bond.clone(); updated_bond.is_unbonding = true; storage::mixnode_bonds().replace( @@ -375,7 +253,6 @@ pub(crate) fn _try_remove_mixnode( Ok( Response::new().add_event(new_pending_mixnode_unbonding_event( &existing_bond.owner, - &existing_bond.proxy, existing_bond.identity(), existing_bond.mix_id, )), @@ -387,39 +264,13 @@ pub(crate) fn try_update_mixnode_config( info: MessageInfo, new_config: MixNodeConfigUpdate, ) -> Result { - let owner = info.sender; - _try_update_mixnode_config(deps, new_config, owner, None) -} - -pub(crate) fn try_update_mixnode_config_on_behalf( - deps: DepsMut<'_>, - info: MessageInfo, - new_config: MixNodeConfigUpdate, - owner: String, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let owner = deps.api.addr_validate(&owner)?; - let proxy = info.sender; - _try_update_mixnode_config(deps, new_config, owner, Some(proxy)) -} - -pub(crate) fn _try_update_mixnode_config( - deps: DepsMut<'_>, - new_config: MixNodeConfigUpdate, - owner: Addr, - proxy: Option, -) -> Result { - let existing_bond = must_get_mixnode_bond_by_owner(deps.storage, &owner)?; + let existing_bond = must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; ensure_bonded(&existing_bond)?; - ensure_proxy_match(&proxy, &existing_bond.proxy)?; let cfg_update_event = - new_mixnode_config_update_event(existing_bond.mix_id, &owner, &proxy, &new_config); + new_mixnode_config_update_event(existing_bond.mix_id, &info.sender, &new_config); - // clippy beta 1.70.0-beta.1 false positive - #[allow(clippy::redundant_clone)] let mut updated_bond = existing_bond.clone(); updated_bond.mix_node.host = new_config.host; updated_bond.mix_node.mix_port = new_config.mix_port; @@ -442,45 +293,24 @@ pub(crate) fn try_update_mixnode_cost_params( env: Env, info: MessageInfo, new_costs: MixNodeCostParams, -) -> Result { - let owner = info.sender; - _try_update_mixnode_cost_params(deps, env, new_costs, owner, None) -} - -pub(crate) fn try_update_mixnode_cost_params_on_behalf( - deps: DepsMut, - env: Env, - info: MessageInfo, - new_costs: MixNodeCostParams, - owner: String, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let owner = deps.api.addr_validate(&owner)?; - let proxy = info.sender; - _try_update_mixnode_cost_params(deps, env, new_costs, owner, Some(proxy)) -} - -pub(crate) fn _try_update_mixnode_cost_params( - deps: DepsMut, - env: Env, - new_costs: MixNodeCostParams, - owner: Addr, - proxy: Option, ) -> Result { // see if the node still exists - let existing_bond = must_get_mixnode_bond_by_owner(deps.storage, &owner)?; + let existing_bond = must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; // changing cost params is only allowed if the epoch is currently not in the process of being advanced ensure_epoch_in_progress_state(deps.storage)?; - 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, - &proxy, + &info.sender, &new_costs, ); @@ -497,7 +327,7 @@ pub(crate) fn _try_update_mixnode_cost_params( #[cfg(test)] pub mod tests { use cosmwasm_std::testing::mock_info; - use cosmwasm_std::{Order, StdResult, Uint128}; + use cosmwasm_std::{Addr, Order, StdResult, Uint128}; use mixnet_contract_common::mixnode::PendingMixNodeChanges; use mixnet_contract_common::{EpochState, EpochStatus, ExecuteMsg, LayerDistribution, Percent}; @@ -680,38 +510,6 @@ pub mod tests { assert_eq!(res, Err(MixnetContractError::InvalidEd25519Signature)); } - #[test] - fn mixnode_add_with_illegal_proxy() { - let mut test = TestSetup::new(); - let env = test.env(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let owner = "alice"; - let (mixnode, sig, _) = test.mixnode_with_signature(owner, None); - let cost_params = fixtures::mix_node_cost_params_fixture(); - - let res = try_add_mixnode_on_behalf( - test.deps_mut(), - env, - mock_info(illegal_proxy.as_ref(), &good_mixnode_pledge()), - mixnode, - cost_params, - owner.to_string(), - sig, - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract, - } - ) - } - #[test] fn removing_mixnode_cant_be_performed_if_epoch_transition_is_in_progress() { let bad_states = vec![ @@ -761,23 +559,6 @@ pub mod tests { ); let mix_id = test.add_dummy_mixnode(owner, None); - let vesting_contract = test.vesting_contract(); - - // attempted to remove on behalf with invalid proxy (current is `None`) - let res = try_remove_mixnode_on_behalf( - test.deps_mut(), - env.clone(), - mock_info(vesting_contract.as_ref(), &[]), - owner.to_string(), - ); - - assert_eq!( - res, - Err(MixnetContractError::ProxyMismatch { - existing: "None".to_string(), - incoming: vesting_contract.into_string(), - }) - ); // "normal" unbonding succeeds and unbonding event is pushed to the pending epoch events let res = try_remove_mixnode(test.deps_mut(), env.clone(), info.clone()); @@ -799,35 +580,6 @@ pub mod tests { assert_eq!(res, Err(MixnetContractError::MixnodeIsUnbonding { mix_id })) } - #[test] - fn mixnode_remove_with_illegal_proxy() { - let mut test = TestSetup::new(); - let env = test.env(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let owner = "alice"; - - test.add_dummy_mixnode_with_illegal_proxy(owner, None, illegal_proxy.clone()); - - let res = try_remove_mixnode_on_behalf( - test.deps_mut(), - env, - mock_info(illegal_proxy.as_ref(), &[]), - owner.to_string(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract, - } - ) - } - #[test] fn mixnode_remove_is_not_allowed_if_there_are_pending_pledge_changes() { let mut test = TestSetup::new(); @@ -908,22 +660,7 @@ pub mod tests { ); let mix_id = test.add_dummy_mixnode(owner, None); - let vesting_contract = test.vesting_contract(); - // attempted to remove on behalf with invalid proxy (current is `None`) - let res = try_update_mixnode_config_on_behalf( - test.deps_mut(), - mock_info(vesting_contract.as_ref(), &[]), - update.clone(), - owner.to_string(), - ); - assert_eq!( - res, - Err(MixnetContractError::ProxyMismatch { - existing: "None".to_string(), - incoming: vesting_contract.into_string(), - }) - ); // "normal" update succeeds let res = try_update_mixnode_config(test.deps_mut(), info.clone(), update.clone()); assert!(res.is_ok()); @@ -943,41 +680,6 @@ pub mod tests { assert_eq!(res, Err(MixnetContractError::MixnodeIsUnbonding { mix_id })) } - #[test] - fn updating_mixnode_config_with_illegal_proxy() { - let mut test = TestSetup::new(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let owner = "alice"; - - test.add_dummy_mixnode_with_illegal_proxy(owner, None, illegal_proxy.clone()); - let update = MixNodeConfigUpdate { - host: "1.1.1.1:1234".to_string(), - mix_port: 1234, - verloc_port: 1235, - http_api_port: 1236, - version: "v1.2.3".to_string(), - }; - - let res = try_update_mixnode_config_on_behalf( - test.deps_mut(), - mock_info(illegal_proxy.as_ref(), &[]), - update, - owner.to_string(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract, - } - ) - } - #[test] fn mixnode_cost_params_cant_be_updated_when_epoch_transition_is_in_progress() { let bad_states = vec![ @@ -1042,23 +744,7 @@ pub mod tests { ); let mix_id = test.add_dummy_mixnode(owner, None); - let vesting_contract = test.vesting_contract(); - // attempted to remove on behalf with invalid proxy (current is `None`) - let res = try_update_mixnode_cost_params_on_behalf( - test.deps_mut(), - env.clone(), - mock_info(vesting_contract.as_ref(), &[]), - update.clone(), - owner.to_string(), - ); - assert_eq!( - res, - Err(MixnetContractError::ProxyMismatch { - existing: "None".to_string(), - incoming: vesting_contract.into_string(), - }) - ); // "normal" update succeeds let res = try_update_mixnode_cost_params( test.deps_mut(), @@ -1099,40 +785,6 @@ pub mod tests { assert_eq!(res, Err(MixnetContractError::MixnodeIsUnbonding { mix_id })) } - #[test] - fn updating_mixnode_cost_params_with_illegal_proxy() { - let mut test = TestSetup::new(); - let env = test.env(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let owner = "alice"; - - test.add_dummy_mixnode_with_illegal_proxy(owner, None, illegal_proxy.clone()); - let update = MixNodeCostParams { - profit_margin_percent: Percent::from_percentage_value(42).unwrap(), - interval_operating_cost: Coin::new(12345678, TEST_COIN_DENOM), - }; - - let res = try_update_mixnode_cost_params_on_behalf( - test.deps_mut(), - env, - mock_info(illegal_proxy.as_ref(), &[]), - update, - owner.to_string(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract, - } - ) - } - #[test] fn adding_mixnode_with_duplicate_sphinx_key_errors_out() { let mut test = TestSetup::new(); @@ -1240,69 +892,6 @@ pub mod tests { ) } - #[test] - fn is_not_allowed_if_theres_proxy_mismatch() { - let mut test = TestSetup::new(); - let env = test.env(); - - let owner_without_proxy = Addr::unchecked("no-proxy"); - let owner_with_proxy = Addr::unchecked("with-proxy"); - let proxy = Addr::unchecked("proxy"); - let wrong_proxy = Addr::unchecked("unrelated-proxy"); - - test.add_dummy_mixnode(owner_without_proxy.as_str(), None); - test.add_dummy_mixnode_with_illegal_proxy( - owner_with_proxy.as_str(), - None, - proxy.clone(), - ); - - let res = _try_increase_pledge( - test.deps_mut(), - env.clone(), - Vec::new(), - owner_without_proxy.clone(), - Some(proxy), - ); - assert_eq!( - res, - Err(MixnetContractError::ProxyMismatch { - existing: "None".to_string(), - incoming: "proxy".to_string(), - }) - ); - - let res = _try_increase_pledge( - test.deps_mut(), - env.clone(), - Vec::new(), - owner_with_proxy.clone(), - None, - ); - assert_eq!( - res, - Err(MixnetContractError::ProxyMismatch { - existing: "proxy".to_string(), - incoming: "None".to_string(), - }) - ); - - let res = _try_increase_pledge( - test.deps_mut(), - env, - Vec::new(), - owner_with_proxy.clone(), - Some(wrong_proxy), - ); - assert_eq!( - res, - Err(MixnetContractError::ProxyMismatch { - existing: "proxy".to_string(), - incoming: "unrelated-proxy".to_string(), - }) - ) - } - #[test] fn is_not_allowed_if_mixnode_has_unbonded_or_is_unbonding() { let mut test = TestSetup::new(); @@ -1457,35 +1046,6 @@ pub mod tests { } ); } - - #[test] - fn fails_for_illegal_proxy() { - let mut test = TestSetup::new(); - let env = test.env(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let owner = "alice"; - - test.add_dummy_mixnode_with_illegal_proxy(owner, None, illegal_proxy.clone()); - - let res = try_increase_pledge_on_behalf( - test.deps_mut(), - env, - mock_info(illegal_proxy.as_ref(), &[coin(123, TEST_COIN_DENOM)]), - owner.to_string(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract, - } - ) - } } #[cfg(test)] @@ -1546,73 +1106,6 @@ pub mod tests { ) } - #[test] - fn is_not_allowed_if_theres_proxy_mismatch() { - let mut test = TestSetup::new(); - let env = test.env(); - - let owner_without_proxy = Addr::unchecked("no-proxy"); - let owner_with_proxy = Addr::unchecked("with-proxy"); - let proxy = Addr::unchecked("proxy"); - let wrong_proxy = Addr::unchecked("unrelated-proxy"); - - // just to make sure that after decrease the value would still be above the minimum - let stake = Uint128::new(100_000_000_000); - let decrease = test.coin(1000); - - test.add_dummy_mixnode(owner_without_proxy.as_str(), Some(stake)); - test.add_dummy_mixnode_with_illegal_proxy( - owner_with_proxy.as_str(), - Some(stake), - proxy.clone(), - ); - - let res = _try_decrease_pledge( - test.deps_mut(), - env.clone(), - decrease.clone(), - owner_without_proxy.clone(), - Some(proxy), - ); - assert_eq!( - res, - Err(MixnetContractError::ProxyMismatch { - existing: "None".to_string(), - incoming: "proxy".to_string(), - }) - ); - - let res = _try_decrease_pledge( - test.deps_mut(), - env.clone(), - decrease.clone(), - owner_with_proxy.clone(), - None, - ); - assert_eq!( - res, - Err(MixnetContractError::ProxyMismatch { - existing: "proxy".to_string(), - incoming: "None".to_string(), - }) - ); - - let res = _try_decrease_pledge( - test.deps_mut(), - env, - decrease, - owner_with_proxy.clone(), - Some(wrong_proxy), - ); - assert_eq!( - res, - Err(MixnetContractError::ProxyMismatch { - existing: "proxy".to_string(), - incoming: "unrelated-proxy".to_string(), - }) - ) - } - #[test] fn is_not_allowed_if_mixnode_has_unbonded_or_is_unbonding() { let mut test = TestSetup::new(); @@ -1809,38 +1302,5 @@ pub mod tests { } ); } - - #[test] - fn fails_for_illegal_proxy() { - let mut test = TestSetup::new(); - let env = test.env(); - - let stake = Uint128::new(100_000_000_000); - let decrease = test.coin(1000); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let owner = "alice"; - - test.add_dummy_mixnode_with_illegal_proxy(owner, Some(stake), illegal_proxy.clone()); - - let res = try_decrease_pledge_on_behalf( - test.deps_mut(), - env, - mock_info(illegal_proxy.as_ref(), &[coin(123, TEST_COIN_DENOM)]), - decrease, - owner.to_string(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract, - } - ) - } } } diff --git a/contracts/mixnet/src/queued_migrations.rs b/contracts/mixnet/src/queued_migrations.rs index 8df3ef5d863..ab410897e92 100644 --- a/contracts/mixnet/src/queued_migrations.rs +++ b/contracts/mixnet/src/queued_migrations.rs @@ -1,2 +1,51 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 + +use crate::interval::storage as interval_storage; +use cosmwasm_std::{DepsMut, Order, Storage}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::PendingEpochEventKind; + +fn ensure_no_pending_proxy_events(storage: &dyn Storage) -> Result<(), MixnetContractError> { + let last_executed = interval_storage::LAST_PROCESSED_EPOCH_EVENT.load(storage)?; + let last_inserted = interval_storage::EPOCH_EVENT_ID_COUNTER.load(storage)?; + + // no pending events + if last_executed == last_inserted { + return Ok(()); + } + + for maybe_event in + interval_storage::PENDING_EPOCH_EVENTS.range(storage, None, None, Order::Ascending) + { + let (id, event_data) = maybe_event?; + match event_data.kind { + PendingEpochEventKind::Delegate { proxy, .. } => { + if proxy.is_some() { + return Err(MixnetContractError::FailedMigration { + comment: format!( + "there is a pending vesting contract delegation with id {id}" + ), + }); + } + } + PendingEpochEventKind::Undelegate { proxy, .. } => { + if proxy.is_some() { + return Err(MixnetContractError::FailedMigration { + comment: format!( + "there is a pending vesting contract undelegation with id {id}" + ), + }); + } + } + _ => continue, + } + } + Ok(()) +} + +pub(crate) fn vesting_purge(deps: DepsMut) -> Result<(), MixnetContractError> { + ensure_no_pending_proxy_events(deps.storage)?; + + Ok(()) +} diff --git a/contracts/mixnet/src/rewards/transactions.rs b/contracts/mixnet/src/rewards/transactions.rs index 9aa417ae7ca..3f9b0e329a1 100644 --- a/contracts/mixnet/src/rewards/transactions.rs +++ b/contracts/mixnet/src/rewards/transactions.rs @@ -1,8 +1,20 @@ -// Copyright 2021-2023 - Nym Technologies SA +// Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{wasm_execute, Addr, DepsMut, Env, MessageInfo, Response}; - +use super::storage; +use crate::delegations::storage as delegations_storage; +use crate::interval::storage as interval_storage; +use crate::interval::storage::{push_new_epoch_event, push_new_interval_event}; +use crate::mixnet_contract_settings::storage as mixnet_params_storage; +use crate::mixnodes::helpers::get_mixnode_details_by_owner; +use crate::mixnodes::storage as mixnodes_storage; +use crate::rewards::helpers; +use crate::rewards::helpers::update_and_save_last_rewarded; +use crate::support::helpers::{ + ensure_bonded, ensure_can_advance_epoch, ensure_epoch_in_progress_state, ensure_is_owner, + AttachSendTokens, +}; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ new_active_set_update_event, new_mix_rewarding_event, @@ -16,22 +28,6 @@ use mixnet_contract_common::reward_params::{ IntervalRewardingParamsUpdate, NodeRewardParams, Performance, }; use mixnet_contract_common::{Delegation, EpochState, MixId}; -use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; - -use crate::delegations::storage as delegations_storage; -use crate::interval::storage as interval_storage; -use crate::interval::storage::{push_new_epoch_event, push_new_interval_event}; -use crate::mixnet_contract_settings::storage as mixnet_params_storage; -use crate::mixnodes::helpers::get_mixnode_details_by_owner; -use crate::mixnodes::storage as mixnodes_storage; -use crate::rewards::helpers; -use crate::rewards::helpers::update_and_save_last_rewarded; -use crate::support::helpers::{ - ensure_bonded, ensure_can_advance_epoch, ensure_epoch_in_progress_state, ensure_is_owner, - ensure_proxy_match, ensure_sent_by_vesting_contract, send_to_proxy_or_owner, -}; - -use super::storage; pub(crate) fn try_reward_mixnode( deps: DepsMut<'_>, @@ -111,6 +107,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()); @@ -140,37 +144,16 @@ pub(crate) fn try_withdraw_operator_reward( deps: DepsMut<'_>, info: MessageInfo, ) -> Result { - _try_withdraw_operator_reward(deps, info.sender, None) -} - -pub(crate) fn try_withdraw_operator_reward_on_behalf( - deps: DepsMut<'_>, - info: MessageInfo, - owner: String, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let proxy = info.sender; - let owner = deps.api.addr_validate(&owner)?; - _try_withdraw_operator_reward(deps, owner, Some(proxy)) -} - -pub(crate) fn _try_withdraw_operator_reward( - deps: DepsMut<'_>, - owner: Addr, - proxy: Option, -) -> Result { - // we need to grab all of the node's details so we'd known original pledge alongside + // we need to grab all of the node's details, so we'd known original pledge alongside // all the earned rewards (and obviously to know if this node even exists and is still // in the bonded state) - let mix_details = get_mixnode_details_by_owner(deps.storage, owner.clone())?.ok_or( + let mix_details = get_mixnode_details_by_owner(deps.storage, info.sender.clone())?.ok_or( MixnetContractError::NoAssociatedMixNodeBond { - owner: owner.clone(), + owner: info.sender.clone(), }, )?; let mix_id = mix_details.mix_id(); - ensure_proxy_match(&proxy, &mix_details.bond_information.proxy)?; ensure_bonded(&mix_details.bond_information)?; let reward = helpers::withdraw_operator_reward(deps.storage, mix_details)?; @@ -178,26 +161,13 @@ pub(crate) fn _try_withdraw_operator_reward( // if the reward is zero, don't track or send anything - there's no point if !reward.amount.is_zero() { - let return_tokens = send_to_proxy_or_owner(&proxy, &owner, vec![reward.clone()]); - response = response.add_message(return_tokens); - - if let Some(proxy) = &proxy { - // we can only attempt to send the message to the vesting contract if the proxy IS the vesting contract - // otherwise, we don't care - let vesting_contract = mixnet_params_storage::vesting_contract_address(deps.storage)?; - if proxy == vesting_contract { - let msg = VestingContractExecuteMsg::TrackReward { - amount: reward.clone(), - address: owner.clone().into_string(), - }; - let track_reward_message = wasm_execute(proxy, &msg, vec![])?; - response = response.add_message(track_reward_message); - } - } + response = response.send_tokens(&info.sender, reward.clone()) } Ok(response.add_event(new_withdraw_operator_reward_event( - &owner, &proxy, reward, mix_id, + &info.sender, + reward, + mix_id, ))) } @@ -205,37 +175,15 @@ pub(crate) fn try_withdraw_delegator_reward( deps: DepsMut<'_>, info: MessageInfo, mix_id: MixId, -) -> Result { - _try_withdraw_delegator_reward(deps, mix_id, info.sender, None) -} - -pub(crate) fn try_withdraw_delegator_reward_on_behalf( - deps: DepsMut<'_>, - info: MessageInfo, - mix_id: MixId, - owner: String, -) -> Result { - ensure_sent_by_vesting_contract(&info, deps.storage)?; - - let proxy = info.sender; - let owner = deps.api.addr_validate(&owner)?; - _try_withdraw_delegator_reward(deps, mix_id, owner, Some(proxy)) -} - -pub(crate) fn _try_withdraw_delegator_reward( - deps: DepsMut<'_>, - mix_id: MixId, - owner: Addr, - proxy: Option, ) -> Result { // see if the delegation even exists - let storage_key = Delegation::generate_storage_key(mix_id, &owner, proxy.as_ref()); + let storage_key = Delegation::generate_storage_key(mix_id, &info.sender, None); let delegation = match delegations_storage::delegations().may_load(deps.storage, storage_key)? { None => { return Err(MixnetContractError::NoMixnodeDelegationFound { mix_id, - address: owner.into_string(), - proxy: proxy.map(Addr::into_string), + address: info.sender.into_string(), + proxy: None, }); } Some(delegation) => delegation, @@ -257,33 +205,18 @@ pub(crate) fn _try_withdraw_delegator_reward( _ => (), }; - ensure_proxy_match(&proxy, &delegation.proxy)?; - let reward = helpers::withdraw_delegator_reward(deps.storage, delegation, mix_rewarding)?; let mut response = Response::new(); // if the reward is zero, don't track or send anything - there's no point if !reward.amount.is_zero() { - let return_tokens = send_to_proxy_or_owner(&proxy, &owner, vec![reward.clone()]); - response = response.add_message(return_tokens); - - if let Some(proxy) = &proxy { - // we can only attempt to send the message to the vesting contract if the proxy IS the vesting contract - // otherwise, we don't care - let vesting_contract = mixnet_params_storage::vesting_contract_address(deps.storage)?; - if proxy == vesting_contract { - let msg = VestingContractExecuteMsg::TrackReward { - amount: reward.clone(), - address: owner.clone().into_string(), - }; - let track_reward_message = wasm_execute(proxy, &msg, vec![])?; - response = response.add_message(track_reward_message); - } - } + response = response.send_tokens(&info.sender, reward.clone()) } Ok(response.add_event(new_withdraw_delegator_reward_event( - &owner, &proxy, reward, mix_id, + &info.sender, + reward, + mix_id, ))) } @@ -1405,13 +1338,10 @@ pub mod tests { #[cfg(test)] mod withdrawing_delegator_reward { - use cosmwasm_std::{coin, BankMsg, CosmosMsg, Decimal, Uint128}; - - use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; - use crate::interval::pending_events; - use crate::support::tests::fixtures::TEST_COIN_DENOM; use crate::support::tests::test_helpers::{assert_eq_with_leeway, TestSetup}; + use cosmwasm_std::{BankMsg, CosmosMsg, Decimal, Uint128}; + use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; use super::*; @@ -1742,60 +1672,14 @@ pub mod tests { let accumulated_actual = truncate_reward_amount(accumulated_quad); assert_eq_with_leeway(total_claimed, accumulated_actual, Uint128::new(6)); } - - #[test] - fn fails_for_illegal_proxy() { - let test = TestSetup::new(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let mut test = TestSetup::new(); - let mix_id = - test.add_dummy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); - - let delegator = "delegator"; - - test.add_immediate_delegation_with_illegal_proxy( - delegator, - 100_000_000u128, - mix_id, - illegal_proxy.clone(), - ); - - // reward the node - test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); - test.start_epoch_transition(); - test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); - - let res = try_withdraw_delegator_reward_on_behalf( - test.deps_mut(), - mock_info(illegal_proxy.as_ref(), &[coin(123, TEST_COIN_DENOM)]), - mix_id, - delegator.to_string(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract, - } - ) - } } #[cfg(test)] mod withdrawing_operator_reward { - use cosmwasm_std::{coin, BankMsg, CosmosMsg, Uint128}; - + use super::*; use crate::interval::pending_events; - use crate::support::tests::fixtures::TEST_COIN_DENOM; use crate::support::tests::test_helpers::TestSetup; - - use super::*; + use cosmwasm_std::{Addr, BankMsg, CosmosMsg, Uint128}; #[test] fn can_only_be_done_if_bond_exists() { @@ -1908,42 +1792,6 @@ pub mod tests { }) ); } - - #[test] - fn fails_for_illegal_proxy() { - let mut test = TestSetup::new(); - - let illegal_proxy = Addr::unchecked("not-vesting-contract"); - let vesting_contract = test.vesting_contract(); - - let owner = "mix-owner1"; - let mix_id = test.add_dummy_mixnode_with_illegal_proxy( - owner, - Some(Uint128::new(1_000_000_000_000)), - illegal_proxy.clone(), - ); - - // reward the node - test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); - test.start_epoch_transition(); - test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); - - let res = try_withdraw_operator_reward_on_behalf( - test.deps_mut(), - mock_info(illegal_proxy.as_ref(), &[coin(123, TEST_COIN_DENOM)]), - owner.to_string(), - ) - .unwrap_err(); - - assert_eq!( - res, - MixnetContractError::SenderIsNotVestingContract { - received: illegal_proxy, - vesting_contract, - } - ) - } } #[cfg(test)] diff --git a/contracts/mixnet/src/support/helpers.rs b/contracts/mixnet/src/support/helpers.rs index 12c02a675a0..09738cea81e 100644 --- a/contracts/mixnet/src/support/helpers.rs +++ b/contracts/mixnet/src/support/helpers.rs @@ -4,11 +4,11 @@ use crate::gateways::storage as gateways_storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::mixnodes::storage as mixnodes_storage; -use cosmwasm_std::{wasm_execute, Addr, BankMsg, Coin, CosmosMsg, MessageInfo, Response, Storage}; +use cosmwasm_std::{Addr, BankMsg, Coin, CosmosMsg, Response, Storage}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::mixnode::PendingMixNodeChanges; -use mixnet_contract_common::{EpochState, EpochStatus, IdentityKeyRef, MixId, MixNodeBond}; -use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; +use mixnet_contract_common::{EpochState, EpochStatus, IdentityKeyRef, MixNodeBond}; +use nym_contracts_common::Percent; // helper trait to attach `Msg` to a response if it's provided #[allow(dead_code)] @@ -26,131 +26,16 @@ impl AttachOptionalMessage for Response { } } -// another helper trait to remove some duplicate code and consolidate comments regarding -// possible epoch progression halting behaviour -pub(crate) trait VestingTracking -where - Self: Sized, -{ - fn maybe_add_track_vesting_undelegation_message( - self, - storage: &dyn Storage, - proxy: Option, - owner: String, - mix_id: MixId, - amount: Coin, - ) -> Result; - - fn maybe_add_track_vesting_unbond_mixnode_message( - self, - storage: &dyn Storage, - proxy: Option, - owner: String, - amount: Coin, - ) -> Result; - - fn maybe_add_track_vesting_decrease_mixnode_pledge( - self, - storage: &dyn Storage, - proxy: Option, - owner: String, - amount: Coin, - ) -> Result; +pub(crate) trait AttachSendTokens { + fn send_tokens(self, to: impl AsRef, amount: Coin) -> Self; } -impl VestingTracking for Response { - fn maybe_add_track_vesting_undelegation_message( - self, - storage: &dyn Storage, - proxy: Option, - owner: String, - mix_id: MixId, - amount: Coin, - ) -> Result { - // if there's a proxy set (i.e. the vesting contract), send the track message - if let Some(proxy) = proxy { - let vesting_contract = mixnet_params_storage::vesting_contract_address(storage)?; - - // Note: this can INTENTIONALLY cause epoch progression halt if the proxy is not the vesting contract - // But this is fine, since this situation should have NEVER occurred in the first place - // (as all 'on_behalf' methods, including 'DelegateToMixnodeOnBehalf' that got us here, - // explicitly require the proxy to be the vesting contract) - // 'fixing' it would require manually inspecting the problematic event, investigating - // it's cause and manually (presumably via migration) clearing it. - if proxy != vesting_contract { - return Err(MixnetContractError::ProxyIsNotVestingContract { - received: proxy, - vesting_contract, - }); - } - - let msg = VestingContractExecuteMsg::TrackUndelegation { - owner, - mix_id, - amount, - }; - - let track_undelegate_message = wasm_execute(proxy, &msg, vec![])?; - Ok(self.add_message(track_undelegate_message)) - } else { - // there's no proxy so nothing to do - Ok(self) - } - } - - fn maybe_add_track_vesting_unbond_mixnode_message( - self, - storage: &dyn Storage, - proxy: Option, - owner: String, - amount: Coin, - ) -> Result { - // if there's a proxy set (i.e. the vesting contract), send the track message - if let Some(proxy) = proxy { - let vesting_contract = mixnet_params_storage::vesting_contract_address(storage)?; - - // exactly the same possible halting behaviour as in `maybe_add_track_vesting_undelegation_message`. - if proxy != vesting_contract { - return Err(MixnetContractError::ProxyIsNotVestingContract { - received: proxy, - vesting_contract, - }); - } - - let msg = VestingContractExecuteMsg::TrackUnbondMixnode { owner, amount }; - let track_unbond_message = wasm_execute(proxy, &msg, vec![])?; - Ok(self.add_message(track_unbond_message)) - } else { - // there's no proxy so nothing to do - Ok(self) - } - } - - fn maybe_add_track_vesting_decrease_mixnode_pledge( - self, - storage: &dyn Storage, - proxy: Option, - owner: String, - amount: Coin, - ) -> Result { - if let Some(proxy) = proxy { - let vesting_contract = mixnet_params_storage::vesting_contract_address(storage)?; - - // exactly the same possible halting behaviour as in `maybe_add_track_vesting_undelegation_message`. - if proxy != vesting_contract { - return Err(MixnetContractError::ProxyIsNotVestingContract { - received: proxy, - vesting_contract, - }); - } - - let msg = VestingContractExecuteMsg::TrackDecreasePledge { owner, amount }; - let track_decrease_pledge_message = wasm_execute(proxy, &msg, vec![])?; - Ok(self.add_message(track_decrease_pledge_message)) - } else { - // there's no proxy so nothing to do - Ok(self) - } +impl AttachSendTokens for Response { + fn send_tokens(self, to: impl AsRef, amount: Coin) -> Self { + self.add_message(BankMsg::Send { + to_address: to.as_ref().to_string(), + amount: vec![amount], + }) } } @@ -158,20 +43,6 @@ impl VestingTracking for Response { // api.debug(&*format!("\n\n\n=========================================\n{}\n=========================================\n\n\n", msg.into())); // } -/// Attempts to construct a `BankMsg` to send specified tokens to the provided -/// proxy address. If that's unavailable, the `BankMsg` will use the "owner" as the -/// "to_address". -pub(crate) fn send_to_proxy_or_owner( - proxy: &Option, - owner: &Addr, - amount: Vec, -) -> BankMsg { - BankMsg::Send { - to_address: proxy.as_ref().unwrap_or(owner).to_string(), - amount, - } -} - pub(crate) fn validate_pledge( mut pledge: Vec, minimum_pledge: Coin, @@ -337,39 +208,6 @@ pub(crate) fn ensure_is_owner( Ok(()) } -pub(crate) fn ensure_proxy_match( - actual: &Option, - expected: &Option, -) -> Result<(), MixnetContractError> { - if actual != expected { - return Err(MixnetContractError::ProxyMismatch { - existing: expected - .as_ref() - .map_or_else(|| "None".to_string(), |a| a.as_str().to_string()), - incoming: actual - .as_ref() - .map_or_else(|| "None".to_string(), |a| a.as_str().to_string()), - }); - } - Ok(()) -} - -pub(crate) fn ensure_sent_by_vesting_contract( - info: &MessageInfo, - storage: &dyn Storage, -) -> Result<(), MixnetContractError> { - let vesting_contract_address = - crate::mixnet_contract_settings::storage::vesting_contract_address(storage)?; - if info.sender != vesting_contract_address { - Err(MixnetContractError::SenderIsNotVestingContract { - received: info.sender.clone(), - vesting_contract: vesting_contract_address, - }) - } else { - Ok(()) - } -} - pub(crate) fn ensure_bonded(bond: &MixNodeBond) -> Result<(), MixnetContractError> { if bond.is_unbonding { return Err(MixnetContractError::MixnodeIsUnbonding { @@ -431,3 +269,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/messages.rs b/contracts/mixnet/src/support/tests/messages.rs index a1bf5ab37ec..61a0815d917 100644 --- a/contracts/mixnet/src/support/tests/messages.rs +++ b/contracts/mixnet/src/support/tests/messages.rs @@ -22,7 +22,7 @@ pub(crate) fn valid_bond_gateway_msg( ..tests::fixtures::gateway_fixture() }; - let msg = gateway_bonding_sign_payload(deps, sender, None, gateway.clone(), stake); + let msg = gateway_bonding_sign_payload(deps, sender, gateway.clone(), stake); let owner_signature = ed25519_sign_message(msg, keypair.private_key()); let identity_key = keypair.public_key().to_base58_string(); diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index 2bd98eb1753..976138fdc27 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -14,8 +14,7 @@ pub mod test_helpers { use crate::delegations::storage as delegations_storage; use crate::delegations::transactions::try_delegate_to_mixnode; use crate::families::transactions::{try_create_family, try_join_family}; - use crate::gateways::storage as gateways_storage; - use crate::gateways::transactions::{try_add_gateway, try_add_gateway_on_behalf}; + use crate::gateways::transactions::try_add_gateway; use crate::interval::transactions::{ perform_pending_epoch_actions, perform_pending_interval_actions, try_begin_epoch_transition, }; @@ -27,9 +26,7 @@ pub mod test_helpers { }; use crate::mixnodes::storage as mixnodes_storage; use crate::mixnodes::storage::mixnode_bonds; - use crate::mixnodes::transactions::{ - try_add_mixnode, try_add_mixnode_on_behalf, try_remove_mixnode, - }; + use crate::mixnodes::transactions::{try_add_mixnode, try_remove_mixnode}; use crate::rewards::queries::{ query_pending_delegator_reward, query_pending_mixnode_operator_reward, }; @@ -45,7 +42,7 @@ pub mod test_helpers { use cosmwasm_std::testing::mock_info; use cosmwasm_std::testing::MockApi; use cosmwasm_std::testing::MockQuerier; - use cosmwasm_std::{coin, coins, Addr, Api, BankMsg, CosmosMsg, Storage}; + use cosmwasm_std::{coin, coins, Addr, BankMsg, CosmosMsg, Storage}; use cosmwasm_std::{Coin, Order}; use cosmwasm_std::{Decimal, Empty, MemoryStorage}; use cosmwasm_std::{Deps, OwnedDeps}; @@ -148,13 +145,6 @@ pub mod test_helpers { self.owner.clone() } - pub fn vesting_contract(&self) -> Addr { - mixnet_params_storage::CONTRACT_STATE - .load(self.deps().storage) - .unwrap() - .vesting_contract_address - } - pub fn coin(&self, amount: u128) -> Coin { coin(amount, rewarding_denom(self.deps().storage).unwrap()) } @@ -178,7 +168,6 @@ pub mod test_helpers { &mut self, family_owner_keys: &identity::KeyPair, member_node: IdentityKeyRef, - vesting: bool, ) -> MessageSignature { let identity = family_owner_keys.public_key().to_base58_string(); @@ -195,14 +184,7 @@ pub mod test_helpers { let nonce = signing_storage::get_signing_nonce(self.deps().storage, owner).unwrap(); - let proxy = if vesting { - Some(self.vesting_contract()) - } else { - None - }; - - let msg = - construct_family_join_permit(nonce, family_head, proxy, member_node.to_owned()); + let msg = construct_family_join_permit(nonce, family_head, member_node.to_owned()); let sig_bytes = family_owner_keys .private_key() @@ -217,13 +199,11 @@ pub mod test_helpers { member: &str, member_keys: &identity::KeyPair, head_keys: &identity::KeyPair, - vesting: bool, ) { let member_identity = member_keys.public_key().to_base58_string(); let head_identity = head_keys.public_key().to_base58_string(); - let join_permit = - self.generate_family_join_permit(head_keys, &member_identity, vesting); + let join_permit = self.generate_family_join_permit(head_keys, &member_identity); let family_head = FamilyHead::new(head_identity); try_join_family( @@ -235,12 +215,13 @@ pub mod test_helpers { .unwrap(); } + #[allow(dead_code)] pub fn create_dummy_mixnode_with_new_family( &mut self, head: &str, label: &str, ) -> (MixId, identity::KeyPair) { - let (mix_id, keys) = self.add_dummy_mixnode_with_proxy_and_keypair(head, None); + let (mix_id, keys) = self.add_dummy_mixnode_with_keypair(head, None); try_create_family(self.deps_mut(), mock_info(head, &[]), label.to_string()).unwrap(); (mix_id, keys) @@ -338,7 +319,7 @@ pub mod test_helpers { stake: Option, ) -> MessageSignature { let stake = self.make_mix_pledge(stake); - let msg = mixnode_bonding_sign_payload(self.deps(), owner, None, mixnode, stake); + let msg = mixnode_bonding_sign_payload(self.deps(), owner, mixnode, stake); ed25519_sign_message(msg, key) } @@ -359,13 +340,8 @@ pub mod test_helpers { ..tests::fixtures::mix_node_fixture() }; - let msg = mixnode_bonding_sign_payload( - self.deps(), - owner, - None, - mixnode.clone(), - stake.clone(), - ); + let msg = + mixnode_bonding_sign_payload(self.deps(), owner, mixnode.clone(), stake.clone()); let owner_signature = ed25519_sign_message(msg, keypair.private_key()); let info = mock_info(owner, &stake); @@ -389,156 +365,6 @@ pub mod test_helpers { (current_id_counter + 1, keypair) } - pub fn add_dummy_mixnode_with_proxy_and_keypair( - &mut self, - owner: &str, - stake: Option, - ) -> (MixId, identity::KeyPair) { - let stake = self.make_mix_pledge(stake); - - let proxy = self.vesting_contract(); - - let keypair = identity::KeyPair::new(&mut self.rng); - let identity_key = keypair.public_key().to_base58_string(); - let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut self.rng); - - let mixnode = MixNode { - identity_key, - sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), - ..tests::fixtures::mix_node_fixture() - }; - - let msg = mixnode_bonding_sign_payload( - self.deps(), - owner, - Some(proxy.clone()), - mixnode.clone(), - stake.clone(), - ); - let owner_signature = ed25519_sign_message(msg, keypair.private_key()); - - let info = mock_info(proxy.as_str(), &stake); - let current_id_counter = mixnodes_storage::MIXNODE_ID_COUNTER - .may_load(self.deps().storage) - .unwrap() - .unwrap_or_default(); - - let env = self.env(); - try_add_mixnode_on_behalf( - self.deps_mut(), - env, - info, - mixnode, - tests::fixtures::mix_node_cost_params_fixture(), - owner.to_string(), - owner_signature, - ) - .unwrap(); - - // newly added mixnode gets assigned the current counter + 1 - (current_id_counter + 1, keypair) - } - - pub fn add_dummy_mixnode_with_legal_proxy( - &mut self, - owner: &str, - stake: Option, - ) -> MixId { - self.add_dummy_mixnode_with_proxy_and_keypair(owner, stake) - .0 - } - - pub fn set_illegal_mixnode_proxy(&mut self, mix_id: MixId, proxy: Addr) { - let mut bond_details = mixnodes_storage::mixnode_bonds() - .load(self.deps().storage, mix_id) - .unwrap(); - bond_details.proxy = Some(proxy); - mixnodes_storage::mixnode_bonds() - .save(self.deps_mut().storage, mix_id, &bond_details) - .unwrap(); - } - - pub fn add_dummy_gateway_with_illegal_proxy( - &mut self, - owner: &str, - stake: Option, - proxy: Addr, - ) -> IdentityKey { - let gateway_identity = self.add_dummy_gateway_with_legal_proxy(owner, stake); - self.set_illegal_gateway_proxy(&gateway_identity, proxy); - gateway_identity - } - - pub fn set_illegal_gateway_proxy(&mut self, gateway_id: &str, proxy: Addr) { - let mut gateway = gateways_storage::gateways() - .load(self.deps().storage, gateway_id) - .unwrap(); - gateway.proxy = Some(proxy); - gateways_storage::gateways() - .save(self.deps_mut().storage, gateway_id, &gateway) - .unwrap(); - } - - pub fn add_dummy_gateway_with_legal_proxy( - &mut self, - owner: &str, - stake: Option, - ) -> IdentityKey { - let stake = match stake { - Some(amount) => { - let denom = rewarding_denom(self.deps().storage).unwrap(); - Coin { denom, amount } - } - None => minimum_mixnode_pledge(self.deps.as_ref().storage).unwrap(), - }; - - let keypair = identity::KeyPair::new(&mut self.rng); - let identity_key = keypair.public_key().to_base58_string(); - let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut self.rng); - - let proxy = self.vesting_contract(); - - let gateway = Gateway { - identity_key, - sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), - ..tests::fixtures::gateway_fixture() - }; - - let msg = gateway_bonding_sign_payload( - self.deps(), - owner, - Some(proxy.clone()), - gateway.clone(), - vec![stake.clone()], - ); - let owner_signature = ed25519_sign_message(msg, keypair.private_key()); - - let env = self.env(); - let info = mock_info(proxy.as_ref(), &[stake]); - - try_add_gateway_on_behalf( - self.deps_mut(), - env, - info, - gateway, - owner.to_string(), - owner_signature, - ) - .unwrap(); - keypair.public_key().to_base58_string() - } - - pub fn add_dummy_mixnode_with_illegal_proxy( - &mut self, - owner: &str, - stake: Option, - proxy: Addr, - ) -> MixId { - let mix_id = self.add_dummy_mixnode_with_legal_proxy(owner, stake); - self.set_illegal_mixnode_proxy(mix_id, proxy); - mix_id - } - pub fn mixnode_with_signature( &mut self, sender: &str, @@ -555,8 +381,7 @@ pub mod test_helpers { sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), ..tests::fixtures::mix_node_fixture() }; - let msg = - mixnode_bonding_sign_payload(self.deps(), sender, None, mixnode.clone(), stake); + let msg = mixnode_bonding_sign_payload(self.deps(), sender, mixnode.clone(), stake); let owner_signature = ed25519_sign_message(msg, keypair.private_key()); (mixnode, owner_signature, keypair) @@ -579,8 +404,7 @@ pub mod test_helpers { ..tests::fixtures::gateway_fixture() }; - let msg = - gateway_bonding_sign_payload(self.deps(), sender, None, gateway.clone(), stake); + let msg = gateway_bonding_sign_payload(self.deps(), sender, gateway.clone(), stake); let owner_signature = ed25519_sign_message(msg, keypair.private_key()); (gateway, owner_signature) @@ -625,87 +449,10 @@ pub mod test_helpers { Addr::unchecked(delegator), target, amount, - None, ) .unwrap(); } - pub fn add_immediate_delegation_with_legal_proxy( - &mut self, - delegator: &str, - amount: impl Into, - target: MixId, - ) { - let denom = rewarding_denom(self.deps().storage).unwrap(); - let amount = Coin { - denom, - amount: amount.into(), - }; - let env = self.env(); - let proxy = self.vesting_contract(); - pending_events::delegate( - self.deps_mut(), - &env, - env.block.height, - Addr::unchecked(delegator), - target, - amount, - Some(proxy), - ) - .unwrap(); - } - - // to set illegal proxy we have to bypass "normal" flow and put the value - // directly into the storage - pub fn add_immediate_delegation_with_illegal_proxy( - &mut self, - delegator: &str, - amount: impl Into, - target: MixId, - proxy: Addr, - ) { - let denom = rewarding_denom(self.deps().storage).unwrap(); - let amount = Coin { - denom, - amount: amount.into(), - }; - - let owner = self.deps.api.addr_validate(delegator).unwrap(); - let storage_key = Delegation::generate_storage_key(target, &owner, Some(&proxy)); - - let mut mix_rewarding = self.mix_rewarding(target); - - let mut stored_delegation_amount = amount; - - if let Some(existing_delegation) = delegations_storage::delegations() - .may_load(&self.deps.storage, storage_key.clone()) - .unwrap() - { - let og_with_reward = mix_rewarding.undelegate(&existing_delegation).unwrap(); - stored_delegation_amount.amount += og_with_reward.amount; - } - - mix_rewarding - .add_base_delegation(stored_delegation_amount.amount) - .unwrap(); - - let delegation = Delegation::new( - owner, - target, - mix_rewarding.total_unit_reward, - stored_delegation_amount, - self.env.block.height, - Some(proxy), - ); - - delegations_storage::delegations() - .save(&mut self.deps.storage, storage_key, &delegation) - .unwrap(); - rewards_storage::MIXNODE_REWARDING - .save(&mut self.deps.storage, target, &mix_rewarding) - .unwrap(); - } - #[allow(unused)] pub fn add_delegation( &mut self, @@ -724,14 +471,8 @@ pub mod test_helpers { pub fn remove_immediate_delegation(&mut self, delegator: &str, target: MixId) { let height = self.env.block.height; - pending_events::undelegate( - self.deps_mut(), - height, - Addr::unchecked(delegator), - target, - None, - ) - .unwrap(); + pending_events::undelegate(self.deps_mut(), height, Addr::unchecked(delegator), target) + .unwrap(); } pub fn start_epoch_transition(&mut self) { @@ -1109,7 +850,6 @@ pub mod test_helpers { Addr::unchecked(format!("owner{}", i)), mix_id, tests::fixtures::good_mixnode_pledge().pop().unwrap(), - None, ) .unwrap(); } @@ -1205,7 +945,6 @@ pub mod test_helpers { pub fn mixnode_bonding_sign_payload( deps: Deps<'_>, owner: &str, - proxy: Option, mixnode: MixNode, stake: Vec, ) -> SignableMixNodeBondingMsg { @@ -1214,14 +953,13 @@ pub mod test_helpers { signing_storage::get_signing_nonce(deps.storage, Addr::unchecked(owner)).unwrap(); let payload = MixnodeBondingPayload::new(mixnode, cost_params); - let content = ContractMessageContent::new(Addr::unchecked(owner), proxy, stake, payload); + let content = ContractMessageContent::new(Addr::unchecked(owner), stake, payload); SignableMixNodeBondingMsg::new(nonce, content) } pub fn gateway_bonding_sign_payload( deps: Deps<'_>, owner: &str, - proxy: Option, gateway: Gateway, stake: Vec, ) -> SignableGatewayBondingMsg { @@ -1229,7 +967,7 @@ pub mod test_helpers { signing_storage::get_signing_nonce(deps.storage, Addr::unchecked(owner)).unwrap(); let payload = GatewayBondingPayload::new(gateway); - let content = ContractMessageContent::new(Addr::unchecked(owner), proxy, stake, payload); + let content = ContractMessageContent::new(Addr::unchecked(owner), stake, payload); SignableGatewayBondingMsg::new(nonce, content) } @@ -1258,6 +996,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/contracts/mixnet/src/vesting_migration.rs b/contracts/mixnet/src/vesting_migration.rs new file mode 100644 index 00000000000..3faf826ec05 --- /dev/null +++ b/contracts/mixnet/src/vesting_migration.rs @@ -0,0 +1,95 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::delegations::storage as delegations_storage; +use crate::mixnet_contract_settings::storage as mixnet_params_storage; +use crate::mixnodes::helpers::get_mixnode_details_by_owner; +use crate::mixnodes::storage as mixnodes_storage; +use crate::support::helpers::{ + ensure_bonded, ensure_epoch_in_progress_state, ensure_no_pending_pledge_changes, +}; +use cosmwasm_std::{wasm_execute, DepsMut, MessageInfo, Response}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::{Delegation, MixId}; +use vesting_contract_common::messages::ExecuteMsg as VestingExecuteMsg; + +pub(crate) fn try_migrate_vested_mixnode( + deps: DepsMut<'_>, + info: MessageInfo, +) -> Result { + let mix_details = get_mixnode_details_by_owner(deps.storage, info.sender.clone())?.ok_or( + MixnetContractError::NoAssociatedMixNodeBond { + owner: info.sender.clone(), + }, + )?; + let mix_id = mix_details.mix_id(); + + ensure_epoch_in_progress_state(deps.storage)?; + ensure_no_pending_pledge_changes(&mix_details.pending_changes)?; + ensure_bonded(&mix_details.bond_information)?; + + let Some(proxy) = &mix_details.bond_information.proxy else { + return Err(MixnetContractError::NotAVestingMixnode); + }; + + let vesting_contract = mixnet_params_storage::vesting_contract_address(deps.storage)?; + if proxy != vesting_contract { + return Err(MixnetContractError::ProxyIsNotVestingContract { + received: proxy.clone(), + vesting_contract, + }); + } + + let mut updated_bond = mix_details.bond_information.clone(); + updated_bond.proxy = None; + mixnodes_storage::mixnode_bonds().replace( + deps.storage, + mix_id, + Some(&updated_bond), + Some(&mix_details.bond_information), + )?; + + Ok(Response::new().add_message(wasm_execute( + vesting_contract, + &VestingExecuteMsg::TrackMigratedMixnode { + owner: info.sender.into_string(), + }, + vec![], + )?)) +} + +pub(crate) fn try_migrate_vested_delegation( + deps: DepsMut<'_>, + info: MessageInfo, + mix_id: MixId, +) -> Result { + ensure_epoch_in_progress_state(deps.storage)?; + + let vesting_contract = mixnet_params_storage::vesting_contract_address(deps.storage)?; + + let storage_key = + Delegation::generate_storage_key(mix_id, &info.sender, Some(&vesting_contract)); + let Some(mut delegation) = + delegations_storage::delegations().may_load(deps.storage, storage_key.clone())? + else { + return Err(MixnetContractError::NotAVestingDelegation); + }; + + // sanity check that's meant to blow up the contract + assert_eq!(delegation.proxy, Some(vesting_contract.clone())); + + // update the delegation and save it under the correct storage key + delegation.proxy = None; + let updated_storage_key = Delegation::generate_storage_key(mix_id, &info.sender, None); + delegations_storage::delegations().remove(deps.storage, storage_key)?; + delegations_storage::delegations().save(deps.storage, updated_storage_key, &delegation)?; + + Ok(Response::new().add_message(wasm_execute( + vesting_contract, + &VestingExecuteMsg::TrackMigratedDelegation { + owner: info.sender.into_string(), + mix_id, + }, + vec![], + )?)) +} diff --git a/contracts/vesting/schema/nym-vesting-contract.json b/contracts/vesting/schema/nym-vesting-contract.json index 7d016b8e68f..1a7b2927844 100644 --- a/contracts/vesting/schema/nym-vesting-contract.json +++ b/contracts/vesting/schema/nym-vesting-contract.json @@ -691,6 +691,54 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "track_migrated_mixnode" + ], + "properties": { + "track_migrated_mixnode": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "track_migrated_delegation" + ], + "properties": { + "track_migrated_delegation": { + "type": "object", + "required": [ + "mix_id", + "owner" + ], + "properties": { + "mix_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": { diff --git a/contracts/vesting/schema/raw/execute.json b/contracts/vesting/schema/raw/execute.json index be6d6a73ff0..9723294d67c 100644 --- a/contracts/vesting/schema/raw/execute.json +++ b/contracts/vesting/schema/raw/execute.json @@ -669,6 +669,54 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "track_migrated_mixnode" + ], + "properties": { + "track_migrated_mixnode": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "track_migrated_delegation" + ], + "properties": { + "track_migrated_delegation": { + "type": "object", + "required": [ + "mix_id", + "owner" + ], + "properties": { + "mix_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": { diff --git a/contracts/vesting/src/contract.rs b/contracts/vesting/src/contract.rs index a9689e33907..a9ef9401a68 100644 --- a/contracts/vesting/src/contract.rs +++ b/contracts/vesting/src/contract.rs @@ -70,55 +70,12 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { - ExecuteMsg::CreateFamily { label } => try_create_family(info, deps, label), - ExecuteMsg::JoinFamily { - join_permit, - family_head, - } => try_join_family(info, deps, join_permit, family_head), - ExecuteMsg::LeaveFamily { family_head } => try_leave_family(info, deps, family_head), - ExecuteMsg::KickFamilyMember { member } => try_kick_family_member(info, deps, member), - ExecuteMsg::UpdateLockedPledgeCap { address, cap } => { - try_update_locked_pledge_cap(address, cap, info, deps) - } ExecuteMsg::TrackReward { amount, address } => { try_track_reward(deps, info, amount, &address) } - ExecuteMsg::ClaimOperatorReward {} => try_claim_operator_reward(deps, info), - ExecuteMsg::ClaimDelegatorReward { mix_id } => { - try_claim_delegator_reward(deps, info, mix_id) - } - ExecuteMsg::UpdateMixnodeConfig { new_config } => { - try_update_mixnode_config(new_config, info, deps) - } - ExecuteMsg::UpdateMixnodeCostParams { new_costs } => { - try_update_mixnode_cost_params(new_costs, info, deps) - } ExecuteMsg::UpdateMixnetAddress { address } => { try_update_mixnet_address(address, info, deps) } - ExecuteMsg::DelegateToMixnode { - mix_id, - amount, - on_behalf_of, - } => try_delegate_to_mixnode(mix_id, amount, on_behalf_of, info, env, deps), - ExecuteMsg::UndelegateFromMixnode { - mix_id, - on_behalf_of, - } => try_undelegate_from_mixnode(mix_id, on_behalf_of, info, deps), - ExecuteMsg::CreateAccount { - owner_address, - staking_address, - vesting_spec, - cap, - } => try_create_periodic_vesting_account( - &owner_address, - staking_address, - vesting_spec, - cap, - info, - env, - deps, - ), ExecuteMsg::WithdrawVestedCoins { amount } => { try_withdraw_vested_coins(amount, env, info, deps) } @@ -127,47 +84,22 @@ pub fn execute( mix_id, amount, } => try_track_undelegation(&owner, mix_id, amount, info, deps), - ExecuteMsg::BondMixnode { - mix_node, - cost_params, - owner_signature, - amount, - } => try_bond_mixnode( - mix_node, - cost_params, - owner_signature, - amount, - info, - env, - deps, - ), - ExecuteMsg::PledgeMore { amount } => try_pledge_more(deps, env, info, amount), - ExecuteMsg::DecreasePledge { amount } => try_decrease_pledge(deps, info, amount), - ExecuteMsg::UnbondMixnode {} => try_unbond_mixnode(info, deps), ExecuteMsg::TrackUnbondMixnode { owner, amount } => { try_track_unbond_mixnode(&owner, amount, info, deps) } ExecuteMsg::TrackDecreasePledge { owner, amount } => { try_track_decrease_mixnode_pledge(&owner, amount, info, deps) } - ExecuteMsg::BondGateway { - gateway, - owner_signature, - amount, - } => try_bond_gateway(gateway, owner_signature, amount, info, env, deps), - ExecuteMsg::UnbondGateway {} => try_unbond_gateway(info, deps), ExecuteMsg::TrackUnbondGateway { owner, amount } => { try_track_unbond_gateway(&owner, amount, info, deps) } - ExecuteMsg::UpdateGatewayConfig { new_config } => { - try_update_gateway_config(new_config, info, deps) - } - ExecuteMsg::TransferOwnership { to_address } => { - try_transfer_ownership(to_address, info, deps) - } - ExecuteMsg::UpdateStakingAddress { to_address } => { - try_update_staking_address(to_address, info, deps) + ExecuteMsg::TrackMigratedMixnode { owner } => try_track_migrate_mixnode(&owner, info, deps), + ExecuteMsg::TrackMigratedDelegation { owner, mix_id } => { + try_track_migrate_delegation(&owner, mix_id, info, deps) } + _ => Err(VestingContractError::Other { + message: "the contract has been disabled".to_string(), + }), } } diff --git a/contracts/vesting/src/traits/bonding_account.rs b/contracts/vesting/src/traits/bonding_account.rs index e5e415e30c1..8e4c7c6a620 100644 --- a/contracts/vesting/src/traits/bonding_account.rs +++ b/contracts/vesting/src/traits/bonding_account.rs @@ -61,6 +61,10 @@ pub trait MixnodeBondingAccount { new_costs: MixNodeCostParams, storage: &mut dyn Storage, ) -> Result; + fn try_track_migrated_mixnode( + &self, + storage: &mut dyn Storage, + ) -> Result<(), VestingContractError>; } pub trait GatewayBondingAccount { diff --git a/contracts/vesting/src/traits/delegating_account.rs b/contracts/vesting/src/traits/delegating_account.rs index 629cfee0509..2e7b5a65d38 100644 --- a/contracts/vesting/src/traits/delegating_account.rs +++ b/contracts/vesting/src/traits/delegating_account.rs @@ -44,4 +44,9 @@ pub trait DelegatingAccount { amount: Coin, storage: &mut dyn Storage, ) -> Result<(), VestingContractError>; + fn track_migrated_delegation( + &self, + mix_id: MixId, + storage: &mut dyn Storage, + ) -> Result<(), VestingContractError>; } diff --git a/contracts/vesting/src/transactions.rs b/contracts/vesting/src/transactions.rs index adbfe394363..a51f084862e 100644 --- a/contracts/vesting/src/transactions.rs +++ b/contracts/vesting/src/transactions.rs @@ -18,8 +18,9 @@ use mixnet_contract_common::{ use vesting_contract_common::events::{ new_ownership_transfer_event, new_periodic_vesting_account_event, new_staking_address_update_event, new_track_gateway_unbond_event, - new_track_mixnode_pledge_decrease_event, new_track_mixnode_unbond_event, - new_track_reward_event, new_track_undelegation_event, new_vested_coins_withdraw_event, + new_track_migrate_mixnode_event, new_track_mixnode_pledge_decrease_event, + new_track_mixnode_unbond_event, new_track_reward_event, new_track_undelegation_event, + new_vested_coins_withdraw_event, }; use vesting_contract_common::{Account, PledgeCap, VestingContractError, VestingSpecification}; @@ -255,6 +256,35 @@ pub fn try_track_unbond_gateway( Ok(Response::new().add_event(new_track_gateway_unbond_event())) } +/// Track vesting mixnode being converted into the usage of liquid tokens. invoked by the mixnet contract after successful migration. +pub fn try_track_migrate_mixnode( + owner: &str, + info: MessageInfo, + deps: DepsMut<'_>, +) -> Result { + if info.sender != MIXNET_CONTRACT_ADDRESS.load(deps.storage)? { + return Err(VestingContractError::NotMixnetContract(info.sender)); + } + let account = account_from_address(owner, deps.storage, deps.api)?; + account.try_track_migrated_mixnode(deps.storage)?; + Ok(Response::new().add_event(new_track_migrate_mixnode_event())) +} + +/// Track vesting delegation being converted into the usage of liquid tokens. invoked by the mixnet contract after successful migration. +pub fn try_track_migrate_delegation( + owner: &str, + mix_id: MixId, + info: MessageInfo, + deps: DepsMut<'_>, +) -> Result { + if info.sender != MIXNET_CONTRACT_ADDRESS.load(deps.storage)? { + return Err(VestingContractError::NotMixnetContract(info.sender)); + } + let account = account_from_address(owner, deps.storage, deps.api)?; + account.track_migrated_delegation(mix_id, deps.storage)?; + Ok(Response::new().add_event(new_track_migrate_mixnode_event())) +} + /// Bond a mixnode, sends [mixnet_contract_common::ExecuteMsg::BondMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. pub fn try_bond_mixnode( mix_node: MixNode, diff --git a/contracts/vesting/src/vesting/account/delegating_account.rs b/contracts/vesting/src/vesting/account/delegating_account.rs index 0371bc7cc34..d01bea67891 100644 --- a/contracts/vesting/src/vesting/account/delegating_account.rs +++ b/contracts/vesting/src/vesting/account/delegating_account.rs @@ -125,4 +125,25 @@ impl DelegatingAccount for Account { self.save_balance(new_balance, storage)?; Ok(()) } + + fn track_migrated_delegation( + &self, + mix_id: MixId, + storage: &mut dyn Storage, + ) -> Result<(), VestingContractError> { + let delegation = self.total_delegations_for_mix(mix_id, storage)?; + if delegation.is_zero() { + return Err(VestingContractError::NoSuchDelegation( + self.owner_address.clone(), + mix_id, + )); + } + + // treat the tokens that were used for delegation as 'withdrawn' + let current_withdrawn = self.load_withdrawn(storage)?; + self.save_withdrawn(current_withdrawn + delegation, storage)?; + + // remove the delegation data since it no longer belongs to the vesting contract + self.remove_delegations_for_mix(mix_id, storage) + } } diff --git a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs index 29ee780ff98..ee14207d3e0 100644 --- a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs +++ b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs @@ -221,4 +221,24 @@ impl MixnodeBondingAccount for Account { .add_message(update_mixnode_costs_msg) .add_event(new_vesting_update_mixnode_cost_params_event())) } + + fn try_track_migrated_mixnode( + &self, + storage: &mut dyn Storage, + ) -> Result<(), VestingContractError> { + let Some(pledge) = self.load_mixnode_pledge(storage)? else { + return Err(VestingContractError::NoBondFound( + self.owner_address().as_str().to_string(), + )); + }; + + // treat the tokens that were used for bonding as 'withdrawn' + let current_withdrawn = self.load_withdrawn(storage)?; + self.save_withdrawn(current_withdrawn + pledge.amount.amount, storage)?; + + // don't change the balance as the tokens are left in the mixnet contract + + // remove the pledge data since it no longer belongs to the vesting account + self.remove_mixnode_pledge(storage) + } } diff --git a/contracts/vesting/src/vesting/mod.rs b/contracts/vesting/src/vesting/mod.rs index b16efbdd23f..d8da4050084 100644 --- a/contracts/vesting/src/vesting/mod.rs +++ b/contracts/vesting/src/vesting/mod.rs @@ -13,14 +13,14 @@ pub fn populate_vesting_periods( #[cfg(test)] mod tests { use crate::contract::*; - use crate::storage::*; + use crate::support::tests::helpers::vesting_account_percent_fixture; use crate::support::tests::helpers::{ init_contract, vesting_account_mid_fixture, vesting_account_new_fixture, TEST_COIN_DENOM, }; use crate::traits::DelegatingAccount; + use crate::traits::GatewayBondingAccount; use crate::traits::VestingAccount; - use crate::traits::{GatewayBondingAccount, MixnodeBondingAccount}; use crate::vesting::account::StorableVestingAccountExt; use crate::vesting::populate_vesting_periods; use contracts_common::signing::MessageSignature; @@ -36,162 +36,56 @@ mod tests { fn test_account_creation() { let mut deps = init_contract(); let env = mock_env(); - let info = mock_info("not_admin", &coins(1_000_000_000_000, TEST_COIN_DENOM)); + let msg = ExecuteMsg::CreateAccount { owner_address: "owner".to_string(), staking_address: Some("staking".to_string()), vesting_spec: None, cap: Some(PledgeCap::Absolute(Uint128::from(100_000_000_000u128))), }; - // Try creating an account when not admin - let response = execute(deps.as_mut(), env.clone(), info, msg.clone()); - assert!(response.is_err()); let info = mock_info("admin", &coins(1_000_000_000_000, TEST_COIN_DENOM)); - let _response = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()); - let created_account = load_account(Addr::unchecked("owner"), &deps.storage) - .unwrap() - .unwrap(); - - assert_eq!( - created_account.load_balance(&deps.storage).unwrap(), - // One was liquidated - Uint128::new(1_000_000_000_000) - ); - - // nothing is saved for "staking" account! - let created_account_test_by_staking = - load_account(Addr::unchecked("staking"), &deps.storage).unwrap(); - assert!(created_account_test_by_staking.is_none()); - - // but we can stake on its behalf! - let stake_msg = ExecuteMsg::DelegateToMixnode { - on_behalf_of: Some("owner".to_string()), - mix_id: 42, - amount: coin(500, TEST_COIN_DENOM), - }; - - let response = execute( - deps.as_mut(), - env.clone(), - mock_info("staking", &[]), - stake_msg, - ); - assert!(response.is_ok()); - + let response = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()); assert_eq!( - created_account.load_balance(&deps.storage).unwrap(), - // One was liquidated - Uint128::new(999_999_999_500) + response, + Err(VestingContractError::Other { + message: "the contract has been disabled".to_string() + }) ); - - // Try create the same account again - let response = execute(deps.as_mut(), env.clone(), info, msg); - assert!(response.is_err()); - - let account_again = vesting_account_new_fixture(&mut deps.storage, &env); - assert_eq!(created_account.storage_key(), 1); - assert_ne!(created_account.storage_key(), account_again.storage_key()); } #[test] fn test_ownership_transfer() { let mut deps = init_contract(); - let mut env = mock_env(); + let env = mock_env(); let info = mock_info("owner", &[]); - let account = vesting_account_new_fixture(&mut deps.storage, &env); - let staker = account.staking_address().unwrap(); let msg = ExecuteMsg::TransferOwnership { to_address: "new_owner".to_string(), }; - let _response = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()).unwrap(); - let new_owner_account = load_account(Addr::unchecked("new_owner"), &deps.storage) - .unwrap() - .unwrap(); + let response = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()); assert_eq!( - new_owner_account.load_balance(&deps.storage), - account.load_balance(&deps.storage) - ); - - // Check old account is gone - let old_owner_account = load_account(Addr::unchecked("owner"), &deps.storage).unwrap(); - assert!(old_owner_account.is_none()); - - // Not the owner - let response = execute(deps.as_mut(), env.clone(), info, msg); - assert!(response.is_err()); - - // can't stake on behalf of the original owner anymore, but we can do it for the new one! - let stake_msg = ExecuteMsg::DelegateToMixnode { - on_behalf_of: Some("owner".to_string()), - mix_id: 42, - amount: coin(500, TEST_COIN_DENOM), - }; - let response = execute( - deps.as_mut(), - env.clone(), - mock_info(staker.as_ref(), &[]), - stake_msg, - ); - assert!(response.is_err()); - - let new_stake_msg = ExecuteMsg::DelegateToMixnode { - on_behalf_of: Some("new_owner".to_string()), - mix_id: 42, - amount: coin(500, TEST_COIN_DENOM), - }; - let response = execute( - deps.as_mut(), - env.clone(), - mock_info(staker.as_ref(), &[]), - new_stake_msg, + response, + Err(VestingContractError::Other { + message: "the contract has been disabled".to_string() + }) ); - assert!(response.is_ok()); - - let info = mock_info("new_owner", &[]); - let msg = ExecuteMsg::UpdateStakingAddress { - to_address: Some("new_staking".to_string()), - }; - let _response = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); - - let msg = ExecuteMsg::WithdrawVestedCoins { - amount: Coin { - amount: Uint128::new(1), - denom: TEST_COIN_DENOM.to_string(), - }, - }; - let info = mock_info("new_owner", &[]); - env.block.time = Timestamp::from_nanos(env.block.time.nanos() + 100_000_000_000_000_000); - let response = execute(deps.as_mut(), env.clone(), info, msg.clone()); - assert!(response.is_ok()); - - let info = mock_info("owner", &[]); - let response = execute(deps.as_mut(), env.clone(), info, msg); - assert!(response.is_err()); } #[test] fn test_staking_account() { let mut deps = init_contract(); - let mut env = mock_env(); + let env = mock_env(); let info = mock_info("staking", &[]); let msg = ExecuteMsg::TransferOwnership { to_address: "new_owner".to_string(), }; let response = execute(deps.as_mut(), env.clone(), info.clone(), msg); - // Only owner can transfer - assert!(response.is_err()); - - let msg = ExecuteMsg::WithdrawVestedCoins { - amount: Coin { - amount: Uint128::new(1), - denom: "nym".to_string(), - }, - }; - env.block.time = Timestamp::from_nanos(env.block.time.nanos() + 100_000_000_000_000_000); - let response = execute(deps.as_mut(), env, info, msg); - // Only owner can withdraw - assert!(response.is_err()); + assert_eq!( + response, + Err(VestingContractError::Other { + message: "the contract has been disabled".to_string() + }) + ); } #[test] @@ -213,31 +107,12 @@ mod tests { mock_info(original_staker.as_ref(), &[]), stake_msg.clone(), ); - assert!(response.is_ok()); - - let info = mock_info("owner", &[]); - let msg = ExecuteMsg::UpdateStakingAddress { - to_address: Some("new_staking".to_string()), - }; - let _response = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); - - // the old staking account can't do any staking anymore! - let response = execute( - deps.as_mut(), - env.clone(), - mock_info(original_staker.as_ref(), &[]), - stake_msg.clone(), - ); - assert!(response.is_err()); - - // but the new one can - let response = execute( - deps.as_mut(), - env.clone(), - mock_info("new_staking", &[]), - stake_msg, + assert_eq!( + response, + Err(VestingContractError::Other { + message: "the contract has been disabled".to_string() + }) ); - assert!(response.is_ok()); } #[test] @@ -245,66 +120,27 @@ mod tests { let mut deps = init_contract(); let env = mock_env(); - let amount1 = coin(1000000000, "unym"); - let amount2 = coin(100, "unym"); + let amount = coin(1000000000, "unym"); // create the accounts - let msg1 = ExecuteMsg::CreateAccount { + let msg = ExecuteMsg::CreateAccount { owner_address: "vesting1".to_string(), staking_address: None, vesting_spec: None, cap: None, }; - let res1 = execute( - deps.as_mut(), - env.clone(), - mock_info("admin", &[amount1.clone()]), - msg1, - ); - assert!(res1.is_ok()); - - let msg2 = ExecuteMsg::CreateAccount { - owner_address: "vesting2".to_string(), - staking_address: None, - vesting_spec: None, - cap: None, - }; - let res2 = execute( - deps.as_mut(), - env.clone(), - mock_info("admin", &[amount2.clone()]), - msg2, - ); - assert!(res2.is_ok()); - - let vesting1 = try_get_vesting_coins("vesting1", None, env.clone(), deps.as_ref()).unwrap(); - assert_eq!(vesting1, amount1); - - let vesting2 = try_get_vesting_coins("vesting2", None, env.clone(), deps.as_ref()).unwrap(); - assert_eq!(vesting2, amount2); - - let staking_address_change = ExecuteMsg::UpdateStakingAddress { - to_address: Some("vesting1".to_string()), - }; - let res = execute( + let response = execute( deps.as_mut(), env.clone(), - mock_info("vesting2", &[]), - staking_address_change, + mock_info("admin", &[amount.clone()]), + msg, ); assert_eq!( - Err(VestingContractError::StakingAccountExists( - "vesting1".to_string() - )), - res + response, + Err(VestingContractError::Other { + message: "the contract has been disabled".to_string() + }) ); - - // ensure nothing has changed! - let vesting1 = try_get_vesting_coins("vesting1", None, env.clone(), deps.as_ref()).unwrap(); - assert_eq!(vesting1, amount1); - - let vesting2 = try_get_vesting_coins("vesting2", None, env, deps.as_ref()).unwrap(); - assert_eq!(vesting2, amount2); } #[test] @@ -566,63 +402,13 @@ mod tests { }; let info = mock_info("admin", &coins(1_000_000_000_000, TEST_COIN_DENOM)); - let _response = execute(deps.as_mut(), env.clone(), info, msg); - let account = load_account(Addr::unchecked("owner"), &deps.storage) - .unwrap() - .unwrap(); - - // Try delegating too much - let err = account.try_delegate_to_mixnode( - 1, - Coin { - amount: Uint128::new(1_000_000_000_001), - denom: TEST_COIN_DENOM.to_string(), - }, - &env, - &mut deps.storage, - ); - assert!(err.is_err()); - - let ok = account.try_delegate_to_mixnode( - 1, - Coin { - amount: Uint128::new(90_000_000_000), - denom: TEST_COIN_DENOM.to_string(), - }, - &env, - &mut deps.storage, - ); - assert!(ok.is_ok()); - - // Fails due to delegation locked delegation cap - let ok = account.try_delegate_to_mixnode( - 1, - Coin { - amount: Uint128::new(20_000_000_000), - denom: TEST_COIN_DENOM.to_string(), - }, - &env, - &mut deps.storage, - ); - assert!(ok.is_err()); - - let balance = account.load_balance(&deps.storage).unwrap(); - assert_eq!(balance, Uint128::new(910000000000)); - - // Try delegating too much againcalca - let err = account.try_delegate_to_mixnode( - 1, - Coin { - amount: Uint128::new(500_000_000_001), - denom: TEST_COIN_DENOM.to_string(), - }, - &env, - &mut deps.storage, + let response = execute(deps.as_mut(), env.clone(), info, msg); + assert_eq!( + response, + Err(VestingContractError::Other { + message: "the contract has been disabled".to_string() + }) ); - assert!(err.is_err()); - - let total_delegations = account.total_delegations_for_mix(1, &deps.storage).unwrap(); - assert_eq!(Uint128::new(90_000_000_000), total_delegations); } #[test] @@ -649,52 +435,24 @@ mod tests { amount: Uint128::new(40), }, }; - // Try delegating too much - let err = account.try_bond_mixnode( - mix_node.clone(), - cost_params.clone(), - MessageSignature::from(vec![1, 2, 3]), - Coin { - amount: Uint128::new(1_000_000_000_001), - denom: TEST_COIN_DENOM.to_string(), - }, - &env, - &mut deps.storage, - ); - assert!(err.is_err()); - - let ok = account.try_bond_mixnode( - mix_node.clone(), - cost_params.clone(), - MessageSignature::from(vec![1, 2, 3]), - Coin { - amount: Uint128::new(90_000_000_000), - denom: TEST_COIN_DENOM.to_string(), - }, - &env, - &mut deps.storage, - ); - assert!(ok.is_ok()); - let balance = account.load_balance(&deps.storage).unwrap(); - assert_eq!(balance, Uint128::new(910_000_000_000)); - - // Try delegating too much again - let err = account.try_bond_mixnode( + let msg = ExecuteMsg::BondMixnode { mix_node, cost_params, - MessageSignature::from(vec![1, 2, 3]), - Coin { - amount: Uint128::new(10_000_000_001), + owner_signature: vec![1, 2, 3, 4].into(), + amount: Coin { + amount: Uint128::new(90_000_000_000), denom: TEST_COIN_DENOM.to_string(), }, - &env, - &mut deps.storage, + }; + let info = mock_info(account.owner_address.as_str(), &[]); + let response = execute(deps.as_mut(), env.clone(), info, msg); + assert_eq!( + response, + Err(VestingContractError::Other { + message: "the contract has been disabled".to_string() + }) ); - assert!(err.is_err()); - - let pledge = account.load_mixnode_pledge(&deps.storage).unwrap().unwrap(); - assert_eq!(Uint128::new(90_000_000_000), pledge.amount().amount); } #[test] diff --git a/nym-api/tests/functional_test/status/status-mixnode.test.ts b/nym-api/tests/functional_test/status/status-mixnode.test.ts index e5046510fbb..a5d6f233019 100644 --- a/nym-api/tests/functional_test/status/status-mixnode.test.ts +++ b/nym-api/tests/functional_test/status/status-mixnode.test.ts @@ -1,5 +1,5 @@ import Status from "../../src/endpoints/Status"; -import ConfigHandler from "../../../../common/api-test-utils/config/configHandler" +import ConfigHandler from "../../../../common/api-test-utils/config/configHandler"; let status: Status; let config: ConfigHandler; @@ -153,8 +153,8 @@ describe("Get mixnode data", (): void => { status = new Status(); config = ConfigHandler.getInstance(); }); - - it("with correct data", async (): Promise => { + // TODO - this test needs fixing + it.skip("with correct data", async (): Promise => { const mix_id = config.environmentConfig.mix_id; const response = await status.sendMixnodeRewardEstimatedComputation( mix_id diff --git a/nym-wallet/.storybook/mocks/tauri/index.js b/nym-wallet/.storybook/mocks/tauri/index.js index e2bf50b00dd..3a40c8158f4 100644 --- a/nym-wallet/.storybook/mocks/tauri/index.js +++ b/nym-wallet/.storybook/mocks/tauri/index.js @@ -1,3 +1,55 @@ +const delegations = [ + { + mix_id: 1234, + node_identity: 'FiojKW7oY9WQmLCiYAsCA21tpowZHS6zcUoyYm319p6Z', + delegated_on_iso_datetime: new Date(2021, 1, 1).toDateString(), + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, + amount: { amount: '10', denom: 'nym' }, + owner: '', + block_height: BigInt(100), + cost_params: { + profit_margin_percent: '0.04', + interval_operating_cost: { + amount: '20', + denom: 'nym', + }, + }, + stake_saturation: '0.2', + avg_uptime_percent: 0.5, + accumulated_by_delegates: { amount: '0', denom: 'nym' }, + accumulated_by_operator: { amount: '0', denom: 'nym' }, + uses_vesting_contract_tokens: false, + pending_events: [], + mixnode_is_unbonding: false, + errors: null, + }, + { + mix_id: 5678, + node_identity: 'DT8S942S8AQs2zKHS9SVo1GyHmuca3pfL2uLhLksJ3D8', + unclaimed_rewards: { amount: '0.1', denom: 'nym' }, + amount: { amount: '100', denom: 'nym' }, + delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), + owner: '', + block_height: BigInt(4000), + stake_saturation: '0.5', + avg_uptime_percent: 0.1, + cost_params: { + profit_margin_percent: '0.04', + interval_operating_cost: { + amount: '60', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '0', denom: 'nym' }, + accumulated_by_operator: { amount: '0', denom: 'nym' }, + uses_vesting_contract_tokens: true, + pending_events: [], + mixnode_is_unbonding: false, + errors: null, + }, +]; + + /** * This is a mock for Tauri's API package (@tauri-apps/api), to prevent stories from being excluded, because they either use * or import dependencies that use Tauri. @@ -29,6 +81,26 @@ module.exports = { }, }; } + case 'get_delegation_summary': { + return { + delegations, + total_delegations: { + amount: '1000', + denom: 'nymt', + }, + total_rewards: { + amount: '42', + denom: 'nymt', + }, + }; + } + case 'get_pending_delegation_events' : { + return []; + } + case 'migrate_vested_delegations': { + delegations[1].uses_vesting_contract_tokens = false; + return {}; + } } console.error( diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 061b49cc26e..c037000b2cc 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -656,25 +656,6 @@ dependencies = [ "strsim", ] -[[package]] -name = "clap_complete" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc443334c81a804575546c5a8a79b4913b50e28d69232903604cada1de817ce" -dependencies = [ - "clap", -] - -[[package]] -name = "clap_complete_fig" -version = "4.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99fee1d30a51305a6c2ed3fc5709be3c8af626c9c958e04dd9ae94e27bcbce9f" -dependencies = [ - "clap", - "clap_complete", -] - [[package]] name = "clap_derive" version = "4.4.7" @@ -3128,9 +3109,6 @@ dependencies = [ name = "nym-bin-common" version = "0.6.0" dependencies = [ - "clap", - "clap_complete", - "clap_complete_fig", "const-str", "log", "pretty_env_logger", @@ -3283,6 +3261,7 @@ version = "0.1.0" dependencies = [ "async-trait", "http 1.1.0", + "nym-bin-common", "reqwest 0.12.4", "serde", "serde_json", 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(), + }, }) } } diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index 75a4046264e..8708665bd24 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -8,7 +8,7 @@ repository = "" default-run = "nym_wallet" edition = "2021" build = "src/build.rs" -rust-version = "1.58" +rust-version = "1.76" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index 1ed617dc4bf..9d879c706e1 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -134,6 +134,10 @@ pub enum BackendError { WalletValidatorConnectionFailed, #[error("No defined default validator URL")] WalletNoDefaultValidator, + #[error( + "this vesting operation has been disabled. please use the non-vesting variant instead." + )] + UnsupportedVestingOperation, #[error(transparent)] WalletError { @@ -151,6 +155,9 @@ pub enum BackendError { #[error("This command ({name}) has been removed. Please try to use {alternative} instead.")] RemovedCommand { name: String, alternative: String }, + + #[error("there aren't any vesting delegations to migrate")] + NoVestingDelegations, } impl Serialize for BackendError { diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index 148dcded2bd..623ab63e454 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -129,6 +129,8 @@ fn main() { vesting::bond::withdraw_vested_coins, vesting::delegate::vesting_delegate_to_mixnode, vesting::delegate::vesting_undelegate_from_mixnode, + vesting::migrate::migrate_vested_mixnode, + vesting::migrate::migrate_vested_delegations, vesting::queries::get_account_info, vesting::queries::get_current_vesting_period, vesting::queries::locked_coins, diff --git a/nym-wallet/src-tauri/src/operations/helpers.rs b/nym-wallet/src-tauri/src/operations/helpers.rs index 9f66f292894..2b5631eb580 100644 --- a/nym-wallet/src-tauri/src/operations/helpers.rs +++ b/nym-wallet/src-tauri/src/operations/helpers.rs @@ -12,7 +12,7 @@ use nym_mixnet_contract_common::{ construct_mixnode_bonding_sign_payload, Gateway, GatewayBondingPayload, MixNode, MixNodeCostParams, SignableGatewayBondingMsg, SignableMixNodeBondingMsg, }; -use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, NymContractsProvider}; +use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::Coin; use nym_validator_client::DirectSigningHttpRpcValidatorClient; @@ -21,7 +21,6 @@ use nym_validator_client::DirectSigningHttpRpcValidatorClient; #[async_trait] pub(crate) trait AddressAndNonceProvider { async fn get_signing_nonce(&self) -> Result; - fn vesting_contract_address(&self) -> Addr; fn cw_address(&self) -> Addr; } @@ -31,30 +30,11 @@ impl AddressAndNonceProvider for DirectSigningHttpRpcValidatorClient { self.nyxd.get_signing_nonce(&self.nyxd.address()).await } - fn vesting_contract_address(&self) -> Addr { - // the call to unchecked is fine here as we're converting directly from `AccountId` - // which must have been a valid bech32 address - Addr::unchecked( - self.nyxd - .vesting_contract_address() - .expect("unknown vesting contract address") - .as_ref(), - ) - } - fn cw_address(&self) -> Addr { self.nyxd.cw_address() } } -fn proxy(client: &P, vesting: bool) -> Option { - if vesting { - Some(client.vesting_contract_address()) - } else { - None - } -} - // since the message has to go back to the user due to the increasing nonce, we might as well sign the entire payload pub(crate) async fn create_mixnode_bonding_sign_payload( client: &P, @@ -63,14 +43,15 @@ pub(crate) async fn create_mixnode_bonding_sign_payload Result { + if vesting { + return Err(BackendError::UnsupportedVestingOperation); + } let sender = client.cw_address(); - let proxy = proxy(client, vesting); let nonce = client.get_signing_nonce().await?; Ok(construct_mixnode_bonding_sign_payload( nonce, sender, - proxy, pledge.into(), mix_node, cost_params, @@ -85,6 +66,9 @@ pub(crate) async fn verify_mixnode_bonding_sign_payload Result<(), BackendError> { + if vesting { + return Err(BackendError::UnsupportedVestingOperation); + } let identity_key = identity::PublicKey::from_base58_string(&mix_node.identity_key)?; let signature = identity::Signature::from_bytes(msg_signature.as_ref())?; @@ -118,10 +102,12 @@ pub(crate) async fn create_gateway_bonding_sign_payload Result { + if vesting { + return Err(BackendError::UnsupportedVestingOperation); + } let payload = GatewayBondingPayload::new(gateway); let sender = client.cw_address(); - let proxy = proxy(client, vesting); - let content = ContractMessageContent::new(sender, proxy, vec![pledge.into()], payload); + let content = ContractMessageContent::new(sender, vec![pledge.into()], payload); let nonce = client.get_signing_nonce().await?; Ok(SignableMessage::new(nonce, content)) @@ -134,6 +120,9 @@ pub(crate) async fn verify_gateway_bonding_sign_payload Result<(), BackendError> { + if vesting { + return Err(BackendError::UnsupportedVestingOperation); + } let identity_key = identity::PublicKey::from_base58_string(&gateway.identity_key)?; let signature = identity::Signature::from_bytes(msg_signature.as_ref())?; @@ -170,7 +159,6 @@ mod tests { struct MockClient { address: Addr, - vesting_contract: Addr, signing_nonce: Nonce, } @@ -180,10 +168,6 @@ mod tests { Ok(self.signing_nonce) } - fn vesting_contract_address(&self) -> Addr { - self.vesting_contract.clone() - } - fn cw_address(&self) -> Addr { self.address.clone() } @@ -211,7 +195,6 @@ mod tests { let dummy_account = Addr::unchecked("n16t2umcd83zjpl5puyuuq6lgmy4p3qedjd8ynn6"); let dummy_client = MockClient { address: dummy_account, - vesting_contract: Addr::unchecked("n17tj0a0w6v7r2dc54rnkzfza6s8hxs87rj273a5"), signing_nonce: 42, }; @@ -240,16 +223,8 @@ mod tests { dummy_pledge.clone(), true, ) - .await - .unwrap(); - - let plaintext_vesting = signing_msg_vesting.to_plaintext().unwrap(); - let sig_vesting: MessageSignature = identity_keypair - .private_key() - .sign(&plaintext_vesting) - .to_bytes() - .as_ref() - .into(); + .await; + assert!(signing_msg_vesting.is_err()); let res = verify_mixnode_bonding_sign_payload( &dummy_client, @@ -262,28 +237,6 @@ mod tests { .await; assert!(res.is_ok()); - let res = verify_mixnode_bonding_sign_payload( - &dummy_client, - &dummy_mixnode, - &dummy_cost_params, - &dummy_pledge, - true, - &sig_vesting, - ) - .await; - assert!(res.is_ok()); - - let res = verify_mixnode_bonding_sign_payload( - &dummy_client, - &dummy_mixnode, - &dummy_cost_params, - &dummy_pledge, - false, - &sig_vesting, - ) - .await; - assert!(res.is_err()); - let res = verify_mixnode_bonding_sign_payload( &dummy_client, &dummy_mixnode, @@ -315,7 +268,6 @@ mod tests { let dummy_account = Addr::unchecked("n16t2umcd83zjpl5puyuuq6lgmy4p3qedjd8ynn6"); let dummy_client = MockClient { address: dummy_account, - vesting_contract: Addr::unchecked("n17tj0a0w6v7r2dc54rnkzfza6s8hxs87rj273a5"), signing_nonce: 42, }; @@ -342,16 +294,8 @@ mod tests { dummy_pledge.clone(), true, ) - .await - .unwrap(); - - let plaintext_vesting = signing_msg_vesting.to_plaintext().unwrap(); - let sig_vesting: MessageSignature = identity_keypair - .private_key() - .sign(&plaintext_vesting) - .to_bytes() - .as_ref() - .into(); + .await; + assert!(signing_msg_vesting.is_err()); let res = verify_gateway_bonding_sign_payload( &dummy_client, @@ -363,26 +307,6 @@ mod tests { .await; assert!(res.is_ok()); - let res = verify_gateway_bonding_sign_payload( - &dummy_client, - &dummy_gateway, - &dummy_pledge, - true, - &sig_vesting, - ) - .await; - assert!(res.is_ok()); - - let res = verify_gateway_bonding_sign_payload( - &dummy_client, - &dummy_gateway, - &dummy_pledge, - false, - &sig_vesting, - ) - .await; - assert!(res.is_err()); - let res = verify_gateway_bonding_sign_payload( &dummy_client, &dummy_gateway, diff --git a/nym-wallet/src-tauri/src/operations/vesting/migrate.rs b/nym-wallet/src-tauri/src/operations/vesting/migrate.rs new file mode 100644 index 00000000000..e5624cfbf4e --- /dev/null +++ b/nym-wallet/src-tauri/src/operations/vesting/migrate.rs @@ -0,0 +1,93 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::BackendError; +use crate::nyxd_client; +use crate::state::WalletState; +use nym_mixnet_contract_common::ExecuteMsg; +use nym_types::transaction::TransactionExecuteResult; +use nym_validator_client::nyxd::contract_traits::{ + MixnetSigningClient, NymContractsProvider, PagedMixnetQueryClient, +}; +use nym_validator_client::nyxd::Fee; + +#[tauri::command] +pub async fn migrate_vested_mixnode( + fee: Option, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.read().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + log::info!(">>> migrate vested mixnode, fee = {fee:?}"); + + let res = nyxd_client!(state).migrate_vested_mixnode(fee).await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + +#[tauri::command] +pub async fn migrate_vested_delegations( + state: tauri::State<'_, WalletState>, +) -> Result { + log::info!(">>> migrate vested delegations"); + + let guard = state.read().await; + let client = guard.current_client()?; + + let address = client.nyxd.address(); + let mixnet_contract = client + .nyxd + .mixnet_contract_address() + .expect("unavailable mixnet contract address"); + + log::info!(" >>> Get delegations"); + let delegations = client + .nyxd + .get_all_delegator_delegations(&address) + .await + .inspect_err(|err| { + log::error!(" <<< Failed to get delegations. Error: {}", err); + })?; + log::info!(" <<< {} delegations", delegations.len()); + + let vesting_delegations = delegations + .into_iter() + .filter(|d| d.proxy.is_some()) + .collect::>(); + + log::info!(" <<< {} vesting delegations", vesting_delegations.len()); + + if vesting_delegations.is_empty() { + return Err(BackendError::NoVestingDelegations); + } + + let mut migrate_msgs = Vec::new(); + for delegation in &vesting_delegations { + migrate_msgs.push(( + ExecuteMsg::MigrateVestedDelegation { + mix_id: delegation.mix_id, + }, + Vec::new(), + )); + } + + let res = client + .nyxd + .execute_multiple( + mixnet_contract, + migrate_msgs, + None, + format!( + "migrating {} vesting delegations", + vesting_delegations.len() + ), + ) + .await?; + + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result(res, None)?) +} diff --git a/nym-wallet/src-tauri/src/operations/vesting/mod.rs b/nym-wallet/src-tauri/src/operations/vesting/mod.rs index fb80d4ed432..7ed3d0f0dc3 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/mod.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/mod.rs @@ -1,4 +1,5 @@ pub mod bond; pub mod delegate; +pub mod migrate; pub mod queries; pub mod rewards; diff --git a/nym-wallet/src/components/Bonding/BondedMixnode.tsx b/nym-wallet/src/components/Bonding/BondedMixnode.tsx index 199c8f5653d..e569cd7aec3 100644 --- a/nym-wallet/src/components/Bonding/BondedMixnode.tsx +++ b/nym-wallet/src/components/Bonding/BondedMixnode.tsx @@ -127,6 +127,7 @@ export const BondedMixnode = ({ ), id: 'actions-cell', diff --git a/nym-wallet/src/components/Bonding/forms/MixnodeAmountForm.tsx b/nym-wallet/src/components/Bonding/forms/MixnodeAmountForm.tsx index 42a8d880618..f24f9138b7c 100644 --- a/nym-wallet/src/components/Bonding/forms/MixnodeAmountForm.tsx +++ b/nym-wallet/src/components/Bonding/forms/MixnodeAmountForm.tsx @@ -22,6 +22,8 @@ const MixnodeAmountForm = ({ denom: CurrencyDenom; onNext: (data: MixnodeAmount) => void; }) => { + const { mixnetContractParams } = useContext(AppContext); + const { register, formState: { errors }, @@ -29,7 +31,7 @@ const MixnodeAmountForm = ({ setValue, getValues, setError, - } = useForm({ resolver: yupResolver(amountSchema), defaultValues: amountData }); + } = useForm({ resolver: yupResolver(amountSchema(mixnetContractParams)), defaultValues: amountData }); const { userBalance } = useContext(AppContext); diff --git a/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts b/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts index 4ba393db38b..a72364377c7 100644 --- a/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts +++ b/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts @@ -8,6 +8,7 @@ import { validateRawPort, validateVersion, } from 'src/utils'; +import { TauriContractStateParams } from '../../../types'; export const mixnodeValidationSchema = Yup.object().shape({ identityKey: Yup.string() @@ -41,39 +42,60 @@ export const mixnodeValidationSchema = Yup.object().shape({ .test('valid-http', 'A valid http-api port is required', (value) => (value ? validateRawPort(value) : false)), }); -const operatingCostAndPmValidation = { - profitMargin: Yup.number().required('Profit Percentage is required').min(4).max(80), - operatorCost: Yup.object().shape({ - amount: Yup.string() - .required('An operating cost is required') - // eslint-disable-next-line prefer-arrow-callback - .test( - 'valid-operating-cost', - 'A valid amount is required (min 40 - max 2000)', - async function isValidAmount(this, value) { - if (value && (!Number(value) || isLessThan(+value, 40) || isGreaterThan(+value, 2000))) { - return this.createError({ message: 'A valid amount is required (min 40 - max 2000)' }); +const operatingCostAndPmValidation = (params?: TauriContractStateParams) => { + const defaultParams = { + profit_margin: { + minimum: parseFloat(params?.profit_margin.minimum || '0%'), + maximum: parseFloat(params?.profit_margin.maximum || '100%'), + }, + + operating_cost: { + minimum: parseFloat(params?.operating_cost.minimum.amount || '0'), + maximum: parseFloat(params?.operating_cost.maximum.amount || '1000000000'), + }, + }; + + return { + profitMargin: Yup.number() + .required('Profit Percentage is required') + .min(defaultParams.profit_margin.minimum) + .max(defaultParams.profit_margin.maximum), + operatorCost: Yup.object().shape({ + amount: Yup.string() + .required('An operating cost is required') + // eslint-disable-next-line prefer-arrow-callback + .test('valid-operating-cost', 'A valid amount is required', async function isValidAmount(this, value) { + if ( + value && + (!Number(value) || + isLessThan(+value, defaultParams.operating_cost.minimum) || + isGreaterThan(+value, Number(defaultParams.operating_cost.maximum))) + ) { + return this.createError({ + message: `A valid amount is required (min ${defaultParams?.operating_cost.minimum} - max ${defaultParams?.operating_cost.maximum})`, + }); } return true; - }, - ), - }), + }), + }), + }; }; -export const amountSchema = Yup.object().shape({ - amount: Yup.object().shape({ - amount: Yup.string() - .required('An amount is required') - .test('valid-amount', 'Pledge error', async function isValidAmount(this, value) { - const isValid = await validateAmount(value || '', '100'); - if (!isValid) { - return this.createError({ message: 'A valid amount is required (min 100)' }); - } - return true; - }), - }), - ...operatingCostAndPmValidation, -}); +export const amountSchema = (params?: TauriContractStateParams) => + Yup.object().shape({ + amount: Yup.object().shape({ + amount: Yup.string() + .required('An amount is required') + .test('valid-amount', 'Pledge error', async function isValidAmount(this, value) { + const isValid = await validateAmount(value || '', '100'); + if (!isValid) { + return this.createError({ message: 'A valid amount is required (min 100)' }); + } + return true; + }), + }), + ...operatingCostAndPmValidation(params), + }); export const bondedInfoParametersValidationSchema = Yup.object().shape({ host: Yup.string() @@ -99,6 +121,7 @@ export const bondedInfoParametersValidationSchema = Yup.object().shape({ .test('valid-http', 'A valid http-api port is required', (value) => (value ? validateRawPort(value) : false)), }); -export const bondedNodeParametersValidationSchema = Yup.object().shape({ - ...operatingCostAndPmValidation, -}); +export const bondedNodeParametersValidationSchema = (params?: TauriContractStateParams) => + Yup.object().shape({ + ...operatingCostAndPmValidation(params), + }); diff --git a/nym-wallet/src/components/VestingWarningModal/index.tsx b/nym-wallet/src/components/VestingWarningModal/index.tsx new file mode 100644 index 00000000000..58fc4d30b41 --- /dev/null +++ b/nym-wallet/src/components/VestingWarningModal/index.tsx @@ -0,0 +1,33 @@ +import React, { FC } from 'react'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; + +export const VestingWarningModal: FC<{ + kind: 'delegations' | 'bond'; + isVisible: boolean; + handleClose: () => void; + handleMigrate: () => Promise; +}> = ({ kind, isVisible, handleClose, handleMigrate }) => ( + + Migrate your {kind}? + + + By clicking yes we will migrate your {kind} to the mixnet contract. + + + The operation will be instant, you will keep your rewards and they will continue to accumulate. Once migrated, + you will be able to withdraw your rewards. + + + + + + + +); diff --git a/nym-wallet/src/context/bonding.tsx b/nym-wallet/src/context/bonding.tsx index 687c754ff7d..58c212a4e85 100644 --- a/nym-wallet/src/context/bonding.tsx +++ b/nym-wallet/src/context/bonding.tsx @@ -53,6 +53,7 @@ import { generateGatewayMsgPayload as generateGatewayMsgPayloadReq, updateBond as updateBondReq, vestingUpdateBond as vestingUpdateBondReq, + migrateVestedMixnode as tauriMigrateVestedMixnode, } from '../requests'; import { useCheckOwnership } from '../hooks/useCheckOwnership'; import { AppContext } from './main'; @@ -128,6 +129,7 @@ export type TBondingContext = { generateMixnodeMsgPayload: (data: TBondMixnodeSignatureArgs) => Promise; generateGatewayMsgPayload: (data: TBondGatewaySignatureArgs) => Promise; isVestingAccount: boolean; + migrateVestedMixnode: () => Promise; }; export const BondingContext = createContext({ @@ -157,6 +159,9 @@ export const BondingContext = createContext({ generateGatewayMsgPayload: async () => { throw new Error('Not implemented'); }, + migrateVestedMixnode: async () => { + throw new Error('Not implemented'); + }, isVestingAccount: false, }); @@ -599,6 +604,13 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen return message; }; + const migrateVestedMixnode = async () => { + setIsLoading(true); + const tx = await tauriMigrateVestedMixnode(); + setIsLoading(false); + return tx; + }; + const memoizedValue = useMemo( () => ({ isLoading: isLoading || isOwnershipLoading, @@ -613,6 +625,7 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen updateBondAmount, generateMixnodeMsgPayload, generateGatewayMsgPayload, + migrateVestedMixnode, isVestingAccount, }), [isLoading, isOwnershipLoading, error, bondedNode, isVestingAccount], diff --git a/nym-wallet/src/context/main.tsx b/nym-wallet/src/context/main.tsx index af965b79a00..24970ca3b6e 100644 --- a/nym-wallet/src/context/main.tsx +++ b/nym-wallet/src/context/main.tsx @@ -4,9 +4,10 @@ import { useNavigate } from 'react-router-dom'; import { useSnackbar } from 'notistack'; import { Account, AccountEntry, MixNodeDetails } from '@nymproject/types'; import { getVersion } from '@tauri-apps/api/app'; -import { AppEnv, Network } from '../types'; +import { AppEnv, Network, TauriContractStateParams } from '../types'; import { TUseuserBalance, useGetBalance } from '../hooks/useGetBalance'; import { + getContractParams, getEnv, getMixnodeBondDetails, listAccounts, @@ -67,6 +68,7 @@ export type TAppContext = { keepState: () => Promise; printBalance: string; printVestedBalance?: string; // spendable vested token + mixnetContractParams?: TauriContractStateParams; }; interface RustState { @@ -94,6 +96,7 @@ export const AppProvider: FCWithChildren = ({ children }) => { const [showReceiveModal, setShowReceiveModal] = useState(false); const [printBalance, setPrintBalance] = useState('-'); const [printVestedBalance, setPrintVestedBalance] = useState(); + const [mixnetContractParams, setMixnetContractParams] = useState(); const userBalance = useGetBalance(clientDetails); const navigate = useNavigate(); @@ -230,6 +233,10 @@ export const AppProvider: FCWithChildren = ({ children }) => { } } setIsAdminAddress(newValue); + + getContractParams().then((params) => { + setMixnetContractParams(params); + }); }, [appEnv, network, clientDetails?.client_address]); const logIn = async ({ type, value }: { type: TLoginType; value: string }) => { @@ -329,6 +336,7 @@ export const AppProvider: FCWithChildren = ({ children }) => { handleSwitchMode, printBalance, printVestedBalance, + mixnetContractParams, }), [ appVersion, @@ -347,6 +355,7 @@ export const AppProvider: FCWithChildren = ({ children }) => { showTerminal, showSendModal, showReceiveModal, + mixnetContractParams, ], ); diff --git a/nym-wallet/src/context/mocks/bonding.tsx b/nym-wallet/src/context/mocks/bonding.tsx index 6218e9aa504..e961272c664 100644 --- a/nym-wallet/src/context/mocks/bonding.tsx +++ b/nym-wallet/src/context/mocks/bonding.tsx @@ -207,6 +207,7 @@ export const MockBondingContextProvider = ({ generateMixnodeMsgPayload, generateGatewayMsgPayload, isVestingAccount: false, + migrateVestedMixnode: async () => undefined, }), [isLoading, error, bondedMixnode, bondedGateway, trigger, fee], ); diff --git a/nym-wallet/src/pages/bonding/Bonding.tsx b/nym-wallet/src/pages/bonding/Bonding.tsx index 24aeb768eed..3ab472d1e47 100644 --- a/nym-wallet/src/pages/bonding/Bonding.tsx +++ b/nym-wallet/src/pages/bonding/Bonding.tsx @@ -1,7 +1,7 @@ import React, { useContext, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { FeeDetails } from '@nymproject/types'; -import { Box } from '@mui/material'; +import { Alert, AlertTitle, Box, Button, Typography } from '@mui/material'; import { TPoolOption } from 'src/components'; import { Bond } from 'src/components/Bonding/Bond'; import { BondedMixnode } from 'src/components/Bonding/BondedMixnode'; @@ -17,15 +17,16 @@ import { AppContext, urls } from 'src/context/main'; import { isGateway, isMixnode, TBondGatewayArgs, TBondMixNodeArgs, TUpdateBondArgs } from 'src/types'; import { BondedGateway } from 'src/components/Bonding/BondedGateway'; import { RedeemRewardsModal } from 'src/components/Bonding/modals/RedeemRewardsModal'; +import { VestingWarningModal } from 'src/components/VestingWarningModal'; import { BondingContextProvider, useBondingContext } from '../../context'; -const Bonding = () => { +export const Bonding = () => { const [showModal, setShowModal] = useState< 'bond-mixnode' | 'bond-gateway' | 'update-bond' | 'update-bond-oversaturated' | 'unbond' | 'redeem' >(); const [confirmationDetails, setConfirmationDetails] = useState(); const [uncappedSaturation, setUncappedSaturation] = useState(); - + const [showMigrationModal, setShowMigrationModal] = useState(false); const { network, clientDetails, @@ -34,8 +35,17 @@ const Bonding = () => { const navigate = useNavigate(); - const { bondedNode, bondMixnode, bondGateway, redeemRewards, isLoading, updateBondAmount, error, refresh } = - useBondingContext(); + const { + bondedNode, + bondMixnode, + bondGateway, + redeemRewards, + isLoading, + updateBondAmount, + error, + refresh, + migrateVestedMixnode, + } = useBondingContext(); useEffect(() => { if (bondedNode && isMixnode(bondedNode) && bondedNode.uncappedStakeSaturation) { @@ -43,6 +53,18 @@ const Bonding = () => { } }, [bondedNode]); + const handleMigrateVestedMixnode = async () => { + setShowMigrationModal(false); + const tx = await migrateVestedMixnode(); + if (tx) { + setConfirmationDetails({ + status: 'success', + title: 'Migration successful', + txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`, + }); + } + }; + const handleCloseModal = async () => { setShowModal(undefined); refresh(); @@ -138,6 +160,33 @@ const Bonding = () => { return ( + {bondedNode?.proxy && ( + + Your bonded node is using tokens from the vesting contract! + + In order to claim your rewards, you will need to migrate it out of the vesting contract.{' '} + + + Never fear, if you do not migrate them, you will continue to get rewards. + However, please migrate your bonded node as soon as possible. + + + + )} + + { + setShowMigrationModal(false); + }} + handleMigrate={async () => { + await handleMigrateVestedMixnode(); + }} + /> + {!bondedNode && setShowModal('bond-mixnode')} />} {bondedNode && isMixnode(bondedNode) && ( diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/ParametersSettings.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/ParametersSettings.tsx index 0558d0af354..4f2032ac30d 100644 --- a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/ParametersSettings.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/ParametersSettings.tsx @@ -51,6 +51,8 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode const { fee, getFee, resetFeeState } = useGetFee(); + const { mixnetContractParams } = useContext(AppContext); + const defaultValues = { operatorCost: bondedNode.operatorCost, profitMargin: bondedNode.profitMargin, @@ -63,7 +65,7 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode setValue, formState: { errors, isSubmitting, isDirty, isValid }, } = useForm({ - resolver: yupResolver(bondedNodeParametersValidationSchema), + resolver: yupResolver(bondedNodeParametersValidationSchema(mixnetContractParams)), mode: 'onChange', defaultValues, }); diff --git a/nym-wallet/src/pages/delegation/index.tsx b/nym-wallet/src/pages/delegation/index.tsx index 3e985a35260..b51ebcf60f2 100644 --- a/nym-wallet/src/pages/delegation/index.tsx +++ b/nym-wallet/src/pages/delegation/index.tsx @@ -1,5 +1,5 @@ import React, { FC, useContext, useEffect, useState } from 'react'; -import { Box, Button, Paper, Stack, Typography } from '@mui/material'; +import { Alert, AlertTitle, Box, Button, Paper, Stack, Typography } from '@mui/material'; import { Theme, useTheme } from '@mui/material/styles'; import { DecCoin, decimalToFloatApproximation, DelegationWithEverything, FeeDetails } from '@nymproject/types'; import { Link } from '@nymproject/react/link/Link'; @@ -8,11 +8,11 @@ import { DelegationList } from 'src/components/Delegation/DelegationList'; import { TPoolOption } from 'src/components'; import { Console } from 'src/utils/console'; import { OverSaturatedBlockerModal } from 'src/components/Delegation/DelegateBlocker'; -import { getSpendableCoins, userBalance } from 'src/requests'; +import { getSpendableCoins, migrateVestedDelegations, userBalance } from 'src/requests'; import { LoadingModal } from 'src/components/Modals/LoadingModal'; import { getIntervalAsDate, toPercentIntegerString } from 'src/utils'; import { RewardsSummary } from '../../components/Rewards/RewardsSummary'; -import { DelegationContextProvider, TDelegations, useDelegationContext } from '../../context/delegations'; +import { DelegationContextProvider, isDelegation, TDelegations, useDelegationContext } from '../../context/delegations'; import { RewardsContextProvider, useRewardsContext } from '../../context/rewards'; import { DelegateModal } from '../../components/Delegation/DelegateModal'; import { UndelegateModal } from '../../components/Delegation/UndelegateModal'; @@ -20,6 +20,7 @@ import { DelegationListItemActions } from '../../components/Delegation/Delegatio import { RedeemModal } from '../../components/Rewards/RedeemModal'; import { DelegationModal, DelegationModalProps } from '../../components/Delegation/DelegationModal'; import { backDropStyles, modalStyles } from '../../../.storybook/storiesStyles'; +import { VestingWarningModal } from '../../components/VestingWarningModal'; const storybookStyles = (theme: Theme, isStorybook?: boolean, backdropProps?: object) => isStorybook @@ -38,6 +39,8 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { const [currentDelegationListActionItem, setCurrentDelegationListActionItem] = useState(); const [saturationError, setSaturationError] = useState<{ action: 'compound' | 'delegate'; saturation: string }>(); const [nextEpoch, setNextEpoch] = useState(); + const [showVestingWarningModal, setShowVestingWarningModal] = useState(false); + const [showVestingMigrationProgressModal, setShowVestingMigrationProgressModal] = useState(false); const theme = useTheme(); const { @@ -57,6 +60,11 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { refresh: refreshDelegations, } = useDelegationContext(); + const delegationsUseVestingTokens: boolean = React.useMemo( + () => Boolean(delegations?.filter((d) => isDelegation(d) && d.uses_vesting_contract_tokens).length), + [delegations], + ); + const { refresh: refreshRewards, claimRewards } = useRewardsContext(); const refresh = async () => Promise.all([refreshDelegations(), refreshRewards()]); @@ -105,6 +113,13 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { return () => clearInterval(timer); }, []); + const doMigrateNow = async () => { + setShowVestingMigrationProgressModal(true); + await migrateVestedDelegations(); + await refresh(); + setShowVestingMigrationProgressModal(false); + }; + useEffect(() => { refreshWithIntervalUpdate(); }, [clientDetails, confirmationModalProps]); @@ -119,6 +134,11 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { return; } + if (item.uses_vesting_contract_tokens) { + setShowVestingWarningModal(true); + return; + } + setCurrentDelegationListActionItem(item); // eslint-disable-next-line default-case switch (action) { @@ -305,12 +325,46 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { const delegationsComponent = (delegationItems: TDelegations | undefined) => { if (delegationItems && Boolean(delegationItems?.length)) { return ( - + <> + {delegationsUseVestingTokens && ( + <> + + + Some of your delegations are using tokens from the vesting contract! + + + In order to claim your rewards, you will need to migrate them out of the vesting contract.{' '} + + + Never fear, if you do not migrate them,{' '} + you will continue to get rewards. However, please migrate your delegations as soon as + possible. + + + + setShowVestingWarningModal(false)} + /> + {showVestingMigrationProgressModal && } + + )} + + ); } diff --git a/nym-wallet/src/requests/actions.ts b/nym-wallet/src/requests/actions.ts index cf09085d5fa..7361841f3f5 100644 --- a/nym-wallet/src/requests/actions.ts +++ b/nym-wallet/src/requests/actions.ts @@ -52,3 +52,5 @@ export const unbond = async (type: EnumNodeType) => { export const updateBond = async (args: TUpdateBondArgs) => invokeWrapper('update_pledge', args); + +export const migrateVestedMixnode = async () => invokeWrapper('migrate_vested_mixnode'); diff --git a/nym-wallet/src/requests/delegation.ts b/nym-wallet/src/requests/delegation.ts index d68d8a9084f..1ab73ba655a 100644 --- a/nym-wallet/src/requests/delegation.ts +++ b/nym-wallet/src/requests/delegation.ts @@ -31,3 +31,6 @@ export const undelegateAllFromMixnode = async ( export const delegateToMixnode = async (mixId: number, amount: DecCoin, fee?: Fee) => invokeWrapper('delegate_to_mixnode', { mixId, amount, fee }); + +export const migrateVestedDelegations = async () => + invokeWrapper('migrate_vested_delegations'); diff --git a/nym-wallet/src/types/rust/OperatingCostRange.ts b/nym-wallet/src/types/rust/OperatingCostRange.ts new file mode 100644 index 00000000000..b3877c83479 --- /dev/null +++ b/nym-wallet/src/types/rust/OperatingCostRange.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DecCoin } from '@nymproject/types/src/types/rust/DecCoin'; + +export interface TauriOperatingCostRange { + minimum: DecCoin; + maximum: DecCoin; +} diff --git a/nym-wallet/src/types/rust/ProfitMarginRange.ts b/nym-wallet/src/types/rust/ProfitMarginRange.ts new file mode 100644 index 00000000000..c12d49d5d91 --- /dev/null +++ b/nym-wallet/src/types/rust/ProfitMarginRange.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface TauriProfitMarginRange { + minimum: string; + maximum: string; +} diff --git a/nym-wallet/src/types/rust/StateParams.ts b/nym-wallet/src/types/rust/StateParams.ts index 2119c59470a..b909e4c7343 100644 --- a/nym-wallet/src/types/rust/StateParams.ts +++ b/nym-wallet/src/types/rust/StateParams.ts @@ -1,8 +1,12 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { DecCoin } from '@nymproject/types/src/types/rust/DecCoin'; +import type { TauriOperatingCostRange } from './OperatingCostRange'; +import type { TauriProfitMarginRange } from './ProfitMarginRange'; export interface TauriContractStateParams { minimum_mixnode_pledge: DecCoin; minimum_gateway_pledge: DecCoin; minimum_mixnode_delegation: DecCoin | null; + operating_cost: TauriOperatingCostRange; + profit_margin: TauriProfitMarginRange; } diff --git a/sdk/typescript/packages/react-components/package.json b/sdk/typescript/packages/react-components/package.json index 49c288d4a24..6d7a21747fe 100644 --- a/sdk/typescript/packages/react-components/package.json +++ b/sdk/typescript/packages/react-components/package.json @@ -26,9 +26,9 @@ "@cosmjs/math": "^0.27.1", "@mui/icons-material": ">= 5", "@mui/lab": "^5.0.0-alpha.72", - "@mui/material": ">= 5", - "@mui/styles": ">= 5", - "@mui/system": ">= 5", + "@mui/material": "5", + "@mui/styles": "5", + "@mui/system": "5", "@nymproject/mui-theme": "1", "@nymproject/nym-validator-client": "^0.18.0", "@nymproject/types": "1", @@ -39,7 +39,8 @@ "zxcvbn": "^4.4.2" }, "dependencies": { - "@mui/x-tree-view": "^7.10.0", + "@mui/lab": "5.0.0-alpha.170", + "@mui/x-tree-view": "^7.11.1", "flat": "^5.0.2", "use-clipboard-copy": "^0.2.0" }, diff --git a/tools/nym-cli/src/validator/mixnet/delegators/mod.rs b/tools/nym-cli/src/validator/mixnet/delegators/mod.rs index 8ccfbd59a3f..08e5df592ab 100644 --- a/tools/nym-cli/src/validator/mixnet/delegators/mod.rs +++ b/tools/nym-cli/src/validator/mixnet/delegators/mod.rs @@ -34,7 +34,10 @@ pub(crate) async fn execute( } nym_cli_commands::validator::mixnet::delegators::MixnetDelegatorsCommands::List(args) => { nym_cli_commands::validator::mixnet::delegators::query_for_delegations::execute(args, create_signing_client_with_nym_api(global_args, network_details)?).await - } + }, + nym_cli_commands::validator::mixnet::delegators::MixnetDelegatorsCommands::MigrateVestedDelegation(args) => { + nym_cli_commands::validator::mixnet::delegators::migrate_vested_delegation::migrate_vested_delegation(args, create_signing_client(global_args, network_details)?).await + }, } Ok(()) } diff --git a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/mod.rs b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/mod.rs index b3cf6d4e28d..ac2d301f4fd 100644 --- a/tools/nym-cli/src/validator/mixnet/operators/mixnodes/mod.rs +++ b/tools/nym-cli/src/validator/mixnet/operators/mixnodes/mod.rs @@ -48,7 +48,10 @@ pub(crate) async fn execute( nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::DecreasePledgeVesting(args) => { nym_cli_commands::validator::mixnet::operators::mixnode::vesting_decrease_pledge::vesting_decrease_pledge(args, create_signing_client(global_args, network_details)?).await } - _ => unreachable!(), + nym_cli_commands::validator::mixnet::operators::mixnode::MixnetOperatorsMixnodeCommands::MigrateVestedNode(args) => { + nym_cli_commands::validator::mixnet::operators::mixnode::migrate_vested_mixnode::migrate_vested_mixnode(args, create_signing_client(global_args, network_details)?).await + } + _ => unreachable!() } Ok(()) } diff --git a/tools/ts-rs-cli/src/main.rs b/tools/ts-rs-cli/src/main.rs index d5a58780082..e3728bd234f 100644 --- a/tools/ts-rs-cli/src/main.rs +++ b/tools/ts-rs-cli/src/main.rs @@ -27,7 +27,9 @@ use nym_types::transaction::{ }; use nym_types::vesting::{OriginalVestingResponse, PledgeData, VestingAccountInfo, VestingPeriod}; use nym_vesting_contract_common::Period; -use nym_wallet_types::admin::TauriContractStateParams; +use nym_wallet_types::admin::{ + TauriContractStateParams, TauriOperatingCostRange, TauriProfitMarginRange, +}; use nym_wallet_types::app::AppEnv; use nym_wallet_types::app::AppVersion; use nym_wallet_types::interval::Interval; @@ -131,6 +133,8 @@ fn main() { do_export!(Interval); do_export!(Network); do_export!(TauriContractStateParams); + do_export!(TauriOperatingCostRange); + do_export!(TauriProfitMarginRange); do_export!(Validator); do_export!(ValidatorUrl); do_export!(ValidatorUrls); diff --git a/yarn.lock b/yarn.lock index f17664b8ed2..40513f47f47 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1212,7 +1212,7 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/runtime@^7.24.7": +"@babel/runtime@^7.24.8": version "7.24.8" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.8.tgz#5d958c3827b13cc6d05e038c07fb2e5e3420d82e" integrity sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA== @@ -2965,6 +2965,19 @@ dependencies: "@babel/runtime" "^7.23.9" +"@mui/lab@5.0.0-alpha.170": + version "5.0.0-alpha.170" + resolved "https://registry.yarnpkg.com/@mui/lab/-/lab-5.0.0-alpha.170.tgz#4519dfc8d1c51ca54fb9d8b91b95a3733d07be16" + integrity sha512-0bDVECGmrNjd3+bLdcLiwYZ0O4HP5j5WSQm5DV6iA/Z9kr8O6AnvZ1bv9ImQbbX7Gj3pX4o43EKwCutj3EQxQg== + dependencies: + "@babel/runtime" "^7.23.9" + "@mui/base" "5.0.0-beta.40" + "@mui/system" "^5.15.15" + "@mui/types" "^7.2.14" + "@mui/utils" "^5.15.14" + clsx "^2.1.0" + prop-types "^15.8.1" + "@mui/material@^5.0.1", "@mui/material@^5.2.2": version "5.15.15" resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.15.15.tgz#e3ba35f50b510aa677cec3261abddc2db7b20b59" @@ -2992,13 +3005,13 @@ "@mui/utils" "^5.15.14" prop-types "^15.8.1" -"@mui/private-theming@^5.16.1": - version "5.16.1" - resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.16.1.tgz#e7f1b0cfd9b238231fca9a0b13a5b2a9d9592b35" - integrity sha512-2EGCKnAlq9vRIFj61jNWNXlKAxXp56577OVvsts7fAqRx+G1y6F+N7Q198SBaz8jYQeGKSz8ZMXK/M3FqjdEyw== +"@mui/private-theming@^5.16.5": + version "5.16.5" + resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.16.5.tgz#b90afcfa76ea50778453c633656ff59cb1b5494d" + integrity sha512-CSLg0YkpDqg0aXOxtjo3oTMd3XWMxvNb5d0v4AYVqwOltU8q6GvnZjhWyCLjGSCrcgfwm6/VDjaKLPlR14wxIA== dependencies: "@babel/runtime" "^7.23.9" - "@mui/utils" "^5.16.1" + "@mui/utils" "^5.16.5" prop-types "^15.8.1" "@mui/styled-engine@^5.15.14": @@ -3011,10 +3024,10 @@ csstype "^3.1.3" prop-types "^15.8.1" -"@mui/styled-engine@^5.16.1": - version "5.16.1" - resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.16.1.tgz#7518f64f66edd6e09f129289cf6ece502520947f" - integrity sha512-JwWUBaYR8HHCFefSeos0z6JoTbu0MnjAuNHu4QoDgPxl2EE70XH38CsKay66Iy0QkNWmGTRXVU2sVFgUOPL/Dw== +"@mui/styled-engine@^5.16.4": + version "5.16.4" + resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.16.4.tgz#a7a8c9079c307bab91ccd65ed5dd1496ddf2a3ab" + integrity sha512-0+mnkf+UiAmTVB8PZFqOhqf729Yh0Cxq29/5cA3VAyDVTRIUUQ8FXQhiAhUIbijFmM72rY80ahFPXIm4WDbzcA== dependencies: "@babel/runtime" "^7.23.9" "@emotion/cache" "^11.11.0" @@ -3058,16 +3071,16 @@ csstype "^3.1.3" prop-types "^15.8.1" -"@mui/system@^5.16.0": - version "5.16.1" - resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.16.1.tgz#c08fddba592511d6916c6a70da292a7658551ccb" - integrity sha512-VaFcClC+uhvIEzhzcNmh9FRBvrG9IPjsOokhj6U1HPZsFnLzHV7AD7dJcT6LxWoiIZj9Ej0GK+MGh/b8+BtSlQ== +"@mui/system@^5.16.5": + version "5.16.5" + resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.16.5.tgz#a90239e1467f7fce960167939dde9f44f6869484" + integrity sha512-uzIUGdrWddUx1HPxW4+B2o4vpgKyRxGe/8BxbfXVDPNPHX75c782TseoCnR/VyfnZJfqX87GcxDmnZEE1c031g== dependencies: "@babel/runtime" "^7.23.9" - "@mui/private-theming" "^5.16.1" - "@mui/styled-engine" "^5.16.1" + "@mui/private-theming" "^5.16.5" + "@mui/styled-engine" "^5.16.4" "@mui/types" "^7.2.15" - "@mui/utils" "^5.16.1" + "@mui/utils" "^5.16.5" clsx "^2.1.0" csstype "^3.1.3" prop-types "^15.8.1" @@ -3092,13 +3105,15 @@ prop-types "^15.8.1" react-is "^18.2.0" -"@mui/utils@^5.16.0", "@mui/utils@^5.16.1": - version "5.16.1" - resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.16.1.tgz#068ddc236c10f71768133c144b3286d2cc815f5f" - integrity sha512-4UQzK46tAEYs2xZv79hRiIc3GxZScd00kGPDadNrGztAEZlmSaUY8cb9ITd2xCiTfzsx5AN6DH8aaQ8QEKJQeQ== +"@mui/utils@^5.16.5": + version "5.16.5" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.16.5.tgz#3a16877a80166a7f8b58c893d08e0993040fc49e" + integrity sha512-CwhcA9y44XwK7k2joL3Y29mRUnoBt+gOZZdGyw7YihbEwEErJYBtDwbZwVgH68zAljGe/b+Kd5bzfl63Gi3R2A== dependencies: "@babel/runtime" "^7.23.9" + "@mui/types" "^7.2.15" "@types/prop-types" "^15.7.12" + clsx "^2.1.1" prop-types "^15.8.1" react-is "^18.3.1" @@ -3139,15 +3154,24 @@ prop-types "^15.8.1" react-transition-group "^4.4.5" -"@mui/x-tree-view@^7.10.0": - version "7.10.0" - resolved "https://registry.yarnpkg.com/@mui/x-tree-view/-/x-tree-view-7.10.0.tgz#7aed69461df425e6aaebf0fd775ae5ff52cdc065" - integrity sha512-9OCAIb0wS5uuEDyjcSwSturrB4RUXBfE0UO/xpKjrMvRzCaAvxbCf2aFILP8uH9NyynYZkIGYfGnlqdAPy2OLg== +"@mui/x-internals@7.11.1": + version "7.11.1" + resolved "https://registry.yarnpkg.com/@mui/x-internals/-/x-internals-7.11.1.tgz#ab5f1155ba09665d9e53a9a461b4c52b89aaa43f" + integrity sha512-CN9HmtcyJ6/1fd8by5h1/R8WmFN4xyk6XYvYG9++oAaSF1ttX16oiE5vB+gGafl7St0epCWWjvOzl21h29k6WQ== + dependencies: + "@babel/runtime" "^7.24.8" + "@mui/utils" "^5.16.5" + +"@mui/x-tree-view@^7.11.1": + version "7.11.1" + resolved "https://registry.yarnpkg.com/@mui/x-tree-view/-/x-tree-view-7.11.1.tgz#77748013f368a9bd5f1e5e03adf3d6a788fb0f76" + integrity sha512-BiQnx/bGnEFjPge10v9X1QTJVRvv5aAH2Q35YV8XX2iuONQO2fpam/jALyQuX9xY7LV6zoHG48oISV4VPpWo7g== dependencies: - "@babel/runtime" "^7.24.7" + "@babel/runtime" "^7.24.8" "@mui/base" "^5.0.0-beta.40" - "@mui/system" "^5.16.0" - "@mui/utils" "^5.16.0" + "@mui/system" "^5.16.5" + "@mui/utils" "^5.16.5" + "@mui/x-internals" "7.11.1" "@types/react-transition-group" "^4.4.10" clsx "^2.1.1" prop-types "^15.8.1"