diff --git a/Cargo.lock b/Cargo.lock index e7c7440bf6..53225fe039 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4993,9 +4993,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.83" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" dependencies = [ "itoa 1.0.1", "ryu", diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index a9b3e8fbf3..22e4356edc 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -2,8 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::reward_params::NodeRewardParams; -use crate::ContractStateParams; +use crate::{ContractStateParams, Layer, SphinxKey}; use crate::{Gateway, IdentityKey, MixNode}; +use cosmwasm_std::{Addr, Coin}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -213,17 +214,121 @@ pub enum QueryMsg { }, } +// all of those `serde rename` are here to reduce the rpc response size to bare minimum + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +#[serde(rename = "op")] +pub enum V2MigrationOperation { + #[serde(rename = "m1")] + MigrateOperator { + #[serde(rename = "a1")] + node_identity: String, + }, + + #[serde(rename = "m2")] + MigrateDelegator { + #[serde(rename = "a1")] + address: Addr, + + #[serde(rename = "a2")] + node_identity: String, + + #[serde(rename = "a3")] + proxy: Option, + + #[serde(rename = "a4")] + new_mix_id: Option, + }, + + #[serde(rename = "m3")] + RemoveOperator { + #[serde(rename = "a1")] + node_identity: String, + }, + + #[serde(rename = "m4")] + RemoveDelegator { + #[serde(rename = "a1")] + address: Addr, + + #[serde(rename = "a2")] + node_identity: String, + + #[serde(rename = "a3")] + proxy: Option, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub enum SpecialVestingMigrateExecuteMsg { + UpdateDelegation { + owner: String, + node_identity: String, + mix_id: u64, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub enum SpecialV2ExecuteMsg { + ToBeNamedSaveMixnode { + #[serde(rename = "a1")] + host: String, + + #[serde(rename = "a2")] + mix_port: u16, + + #[serde(rename = "a3")] + verloc_port: u16, + + #[serde(rename = "a4")] + http_api_port: u16, + + #[serde(rename = "a5")] + sphinx_key: SphinxKey, + + #[serde(rename = "a6")] + identity_key: IdentityKey, + + #[serde(rename = "a7")] + version: String, + + #[serde(rename = "a8")] + pledge_amount: Coin, + + #[serde(rename = "a9")] + owner: Addr, + + #[serde(rename = "a10")] + block_height: u64, + + #[serde(rename = "a11")] + proxy: Option, + }, + ToBeNamedSaveDelegation { + #[serde(rename = "a1")] + owner: Addr, + + #[serde(rename = "a2")] + mix_id: u64, + + #[serde(rename = "a3")] + amount: Coin, + + #[serde(rename = "a4")] + block_height: u64, + + #[serde(rename = "a5")] + proxy: Option, + }, +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct MigrateMsg { - pub mixnet_denom: String, - nodes_to_remove: Option>, -} - -impl MigrateMsg { - pub fn nodes_to_remove(&self) -> Vec { - self.nodes_to_remove.clone().unwrap_or_default() - } + pub v2_contract_address: String, + pub vesting_contract_address: String, + pub operations: Vec, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs index a6e90c917c..249880f21a 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs @@ -119,6 +119,12 @@ pub enum ExecuteMsg { UpdateLockedPledgeCap { amount: Uint128, }, + + AuthorisedUpdateToV2 { + owner: String, + node_identity: IdentityKey, + mix_id: u64, + }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 3eaa1f6035..cfead3ac01 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -7,6 +7,7 @@ use crate::delegations::queries::query_mixnode_delegation; use crate::delegations::queries::{ query_mixnode_delegations_paged, query_pending_delegation_events, }; +use crate::delegations::storage::delegations; use crate::error::ContractError; use crate::gateways::queries::query_owns_gateway; use crate::gateways::queries::{query_gateway_bond, query_gateways_paged}; @@ -24,18 +25,19 @@ use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::mixnet_contract_settings::transactions::try_update_rewarding_validator_address; use crate::mixnodes::bonding_queries as mixnode_queries; use crate::mixnodes::bonding_queries::{ - query_checkpoints_for_mixnode, query_mixnode_at_height, query_mixnodes_paged, + query_checkpoints_for_mixnode, query_mixnode_at_height, query_mixnode_bond, + query_mixnodes_paged, }; use crate::mixnodes::layer_queries::query_layer_distribution; use crate::mixnodes::transactions::_try_remove_mixnode; -use crate::queued_migrations::migrate_config_from_env; +use crate::queued_migrations::v2_migration; use crate::rewards::queries::{ query_circulating_supply, query_reward_pool, query_rewarding_status, query_staking_supply, }; use crate::rewards::storage as rewards_storage; use cosmwasm_std::{ entry_point, to_binary, Addr, Api, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, - Storage, Uint128, + Storage, SubMsg, Uint128, }; use mixnet_contract_common::{ ContractStateParams, ExecuteMsg, InstantiateMsg, MigrateMsg, NodeToRemove, QueryMsg, @@ -114,6 +116,7 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { + // we must be able to perform universal compound alongside reconcile ExecuteMsg::CompoundReward { operator, delegator, @@ -127,215 +130,217 @@ pub fn execute( mix_identity, proxy, ), - ExecuteMsg::UpdateRewardingValidatorAddress { address } => { - try_update_rewarding_validator_address(deps, info, address) - } - ExecuteMsg::InitEpoch {} => try_init_epoch(info, deps.storage, env), - ExecuteMsg::BondMixnode { - mix_node, - owner_signature, - } => crate::mixnodes::transactions::try_add_mixnode( - deps, - env, - info, - mix_node, - owner_signature, - ), - ExecuteMsg::UnbondMixnode {} => { - crate::mixnodes::transactions::try_remove_mixnode(&env, deps.storage, deps.api, info) - } - ExecuteMsg::UpdateMixnodeConfig { - profit_margin_percent, - } => crate::mixnodes::transactions::try_update_mixnode_config( - deps, - env, - info, - profit_margin_percent, - ), - ExecuteMsg::UpdateMixnodeConfigOnBehalf { - profit_margin_percent, - owner, - } => crate::mixnodes::transactions::try_update_mixnode_config_on_behalf( - deps, - env, - info, - profit_margin_percent, - owner, - ), - ExecuteMsg::BondGateway { - gateway, - owner_signature, - } => crate::gateways::transactions::try_add_gateway( - deps, - env, - info, - gateway, - owner_signature, - ), - ExecuteMsg::UnbondGateway {} => { - crate::gateways::transactions::try_remove_gateway(deps, info) - } - ExecuteMsg::UpdateContractStateParams(params) => { - crate::mixnet_contract_settings::transactions::try_update_contract_settings( - deps, info, params, - ) - } - ExecuteMsg::RewardMixnode { identity, params } => { - crate::rewards::transactions::try_reward_mixnode(deps, env, info, identity, params) - } - ExecuteMsg::DelegateToMixnode { mix_identity } => { - crate::delegations::transactions::try_delegate_to_mixnode(deps, env, info, mix_identity) - } - ExecuteMsg::UndelegateFromMixnode { mix_identity } => { - crate::delegations::transactions::try_remove_delegation_from_mixnode( - deps, - env, - info, - mix_identity, - ) - } - // ExecuteMsg::RewardNextMixDelegators { - // mix_identity, - // interval_id, - // } => crate::rewards::transactions::try_reward_next_mixnode_delegators( - // deps, - // info, - // mix_identity, - // interval_id, - // ), - ExecuteMsg::DelegateToMixnodeOnBehalf { - mix_identity, - delegate, - } => crate::delegations::transactions::try_delegate_to_mixnode_on_behalf( - deps, - env, - info, - mix_identity, - delegate, - ), - ExecuteMsg::UndelegateFromMixnodeOnBehalf { - mix_identity, - delegate, - } => crate::delegations::transactions::try_remove_delegation_from_mixnode_on_behalf( - deps, - env, - info, - mix_identity, - delegate, - ), - ExecuteMsg::BondMixnodeOnBehalf { - mix_node, - owner, - owner_signature, - } => crate::mixnodes::transactions::try_add_mixnode_on_behalf( - deps, - env, - info, - mix_node, - owner, - owner_signature, - ), - ExecuteMsg::UnbondMixnodeOnBehalf { owner } => { - crate::mixnodes::transactions::try_remove_mixnode_on_behalf( - &env, - deps.storage, - deps.api, - info, - owner, - ) - } - ExecuteMsg::BondGatewayOnBehalf { - gateway, - owner, - owner_signature, - } => crate::gateways::transactions::try_add_gateway_on_behalf( - deps, - env, - info, - gateway, - owner, - owner_signature, - ), - ExecuteMsg::UnbondGatewayOnBehalf { owner } => { - crate::gateways::transactions::try_remove_gateway_on_behalf(deps, info, owner) - } - ExecuteMsg::WriteRewardedSet { - rewarded_set, - expected_active_set_size, - } => crate::interval::transactions::try_write_rewarded_set( - deps, - env, - info, - rewarded_set, - expected_active_set_size, - ), - ExecuteMsg::AdvanceCurrentEpoch {} => crate::interval::transactions::try_advance_epoch( - env, - deps.storage, - info.sender.to_string(), - ), - ExecuteMsg::CompoundDelegatorReward { mix_identity } => { - crate::rewards::transactions::try_compound_delegator_reward( - deps, - env, - info, - mix_identity, - ) - } - ExecuteMsg::CompoundOperatorReward {} => { - crate::rewards::transactions::try_compound_operator_reward(deps, env, info) - } - ExecuteMsg::CompoundDelegatorRewardOnBehalf { - owner, - mix_identity, - } => crate::rewards::transactions::try_compound_delegator_reward_on_behalf( - deps, - env, - info, - owner, - mix_identity, - ), - ExecuteMsg::CompoundOperatorRewardOnBehalf { owner } => { - crate::rewards::transactions::try_compound_operator_reward_on_behalf( - deps, env, info, owner, - ) - } ExecuteMsg::ReconcileDelegations {} => { crate::delegations::transactions::try_reconcile_all_delegation_events(deps) } - ExecuteMsg::CheckpointMixnodes {} => { - crate::mixnodes::transactions::try_checkpoint_mixnodes( - deps.storage, - env.block.height, - info, - ) - } - ExecuteMsg::ClaimOperatorReward {} => { - crate::rewards::transactions::try_claim_operator_reward(deps, &env, &info) - } - ExecuteMsg::ClaimOperatorRewardOnBehalf { owner } => { - crate::rewards::transactions::try_claim_operator_reward_on_behalf( - deps, &env, &info, owner, - ) - } - ExecuteMsg::ClaimDelegatorReward { mix_identity } => { - crate::rewards::transactions::try_claim_delegator_reward( - deps, - &env, - &info, - &mix_identity, - ) - } - ExecuteMsg::ClaimDelegatorRewardOnBehalf { - mix_identity, - owner, - } => crate::rewards::transactions::try_claim_delegator_reward_on_behalf( - deps, - &env, - &info, - owner, - &mix_identity, - ), + _ => Err(ContractError::MaintenanceMode), + // ExecuteMsg::UpdateRewardingValidatorAddress { address } => { + // try_update_rewarding_validator_address(deps, info, address) + // } + // ExecuteMsg::InitEpoch {} => try_init_epoch(info, deps.storage, env), + // ExecuteMsg::BondMixnode { + // mix_node, + // owner_signature, + // } => crate::mixnodes::transactions::try_add_mixnode( + // deps, + // env, + // info, + // mix_node, + // owner_signature, + // ), + // ExecuteMsg::UnbondMixnode {} => { + // crate::mixnodes::transactions::try_remove_mixnode(&env, deps.storage, deps.api, info) + // } + // ExecuteMsg::UpdateMixnodeConfig { + // profit_margin_percent, + // } => crate::mixnodes::transactions::try_update_mixnode_config( + // deps, + // env, + // info, + // profit_margin_percent, + // ), + // ExecuteMsg::UpdateMixnodeConfigOnBehalf { + // profit_margin_percent, + // owner, + // } => crate::mixnodes::transactions::try_update_mixnode_config_on_behalf( + // deps, + // env, + // info, + // profit_margin_percent, + // owner, + // ), + // ExecuteMsg::BondGateway { + // gateway, + // owner_signature, + // } => crate::gateways::transactions::try_add_gateway( + // deps, + // env, + // info, + // gateway, + // owner_signature, + // ), + // ExecuteMsg::UnbondGateway {} => { + // crate::gateways::transactions::try_remove_gateway(deps, info) + // } + // ExecuteMsg::UpdateContractStateParams(params) => { + // crate::mixnet_contract_settings::transactions::try_update_contract_settings( + // deps, info, params, + // ) + // } + // ExecuteMsg::RewardMixnode { identity, params } => { + // crate::rewards::transactions::try_reward_mixnode(deps, env, info, identity, params) + // } + // ExecuteMsg::DelegateToMixnode { mix_identity } => { + // crate::delegations::transactions::try_delegate_to_mixnode(deps, env, info, mix_identity) + // } + // ExecuteMsg::UndelegateFromMixnode { mix_identity } => { + // crate::delegations::transactions::try_remove_delegation_from_mixnode( + // deps, + // env, + // info, + // mix_identity, + // ) + // } + // // ExecuteMsg::RewardNextMixDelegators { + // // mix_identity, + // // interval_id, + // // } => crate::rewards::transactions::try_reward_next_mixnode_delegators( + // // deps, + // // info, + // // mix_identity, + // // interval_id, + // // ), + // ExecuteMsg::DelegateToMixnodeOnBehalf { + // mix_identity, + // delegate, + // } => crate::delegations::transactions::try_delegate_to_mixnode_on_behalf( + // deps, + // env, + // info, + // mix_identity, + // delegate, + // ), + // ExecuteMsg::UndelegateFromMixnodeOnBehalf { + // mix_identity, + // delegate, + // } => crate::delegations::transactions::try_remove_delegation_from_mixnode_on_behalf( + // deps, + // env, + // info, + // mix_identity, + // delegate, + // ), + // ExecuteMsg::BondMixnodeOnBehalf { + // mix_node, + // owner, + // owner_signature, + // } => crate::mixnodes::transactions::try_add_mixnode_on_behalf( + // deps, + // env, + // info, + // mix_node, + // owner, + // owner_signature, + // ), + // ExecuteMsg::UnbondMixnodeOnBehalf { owner } => { + // crate::mixnodes::transactions::try_remove_mixnode_on_behalf( + // &env, + // deps.storage, + // deps.api, + // info, + // owner, + // ) + // } + // ExecuteMsg::BondGatewayOnBehalf { + // gateway, + // owner, + // owner_signature, + // } => crate::gateways::transactions::try_add_gateway_on_behalf( + // deps, + // env, + // info, + // gateway, + // owner, + // owner_signature, + // ), + // ExecuteMsg::UnbondGatewayOnBehalf { owner } => { + // crate::gateways::transactions::try_remove_gateway_on_behalf(deps, info, owner) + // } + // ExecuteMsg::WriteRewardedSet { + // rewarded_set, + // expected_active_set_size, + // } => crate::interval::transactions::try_write_rewarded_set( + // deps, + // env, + // info, + // rewarded_set, + // expected_active_set_size, + // ), + // ExecuteMsg::AdvanceCurrentEpoch {} => crate::interval::transactions::try_advance_epoch( + // env, + // deps.storage, + // info.sender.to_string(), + // ), + // ExecuteMsg::CompoundDelegatorReward { mix_identity } => { + // crate::rewards::transactions::try_compound_delegator_reward( + // deps, + // env, + // info, + // mix_identity, + // ) + // } + // ExecuteMsg::CompoundOperatorReward {} => { + // crate::rewards::transactions::try_compound_operator_reward(deps, env, info) + // } + // ExecuteMsg::CompoundDelegatorRewardOnBehalf { + // owner, + // mix_identity, + // } => crate::rewards::transactions::try_compound_delegator_reward_on_behalf( + // deps, + // env, + // info, + // owner, + // mix_identity, + // ), + // ExecuteMsg::CompoundOperatorRewardOnBehalf { owner } => { + // crate::rewards::transactions::try_compound_operator_reward_on_behalf( + // deps, env, info, owner, + // ) + // } + // + // ExecuteMsg::CheckpointMixnodes {} => { + // crate::mixnodes::transactions::try_checkpoint_mixnodes( + // deps.storage, + // env.block.height, + // info, + // ) + // } + // ExecuteMsg::ClaimOperatorReward {} => { + // crate::rewards::transactions::try_claim_operator_reward(deps, &env, &info) + // } + // ExecuteMsg::ClaimOperatorRewardOnBehalf { owner } => { + // crate::rewards::transactions::try_claim_operator_reward_on_behalf( + // deps, &env, &info, owner, + // ) + // } + // ExecuteMsg::ClaimDelegatorReward { mix_identity } => { + // crate::rewards::transactions::try_claim_delegator_reward( + // deps, + // &env, + // &info, + // &mix_identity, + // ) + // } + // ExecuteMsg::ClaimDelegatorRewardOnBehalf { + // mix_identity, + // owner, + // } => crate::rewards::transactions::try_claim_delegator_reward_on_behalf( + // deps, + // &env, + // &info, + // owner, + // &mix_identity, + // ), } } @@ -509,17 +514,7 @@ fn remove_malicious_node( #[entry_point] pub fn migrate(deps: DepsMut<'_>, env: Env, msg: MigrateMsg) -> Result { - migrate_config_from_env(deps.storage, &msg)?; - let mut response = Response::new(); - for node in msg.nodes_to_remove().iter() { - let mut sub_response = remove_malicious_node(deps.storage, deps.api, &env, node) - .unwrap_or_else(|_| panic!("Could not remove node: {:?}", node)); - response.messages.append(&mut sub_response.messages); - response.attributes.append(&mut sub_response.attributes); - response.events.append(&mut sub_response.events); - } - - Ok(response) + v2_migration(deps, env, msg) } #[cfg(test)] diff --git a/contracts/mixnet/src/error.rs b/contracts/mixnet/src/error.rs index e1d3a22335..e045ec5c13 100644 --- a/contracts/mixnet/src/error.rs +++ b/contracts/mixnet/src/error.rs @@ -185,4 +185,7 @@ pub enum ContractError { #[error("Mixnode {identity} has been blacklisted on the network")] MixnodeBlacklisted { identity: String }, + + #[error("Contract is currently set to the maintenance mode - all transactions are temporarily disabled")] + MaintenanceMode, } diff --git a/contracts/mixnet/src/queued_migrations.rs b/contracts/mixnet/src/queued_migrations.rs index 56e4a9f890..4c004cde29 100644 --- a/contracts/mixnet/src/queued_migrations.rs +++ b/contracts/mixnet/src/queued_migrations.rs @@ -1,37 +1,256 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{Addr, Response, Storage}; -use cw_storage_plus::Item; -use mixnet_contract_common::{ContractStateParams, MigrateMsg}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - +use crate::delegations::queries::query_mixnode_delegation; +use crate::delegations::storage::delegations; use crate::error::ContractError; -use crate::mixnet_contract_settings::models::ContractState; -use crate::mixnet_contract_settings::storage::CONTRACT_STATE; +use crate::mixnodes::storage::mixnodes; +use cosmwasm_std::{wasm_execute, Addr, BankMsg, DepsMut, Env, Response, SubMsg}; +use cw_storage_plus::Map; +use mixnet_contract_common::delegation::generate_storage_key; +use mixnet_contract_common::{IdentityKey, MigrateMsg, SpecialV2ExecuteMsg, V2MigrationOperation}; +use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; -pub fn migrate_config_from_env( - storage: &mut dyn Storage, - msg: &MigrateMsg, -) -> Result { - #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] - pub struct OldContractState { - pub owner: Addr, - pub rewarding_validator_address: Addr, - pub params: ContractStateParams, +const MIGRATED_MIXNODES: Map = Map::new("migrated-mixnodes"); +type OwnerXorProxy = Vec; +const MIGRATED_DELEGATES: Map<(IdentityKey, OwnerXorProxy), u8> = Map::new("migrated-delegates"); + +fn migrate_operator( + deps: &mut DepsMut, + v2_mixnet_contract: &str, + node_identity: String, + response: &mut Response, +) -> Result<(), ContractError> { + if MIGRATED_MIXNODES.has(deps.storage, node_identity.clone()) { + // again, panic here because this should never occur and it's incredible dangerous to let it happen + panic!("mixnode {} has already been migrated!", node_identity); } - const OLD_CONTRACT_STATE: Item<'_, OldContractState> = Item::new("config"); + MIGRATED_MIXNODES.save(deps.storage, node_identity.clone(), &1u8)?; - let old_state = OLD_CONTRACT_STATE.load(storage)?; - let new_state = ContractState { - owner: old_state.owner, - mix_denom: msg.mixnet_denom.clone(), - rewarding_validator_address: old_state.rewarding_validator_address, - params: old_state.params, + let bond = mixnodes() + .load(deps.storage, &node_identity) + .expect("failed to read mixnode bond"); + + let pledge = bond.pledge_amount.clone(); + let v2_message = SpecialV2ExecuteMsg::ToBeNamedSaveMixnode { + host: bond.mix_node.host, + mix_port: bond.mix_node.mix_port, + verloc_port: bond.mix_node.verloc_port, + http_api_port: bond.mix_node.http_api_port, + sphinx_key: bond.mix_node.sphinx_key, + identity_key: bond.mix_node.identity_key, + version: bond.mix_node.version, + pledge_amount: bond.pledge_amount, + owner: bond.owner, + block_height: bond.block_height, + proxy: bond.proxy, }; - CONTRACT_STATE.save(storage, &new_state)?; - - Ok(Default::default()) + // TODO: do we need separate BankMsg here, or can we just use 'funds' here directly? + let wasm_msg = wasm_execute(v2_mixnet_contract, &v2_message, vec![pledge]) + .expect("failed to serialize mixnode migration msg"); + response.messages.push(SubMsg::new(wasm_msg)); + Ok(()) +} + +fn is_proxy_vesting(proxy: Option<&Addr>, vesting_contract: &str) -> bool { + if let Some(proxy) = proxy { + if proxy.as_ref() == vesting_contract { + return true; + } + } + false +} + +fn migrate_delegator( + deps: &mut DepsMut, + v2_mixnet_contract: &str, + vesting_contract: &str, + address: Addr, + node_identity: String, + proxy: Option, + new_mix_id: Option, + response: &mut Response, +) -> Result<(), ContractError> { + let owner_proxy = generate_storage_key(&address, proxy.as_ref()); + + let storage_key = (node_identity.clone(), owner_proxy); + if MIGRATED_DELEGATES.has(deps.storage, storage_key.clone()) { + // again, panic here because this should never occur and it's incredible dangerous to let it happen + panic!( + "delegator {}/{} has already been migrated!", + address, node_identity + ); + } + MIGRATED_DELEGATES.save(deps.storage, storage_key, &1u8)?; + + // there should only be one (as we ensured it during previous migration steps, if not, then somebody is not following the migration instructions + // and we're in an inconsistent state) + // also this entry MUST exist as we're explicitly migrating this one + let mut delegation = query_mixnode_delegation( + deps.storage, + deps.api, + node_identity.clone(), + address.clone().into_string(), + proxy.map(Addr::into_string), + ) + .expect("specified delegation doesn't exist!!"); + + if delegation.len() != 1 { + panic!("the universal compound hasn't been run prior to this migration!!") + } + + // take ownership of the one and only entry + let delegation = delegation.pop().unwrap(); + + // if mix_id is `None`, it means target mixnode doesn't exist anymore -> return the tokens + // otherwise attempt to migrate it into the new contract + if let Some(migrated_mix_id) = new_mix_id { + if is_proxy_vesting(delegation.proxy.as_ref(), vesting_contract) { + let vesting_update = VestingContractExecuteMsg::AuthorisedUpdateToV2 { + owner: address.into_string(), + node_identity, + mix_id: migrated_mix_id, + }; + + let wasm_msg = wasm_execute(vesting_contract, &vesting_update, vec![]) + .expect("failed to serialize vesting migration msg"); + response.messages.push(SubMsg::new(wasm_msg)); + } + + let stake = delegation.amount.clone(); + let v2_message = SpecialV2ExecuteMsg::ToBeNamedSaveDelegation { + owner: delegation.owner, + mix_id: migrated_mix_id, + amount: delegation.amount, + block_height: delegation.block_height, + proxy: delegation.proxy, + }; + + let wasm_msg = wasm_execute(v2_mixnet_contract, &v2_message, vec![stake]) + .expect("failed to serialize mixnode migration msg"); + response.messages.push(SubMsg::new(wasm_msg)); + } else { + let mut to_address = delegation.owner.to_string(); + let mut make_bank_msg = true; + // if the specified proxy matches the vesting contract address -> treat it as "proper" undelegation + // and send tokens back there + if let Some(proxy) = delegation.proxy { + to_address = proxy.to_string(); + if proxy == vesting_contract { + make_bank_msg = false; + let vesting_track = VestingContractExecuteMsg::TrackUndelegation { + owner: delegation.owner.to_string(), + mix_identity: delegation.node_identity, + amount: delegation.amount.clone(), + }; + let wasm_msg = wasm_execute( + vesting_contract, + &vesting_track, + vec![delegation.amount.clone()], + )?; + response.messages.push(SubMsg::new(wasm_msg)); + } + } + + // otherwise just send tokens back to the user / different proxy + if make_bank_msg { + let return_tokens = BankMsg::Send { + to_address, + amount: vec![delegation.amount], + }; + response.messages.push(SubMsg::new(return_tokens)); + } + } + Ok(()) +} + +fn remove_operator( + deps: &mut DepsMut, + env: &Env, + node_identity: String, +) -> Result<(), ContractError> { + if !MIGRATED_MIXNODES.has(deps.storage, node_identity.clone()) { + // again, panic here because this should never occur and it's incredible dangerous to let it happen + panic!( + "attempted to remove mixnode {} without prior migration!", + node_identity + ); + } + mixnodes().remove(deps.storage, &node_identity, env.block.height)?; + Ok(()) +} + +fn remove_delegator( + deps: &mut DepsMut, + address: Addr, + node_identity: String, + proxy: Option, +) -> Result<(), ContractError> { + let owner_proxy = generate_storage_key(&address, proxy.as_ref()); + + let storage_key = (node_identity.clone(), owner_proxy); + if !MIGRATED_DELEGATES.has(deps.storage, storage_key.clone()) { + // again, panic here because this should never occur and it's incredible dangerous to let it happen + panic!( + "attempted to remove delegator {}/{} without prior migration!", + address, node_identity + ); + } + + let height = delegations() + .prefix(storage_key.clone()) + .keys(deps.storage, None, None, cosmwasm_std::Order::Ascending) + .next() + .unwrap() + .unwrap(); + delegations().remove(deps.storage, (storage_key.0, storage_key.1, height))?; + Ok(()) +} + +pub fn v2_migration( + mut deps: DepsMut<'_>, + env: Env, + msg: MigrateMsg, +) -> Result { + let mut response = Response::new(); + + // note: we're explicitly failing on failures here because we expect EVERYTHING we migrate to exist + for op in msg.operations { + match op { + V2MigrationOperation::MigrateOperator { node_identity } => { + migrate_operator( + &mut deps, + &msg.v2_contract_address, + node_identity, + &mut response, + )?; + } + V2MigrationOperation::MigrateDelegator { + address, + node_identity, + proxy, + new_mix_id, + } => migrate_delegator( + &mut deps, + &msg.v2_contract_address, + &msg.vesting_contract_address, + address, + node_identity, + proxy, + new_mix_id, + &mut response, + )?, + V2MigrationOperation::RemoveOperator { node_identity } => { + remove_operator(&mut deps, &env, node_identity)?; + } + V2MigrationOperation::RemoveDelegator { + address, + node_identity, + proxy, + } => remove_delegator(&mut deps, address, node_identity, proxy)?, + } + } + + Ok(response) }