diff --git a/substrate/frame/staking/src/lib.rs b/substrate/frame/staking/src/lib.rs index 09185a690be1..3a1bb5837c83 100644 --- a/substrate/frame/staking/src/lib.rs +++ b/substrate/frame/staking/src/lib.rs @@ -294,6 +294,7 @@ pub mod inflation; pub mod ledger; pub mod migrations; pub mod slashing; +pub mod traits; pub mod weights; mod pallet; diff --git a/substrate/frame/staking/src/pallet/impls.rs b/substrate/frame/staking/src/pallet/impls.rs index 8b45430c688e..6cf4f7b1603c 100644 --- a/substrate/frame/staking/src/pallet/impls.rs +++ b/substrate/frame/staking/src/pallet/impls.rs @@ -51,9 +51,12 @@ use crate::{ election_size_tracker::StaticTracker, log, slashing, weights::WeightInfo, ActiveEraInfo, BalanceOf, EraInfo, EraPayout, Exposure, ExposureOf, Forcing, IndividualExposure, MaxNominationsOf, MaxWinnersOf, Nominations, NominationsQuota, PositiveImbalanceOf, - RewardDestination, SessionInterface, StakingLedger, ValidatorPrefs, + RewardDestination, SessionInterface, StakingLedger, traits::FusionExt, ValidatorPrefs, }; +// FUSION CHANGE +// use pallet_fusion::FusionExt; + use super::pallet::*; #[cfg(feature = "try-runtime")] @@ -336,7 +339,14 @@ impl Pallet { if amount.is_zero() { return None } - let dest = Self::payee(StakingAccount::Stash(stash.clone()))?; + // FUSION CHANGE + let dest = Self::payee(StakingAccount::Stash(stash.clone())).or_else(|| { + if T::FusionExt::get_pool_id_from_funds_account(stash).is_some() { + Some(RewardDestination::Account(stash.clone())) + } else { + None + } + })?; let maybe_imbalance = match dest { RewardDestination::Stash => T::Currency::deposit_into_existing(stash, amount).ok(), @@ -492,6 +502,9 @@ impl Pallet { }); Self::apply_unapplied_slashes(active_era); + + // FUSION CHANGE + T::FusionExt::set_fusion_exposures(); } /// Compute payout for era. @@ -502,8 +515,12 @@ impl Pallet { let era_duration = (now_as_millis_u64.defensive_saturating_sub(active_era_start)) .saturated_into::(); - let staked = Self::eras_total_stake(&active_era.index); + let mut staked = Self::eras_total_stake(&active_era.index); let issuance = T::Currency::total_issuance(); + // FUSION CHANGE + if staked > issuance { + staked = issuance; + } let (validator_payout, remainder) = T::EraPayout::era_payout(staked, issuance, era_duration); @@ -517,6 +534,9 @@ impl Pallet { >::insert(&active_era.index, validator_payout); T::RewardRemainder::on_unbalanced(T::Currency::issue(remainder)); + // FUSION CHANGE + T::FusionExt::handle_end_era(active_era.index, era_duration); + // Clear offending validators. >::kill(); } @@ -671,6 +691,10 @@ impl Pallet { T::CurrencyToVote::to_currency(e, total_issuance) }; + let active_era = ActiveEra::::get() + .map(|era_info| era_info.index) + .unwrap_or(0); + supports .into_iter() .map(|(validator, support)| { @@ -686,6 +710,11 @@ impl Pallet { if nominator == validator { own = own.saturating_add(stake); } else { + // FUSION CHANGE + // This will update the fusion exposure in case the nominator is a fusion pool. + let _ = T::FusionExt::update_pool_exposure( + &nominator, &validator, stake, active_era, + ); others.push(IndividualExposure { who: nominator, value: stake }); } total = total.saturating_add(stake); @@ -825,6 +854,14 @@ impl Pallet { bounds.count.unwrap_or(all_voter_count.into()).min(all_voter_count.into()).0 }; + // FUSION CHANGE + // We account for the fusion voters count in the final_predicted_len. + // We do not update final_predicted_len as the next 'while' loop would have been unecessary longer. + let fusion_voters_count = T::FusionExt::get_active_pool_count() + .try_into() + .unwrap_or(u32::MIN); + let final_predicted_len = final_predicted_len.saturating_add(fusion_voters_count); + let mut all_voters = Vec::<_>::with_capacity(final_predicted_len as usize); // cache a few things. @@ -835,76 +872,107 @@ impl Pallet { let mut nominators_taken = 0u32; let mut min_active_stake = u64::MAX; + // FUSION CHANGE + let mut snapshot_voters_size_exceeded = false; + if fusion_voters_count > 0 { + let fusion_voters = T::FusionExt::get_fusion_voters(); + for (account, value, targets) in fusion_voters.into_iter() { + let Ok(bounded_targets) = BoundedVec::try_from(targets) else { + log::error!("Failed to convert targets for account: {:?}", account); + continue; + }; + let fusion_vote = (account, value, bounded_targets); + if voters_size_tracker + .try_register_voter(&fusion_vote, &bounds) + .is_err() + { + // No more space left for the election snapshot, stop iterating. + Self::deposit_event(Event::::SnapshotVotersSizeExceeded { + size: voters_size_tracker.size as u32, + }); + snapshot_voters_size_exceeded = true; + break; + } + all_voters.push(fusion_vote); + nominators_taken.saturating_inc(); + if value < min_active_stake { + min_active_stake = value; + } + } + } + let mut sorted_voters = T::VoterList::iter(); - while all_voters.len() < final_predicted_len as usize && - voters_seen < (NPOS_MAX_ITERATIONS_COEFFICIENT * final_predicted_len as u32) - { - let voter = match sorted_voters.next() { - Some(voter) => { - voters_seen.saturating_inc(); - voter - }, - None => break, - }; + if !snapshot_voters_size_exceeded { + while all_voters.len() < final_predicted_len as usize && + voters_seen < (NPOS_MAX_ITERATIONS_COEFFICIENT * final_predicted_len as u32) + { + let voter = match sorted_voters.next() { + Some(voter) => { + voters_seen.saturating_inc(); + voter + }, + None => break, + }; - let voter_weight = weight_of(&voter); - // if voter weight is zero, do not consider this voter for the snapshot. - if voter_weight.is_zero() { - log!(debug, "voter's active balance is 0. skip this voter."); - continue - } + let voter_weight = weight_of(&voter); + // if voter weight is zero, do not consider this voter for the snapshot. + if voter_weight.is_zero() { + log!(debug, "voter's active balance is 0. skip this voter."); + continue + } - if let Some(Nominations { targets, .. }) = >::get(&voter) { - if !targets.is_empty() { - // Note on lazy nomination quota: we do not check the nomination quota of the - // voter at this point and accept all the current nominations. The nomination - // quota is only enforced at `nominate` time. + if let Some(Nominations { targets, .. }) = >::get(&voter) { + if !targets.is_empty() { + // Note on lazy nomination quota: we do not check the nomination quota of the + // voter at this point and accept all the current nominations. The nomination + // quota is only enforced at `nominate` time. + + let voter = (voter, voter_weight, targets); + if voters_size_tracker.try_register_voter(&voter, &bounds).is_err() { + // no more space left for the election result, stop iterating. + Self::deposit_event(Event::::SnapshotVotersSizeExceeded { + size: voters_size_tracker.size as u32, + }); + break + } + + all_voters.push(voter); + nominators_taken.saturating_inc(); + } else { + // technically should never happen, but not much we can do about it. + } + min_active_stake = + if voter_weight < min_active_stake { voter_weight } else { min_active_stake }; + } else if Validators::::contains_key(&voter) { + // if this voter is a validator: + let self_vote = ( + voter.clone(), + voter_weight, + vec![voter.clone()] + .try_into() + .expect("`MaxVotesPerVoter` must be greater than or equal to 1"), + ); - let voter = (voter, voter_weight, targets); - if voters_size_tracker.try_register_voter(&voter, &bounds).is_err() { - // no more space left for the election result, stop iterating. + if voters_size_tracker.try_register_voter(&self_vote, &bounds).is_err() { + // no more space left for the election snapshot, stop iterating. Self::deposit_event(Event::::SnapshotVotersSizeExceeded { size: voters_size_tracker.size as u32, }); break } - - all_voters.push(voter); - nominators_taken.saturating_inc(); + all_voters.push(self_vote); + validators_taken.saturating_inc(); } else { - // technically should never happen, but not much we can do about it. - } - min_active_stake = - if voter_weight < min_active_stake { voter_weight } else { min_active_stake }; - } else if Validators::::contains_key(&voter) { - // if this voter is a validator: - let self_vote = ( - voter.clone(), - voter_weight, - vec![voter.clone()] - .try_into() - .expect("`MaxVotesPerVoter` must be greater than or equal to 1"), - ); - - if voters_size_tracker.try_register_voter(&self_vote, &bounds).is_err() { - // no more space left for the election snapshot, stop iterating. - Self::deposit_event(Event::::SnapshotVotersSizeExceeded { - size: voters_size_tracker.size as u32, - }); - break + // this can only happen if: 1. there a bug in the bags-list (or whatever is the + // sorted list) logic and the state of the two pallets is no longer compatible, or + // because the nominators is not decodable since they have more nomination than + // `T::NominationsQuota::get_quota`. The latter can rarely happen, and is not + // really an emergency or bug if it does. + defensive!( + "DEFENSIVE: invalid item in `VoterList`: {:?}, this nominator probably has too many nominations now", + voter, + ); } - all_voters.push(self_vote); - validators_taken.saturating_inc(); - } else { - // this can only happen if: 1. there a bug in the bags-list (or whatever is the - // sorted list) logic and the state of the two pallets is no longer compatible, or - // because the nominators is not decodable since they have more nomination than - // `T::NominationsQuota::get_quota`. The latter can rarely happen, and is not - // really an emergency or bug if it does. - defensive!( - "DEFENSIVE: invalid item in `VoterList`: {:?}, this nominator probably has too many nominations now", - voter, - ); } } @@ -1369,6 +1437,9 @@ where consumed_weight += T::DbWeight::get().reads_writes(reads, writes); }; + // FUSION CHANGE + let mut fusion_weight = Weight::from_parts(0, 0); + let active_era = { let active_era = Self::active_era(); add_db_reads_writes(1, 0); @@ -1445,6 +1516,13 @@ where add_db_reads_writes(rw, rw); } unapplied.reporters = details.reporters.clone(); + + // FUSION CHANGE + // We need to notify slashing in Fusion and record the funds, if needed. + // We need to do this so if the slash is applied manually, we find it + fusion_weight = + T::FusionExt::add_fusion_slash(slash_era, &stash, &unapplied.others); + if slash_defer_duration == 0 { // Apply right away. slashing::apply_slash::(unapplied, slash_era); @@ -1477,7 +1555,7 @@ where } } - consumed_weight + consumed_weight.saturating_add(fusion_weight) } } diff --git a/substrate/frame/staking/src/pallet/mod.rs b/substrate/frame/staking/src/pallet/mod.rs index 0689418d02bd..57eeaa36660d 100644 --- a/substrate/frame/staking/src/pallet/mod.rs +++ b/substrate/frame/staking/src/pallet/mod.rs @@ -50,7 +50,7 @@ use crate::{ slashing, weights::WeightInfo, AccountIdLookupOf, ActiveEraInfo, BalanceOf, EraPayout, EraRewardPoints, Exposure, ExposurePage, Forcing, MaxNominationsOf, NegativeImbalanceOf, Nominations, NominationsQuota, PositiveImbalanceOf, RewardDestination, SessionInterface, - StakingLedger, UnappliedSlash, UnlockChunk, ValidatorPrefs, + StakingLedger, traits::FusionExt, UnappliedSlash, UnlockChunk, ValidatorPrefs, }; // The speculative number of spans are used as an input of the weight annotation of @@ -61,6 +61,7 @@ pub(crate) const SPECULATIVE_NUM_SPANS: u32 = 32; #[frame_support::pallet] pub mod pallet { use frame_election_provider_support::ElectionDataProvider; + // use pallet_fusion::FusionExt; use crate::{BenchmarkingConfig, PagedExposureMetadata}; @@ -283,6 +284,9 @@ pub mod pallet { /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; + + /// Fusion pallet trait + type FusionExt: FusionExt, u32>; } /// The ideal number of active validators. @@ -1533,9 +1537,16 @@ pub mod pallet { let last_item = slash_indices[slash_indices.len() - 1]; ensure!((last_item as usize) < unapplied.len(), Error::::InvalidSlashIndex); + // FUSION CHANGE + let mut removed_slash_validators = Vec::new(); for (removed, index) in slash_indices.into_iter().enumerate() { let index = (index as usize) - removed; - unapplied.remove(index); + let removed_element = unapplied.remove(index); + removed_slash_validators.push(removed_element.validator); + } + + if !removed_slash_validators.is_empty() { + T::FusionExt::cancel_fusion_slash(era, &removed_slash_validators); } UnappliedSlashes::::insert(&era, &unapplied); diff --git a/substrate/frame/staking/src/slashing.rs b/substrate/frame/staking/src/slashing.rs index 709fd1441ec3..d669b5a201c3 100644 --- a/substrate/frame/staking/src/slashing.rs +++ b/substrate/frame/staking/src/slashing.rs @@ -51,7 +51,7 @@ use crate::{ BalanceOf, Config, Error, Exposure, NegativeImbalanceOf, NominatorSlashInEra, - OffendingValidators, Pallet, Perbill, SessionInterface, SpanSlash, UnappliedSlash, + OffendingValidators, Pallet, Perbill, SessionInterface, SpanSlash, traits::FusionExt, UnappliedSlash, ValidatorSlashInEra, }; use codec::{Decode, Encode, MaxEncodedLen}; @@ -59,6 +59,7 @@ use frame_support::{ ensure, traits::{Currency, Defensive, DefensiveSaturating, Get, Imbalance, OnUnbalanced}, }; +// use pallet_fusion::FusionExt; use scale_info::TypeInfo; use sp_runtime::{ traits::{Saturating, Zero}, @@ -648,13 +649,18 @@ pub(crate) fn apply_slash( ); for &(ref nominator, nominator_slash) in &unapplied_slash.others { - do_slash::( - nominator, - nominator_slash, - &mut reward_payout, - &mut slashed_imbalance, - slash_era, - ); + // FUSION CHANGE + let is_fusion_pool = + T::FusionExt::apply_fusion_slash(slash_era, &unapplied_slash.validator, nominator); + if !is_fusion_pool { + do_slash::( + nominator, + nominator_slash, + &mut reward_payout, + &mut slashed_imbalance, + slash_era, + ); + } } pay_reporters::(reward_payout, slashed_imbalance, &unapplied_slash.reporters); diff --git a/substrate/frame/staking/src/traits.rs b/substrate/frame/staking/src/traits.rs new file mode 100644 index 000000000000..57469620228e --- /dev/null +++ b/substrate/frame/staking/src/traits.rs @@ -0,0 +1,98 @@ +use crate::*; + +// A trait for Fusion operations with a generic `AccountId` and `Balance` and `PoolId`. +pub trait FusionExt { + /// Handles the change of an era, which includes operations like distributing rewards and cleaning up old data. + fn handle_end_era(era: EraIndex, era_duration: u64) -> (); + + /// Set the exposure for each pool for reward computation + /// Exposure is set at the beginning of the era N for era N using stake from era N-1 + fn set_fusion_exposures() -> (); + + /// Return the fusion voters to add to the staking pallet + fn get_fusion_voters() -> Vec<(AccountId, u64, Vec)>; + + /// Return the fusion voters count + fn get_active_pool_count() -> usize; + + /// Returns the pool id if the account is a pool funds account + fn get_pool_id_from_funds_account(account: &AccountId) -> Option; + + /// Updates the Fusion exposure with election data result + fn update_pool_exposure( + maybe_pool_account: &AccountId, + validator: &AccountId, + value: Balance, + era: EraIndex, + ) -> (); + + /// In the staking pallet, if a pool was slashed, we record an unapplied slash + fn add_fusion_slash( + era: EraIndex, + validator: &AccountId, + nominators: &Vec<(AccountId, Balance)>, + ) -> Weight; + + /// If a slash was cancelled and it concerned a Fusion pool, we need to cancel it there too + fn cancel_fusion_slash(era: EraIndex, slash_validators: &Vec) -> (); + + /// If a slash is applied, we need to intercept it and take the corresponding fusion currencies + /// Returns true if the nominator is a fusion pool (regardless if it succeed to get slashed) + /// In this function we will give 100% of the slash amount to the treasury, + /// the rewards for validator are going to get minted in the staking pallet like before + fn apply_fusion_slash( + slash_era: EraIndex, + validator: &AccountId, + funds_account: &AccountId, + ) -> bool; +} +impl FusionExt for () { + fn handle_end_era(_era: EraIndex, _era_duration: u64) { + () + } + + fn set_fusion_exposures() { + () + } + + fn get_fusion_voters() -> Vec<(AccountId, u64, Vec)> { + Vec::default() + } + + fn get_active_pool_count() -> usize { + 0 + } + + fn get_pool_id_from_funds_account(_account: &AccountId) -> Option { + None + } + + fn update_pool_exposure( + _maybe_pool_account: &AccountId, + _validator: &AccountId, + _value: Balance, + _era: EraIndex, + ) { + () + } + + fn add_fusion_slash( + _era: EraIndex, + _validator: &AccountId, + _nominators: &Vec<(AccountId, Balance)>, + ) -> Weight { + Weight::from_parts(0, 0) + } + + fn cancel_fusion_slash(_era: EraIndex, _slash_validators: &Vec) -> () { + () + } + + fn apply_fusion_slash( + _slash_era: EraIndex, + _validator: &AccountId, + _funds_account: &AccountId, + ) -> bool { + false + } +}