diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 8ed5f1c847..aa1a525bf0 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -581,8 +581,9 @@ impl pallet_staking::Config for Runtime { impl pallet_fast_unstake::Config for Runtime { type RuntimeEvent = RuntimeEvent; - type SlashPerEra = ConstU128<{ DOLLARS }>; type ControlOrigin = frame_system::EnsureRoot; + type Deposit = ConstU128<{ DOLLARS }>; + type DepositCurrency = Balances; type WeightInfo = (); } diff --git a/substrate/frame/fast-unstake/src/benchmarking.rs b/substrate/frame/fast-unstake/src/benchmarking.rs index 5690d5ce6f..8770cc6b64 100644 --- a/substrate/frame/fast-unstake/src/benchmarking.rs +++ b/substrate/frame/fast-unstake/src/benchmarking.rs @@ -110,18 +110,18 @@ fn on_idle_full_block() { benchmarks! { // on_idle, we we don't check anyone, but fully unbond and move them to another pool. on_idle_unstake { + ErasToCheckPerBlock::::put(1); let who = create_unexposed_nominator::(); assert_ok!(FastUnstake::::register_fast_unstake( RawOrigin::Signed(who.clone()).into(), )); - ErasToCheckPerBlock::::put(1); // run on_idle once. This will check era 0. assert_eq!(Head::::get(), None); on_idle_full_block::(); assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: who.clone(), checked: vec![0].try_into().unwrap() }) + Some(UnstakeRequest { stash: who.clone(), checked: vec![0].try_into().unwrap(), deposit: T::Deposit::get() }) ); } : { @@ -162,7 +162,7 @@ benchmarks! { let checked: frame_support::BoundedVec<_, _> = (1..=u).rev().collect::>().try_into().unwrap(); assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: who.clone(), checked }) + Some(UnstakeRequest { stash: who.clone(), checked, deposit: T::Deposit::get() }) ); assert!(matches!( fast_unstake_events::().last(), @@ -171,6 +171,7 @@ benchmarks! { } register_fast_unstake { + ErasToCheckPerBlock::::put(1); let who = create_unexposed_nominator::(); whitelist_account!(who); assert_eq!(Queue::::count(), 0); @@ -182,6 +183,7 @@ benchmarks! { } deregister { + ErasToCheckPerBlock::::put(1); let who = create_unexposed_nominator::(); assert_ok!(FastUnstake::::register_fast_unstake( RawOrigin::Signed(who.clone()).into(), diff --git a/substrate/frame/fast-unstake/src/lib.rs b/substrate/frame/fast-unstake/src/lib.rs index 7fbac8560e..ed26d6b436 100644 --- a/substrate/frame/fast-unstake/src/lib.rs +++ b/substrate/frame/fast-unstake/src/lib.rs @@ -81,7 +81,10 @@ pub mod pallet { use super::*; use crate::types::*; use frame_election_provider_support::ElectionProvider; - use frame_support::pallet_prelude::*; + use frame_support::{ + pallet_prelude::*, + traits::{Defensive, ReservableCurrency}, + }; use frame_system::{pallet_prelude::*, RawOrigin}; use pallet_staking::Pallet as Staking; use sp_runtime::{ @@ -90,7 +93,6 @@ pub mod pallet { }; use sp_staking::EraIndex; use sp_std::{prelude::*, vec::Vec}; - pub use types::PreventStakingOpsIfUnbonding; pub use weights::WeightInfo; #[derive(scale_info::TypeInfo, codec::Encode, codec::Decode, codec::MaxEncodedLen)] @@ -113,10 +115,12 @@ pub mod pallet { + IsType<::RuntimeEvent> + TryInto>; - /// The amount of balance slashed per each era that was wastefully checked. - /// - /// A reasonable value could be `runtime_weight_to_fee(weight_per_era_check)`. - type SlashPerEra: Get>; + /// The currency used for deposits. + type DepositCurrency: ReservableCurrency>; + + /// Deposit to take for unstaking, to make sure we're able to slash the it in order to cover + /// the costs of resources on unsuccessful unstake. + type Deposit: Get>; /// The origin that can control this pallet. type ControlOrigin: frame_support::traits::EnsureOrigin; @@ -128,13 +132,13 @@ pub mod pallet { /// The current "head of the queue" being unstaked. #[pallet::storage] pub type Head = - StorageValue<_, UnstakeRequest>, OptionQuery>; + StorageValue<_, UnstakeRequest, BalanceOf>, OptionQuery>; /// The map of all accounts wishing to be unstaked. /// - /// Keeps track of `AccountId` wishing to unstake. + /// Keeps track of `AccountId` wishing to unstake and it's corresponding deposit. #[pallet::storage] - pub type Queue = CountedStorageMap<_, Twox64Concat, T::AccountId, ()>; + pub type Queue = CountedStorageMap<_, Twox64Concat, T::AccountId, BalanceOf>; /// Number of eras to check per block. /// @@ -177,6 +181,8 @@ pub mod pallet { NotQueued, /// The provided un-staker is already in Head, and cannot deregister. AlreadyHead, + /// The call is not allowed at this point because the pallet is not active. + CallNotAllowed, } #[pallet::hooks] @@ -214,6 +220,8 @@ pub mod pallet { pub fn register_fast_unstake(origin: OriginFor) -> DispatchResult { let ctrl = ensure_signed(origin)?; + ensure!(ErasToCheckPerBlock::::get() != 0, >::CallNotAllowed); + let ledger = pallet_staking::Ledger::::get(&ctrl).ok_or(Error::::NotController)?; ensure!(!Queue::::contains_key(&ledger.stash), Error::::AlreadyQueued); @@ -231,8 +239,10 @@ pub mod pallet { Staking::::chill(RawOrigin::Signed(ctrl.clone()).into())?; Staking::::unbond(RawOrigin::Signed(ctrl).into(), ledger.total)?; + T::DepositCurrency::reserve(&ledger.stash, T::Deposit::get())?; + // enqueue them. - Queue::::insert(ledger.stash, ()); + Queue::::insert(ledger.stash, T::Deposit::get()); Ok(()) } @@ -246,6 +256,9 @@ pub mod pallet { #[pallet::weight(::WeightInfo::deregister())] pub fn deregister(origin: OriginFor) -> DispatchResult { let ctrl = ensure_signed(origin)?; + + ensure!(ErasToCheckPerBlock::::get() != 0, >::CallNotAllowed); + let stash = pallet_staking::Ledger::::get(&ctrl) .map(|l| l.stash) .ok_or(Error::::NotController)?; @@ -254,7 +267,17 @@ pub mod pallet { Head::::get().map_or(true, |UnstakeRequest { stash, .. }| stash != stash), Error::::AlreadyHead ); - Queue::::remove(stash); + let deposit = Queue::::take(stash.clone()); + + if let Some(deposit) = deposit.defensive() { + let remaining = T::DepositCurrency::unreserve(&stash, deposit); + if !remaining.is_zero() { + frame_support::defensive!("`not enough balance to unreserve`"); + ErasToCheckPerBlock::::put(0); + Self::deposit_event(Event::::InternalError) + } + } + Ok(()) } @@ -315,18 +338,23 @@ pub mod pallet { return T::DbWeight::get().reads(2) } - let UnstakeRequest { stash, mut checked } = match Head::::take().or_else(|| { - // NOTE: there is no order guarantees in `Queue`. - Queue::::drain() - .map(|(stash, _)| UnstakeRequest { stash, checked: Default::default() }) - .next() - }) { - None => { - // There's no `Head` and nothing in the `Queue`, nothing to do here. - return T::DbWeight::get().reads(4) - }, - Some(head) => head, - }; + let UnstakeRequest { stash, mut checked, deposit } = + match Head::::take().or_else(|| { + // NOTE: there is no order guarantees in `Queue`. + Queue::::drain() + .map(|(stash, deposit)| UnstakeRequest { + stash, + deposit, + checked: Default::default(), + }) + .next() + }) { + None => { + // There's no `Head` and nothing in the `Queue`, nothing to do here. + return T::DbWeight::get().reads(4) + }, + Some(head) => head, + }; log!( debug, @@ -381,9 +409,16 @@ pub mod pallet { num_slashing_spans, ); - log!(info, "unstaked {:?}, outcome: {:?}", stash, result); + let remaining = T::DepositCurrency::unreserve(&stash, deposit); + if !remaining.is_zero() { + frame_support::defensive!("`not enough balance to unreserve`"); + ErasToCheckPerBlock::::put(0); + Self::deposit_event(Event::::InternalError) + } else { + log!(info, "unstaked {:?}, outcome: {:?}", stash, result); + Self::deposit_event(Event::::Unstaked { stash, result }); + } - Self::deposit_event(Event::::Unstaked { stash, result }); ::WeightInfo::on_idle_unstake() } else { // eras remaining to be checked. @@ -406,22 +441,18 @@ pub mod pallet { // the last 28 eras, have registered yourself to be unstaked, midway being checked, // you are exposed. if is_exposed { - let amount = T::SlashPerEra::get() - .saturating_mul(eras_checked.saturating_add(checked.len() as u32).into()); - pallet_staking::slashing::do_slash::( - &stash, - amount, - &mut Default::default(), - &mut Default::default(), - current_era, - ); - log!(info, "slashed {:?} by {:?}", stash, amount); - Self::deposit_event(Event::::Slashed { stash, amount }); + T::DepositCurrency::slash_reserved(&stash, deposit); + log!(info, "slashed {:?} by {:?}", stash, deposit); + Self::deposit_event(Event::::Slashed { stash, amount: deposit }); } else { // Not exposed in these eras. match checked.try_extend(unchecked_eras_to_check.clone().into_iter()) { Ok(_) => { - Head::::put(UnstakeRequest { stash: stash.clone(), checked }); + Head::::put(UnstakeRequest { + stash: stash.clone(), + checked, + deposit, + }); Self::deposit_event(Event::::Checking { stash, eras: unchecked_eras_to_check, diff --git a/substrate/frame/fast-unstake/src/mock.rs b/substrate/frame/fast-unstake/src/mock.rs index 62f343709e..4c4c5f9ff2 100644 --- a/substrate/frame/fast-unstake/src/mock.rs +++ b/substrate/frame/fast-unstake/src/mock.rs @@ -164,12 +164,13 @@ impl Convert for U256ToBalance { } parameter_types! { - pub static SlashPerEra: u32 = 100; + pub static DepositAmount: u128 = 7; } impl fast_unstake::Config for Runtime { type RuntimeEvent = RuntimeEvent; - type SlashPerEra = SlashPerEra; + type Deposit = DepositAmount; + type DepositCurrency = Balances; type ControlOrigin = frame_system::EnsureRoot; type WeightInfo = (); } @@ -213,11 +214,11 @@ impl Default for ExtBuilder { fn default() -> Self { Self { exposed_nominators: vec![ - (1, 2, 100), - (3, 4, 100), - (5, 6, 100), - (7, 8, 100), - (9, 10, 100), + (1, 2, 7 + 100), + (3, 4, 7 + 100), + (5, 6, 7 + 100), + (7, 8, 7 + 100), + (9, 10, 7 + 100), ], } } @@ -270,8 +271,8 @@ impl ExtBuilder { .into_iter() .map(|(_, ctrl, balance)| (ctrl, balance * 2)), ) - .chain(validators_range.clone().map(|x| (x, 100))) - .chain(nominators_range.clone().map(|x| (x, 100))) + .chain(validators_range.clone().map(|x| (x, 7 + 100))) + .chain(nominators_range.clone().map(|x| (x, 7 + 100))) .collect::>(), } .assimilate_storage(&mut storage); diff --git a/substrate/frame/fast-unstake/src/tests.rs b/substrate/frame/fast-unstake/src/tests.rs index 5586443ce7..6e617fd992 100644 --- a/substrate/frame/fast-unstake/src/tests.rs +++ b/substrate/frame/fast-unstake/src/tests.rs @@ -35,6 +35,7 @@ fn test_setup_works() { #[test] fn register_works() { ExtBuilder::default().build_and_execute(|| { + ErasToCheckPerBlock::::put(1); // Controller account registers for fast unstake. assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2))); // Ensure stash is in the queue. @@ -42,9 +43,38 @@ fn register_works() { }); } +#[test] +fn register_insufficient_funds_fails() { + use pallet_balances::Error as BalancesError; + ExtBuilder::default().build_and_execute(|| { + ErasToCheckPerBlock::::put(1); + ::DepositCurrency::make_free_balance_be(&1, 3); + + // Controller account registers for fast unstake. + assert_noop!( + FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2)), + BalancesError::::InsufficientBalance, + ); + + // Ensure stash is in the queue. + assert_eq!(Queue::::get(1), None); + }); +} + +#[test] +fn register_disabled_fails() { + ExtBuilder::default().build_and_execute(|| { + assert_noop!( + FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2)), + Error::::CallNotAllowed + ); + }); +} + #[test] fn cannot_register_if_not_bonded() { ExtBuilder::default().build_and_execute(|| { + ErasToCheckPerBlock::::put(1); // Mint accounts 1 and 2 with 200 tokens. for _ in 1..2 { let _ = Balances::make_free_balance_be(&1, 200); @@ -60,8 +90,9 @@ fn cannot_register_if_not_bonded() { #[test] fn cannot_register_if_in_queue() { ExtBuilder::default().build_and_execute(|| { + ErasToCheckPerBlock::::put(1); // Insert some Queue item - Queue::::insert(1, ()); + Queue::::insert(1, 10); // Cannot re-register, already in queue assert_noop!( FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2)), @@ -73,8 +104,13 @@ fn cannot_register_if_in_queue() { #[test] fn cannot_register_if_head() { ExtBuilder::default().build_and_execute(|| { + ErasToCheckPerBlock::::put(1); // Insert some Head item for stash - Head::::put(UnstakeRequest { stash: 1, checked: bounded_vec![] }); + Head::::put(UnstakeRequest { + stash: 1, + checked: bounded_vec![], + deposit: DepositAmount::get(), + }); // Controller attempts to regsiter assert_noop!( FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2)), @@ -86,6 +122,7 @@ fn cannot_register_if_head() { #[test] fn cannot_register_if_has_unlocking_chunks() { ExtBuilder::default().build_and_execute(|| { + ErasToCheckPerBlock::::put(1); // Start unbonding half of staked tokens assert_ok!(Staking::unbond(RuntimeOrigin::signed(2), 50_u128)); // Cannot register for fast unstake with unlock chunks active @@ -99,18 +136,37 @@ fn cannot_register_if_has_unlocking_chunks() { #[test] fn deregister_works() { ExtBuilder::default().build_and_execute(|| { + ErasToCheckPerBlock::::put(1); + + assert_eq!(::DepositCurrency::reserved_balance(&1), 0); + // Controller account registers for fast unstake. assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2))); + assert_eq!(::DepositCurrency::reserved_balance(&1), DepositAmount::get()); + // Controller then changes mind and deregisters. assert_ok!(FastUnstake::deregister(RuntimeOrigin::signed(2))); + assert_eq!(::DepositCurrency::reserved_balance(&1), 0); + // Ensure stash no longer exists in the queue. assert_eq!(Queue::::get(1), None); }); } +#[test] +fn deregister_disabled_fails() { + ExtBuilder::default().build_and_execute(|| { + ErasToCheckPerBlock::::put(1); + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2))); + ErasToCheckPerBlock::::put(0); + assert_noop!(FastUnstake::deregister(RuntimeOrigin::signed(2)), Error::::CallNotAllowed); + }); +} + #[test] fn cannot_deregister_if_not_controller() { ExtBuilder::default().build_and_execute(|| { + ErasToCheckPerBlock::::put(1); // Controller account registers for fast unstake. assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2))); // Stash tries to deregister. @@ -121,6 +177,7 @@ fn cannot_deregister_if_not_controller() { #[test] fn cannot_deregister_if_not_queued() { ExtBuilder::default().build_and_execute(|| { + ErasToCheckPerBlock::::put(1); // Controller tries to deregister without first registering assert_noop!(FastUnstake::deregister(RuntimeOrigin::signed(2)), Error::::NotQueued); }); @@ -129,10 +186,15 @@ fn cannot_deregister_if_not_queued() { #[test] fn cannot_deregister_already_head() { ExtBuilder::default().build_and_execute(|| { + ErasToCheckPerBlock::::put(1); // Controller attempts to register, should fail assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2))); // Insert some Head item for stash. - Head::::put(UnstakeRequest { stash: 1, checked: bounded_vec![] }); + Head::::put(UnstakeRequest { + stash: 1, + checked: bounded_vec![], + deposit: DepositAmount::get(), + }); // Controller attempts to deregister assert_noop!(FastUnstake::deregister(RuntimeOrigin::signed(2)), Error::::AlreadyHead); }); @@ -165,14 +227,14 @@ mod on_idle { // set up Queue item assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2))); - assert_eq!(Queue::::get(1), Some(())); + assert_eq!(Queue::::get(1), Some(DepositAmount::get())); // call on_idle with no remaining weight FastUnstake::on_idle(System::block_number(), Weight::from_ref_time(0)); // assert nothing changed in Queue and Head assert_eq!(Head::::get(), None); - assert_eq!(Queue::::get(1), Some(())); + assert_eq!(Queue::::get(1), Some(DepositAmount::get())); }); } @@ -185,7 +247,7 @@ mod on_idle { // given assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2))); - assert_eq!(Queue::::get(1), Some(())); + assert_eq!(Queue::::get(1), Some(DepositAmount::get())); assert_eq!(Queue::::count(), 1); assert_eq!(Head::::get(), None); @@ -204,7 +266,11 @@ mod on_idle { ); assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: 1, checked: bounded_vec![3] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: 1, + checked: bounded_vec![3] + }) ); // when: another 1 era. @@ -220,7 +286,11 @@ mod on_idle { ); assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: 1, checked: bounded_vec![3, 2] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: 1, + checked: bounded_vec![3, 2] + }) ); // when: then 5 eras, we only need 2 more. @@ -242,7 +312,11 @@ mod on_idle { ); assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: 1, checked: bounded_vec![3, 2, 1, 0] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: 1, + checked: bounded_vec![3, 2, 1, 0] + }) ); // when: not enough weight to unstake: @@ -254,7 +328,11 @@ mod on_idle { assert_eq!(fast_unstake_events_since_last_call(), vec![]); assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: 1, checked: bounded_vec![3, 2, 1, 0] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: 1, + checked: bounded_vec![3, 2, 1, 0] + }) ); // when: enough weight to get over at least one iteration: then we are unblocked and can @@ -285,12 +363,16 @@ mod on_idle { CurrentEra::::put(BondingDuration::get()); // given + assert_eq!(::DepositCurrency::reserved_balance(&1), 0); + assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2))); assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(4))); assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(6))); assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(8))); assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(10))); + assert_eq!(::DepositCurrency::reserved_balance(&1), DepositAmount::get()); + assert_eq!(Queue::::count(), 5); assert_eq!(Head::::get(), None); @@ -300,7 +382,11 @@ mod on_idle { // then assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: 1, checked: bounded_vec![3, 2, 1, 0] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: 1, + checked: bounded_vec![3, 2, 1, 0] + }) ); assert_eq!(Queue::::count(), 4); @@ -317,10 +403,16 @@ mod on_idle { // then assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: 5, checked: bounded_vec![3, 2, 1, 0] }), + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: 5, + checked: bounded_vec![3, 2, 1, 0] + }), ); assert_eq!(Queue::::count(), 3); + assert_eq!(::DepositCurrency::reserved_balance(&1), 0); + assert_eq!( fast_unstake_events_since_last_call(), vec![ @@ -340,9 +432,9 @@ mod on_idle { // register multi accounts for fast unstake assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2))); - assert_eq!(Queue::::get(1), Some(())); + assert_eq!(Queue::::get(1), Some(DepositAmount::get())); assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(4))); - assert_eq!(Queue::::get(3), Some(())); + assert_eq!(Queue::::get(3), Some(DepositAmount::get())); // assert 2 queue items are in Queue & None in Head to start with assert_eq!(Queue::::count(), 2); @@ -391,7 +483,7 @@ mod on_idle { // register for fast unstake assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2))); - assert_eq!(Queue::::get(1), Some(())); + assert_eq!(Queue::::get(1), Some(DepositAmount::get())); // process on idle next_block(true); @@ -402,7 +494,11 @@ mod on_idle { // assert head item present assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: 1, checked: bounded_vec![3, 2, 1, 0] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: 1, + checked: bounded_vec![3, 2, 1, 0] + }) ); next_block(true); @@ -425,9 +521,11 @@ mod on_idle { ErasToCheckPerBlock::::put(BondingDuration::get() + 1); CurrentEra::::put(BondingDuration::get()); + Balances::make_free_balance_be(&2, 100); + // register for fast unstake assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2))); - assert_eq!(Queue::::get(1), Some(())); + assert_eq!(Queue::::get(1), Some(DepositAmount::get())); // process on idle next_block(true); @@ -438,7 +536,11 @@ mod on_idle { // assert head item present assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: 1, checked: bounded_vec![3, 2, 1, 0] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: 1, + checked: bounded_vec![3, 2, 1, 0] + }) ); next_block(true); @@ -464,7 +566,7 @@ mod on_idle { // register for fast unstake assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2))); - assert_eq!(Queue::::get(1), Some(())); + assert_eq!(Queue::::get(1), Some(DepositAmount::get())); // process on idle next_block(true); @@ -475,28 +577,44 @@ mod on_idle { // assert head item present assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: 1, checked: bounded_vec![3] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: 1, + checked: bounded_vec![3] + }) ); next_block(true); assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: 1, checked: bounded_vec![3, 2] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: 1, + checked: bounded_vec![3, 2] + }) ); next_block(true); assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: 1, checked: bounded_vec![3, 2, 1] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: 1, + checked: bounded_vec![3, 2, 1] + }) ); next_block(true); assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: 1, checked: bounded_vec![3, 2, 1, 0] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: 1, + checked: bounded_vec![3, 2, 1, 0] + }) ); next_block(true); @@ -529,30 +647,46 @@ mod on_idle { // register for fast unstake assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2))); - assert_eq!(Queue::::get(1), Some(())); + assert_eq!(Queue::::get(1), Some(DepositAmount::get())); next_block(true); assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: 1, checked: bounded_vec![3] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: 1, + checked: bounded_vec![3] + }) ); next_block(true); assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: 1, checked: bounded_vec![3, 2] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: 1, + checked: bounded_vec![3, 2] + }) ); next_block(true); assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: 1, checked: bounded_vec![3, 2, 1] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: 1, + checked: bounded_vec![3, 2, 1] + }) ); next_block(true); assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: 1, checked: bounded_vec![3, 2, 1, 0] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: 1, + checked: bounded_vec![3, 2, 1, 0] + }) ); // when: a new era happens right before one is free. @@ -567,6 +701,7 @@ mod on_idle { stash: 1, // note era 0 is pruned to keep the vector length sane. checked: bounded_vec![3, 2, 1, 4], + deposit: DepositAmount::get(), }) ); @@ -602,13 +737,21 @@ mod on_idle { next_block(true); assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: 1, checked: bounded_vec![3] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: 1, + checked: bounded_vec![3] + }) ); next_block(true); assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: 1, checked: bounded_vec![3, 2] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: 1, + checked: bounded_vec![3, 2] + }) ); // when @@ -618,13 +761,21 @@ mod on_idle { next_block(true); assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: 1, checked: bounded_vec![3, 2] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: 1, + checked: bounded_vec![3, 2] + }) ); next_block(true); assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: 1, checked: bounded_vec![3, 2] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: 1, + checked: bounded_vec![3, 2] + }) ); // then we register a new era. @@ -636,14 +787,22 @@ mod on_idle { next_block(true); assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: 1, checked: bounded_vec![3, 2, 4] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: 1, + checked: bounded_vec![3, 2, 4] + }) ); // progress to end next_block(true); assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: 1, checked: bounded_vec![3, 2, 4, 1] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: 1, + checked: bounded_vec![3, 2, 4, 1] + }) ); // but notice that we don't care about era 0 instead anymore! we're done. @@ -669,7 +828,6 @@ mod on_idle { fn exposed_nominator_cannot_unstake() { ExtBuilder::default().build_and_execute(|| { ErasToCheckPerBlock::::put(1); - SlashPerEra::set(7); CurrentEra::::put(BondingDuration::get()); // create an exposed nominator in era 1 @@ -686,6 +844,7 @@ mod on_idle { )); assert_ok!(Staking::nominate(RuntimeOrigin::signed(exposed), vec![exposed])); + Balances::make_free_balance_be(&exposed, 100_000); // register the exposed one. assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(exposed))); @@ -693,23 +852,30 @@ mod on_idle { next_block(true); assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: exposed, checked: bounded_vec![3] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: exposed, + checked: bounded_vec![3] + }) ); next_block(true); assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: exposed, checked: bounded_vec![3, 2] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: exposed, + checked: bounded_vec![3, 2] + }) ); next_block(true); assert_eq!(Head::::get(), None); assert_eq!( fast_unstake_events_since_last_call(), - // we slash them by 21, since we checked 3 eras in total (3, 2, 1). vec![ Event::Checking { stash: exposed, eras: vec![3] }, Event::Checking { stash: exposed, eras: vec![2] }, - Event::Slashed { stash: exposed, amount: 3 * 7 } + Event::Slashed { stash: exposed, amount: DepositAmount::get() } ] ); }); @@ -721,7 +887,6 @@ mod on_idle { // same as the previous check, but we check 2 eras per block, and we make the exposed be // exposed in era 0, so that it is detected halfway in a check era. ErasToCheckPerBlock::::put(2); - SlashPerEra::set(7); CurrentEra::::put(BondingDuration::get()); // create an exposed nominator in era 1 @@ -729,7 +894,7 @@ mod on_idle { pallet_staking::ErasStakers::::mutate(0, VALIDATORS_PER_ERA, |expo| { expo.others.push(IndividualExposure { who: exposed, value: 0 as Balance }); }); - Balances::make_free_balance_be(&exposed, 100); + Balances::make_free_balance_be(&exposed, DepositAmount::get() + 100); assert_ok!(Staking::bond( RuntimeOrigin::signed(exposed), exposed, @@ -745,17 +910,21 @@ mod on_idle { next_block(true); assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: exposed, checked: bounded_vec![3, 2] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: exposed, + checked: bounded_vec![3, 2] + }) ); next_block(true); assert_eq!(Head::::get(), None); assert_eq!( fast_unstake_events_since_last_call(), - // we slash them by 28, since we checked 4 eras in total. + // we slash them vec![ Event::Checking { stash: exposed, eras: vec![3, 2] }, - Event::Slashed { stash: exposed, amount: 4 * 7 } + Event::Slashed { stash: exposed, amount: DepositAmount::get() } ] ); }); @@ -786,7 +955,7 @@ mod on_idle { assert_eq!( fast_unstake_events_since_last_call(), - vec![Event::Slashed { stash: 100, amount: 100 }] + vec![Event::Slashed { stash: 100, amount: DepositAmount::get() }] ); }); } @@ -798,7 +967,7 @@ mod on_idle { CurrentEra::::put(BondingDuration::get()); // create a new validator that 100% not exposed. - Balances::make_free_balance_be(&42, 100); + Balances::make_free_balance_be(&42, 100 + DepositAmount::get()); assert_ok!(Staking::bond(RuntimeOrigin::signed(42), 42, 10, RewardDestination::Staked)); assert_ok!(Staking::validate(RuntimeOrigin::signed(42), Default::default())); @@ -809,7 +978,11 @@ mod on_idle { next_block(true); assert_eq!( Head::::get(), - Some(UnstakeRequest { stash: 42, checked: bounded_vec![3, 2, 1, 0] }) + Some(UnstakeRequest { + deposit: DepositAmount::get(), + stash: 42, + checked: bounded_vec![3, 2, 1, 0] + }) ); next_block(true); assert_eq!(Head::::get(), None); @@ -824,69 +997,3 @@ mod on_idle { }); } } - -mod signed_extension { - use super::*; - use sp_runtime::traits::SignedExtension; - - const STAKING_CALL: crate::mock::RuntimeCall = - crate::mock::RuntimeCall::Staking(pallet_staking::Call::::chill {}); - - #[test] - fn does_nothing_if_not_queued() { - ExtBuilder::default().build_and_execute(|| { - assert!(PreventStakingOpsIfUnbonding::::new() - .pre_dispatch(&1, &STAKING_CALL, &Default::default(), Default::default()) - .is_ok()); - }) - } - - #[test] - fn prevents_queued() { - ExtBuilder::default().build_and_execute(|| { - // given: stash for 2 is 1. - // when - assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2))); - - // then - // stash can't. - assert!(PreventStakingOpsIfUnbonding::::new() - .pre_dispatch(&1, &STAKING_CALL, &Default::default(), Default::default()) - .is_err()); - - // controller can't. - assert!(PreventStakingOpsIfUnbonding::::new() - .pre_dispatch(&2, &STAKING_CALL, &Default::default(), Default::default()) - .is_err()); - }) - } - - #[test] - fn prevents_head_stash() { - ExtBuilder::default().build_and_execute(|| { - // given: stash for 2 is 1. - // when - assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2))); - - ErasToCheckPerBlock::::put(1); - CurrentEra::::put(BondingDuration::get()); - next_block(true); - - assert_eq!( - Head::::get(), - Some(UnstakeRequest { stash: 1, checked: bounded_vec![3] }) - ); - - // then - // stash can't - assert!(PreventStakingOpsIfUnbonding::::new() - .pre_dispatch(&2, &STAKING_CALL, &Default::default(), Default::default()) - .is_err()); - - // controller can't - assert!(PreventStakingOpsIfUnbonding::::new() - .pre_dispatch(&1, &STAKING_CALL, &Default::default(), Default::default()) - .is_err()); - }) - } -} diff --git a/substrate/frame/fast-unstake/src/types.rs b/substrate/frame/fast-unstake/src/types.rs index 2ddb8dca27..08b9ab4326 100644 --- a/substrate/frame/fast-unstake/src/types.rs +++ b/substrate/frame/fast-unstake/src/types.rs @@ -17,14 +17,12 @@ //! Types used in the Fast Unstake pallet. -use crate::*; use codec::{Decode, Encode, MaxEncodedLen}; use frame_support::{ - traits::{Currency, Get, IsSubType}, + traits::{Currency, Get}, BoundedVec, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound, }; use scale_info::TypeInfo; -use sp_runtime::transaction_validity::{InvalidTransaction, TransactionValidityError}; use sp_staking::EraIndex; use sp_std::{fmt::Debug, prelude::*}; @@ -36,80 +34,15 @@ pub type BalanceOf = <::Currency as Currency< #[derive( Encode, Decode, EqNoBound, PartialEqNoBound, Clone, TypeInfo, RuntimeDebugNoBound, MaxEncodedLen, )] -pub struct UnstakeRequest> { +pub struct UnstakeRequest< + AccountId: Eq + PartialEq + Debug, + MaxChecked: Get, + Balance: PartialEq + Debug, +> { /// Their stash account. pub(crate) stash: AccountId, /// The list of eras for which they have been checked. pub(crate) checked: BoundedVec, -} - -#[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo, RuntimeDebugNoBound)] -#[scale_info(skip_type_params(T))] -pub struct PreventStakingOpsIfUnbonding(sp_std::marker::PhantomData); - -impl PreventStakingOpsIfUnbonding { - pub fn new() -> Self { - Self(Default::default()) - } -} - -impl sp_runtime::traits::SignedExtension - for PreventStakingOpsIfUnbonding -where - ::RuntimeCall: IsSubType>, -{ - type AccountId = T::AccountId; - type Call = ::RuntimeCall; - type AdditionalSigned = (); - type Pre = (); - const IDENTIFIER: &'static str = "PreventStakingOpsIfUnbonding"; - - fn additional_signed(&self) -> Result { - Ok(()) - } - - fn pre_dispatch( - self, - // NOTE: we want to prevent this stash-controller pair from doing anything in the - // staking system as long as they are registered here. - stash_or_controller: &Self::AccountId, - call: &Self::Call, - _info: &sp_runtime::traits::DispatchInfoOf, - _len: usize, - ) -> Result { - // we don't check this in the tx-pool as it requires a storage read. - if >>::is_sub_type(call).is_some() { - let check_stash = |stash: &T::AccountId| { - if Queue::::contains_key(&stash) || - Head::::get().map_or(false, |u| &u.stash == stash) - { - Err(TransactionValidityError::Invalid(InvalidTransaction::Call)) - } else { - Ok(()) - } - }; - match ( - // mapped from controller. - pallet_staking::Ledger::::get(&stash_or_controller), - // mapped from stash. - pallet_staking::Bonded::::get(&stash_or_controller), - ) { - (Some(ledger), None) => { - // it is a controller. - check_stash(&ledger.stash) - }, - (_, Some(_)) => { - // it's a stash. - let stash = stash_or_controller; - check_stash(stash) - }, - (None, None) => { - // They are not a staker -- let them execute. - Ok(()) - }, - } - } else { - Ok(()) - } - } + /// Deposit to be slashed if the unstake was unsuccessful. + pub(crate) deposit: Balance, }