From 9a9d91cb4f93c2802dccfb8db0a48ae802268414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 23 Sep 2024 11:53:48 +0100 Subject: [PATCH] additional unit tests for compatibility-based txs + bug fixes --- .../contract_traits/mixnet_query_client.rs | 32 +- .../mixnet-contract/src/msg.rs | 1 + .../mixnet-contract/src/types.rs | 4 + contracts/mixnet/src/compat/helpers.rs | 433 +++++++++++++ contracts/mixnet/src/compat/transactions.rs | 568 ++++++++++++++++++ contracts/mixnet/src/gateways/transactions.rs | 24 +- .../src/mixnet_contract_settings/storage.rs | 2 +- contracts/mixnet/src/mixnodes/transactions.rs | 339 ++++------- contracts/mixnet/src/rewards/transactions.rs | 16 +- contracts/mixnet/src/support/helpers.rs | 2 +- contracts/mixnet/src/support/tests/mod.rs | 76 ++- 11 files changed, 1255 insertions(+), 242 deletions(-) diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs index 2f1c3cf0e6..a217f91663 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs @@ -886,50 +886,54 @@ mod tests { MixnetQueryMsg::GetSigningNonce { address } => { client.get_signing_nonce(&address.parse().unwrap()).ignore() } - QueryMsg::GetPreassignedGatewayIds { start_after, limit } => client + MixnetQueryMsg::GetPreassignedGatewayIds { start_after, limit } => client .get_preassigned_gateway_ids_paged(start_after, limit) .ignore(), - QueryMsg::GetNymNodeBondsPaged { limit, start_after } => { + MixnetQueryMsg::GetNymNodeBondsPaged { limit, start_after } => { client.get_nymnode_bonds_paged(limit, start_after).ignore() } - QueryMsg::GetNymNodesDetailedPaged { limit, start_after } => client + MixnetQueryMsg::GetNymNodesDetailedPaged { limit, start_after } => client .get_nymnodes_detailed_paged(limit, start_after) .ignore(), - QueryMsg::GetUnbondedNymNode { node_id } => { + MixnetQueryMsg::GetUnbondedNymNode { node_id } => { client.get_unbonded_nymnode_information(node_id).ignore() } - QueryMsg::GetUnbondedNymNodesPaged { limit, start_after } => client + MixnetQueryMsg::GetUnbondedNymNodesPaged { limit, start_after } => client .get_unbonded_nymnodes_paged(limit, start_after) .ignore(), - QueryMsg::GetUnbondedNymNodesByOwnerPaged { + MixnetQueryMsg::GetUnbondedNymNodesByOwnerPaged { owner, limit, start_after, } => client .get_unbonded_nymnodes_by_owner_paged(&owner.parse().unwrap(), limit, start_after) .ignore(), - QueryMsg::GetUnbondedNymNodesByIdentityKeyPaged { + MixnetQueryMsg::GetUnbondedNymNodesByIdentityKeyPaged { identity_key, limit, start_after, } => client .get_unbonded_nymnodes_by_identity_paged(&identity_key, limit, start_after) .ignore(), - QueryMsg::GetOwnedNymNode { address } => { + MixnetQueryMsg::GetOwnedNymNode { address } => { client.get_owned_nymnode(&address.parse().unwrap()).ignore() } - QueryMsg::GetNymNodeDetails { node_id } => client.get_nymnode_details(node_id).ignore(), - QueryMsg::GetNymNodeDetailsByIdentityKey { node_identity } => client + MixnetQueryMsg::GetNymNodeDetails { node_id } => { + client.get_nymnode_details(node_id).ignore() + } + MixnetQueryMsg::GetNymNodeDetailsByIdentityKey { node_identity } => client .get_nymnode_details_by_identity(node_identity) .ignore(), - QueryMsg::GetNodeRewardingDetails { node_id } => { + MixnetQueryMsg::GetNodeRewardingDetails { node_id } => { client.get_nymnode_rewarding_details(node_id).ignore() } - QueryMsg::GetNodeStakeSaturation { node_id } => { + MixnetQueryMsg::GetNodeStakeSaturation { node_id } => { client.get_node_stake_saturation(node_id).ignore() } - QueryMsg::GetRoleAssignment { role } => client.get_role_assignment(role).ignore(), - QueryMsg::GetRewardedSetMetadata {} => client.get_rewarded_set_metadata().ignore(), + MixnetQueryMsg::GetRoleAssignment { role } => client.get_role_assignment(role).ignore(), + MixnetQueryMsg::GetRewardedSetMetadata {} => { + client.get_rewarded_set_metadata().ignore() + } } } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index 39a81b93c5..5e7570714d 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -672,6 +672,7 @@ pub enum QueryMsg { // delegation-related: /// Gets all delegations associated with particular node #[cfg_attr(feature = "schema", returns(PagedNodeDelegationsResponse))] + #[serde(alias = "GetMixnodeDelegations", alias = "get_mixnode_delegations")] GetNodeDelegations { /// Id of the node to query. #[serde(alias = "mix_id")] diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index 35aac81f53..f695bb22f7 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -166,6 +166,10 @@ impl RangedValue where T: Copy + PartialOrd + PartialEq, { + pub fn new(minimum: T, maximum: T) -> Self { + RangedValue { minimum, maximum } + } + pub fn normalise(&self, value: T) -> T { if value < self.minimum { self.minimum diff --git a/contracts/mixnet/src/compat/helpers.rs b/contracts/mixnet/src/compat/helpers.rs index 0f57650eaf..0664bed06b 100644 --- a/contracts/mixnet/src/compat/helpers.rs +++ b/contracts/mixnet/src/compat/helpers.rs @@ -142,3 +142,436 @@ pub fn may_get_bond( Ok(None) } } + +#[cfg(test)] +mod tests { + use super::*; + use anyhow::bail; + + #[cfg(test)] + mod reward_withdrawing_permission { + use super::*; + use crate::support::tests::test_helpers::TestSetup; + + #[test] + fn for_legacy_mixnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let node_id = test.add_legacy_mixnode("owner", None); + let details = test.mixnode_by_id(node_id).unwrap(); + + // node must not be in the process of unbonding + assert!(ensure_can_withdraw_rewards(&details).is_ok()); + + test.start_unbonding_mixnode(node_id); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_withdraw_rewards(&details).unwrap_err(); + assert_eq!(res, MixnetContractError::NodeIsUnbonding { node_id }); + Ok(()) + } + + #[test] + fn for_nymnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("owner", None); + let details = test.nymnode_by_id(node_id).unwrap(); + + // node must not be in the process of unbonding + assert!(ensure_can_withdraw_rewards(&details).is_ok()); + + test.start_unbonding_nymnode(node_id); + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_withdraw_rewards(&details).unwrap_err(); + assert_eq!(res, MixnetContractError::NodeIsUnbonding { node_id }); + Ok(()) + } + } + + #[cfg(test)] + mod modifying_cost_params_permission { + use super::*; + use crate::support::tests::test_helpers::TestSetup; + + #[test] + fn for_legacy_mixnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let node_id = test.add_legacy_mixnode("owner", None); + + let details = test.mixnode_by_id(node_id).unwrap(); + assert!(ensure_can_modify_cost_params(test.deps().storage, &details).is_ok()); + + // epoch must not be mid-transition + test.skip_to_current_epoch_end(); + test.start_epoch_transition(); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_modify_cost_params(test.deps().storage, &details).unwrap_err(); + assert!(matches!( + res, + MixnetContractError::EpochAdvancementInProgress { .. } + )); + test.set_epoch_in_progress_state(); + + // node must not be in the process of unbonding + test.start_unbonding_mixnode(node_id); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_modify_cost_params(test.deps().storage, &details).unwrap_err(); + assert_eq!(res, MixnetContractError::NodeIsUnbonding { node_id }); + + Ok(()) + } + + #[test] + fn for_nymnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("owner", None); + + let details = test.nymnode_by_id(node_id).unwrap(); + assert!(ensure_can_modify_cost_params(test.deps().storage, &details).is_ok()); + + // epoch must not be mid-transition + test.skip_to_current_epoch_end(); + test.start_epoch_transition(); + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_modify_cost_params(test.deps().storage, &details).unwrap_err(); + assert!(matches!( + res, + MixnetContractError::EpochAdvancementInProgress { .. } + )); + test.set_epoch_in_progress_state(); + + // node must not be in the process of unbonding + test.start_unbonding_nymnode(node_id); + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_modify_cost_params(test.deps().storage, &details).unwrap_err(); + assert_eq!(res, MixnetContractError::NodeIsUnbonding { node_id }); + + Ok(()) + } + } + + #[cfg(test)] + mod increasing_pledge_permission { + use super::*; + use crate::compat::transactions::{try_decrease_pledge, try_increase_pledge}; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::testing::mock_info; + + #[test] + fn for_legacy_mixnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let node_id = test.add_legacy_mixnode("owner", None); + + let details = test.mixnode_by_id(node_id).unwrap(); + assert!(ensure_can_increase_pledge(test.deps().storage, &details).is_ok()); + + // epoch must not be mid-transition + test.skip_to_current_epoch_end(); + test.start_epoch_transition(); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_increase_pledge(test.deps().storage, &details).unwrap_err(); + assert!(matches!( + res, + MixnetContractError::EpochAdvancementInProgress { .. } + )); + test.set_epoch_in_progress_state(); + + // node must not be in the process of unbonding + test.start_unbonding_mixnode(node_id); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_increase_pledge(test.deps().storage, &details).unwrap_err(); + assert_eq!(res, MixnetContractError::NodeIsUnbonding { node_id }); + + // node can't have any pending pledge changes: + // - increase + let node_id = test.add_legacy_mixnode("owner2", Some(100_000_000_000u128.into())); + let pledge_change = test.coin(100000); + test.execute_fn(try_increase_pledge, mock_info("owner2", &[pledge_change]))?; + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_increase_pledge(test.deps().storage, &details).unwrap_err(); + assert_eq!( + res, + MixnetContractError::PendingPledgeChange { + pending_event_id: details.pending_changes.pledge_change.unwrap() + } + ); + + // - decrease + let node_id = test.add_legacy_mixnode("owner3", Some(100_000_000_000u128.into())); + let pledge_change = test.coin(100000); + let env = test.env(); + try_decrease_pledge( + test.deps_mut(), + env, + mock_info("owner3", &[]), + pledge_change, + )?; + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_increase_pledge(test.deps().storage, &details).unwrap_err(); + assert_eq!( + res, + MixnetContractError::PendingPledgeChange { + pending_event_id: details.pending_changes.pledge_change.unwrap() + } + ); + Ok(()) + } + + #[test] + fn for_nymnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("owner", None); + + let details = test.nymnode_by_id(node_id).unwrap(); + assert!(ensure_can_increase_pledge(test.deps().storage, &details).is_ok()); + + // epoch must not be mid-transition + test.skip_to_current_epoch_end(); + test.start_epoch_transition(); + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_increase_pledge(test.deps().storage, &details).unwrap_err(); + assert!(matches!( + res, + MixnetContractError::EpochAdvancementInProgress { .. } + )); + test.set_epoch_in_progress_state(); + + // node must not be in the process of unbonding + test.start_unbonding_nymnode(node_id); + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_increase_pledge(test.deps().storage, &details).unwrap_err(); + assert_eq!(res, MixnetContractError::NodeIsUnbonding { node_id }); + + // node can't have any pending pledge changes: + // - increase + let node_id = test.add_dummy_nymnode("owner2", Some(100_000_000_000u128.into())); + let pledge_change = test.coin(100000); + test.execute_fn(try_increase_pledge, mock_info("owner2", &[pledge_change]))?; + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_increase_pledge(test.deps().storage, &details).unwrap_err(); + assert_eq!( + res, + MixnetContractError::PendingPledgeChange { + pending_event_id: details.pending_changes.pledge_change.unwrap() + } + ); + + // - decrease + let node_id = test.add_dummy_nymnode("owner3", Some(100_000_000_000u128.into())); + let pledge_change = test.coin(100000); + let env = test.env(); + try_decrease_pledge( + test.deps_mut(), + env, + mock_info("owner3", &[]), + pledge_change, + )?; + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_increase_pledge(test.deps().storage, &details).unwrap_err(); + assert_eq!( + res, + MixnetContractError::PendingPledgeChange { + pending_event_id: details.pending_changes.pledge_change.unwrap() + } + ); + Ok(()) + } + } + + #[cfg(test)] + mod decreasing_pledge_permission { + use super::*; + use crate::compat::transactions::{try_decrease_pledge, try_increase_pledge}; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::coin; + use cosmwasm_std::testing::mock_info; + + #[test] + fn for_legacy_mixnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let node_id = test.add_legacy_mixnode("owner", Some(100_000_000_000u128.into())); + let valid_decrease = test.coin(100); + + let details = test.mixnode_by_id(node_id).unwrap(); + assert!( + ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease).is_ok() + ); + + // epoch must not be mid-transition + test.skip_to_current_epoch_end(); + test.start_epoch_transition(); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease) + .unwrap_err(); + assert!(matches!( + res, + MixnetContractError::EpochAdvancementInProgress { .. } + )); + test.set_epoch_in_progress_state(); + + // node must not be in the process of unbonding + test.start_unbonding_mixnode(node_id); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease) + .unwrap_err(); + assert_eq!(res, MixnetContractError::NodeIsUnbonding { node_id }); + + // node can't have any pending pledge changes: + // - increase + let node_id = test.add_legacy_mixnode("owner2", Some(100_000_000_000u128.into())); + let pledge_change = test.coin(100000); + test.execute_fn(try_increase_pledge, mock_info("owner2", &[pledge_change]))?; + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease) + .unwrap_err(); + assert_eq!( + res, + MixnetContractError::PendingPledgeChange { + pending_event_id: details.pending_changes.pledge_change.unwrap() + } + ); + + // - decrease + let node_id = test.add_legacy_mixnode("owner3", Some(100_000_000_000u128.into())); + let pledge_change = test.coin(100000); + let env = test.env(); + try_decrease_pledge( + test.deps_mut(), + env, + mock_info("owner3", &[]), + pledge_change, + )?; + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease) + .unwrap_err(); + assert_eq!( + res, + MixnetContractError::PendingPledgeChange { + pending_event_id: details.pending_changes.pledge_change.unwrap() + } + ); + + // denom must match + let node_id = test.add_legacy_mixnode("owner4", Some(100_000_000_000u128.into())); + let details = test.mixnode_by_id(node_id).unwrap(); + let bad_decrease = coin(123, "weird-denom"); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &bad_decrease) + .unwrap_err(); + assert!(matches!(res, MixnetContractError::WrongDenom { .. })); + + // value must be non-zero + let node_id = test.add_legacy_mixnode("owner5", Some(100_000_000_000u128.into())); + let details = test.mixnode_by_id(node_id).unwrap(); + let bad_decrease = test.coin(0); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &bad_decrease) + .unwrap_err(); + assert_eq!(res, MixnetContractError::ZeroCoinAmount); + + // new pledge must be bigger than minimum + let node_id = test.add_legacy_mixnode("owner6", Some(100_000_100u128.into())); + let details = test.mixnode_by_id(node_id).unwrap(); + let bad_decrease = test.coin(101); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &bad_decrease) + .unwrap_err(); + assert!(matches!( + res, + MixnetContractError::InvalidPledgeReduction { .. } + )); + + Ok(()) + } + + #[test] + fn for_nymnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("owner", Some(100_000_000_000u128.into())); + let valid_decrease = test.coin(100); + + let details = test.nymnode_by_id(node_id).unwrap(); + assert!( + ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease).is_ok() + ); + + // epoch must not be mid-transition + test.skip_to_current_epoch_end(); + test.start_epoch_transition(); + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease) + .unwrap_err(); + assert!(matches!( + res, + MixnetContractError::EpochAdvancementInProgress { .. } + )); + test.set_epoch_in_progress_state(); + + // node must not be in the process of unbonding + test.start_unbonding_nymnode(node_id); + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease) + .unwrap_err(); + assert_eq!(res, MixnetContractError::NodeIsUnbonding { node_id }); + + // node can't have any pending pledge changes: + // - increase + let node_id = test.add_dummy_nymnode("owner2", Some(100_000_000_000u128.into())); + let pledge_change = test.coin(100000); + test.execute_fn(try_increase_pledge, mock_info("owner2", &[pledge_change]))?; + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease) + .unwrap_err(); + assert_eq!( + res, + MixnetContractError::PendingPledgeChange { + pending_event_id: details.pending_changes.pledge_change.unwrap() + } + ); + + // - decrease + let node_id = test.add_dummy_nymnode("owner3", Some(100_000_000_000u128.into())); + let pledge_change = test.coin(100000); + let env = test.env(); + try_decrease_pledge( + test.deps_mut(), + env, + mock_info("owner3", &[]), + pledge_change, + )?; + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease) + .unwrap_err(); + assert_eq!( + res, + MixnetContractError::PendingPledgeChange { + pending_event_id: details.pending_changes.pledge_change.unwrap() + } + ); + + // denom must match + let node_id = test.add_dummy_nymnode("owner4", Some(100_000_000_000u128.into())); + let details = test.nymnode_by_id(node_id).unwrap(); + let bad_decrease = coin(123, "weird-denom"); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &bad_decrease) + .unwrap_err(); + assert!(matches!(res, MixnetContractError::WrongDenom { .. })); + + // value must be non-zero + let node_id = test.add_dummy_nymnode("owner5", Some(100_000_000_000u128.into())); + let details = test.nymnode_by_id(node_id).unwrap(); + let bad_decrease = test.coin(0); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &bad_decrease) + .unwrap_err(); + assert_eq!(res, MixnetContractError::ZeroCoinAmount); + + // new pledge must be bigger than minimum + let node_id = test.add_dummy_nymnode("owner6", Some(100_000_100u128.into())); + let details = test.nymnode_by_id(node_id).unwrap(); + let bad_decrease = test.coin(101); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &bad_decrease) + .unwrap_err(); + assert!(matches!( + res, + MixnetContractError::InvalidPledgeReduction { .. } + )); + + Ok(()) + } + } +} diff --git a/contracts/mixnet/src/compat/transactions.rs b/contracts/mixnet/src/compat/transactions.rs index dbedab1325..77b20da946 100644 --- a/contracts/mixnet/src/compat/transactions.rs +++ b/contracts/mixnet/src/compat/transactions.rs @@ -93,3 +93,571 @@ pub(crate) fn try_withdraw_operator_reward( Err(MixnetContractError::NoAssociatedNodeBond { owner: info.sender }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(test)] + mod increasing_pledge { + use super::*; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::Addr; + + #[test] + fn when_there_are_no_nodes() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let sender = mock_info("owner", &[test.coin(100000)]); + let err = test.execute_fn(try_increase_pledge, sender).unwrap_err(); + + assert_eq!( + MixnetContractError::NoAssociatedNodeBond { + owner: Addr::unchecked("owner"), + }, + err + ); + + Ok(()) + } + + #[test] + fn for_legacy_mixnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let node_id = test.add_legacy_mixnode("owner", Some(100_000_000u128.into())); + let sender = mock_info("owner", &[test.coin(100_000)]); + test.assert_simple_execution(try_increase_pledge, sender); + + let after = test.mixnode_by_id(node_id).unwrap(); + let event_id = after.pending_changes.pledge_change.unwrap(); + assert_eq!(event_id, 1); + + test.execute_all_pending_events(); + + let after = test.mixnode_by_id(node_id).unwrap(); + assert_eq!( + after.bond_information.original_pledge.amount.u128(), + 100_100_000u128 + ); + + Ok(()) + } + + #[test] + fn for_legacy_gateway() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + test.add_legacy_gateway("owner", None); + let sender = mock_info("owner", &[test.coin(100000)]); + let err = test.execute_fn(try_increase_pledge, sender).unwrap_err(); + + // it's illegal to increase pledge for legacy gateways + assert_eq!( + MixnetContractError::NoAssociatedNodeBond { + owner: Addr::unchecked("owner"), + }, + err + ); + + Ok(()) + } + + #[test] + fn for_nymnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let node_id = test.add_dummy_nymnode("owner", Some(100_000_000u128.into())); + let sender = mock_info("owner", &[test.coin(100_000)]); + test.assert_simple_execution(try_increase_pledge, sender); + + let after = test.nymnode_by_id(node_id).unwrap(); + let event_id = after.pending_changes.pledge_change.unwrap(); + assert_eq!(event_id, 1); + + test.execute_all_pending_events(); + + let after = test.nymnode_by_id(node_id).unwrap(); + assert_eq!( + after.bond_information.original_pledge.amount.u128(), + 100_100_000u128 + ); + + Ok(()) + } + } + + #[cfg(test)] + mod decreasing_pledge { + use super::*; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::Addr; + + #[test] + fn when_there_are_no_nodes() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let sender = mock_info("owner", &[]); + let env = test.env(); + let decrease_by = test.coin(1000); + let err = try_decrease_pledge(test.deps_mut(), env, sender, decrease_by).unwrap_err(); + + assert_eq!( + MixnetContractError::NoAssociatedNodeBond { + owner: Addr::unchecked("owner"), + }, + err + ); + + Ok(()) + } + + #[test] + fn for_legacy_mixnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let node_id = test.add_legacy_mixnode("owner", Some(120_000_000u128.into())); + let sender = mock_info("owner", &[]); + let env = test.env(); + let decrease_by = test.coin(1000); + try_decrease_pledge(test.deps_mut(), env, sender, decrease_by)?; + + let after = test.mixnode_by_id(node_id).unwrap(); + let event_id = after.pending_changes.pledge_change.unwrap(); + assert_eq!(event_id, 1); + + test.execute_all_pending_events(); + + let after = test.mixnode_by_id(node_id).unwrap(); + assert_eq!( + after.bond_information.original_pledge.amount.u128(), + 119_999_000u128 + ); + + Ok(()) + } + + #[test] + fn for_legacy_gateway() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + test.add_legacy_gateway("owner", None); + let sender = mock_info("owner", &[]); + let env = test.env(); + let decrease_by = test.coin(1000); + let err = try_decrease_pledge(test.deps_mut(), env, sender, decrease_by).unwrap_err(); + + // it's illegal to decrease pledge for legacy gateways + assert_eq!( + MixnetContractError::NoAssociatedNodeBond { + owner: Addr::unchecked("owner"), + }, + err + ); + + Ok(()) + } + + #[test] + fn for_nymnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let node_id = test.add_dummy_nymnode("owner", Some(120_000_000u128.into())); + let sender = mock_info("owner", &[]); + let env = test.env(); + let decrease_by = test.coin(1000); + + try_decrease_pledge(test.deps_mut(), env, sender, decrease_by)?; + + let after = test.nymnode_by_id(node_id).unwrap(); + let event_id = after.pending_changes.pledge_change.unwrap(); + assert_eq!(event_id, 1); + + test.execute_all_pending_events(); + + let after = test.nymnode_by_id(node_id).unwrap(); + assert_eq!( + after.bond_information.original_pledge.amount.u128(), + 119_999_000u128 + ); + + Ok(()) + } + } + + #[cfg(test)] + mod updating_cost_params { + use super::*; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::{Addr, Uint128}; + use mixnet_contract_common::{OperatingCostRange, ProfitMarginRange}; + use nym_contracts_common::Percent; + + fn new_dummy_params() -> NodeCostParams { + NodeCostParams { + profit_margin_percent: Percent::from_percentage_value(69).unwrap(), + interval_operating_cost: Coin { + denom: TEST_COIN_DENOM.to_string(), + amount: 123456789u128.into(), + }, + } + } + + #[test] + fn profit_margin_must_be_within_range() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let minimum = Percent::from_percentage_value(10)?; + let maximum = Percent::from_percentage_value(80)?; + let range = ProfitMarginRange::new(minimum, maximum); + test.update_profit_margin_range(range); + + // below lower + test.add_dummy_nymnode("owner1", None); + let sender = mock_info("owner1", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.profit_margin_percent = Percent::from_percentage_value(9)?; + let err = + try_update_cost_params(test.deps_mut(), env, sender, update.clone()).unwrap_err(); + assert!(matches!( + err, + MixnetContractError::ProfitMarginOutsideRange { .. } + )); + + // zero + test.add_dummy_nymnode("owner2", None); + let sender = mock_info("owner2", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.profit_margin_percent = Percent::zero(); + let err = + try_update_cost_params(test.deps_mut(), env, sender, update.clone()).unwrap_err(); + assert!(matches!( + err, + MixnetContractError::ProfitMarginOutsideRange { .. } + )); + + // exactly at lower + test.add_dummy_nymnode("owner3", None); + let sender = mock_info("owner3", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.profit_margin_percent = minimum; + let res = try_update_cost_params(test.deps_mut(), env, sender, update.clone()); + assert!(res.is_ok()); + + // above upper + test.add_dummy_nymnode("owner4", None); + let sender = mock_info("owner4", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.profit_margin_percent = Percent::from_percentage_value(81)?; + let err = + try_update_cost_params(test.deps_mut(), env, sender, update.clone()).unwrap_err(); + assert!(matches!( + err, + MixnetContractError::ProfitMarginOutsideRange { .. } + )); + + // hundred + test.add_dummy_nymnode("owner5", None); + let sender = mock_info("owner5", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.profit_margin_percent = Percent::hundred(); + let err = + try_update_cost_params(test.deps_mut(), env, sender, update.clone()).unwrap_err(); + assert!(matches!( + err, + MixnetContractError::ProfitMarginOutsideRange { .. } + )); + + // exactly at upper + test.add_dummy_nymnode("owner6", None); + let sender = mock_info("owner6", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.profit_margin_percent = maximum; + let res = try_update_cost_params(test.deps_mut(), env, sender, update.clone()); + assert!(res.is_ok()); + + Ok(()) + } + + #[test] + fn operating_cost_must_be_within_range() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let minimum = Uint128::new(1000); + let maximum = Uint128::new(100_000_000); + let range = OperatingCostRange::new(minimum, maximum); + test.update_operating_cost_range(range); + + // below lower + test.add_dummy_nymnode("owner1", None); + let sender = mock_info("owner1", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.interval_operating_cost = test.coin(999); + let err = + try_update_cost_params(test.deps_mut(), env, sender, update.clone()).unwrap_err(); + assert!(matches!( + err, + MixnetContractError::OperatingCostOutsideRange { .. } + )); + + // zero + test.add_dummy_nymnode("owner2", None); + let sender = mock_info("owner2", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.interval_operating_cost = test.coin(0); + let err = + try_update_cost_params(test.deps_mut(), env, sender, update.clone()).unwrap_err(); + assert!(matches!( + err, + MixnetContractError::OperatingCostOutsideRange { .. } + )); + + // exactly at lower + test.add_dummy_nymnode("owner3", None); + let sender = mock_info("owner3", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.interval_operating_cost = test.coin(minimum.u128()); + let res = try_update_cost_params(test.deps_mut(), env, sender, update.clone()); + assert!(res.is_ok()); + + // above upper + test.add_dummy_nymnode("owner4", None); + let sender = mock_info("owner4", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.interval_operating_cost = test.coin(100_000_001); + let err = + try_update_cost_params(test.deps_mut(), env, sender, update.clone()).unwrap_err(); + assert!(matches!( + err, + MixnetContractError::OperatingCostOutsideRange { .. } + )); + + // max + test.add_dummy_nymnode("owner5", None); + let sender = mock_info("owner5", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.interval_operating_cost = test.coin(u128::MAX); + let err = + try_update_cost_params(test.deps_mut(), env, sender, update.clone()).unwrap_err(); + assert!(matches!( + err, + MixnetContractError::OperatingCostOutsideRange { .. } + )); + + // exactly at upper + test.add_dummy_nymnode("owner6", None); + let sender = mock_info("owner6", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.interval_operating_cost = test.coin(100_000_000); + let res = try_update_cost_params(test.deps_mut(), env, sender, update.clone()); + assert!(res.is_ok()); + + Ok(()) + } + + #[test] + fn when_there_are_no_nodes() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let sender = mock_info("owner", &[]); + let env = test.env(); + let err = try_update_cost_params(test.deps_mut(), env, sender, new_dummy_params()) + .unwrap_err(); + + assert_eq!( + MixnetContractError::NoAssociatedNodeBond { + owner: Addr::unchecked("owner"), + }, + err + ); + + Ok(()) + } + + #[test] + fn for_legacy_mixnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let node_id = test.add_legacy_mixnode("owner", None); + let sender = mock_info("owner", &[]); + let env = test.env(); + + let update = new_dummy_params(); + try_update_cost_params(test.deps_mut(), env, sender, update.clone())?; + + let after = test.mixnode_by_id(node_id).unwrap(); + let event_id = after.pending_changes.cost_params_change.unwrap(); + assert_eq!(event_id, 1); + + test.execute_all_pending_events(); + + let after = test.mixnode_by_id(node_id).unwrap(); + assert_eq!(update, after.rewarding_details.cost_params); + + Ok(()) + } + + #[test] + fn for_legacy_gateway() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + test.add_legacy_gateway("owner", None); + + let sender = mock_info("owner", &[]); + let env = test.env(); + let err = try_update_cost_params(test.deps_mut(), env, sender, new_dummy_params()) + .unwrap_err(); + + // it's illegal to update cost parameters for legacy gateways + assert_eq!( + MixnetContractError::NoAssociatedNodeBond { + owner: Addr::unchecked("owner"), + }, + err + ); + Ok(()) + } + + #[test] + fn for_nymnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let node_id = test.add_dummy_nymnode("owner", None); + let sender = mock_info("owner", &[]); + let env = test.env(); + + let update = new_dummy_params(); + try_update_cost_params(test.deps_mut(), env, sender, update.clone())?; + + let after = test.nymnode_by_id(node_id).unwrap(); + let event_id = after.pending_changes.cost_params_change.unwrap(); + assert_eq!(event_id, 1); + + test.execute_all_pending_events(); + + let after = test.nymnode_by_id(node_id).unwrap(); + assert_eq!(update, after.rewarding_details.cost_params); + + Ok(()) + } + } + + #[cfg(test)] + mod withdrawing_operator_reward { + use super::*; + use crate::support::tests::test_helpers; + use crate::support::tests::test_helpers::{ExtractBankMsg, TestSetup}; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::Addr; + + #[test] + fn when_there_are_no_nodes() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let sender = mock_info("owner", &[]); + let err = try_withdraw_operator_reward(test.deps_mut(), sender).unwrap_err(); + + assert_eq!( + MixnetContractError::NoAssociatedNodeBond { + owner: Addr::unchecked("owner"), + }, + err + ); + + Ok(()) + } + + #[test] + fn for_legacy_mixnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + // no rewards + test.add_legacy_mixnode("owner1", None); + let sender = mock_info("owner1", &[]); + + let res = try_withdraw_operator_reward(test.deps_mut(), sender)?; + let maybe_bank = res.unwrap_bank_msg(); + assert!(maybe_bank.is_none()); + + let node_id = test.add_legacy_mixnode("owner2", None); + let sender = mock_info("owner2", &[]); + test.skip_to_next_epoch_end(); + test.force_change_rewarded_set(vec![node_id]); + test.start_epoch_transition(); + test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id, + test_helpers::performance(100.), + ); + + let res = try_withdraw_operator_reward(test.deps_mut(), sender)?; + let maybe_bank = res.unwrap_bank_msg(); + assert!(maybe_bank.is_some()); + + Ok(()) + } + + #[test] + fn for_legacy_gateway() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + test.add_legacy_gateway("owner", None); + + let sender = mock_info("owner", &[]); + let err = try_withdraw_operator_reward(test.deps_mut(), sender).unwrap_err(); + + // no rewards for legacy gateways... + assert_eq!( + MixnetContractError::NoAssociatedNodeBond { + owner: Addr::unchecked("owner"), + }, + err + ); + Ok(()) + } + + #[test] + fn for_nymnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + // no rewards + test.add_dummy_nymnode("owner1", None); + let sender = mock_info("owner1", &[]); + + let res = try_withdraw_operator_reward(test.deps_mut(), sender)?; + let maybe_bank = res.unwrap_bank_msg(); + assert!(maybe_bank.is_none()); + + let node_id = test.add_dummy_nymnode("owner2", None); + let sender = mock_info("owner2", &[]); + test.skip_to_next_epoch_end(); + test.force_change_rewarded_set(vec![node_id]); + test.start_epoch_transition(); + test.legacy_reward_with_distribution_and_legacy_work_factor( + node_id, + test_helpers::performance(100.), + ); + + let res = try_withdraw_operator_reward(test.deps_mut(), sender)?; + let maybe_bank = res.unwrap_bank_msg(); + assert!(maybe_bank.is_some()); + + Ok(()) + } + } +} diff --git a/contracts/mixnet/src/gateways/transactions.rs b/contracts/mixnet/src/gateways/transactions.rs index 5720c48455..da03caaad3 100644 --- a/contracts/mixnet/src/gateways/transactions.rs +++ b/contracts/mixnet/src/gateways/transactions.rs @@ -143,7 +143,7 @@ pub mod tests { use crate::gateways::queries; use crate::interval::pending_events; use crate::mixnet_contract_settings::storage::minimum_node_pledge; - use crate::nodes::helpers::get_node_details_by_identity; + use crate::nodes::helpers::{get_node_details_by_identity, must_get_node_bond_by_owner}; use crate::signing::storage as signing_storage; use crate::support::tests; use crate::support::tests::fixtures::{good_gateway_pledge, good_mixnode_pledge}; @@ -153,7 +153,7 @@ pub mod tests { use mixnet_contract_common::ExecuteMsg; #[test] - fn gateway_add() { + fn gateway_add() -> anyhow::Result<()> { let mut test = TestSetup::new(); // if we fail validation (by say not sending enough funds @@ -218,10 +218,22 @@ pub mod tests { assert!(result.is_ok()); // and the node has been added as a nym-node - let nym_node = get_node_details_by_identity(test.deps().storage, gateway.identity_key) - .unwrap() - .unwrap(); - assert_eq!(nym_node.bond_information.owner, info.sender) + let nym_node = + get_node_details_by_identity(test.deps().storage, gateway.identity_key.clone()) + .unwrap() + .unwrap(); + assert_eq!(nym_node.bond_information.owner, info.sender); + + let maybe_legacy = + storage::gateways().may_load(test.deps().storage, &gateway.identity_key)?; + assert!(maybe_legacy.is_none()); + + // make sure we got assigned the next id (note: we have already bonded a mixnode and a gateway before in this test) + let bond = + must_get_node_bond_by_owner(test.deps().storage, &Addr::unchecked(sender2)).unwrap(); + assert_eq!(3, bond.node_id); + + Ok(()) } #[test] diff --git a/contracts/mixnet/src/mixnet_contract_settings/storage.rs b/contracts/mixnet/src/mixnet_contract_settings/storage.rs index 3897585856..f9024673f2 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/storage.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/storage.rs @@ -34,7 +34,7 @@ pub(crate) fn profit_margin_range( .map(|state| state.params.profit_margin)?) } -pub(crate) fn interval_oprating_cost_range( +pub(crate) fn interval_operating_cost_range( storage: &dyn Storage, ) -> Result { Ok(CONTRACT_STATE diff --git a/contracts/mixnet/src/mixnodes/transactions.rs b/contracts/mixnet/src/mixnodes/transactions.rs index 7b8ed90d04..52fe1a45ed 100644 --- a/contracts/mixnet/src/mixnodes/transactions.rs +++ b/contracts/mixnet/src/mixnodes/transactions.rs @@ -257,12 +257,10 @@ pub fn try_migrate_to_nymnode( #[cfg(test)] pub mod tests { use super::*; - use crate::compat::transactions::{ - try_decrease_pledge, try_increase_pledge, try_update_cost_params, - }; + use crate::compat::transactions::try_increase_pledge; use crate::contract::execute; use crate::mixnet_contract_settings::storage::minimum_node_pledge; - use crate::mixnodes::helpers::get_mixnode_details_by_id; + use crate::mixnodes::helpers::{get_mixnode_details_by_id, get_mixnode_details_by_identity}; use crate::nodes::helpers::{get_node_details_by_identity, must_get_node_bond_by_owner}; use crate::signing::storage as signing_storage; use crate::support::tests::fixtures::{good_mixnode_pledge, TEST_COIN_DENOM}; @@ -275,7 +273,7 @@ pub mod tests { use mixnet_contract_common::{EpochState, EpochStatus, ExecuteMsg, Percent}; #[test] - fn mixnode_add() { + fn mixnode_add() -> anyhow::Result<()> { let mut test = TestSetup::new(); let env = test.env(); @@ -357,15 +355,22 @@ pub mod tests { assert!(result.is_ok()); // and the node has been added as a nym-node - let nym_node = get_node_details_by_identity(test.deps().storage, mixnode.identity_key) - .unwrap() - .unwrap(); + let nym_node = + get_node_details_by_identity(test.deps().storage, mixnode.identity_key.clone()) + .unwrap() + .unwrap(); assert_eq!(nym_node.bond_information.owner, info.sender); + let maybe_legacy = + get_mixnode_details_by_identity(test.deps().storage, mixnode.identity_key)?; + assert!(maybe_legacy.is_none()); + // make sure we got assigned the next id (note: we have already bonded a mixnode and a gateway before in this test) let bond = must_get_node_bond_by_owner(test.deps().storage, &Addr::unchecked(sender2)).unwrap(); assert_eq!(3, bond.node_id); + + Ok(()) } #[test] @@ -524,7 +529,9 @@ pub mod tests { // prior increase let owner = "mix-owner1"; - test.add_legacy_mixnode(owner, None); + let node_id = test.add_legacy_mixnode(owner, None); + let details = test.mixnode_by_id(node_id).unwrap(); + let sender = mock_info(owner, &[test.coin(1000)]); try_increase_pledge(test.deps_mut(), env.clone(), sender.clone()).unwrap(); @@ -538,10 +545,10 @@ pub mod tests { // prior decrease let owner = "mix-owner2"; - test.add_legacy_mixnode(owner, Some(Uint128::new(10000000000))); - let sender = mock_info(owner, &[]); + let node_id = test.add_legacy_mixnode(owner, Some(Uint128::new(10000000000))); + let details = test.mixnode_by_id(node_id).unwrap(); let amount = test.coin(1000); - try_decrease_pledge(test.deps_mut(), env.clone(), sender, amount).unwrap(); + try_decrease_mixnode_pledge(test.deps_mut(), env.clone(), amount, details).unwrap(); let sender = mock_info(owner, &[test.coin(1000)]); let res = try_remove_mixnode(test.deps_mut(), env.clone(), sender); @@ -640,15 +647,20 @@ pub mod tests { let mut test = TestSetup::new(); let env = test.env(); let owner = "alice"; - let info = mock_info(owner, &[]); - test.add_legacy_mixnode(owner, None); + let node_id = test.add_legacy_mixnode(owner, None); + let details = test.mixnode_by_id(node_id).unwrap(); let mut status = EpochStatus::new(test.rewarding_validator().sender); status.state = bad_state; interval_storage::save_current_epoch_status(test.deps_mut().storage, &status).unwrap(); - let res = try_update_cost_params(test.deps_mut(), env.clone(), info, update.clone()); + let res = try_update_mixnode_cost_params( + test.deps_mut(), + env.clone(), + update.clone(), + details, + ); assert!(matches!( res, Err(MixnetContractError::EpochAdvancementInProgress { .. }) @@ -668,21 +680,16 @@ pub mod tests { interval_operating_cost: Coin::new(12345678, TEST_COIN_DENOM), }; - // try updating a non-existing mixnode bond - let res = - try_update_cost_params(test.deps_mut(), env.clone(), info.clone(), update.clone()); - assert_eq!( - res, - Err(MixnetContractError::NoAssociatedNodeBond { - owner: Addr::unchecked(owner) - }) - ); - - let mix_id = test.add_legacy_mixnode(owner, None); + let node_id = test.add_legacy_mixnode(owner, None); + let details = test.mixnode_by_id(node_id).unwrap(); // "normal" update succeeds - let res = - try_update_cost_params(test.deps_mut(), env.clone(), info.clone(), update.clone()); + let res = try_update_mixnode_cost_params( + test.deps_mut(), + env.clone(), + update.clone(), + details.clone(), + ); assert!(res.is_ok()); // see if the event has been pushed onto the queue @@ -695,7 +702,7 @@ pub mod tests { assert_eq!(1, event.0); assert_eq!( PendingIntervalEventKind::ChangeMixCostParams { - mix_id, + mix_id: node_id, new_costs: update.clone(), }, event.1.kind @@ -705,18 +712,16 @@ pub mod tests { test_helpers::execute_all_pending_events(test.deps_mut(), env.clone()); // and see if the config has actually been updated - let mix = get_mixnode_details_by_id(test.deps().storage, mix_id) + let mix = get_mixnode_details_by_id(test.deps().storage, node_id) .unwrap() .unwrap(); assert_eq!(mix.rewarding_details.cost_params, update); // but we cannot perform any updates whilst the mixnode is already unbonding try_remove_mixnode(test.deps_mut(), env.clone(), info.clone()).unwrap(); - let res = try_update_cost_params(test.deps_mut(), env, info, update); - assert_eq!( - res, - Err(MixnetContractError::NodeIsUnbonding { node_id: mix_id }) - ) + let details = test.mixnode_by_id(node_id).unwrap(); + let res = try_update_mixnode_cost_params(test.deps_mut(), env, update, details); + assert_eq!(res, Err(MixnetContractError::NodeIsUnbonding { node_id })) } #[test] @@ -796,15 +801,15 @@ pub mod tests { let env = test.env(); let owner = "mix-owner"; - test.add_legacy_mixnode(owner, None); - let mut status = EpochStatus::new(test.rewarding_validator().sender); status.state = bad_state; interval_storage::save_current_epoch_status(test.deps_mut().storage, &status) .unwrap(); - let sender = mock_info(owner, &[test.coin(1000)]); - let res = try_increase_pledge(test.deps_mut(), env, sender); + let node_id = test.add_legacy_mixnode(owner, None); + let details = test.mixnode_by_id(node_id).unwrap(); + let increase = test.coins(1000); + let res = try_increase_mixnode_pledge(test.deps_mut(), env, increase, details); assert!(matches!( res, @@ -814,38 +819,21 @@ pub mod tests { } #[test] - fn is_not_allowed_if_account_doesnt_own_mixnode() { + fn is_not_allowed_if_mixnode_has_unbonded() { let mut test = TestSetup::new(); let env = test.env(); - let sender = mock_info("not-mix-owner", &[]); - - let res = try_increase_pledge(test.deps_mut(), env, sender); - assert_eq!( - res, - Err(MixnetContractError::NoAssociatedNodeBond { - owner: Addr::unchecked("not-mix-owner") - }) - ) - } - - #[test] - fn is_not_allowed_if_mixnode_has_unbonded_or_is_unbonding() { - let mut test = TestSetup::new(); - let env = test.env(); - - // TODO: I dislike this cross-test access, but it provides us with exactly what we need - // perhaps it should be refactored a bit? - let owner_unbonding = Addr::unchecked(OWNER_UNBONDING); - let owner_unbonded = Addr::unchecked(OWNER_UNBONDED); - let owner_unbonded_leftover = Addr::unchecked(OWNER_UNBONDED_LEFTOVER); let ids = setup_mix_combinations(&mut test, None); let mix_id_unbonding = ids[1].mix_id; - let res = try_increase_pledge( + let increase = test.coins(1000); + let details = test.mixnode_by_id(mix_id_unbonding).unwrap(); + + let res = try_increase_mixnode_pledge( test.deps_mut(), env.clone(), - mock_info(owner_unbonding.as_str(), &[]), + increase.clone(), + details, ); assert_eq!( res, @@ -853,32 +841,6 @@ pub mod tests { node_id: mix_id_unbonding }) ); - - // if the nodes are gone we treat them as tey never existed in the first place - // (regardless of if there's some leftover data) - let res = try_increase_pledge( - test.deps_mut(), - env.clone(), - mock_info(owner_unbonded_leftover.as_str(), &[]), - ); - assert_eq!( - res, - Err(MixnetContractError::NoAssociatedNodeBond { - owner: owner_unbonded_leftover - }) - ); - - let res = try_increase_pledge( - test.deps_mut(), - env, - mock_info(owner_unbonded.as_str(), &[]), - ); - assert_eq!( - res, - Err(MixnetContractError::NoAssociatedNodeBond { - owner: owner_unbonded - }) - ) } #[test] @@ -887,14 +849,21 @@ pub mod tests { let env = test.env(); let owner = "mix-owner"; - test.add_legacy_mixnode(owner, None); + let node_id = test.add_legacy_mixnode(owner, None); + let details = test.mixnode_by_id(node_id).unwrap(); - let sender_empty = mock_info(owner, &[]); - let res = try_increase_pledge(test.deps_mut(), env.clone(), sender_empty); + let sender_empty = Vec::new(); + let res = try_increase_mixnode_pledge( + test.deps_mut(), + env.clone(), + sender_empty, + details.clone(), + ); assert_eq!(res, Err(MixnetContractError::NoBondFound)); - let sender_zero = mock_info(owner, &[test.coin(0)]); - let res = try_increase_pledge(test.deps_mut(), env, sender_zero); + let sender_zero = test.coins(0); + let res = + try_increase_mixnode_pledge(test.deps_mut(), env, sender_zero, details.clone()); assert_eq!( res, Err(MixnetContractError::InsufficientPledge { @@ -911,11 +880,19 @@ pub mod tests { // prior increase let owner = "mix-owner1"; - test.add_legacy_mixnode(owner, None); - let sender = mock_info(owner, &[test.coin(1000)]); - try_increase_pledge(test.deps_mut(), env.clone(), sender.clone()).unwrap(); + let node_id = test.add_legacy_mixnode(owner, None); + let details = test.mixnode_by_id(node_id).unwrap(); + let sender = test.coins(1000); + try_increase_mixnode_pledge( + test.deps_mut(), + env.clone(), + sender.clone(), + details.clone(), + ) + .unwrap(); - let res = try_increase_pledge(test.deps_mut(), env.clone(), sender); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = try_increase_mixnode_pledge(test.deps_mut(), env.clone(), sender, details); assert_eq!( res, Err(MixnetContractError::PendingPledgeChange { @@ -925,39 +902,22 @@ pub mod tests { // prior decrease let owner = "mix-owner2"; - test.add_legacy_mixnode(owner, Some(Uint128::new(10000000000))); - let sender = mock_info(owner, &[]); - let amount = test.coin(1000); - try_decrease_pledge(test.deps_mut(), env.clone(), sender, amount).unwrap(); + let node_id = test.add_legacy_mixnode(owner, Some(Uint128::new(10000000000))); + let details = test.mixnode_by_id(node_id).unwrap(); - let sender = mock_info(owner, &[test.coin(1000)]); - let res = try_increase_pledge(test.deps_mut(), env.clone(), sender); + let amount = test.coin(1000); + try_decrease_mixnode_pledge(test.deps_mut(), env.clone(), amount, details.clone()) + .unwrap(); + + let sender = test.coins(10000); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = try_increase_mixnode_pledge(test.deps_mut(), env.clone(), sender, details); assert_eq!( res, Err(MixnetContractError::PendingPledgeChange { pending_event_id: 2 }) ); - - // artificial event - let owner = "mix-owner3"; - let mix_id = test.add_legacy_mixnode(owner, None); - let pending_change = PendingMixNodeChanges { - pledge_change: Some(1234), - cost_params_change: None, - }; - storage::PENDING_MIXNODE_CHANGES - .save(test.deps_mut().storage, mix_id, &pending_change) - .unwrap(); - - let sender = mock_info(owner, &[test.coin(1000)]); - let res = try_increase_pledge(test.deps_mut(), env, sender); - assert_eq!( - res, - Err(MixnetContractError::PendingPledgeChange { - pending_event_id: 1234 - }) - ); } #[test] @@ -966,12 +926,13 @@ pub mod tests { let env = test.env(); let owner = "mix-owner"; let mix_id = test.add_legacy_mixnode(owner, None); + let details = test.mixnode_by_id(mix_id).unwrap(); let events = test.pending_epoch_events(); assert!(events.is_empty()); - let sender = mock_info(owner, &[test.coin(1000)]); - try_increase_pledge(test.deps_mut(), env, sender).unwrap(); + let sender = test.coins(1000); + try_increase_mixnode_pledge(test.deps_mut(), env, sender, details).unwrap(); let events = test.pending_epoch_events(); @@ -1012,15 +973,15 @@ pub mod tests { let owner = "mix-owner"; let decrease = test.coin(1000); - test.add_legacy_mixnode(owner, Some(Uint128::new(100_000_000_000))); + let node_id = test.add_legacy_mixnode(owner, Some(Uint128::new(100_000_000_000))); + let details = test.mixnode_by_id(node_id).unwrap(); let mut status = EpochStatus::new(test.rewarding_validator().sender); status.state = bad_state; interval_storage::save_current_epoch_status(test.deps_mut().storage, &status) .unwrap(); - let sender = mock_info(owner, &[]); - let res = try_decrease_pledge(test.deps_mut(), env, sender, decrease); + let res = try_decrease_mixnode_pledge(test.deps_mut(), env, decrease, details); assert!(matches!( res, @@ -1030,44 +991,21 @@ pub mod tests { } #[test] - fn is_not_allowed_if_account_doesnt_own_mixnode() { - let mut test = TestSetup::new(); - let env = test.env(); - let sender = mock_info("not-mix-owner", &[]); - let decrease = test.coin(1000); - - let res = try_decrease_pledge(test.deps_mut(), env, sender, decrease); - assert_eq!( - res, - Err(MixnetContractError::NoAssociatedNodeBond { - owner: Addr::unchecked("not-mix-owner") - }) - ) - } - - #[test] - fn is_not_allowed_if_mixnode_has_unbonded_or_is_unbonding() { + fn is_not_allowed_if_mixnode_is_unbonding() { let mut test = TestSetup::new(); let env = test.env(); - // 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); - - // TODO: I dislike this cross-test access, but it provides us with exactly what we need - // perhaps it should be refactored a bit? - let owner_unbonding = Addr::unchecked(OWNER_UNBONDING); - let owner_unbonded = Addr::unchecked(OWNER_UNBONDED); - let owner_unbonded_leftover = Addr::unchecked(OWNER_UNBONDED_LEFTOVER); - - let ids = setup_mix_combinations(&mut test, Some(stake)); + let ids = setup_mix_combinations(&mut test, None); let mix_id_unbonding = ids[1].mix_id; - let res = try_decrease_pledge( + let decrease = test.coin(1000); + let details = test.mixnode_by_id(mix_id_unbonding).unwrap(); + + let res = try_decrease_mixnode_pledge( test.deps_mut(), env.clone(), - mock_info(owner_unbonding.as_str(), &[]), decrease.clone(), + details, ); assert_eq!( res, @@ -1075,34 +1013,6 @@ pub mod tests { node_id: mix_id_unbonding }) ); - - // if the nodes are gone we treat them as tey never existed in the first place - // (regardless of if there's some leftover data) - let res = try_decrease_pledge( - test.deps_mut(), - env.clone(), - mock_info(owner_unbonded_leftover.as_str(), &[]), - decrease.clone(), - ); - assert_eq!( - res, - Err(MixnetContractError::NoAssociatedNodeBond { - owner: owner_unbonded_leftover - }) - ); - - let res = try_decrease_pledge( - test.deps_mut(), - env, - mock_info(owner_unbonded.as_str(), &[]), - decrease, - ); - assert_eq!( - res, - Err(MixnetContractError::NoAssociatedNodeBond { - owner: owner_unbonded - }) - ) } #[test] @@ -1114,17 +1024,17 @@ pub mod tests { let minimum_pledge = minimum_node_pledge(test.deps().storage).unwrap(); let pledge_amount = minimum_pledge.amount + Uint128::new(100); let pledged = test.coin(pledge_amount.u128()); - test.add_legacy_mixnode(owner, Some(pledge_amount)); + let node_id = test.add_legacy_mixnode(owner, Some(pledge_amount)); + let details = test.mixnode_by_id(node_id).unwrap(); let invalid_decrease = test.coin(150); let valid_decrease = test.coin(50); - let sender = mock_info(owner, &[]); - let res = try_decrease_pledge( + let res = try_decrease_mixnode_pledge( test.deps_mut(), env.clone(), - sender.clone(), invalid_decrease.clone(), + details.clone(), ); assert_eq!( res, @@ -1136,7 +1046,7 @@ pub mod tests { }) ); - let res = try_decrease_pledge(test.deps_mut(), env, sender, valid_decrease); + let res = try_decrease_mixnode_pledge(test.deps_mut(), env, valid_decrease, details); assert!(res.is_ok()) } @@ -1149,10 +1059,10 @@ pub mod tests { let decrease = test.coin(0); let owner = "mix-owner"; - test.add_legacy_mixnode(owner, Some(stake)); + let node_id = test.add_legacy_mixnode(owner, Some(stake)); + let details = test.mixnode_by_id(node_id).unwrap(); - let sender = mock_info(owner, &[]); - let res = try_decrease_pledge(test.deps_mut(), env, sender, decrease); + let res = try_decrease_mixnode_pledge(test.deps_mut(), env, decrease, details); assert_eq!(res, Err(MixnetContractError::ZeroCoinAmount)) } @@ -1165,11 +1075,20 @@ pub mod tests { // prior increase let owner = "mix-owner1"; - test.add_legacy_mixnode(owner, Some(stake)); - let sender = mock_info(owner, &[test.coin(1000)]); - try_increase_pledge(test.deps_mut(), env.clone(), sender.clone()).unwrap(); + let node_id = test.add_legacy_mixnode(owner, Some(stake)); + let details = test.mixnode_by_id(node_id).unwrap(); - let res = try_decrease_pledge(test.deps_mut(), env.clone(), sender, decrease.clone()); + let sender = test.coins(1000); + try_increase_mixnode_pledge(test.deps_mut(), env.clone(), sender.clone(), details) + .unwrap(); + + let details = test.mixnode_by_id(node_id).unwrap(); + let res = try_decrease_mixnode_pledge( + test.deps_mut(), + env.clone(), + decrease.clone(), + details, + ); assert_eq!( res, Err(MixnetContractError::PendingPledgeChange { @@ -1179,13 +1098,18 @@ pub mod tests { // prior decrease let owner = "mix-owner2"; - test.add_legacy_mixnode(owner, Some(stake)); - let sender = mock_info(owner, &[]); + let node_id = test.add_legacy_mixnode(owner, Some(stake)); + let details = test.mixnode_by_id(node_id).unwrap(); let amount = test.coin(1000); - try_decrease_pledge(test.deps_mut(), env.clone(), sender, amount).unwrap(); + try_decrease_mixnode_pledge(test.deps_mut(), env.clone(), amount, details).unwrap(); - let sender = mock_info(owner, &[test.coin(1000)]); - let res = try_decrease_pledge(test.deps_mut(), env.clone(), sender, decrease.clone()); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = try_decrease_mixnode_pledge( + test.deps_mut(), + env.clone(), + decrease.clone(), + details, + ); assert_eq!( res, Err(MixnetContractError::PendingPledgeChange { @@ -1204,8 +1128,8 @@ pub mod tests { .save(test.deps_mut().storage, mix_id, &pending_change) .unwrap(); - let sender = mock_info(owner, &[test.coin(1000)]); - let res = try_decrease_pledge(test.deps_mut(), env, sender, decrease); + let details = test.mixnode_by_id(mix_id).unwrap(); + let res = try_decrease_mixnode_pledge(test.deps_mut(), env, decrease, details); assert_eq!( res, Err(MixnetContractError::PendingPledgeChange { @@ -1225,12 +1149,13 @@ pub mod tests { let owner = "mix-owner"; let mix_id = test.add_legacy_mixnode(owner, Some(stake)); + let details = test.mixnode_by_id(mix_id).unwrap(); let events = test.pending_epoch_events(); assert!(events.is_empty()); let sender = mock_info(owner, &[]); - try_decrease_pledge(test.deps_mut(), env, sender, decrease.clone()).unwrap(); + try_decrease_mixnode_pledge(test.deps_mut(), env, decrease.clone(), details).unwrap(); let events = test.pending_epoch_events(); diff --git a/contracts/mixnet/src/rewards/transactions.rs b/contracts/mixnet/src/rewards/transactions.rs index c5cc5c64d4..21ed0e7b33 100644 --- a/contracts/mixnet/src/rewards/transactions.rs +++ b/contracts/mixnet/src/rewards/transactions.rs @@ -13,7 +13,7 @@ use crate::rewards::helpers::update_and_save_last_rewarded; use crate::rewards::storage::RewardingStorage; use crate::support::helpers::{ ensure_any_node_bonded, ensure_can_advance_epoch, ensure_epoch_in_progress_state, - AttachSendTokens, + AttachSendTokens, }; use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; @@ -149,14 +149,20 @@ pub(crate) fn try_withdraw_nym_node_operator_reward( deps: DepsMut<'_>, node_details: NymNodeDetails, ) -> Result { - ensure_can_withdraw_rewards(&node_details)?; - let node_id = node_details.node_id(); let owner = node_details.bond_information.owner.clone(); - let reward = helpers::withdraw_operator_reward(deps.storage, node_details)?; + ensure_can_withdraw_rewards(&node_details)?; - Ok(Response::new().add_event(new_withdraw_operator_reward_event(&owner, reward, node_id))) + let reward = helpers::withdraw_operator_reward(deps.storage, node_details)?; + 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() { + response = response.send_tokens(&owner, reward.clone()) + } + + Ok(response.add_event(new_withdraw_operator_reward_event(&owner, reward, node_id))) } pub(crate) fn try_withdraw_mixnode_operator_reward( diff --git a/contracts/mixnet/src/support/helpers.rs b/contracts/mixnet/src/support/helpers.rs index 9d8e158e76..8a19de6fa2 100644 --- a/contracts/mixnet/src/support/helpers.rs +++ b/contracts/mixnet/src/support/helpers.rs @@ -301,7 +301,7 @@ 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)?; + let range = mixnet_params_storage::interval_operating_cost_range(storage)?; if !range.within_range(operating_cost.amount) { return Err(MixnetContractError::OperatingCostOutsideRange { denom: operating_cost.denom.clone(), diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index de41b21d43..328c6ef56d 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -16,14 +16,18 @@ pub mod test_helpers { perform_pending_epoch_actions, perform_pending_interval_actions, try_begin_epoch_transition, }; use crate::interval::{pending_events, storage as interval_storage}; + use crate::mixnet_contract_settings::queries::query_contract_settings_params; use crate::mixnet_contract_settings::storage::{ self as mixnet_params_storage, minimum_node_pledge, }; use crate::mixnet_contract_settings::storage::{rewarding_denom, rewarding_validator_address}; + use crate::mixnodes::helpers::get_mixnode_details_by_id; use crate::mixnodes::storage as mixnodes_storage; use crate::mixnodes::storage::mixnode_bonds; use crate::mixnodes::transactions::try_remove_mixnode; - use crate::nodes::helpers::{get_node_details_by_identity, must_get_node_bond_by_owner}; + use crate::nodes::helpers::{ + get_node_details_by_id, get_node_details_by_identity, must_get_node_bond_by_owner, + }; use crate::nodes::storage as nymnodes_storage; use crate::nodes::storage::helpers::RoleStorageBucket; use crate::nodes::storage::rewarded_set::{ACTIVE_ROLES_BUCKET, ROLES, ROLES_METADATA}; @@ -68,10 +72,11 @@ pub mod test_helpers { use mixnet_contract_common::rewarding::simulator::Simulator; use mixnet_contract_common::rewarding::RewardDistribution; use mixnet_contract_common::{ - Delegation, EpochEventId, EpochState, EpochStatus, ExecuteMsg, Gateway, - GatewayBondingPayload, IdentityKey, IdentityKeyRef, InitialRewardingParams, InstantiateMsg, - Interval, MixNode, MixNodeBond, MixnodeBondingPayload, NodeId, NymNode, NymNodeBond, - NymNodeBondingPayload, Percent, RoleAssignment, SignableGatewayBondingMsg, + ContractStateParams, Delegation, EpochEventId, EpochState, EpochStatus, ExecuteMsg, + Gateway, GatewayBondingPayload, IdentityKey, IdentityKeyRef, InitialRewardingParams, + InstantiateMsg, Interval, MixNode, MixNodeBond, MixNodeDetails, MixnodeBondingPayload, + NodeId, NymNode, NymNodeBond, NymNodeBondingPayload, NymNodeDetails, OperatingCostRange, + Percent, ProfitMarginRange, RoleAssignment, SignableGatewayBondingMsg, SignableMixNodeBondingMsg, SignableNymNodeBondingMsg, }; use nym_contracts_common::signing::{ @@ -87,6 +92,23 @@ pub mod test_helpers { use std::str::FromStr; use std::time::Duration; + pub(crate) trait ExtractBankMsg { + fn unwrap_bank_msg(self) -> Option; + } + + impl ExtractBankMsg for Response { + fn unwrap_bank_msg(self) -> Option { + for msg in self.messages { + match msg.msg { + CosmosMsg::Bank(bank_msg) => return Some(bank_msg), + _ => continue, + } + } + + None + } + } + pub enum NodeQueryType { ById(NodeId), ByIdentity(IdentityKey), @@ -168,7 +190,6 @@ pub mod test_helpers { self.env.clone() } - #[allow(unused)] pub fn execute( &mut self, info: MessageInfo, @@ -187,7 +208,6 @@ pub mod test_helpers { self.execute(self.mock_info(sender), msg) } - #[allow(unused)] pub fn execute_fn( &mut self, exec_fn: F, @@ -213,7 +233,6 @@ pub mod test_helpers { self.execute_fn(exec_fn, info) } - #[allow(unused)] #[track_caller] pub fn assert_simple_execution(&mut self, exec_fn: F, info: MessageInfo) -> Response where @@ -239,6 +258,36 @@ pub mod test_helpers { .unwrap_or_else(|err| panic!("{caller} failed with: '{err}' ({err:?})")) } + pub fn update_profit_margin_range(&mut self, range: ProfitMarginRange) { + let current = query_contract_settings_params(self.deps()).unwrap(); + + self.execute( + self.owner(), + ExecuteMsg::UpdateContractStateParams { + updated_parameters: ContractStateParams { + profit_margin: range, + ..current + }, + }, + ) + .unwrap(); + } + + pub fn update_operating_cost_range(&mut self, range: OperatingCostRange) { + let current = query_contract_settings_params(self.deps()).unwrap(); + + self.execute( + self.owner(), + ExecuteMsg::UpdateContractStateParams { + updated_parameters: ContractStateParams { + interval_operating_cost: range, + ..current + }, + }, + ) + .unwrap(); + } + pub fn get_node_id(&self, query_type: impl Into) -> NodeId { match query_type.into() { NodeQueryType::ById(id) => id, @@ -452,6 +501,7 @@ pub mod test_helpers { (bond.node_id, keypair) } + #[track_caller] pub fn add_legacy_mixnode(&mut self, owner: &str, stake: Option) -> NodeId { let stake = self.make_mix_pledge(stake); let (mixnode, _, _) = self.mixnode_with_signature(owner, Some(stake.clone())); @@ -547,6 +597,14 @@ pub mod test_helpers { self.make_node_pledge(stake) } + pub fn mixnode_by_id(&self, node_id: NodeId) -> Option { + get_mixnode_details_by_id(self.deps().storage, node_id).unwrap() + } + + pub fn nymnode_by_id(&self, node_id: NodeId) -> Option { + get_node_details_by_id(self.deps().storage, node_id).unwrap() + } + pub fn mixnode_bonding_signature( &mut self, key: &identity::PrivateKey, @@ -657,6 +715,7 @@ pub mod test_helpers { (gateway, owner_signature) } + #[track_caller] pub fn start_unbonding_mixnode(&mut self, mix_id: NodeId) { let bond_details = mixnodes_storage::mixnode_bonds() .load(self.deps().storage, mix_id) @@ -671,6 +730,7 @@ pub mod test_helpers { .unwrap(); } + #[track_caller] pub fn start_unbonding_nymnode(&mut self, node_id: NodeId) { let bond_details = nymnodes_storage::nym_nodes() .load(self.deps().storage, node_id)