From 82e952cf852bd9d5301595968668ace6fb82877c Mon Sep 17 00:00:00 2001 From: vedhavyas Date: Tue, 21 May 2024 16:56:28 +0530 Subject: [PATCH] make slashing operator spread over multiple consensus blocks when bundle is submitted for the domain. --- crates/pallet-domains/src/benchmarking.rs | 21 +- crates/pallet-domains/src/lib.rs | 38 ++- crates/pallet-domains/src/staking.rs | 282 ++++++++++++++--- crates/pallet-domains/src/staking_epoch.rs | 339 ++++++++++++--------- crates/pallet-domains/src/tests.rs | 6 +- 5 files changed, 462 insertions(+), 224 deletions(-) diff --git a/crates/pallet-domains/src/benchmarking.rs b/crates/pallet-domains/src/benchmarking.rs index eef4c95c6e..6cd4ef8fdc 100644 --- a/crates/pallet-domains/src/benchmarking.rs +++ b/crates/pallet-domains/src/benchmarking.rs @@ -7,14 +7,14 @@ use crate::block_tree::{prune_receipt, BlockTreeNode}; use crate::bundle_storage_fund::refund_storage_fee; use crate::domain_registry::DomainConfig; use crate::staking::{ - do_convert_previous_epoch_deposits, do_reward_operators, do_slash_operators, OperatorConfig, - OperatorStatus, + do_convert_previous_epoch_deposits, do_mark_operators_as_slashed, do_reward_operators, + OperatorConfig, OperatorStatus, }; use crate::staking_epoch::{ - do_finalize_domain_current_epoch, do_finalize_domain_epoch_staking, - do_finalize_slashed_operators, operator_take_reward_tax_and_stake, + do_finalize_domain_current_epoch, do_finalize_domain_epoch_staking, do_slash_operator, + operator_take_reward_tax_and_stake, }; -use crate::{DomainBlockNumberFor, Pallet as Domains}; +use crate::{DomainBlockNumberFor, Pallet as Domains, MAX_NOMINATORS_TO_SLASH}; #[cfg(not(feature = "std"))] use alloc::borrow::ToOwned; #[cfg(not(feature = "std"))] @@ -225,7 +225,7 @@ mod benchmarks { .expect("prune bad receipt should success") .expect("block tree node must exist"); - do_slash_operators::( + do_mark_operators_as_slashed::( block_tree_node.operator_ids.into_iter(), SlashedReason::BadExecutionReceipt(receipt_hash), ) @@ -290,7 +290,7 @@ mod benchmarks { ) .expect("reward operator should success"); - do_slash_operators::( + do_mark_operators_as_slashed::( operator_ids[n as usize..].to_vec().into_iter(), SlashedReason::InvalidBundle(1u32.into()), ) @@ -402,7 +402,7 @@ mod benchmarks { } // Slash operator - do_slash_operators::( + do_mark_operators_as_slashed::( operator_ids.into_iter(), SlashedReason::InvalidBundle(1u32.into()), ) @@ -410,14 +410,15 @@ mod benchmarks { assert_eq!( PendingSlashes::::get(domain_id) - .expect("pedning slash must exist") + .expect("pending slash must exist") .len(), operator_count as usize ); #[block] { - do_finalize_slashed_operators::(domain_id).expect("finalize slash should success"); + do_slash_operator::(domain_id, MAX_NOMINATORS_TO_SLASH) + .expect("finalize slash should success"); } assert!(PendingSlashes::::get(domain_id).is_none()); diff --git a/crates/pallet-domains/src/lib.rs b/crates/pallet-domains/src/lib.rs index 589ed1741a..cd9f6aee77 100644 --- a/crates/pallet-domains/src/lib.rs +++ b/crates/pallet-domains/src/lib.rs @@ -86,6 +86,9 @@ use sp_subspace_mmr::{ConsensusChainMmrLeafProof, MmrProofVerifier}; pub use staking::OperatorConfig; use subspace_core_primitives::{BlockHash, PotOutput, SlotNumber, U256}; +/// Maximum number of nominators to slash within a give operator at a time. +pub const MAX_NOMINATORS_TO_SLASH: u32 = 10; + pub(crate) type BalanceOf = ::Balance; pub(crate) type FungibleHoldId = @@ -180,22 +183,24 @@ mod pallet { register_runtime_at_genesis, Error as RuntimeRegistryError, ScheduledRuntimeUpgrade, }; #[cfg(not(feature = "runtime-benchmarks"))] - use crate::staking::do_reward_operators; + use crate::staking::do_mark_operators_as_slashed; #[cfg(not(feature = "runtime-benchmarks"))] - use crate::staking::do_slash_operators; + use crate::staking::do_reward_operators; use crate::staking::{ do_deregister_operator, do_nominate_operator, do_register_operator, do_unlock_funds, do_unlock_nominator, do_withdraw_stake, Deposit, DomainEpoch, Error as StakingError, Operator, OperatorConfig, SharePrice, StakingSummary, Withdrawal, }; - use crate::staking_epoch::{do_finalize_domain_current_epoch, Error as StakingEpochError}; + use crate::staking_epoch::{ + do_finalize_domain_current_epoch, do_slash_operator, Error as StakingEpochError, + }; use crate::weights::WeightInfo; #[cfg(not(feature = "runtime-benchmarks"))] use crate::DomainHashingFor; use crate::{ BalanceOf, BlockSlot, BlockTreeNodeFor, DomainBlockNumberFor, ElectionVerificationParams, FraudProofFor, HoldIdentifier, NominatorId, OpaqueBundleOf, ReceiptHashFor, StateRootOf, - MAX_BUNLDE_PER_BLOCK, STORAGE_VERSION, + MAX_BUNLDE_PER_BLOCK, MAX_NOMINATORS_TO_SLASH, STORAGE_VERSION, }; #[cfg(not(feature = "std"))] use alloc::string::String; @@ -1003,7 +1008,7 @@ mod pallet { let bad_receipt_hash = block_tree_node .execution_receipt .hash::>(); - do_slash_operators::( + do_mark_operators_as_slashed::( block_tree_node.operator_ids.into_iter(), SlashedReason::BadExecutionReceipt(bad_receipt_hash), ) @@ -1046,7 +1051,7 @@ mod pallet { ) .map_err(Error::::from)?; - do_slash_operators::( + do_mark_operators_as_slashed::( confirmed_block_info.invalid_bundle_authors.into_iter(), SlashedReason::InvalidBundle(confirmed_block_info.domain_block_number), ) @@ -1098,6 +1103,12 @@ mod pallet { SuccessfulBundles::::append(domain_id, bundle_hash); + // slash operator who are in pending slash + // TODO: include this for benchmarking + let _slashed_nominator_count = + do_slash_operator::(domain_id, MAX_NOMINATORS_TO_SLASH) + .map_err(Error::::from)?; + Self::deposit_event(Event::BundleStored { domain_id, bundle_hash, @@ -1160,7 +1171,7 @@ mod pallet { (block_tree_node.operator_ids.len() as u32).min(MAX_BUNLDE_PER_BLOCK), )); - do_slash_operators::( + do_mark_operators_as_slashed::( block_tree_node.operator_ids.into_iter(), SlashedReason::BadExecutionReceipt(bad_receipt_hash), ) @@ -2333,18 +2344,13 @@ impl Pallet { fn actual_epoch_transition_weight(epoch_transition_res: EpochTransitionResult) -> Weight { let EpochTransitionResult { rewarded_operator_count, - slashed_nominator_count, finalized_operator_count, - .. + completed_epoch_index: _, } = epoch_transition_res; - T::WeightInfo::operator_reward_tax_and_restake(rewarded_operator_count) - .saturating_add(T::WeightInfo::finalize_slashed_operators( - slashed_nominator_count, - )) - .saturating_add(T::WeightInfo::finalize_domain_epoch_staking( - finalized_operator_count, - )) + T::WeightInfo::operator_reward_tax_and_restake(rewarded_operator_count).saturating_add( + T::WeightInfo::finalize_domain_epoch_staking(finalized_operator_count), + ) } pub fn storage_fund_account_balance(operator_id: OperatorId) -> BalanceOf { diff --git a/crates/pallet-domains/src/staking.rs b/crates/pallet-domains/src/staking.rs index 295412f75e..c1c980e947 100644 --- a/crates/pallet-domains/src/staking.rs +++ b/crates/pallet-domains/src/staking.rs @@ -1149,38 +1149,47 @@ pub(crate) fn do_unlock_nominator( let cleanup_operator = current_nominator_count == 0 && !Deposits::::contains_key(operator_id, operator_owner); - if !cleanup_operator { + if cleanup_operator { + do_cleanup_operator::(operator_id, total_stake, operator.signing_key.clone())? + } else { // set update total shares, total stake and total storage fee deposit for operator operator.current_total_shares = total_shares; operator.current_total_stake = total_stake; operator.total_storage_fee_deposit = total_storage_fee_deposit; - NominatorCount::::set(operator_id, current_nominator_count); - *maybe_operator = Some(operator); - } else { - // transfer any remaining storage fund to treasury - bundle_storage_fund::transfer_all_to_treasury::(operator_id) - .map_err(Error::BundleStorageFund)?; + } + + Ok(()) + }) +} + +/// Removes all operator storages and mints the total stake back to treasury. +pub(crate) fn do_cleanup_operator( + operator_id: OperatorId, + total_stake: BalanceOf, + operator_signing_key: OperatorPublicKey, +) -> Result<(), Error> { + // transfer any remaining storage fund to treasury + bundle_storage_fund::transfer_all_to_treasury::(operator_id) + .map_err(Error::BundleStorageFund)?; - // transfer any remaining amount to treasury - mint_into_treasury::(total_stake).ok_or(Error::MintBalance)?; + // transfer any remaining amount to treasury + mint_into_treasury::(total_stake).ok_or(Error::MintBalance)?; - // remove OperatorOwner Details - OperatorIdOwner::::remove(operator_id); + // remove OperatorOwner Details + OperatorIdOwner::::remove(operator_id); - // remove operator signing key - OperatorSigningKey::::remove(operator.signing_key.clone()); + // remove operator signing key + OperatorSigningKey::::remove(operator_signing_key); - // remove operator epoch share prices - let _ = OperatorEpochSharePrice::::clear_prefix(operator_id, u32::MAX, None); + // remove operator epoch share prices + let _ = OperatorEpochSharePrice::::clear_prefix(operator_id, u32::MAX, None); - // remove nominator count for this operator. - NominatorCount::::remove(operator_id); - } + // remove nominator count for this operator. + NominatorCount::::remove(operator_id); - Ok(()) - }) + Ok(()) } /// Distribute the reward to the operators equally and drop any dust to treasury. @@ -1254,7 +1263,7 @@ pub(crate) fn do_reward_operators( /// Freezes the slashed operators and moves the operator to be removed once the domain they are /// operating finishes the epoch. -pub(crate) fn do_slash_operators( +pub(crate) fn do_mark_operators_as_slashed( operator_ids: impl AsRef<[OperatorId]>, slash_reason: SlashedReason, ReceiptHashFor>, ) -> Result<(), Error> { @@ -1308,13 +1317,15 @@ pub(crate) mod tests { NextOperatorId, NominatorCount, OperatorIdOwner, Operators, PendingSlashes, Withdrawals, }; use crate::staking::{ - do_convert_previous_epoch_withdrawal, do_nominate_operator, do_reward_operators, - do_slash_operators, do_unlock_funds, do_withdraw_stake, Error as StakingError, Operator, + do_convert_previous_epoch_withdrawal, do_mark_operators_as_slashed, do_nominate_operator, + do_reward_operators, do_unlock_funds, do_withdraw_stake, Error as StakingError, Operator, OperatorConfig, OperatorSigningKeyProofOfOwnershipData, OperatorStatus, StakingSummary, }; - use crate::staking_epoch::do_finalize_domain_current_epoch; - use crate::tests::{new_test_ext, ExistentialDeposit, RuntimeOrigin, Test}; - use crate::{bundle_storage_fund, BalanceOf, Error, NominatorId, SlashedReason}; + use crate::staking_epoch::{do_finalize_domain_current_epoch, do_slash_operator}; + use crate::tests::{new_test_ext, AccountId, ExistentialDeposit, RuntimeOrigin, Test}; + use crate::{ + bundle_storage_fund, BalanceOf, Error, NominatorId, SlashedReason, MAX_NOMINATORS_TO_SLASH, + }; use codec::Encode; use frame_support::traits::fungible::Mutate; use frame_support::traits::Currency; @@ -1330,7 +1341,7 @@ pub(crate) mod tests { use sp_runtime::{PerThing, Perbill}; use std::collections::{BTreeMap, BTreeSet}; use std::vec; - use subspace_runtime_primitives::SSC; + use subspace_runtime_primitives::{Balance, SSC}; type Balances = pallet_balances::Pallet; type Domains = crate::Pallet; @@ -1716,10 +1727,16 @@ pub(crate) mod tests { operator_owner: operator_account, }; let signature = pair.sign(&data.encode()); - let nominator_account = 7; + let nominator_account = 26; let nominator_free_balance = 150 * SSC; let nominator_stake = 100 * SSC; + let nominator_accounts: Vec = (1..=25).collect(); + let nominators: BTreeMap = nominator_accounts + .into_iter() + .map(|nominator_id| (nominator_id, (nominator_free_balance, nominator_stake))) + .collect(); + let mut ext = new_test_ext(); ext.execute_with(|| { let (operator_id, _) = register_operator( @@ -1730,13 +1747,7 @@ pub(crate) mod tests { 10 * SSC, pair.public(), signature, - BTreeMap::from_iter(vec![ - (1, (nominator_free_balance, nominator_stake)), - (2, (nominator_free_balance, nominator_stake)), - (3, (nominator_free_balance, nominator_stake)), - (4, (nominator_free_balance, nominator_stake)), - (5, (nominator_free_balance, nominator_stake)), - ]), + nominators, ); Balances::set_balance(&nominator_account, nominator_free_balance); @@ -2648,7 +2659,11 @@ pub(crate) mod tests { do_nominate_operator::(operator_id, deposit.0, deposit.1).unwrap(); } - do_slash_operators::(vec![operator_id], SlashedReason::InvalidBundle(1)).unwrap(); + do_mark_operators_as_slashed::( + vec![operator_id], + SlashedReason::InvalidBundle(1), + ) + .unwrap(); let domain_stake_summary = DomainStakingSummary::::get(domain_id).unwrap(); assert!(!domain_stake_summary.next_operators.contains(&operator_id)); @@ -2667,7 +2682,7 @@ pub(crate) mod tests { 0 ); - do_finalize_domain_current_epoch::(domain_id).unwrap(); + do_slash_operator::(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap(); assert_eq!(PendingSlashes::::get(domain_id), None); assert_eq!(Operators::::get(operator_id), None); assert_eq!(OperatorIdOwner::::get(operator_id), None); @@ -2686,6 +2701,172 @@ pub(crate) mod tests { }); } + #[test] + fn slash_operator_with_more_than_max_nominators_to_slash() { + let domain_id = DomainId::new(0); + let operator_account = 1; + let operator_free_balance = 250 * SSC; + let operator_stake = 200 * SSC; + let operator_extra_deposit = 40 * SSC; + let pair = OperatorPair::from_seed(&U256::from(0u32).into()); + let data = OperatorSigningKeyProofOfOwnershipData { + operator_owner: operator_account, + }; + let signature = pair.sign(&data.encode()); + + let nominator_accounts: Vec = (2..22).collect(); + let nominator_free_balance = 150 * SSC; + let nominator_stake = 100 * SSC; + let nominator_extra_deposit = 40 * SSC; + + let mut nominators = vec![(operator_account, (operator_free_balance, operator_stake))]; + for nominator_account in nominator_accounts.clone() { + nominators.push((nominator_account, (nominator_free_balance, nominator_stake))) + } + + let last_nominator_account = nominator_accounts.last().cloned().unwrap(); + let unlocking = vec![ + (operator_account, 10 * SSC), + (last_nominator_account, 10 * SSC), + ]; + + let deposits = vec![ + (operator_account, operator_extra_deposit), + (last_nominator_account, nominator_extra_deposit), + ]; + + let init_total_stake = STORAGE_FEE_RESERVE.left_from_one() + * (200 + (100 * nominator_accounts.len() as u128)) + * SSC; + let init_total_storage_fund = + STORAGE_FEE_RESERVE * (200 + (100 * nominator_accounts.len() as u128)) * SSC; + + let mut ext = new_test_ext(); + ext.execute_with(|| { + let (operator_id, _) = register_operator( + domain_id, + operator_account, + operator_free_balance, + operator_stake, + 10 * SSC, + pair.public(), + signature, + BTreeMap::from_iter(nominators), + ); + + do_finalize_domain_current_epoch::(domain_id).unwrap(); + let domain_stake_summary = DomainStakingSummary::::get(domain_id).unwrap(); + assert_eq!(domain_stake_summary.current_total_stake, init_total_stake); + + let operator = Operators::::get(operator_id).unwrap(); + assert_eq!(operator.current_total_stake, init_total_stake); + assert_eq!(operator.total_storage_fee_deposit, init_total_storage_fund); + assert_eq!( + operator.total_storage_fee_deposit, + bundle_storage_fund::total_balance::(operator_id) + ); + + for unlock in &unlocking { + do_withdraw_stake::(operator_id, unlock.0, unlock.1).unwrap(); + } + + do_reward_operators::(domain_id, vec![operator_id].into_iter(), 20 * SSC) + .unwrap(); + do_finalize_domain_current_epoch::(domain_id).unwrap(); + + // Manually convert previous withdrawal in share to balance + for id in [operator_account, last_nominator_account] { + Withdrawals::::try_mutate(operator_id, id, |maybe_withdrawal| { + do_convert_previous_epoch_withdrawal::( + operator_id, + maybe_withdrawal.as_mut().unwrap(), + ) + }) + .unwrap(); + } + + // post epoch transition, domain stake has 21.666 amount reduced and storage fund has 5 amount reduced + // due to withdrawal of 20 shares + let operator = Operators::::get(operator_id).unwrap(); + let domain_stake_summary = DomainStakingSummary::::get(domain_id).unwrap(); + let operator_withdrawal = + Withdrawals::::get(operator_id, operator_account).unwrap(); + let nominator_withdrawal = + Withdrawals::::get(operator_id, last_nominator_account).unwrap(); + + let total_deposit = + domain_stake_summary.current_total_stake + operator.total_storage_fee_deposit; + let total_stake_withdrawal = operator_withdrawal.total_withdrawal_amount + + nominator_withdrawal.total_withdrawal_amount; + let total_storage_fee_withdrawal = operator_withdrawal.withdrawals[0] + .storage_fee_refund + + nominator_withdrawal.withdrawals[0].storage_fee_refund; + assert_eq!(2194772727253419421470, total_deposit,); + assert_eq!(20227272746580578530, total_stake_withdrawal); + assert_eq!(5000000000000000000, total_storage_fee_withdrawal); + assert_eq!( + 2220 * SSC, + total_deposit + total_stake_withdrawal + total_storage_fee_withdrawal + ); + + assert_eq!( + operator.total_storage_fee_deposit, + bundle_storage_fund::total_balance::(operator_id) + ); + + for deposit in deposits { + do_nominate_operator::(operator_id, deposit.0, deposit.1).unwrap(); + } + + do_mark_operators_as_slashed::( + vec![operator_id], + SlashedReason::InvalidBundle(1), + ) + .unwrap(); + + let domain_stake_summary = DomainStakingSummary::::get(domain_id).unwrap(); + assert!(!domain_stake_summary.next_operators.contains(&operator_id)); + + let operator = Operators::::get(operator_id).unwrap(); + assert_eq!( + *operator.status::(operator_id), + OperatorStatus::Slashed + ); + + let pending_slashes = PendingSlashes::::get(domain_id).unwrap(); + assert!(pending_slashes.contains(&operator_id)); + + assert_eq!( + Balances::total_balance(&crate::tests::TreasuryAccount::get()), + 0 + ); + + // since we only slash 10 nominators a time but we have a total of 21 nominators, + // do 3 iterations + do_slash_operator::(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap(); + do_slash_operator::(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap(); + do_slash_operator::(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap(); + + assert_eq!(PendingSlashes::::get(domain_id), None); + assert_eq!(Operators::::get(operator_id), None); + assert_eq!(OperatorIdOwner::::get(operator_id), None); + + assert_eq!( + Balances::total_balance(&operator_account), + operator_free_balance - operator_stake + ); + for nominator_account in nominator_accounts { + assert_eq!( + Balances::total_balance(&nominator_account), + nominator_free_balance - nominator_stake + ); + } + + assert!(Balances::total_balance(&crate::tests::TreasuryAccount::get()) >= 2220 * SSC); + assert_eq!(bundle_storage_fund::total_balance::(operator_id), 0); + }); + } + #[test] fn slash_operators() { let domain_id = DomainId::new(0); @@ -2771,12 +2952,21 @@ pub(crate) mod tests { ); } - do_slash_operators::(vec![operator_id_1], SlashedReason::InvalidBundle(1)) - .unwrap(); - do_slash_operators::(vec![operator_id_2], SlashedReason::InvalidBundle(2)) - .unwrap(); - do_slash_operators::(vec![operator_id_3], SlashedReason::InvalidBundle(3)) - .unwrap(); + do_mark_operators_as_slashed::( + vec![operator_id_1], + SlashedReason::InvalidBundle(1), + ) + .unwrap(); + do_mark_operators_as_slashed::( + vec![operator_id_2], + SlashedReason::InvalidBundle(2), + ) + .unwrap(); + do_mark_operators_as_slashed::( + vec![operator_id_3], + SlashedReason::InvalidBundle(3), + ) + .unwrap(); let domain_stake_summary = DomainStakingSummary::::get(domain_id).unwrap(); assert!(!domain_stake_summary.next_operators.contains(&operator_id_1)); @@ -2806,7 +2996,11 @@ pub(crate) mod tests { 0 ); - do_finalize_domain_current_epoch::(domain_id).unwrap(); + let slashed_operators = PendingSlashes::::get(domain_id).unwrap(); + slashed_operators.into_iter().for_each(|_| { + do_slash_operator::(domain_id, MAX_NOMINATORS_TO_SLASH).unwrap(); + }); + assert_eq!(PendingSlashes::::get(domain_id), None); assert_eq!(Operators::::get(operator_id_1), None); assert_eq!(OperatorIdOwner::::get(operator_id_1), None); diff --git a/crates/pallet-domains/src/staking_epoch.rs b/crates/pallet-domains/src/staking_epoch.rs index 9b08d0efa8..b76c6d4b1b 100644 --- a/crates/pallet-domains/src/staking_epoch.rs +++ b/crates/pallet-domains/src/staking_epoch.rs @@ -2,16 +2,19 @@ use crate::bundle_storage_fund::deposit_reserve_for_storage_fund; use crate::pallet::{ AccumulatedTreasuryFunds, Deposits, DomainStakingSummary, LastEpochStakingDistribution, - OperatorIdOwner, Operators, PendingSlashes, PendingStakingOperationCount, Withdrawals, + NominatorCount, OperatorIdOwner, Operators, PendingSlashes, PendingStakingOperationCount, + Withdrawals, }; use crate::staking::{ - do_convert_previous_epoch_deposits, do_convert_previous_epoch_withdrawal, DomainEpoch, - Error as TransitionError, OperatorStatus, SharePrice, WithdrawalInShares, + do_cleanup_operator, do_convert_previous_epoch_deposits, do_convert_previous_epoch_withdrawal, + DomainEpoch, Error as TransitionError, OperatorStatus, SharePrice, WithdrawalInShares, }; use crate::{ bundle_storage_fund, BalanceOf, Config, ElectionVerificationParams, Event, HoldIdentifier, OperatorEpochSharePrice, Pallet, }; +#[cfg(not(feature = "std"))] +use alloc::vec; use codec::{Decode, Encode}; use frame_support::traits::fungible::{Inspect, InspectHold, Mutate, MutateHold}; use frame_support::traits::tokens::{ @@ -30,12 +33,10 @@ use sp_std::collections::btree_set::BTreeSet; pub enum Error { FinalizeDomainEpochStaking(TransitionError), OperatorRewardStaking(TransitionError), - SlashOperator(TransitionError), } pub(crate) struct EpochTransitionResult { pub rewarded_operator_count: u32, - pub slashed_nominator_count: u32, pub finalized_operator_count: u32, pub completed_epoch_index: EpochIndex, } @@ -51,17 +52,12 @@ pub(crate) fn do_finalize_domain_current_epoch( // re stake operator's tax from the rewards let rewarded_operator_count = operator_take_reward_tax_and_stake::(domain_id)?; - // slash the operators - let slashed_nominator_count = - do_finalize_slashed_operators::(domain_id).map_err(Error::SlashOperator)?; - // finalize any withdrawals and then deposits let (completed_epoch_index, finalized_operator_count) = do_finalize_domain_epoch_staking::(domain_id)?; Ok(EpochTransitionResult { rewarded_operator_count, - slashed_nominator_count, finalized_operator_count, completed_epoch_index, }) @@ -331,160 +327,199 @@ pub(crate) fn mint_into_treasury(amount: BalanceOf) -> Option<()> Some(()) } -pub(crate) fn do_finalize_slashed_operators( +/// Slashes any pending slashed operators. +/// At max slashes the `max_nominator_count` under given operator +pub(crate) fn do_slash_operator( domain_id: DomainId, + max_nominator_count: u32, ) -> Result { - let mut slashed_nominator_count = 0; - for operator_id in PendingSlashes::::take(domain_id).unwrap_or_default() { - Operators::::try_mutate_exists(operator_id, |maybe_operator| { - // take the operator so this operator info is removed once we slash the operator. - let operator = maybe_operator - .take() - .ok_or(TransitionError::UnknownOperator)?; - - // remove OperatorOwner Details - OperatorIdOwner::::remove(operator_id); - - let staked_hold_id = T::HoldIdentifier::staking_staked(operator_id); - let mut total_stake = operator - .current_total_stake - .checked_add(&operator.current_epoch_rewards) - .ok_or(TransitionError::BalanceOverflow)?; - let total_shares = operator.current_total_shares; - let share_price = SharePrice::new::(total_shares, total_stake); + let mut slashed_nominators = vec![]; + let (operator_id, slashed_operators) = match PendingSlashes::::get(domain_id) { + None => return Ok(0), + Some(mut slashed_operators) => match slashed_operators.pop_first() { + None => { + PendingSlashes::::remove(domain_id); + return Ok(0); + } + Some(operator_id) => (operator_id, slashed_operators), + }, + }; - let storage_fund_hold_id = T::HoldIdentifier::storage_fund_withdrawal(operator_id); - let storage_fund_redeem_price = bundle_storage_fund::storage_fund_redeem_price::( - operator_id, - operator.total_storage_fee_deposit, - ); + Operators::::try_mutate_exists(operator_id, |maybe_operator| { + // take the operator so this operator info is removed once we slash the operator. + let mut operator = maybe_operator + .take() + .ok_or(TransitionError::UnknownOperator)?; + + let operator_owner = + OperatorIdOwner::::get(operator_id).ok_or(TransitionError::UnknownOperator)?; + + let staked_hold_id = T::HoldIdentifier::staking_staked(operator_id); - // transfer all the staked funds to the treasury account - // any gains will be minted to treasury account - Deposits::::drain_prefix(operator_id).try_for_each( - |(nominator_id, mut deposit)| { - let locked_amount = - T::Currency::balance_on_hold(&staked_hold_id, &nominator_id); - - // convert any previous epoch deposits - do_convert_previous_epoch_deposits::(operator_id, &mut deposit)?; - - // there maybe some withdrawals that are initiated in this epoch where operator was slashed - // then collect and include them to find the final stake amount - let (amount_ready_to_withdraw, shares_withdrew_in_current_epoch) = - Withdrawals::::take(operator_id, nominator_id.clone()) - .map(|mut withdrawal| { - do_convert_previous_epoch_withdrawal::( - operator_id, - &mut withdrawal, - )?; - Ok(( - withdrawal.total_withdrawal_amount, - withdrawal - .withdrawal_in_shares - .map(|WithdrawalInShares { shares, .. }| shares) - .unwrap_or_default(), - )) - }) - .unwrap_or(Ok((Zero::zero(), Zero::zero())))?; - - // include all the known shares and shares that were withdrawn in the current epoch - let nominator_shares = deposit - .known - .shares - .checked_add(&shares_withdrew_in_current_epoch) - .ok_or(TransitionError::ShareOverflow)?; - - // current staked amount - let nominator_staked_amount = - share_price.shares_to_stake::(nominator_shares); - - // do not slash the deposit that is not staked yet - let amount_to_slash_in_holding = locked_amount - .checked_sub( - &deposit - .pending - .map(|pending_deposit| pending_deposit.amount) + let mut total_stake = operator + .current_total_stake + .checked_add(&operator.current_epoch_rewards) + .ok_or(TransitionError::BalanceOverflow)?; + + operator.current_epoch_rewards = Zero::zero(); + let mut total_shares = operator.current_total_shares; + let share_price = SharePrice::new::(total_shares, total_stake); + + let mut total_storage_fee_deposit = operator.total_storage_fee_deposit; + let storage_fund_hold_id = T::HoldIdentifier::storage_fund_withdrawal(operator_id); + + // transfer all the staked funds to the treasury account + // any gains will be minted to treasury account + for (nominator_id, mut deposit) in Deposits::::iter_prefix(operator_id) { + let locked_amount = T::Currency::balance_on_hold(&staked_hold_id, &nominator_id); + + // convert any previous epoch deposits + do_convert_previous_epoch_deposits::(operator_id, &mut deposit)?; + + // there maybe some withdrawals that are initiated in this epoch where operator was slashed + // then collect and include them to find the final stake amount + let (amount_ready_to_withdraw, shares_withdrew_in_current_epoch) = + Withdrawals::::take(operator_id, nominator_id.clone()) + .map(|mut withdrawal| { + do_convert_previous_epoch_withdrawal::(operator_id, &mut withdrawal)?; + Ok(( + withdrawal.total_withdrawal_amount, + withdrawal + .withdrawal_in_shares + .map(|WithdrawalInShares { shares, .. }| shares) .unwrap_or_default(), - ) - .ok_or(TransitionError::BalanceUnderflow)?; + )) + }) + .unwrap_or(Ok((Zero::zero(), Zero::zero())))?; + + // include all the known shares and shares that were withdrawn in the current epoch + let nominator_shares = deposit + .known + .shares + .checked_add(&shares_withdrew_in_current_epoch) + .ok_or(TransitionError::ShareOverflow)?; + + // current staked amount + let nominator_staked_amount = share_price.shares_to_stake::(nominator_shares); + + // do not slash the deposit that is not staked yet + let amount_to_slash_in_holding = locked_amount + .checked_sub( + &deposit + .pending + .map(|pending_deposit| pending_deposit.amount) + .unwrap_or_default(), + ) + .ok_or(TransitionError::BalanceUnderflow)?; + + T::Currency::transfer_on_hold( + &staked_hold_id, + &nominator_id, + &T::TreasuryAccount::get(), + amount_to_slash_in_holding, + Precision::Exact, + Restriction::Free, + Fortitude::Force, + ) + .map_err(|_| TransitionError::RemoveLock)?; + + // these are nominator rewards that will be minted to treasury + // include amount ready to be withdrawn to calculate the final reward + let nominator_reward = nominator_staked_amount + .checked_add(&amount_ready_to_withdraw) + .ok_or(TransitionError::BalanceOverflow)? + .checked_sub(&amount_to_slash_in_holding) + .ok_or(TransitionError::BalanceUnderflow)?; + + mint_into_treasury::(nominator_reward).ok_or(TransitionError::MintBalance)?; + + total_stake = total_stake.saturating_sub(nominator_staked_amount); + total_shares = total_shares.saturating_sub(nominator_shares); + + // release rest of the deposited un staked amount back to nominator + T::Currency::release_all(&staked_hold_id, &nominator_id, Precision::BestEffort) + .map_err(|_| TransitionError::RemoveLock)?; + + // Transfer the deposited non-staked storage fee back to nominator + if let Some(pending_deposit) = deposit.pending { + let storage_fund_redeem_price = bundle_storage_fund::storage_fund_redeem_price::( + operator_id, + total_storage_fee_deposit, + ); - T::Currency::transfer_on_hold( - &staked_hold_id, - &nominator_id, - &T::TreasuryAccount::get(), - amount_to_slash_in_holding, - Precision::Exact, - Restriction::Free, - Fortitude::Force, - ) - .map_err(|_| TransitionError::RemoveLock)?; - - // these are nominator rewards that will be minted to treasury - // include amount ready to be withdrawn to calculate the final reward - let nominator_reward = nominator_staked_amount - .checked_add(&amount_ready_to_withdraw) - .ok_or(TransitionError::BalanceOverflow)? - .checked_sub(&amount_to_slash_in_holding) - .ok_or(TransitionError::BalanceUnderflow)?; - mint_into_treasury::(nominator_reward) - .ok_or(TransitionError::MintBalance)?; - - total_stake = total_stake.saturating_sub(nominator_staked_amount); - - // release rest of the deposited un staked amount back to nominator - T::Currency::release_all(&staked_hold_id, &nominator_id, Precision::BestEffort) - .map_err(|_| TransitionError::RemoveLock)?; - - // Transfer the deposited unstaked storage fee back to nominator - if let Some(pending_deposit) = deposit.pending { - let storage_fee_deposit = bundle_storage_fund::withdraw_and_hold::( - operator_id, - &nominator_id, - storage_fund_redeem_price.redeem(pending_deposit.storage_fee_deposit), - ) - .map_err(TransitionError::BundleStorageFund)?; - T::Currency::release( - &storage_fund_hold_id, - &nominator_id, - storage_fee_deposit, - Precision::Exact, - ) - .map_err(|_| TransitionError::RemoveLock)?; - } - - // Transfer all the storage fee on withdraw to the treasury - let withdraw_storage_fee_on_hold = - T::Currency::balance_on_hold(&storage_fund_hold_id, &nominator_id); - T::Currency::transfer_on_hold( - &storage_fund_hold_id, - &nominator_id, - &T::TreasuryAccount::get(), - withdraw_storage_fee_on_hold, - Precision::Exact, - Restriction::Free, - Fortitude::Force, - ) - .map_err(|_| TransitionError::RemoveLock)?; + let storage_fee_deposit = bundle_storage_fund::withdraw_and_hold::( + operator_id, + &nominator_id, + storage_fund_redeem_price.redeem(pending_deposit.storage_fee_deposit), + ) + .map_err(TransitionError::BundleStorageFund)?; - slashed_nominator_count += 1; + T::Currency::release( + &storage_fund_hold_id, + &nominator_id, + storage_fee_deposit, + Precision::Exact, + ) + .map_err(|_| TransitionError::RemoveLock)?; - Ok(()) - }, - )?; + total_storage_fee_deposit = + total_storage_fee_deposit.saturating_sub(pending_deposit.storage_fee_deposit); + } - // mint any gains to treasury account - mint_into_treasury::(total_stake).ok_or(TransitionError::MintBalance)?; + // Transfer all the storage fee on withdraw to the treasury + let withdraw_storage_fee_on_hold = + T::Currency::balance_on_hold(&storage_fund_hold_id, &nominator_id); + + T::Currency::transfer_on_hold( + &storage_fund_hold_id, + &nominator_id, + &T::TreasuryAccount::get(), + withdraw_storage_fee_on_hold, + Precision::Exact, + Restriction::Free, + Fortitude::Force, + ) + .map_err(|_| TransitionError::RemoveLock)?; + + // update nominator count. + let nominator_count = NominatorCount::::get(operator_id); + if operator_owner != nominator_id && nominator_count > 0 { + NominatorCount::::set(operator_id, nominator_count - 1); + } - // Transfer all the storage fund to treasury - bundle_storage_fund::transfer_all_to_treasury::(operator_id) - .map_err(TransitionError::BundleStorageFund)?; + slashed_nominators.push(nominator_id); + if slashed_nominators.len() as u32 >= max_nominator_count { + break; + } + } - Ok(()) - })?; - } + // for all slashed nominators, remove their deposits + let slashed_nominator_count = slashed_nominators.len() as u32; + slashed_nominators.into_iter().for_each(|nominator_id| { + Deposits::::remove(operator_id, nominator_id); + }); - Ok(slashed_nominator_count) + let nominator_count = NominatorCount::::get(operator_id); + let cleanup_operator = + nominator_count == 0 && !Deposits::::contains_key(operator_id, operator_owner); + + if cleanup_operator { + do_cleanup_operator::(operator_id, total_stake, operator.signing_key)?; + if slashed_operators.is_empty() { + PendingSlashes::::remove(domain_id); + } else { + PendingSlashes::::set(domain_id, Some(slashed_operators)); + } + } else { + // set update total shares, total stake and total storage fee deposit for operator + operator.current_total_shares = total_shares; + operator.current_total_stake = total_stake; + operator.total_storage_fee_deposit = total_storage_fee_deposit; + *maybe_operator = Some(operator); + } + + Ok(slashed_nominator_count) + }) } #[cfg(test)] diff --git a/crates/pallet-domains/src/tests.rs b/crates/pallet-domains/src/tests.rs index c66377beca..7aa490e37e 100644 --- a/crates/pallet-domains/src/tests.rs +++ b/crates/pallet-domains/src/tests.rs @@ -1,5 +1,6 @@ use crate::block_tree::BlockTreeNode; use crate::domain_registry::{DomainConfig, DomainObject}; +use crate::pallet::OperatorIdOwner; use crate::staking::Operator; use crate::{ self as pallet_domains, BalanceOf, BlockSlot, BlockTree, BlockTreeNodes, BundleError, Config, @@ -60,7 +61,7 @@ frame_support::construct_runtime!( type BlockNumber = u64; type Hash = H256; -type AccountId = u128; +pub(crate) type AccountId = u128; #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] impl frame_system::Config for Test { @@ -145,7 +146,7 @@ parameter_types! { pub TreasuryAccount: u128 = PalletId(*b"treasury").into_account_truncating(); pub const BlockReward: Balance = 10 * SSC; pub const MaxPendingStakingOperation: u32 = 512; - pub const MaxNominators: u32 = 5; + pub const MaxNominators: u32 = 25; pub const DomainsPalletId: PalletId = PalletId(*b"domains_"); pub const DomainChainByteFee: Balance = 1; pub const MaxInitialDomainAccounts: u32 = 5; @@ -483,6 +484,7 @@ pub(crate) fn register_genesis_domain(creator: u128, operator_ids: Vec::insert(operator_id, Operator::dummy(domain_id, pair.public(), SSC)); + OperatorIdOwner::::insert(operator_id, creator); } domain_id