diff --git a/Cargo.lock b/Cargo.lock index 1683cc2b06b69..1995ebb46023e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5892,6 +5892,7 @@ version = "1.0.0" dependencies = [ "frame-support", "frame-system", + "log", "pallet-balances", "parity-scale-codec", "scale-info", diff --git a/frame/nomination-pools/Cargo.toml b/frame/nomination-pools/Cargo.toml index 8c2f9daf2777b..1ac8caa5f64b4 100644 --- a/frame/nomination-pools/Cargo.toml +++ b/frame/nomination-pools/Cargo.toml @@ -23,6 +23,7 @@ sp-runtime = { version = "6.0.0", default-features = false, path = "../../primit sp-std = { version = "4.0.0", default-features = false, path = "../../primitives/std" } sp-staking = { version = "4.0.0-dev", default-features = false, path = "../../primitives/staking" } sp-core = { version = "6.0.0", default-features = false, path = "../../primitives/core" } +log = { version = "0.4.0", default-features = false } [dev-dependencies] pallet-balances = { version = "4.0.0-dev", path = "../balances" } @@ -42,4 +43,5 @@ std = [ "sp-std/std", "sp-staking/std", "sp-core/std", + "log/std", ] diff --git a/frame/nomination-pools/benchmarking/src/lib.rs b/frame/nomination-pools/benchmarking/src/lib.rs index aa4c093dcf0d4..275b914cda297 100644 --- a/frame/nomination-pools/benchmarking/src/lib.rs +++ b/frame/nomination-pools/benchmarking/src/lib.rs @@ -526,9 +526,9 @@ frame_benchmarking::benchmarks! { member_counter: 1, roles: PoolRoles { depositor: depositor.clone(), - root: depositor.clone(), - nominator: depositor.clone(), - state_toggler: depositor.clone(), + root: Some(depositor.clone()), + nominator: Some(depositor.clone()), + state_toggler: Some(depositor.clone()), }, } ); @@ -567,9 +567,9 @@ frame_benchmarking::benchmarks! { member_counter: 1, roles: PoolRoles { depositor: depositor.clone(), - root: depositor.clone(), - nominator: depositor.clone(), - state_toggler: depositor.clone(), + root: Some(depositor.clone()), + nominator: Some(depositor.clone()), + state_toggler: Some(depositor.clone()), } } ); @@ -638,17 +638,17 @@ frame_benchmarking::benchmarks! { }:_( Origin::Signed(root.clone()), first_id, - Some(random.clone()), - Some(random.clone()), - Some(random.clone()) + ConfigOp::Set(random.clone()), + ConfigOp::Set(random.clone()), + ConfigOp::Set(random.clone()) ) verify { assert_eq!( pallet_nomination_pools::BondedPools::::get(first_id).unwrap().roles, pallet_nomination_pools::PoolRoles { depositor: root, - nominator: random.clone(), - state_toggler: random.clone(), - root: random, + nominator: Some(random.clone()), + state_toggler: Some(random.clone()), + root: Some(random), }, ) } diff --git a/frame/nomination-pools/src/lib.rs b/frame/nomination-pools/src/lib.rs index 0fff470eec43b..d68a6f09c31d1 100644 --- a/frame/nomination-pools/src/lib.rs +++ b/frame/nomination-pools/src/lib.rs @@ -324,11 +324,26 @@ use sp_runtime::traits::{AccountIdConversion, Bounded, CheckedSub, Convert, Satu use sp_staking::{EraIndex, OnStakerSlash, StakingInterface}; use sp_std::{collections::btree_map::BTreeMap, fmt::Debug, ops::Div, vec::Vec}; +/// The log target of this pallet. +pub const LOG_TARGET: &'static str = "runtime::nomination-pools"; + +// syntactic sugar for logging. +#[macro_export] +macro_rules! log { + ($level:tt, $patter:expr $(, $values:expr)* $(,)?) => { + log::$level!( + target: crate::LOG_TARGET, + concat!("[{:?}] 🏊‍♂️ ", $patter), >::block_number() $(, $values)* + ) + }; +} + #[cfg(test)] mod mock; #[cfg(test)] mod tests; +pub mod migration; pub mod weights; pub use pallet::*; @@ -502,7 +517,11 @@ pub enum PoolState { Destroying, } -/// Pool adminstration roles. +/// Pool administration roles. +/// +/// Any pool has a depositor, which can never change. But, all the other roles are optional, and +/// cannot exist. Note that if `root` is set to `None`, it basically means that the roles of this +/// pool can never change again (except via governance). #[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Debug, PartialEq, Clone)] pub struct PoolRoles { /// Creates the pool and is the initial member. They can only leave the pool once all other @@ -510,11 +529,11 @@ pub struct PoolRoles { pub depositor: AccountId, /// Can change the nominator, state-toggler, or itself and can perform any of the actions the /// nominator or state-toggler can. - pub root: AccountId, + pub root: Option, /// Can select which validators the pool nominates. - pub nominator: AccountId, + pub nominator: Option, /// Can change the pools state and kick members if the pool is blocked. - pub state_toggler: AccountId, + pub state_toggler: Option, } /// Pool permissions and state @@ -665,25 +684,36 @@ impl BondedPool { .saturating_sub(T::StakingInterface::active_stake(&account).unwrap_or_default()) } + fn is_root(&self, who: &T::AccountId) -> bool { + self.roles.root.as_ref().map_or(false, |root| root == who) + } + + fn is_state_toggler(&self, who: &T::AccountId) -> bool { + self.roles + .state_toggler + .as_ref() + .map_or(false, |state_toggler| state_toggler == who) + } + fn can_update_roles(&self, who: &T::AccountId) -> bool { - *who == self.roles.root + self.is_root(who) } fn can_nominate(&self, who: &T::AccountId) -> bool { - *who == self.roles.root || *who == self.roles.nominator + self.is_root(who) || + self.roles.nominator.as_ref().map_or(false, |nominator| nominator == who) } fn can_kick(&self, who: &T::AccountId) -> bool { - (*who == self.roles.root || *who == self.roles.state_toggler) && - self.state == PoolState::Blocked + self.state == PoolState::Blocked && (self.is_root(who) || self.is_state_toggler(who)) } fn can_toggle_state(&self, who: &T::AccountId) -> bool { - (*who == self.roles.root || *who == self.roles.state_toggler) && !self.is_destroying() + (self.is_root(who) || self.is_state_toggler(who)) && !self.is_destroying() } fn can_set_metadata(&self, who: &T::AccountId) -> bool { - *who == self.roles.root || *who == self.roles.state_toggler + self.is_root(who) || self.is_state_toggler(who) } fn is_destroying(&self) -> bool { @@ -987,11 +1017,12 @@ impl SubPools { /// /// This is often used whilst getting the sub-pool from storage, thus it consumes and returns /// `Self` for ergonomic purposes. - fn maybe_merge_pools(mut self, unbond_era: EraIndex) -> Self { + fn maybe_merge_pools(mut self, current_era: EraIndex) -> Self { // Ex: if `TotalUnbondingPools` is 5 and current era is 10, we only want to retain pools // 6..=10. Note that in the first few eras where `checked_sub` is `None`, we don't remove // anything. - if let Some(newest_era_to_remove) = unbond_era.checked_sub(TotalUnbondingPools::::get()) + if let Some(newest_era_to_remove) = + current_era.checked_sub(T::PostUnbondingPoolsWindow::get()) { self.with_era.retain(|k, v| { if *k > newest_era_to_remove { @@ -1045,11 +1076,15 @@ impl Get for TotalUnbondingPools { #[frame_support::pallet] pub mod pallet { use super::*; - use frame_support::transactional; + use frame_support::{traits::StorageVersion, transactional}; use frame_system::{ensure_signed, pallet_prelude::*}; + /// The current storage version. + const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + #[pallet::pallet] #[pallet::generate_store(pub(crate) trait Store)] + #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(_); #[pallet::config] @@ -1218,8 +1253,13 @@ pub mod pallet { /// /// The removal can be voluntary (withdrawn all unbonded funds) or involuntary (kicked). MemberRemoved { pool_id: PoolId, member: T::AccountId }, - /// The roles of a pool have been updated to the given new roles. - RolesUpdated { root: T::AccountId, state_toggler: T::AccountId, nominator: T::AccountId }, + /// The roles of a pool have been updated to the given new roles. Note that the depositor + /// can never change. + RolesUpdated { + root: Option, + state_toggler: Option, + nominator: Option, + }, } #[pallet::error] @@ -1470,7 +1510,7 @@ pub mod pallet { // Note that we lazily create the unbonding pools here if they don't already exist let mut sub_pools = SubPoolsStorage::::get(member.pool_id) .unwrap_or_default() - .maybe_merge_pools(unbond_era); + .maybe_merge_pools(current_era); // Update the unbond pool associated with the current era with the unbonded funds. Note // that we lazily create the unbond pool if it does not yet exist. @@ -1693,7 +1733,12 @@ pub mod pallet { }); let mut bonded_pool = BondedPool::::new( pool_id, - PoolRoles { root, nominator, state_toggler, depositor: who.clone() }, + PoolRoles { + root: Some(root), + nominator: Some(nominator), + state_toggler: Some(state_toggler), + depositor: who.clone(), + }, ); bonded_pool.try_inc_members()?; @@ -1850,9 +1895,9 @@ pub mod pallet { pub fn update_roles( origin: OriginFor, pool_id: PoolId, - root: Option, - nominator: Option, - state_toggler: Option, + new_root: ConfigOp, + new_nominator: ConfigOp, + new_state_toggler: ConfigOp, ) -> DispatchResult { let mut bonded_pool = match ensure_root(origin.clone()) { Ok(()) => BondedPool::::get(pool_id).ok_or(Error::::PoolNotFound)?, @@ -1865,17 +1910,20 @@ pub mod pallet { }, }; - match root { - None => (), - Some(v) => bonded_pool.roles.root = v, + match new_root { + ConfigOp::Noop => (), + ConfigOp::Remove => bonded_pool.roles.root = None, + ConfigOp::Set(v) => bonded_pool.roles.root = Some(v), }; - match nominator { - None => (), - Some(v) => bonded_pool.roles.nominator = v, + match new_nominator { + ConfigOp::Noop => (), + ConfigOp::Remove => bonded_pool.roles.nominator = None, + ConfigOp::Set(v) => bonded_pool.roles.nominator = Some(v), }; - match state_toggler { - None => (), - Some(v) => bonded_pool.roles.state_toggler = v, + match new_state_toggler { + ConfigOp::Noop => (), + ConfigOp::Remove => bonded_pool.roles.state_toggler = None, + ConfigOp::Set(v) => bonded_pool.roles.state_toggler = Some(v), }; Self::deposit_event(Event::::RolesUpdated { @@ -2282,7 +2330,7 @@ impl OnStakerSlash> for Pallet { _slashed_bonded: BalanceOf, slashed_unlocking: &BTreeMap>, ) { - if let Some(pool_id) = ReversePoolIdLookup::::get(pool_account) { + if let Some(pool_id) = ReversePoolIdLookup::::get(pool_account).defensive() { let mut sub_pools = match SubPoolsStorage::::get(pool_id).defensive() { Some(sub_pools) => sub_pools, None => return, diff --git a/frame/nomination-pools/src/migration.rs b/frame/nomination-pools/src/migration.rs new file mode 100644 index 0000000000000..e23a35fe85602 --- /dev/null +++ b/frame/nomination-pools/src/migration.rs @@ -0,0 +1,105 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; + +pub mod v1 { + use super::*; + use crate::log; + use frame_support::traits::OnRuntimeUpgrade; + + #[derive(Decode)] + pub struct OldPoolRoles { + pub depositor: AccountId, + pub root: AccountId, + pub nominator: AccountId, + pub state_toggler: AccountId, + } + + impl OldPoolRoles { + fn migrate_to_v1(self) -> PoolRoles { + PoolRoles { + depositor: self.depositor, + root: Some(self.root), + nominator: Some(self.nominator), + state_toggler: Some(self.state_toggler), + } + } + } + + #[derive(Decode)] + pub struct OldBondedPoolInner { + pub points: BalanceOf, + pub state: PoolState, + pub member_counter: u32, + pub roles: OldPoolRoles, + } + + impl OldBondedPoolInner { + fn migrate_to_v1(self) -> BondedPoolInner { + BondedPoolInner { + member_counter: self.member_counter, + points: self.points, + state: self.state, + roles: self.roles.migrate_to_v1(), + } + } + } + + /// Trivial migration which makes the roles of each pool optional. + /// + /// Note: The depositor is not optional since he can never change. + pub struct MigrateToV1(sp_std::marker::PhantomData); + impl OnRuntimeUpgrade for MigrateToV1 { + fn on_runtime_upgrade() -> Weight { + let current = Pallet::::current_storage_version(); + let onchain = Pallet::::on_chain_storage_version(); + + log!( + info, + "Running migration with current storage version {:?} / onchain {:?}", + current, + onchain + ); + + if current == 1 && onchain == 0 { + // this is safe to execute on any runtime that has a bounded number of pools. + let mut translated = 0u64; + BondedPools::::translate::, _>(|_key, old_value| { + translated.saturating_inc(); + Some(old_value.migrate_to_v1()) + }); + + current.put::>(); + + log!(info, "Upgraded {} pools, storage to version {:?}", translated, current); + + T::DbWeight::get().reads_writes(translated + 1, translated + 1) + } else { + log!(info, "Migration did not executed. This probably should be removed"); + T::DbWeight::get().reads(1) + } + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade() -> Result<(), &'static str> { + // new version must be set. + assert_eq!(Pallet::::on_chain_storage_version(), 1); + Ok(()) + } + } +} diff --git a/frame/nomination-pools/src/tests.rs b/frame/nomination-pools/src/tests.rs index f39b5c92375c4..fe78da3bb14af 100644 --- a/frame/nomination-pools/src/tests.rs +++ b/frame/nomination-pools/src/tests.rs @@ -38,7 +38,7 @@ macro_rules! member_unbonding_eras { } pub const DEFAULT_ROLES: PoolRoles = - PoolRoles { depositor: 10, root: 900, nominator: 901, state_toggler: 902 }; + PoolRoles { depositor: 10, root: Some(900), nominator: Some(901), state_toggler: Some(902) }; #[test] fn test_setup_works() { @@ -333,6 +333,8 @@ mod sub_pools { fn maybe_merge_pools_works() { ExtBuilder::default().build_and_execute(|| { assert_eq!(TotalUnbondingPools::::get(), 5); + assert_eq!(BondingDuration::get(), 3); + assert_eq!(PostUnbondingPoolsWindow::get(), 2); // Given let mut sub_pool_0 = SubPools:: { @@ -347,19 +349,19 @@ mod sub_pools { }; // When `current_era < TotalUnbondingPools`, - let sub_pool_1 = sub_pool_0.clone().maybe_merge_pools(3); + let sub_pool_1 = sub_pool_0.clone().maybe_merge_pools(0); // Then it exits early without modifications assert_eq!(sub_pool_1, sub_pool_0); // When `current_era == TotalUnbondingPools`, - let sub_pool_1 = sub_pool_1.maybe_merge_pools(4); + let sub_pool_1 = sub_pool_1.maybe_merge_pools(1); // Then it exits early without modifications assert_eq!(sub_pool_1, sub_pool_0); // When `current_era - TotalUnbondingPools == 0`, - let mut sub_pool_1 = sub_pool_1.maybe_merge_pools(5); + let mut sub_pool_1 = sub_pool_1.maybe_merge_pools(2); // Then era 0 is merged into the `no_era` pool sub_pool_0.no_era = sub_pool_0.with_era.remove(&0).unwrap(); @@ -376,7 +378,7 @@ mod sub_pools { .unwrap(); // When `current_era - TotalUnbondingPools == 1` - let sub_pool_2 = sub_pool_1.maybe_merge_pools(6); + let sub_pool_2 = sub_pool_1.maybe_merge_pools(3); let era_1_pool = sub_pool_0.with_era.remove(&1).unwrap(); // Then era 1 is merged into the `no_era` pool @@ -385,7 +387,7 @@ mod sub_pools { assert_eq!(sub_pool_2, sub_pool_0); // When `current_era - TotalUnbondingPools == 5`, so all pools with era <= 4 are removed - let sub_pool_3 = sub_pool_2.maybe_merge_pools(10); + let sub_pool_3 = sub_pool_2.maybe_merge_pools(7); // Then all eras <= 5 are merged into the `no_era` pool for era in 2..=5 { @@ -1723,9 +1725,9 @@ mod unbond { // Given unsafe_set_state(1, PoolState::Blocked).unwrap(); let bonded_pool = BondedPool::::get(1).unwrap(); - assert_eq!(bonded_pool.roles.root, 900); - assert_eq!(bonded_pool.roles.nominator, 901); - assert_eq!(bonded_pool.roles.state_toggler, 902); + assert_eq!(bonded_pool.roles.root.unwrap(), 900); + assert_eq!(bonded_pool.roles.nominator.unwrap(), 901); + assert_eq!(bonded_pool.roles.state_toggler.unwrap(), 902); // When the nominator tries to kick, then its a noop assert_noop!( @@ -3143,9 +3145,9 @@ mod create { state: PoolState::Open, roles: PoolRoles { depositor: 11, - root: 123, - nominator: 456, - state_toggler: 789 + root: Some(123), + nominator: Some(456), + state_toggler: Some(789) } } } @@ -3590,71 +3592,164 @@ mod update_roles { ExtBuilder::default().build_and_execute(|| { assert_eq!( BondedPools::::get(1).unwrap().roles, - PoolRoles { depositor: 10, root: 900, nominator: 901, state_toggler: 902 }, + PoolRoles { + depositor: 10, + root: Some(900), + nominator: Some(901), + state_toggler: Some(902) + }, ); // non-existent pools assert_noop!( - Pools::update_roles(Origin::signed(1), 2, Some(5), Some(6), Some(7)), + Pools::update_roles( + Origin::signed(1), + 2, + ConfigOp::Set(5), + ConfigOp::Set(6), + ConfigOp::Set(7) + ), Error::::PoolNotFound, ); // depositor cannot change roles. assert_noop!( - Pools::update_roles(Origin::signed(1), 1, Some(5), Some(6), Some(7)), + Pools::update_roles( + Origin::signed(1), + 1, + ConfigOp::Set(5), + ConfigOp::Set(6), + ConfigOp::Set(7) + ), Error::::DoesNotHavePermission, ); // nominator cannot change roles. assert_noop!( - Pools::update_roles(Origin::signed(901), 1, Some(5), Some(6), Some(7)), + Pools::update_roles( + Origin::signed(901), + 1, + ConfigOp::Set(5), + ConfigOp::Set(6), + ConfigOp::Set(7) + ), Error::::DoesNotHavePermission, ); // state-toggler assert_noop!( - Pools::update_roles(Origin::signed(902), 1, Some(5), Some(6), Some(7)), + Pools::update_roles( + Origin::signed(902), + 1, + ConfigOp::Set(5), + ConfigOp::Set(6), + ConfigOp::Set(7) + ), Error::::DoesNotHavePermission, ); // but root can - assert_ok!(Pools::update_roles(Origin::signed(900), 1, Some(5), Some(6), Some(7))); + assert_ok!(Pools::update_roles( + Origin::signed(900), + 1, + ConfigOp::Set(5), + ConfigOp::Set(6), + ConfigOp::Set(7) + )); assert_eq!( pool_events_since_last_call(), vec![ Event::Created { depositor: 10, pool_id: 1 }, Event::Bonded { member: 10, pool_id: 1, bonded: 10, joined: true }, - Event::RolesUpdated { root: 5, state_toggler: 7, nominator: 6 } + Event::RolesUpdated { + root: Some(5), + state_toggler: Some(7), + nominator: Some(6) + } ] ); assert_eq!( BondedPools::::get(1).unwrap().roles, - PoolRoles { depositor: 10, root: 5, nominator: 6, state_toggler: 7 }, + PoolRoles { + depositor: 10, + root: Some(5), + nominator: Some(6), + state_toggler: Some(7) + }, ); // also root origin can - assert_ok!(Pools::update_roles(Origin::root(), 1, Some(1), Some(2), Some(3))); + assert_ok!(Pools::update_roles( + Origin::root(), + 1, + ConfigOp::Set(1), + ConfigOp::Set(2), + ConfigOp::Set(3) + )); + + assert_eq!( + pool_events_since_last_call(), + vec![Event::RolesUpdated { + root: Some(1), + state_toggler: Some(3), + nominator: Some(2) + }] + ); + assert_eq!( + BondedPools::::get(1).unwrap().roles, + PoolRoles { + depositor: 10, + root: Some(1), + nominator: Some(2), + state_toggler: Some(3) + }, + ); + + // Noop works + assert_ok!(Pools::update_roles( + Origin::root(), + 1, + ConfigOp::Set(11), + ConfigOp::Noop, + ConfigOp::Noop + )); assert_eq!( pool_events_since_last_call(), - vec![Event::RolesUpdated { root: 1, state_toggler: 3, nominator: 2 }] + vec![Event::RolesUpdated { + root: Some(11), + state_toggler: Some(3), + nominator: Some(2) + }] ); + assert_eq!( BondedPools::::get(1).unwrap().roles, - PoolRoles { depositor: 10, root: 1, nominator: 2, state_toggler: 3 }, + PoolRoles { + depositor: 10, + root: Some(11), + nominator: Some(2), + state_toggler: Some(3) + }, ); - // None is a noop - assert_ok!(Pools::update_roles(Origin::root(), 1, Some(11), None, None)); + // Remove works + assert_ok!(Pools::update_roles( + Origin::root(), + 1, + ConfigOp::Set(69), + ConfigOp::Remove, + ConfigOp::Remove + )); assert_eq!( pool_events_since_last_call(), - vec![Event::RolesUpdated { root: 11, state_toggler: 3, nominator: 2 }] + vec![Event::RolesUpdated { root: Some(69), state_toggler: None, nominator: None }] ); assert_eq!( BondedPools::::get(1).unwrap().roles, - PoolRoles { depositor: 10, root: 11, nominator: 2, state_toggler: 3 }, + PoolRoles { depositor: 10, root: Some(69), nominator: None, state_toggler: None }, ); }) } diff --git a/frame/staking/src/lib.rs b/frame/staking/src/lib.rs index 2d4c1ea9a3488..f17d09b413606 100644 --- a/frame/staking/src/lib.rs +++ b/frame/staking/src/lib.rs @@ -533,10 +533,12 @@ impl StakingLedger { /// case that either the active bonded or some unlocking chunks become dust after slashing. /// Returns the amount of funds actually slashed. /// + /// `slash_era` is the era in which the slash (which is being enacted now) actually happened. + /// /// # Note /// - /// This calls `Config::OnStakerSlash::on_slash` with information as to how the slash - /// was applied. + /// This calls `Config::OnStakerSlash::on_slash` with information as to how the slash was + /// applied. fn slash( &mut self, slash_amount: BalanceOf, @@ -615,6 +617,7 @@ impl StakingLedger { break } } + self.unlocking.retain(|c| !c.value.is_zero()); T::OnStakerSlash::on_slash(&self.stash, self.active, &slashed_unlocking); pre_slash_total.saturating_sub(self.total) diff --git a/frame/staking/src/tests.rs b/frame/staking/src/tests.rs index 21d4714985c6b..ccd9558c5c21d 100644 --- a/frame/staking/src/tests.rs +++ b/frame/staking/src/tests.rs @@ -2787,6 +2787,80 @@ fn deferred_slashes_are_deferred() { }) } +#[test] +fn staker_cannot_bail_deferred_slash() { + // as long as SlashDeferDuration is less than BondingDuration, this should not be possible. + ExtBuilder::default().slash_defer_duration(2).build_and_execute(|| { + mock::start_active_era(1); + + assert_eq!(Balances::free_balance(11), 1000); + assert_eq!(Balances::free_balance(101), 2000); + + let exposure = Staking::eras_stakers(active_era(), 11); + let nominated_value = exposure.others.iter().find(|o| o.who == 101).unwrap().value; + + on_offence_now( + &[OffenceDetails { + offender: (11, Staking::eras_stakers(active_era(), 11)), + reporters: vec![], + }], + &[Perbill::from_percent(10)], + ); + + // now we chill + assert_ok!(Staking::chill(Origin::signed(100))); + assert_ok!(Staking::unbond(Origin::signed(100), 500)); + + assert_eq!(Staking::current_era().unwrap(), 1); + assert_eq!(active_era(), 1); + + assert_eq!( + Ledger::::get(100).unwrap(), + StakingLedger { + active: 0, + total: 500, + stash: 101, + claimed_rewards: Default::default(), + unlocking: bounded_vec![UnlockChunk { era: 4u32, value: 500 }], + } + ); + + // no slash yet. + assert_eq!(Balances::free_balance(11), 1000); + assert_eq!(Balances::free_balance(101), 2000); + + // no slash yet. + mock::start_active_era(2); + assert_eq!(Balances::free_balance(11), 1000); + assert_eq!(Balances::free_balance(101), 2000); + assert_eq!(Staking::current_era().unwrap(), 2); + assert_eq!(active_era(), 2); + + // no slash yet. + mock::start_active_era(3); + assert_eq!(Balances::free_balance(11), 1000); + assert_eq!(Balances::free_balance(101), 2000); + assert_eq!(Staking::current_era().unwrap(), 3); + assert_eq!(active_era(), 3); + + // and cannot yet unbond: + assert_storage_noop!(assert!(Staking::withdraw_unbonded(Origin::signed(100), 0).is_ok())); + assert_eq!( + Ledger::::get(100).unwrap().unlocking.into_inner(), + vec![UnlockChunk { era: 4u32, value: 500 as Balance }], + ); + + // at the start of era 4, slashes from era 1 are processed, + // after being deferred for at least 2 full eras. + mock::start_active_era(4); + + assert_eq!(Balances::free_balance(11), 900); + assert_eq!(Balances::free_balance(101), 2000 - (nominated_value / 10)); + + // and the leftover of the funds can now be unbonded. + }) +} + #[test] fn remove_deferred() { ExtBuilder::default().slash_defer_duration(2).build_and_execute(|| { @@ -4856,7 +4930,7 @@ fn force_apply_min_commission_works() { } #[test] -fn ledger_slash_works() { +fn proportional_ledger_slash_works() { let c = |era, value| UnlockChunk:: { era, value }; // Given let mut ledger = StakingLedger:: {