From 32dd987e124cd30b2c66fb27e015c649cda5a80e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 12 Jul 2024 15:26:50 +0100 Subject: [PATCH] implemented migration into non-vesting mixnodes/delegations --- Cargo.lock | 17 ---- .../mixnet-contract/src/error.rs | 14 +++ .../mixnet-contract/src/msg.rs | 9 ++ .../vesting-contract/src/events.rs | 8 ++ .../vesting-contract/src/messages.rs | 12 +++ common/types/src/pending_events.rs | 2 + contracts/mixnet/src/contract.rs | 8 ++ contracts/mixnet/src/lib.rs | 1 + .../src/mixnet_contract_settings/storage.rs | 6 ++ contracts/mixnet/src/vesting_migration.rs | 95 +++++++++++++++++++ contracts/vesting/src/contract.rs | 79 ++------------- .../vesting/src/traits/bonding_account.rs | 4 + .../vesting/src/traits/delegating_account.rs | 5 + contracts/vesting/src/transactions.rs | 34 ++++++- .../src/vesting/account/delegating_account.rs | 21 ++++ .../account/mixnode_bonding_account.rs | 20 ++++ 16 files changed, 243 insertions(+), 92 deletions(-) create mode 100644 contracts/mixnet/src/vesting_migration.rs diff --git a/Cargo.lock b/Cargo.lock index 0bc53266de..0822026fe5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4999,23 +4999,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "nym-network-statistics" -version = "1.1.34" -dependencies = [ - "dirs 4.0.0", - "log", - "nym-bin-common", - "nym-statistics-common", - "nym-task", - "pretty_env_logger", - "rocket", - "serde", - "sqlx", - "thiserror", - "tokio", -] - [[package]] name = "nym-node" version = "1.1.4" diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs index 08a0f2b479..39dab44263 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs @@ -76,6 +76,12 @@ pub enum MixnetContractError { #[error("Received multiple coin types during staking")] MultipleDenoms, + #[error("Proxy address ({received}) is not set to the vesting contract ({vesting_contract})")] + ProxyIsNotVestingContract { + received: Addr, + vesting_contract: Addr, + }, + #[error("Failed to recover ed25519 public key from its base58 representation - {0}")] MalformedEd25519IdentityKey(String), @@ -226,6 +232,14 @@ pub enum MixnetContractError { #[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, } impl MixnetContractError { diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index ca154e231b..e88bfc098b 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -269,6 +269,12 @@ pub enum ExecuteMsg { owner: String, }, + // vesting migration: + MigrateVestedMixNode {}, + MigratedVestedDelegation { + mix_id: MixId, + }, + // testing-only #[cfg(feature = "contract-testing")] TestingResolveAllPendingEvents { @@ -381,6 +387,9 @@ impl ExecuteMsg { ExecuteMsg::WithdrawDelegatorRewardOnBehalf { mix_id, .. } => { format!("withdrawing delegator reward from mixnode {mix_id} on behalf") } + ExecuteMsg::MigrateVestedMixNode { .. } => "migrate vested mixnode".into(), + ExecuteMsg::MigratedVestedDelegation { .. } => "migrate vested delegation".to_string(), + #[cfg(feature = "contract-testing")] ExecuteMsg::TestingResolveAllPendingEvents { .. } => { "resolving all pending events".into() diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs index d7dc757956..b957adbe10 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 6593926445..eefe07e9f1 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/pending_events.rs b/common/types/src/pending_events.rs index f5789890bf..d36e0b978c 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/src/contract.rs b/contracts/mixnet/src/contract.rs index a8afbca564..f2cb28f81c 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -253,6 +253,14 @@ pub fn execute( crate::rewards::transactions::try_withdraw_delegator_reward(deps, info, mix_id) } + // vesting migration: + ExecuteMsg::MigrateVestedMixNode { .. } => { + crate::vesting_migration::try_migrate_vested_mixnode(deps, info) + } + ExecuteMsg::MigratedVestedDelegation { mix_id } => { + crate::vesting_migration::try_migrate_vested_delegation(deps, info, mix_id) + } + // legacy vesting ExecuteMsg::CreateFamilyOnBehalf { .. } | ExecuteMsg::JoinFamilyOnBehalf { .. } diff --git a/contracts/mixnet/src/lib.rs b/contracts/mixnet/src/lib.rs index 80e6822f89..985cffda57 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/storage.rs b/contracts/mixnet/src/mixnet_contract_settings/storage.rs index 2bcd8aa123..4bb85c1025 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/storage.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/storage.rs @@ -43,6 +43,12 @@ pub(crate) fn rewarding_denom(storage: &dyn Storage) -> Result Result { + Ok(CONTRACT_STATE + .load(storage) + .map(|state| state.vesting_contract_address)?) +} + pub(crate) fn initialise_storage( storage: &mut dyn Storage, initial_state: ContractState, diff --git a/contracts/mixnet/src/vesting_migration.rs b/contracts/mixnet/src/vesting_migration.rs new file mode 100644 index 0000000000..3faf826ec0 --- /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/src/contract.rs b/contracts/vesting/src/contract.rs index a9689e3390..fcd3727596 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,23 @@ 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 e5e415e30c..8e4c7c6a62 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 629cfee050..2e7b5a65d3 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 adbfe39436..a51f084862 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 0371bc7cc3..d01bea6789 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 29ee780ff9..ee14207d3e 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) + } }