diff --git a/pallet/account-migration/Cargo.toml b/pallet/account-migration/Cargo.toml index eecf991ff..9239047ed 100644 --- a/pallet/account-migration/Cargo.toml +++ b/pallet/account-migration/Cargo.toml @@ -80,7 +80,7 @@ try-runtime = [ # darwinia "darwinia-deposit/try-runtime", "darwinia-staking/try-runtime", - + # substrate "frame-support/try-runtime", "frame-system/try-runtime", diff --git a/pallet/account-migration/src/mock.rs b/pallet/account-migration/src/mock.rs index 06c4eeac7..a0edb2558 100644 --- a/pallet/account-migration/src/mock.rs +++ b/pallet/account-migration/src/mock.rs @@ -160,14 +160,16 @@ impl darwinia_deposit::Config for Runtime { } impl darwinia_staking::Config for Runtime { + type Currency = Balances; type Deposit = Deposit; + type InflationManager = (); type Kton = Dummy; type MaxDeposits = (); type MaxUnstakings = (); type MinStakingDuration = (); - type OnSessionEnd = (); type Ring = Dummy; type RuntimeEvent = RuntimeEvent; + type ShouldEndSession = (); type WeightInfo = (); } #[cfg(not(feature = "runtime-benchmarks"))] diff --git a/pallet/staking/Cargo.toml b/pallet/staking/Cargo.toml index 76d51aec2..6169da550 100644 --- a/pallet/staking/Cargo.toml +++ b/pallet/staking/Cargo.toml @@ -80,6 +80,7 @@ runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", + "pallet-assets/runtime-benchmarks", "sp-runtime/runtime-benchmarks", ] diff --git a/pallet/staking/src/benchmarking.rs b/pallet/staking/src/benchmarking.rs index 342e4f6c2..f218f10d0 100644 --- a/pallet/staking/src/benchmarking.rs +++ b/pallet/staking/src/benchmarking.rs @@ -198,6 +198,32 @@ mod benchmarks { _(RawOrigin::Signed(a)); } + #[benchmark] + fn payout() { + let a = frame_benchmarking::whitelisted_caller::(); + let sender = a.clone(); + + assert_eq!(>::get().0, ExposureCacheState::Previous); + + >::insert( + &a, + Exposure { + commission: Perbill::zero(), + vote: 32, + nominators: (0..32) + .map(|i| IndividualExposure { + who: frame_benchmarking::account("", i, i), + vote: 1, + }) + .collect(), + }, + ); + >::insert(&a, 500); + + #[extrinsic_call] + _(RawOrigin::Signed(sender), a); + } + #[benchmark] fn set_collator_count() { // Worst-case scenario: diff --git a/pallet/staking/src/lib.rs b/pallet/staking/src/lib.rs index 3c59069da..030f46b07 100644 --- a/pallet/staking/src/lib.rs +++ b/pallet/staking/src/lib.rs @@ -28,12 +28,12 @@ //! - KTON: Darwinia's commitment token //! - Deposit: Locking RINGs' ticket -// TODO: nomination upper limit - #![cfg_attr(not(feature = "std"), no_std)] #![deny(missing_docs)] #![deny(unused_crate_dependencies)] +pub mod migration; + #[cfg(test)] mod mock; #[cfg(test)] @@ -52,19 +52,44 @@ use codec::FullCodec; // darwinia use dc_types::{Balance, Moment}; // substrate -use frame_support::{pallet_prelude::*, DefaultNoBound, EqNoBound, PalletId, PartialEqNoBound}; +use frame_support::{ + pallet_prelude::*, traits::Currency, DefaultNoBound, EqNoBound, PalletId, PartialEqNoBound, +}; use frame_system::{pallet_prelude::*, RawOrigin}; use sp_runtime::{ - traits::{AccountIdConversion, Convert}, + traits::{AccountIdConversion, Convert, One, Zero}, Perbill, Perquintill, }; use sp_std::{collections::btree_map::BTreeMap, prelude::*}; +macro_rules! call_on_exposure { + ($s_e:expr, $s:ident$($f:tt)*) => {{ + match $s_e { + (ExposureCacheState::$s, _, _) => Ok(>$($f)*), + (_, ExposureCacheState::$s, _) => Ok(>$($f)*), + (_, _, ExposureCacheState::$s) => Ok(>$($f)*), + _ => { + log::error!("[pallet::staking] exposure cache states must be correct; qed"); + + Err("[pallet::staking] exposure cache states must be correct; qed") + }, + } + }}; + ($s:ident$($f:tt)*) => {{ + let s = >::get(); + + call_on_exposure!(s, $s$($f)*) + }}; +} + #[frame_support::pallet] pub mod pallet { // darwinia use crate::*; + // TODO: limit the number of nominators that a collator can have. + // const MAX_NOMINATIONS: u32 = 32; + // Deposit helper for runtime benchmark. #[cfg(feature = "runtime-benchmarks")] use darwinia_deposit::Config as DepositConfig; @@ -89,8 +114,14 @@ pub mod pallet { /// Deposit [`StakeExt`] interface. type Deposit: StakeExt; - /// On session end handler. - type OnSessionEnd: OnSessionEnd; + /// Currency interface to pay the reward. + type Currency: Currency; + + /// Inflation and reward manager. + type InflationManager: InflationManager; + + /// Pass [`pallet_session::Config::ShouldEndSession`]'s result to here. + type ShouldEndSession: Get; /// Minimum time to stake at least. #[pallet::constant] @@ -133,7 +164,7 @@ pub mod pallet { amount: Balance, }, /// Unable to pay the staker's reward. - Unpaied { + Unpaid { staker: T::AccountId, amount: Balance, }, @@ -157,6 +188,8 @@ pub mod pallet { TargetNotCollator, /// Collator count mustn't be zero. ZeroCollatorCount, + /// No reward to pay for this collator. + NoReward, } /// All staking ledgers. @@ -181,18 +214,62 @@ pub mod pallet { #[pallet::getter(fn collator_of)] pub type Collators = StorageMap<_, Twox64Concat, T::AccountId, Perbill>; - /// Current stakers' exposure. + /// Exposure cache states. + /// + /// To avoid extra DB RWs during new session, such as: + /// ```nocompile + /// previous = current; + /// current = next; + /// next = elect(); + /// ``` + /// + /// Now, with data: + /// ```nocompile + /// cache1 == previous; + /// cache2 == current; + /// cache3 == next; + /// ``` + /// Just need to shift the marker and write the storage map once: + /// ```nocompile + /// mark(cache3, current); + /// mark(cache2, previous); + /// mark(cache1, next); + /// cache1 = elect(); + /// ``` + #[pallet::storage] + #[pallet::getter(fn exposure_cache_states)] + pub type ExposureCacheStates = StorageValue< + _, + (ExposureCacheState, ExposureCacheState, ExposureCacheState), + ValueQuery, + ExposureCacheStatesDefault, + >; + /// Default value for [`ExposureCacheStates`]. + #[pallet::type_value] + pub fn ExposureCacheStatesDefault( + ) -> (ExposureCacheState, ExposureCacheState, ExposureCacheState) { + (ExposureCacheState::Previous, ExposureCacheState::Current, ExposureCacheState::Next) + } + + /// Exposure cache 0. + #[pallet::storage] + #[pallet::unbounded] + #[pallet::getter(fn exposure_cache_0_of)] + pub type ExposureCache0 = + StorageMap<_, Twox64Concat, T::AccountId, Exposure>; + + /// Exposure cache 1. #[pallet::storage] #[pallet::unbounded] - #[pallet::getter(fn exposure_of)] - pub type Exposures = + #[pallet::getter(fn exposure_cache_1_of)] + pub type ExposureCache1 = StorageMap<_, Twox64Concat, T::AccountId, Exposure>; - /// Next stakers' exposure. + /// Exposure cache 2. #[pallet::storage] #[pallet::unbounded] - #[pallet::getter(fn next_exposure_of)] - pub type NextExposures = + #[pallet::getter(fn exposure_cache_2_of)] + pub type ExposureCache2 = StorageMap<_, Twox64Concat, T::AccountId, Exposure>; /// The ideal number of active collators. @@ -206,12 +283,18 @@ pub mod pallet { #[pallet::getter(fn nominator_of)] pub type Nominators = StorageMap<_, Twox64Concat, T::AccountId, T::AccountId>; - /// Collator's reward points. + /// Number of blocks authored by the collator within current session. + #[pallet::storage] + #[pallet::unbounded] + #[pallet::getter(fn authored_block_count)] + pub type AuthoredBlocksCount = + StorageValue<_, (BlockNumberFor, BTreeMap>), ValueQuery>; + + /// All outstanding rewards since the last payment. #[pallet::storage] #[pallet::unbounded] - #[pallet::getter(fn reward_points)] - pub type RewardPoints = - StorageValue<_, (RewardPoint, BTreeMap), ValueQuery>; + #[pallet::getter(fn pending_reward_of)] + pub type PendingRewards = StorageMap<_, Twox64Concat, T::AccountId, Balance>; /// Active session's start-time. #[pallet::storage] @@ -260,6 +343,24 @@ pub mod pallet { #[pallet::pallet] pub struct Pallet(PhantomData); + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(_: BlockNumberFor) -> Weight { + // There are already plenty of tasks to handle during the new session, + // so refrain from assigning any additional ones here. + if !T::ShouldEndSession::get() { + call_on_exposure!(Previous::iter_keys() + // TODO?: make this value adjustable + .drain() + .take(1) + .fold(Zero::zero(), |acc, e| acc + + Self::payout_inner(e).unwrap_or(Zero::zero()))) + .unwrap_or_default() + } else { + Zero::zero() + } + } + } #[pallet::call] impl Pallet { /// Add stakes to the staking pool. @@ -364,8 +465,6 @@ pub mod pallet { DispatchResult::Ok(()) })?; - // TODO: event? - Ok(()) } @@ -411,8 +510,6 @@ pub mod pallet { DispatchResult::Ok(()) })?; - // TODO: event? - Ok(()) } @@ -426,8 +523,6 @@ pub mod pallet { Self::claim_unstakings(&who)?; Self::try_clean_ledger_of(&who); - // TODO: event? - Ok(()) } @@ -463,8 +558,6 @@ pub mod pallet { >::mutate(&who, |n| *n = Some(target)); - // TODO: event? - Ok(()) } @@ -481,7 +574,16 @@ pub mod pallet { >::remove(&who); >::remove(&who); - // TODO: event? + Ok(()) + } + + /// Making the payout for the specified collators and its nominators. + #[pallet::call_index(8)] + #[pallet::weight(::WeightInfo::payout())] + pub fn payout(origin: OriginFor, who: T::AccountId) -> DispatchResult { + ensure_signed(origin)?; + + Self::payout_inner(who)?; Ok(()) } @@ -735,155 +837,199 @@ pub mod pallet { }); } - /// Add reward points to collators using their account id. - pub fn reward_by_ids(collators: &[(T::AccountId, RewardPoint)]) { - >::mutate(|(total, reward_map)| { - collators.iter().cloned().for_each(|(c, p)| { - *total += p; + /// Update the record of block production. + pub fn note_authors(authors: &[T::AccountId]) { + >::mutate(|(sum, map)| { + authors.iter().cloned().for_each(|c| { + *sum += One::one(); - reward_map.entry(c).and_modify(|p_| *p_ += p).or_insert(p); + map.entry(c).and_modify(|p_| *p_ += One::one()).or_insert(One::one()); }); }); } - // Power is a mixture of RING and KTON. - // - `total_ring_power = (amount / total_staked_ring) * HALF_POWER` - // - `total_kton_power = (amount / total_staked_kton) * HALF_POWER` - fn balance2power

(amount: Balance) -> Power - where - P: frame_support::StorageValue, - { - (Perquintill::from_rational(amount, P::get().max(1)) * 500_000_000_u128) as _ + /// Calculate the power of the given account. + #[cfg(any(feature = "runtime-benchmarks", test))] + pub fn quick_power_of(who: &T::AccountId) -> Power { + Self::power_of(who, >::get(), >::get()) } /// Calculate the power of the given account. - pub fn power_of(who: &T::AccountId) -> Power { + /// + /// This is an optimized version of [`Self::quick_power_of`]. + /// Avoiding read the pools' storage multiple times. + pub fn power_of(who: &T::AccountId, ring_pool: Balance, kton_pool: Balance) -> Power { + // Power is a mixture of RING and KTON. + // - `total_ring_power = (amount / total_staked_ring) * HALF_POWER` + // - `total_kton_power = (amount / total_staked_kton) * HALF_POWER` + + const HALF_POWER: u128 = 500_000_000; + >::get(who) .map(|l| { - Self::balance2power::>( + (Perquintill::from_rational( l.staked_ring + l.staked_deposits .into_iter() // We don't care if the deposit exists here. // It was guaranteed by the `stake`/`unstake`/`restake` functions. .fold(0, |r, d| r + T::Deposit::amount(who, d).unwrap_or_default()), - ) + Self::balance2power::>(l.staked_kton) + ring_pool.max(1), + ) * HALF_POWER) + .saturating_add( + Perquintill::from_rational(l.staked_kton, kton_pool.max(1)) + * HALF_POWER, + ) as _ }) .unwrap_or_default() } - // TODO: weight - /// Pay the session reward to the stakers. - pub fn payout(amount: Balance) -> Balance { - let (total_points, reward_map) = >::get(); - // Due to the `payout * percent` there might be some losses. - let mut actual_payout = 0; - - for (c, p) in reward_map { - let Some(commission) = >::get(&c) else { - #[cfg(test)] - panic!("[pallet::staking] collator({c:?}) must be found; qed"); - #[cfg(not(test))] - { - log::error!("[pallet::staking] collator({c:?}) must be found; qed"); - - continue; - } - }; - let c_total_payout = Perbill::from_rational(p, total_points) * amount; - let mut c_payout = commission * c_total_payout; - let n_payout = c_total_payout - c_payout; - let Some(c_exposure) = >::get(&c) else { - #[cfg(test)] - panic!("[pallet::staking] exposure({c:?}) must be found; qed"); - #[cfg(not(test))] - { - log::error!("[pallet::staking] exposure({c:?}) must be found; qed"); - - continue; - } - }; - - for n_exposure in c_exposure.nominators { - let n_payout = - Perbill::from_rational(n_exposure.vote, c_exposure.vote) * n_payout; + /// Distribute the session reward to staking pot and update the stakers' reward record. + pub fn distribute_session_reward(amount: Balance) -> Balance { + let (sum, map) = >::take(); + let staking_pot = account_id(); + let mut actual_reward = 0; + let mut unpaid = 0; - if c == n_exposure.who { - // If the collator nominated themselves. + map.into_iter().for_each(|(c, p)| { + let r = Perbill::from_rational(p, sum) * amount; - c_payout += n_payout; - } else if T::OnSessionEnd::reward(&n_exposure.who, n_payout).is_ok() { - actual_payout += n_payout; + >::mutate(&c, |u| *u = u.map(|u| u + r).or(Some(r))); - Self::deposit_event(Event::Payout { - staker: n_exposure.who, - amount: n_payout, - }); - } else { - Self::deposit_event(Event::Unpaied { - staker: n_exposure.who, - amount: n_payout, - }); - } + if T::InflationManager::reward(&staking_pot, r).is_ok() { + actual_reward += r; + } else { + unpaid += r; } + }); + + Self::deposit_event(Event::Payout { + staker: staking_pot.clone(), + amount: actual_reward, + }); - if T::OnSessionEnd::reward(&c, c_payout).is_ok() { - actual_payout += c_payout; + if unpaid != 0 { + Self::deposit_event(Event::Unpaid { staker: staking_pot, amount: unpaid }); + } + + actual_reward + } - Self::deposit_event(Event::Payout { staker: c, amount: c_payout }); + /// Pay the reward to the collator and its nominators. + pub fn payout_inner(collator: T::AccountId) -> Result { + let c_exposure = + call_on_exposure!(Previous::get(&collator).ok_or(>::NoReward)?)?; + let c_total_payout = + >::take(&collator).ok_or(>::NoReward)?; + let mut c_payout = c_exposure.commission * c_total_payout; + let n_payout = c_total_payout - c_payout; + for n_exposure in c_exposure.nominators { + let n_payout = Perbill::from_rational(n_exposure.vote, c_exposure.vote) * n_payout; + + if collator == n_exposure.who { + // If the collator nominated themselves. + + c_payout += n_payout; + } else if T::InflationManager::reward(&n_exposure.who, n_payout).is_ok() { + Self::deposit_event(Event::Payout { staker: n_exposure.who, amount: n_payout }); } else { - Self::deposit_event(Event::Unpaied { staker: c, amount: c_payout }); + Self::deposit_event(Event::Unpaid { staker: n_exposure.who, amount: n_payout }); } } - actual_payout + if T::InflationManager::reward(&collator, c_payout).is_ok() { + Self::deposit_event(Event::Payout { staker: collator, amount: c_payout }); + } else { + Self::deposit_event(Event::Unpaid { staker: collator, amount: c_payout }); + } + + Ok(::WeightInfo::payout()) } /// Prepare the session state. - pub fn prepare_new_session() { - >::kill(); + pub fn prepare_new_session(index: u32) -> Option> { + >::shift_exposure_cache_states(); + #[allow(deprecated)] - >::remove_all(None); - >::iter().drain().for_each(|(k, v)| { - >::insert(k, v); - }); + if call_on_exposure!(Next::remove_all(None)).is_err() { + return None; + } + + log::info!( + "[pallet::staking] assembling new collators for new session {index} at #{:?}", + >::block_number(), + ); + + if let Ok(collators) = Self::elect() { + // TODO?: if we really need this event + Self::deposit_event(Event::Elected { collators: collators.clone() }); + + Some(collators) + } else { + // Impossible case. + // + // But if there is an issue, retain the old collators; do not alter the session + // collators if any error occurs to prevent the chain from stalling. + None + } + } + + /// Shift the exposure cache states. + /// + /// Previous Current Next + /// Next Previous Current + /// Current Next Previous + /// + /// ```nocompile + /// loop { mutate(2, 0, 1) } + /// ``` + pub fn shift_exposure_cache_states() { + let (s0, s1, s2) = >::get(); + + >::put((s2, s0, s1)); } /// Elect the new collators. /// /// This should only be called by the [`pallet_session::SessionManager::new_session`]. - pub fn elect() -> Vec { + pub fn elect() -> Result, DispatchError> { + let nominators = >::iter().collect::>(); + let ring_pool = >::get(); + let kton_pool = >::get(); let mut collators = >::iter() .map(|(c, cm)| { let scaler = Perbill::one() - cm; let mut collator_v = 0; - let nominators = >::iter() + let nominators = nominators + .iter() .filter_map(|(n, c_)| { - if c_ == c { - let nominator_v = scaler * Self::power_of(&n); + if c_ == &c { + let nominator_v = scaler * Self::power_of(n, ring_pool, kton_pool); collator_v += nominator_v; - Some(IndividualExposure { who: n, vote: nominator_v }) + Some(IndividualExposure { who: n.to_owned(), vote: nominator_v }) } else { None } }) .collect(); - ((c, Exposure { vote: collator_v, nominators }), collator_v) + ((c, Exposure { commission: cm, vote: collator_v, nominators }), collator_v) }) .collect::>(); collators.sort_by(|(_, a), (_, b)| b.cmp(a)); + let cache_states = >::get(); + collators .into_iter() .take(>::get() as _) .map(|((c, e), _)| { - >::insert(&c, e); - - c + call_on_exposure!(cache_states, Next::insert(&c, e)) + .map(|_| c) + .map_err(Into::into) }) .collect() } @@ -891,16 +1037,13 @@ pub mod pallet { } pub use pallet::*; -type RewardPoint = u32; type Power = u32; type Vote = u32; type DepositId = <::Deposit as Stake>::Item; -/// On session end handler. -/// -/// Currently it is only used to control the inflation. -pub trait OnSessionEnd +/// Inflation and reward manager. +pub trait InflationManager where T: Config, { @@ -908,42 +1051,48 @@ where fn on_session_end() { let inflation = Self::inflate(); let reward = Self::calculate_reward(inflation); - let actual_payout = >::payout(reward); + let actual_reward = >::distribute_session_reward(reward); - Self::clean(inflation.unwrap_or(reward).saturating_sub(actual_payout)); + if inflation != 0 { + Self::clear(inflation.saturating_sub(actual_reward)); + } } /// Inflation settings. - fn inflate() -> Option { - None + fn inflate() -> Balance { + 0 } /// Calculate the reward. - fn calculate_reward(maybe_inflation: Option) -> Balance; + fn calculate_reward(inflation: Balance) -> Balance; /// The reward function. fn reward(who: &T::AccountId, amount: Balance) -> DispatchResult; - /// Clean the data; currently, only the unissued reward is present. - fn clean(_unissued: Balance) {} + /// Clear the remaining inflation. + fn clear(_remaining: Balance) {} } -impl OnSessionEnd for () +impl InflationManager for () where T: Config, { - fn inflate() -> Option { - None - } - - fn calculate_reward(_maybe_inflation: Option) -> Balance { + fn calculate_reward(_inflation: Balance) -> Balance { 0 } fn reward(_who: &T::AccountId, _amount: Balance) -> DispatchResult { Ok(()) } +} - fn clean(_unissued: Balance) {} +/// Exposure cache's state. +#[allow(missing_docs)] +#[cfg_attr(any(feature = "runtime-benchmarks", feature = "try-runtime"), derive(PartialEq))] +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, RuntimeDebug)] +pub enum ExposureCacheState { + Previous, + Current, + Next, } /// A convertor from collators id. Since this pallet does not have stash/controller, this is @@ -989,42 +1138,17 @@ where } } -// TODO: remove these -/// A snapshot of the stake backing a single collator in the system. -#[cfg(feature = "try-runtime")] -#[derive(PartialEq, Encode, Decode, MaxEncodedLen, TypeInfo, RuntimeDebug)] -pub struct Exposure -where - AccountId: PartialEq, -{ - /// The total vote backing this collator. - pub vote: Vote, - /// Nominator staking map. - pub nominators: Vec>, -} /// A snapshot of the stake backing a single collator in the system. -#[cfg(not(feature = "try-runtime"))] #[derive(Encode, Decode, MaxEncodedLen, TypeInfo, RuntimeDebug)] pub struct Exposure { + /// The commission of this collator. + pub commission: Perbill, /// The total vote backing this collator. pub vote: Vote, /// Nominator staking map. pub nominators: Vec>, } /// A snapshot of the staker's state. -#[cfg(feature = "try-runtime")] -#[derive(PartialEq, Encode, Decode, MaxEncodedLen, TypeInfo, RuntimeDebug)] -pub struct IndividualExposure -where - AccountId: PartialEq, -{ - /// Nominator. - pub who: AccountId, - /// Nominator's staking vote. - pub vote: Vote, -} -/// A snapshot of the staker's state. -#[cfg(not(feature = "try-runtime"))] #[derive(Encode, Decode, MaxEncodedLen, TypeInfo, RuntimeDebug)] pub struct IndividualExposure { /// Nominator. @@ -1042,7 +1166,7 @@ where T: Config + pallet_authorship::Config + pallet_session::Config, { fn note_author(author: T::AccountId) { - Self::reward_by_ids(&[(author, 20)]) + Self::note_authors(&[author]) } } @@ -1052,25 +1176,13 @@ where T: Config, { fn end_session(_: u32) { - T::OnSessionEnd::on_session_end(); + T::InflationManager::on_session_end(); } fn start_session(_: u32) {} fn new_session(index: u32) -> Option> { - Self::prepare_new_session(); - - log::info!( - "[pallet::staking] assembling new collators for new session {} at #{:?}", - index, - >::block_number(), - ); - - let collators = Self::elect(); - - Self::deposit_event(Event::Elected { collators: collators.clone() }); - - Some(collators) + Self::prepare_new_session(index) } } diff --git a/pallet/staking/src/migration.rs b/pallet/staking/src/migration.rs new file mode 100644 index 000000000..8c496d8f8 --- /dev/null +++ b/pallet/staking/src/migration.rs @@ -0,0 +1,137 @@ +//! Pallet migrations. + +// core +use core::marker::PhantomData; +// darwinia +use crate::*; +// substrate +use frame_support::traits::OnRuntimeUpgrade; +#[cfg(feature = "try-runtime")] +use sp_runtime::TryRuntimeError; + +/// Migration version 1. +pub mod v1 { + // darwinia + use super::*; + + type AccountId = ::AccountId; + + #[frame_support::storage_alias] + type NextExposures = + StorageMap, Twox64Concat, AccountId, ExposureV0>>; + + #[frame_support::storage_alias] + type Exposures = + StorageMap, Twox64Concat, AccountId, ExposureV0>>; + + #[frame_support::storage_alias] + type RewardPoints = + StorageValue, (u32, BTreeMap, u32>), ValueQuery>; + + #[derive(Encode, Decode, MaxEncodedLen, TypeInfo, RuntimeDebug)] + struct ExposureV0 { + vote: Vote, + nominators: Vec>, + } + + /// Migrate darwinia-staking from v0 to v1. + pub struct MigrateToV1(PhantomData); + impl OnRuntimeUpgrade for MigrateToV1 + where + T: Config, + { + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, TryRuntimeError> { + assert_eq!(StorageVersion::get::>(), 0, "Can only upgrade from version 0."); + + Ok(Vec::new()) + } + + fn on_runtime_upgrade() -> Weight { + let version = StorageVersion::get::>(); + + if version != 0 { + log::warn!( + "[pallet::staking] skipping v0 to v1 migration: executed on wrong storage version. Expected version 1, found {version:?}", + ); + + return T::DbWeight::get().reads(1); + } + + let mut count = 4; + + let (sum, map) = >::take(); + >::put(( + >::from(sum / 20), + map.into_iter() + .map(|(k, v)| (k, >::from(v / 20))) + .collect::>(), + )); + + >::iter().drain().for_each(|(k, v)| { + count += 1; + + >::insert( + &k, + Exposure { + commission: >::get(&k).unwrap_or_default(), + vote: v.vote, + nominators: v.nominators, + }, + ); + }); + >::iter().drain().for_each(|(k, v)| { + count += 1; + + >::insert( + &k, + Exposure { + commission: >::get(&k).unwrap_or_default(), + vote: v.vote, + nominators: v.nominators, + }, + ); + }); + + StorageVersion::new(1).put::>(); + + T::DbWeight::get().reads_writes(count, count) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(_: Vec) -> Result<(), TryRuntimeError> { + assert_eq!(StorageVersion::get::>(), 1, "Version must be upgraded."); + + // Old storages should be killed. + assert_eq!(>::iter_keys().count(), 0); + assert_eq!(>::iter_keys().count(), 0); + assert!(!>::exists()); + + // Check the starting state is correct. + assert_eq!( + >::get(), + ( + ExposureCacheState::Previous, + ExposureCacheState::Current, + ExposureCacheState::Next + ) + ); + + assert_eq!( + >::iter_keys().count(), + 0, + "Previous exposure should be empty at start." + ); + + // Check that everything decoded fine. + >::iter_keys().for_each(|k| { + assert!(>::try_get(k).is_ok(), "Can not decode V1 `Exposure`."); + }); + >::iter_keys().for_each(|k| { + assert!(>::try_get(k).is_ok(), "Can not decode V1 `Exposure`."); + }); + + Ok(()) + } + } +} diff --git a/pallet/staking/src/mock.rs b/pallet/staking/src/mock.rs index b88cf9af0..eabd547e1 100644 --- a/pallet/staking/src/mock.rs +++ b/pallet/staking/src/mock.rs @@ -21,10 +21,22 @@ pub use crate as darwinia_staking; // darwinia use dc_types::{AssetId, Balance, Moment, UNIT}; // substrate -use frame_support::traits::{Currency, OnInitialize, OnUnbalanced}; +use frame_support::traits::{Currency, OnInitialize}; use sp_io::TestExternalities; use sp_runtime::{BuildStorage, RuntimeAppPublic}; +#[macro_export] +macro_rules! call_on_exposure_test { + ($s:ident$($f:tt)*) => {{ + match >::get() { + (darwinia_staking::ExposureCacheState::$s, _, _) => >$($f)*, + (_, darwinia_staking::ExposureCacheState::$s, _) => >$($f)*, + (_, _, darwinia_staking::ExposureCacheState::$s) => >$($f)*, + _ => unreachable!(), + } + }}; +} + type BlockNumber = u64; type AccountId = u32; @@ -168,7 +180,7 @@ sp_runtime::impl_opaque_keys! { pub uint: SessionHandler, } } -type Period = frame_support::traits::ConstU64<3>; +pub type Period = frame_support::traits::ConstU64<3>; pub struct SessionHandler; impl pallet_session::SessionHandler for SessionHandler { const KEY_TYPE_IDS: &'static [sp_runtime::KeyTypeId] = @@ -232,7 +244,7 @@ impl pallet_treasury::Config for Runtime { frame_support::parameter_types! { pub PayoutFraction: sp_runtime::Perbill = sp_runtime::Perbill::from_percent(40); - pub static OnSessionEnd: u8 = 0; + pub static InflationType: u8 = 0; } pub enum KtonStaking {} impl darwinia_staking::Stake for KtonStaking { @@ -258,42 +270,42 @@ impl darwinia_staking::Stake for KtonStaking { } } pub enum StatedOnSessionEnd {} -impl darwinia_staking::OnSessionEnd for StatedOnSessionEnd { - fn inflate() -> Option { - if ON_SESSION_END.with(|v| *v.borrow()) == 0 { +impl darwinia_staking::InflationManager for StatedOnSessionEnd { + fn inflate() -> Balance { + if INFLATION_TYPE.with(|v| *v.borrow()) == 0 { OnDarwiniaSessionEnd::inflate() } else { OnCrabSessionEnd::inflate() } } - fn calculate_reward(maybe_inflation: Option) -> Balance { - if ON_SESSION_END.with(|v| *v.borrow()) == 0 { - OnDarwiniaSessionEnd::calculate_reward(maybe_inflation) + fn calculate_reward(inflation: Balance) -> Balance { + if INFLATION_TYPE.with(|v| *v.borrow()) == 0 { + OnDarwiniaSessionEnd::calculate_reward(inflation) } else { - OnCrabSessionEnd::calculate_reward(maybe_inflation) + OnCrabSessionEnd::calculate_reward(inflation) } } fn reward(who: &AccountId, amount: Balance) -> sp_runtime::DispatchResult { - if ON_SESSION_END.with(|v| *v.borrow()) == 0 { + if INFLATION_TYPE.with(|v| *v.borrow()) == 0 { OnDarwiniaSessionEnd::reward(who, amount) } else { OnCrabSessionEnd::reward(who, amount) } } - fn clean(unissued: Balance) { - if ON_SESSION_END.with(|v| *v.borrow()) == 0 { - OnDarwiniaSessionEnd::clean(unissued) + fn clear(remaining: Balance) { + if INFLATION_TYPE.with(|v| *v.borrow()) == 0 { + OnDarwiniaSessionEnd::clear(remaining) } else { - OnCrabSessionEnd::clean(unissued) + OnCrabSessionEnd::clear(remaining) } } } pub enum OnDarwiniaSessionEnd {} -impl darwinia_staking::OnSessionEnd for OnDarwiniaSessionEnd { - fn inflate() -> Option { +impl darwinia_staking::InflationManager for OnDarwiniaSessionEnd { + fn inflate() -> Balance { let now = Timestamp::now(); let session_duration = now - >::get(); let elapsed_time = >::mutate(|t| { @@ -306,11 +318,11 @@ impl darwinia_staking::OnSessionEnd for OnDarwiniaSessionEnd { let unminted = dc_inflation::TOTAL_SUPPLY.saturating_sub(Balances::total_issuance()); - dc_inflation::in_period(unminted, session_duration, elapsed_time) + dc_inflation::in_period(unminted, session_duration, elapsed_time).unwrap_or_default() } - fn calculate_reward(maybe_inflation: Option) -> Balance { - maybe_inflation.map(|i| PayoutFraction::get() * i).unwrap_or_default() + fn calculate_reward(inflation: Balance) -> Balance { + PayoutFraction::get() * inflation } fn reward(who: &AccountId, amount: Balance) -> sp_runtime::DispatchResult { @@ -319,13 +331,13 @@ impl darwinia_staking::OnSessionEnd for OnDarwiniaSessionEnd { Ok(()) } - fn clean(unissued: Balance) { - Treasury::on_unbalanced(Balances::issue(unissued)); + fn clear(remaining: Balance) { + let _ = Balances::deposit_into_existing(&Treasury::account_id(), remaining); } } pub enum OnCrabSessionEnd {} -impl darwinia_staking::OnSessionEnd for OnCrabSessionEnd { - fn calculate_reward(_maybe_inflation: Option) -> Balance { +impl darwinia_staking::InflationManager for OnCrabSessionEnd { + fn calculate_reward(_inflation: Balance) -> Balance { 10_000 * UNIT } @@ -338,15 +350,28 @@ impl darwinia_staking::OnSessionEnd for OnCrabSessionEnd { ) } } +pub enum ShouldEndSession {} +impl frame_support::traits::Get for ShouldEndSession { + fn get() -> bool { + // substrate + use pallet_session::ShouldEndSession; + + ::ShouldEndSession::should_end_session( + System::block_number(), + ) + } +} impl darwinia_staking::Config for Runtime { + type Currency = Balances; type Deposit = Deposit; + type InflationManager = StatedOnSessionEnd; type Kton = KtonStaking; type MaxDeposits = ::MaxDeposits; type MaxUnstakings = frame_support::traits::ConstU32<16>; type MinStakingDuration = frame_support::traits::ConstU64<3>; - type OnSessionEnd = StatedOnSessionEnd; type Ring = RingStaking; type RuntimeEvent = RuntimeEvent; + type ShouldEndSession = ShouldEndSession; type WeightInfo = (); } #[cfg(not(feature = "runtime-benchmarks"))] @@ -399,8 +424,8 @@ pub struct ExtBuilder { genesis_collator: bool, } impl ExtBuilder { - pub fn on_session_end_type(self, r#type: u8) -> Self { - ON_SESSION_END.with(|v| *v.borrow_mut() = r#type); + pub fn inflation_type(self, r#type: u8) -> Self { + INFLATION_TYPE.with(|v| *v.borrow_mut() = r#type); self } @@ -460,7 +485,7 @@ impl ExtBuilder { let mut ext = TestExternalities::from(storage); - ext.execute_with(|| initialize_block(1)); + ext.execute_with(|| new_session()); ext } @@ -490,3 +515,9 @@ pub fn new_session() { finalize_block(i); }); } + +pub fn payout() { + call_on_exposure_test!(Previous::iter_keys().for_each(|c| { + let _ = Staking::payout_inner(c); + })); +} diff --git a/pallet/staking/src/tests.rs b/pallet/staking/src/tests.rs index e525b6a24..8789d396a 100644 --- a/pallet/staking/src/tests.rs +++ b/pallet/staking/src/tests.rs @@ -119,7 +119,7 @@ fn unstake_should_work() { staked_ring: 2 * UNIT, staked_kton: 3 * UNIT, staked_deposits: BoundedVec::truncate_from(vec![0, 1, 2]), - unstaking_ring: BoundedVec::truncate_from(vec![(UNIT, 4)]), + unstaking_ring: BoundedVec::truncate_from(vec![(UNIT, 6)]), ..ZeroDefault::default() } ); @@ -133,8 +133,8 @@ fn unstake_should_work() { staked_ring: 2 * UNIT, staked_kton: 2 * UNIT, staked_deposits: BoundedVec::truncate_from(vec![0, 1, 2]), - unstaking_ring: BoundedVec::truncate_from(vec![(UNIT, 4)]), - unstaking_kton: BoundedVec::truncate_from(vec![(UNIT, 5)]), + unstaking_ring: BoundedVec::truncate_from(vec![(UNIT, 6)]), + unstaking_kton: BoundedVec::truncate_from(vec![(UNIT, 7)]), ..ZeroDefault::default() } ); @@ -154,9 +154,9 @@ fn unstake_should_work() { staked_ring: 2 * UNIT, staked_kton: 2 * UNIT, staked_deposits: BoundedVec::truncate_from(vec![0, 2]), - unstaking_ring: BoundedVec::truncate_from(vec![(UNIT, 4)]), - unstaking_kton: BoundedVec::truncate_from(vec![(UNIT, 5)]), - unstaking_deposits: BoundedVec::truncate_from(vec![(1, 6)]) + unstaking_ring: BoundedVec::truncate_from(vec![(UNIT, 6)]), + unstaking_kton: BoundedVec::truncate_from(vec![(UNIT, 7)]), + unstaking_deposits: BoundedVec::truncate_from(vec![(1, 8)]) } ); @@ -166,9 +166,9 @@ fn unstake_should_work() { assert_eq!( Staking::ledger_of(1).unwrap(), Ledger { - unstaking_ring: BoundedVec::truncate_from(vec![(UNIT, 4), (2 * UNIT, 7)]), - unstaking_kton: BoundedVec::truncate_from(vec![(UNIT, 5), (2 * UNIT, 7)]), - unstaking_deposits: BoundedVec::truncate_from(vec![(1, 6), (0, 7), (2, 7)]), + unstaking_ring: BoundedVec::truncate_from(vec![(UNIT, 6), (2 * UNIT, 9)]), + unstaking_kton: BoundedVec::truncate_from(vec![(UNIT, 7), (2 * UNIT, 9)]), + unstaking_deposits: BoundedVec::truncate_from(vec![(1, 8), (0, 9), (2, 9)]), ..ZeroDefault::default() } ); @@ -196,9 +196,9 @@ fn restake_should_work() { assert_eq!( Staking::ledger_of(1).unwrap(), Ledger { - unstaking_ring: BoundedVec::truncate_from(vec![(UNIT, 4), (UNIT, 5), (UNIT, 6)]), - unstaking_kton: BoundedVec::truncate_from(vec![(UNIT, 4), (UNIT, 5), (UNIT, 6)]), - unstaking_deposits: BoundedVec::truncate_from(vec![(0, 4), (1, 4), (2, 4)]), + unstaking_ring: BoundedVec::truncate_from(vec![(UNIT, 6), (UNIT, 7), (UNIT, 8)]), + unstaking_kton: BoundedVec::truncate_from(vec![(UNIT, 6), (UNIT, 7), (UNIT, 8)]), + unstaking_deposits: BoundedVec::truncate_from(vec![(0, 6), (1, 6), (2, 6)]), ..ZeroDefault::default() } ); @@ -209,9 +209,9 @@ fn restake_should_work() { Staking::ledger_of(1).unwrap(), Ledger { staked_ring: 3 * UNIT / 2, - unstaking_ring: BoundedVec::truncate_from(vec![(UNIT, 4), (UNIT / 2, 5)]), - unstaking_kton: BoundedVec::truncate_from(vec![(UNIT, 4), (UNIT, 5), (UNIT, 6)]), - unstaking_deposits: BoundedVec::truncate_from(vec![(0, 4), (1, 4), (2, 4)]), + unstaking_ring: BoundedVec::truncate_from(vec![(UNIT, 6), (UNIT / 2, 7)]), + unstaking_kton: BoundedVec::truncate_from(vec![(UNIT, 6), (UNIT, 7), (UNIT, 8)]), + unstaking_deposits: BoundedVec::truncate_from(vec![(0, 6), (1, 6), (2, 6)]), ..ZeroDefault::default() } ); @@ -223,9 +223,9 @@ fn restake_should_work() { Ledger { staked_ring: 3 * UNIT / 2, staked_kton: 3 * UNIT / 2, - unstaking_ring: BoundedVec::truncate_from(vec![(UNIT, 4), (UNIT / 2, 5)]), - unstaking_kton: BoundedVec::truncate_from(vec![(UNIT, 4), (UNIT / 2, 5)]), - unstaking_deposits: BoundedVec::truncate_from(vec![(0, 4), (1, 4), (2, 4)]), + unstaking_ring: BoundedVec::truncate_from(vec![(UNIT, 6), (UNIT / 2, 7)]), + unstaking_kton: BoundedVec::truncate_from(vec![(UNIT, 6), (UNIT / 2, 7)]), + unstaking_deposits: BoundedVec::truncate_from(vec![(0, 6), (1, 6), (2, 6)]), ..ZeroDefault::default() } ); @@ -244,9 +244,9 @@ fn restake_should_work() { staked_ring: 3 * UNIT / 2, staked_kton: 3 * UNIT / 2, staked_deposits: BoundedVec::truncate_from(vec![1]), - unstaking_ring: BoundedVec::truncate_from(vec![(UNIT, 4), (UNIT / 2, 5)]), - unstaking_kton: BoundedVec::truncate_from(vec![(UNIT, 4), (UNIT / 2, 5)]), - unstaking_deposits: BoundedVec::truncate_from(vec![(0, 4), (2, 4)]), + unstaking_ring: BoundedVec::truncate_from(vec![(UNIT, 6), (UNIT / 2, 7)]), + unstaking_kton: BoundedVec::truncate_from(vec![(UNIT, 6), (UNIT / 2, 7)]), + unstaking_deposits: BoundedVec::truncate_from(vec![(0, 6), (2, 6)]), } ); @@ -290,9 +290,9 @@ fn claim_should_work() { assert_eq!( Staking::ledger_of(1).unwrap(), Ledger { - unstaking_ring: BoundedVec::truncate_from(vec![(UNIT, 4), (UNIT, 7)]), - unstaking_kton: BoundedVec::truncate_from(vec![(UNIT, 5), (UNIT, 7)]), - unstaking_deposits: BoundedVec::truncate_from(vec![(0, 6), (1, 7), (2, 7)]), + unstaking_ring: BoundedVec::truncate_from(vec![(UNIT, 6), (UNIT, 9)]), + unstaking_kton: BoundedVec::truncate_from(vec![(UNIT, 7), (UNIT, 9)]), + unstaking_deposits: BoundedVec::truncate_from(vec![(0, 8), (1, 9), (2, 9)]), ..ZeroDefault::default() } ); @@ -304,9 +304,9 @@ fn claim_should_work() { assert_eq!( Staking::ledger_of(1).unwrap(), Ledger { - unstaking_ring: BoundedVec::truncate_from(vec![(UNIT, 7)]), - unstaking_kton: BoundedVec::truncate_from(vec![(UNIT, 5), (UNIT, 7)]), - unstaking_deposits: BoundedVec::truncate_from(vec![(0, 6), (1, 7), (2, 7)]), + unstaking_ring: BoundedVec::truncate_from(vec![(UNIT, 9)]), + unstaking_kton: BoundedVec::truncate_from(vec![(UNIT, 7), (UNIT, 9)]), + unstaking_deposits: BoundedVec::truncate_from(vec![(0, 8), (1, 9), (2, 9)]), ..ZeroDefault::default() } ); @@ -319,9 +319,9 @@ fn claim_should_work() { assert_eq!( Staking::ledger_of(1).unwrap(), Ledger { - unstaking_ring: BoundedVec::truncate_from(vec![(UNIT, 7)]), - unstaking_kton: BoundedVec::truncate_from(vec![(UNIT, 7)]), - unstaking_deposits: BoundedVec::truncate_from(vec![(0, 6), (1, 7), (2, 7)]), + unstaking_ring: BoundedVec::truncate_from(vec![(UNIT, 9)]), + unstaking_kton: BoundedVec::truncate_from(vec![(UNIT, 9)]), + unstaking_deposits: BoundedVec::truncate_from(vec![(0, 8), (1, 9), (2, 9)]), ..ZeroDefault::default() } ); @@ -334,9 +334,9 @@ fn claim_should_work() { assert_eq!( Staking::ledger_of(1).unwrap(), Ledger { - unstaking_ring: BoundedVec::truncate_from(vec![(UNIT, 7)]), - unstaking_kton: BoundedVec::truncate_from(vec![(UNIT, 7)]), - unstaking_deposits: BoundedVec::truncate_from(vec![(1, 7), (2, 7)]), + unstaking_ring: BoundedVec::truncate_from(vec![(UNIT, 9)]), + unstaking_kton: BoundedVec::truncate_from(vec![(UNIT, 9)]), + unstaking_deposits: BoundedVec::truncate_from(vec![(1, 9), (2, 9)]), ..ZeroDefault::default() } ); @@ -419,62 +419,62 @@ fn set_collator_count_should_work() { #[test] fn power_should_work() { ExtBuilder::default().build().execute_with(|| { - assert_eq!(Staking::power_of(&1), 0); - assert_eq!(Staking::power_of(&2), 0); - assert_eq!(Staking::power_of(&3), 0); - assert_eq!(Staking::power_of(&4), 0); + assert_eq!(Staking::quick_power_of(&1), 0); + assert_eq!(Staking::quick_power_of(&2), 0); + assert_eq!(Staking::quick_power_of(&3), 0); + assert_eq!(Staking::quick_power_of(&4), 0); // 1 stakes 1 RING. assert_ok!(Staking::stake(RuntimeOrigin::signed(1), UNIT, 0, Vec::new())); - assert_eq!(Staking::power_of(&1), 500_000_000); + assert_eq!(Staking::quick_power_of(&1), 500_000_000); // 2 stakes 1 KTON. assert_ok!(Staking::stake(RuntimeOrigin::signed(2), 0, UNIT, Vec::new())); - assert_eq!(Staking::power_of(&1), 500_000_000); - assert_eq!(Staking::power_of(&2), 500_000_000); + assert_eq!(Staking::quick_power_of(&1), 500_000_000); + assert_eq!(Staking::quick_power_of(&2), 500_000_000); // 3 stakes 1 deposit. assert_ok!(Deposit::lock(RuntimeOrigin::signed(3), UNIT, 1)); assert_ok!(Staking::stake(RuntimeOrigin::signed(3), 0, 0, vec![0])); - assert_eq!(Staking::power_of(&1), 250_000_000); - assert_eq!(Staking::power_of(&2), 500_000_000); - assert_eq!(Staking::power_of(&3), 250_000_000); + assert_eq!(Staking::quick_power_of(&1), 250_000_000); + assert_eq!(Staking::quick_power_of(&2), 500_000_000); + assert_eq!(Staking::quick_power_of(&3), 250_000_000); // 4 stakes 1 KTON. assert_ok!(Staking::stake(RuntimeOrigin::signed(4), 0, UNIT, Vec::new())); - assert_eq!(Staking::power_of(&1), 250_000_000); - assert_eq!(Staking::power_of(&2), 250_000_000); - assert_eq!(Staking::power_of(&3), 250_000_000); - assert_eq!(Staking::power_of(&4), 250_000_000); + assert_eq!(Staking::quick_power_of(&1), 250_000_000); + assert_eq!(Staking::quick_power_of(&2), 250_000_000); + assert_eq!(Staking::quick_power_of(&3), 250_000_000); + assert_eq!(Staking::quick_power_of(&4), 250_000_000); // 1 unstakes 1 RING. assert_ok!(Staking::unstake(RuntimeOrigin::signed(1), UNIT, 0, Vec::new())); - assert_eq!(Staking::power_of(&1), 0); - assert_eq!(Staking::power_of(&2), 250_000_000); - assert_eq!(Staking::power_of(&3), 500_000_000); - assert_eq!(Staking::power_of(&4), 250_000_000); + assert_eq!(Staking::quick_power_of(&1), 0); + assert_eq!(Staking::quick_power_of(&2), 250_000_000); + assert_eq!(Staking::quick_power_of(&3), 500_000_000); + assert_eq!(Staking::quick_power_of(&4), 250_000_000); // 2 unstakes 1 KTON. assert_ok!(Staking::unstake(RuntimeOrigin::signed(2), 0, UNIT, Vec::new())); - assert_eq!(Staking::power_of(&1), 0); - assert_eq!(Staking::power_of(&2), 0); - assert_eq!(Staking::power_of(&3), 500_000_000); - assert_eq!(Staking::power_of(&4), 500_000_000); + assert_eq!(Staking::quick_power_of(&1), 0); + assert_eq!(Staking::quick_power_of(&2), 0); + assert_eq!(Staking::quick_power_of(&3), 500_000_000); + assert_eq!(Staking::quick_power_of(&4), 500_000_000); // 3 unstakes 1 deposit. assert_ok!(Deposit::lock(RuntimeOrigin::signed(3), UNIT, 1)); assert_ok!(Staking::unstake(RuntimeOrigin::signed(3), 0, 0, vec![0])); - assert_eq!(Staking::power_of(&1), 0); - assert_eq!(Staking::power_of(&2), 0); - assert_eq!(Staking::power_of(&3), 0); - assert_eq!(Staking::power_of(&4), 500_000_000); + assert_eq!(Staking::quick_power_of(&1), 0); + assert_eq!(Staking::quick_power_of(&2), 0); + assert_eq!(Staking::quick_power_of(&3), 0); + assert_eq!(Staking::quick_power_of(&4), 500_000_000); // 4 unstakes 1 KTON. assert_ok!(Staking::unstake(RuntimeOrigin::signed(4), 0, UNIT, Vec::new())); - assert_eq!(Staking::power_of(&1), 0); - assert_eq!(Staking::power_of(&2), 0); - assert_eq!(Staking::power_of(&3), 0); - assert_eq!(Staking::power_of(&4), 0); + assert_eq!(Staking::quick_power_of(&1), 0); + assert_eq!(Staking::quick_power_of(&2), 0); + assert_eq!(Staking::quick_power_of(&3), 0); + assert_eq!(Staking::quick_power_of(&4), 0); }); } @@ -501,7 +501,7 @@ fn elect_should_work() { assert_ok!(Staking::nominate(RuntimeOrigin::signed(i), i - 5)); }); - assert_eq!(Staking::elect(), vec![5, 4, 3]); + assert_eq!(Staking::elect().unwrap(), vec![5, 4, 3]); }); ExtBuilder::default().collator_count(3).build().execute_with(|| { (1..=5).for_each(|i| { @@ -524,7 +524,7 @@ fn elect_should_work() { assert_ok!(Staking::nominate(RuntimeOrigin::signed(i), i - 5)); }); - assert_eq!(Staking::elect(), vec![1, 2, 3]); + assert_eq!(Staking::elect().unwrap(), vec![1, 2, 3]); }); } @@ -551,36 +551,38 @@ fn payout_should_work() { assert_ok!(Staking::nominate(RuntimeOrigin::signed(i), i - 5)); }); new_session(); - new_session(); - Staking::reward_by_ids(&[(1, 20), (2, 20), (3, 20), (4, 20), (5, 20)]); (1..=10).for_each(|i| assert_eq!(Balances::free_balance(i), 1_000 * UNIT)); - let session_duration = Duration::new(6 * 60 * 60, 0).as_millis(); - - Timestamp::set_timestamp(session_duration); + let total_issuance = Balances::total_issuance(); + let session_duration = Duration::new(12 * 600, 0).as_millis(); + Efflux::time(session_duration - >::get() as Moment); + Staking::note_authors(&[1, 2, 3, 4, 5]); new_session(); - - let rewards = [ - 1365981985468705603914, - 2549833009234996736972, - 3551553170742362179958, - 4371142361805028424107, - 5008600632691132655310, - 4097945964602008744224, - 2914094945753252770656, - 1912374784245887327670, - 1092785589904864310528, - 455327319565152874824, + new_session(); + payout(); + + let rewards = vec![ + 455327412809459795649, + 849944493808794062101, + 1183851276145838531437, + 1457047723758662022603, + 1669533853403313367198, + 1365982241160343870361, + 971365161800188293957, + 637458379463143824620, + 364261930757534540090, + 151775801295014161055, ]; - (1..=10) - .zip(rewards.iter()) - .for_each(|(i, r)| assert_eq!(Balances::free_balance(i) - 1_000 * UNIT, *r)); + assert_eq!( + rewards, + (1..=10).map(|i| Balances::free_balance(i) - 1_000 * UNIT).collect::>() + ); assert_eq_error_rate!( PayoutFraction::get() * dc_inflation::in_period( - dc_inflation::TOTAL_SUPPLY - Balances::total_issuance(), + dc_inflation::TOTAL_SUPPLY - total_issuance, session_duration, - 0 + Timestamp::now() ) .unwrap(), rewards.iter().sum::(), @@ -589,7 +591,7 @@ fn payout_should_work() { ); }); - ExtBuilder::default().on_session_end_type(1).collator_count(5).build().execute_with(|| { + ExtBuilder::default().inflation_type(1).collator_count(5).build().execute_with(|| { (1..=5).for_each(|i| { assert_ok!(Staking::stake( RuntimeOrigin::signed(i), @@ -611,14 +613,16 @@ fn payout_should_work() { }); new_session(); new_session(); - Staking::reward_by_ids(&[(1, 20), (2, 20), (3, 20), (4, 20), (5, 20)]); (1..=10).for_each(|i| assert_eq!(Balances::free_balance(i), 1_000 * UNIT)); let total_issuance = Balances::total_issuance(); - + let session_duration = Duration::new(12 * 600, 0).as_millis(); + Efflux::time(session_duration - >::get() as Moment); + Staking::note_authors(&[1, 2, 3, 4, 5]); new_session(); + payout(); - let rewards = [ + let rewards = vec![ 499999998800000000000, 933333320000000000000, 1300000000000000000000, @@ -630,9 +634,10 @@ fn payout_should_work() { 399999999600000000000, 166666663000000000000, ]; - (1..=10) - .zip(rewards.iter()) - .for_each(|(i, r)| assert_eq!(Balances::free_balance(i) - 1_000 * UNIT, *r)); + assert_eq!( + rewards, + (1..=10).map(|i| Balances::free_balance(i) - 1_000 * UNIT).collect::>() + ); assert_eq!(Balances::total_issuance(), total_issuance); assert_eq!( @@ -640,28 +645,27 @@ fn payout_should_work() { 1_000_000 * UNIT - rewards.iter().sum::() ); - dbg!(Balances::free_balance(&Treasury::account_id())); assert_ok!(Balances::transfer_all( RuntimeOrigin::signed(Treasury::account_id()), Default::default(), false )); - dbg!(Balances::free_balance(&Treasury::account_id())); - Staking::reward_by_ids(&[(1, 20)]); + Staking::note_authors(&[1]); System::reset_events(); new_session(); + payout(); assert_eq!( System::events() .into_iter() .filter_map(|e| match e.event { - RuntimeEvent::Staking(e @ Event::Unpaied { .. }) => Some(e), + RuntimeEvent::Staking(e @ Event::Unpaid { .. }) => Some(e), _ => None, }) .collect::>(), vec![ - Event::Unpaied { staker: 6, amount: 7499999997000000000000 }, - Event::Unpaied { staker: 1, amount: 2499999994000000000000 } + Event::Unpaid { staker: 6, amount: 7499999997000000000000 }, + Event::Unpaid { staker: 1, amount: 2499999994000000000000 } ] ); }); @@ -670,42 +674,40 @@ fn payout_should_work() { #[test] fn on_new_session_should_work() { ExtBuilder::default().collator_count(2).genesis_collator().build().execute_with(|| { - assert_eq_uvec!(>::iter_keys().collect::>(), [1, 2]); - assert_eq_uvec!(>::iter_keys().collect::>(), [1, 2]); + assert_eq_uvec!(call_on_exposure_test!(Previous::iter_keys().collect::>()), [1, 2]); + assert_eq_uvec!(call_on_exposure_test!(Current::iter_keys().collect::>()), [1, 2]); + assert_eq_uvec!(call_on_exposure_test!(Next::iter_keys().collect::>()), [1, 2]); assert_ok!(Staking::collect(RuntimeOrigin::signed(3), Perbill::zero())); assert_ok!(Staking::stake(RuntimeOrigin::signed(3), 2 * UNIT, 0, Vec::new())); assert_ok!(Staking::nominate(RuntimeOrigin::signed(3), 3)); - Staking::reward_by_ids( - &Session::validators().into_iter().map(|v| (v, 20)).collect::>(), - ); + Staking::note_authors(&Session::validators()); new_session(); - assert_eq_uvec!(>::iter_keys().collect::>(), [1, 2]); - assert_eq_uvec!(>::iter_keys().collect::>(), [1, 3]); + assert_eq_uvec!(call_on_exposure_test!(Previous::iter_keys().collect::>()), [1, 2]); + assert_eq_uvec!(call_on_exposure_test!(Current::iter_keys().collect::>()), [1, 2]); + assert_eq_uvec!(call_on_exposure_test!(Next::iter_keys().collect::>()), [1, 3]); assert_ok!(Staking::chill(RuntimeOrigin::signed(3))); assert_ok!(Staking::collect(RuntimeOrigin::signed(4), Perbill::zero())); assert_ok!(Staking::stake(RuntimeOrigin::signed(4), 2 * UNIT, 0, Vec::new())); assert_ok!(Staking::nominate(RuntimeOrigin::signed(4), 4)); - Staking::reward_by_ids( - &Session::validators().into_iter().map(|v| (v, 20)).collect::>(), - ); + Staking::note_authors(&Session::validators()); new_session(); - assert_eq_uvec!(>::iter_keys().collect::>(), [1, 3]); - assert_eq_uvec!(>::iter_keys().collect::>(), [1, 4]); + assert_eq_uvec!(call_on_exposure_test!(Previous::iter_keys().collect::>()), [1, 2]); + assert_eq_uvec!(call_on_exposure_test!(Current::iter_keys().collect::>()), [1, 3]); + assert_eq_uvec!(call_on_exposure_test!(Next::iter_keys().collect::>()), [1, 4]); assert_ok!(Staking::chill(RuntimeOrigin::signed(4))); assert_ok!(Staking::collect(RuntimeOrigin::signed(5), Perbill::zero())); assert_ok!(Staking::stake(RuntimeOrigin::signed(5), 2 * UNIT, 0, Vec::new())); assert_ok!(Staking::nominate(RuntimeOrigin::signed(5), 5)); - Staking::reward_by_ids( - &Session::validators().into_iter().map(|v| (v, 20)).collect::>(), - ); + Staking::note_authors(&Session::validators()); new_session(); - assert_eq_uvec!(>::iter_keys().collect::>(), [1, 4]); - assert_eq_uvec!(>::iter_keys().collect::>(), [1, 5]); + assert_eq_uvec!(call_on_exposure_test!(Previous::iter_keys().collect::>()), [1, 3]); + assert_eq_uvec!(call_on_exposure_test!(Current::iter_keys().collect::>()), [1, 4]); + assert_eq_uvec!(call_on_exposure_test!(Next::iter_keys().collect::>()), [1, 5]); }); } diff --git a/pallet/staking/src/weights.rs b/pallet/staking/src/weights.rs index 139250360..7a3648fea 100644 --- a/pallet/staking/src/weights.rs +++ b/pallet/staking/src/weights.rs @@ -63,6 +63,7 @@ pub trait WeightInfo { fn nominate() -> Weight; fn chill() -> Weight; fn set_collator_count() -> Weight; + fn payout() -> Weight; } /// Weights for darwinia_staking using the Substrate node and recommended hardware. @@ -203,6 +204,9 @@ impl WeightInfo for SubstrateWeight { .saturating_add(Weight::from_parts(0, 0)) .saturating_add(T::DbWeight::get().writes(1_u64)) } + fn payout() -> Weight { + Default::default() + } } // For backwards compatibility and tests @@ -342,4 +346,7 @@ impl WeightInfo for () { .saturating_add(Weight::from_parts(0, 0)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } + fn payout() -> Weight { + Default::default() + } } diff --git a/precompile/staking/src/mock.rs b/precompile/staking/src/mock.rs index 2e27ad8d7..b78128593 100644 --- a/precompile/staking/src/mock.rs +++ b/precompile/staking/src/mock.rs @@ -232,14 +232,16 @@ impl darwinia_staking::Stake for KtonStaking { } impl darwinia_staking::Config for Runtime { + type Currency = Balances; type Deposit = Deposit; + type InflationManager = (); type Kton = KtonStaking; type MaxDeposits = ::MaxDeposits; type MaxUnstakings = frame_support::traits::ConstU32<16>; type MinStakingDuration = frame_support::traits::ConstU64<3>; - type OnSessionEnd = (); type Ring = RingStaking; type RuntimeEvent = RuntimeEvent; + type ShouldEndSession = (); type WeightInfo = (); } #[cfg(not(feature = "runtime-benchmarks"))] diff --git a/runtime/crab/src/pallets/staking.rs b/runtime/crab/src/pallets/staking.rs index 7fafe2ffc..39b62f56c 100644 --- a/runtime/crab/src/pallets/staking.rs +++ b/runtime/crab/src/pallets/staking.rs @@ -73,8 +73,8 @@ impl darwinia_staking::Stake for KtonStaking { } pub enum OnCrabSessionEnd {} -impl darwinia_staking::OnSessionEnd for OnCrabSessionEnd { - fn calculate_reward(_maybe_inflation: Option) -> Balance { +impl darwinia_staking::InflationManager for OnCrabSessionEnd { + fn calculate_reward(_inflation: Balance) -> Balance { 20_000 * UNIT } @@ -88,15 +88,29 @@ impl darwinia_staking::OnSessionEnd for OnCrabSessionEnd { } } +pub enum ShouldEndSession {} +impl frame_support::traits::Get for ShouldEndSession { + fn get() -> bool { + // substrate + use pallet_session::ShouldEndSession; + + ::ShouldEndSession::should_end_session( + System::block_number(), + ) + } +} + impl darwinia_staking::Config for Runtime { + type Currency = Balances; type Deposit = Deposit; + type InflationManager = OnCrabSessionEnd; type Kton = KtonStaking; type MaxDeposits = ::MaxDeposits; type MaxUnstakings = ConstU32<16>; type MinStakingDuration = MinStakingDuration; - type OnSessionEnd = OnCrabSessionEnd; type Ring = RingStaking; type RuntimeEvent = RuntimeEvent; + type ShouldEndSession = ShouldEndSession; type WeightInfo = weights::darwinia_staking::WeightInfo; } #[cfg(not(feature = "runtime-benchmarks"))] diff --git a/runtime/crab/src/weights/darwinia_staking.rs b/runtime/crab/src/weights/darwinia_staking.rs index af73a5e85..6416df994 100644 --- a/runtime/crab/src/weights/darwinia_staking.rs +++ b/runtime/crab/src/weights/darwinia_staking.rs @@ -199,4 +199,7 @@ impl darwinia_staking::WeightInfo for WeightInfo { .saturating_add(Weight::from_parts(0, 0)) .saturating_add(T::DbWeight::get().writes(1)) } + fn payout() -> Weight { + Default::default() + } } diff --git a/runtime/darwinia/src/pallets/staking.rs b/runtime/darwinia/src/pallets/staking.rs index 9e2398355..473598b21 100644 --- a/runtime/darwinia/src/pallets/staking.rs +++ b/runtime/darwinia/src/pallets/staking.rs @@ -19,7 +19,7 @@ // darwinia use crate::*; // substrate -use frame_support::traits::{Currency, OnUnbalanced}; +use frame_support::traits::Currency; fast_runtime_or_not!(DURATION, BlockNumber, 5 * MINUTES, 14 * DAYS); @@ -73,8 +73,8 @@ impl darwinia_staking::Stake for KtonStaking { } pub enum OnDarwiniaSessionEnd {} -impl darwinia_staking::OnSessionEnd for OnDarwiniaSessionEnd { - fn inflate() -> Option { +impl darwinia_staking::InflationManager for OnDarwiniaSessionEnd { + fn inflate() -> Balance { let now = Timestamp::now() as Moment; let session_duration = now - >::get(); let elapsed_time = >::mutate(|t| { @@ -87,11 +87,11 @@ impl darwinia_staking::OnSessionEnd for OnDarwiniaSessionEnd { let unminted = dc_inflation::TOTAL_SUPPLY.saturating_sub(Balances::total_issuance()); - dc_inflation::in_period(unminted, session_duration, elapsed_time) + dc_inflation::in_period(unminted, session_duration, elapsed_time).unwrap_or_default() } - fn calculate_reward(maybe_inflation: Option) -> Balance { - maybe_inflation.map(|i| sp_runtime::Perbill::from_percent(40) * i).unwrap_or_default() + fn calculate_reward(inflation: Balance) -> Balance { + sp_runtime::Perbill::from_percent(40) * inflation } fn reward(who: &AccountId, amount: Balance) -> sp_runtime::DispatchResult { @@ -100,20 +100,34 @@ impl darwinia_staking::OnSessionEnd for OnDarwiniaSessionEnd { Ok(()) } - fn clean(unissued: Balance) { - Treasury::on_unbalanced(Balances::issue(unissued)); + fn clear(remaining: Balance) { + let _ = Balances::deposit_into_existing(&Treasury::account_id(), remaining); + } +} + +pub enum ShouldEndSession {} +impl frame_support::traits::Get for ShouldEndSession { + fn get() -> bool { + // substrate + use pallet_session::ShouldEndSession; + + ::ShouldEndSession::should_end_session( + System::block_number(), + ) } } impl darwinia_staking::Config for Runtime { + type Currency = Balances; type Deposit = Deposit; + type InflationManager = OnDarwiniaSessionEnd; type Kton = KtonStaking; type MaxDeposits = ::MaxDeposits; type MaxUnstakings = ConstU32<16>; type MinStakingDuration = MinStakingDuration; - type OnSessionEnd = OnDarwiniaSessionEnd; type Ring = RingStaking; type RuntimeEvent = RuntimeEvent; + type ShouldEndSession = ShouldEndSession; type WeightInfo = weights::darwinia_staking::WeightInfo; } #[cfg(not(feature = "runtime-benchmarks"))] diff --git a/runtime/darwinia/src/weights/darwinia_staking.rs b/runtime/darwinia/src/weights/darwinia_staking.rs index 8c7cb0b94..4f2033b11 100644 --- a/runtime/darwinia/src/weights/darwinia_staking.rs +++ b/runtime/darwinia/src/weights/darwinia_staking.rs @@ -199,4 +199,7 @@ impl darwinia_staking::WeightInfo for WeightInfo { .saturating_add(Weight::from_parts(0, 0)) .saturating_add(T::DbWeight::get().writes(1)) } + fn payout() -> Weight { + Default::default() + } } diff --git a/runtime/pangolin/src/pallets/staking.rs b/runtime/pangolin/src/pallets/staking.rs index fe78c2833..28bf71b61 100644 --- a/runtime/pangolin/src/pallets/staking.rs +++ b/runtime/pangolin/src/pallets/staking.rs @@ -67,8 +67,8 @@ impl darwinia_staking::Stake for KtonStaking { } pub enum OnPangolinSessionEnd {} -impl darwinia_staking::OnSessionEnd for OnPangolinSessionEnd { - fn calculate_reward(_maybe_inflation: Option) -> Balance { +impl darwinia_staking::InflationManager for OnPangolinSessionEnd { + fn calculate_reward(_inflation: Balance) -> Balance { 20_000 * UNIT } @@ -82,15 +82,29 @@ impl darwinia_staking::OnSessionEnd for OnPangolinSessionEnd { } } +pub enum ShouldEndSession {} +impl frame_support::traits::Get for ShouldEndSession { + fn get() -> bool { + // substrate + use pallet_session::ShouldEndSession; + + ::ShouldEndSession::should_end_session( + System::block_number(), + ) + } +} + impl darwinia_staking::Config for Runtime { + type Currency = Balances; type Deposit = Deposit; + type InflationManager = OnPangolinSessionEnd; type Kton = KtonStaking; type MaxDeposits = ::MaxDeposits; type MaxUnstakings = ConstU32<16>; type MinStakingDuration = ConstU32<{ 2 * MINUTES }>; - type OnSessionEnd = OnPangolinSessionEnd; type Ring = RingStaking; type RuntimeEvent = RuntimeEvent; + type ShouldEndSession = ShouldEndSession; type WeightInfo = weights::darwinia_staking::WeightInfo; } #[cfg(not(feature = "runtime-benchmarks"))] diff --git a/runtime/pangolin/src/weights/darwinia_staking.rs b/runtime/pangolin/src/weights/darwinia_staking.rs index 5a63951e5..f7d285884 100644 --- a/runtime/pangolin/src/weights/darwinia_staking.rs +++ b/runtime/pangolin/src/weights/darwinia_staking.rs @@ -199,4 +199,7 @@ impl darwinia_staking::WeightInfo for WeightInfo { .saturating_add(Weight::from_parts(0, 0)) .saturating_add(T::DbWeight::get().writes(1)) } + fn payout() -> Weight { + Default::default() + } } diff --git a/runtime/pangoro/src/pallets/staking.rs b/runtime/pangoro/src/pallets/staking.rs index 8deddfaa9..06e3f63b0 100644 --- a/runtime/pangoro/src/pallets/staking.rs +++ b/runtime/pangoro/src/pallets/staking.rs @@ -19,7 +19,7 @@ // darwinia use crate::*; // substrate -use frame_support::traits::{Currency, OnUnbalanced}; +use frame_support::traits::Currency; pub enum RingStaking {} impl darwinia_staking::Stake for RingStaking { @@ -69,8 +69,8 @@ impl darwinia_staking::Stake for KtonStaking { } pub enum OnPangoroSessionEnd {} -impl darwinia_staking::OnSessionEnd for OnPangoroSessionEnd { - fn inflate() -> Option { +impl darwinia_staking::InflationManager for OnPangoroSessionEnd { + fn inflate() -> Balance { let now = Timestamp::now() as Moment; let session_duration = now - >::get() as Moment; let elapsed_time = >::mutate(|t| { @@ -83,11 +83,11 @@ impl darwinia_staking::OnSessionEnd for OnPangoroSessionEnd { let unminted = dc_inflation::TOTAL_SUPPLY.saturating_sub(Balances::total_issuance()); - dc_inflation::in_period(unminted, session_duration, elapsed_time) + dc_inflation::in_period(unminted, session_duration, elapsed_time).unwrap_or_default() } - fn calculate_reward(maybe_inflation: Option) -> Balance { - maybe_inflation.map(|i| sp_runtime::Perbill::from_percent(40) * i).unwrap_or_default() + fn calculate_reward(inflation: Balance) -> Balance { + sp_runtime::Perbill::from_percent(40) * inflation } fn reward(who: &AccountId, amount: Balance) -> sp_runtime::DispatchResult { @@ -96,20 +96,34 @@ impl darwinia_staking::OnSessionEnd for OnPangoroSessionEnd { Ok(()) } - fn clean(unissued: Balance) { - Treasury::on_unbalanced(Balances::issue(unissued)); + fn clear(remaining: Balance) { + let _ = Balances::deposit_into_existing(&Treasury::account_id(), remaining); + } +} + +pub enum ShouldEndSession {} +impl frame_support::traits::Get for ShouldEndSession { + fn get() -> bool { + // substrate + use pallet_session::ShouldEndSession; + + ::ShouldEndSession::should_end_session( + System::block_number(), + ) } } impl darwinia_staking::Config for Runtime { + type Currency = Balances; type Deposit = Deposit; + type InflationManager = OnPangoroSessionEnd; type Kton = KtonStaking; type MaxDeposits = ::MaxDeposits; type MaxUnstakings = ConstU32<16>; type MinStakingDuration = ConstU32<{ 10 * MINUTES }>; - type OnSessionEnd = OnPangoroSessionEnd; type Ring = RingStaking; type RuntimeEvent = RuntimeEvent; + type ShouldEndSession = ShouldEndSession; type WeightInfo = weights::darwinia_staking::WeightInfo; } #[cfg(not(feature = "runtime-benchmarks"))] diff --git a/runtime/pangoro/src/weights/darwinia_staking.rs b/runtime/pangoro/src/weights/darwinia_staking.rs index 1a951ad2b..53d0521ed 100644 --- a/runtime/pangoro/src/weights/darwinia_staking.rs +++ b/runtime/pangoro/src/weights/darwinia_staking.rs @@ -199,4 +199,7 @@ impl darwinia_staking::WeightInfo for WeightInfo { .saturating_add(Weight::from_parts(0, 0)) .saturating_add(T::DbWeight::get().writes(1)) } + fn payout() -> Weight { + Default::default() + } }