[Feature] Add deposit to fast-unstake (#12366)

* [Feature] Add deposit to fast-unstake

* disable on ErasToCheckPerBlock == 0

* removed signed ext

* remove obsolete import

* remove some obsolete stuff

* fix some comments

* fixed all the comments

* remove obsolete imports

* fix some tests

* CallNotAllowed tests

* Update frame/fast-unstake/src/lib.rs

Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>

* fix tests

* fix deregister + tests

* more fixes

* make sure we go above existential deposit

* fixed the last test

* some nit fixes

* fix node

* fix bench

* last bench fix

* Update frame/fast-unstake/src/lib.rs

* ".git/.scripts/fmt.sh" 1

Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
Co-authored-by: command-bot <>
This commit is contained in:
Roman Useinov
2022-09-27 19:31:12 +02:00
committed by GitHub
parent e6b1aae97f
commit 1f687256fb
6 changed files with 313 additions and 238 deletions
+2 -1
View File
@@ -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<AccountId>;
type Deposit = ConstU128<{ DOLLARS }>;
type DepositCurrency = Balances;
type WeightInfo = ();
}
@@ -110,18 +110,18 @@ fn on_idle_full_block<T: Config>() {
benchmarks! {
// on_idle, we we don't check anyone, but fully unbond and move them to another pool.
on_idle_unstake {
ErasToCheckPerBlock::<T>::put(1);
let who = create_unexposed_nominator::<T>();
assert_ok!(FastUnstake::<T>::register_fast_unstake(
RawOrigin::Signed(who.clone()).into(),
));
ErasToCheckPerBlock::<T>::put(1);
// run on_idle once. This will check era 0.
assert_eq!(Head::<T>::get(), None);
on_idle_full_block::<T>();
assert_eq!(
Head::<T>::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::<Vec<EraIndex>>().try_into().unwrap();
assert_eq!(
Head::<T>::get(),
Some(UnstakeRequest { stash: who.clone(), checked })
Some(UnstakeRequest { stash: who.clone(), checked, deposit: T::Deposit::get() })
);
assert!(matches!(
fast_unstake_events::<T>().last(),
@@ -171,6 +171,7 @@ benchmarks! {
}
register_fast_unstake {
ErasToCheckPerBlock::<T>::put(1);
let who = create_unexposed_nominator::<T>();
whitelist_account!(who);
assert_eq!(Queue::<T>::count(), 0);
@@ -182,6 +183,7 @@ benchmarks! {
}
deregister {
ErasToCheckPerBlock::<T>::put(1);
let who = create_unexposed_nominator::<T>();
assert_ok!(FastUnstake::<T>::register_fast_unstake(
RawOrigin::Signed(who.clone()).into(),
+68 -37
View File
@@ -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<<Self as frame_system::Config>::RuntimeEvent>
+ TryInto<Event<Self>>;
/// 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<BalanceOf<Self>>;
/// The currency used for deposits.
type DepositCurrency: ReservableCurrency<Self::AccountId, Balance = BalanceOf<Self>>;
/// 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<BalanceOf<Self>>;
/// The origin that can control this pallet.
type ControlOrigin: frame_support::traits::EnsureOrigin<Self::RuntimeOrigin>;
@@ -128,13 +132,13 @@ pub mod pallet {
/// The current "head of the queue" being unstaked.
#[pallet::storage]
pub type Head<T: Config> =
StorageValue<_, UnstakeRequest<T::AccountId, MaxChecking<T>>, OptionQuery>;
StorageValue<_, UnstakeRequest<T::AccountId, MaxChecking<T>, BalanceOf<T>>, 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<T: Config> = CountedStorageMap<_, Twox64Concat, T::AccountId, ()>;
pub type Queue<T: Config> = CountedStorageMap<_, Twox64Concat, T::AccountId, BalanceOf<T>>;
/// 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<T>) -> DispatchResult {
let ctrl = ensure_signed(origin)?;
ensure!(ErasToCheckPerBlock::<T>::get() != 0, <Error<T>>::CallNotAllowed);
let ledger =
pallet_staking::Ledger::<T>::get(&ctrl).ok_or(Error::<T>::NotController)?;
ensure!(!Queue::<T>::contains_key(&ledger.stash), Error::<T>::AlreadyQueued);
@@ -231,8 +239,10 @@ pub mod pallet {
Staking::<T>::chill(RawOrigin::Signed(ctrl.clone()).into())?;
Staking::<T>::unbond(RawOrigin::Signed(ctrl).into(), ledger.total)?;
T::DepositCurrency::reserve(&ledger.stash, T::Deposit::get())?;
// enqueue them.
Queue::<T>::insert(ledger.stash, ());
Queue::<T>::insert(ledger.stash, T::Deposit::get());
Ok(())
}
@@ -246,6 +256,9 @@ pub mod pallet {
#[pallet::weight(<T as Config>::WeightInfo::deregister())]
pub fn deregister(origin: OriginFor<T>) -> DispatchResult {
let ctrl = ensure_signed(origin)?;
ensure!(ErasToCheckPerBlock::<T>::get() != 0, <Error<T>>::CallNotAllowed);
let stash = pallet_staking::Ledger::<T>::get(&ctrl)
.map(|l| l.stash)
.ok_or(Error::<T>::NotController)?;
@@ -254,7 +267,17 @@ pub mod pallet {
Head::<T>::get().map_or(true, |UnstakeRequest { stash, .. }| stash != stash),
Error::<T>::AlreadyHead
);
Queue::<T>::remove(stash);
let deposit = Queue::<T>::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::<T>::put(0);
Self::deposit_event(Event::<T>::InternalError)
}
}
Ok(())
}
@@ -315,18 +338,23 @@ pub mod pallet {
return T::DbWeight::get().reads(2)
}
let UnstakeRequest { stash, mut checked } = match Head::<T>::take().or_else(|| {
// NOTE: there is no order guarantees in `Queue`.
Queue::<T>::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::<T>::take().or_else(|| {
// NOTE: there is no order guarantees in `Queue`.
Queue::<T>::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::<T>::put(0);
Self::deposit_event(Event::<T>::InternalError)
} else {
log!(info, "unstaked {:?}, outcome: {:?}", stash, result);
Self::deposit_event(Event::<T>::Unstaked { stash, result });
}
Self::deposit_event(Event::<T>::Unstaked { stash, result });
<T as Config>::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::<T>(
&stash,
amount,
&mut Default::default(),
&mut Default::default(),
current_era,
);
log!(info, "slashed {:?} by {:?}", stash, amount);
Self::deposit_event(Event::<T>::Slashed { stash, amount });
T::DepositCurrency::slash_reserved(&stash, deposit);
log!(info, "slashed {:?} by {:?}", stash, deposit);
Self::deposit_event(Event::<T>::Slashed { stash, amount: deposit });
} else {
// Not exposed in these eras.
match checked.try_extend(unchecked_eras_to_check.clone().into_iter()) {
Ok(_) => {
Head::<T>::put(UnstakeRequest { stash: stash.clone(), checked });
Head::<T>::put(UnstakeRequest {
stash: stash.clone(),
checked,
deposit,
});
Self::deposit_event(Event::<T>::Checking {
stash,
eras: unchecked_eras_to_check,
+10 -9
View File
@@ -164,12 +164,13 @@ impl Convert<sp_core::U256, Balance> 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<Self::AccountId>;
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::<Vec<_>>(),
}
.assimilate_storage(&mut storage);
+220 -113
View File
@@ -35,6 +35,7 @@ fn test_setup_works() {
#[test]
fn register_works() {
ExtBuilder::default().build_and_execute(|| {
ErasToCheckPerBlock::<T>::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::<T>::put(1);
<T as Config>::DepositCurrency::make_free_balance_be(&1, 3);
// Controller account registers for fast unstake.
assert_noop!(
FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2)),
BalancesError::<T, _>::InsufficientBalance,
);
// Ensure stash is in the queue.
assert_eq!(Queue::<T>::get(1), None);
});
}
#[test]
fn register_disabled_fails() {
ExtBuilder::default().build_and_execute(|| {
assert_noop!(
FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2)),
Error::<T>::CallNotAllowed
);
});
}
#[test]
fn cannot_register_if_not_bonded() {
ExtBuilder::default().build_and_execute(|| {
ErasToCheckPerBlock::<T>::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::<T>::put(1);
// Insert some Queue item
Queue::<T>::insert(1, ());
Queue::<T>::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::<T>::put(1);
// Insert some Head item for stash
Head::<T>::put(UnstakeRequest { stash: 1, checked: bounded_vec![] });
Head::<T>::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::<T>::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::<T>::put(1);
assert_eq!(<T as Config>::DepositCurrency::reserved_balance(&1), 0);
// Controller account registers for fast unstake.
assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2)));
assert_eq!(<T as Config>::DepositCurrency::reserved_balance(&1), DepositAmount::get());
// Controller then changes mind and deregisters.
assert_ok!(FastUnstake::deregister(RuntimeOrigin::signed(2)));
assert_eq!(<T as Config>::DepositCurrency::reserved_balance(&1), 0);
// Ensure stash no longer exists in the queue.
assert_eq!(Queue::<T>::get(1), None);
});
}
#[test]
fn deregister_disabled_fails() {
ExtBuilder::default().build_and_execute(|| {
ErasToCheckPerBlock::<T>::put(1);
assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2)));
ErasToCheckPerBlock::<T>::put(0);
assert_noop!(FastUnstake::deregister(RuntimeOrigin::signed(2)), Error::<T>::CallNotAllowed);
});
}
#[test]
fn cannot_deregister_if_not_controller() {
ExtBuilder::default().build_and_execute(|| {
ErasToCheckPerBlock::<T>::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::<T>::put(1);
// Controller tries to deregister without first registering
assert_noop!(FastUnstake::deregister(RuntimeOrigin::signed(2)), Error::<T>::NotQueued);
});
@@ -129,10 +186,15 @@ fn cannot_deregister_if_not_queued() {
#[test]
fn cannot_deregister_already_head() {
ExtBuilder::default().build_and_execute(|| {
ErasToCheckPerBlock::<T>::put(1);
// Controller attempts to register, should fail
assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2)));
// Insert some Head item for stash.
Head::<T>::put(UnstakeRequest { stash: 1, checked: bounded_vec![] });
Head::<T>::put(UnstakeRequest {
stash: 1,
checked: bounded_vec![],
deposit: DepositAmount::get(),
});
// Controller attempts to deregister
assert_noop!(FastUnstake::deregister(RuntimeOrigin::signed(2)), Error::<T>::AlreadyHead);
});
@@ -165,14 +227,14 @@ mod on_idle {
// set up Queue item
assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(2)));
assert_eq!(Queue::<T>::get(1), Some(()));
assert_eq!(Queue::<T>::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::<T>::get(), None);
assert_eq!(Queue::<T>::get(1), Some(()));
assert_eq!(Queue::<T>::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::<T>::get(1), Some(()));
assert_eq!(Queue::<T>::get(1), Some(DepositAmount::get()));
assert_eq!(Queue::<T>::count(), 1);
assert_eq!(Head::<T>::get(), None);
@@ -204,7 +266,11 @@ mod on_idle {
);
assert_eq!(
Head::<T>::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::<T>::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::<T>::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::<T>::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::<T>::put(BondingDuration::get());
// given
assert_eq!(<T as Config>::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!(<T as Config>::DepositCurrency::reserved_balance(&1), DepositAmount::get());
assert_eq!(Queue::<T>::count(), 5);
assert_eq!(Head::<T>::get(), None);
@@ -300,7 +382,11 @@ mod on_idle {
// then
assert_eq!(
Head::<T>::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::<T>::count(), 4);
@@ -317,10 +403,16 @@ mod on_idle {
// then
assert_eq!(
Head::<T>::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::<T>::count(), 3);
assert_eq!(<T as Config>::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::<T>::get(1), Some(()));
assert_eq!(Queue::<T>::get(1), Some(DepositAmount::get()));
assert_ok!(FastUnstake::register_fast_unstake(RuntimeOrigin::signed(4)));
assert_eq!(Queue::<T>::get(3), Some(()));
assert_eq!(Queue::<T>::get(3), Some(DepositAmount::get()));
// assert 2 queue items are in Queue & None in Head to start with
assert_eq!(Queue::<T>::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::<T>::get(1), Some(()));
assert_eq!(Queue::<T>::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::<T>::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::<T>::put(BondingDuration::get() + 1);
CurrentEra::<T>::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::<T>::get(1), Some(()));
assert_eq!(Queue::<T>::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::<T>::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::<T>::get(1), Some(()));
assert_eq!(Queue::<T>::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::<T>::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::<T>::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::<T>::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::<T>::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::<T>::get(1), Some(()));
assert_eq!(Queue::<T>::get(1), Some(DepositAmount::get()));
next_block(true);
assert_eq!(
Head::<T>::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::<T>::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::<T>::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::<T>::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::<T>::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::<T>::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::<T>::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::<T>::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::<T>::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::<T>::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::<T>::put(1);
SlashPerEra::set(7);
CurrentEra::<T>::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::<T>::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::<T>::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::<T>::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::<T>::put(2);
SlashPerEra::set(7);
CurrentEra::<T>::put(BondingDuration::get());
// create an exposed nominator in era 1
@@ -729,7 +894,7 @@ mod on_idle {
pallet_staking::ErasStakers::<T>::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::<T>::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::<T>::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::<T>::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::<T>::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::<T>::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::<T>::chill {});
#[test]
fn does_nothing_if_not_queued() {
ExtBuilder::default().build_and_execute(|| {
assert!(PreventStakingOpsIfUnbonding::<T>::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::<T>::new()
.pre_dispatch(&1, &STAKING_CALL, &Default::default(), Default::default())
.is_err());
// controller can't.
assert!(PreventStakingOpsIfUnbonding::<T>::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::<T>::put(1);
CurrentEra::<T>::put(BondingDuration::get());
next_block(true);
assert_eq!(
Head::<T>::get(),
Some(UnstakeRequest { stash: 1, checked: bounded_vec![3] })
);
// then
// stash can't
assert!(PreventStakingOpsIfUnbonding::<T>::new()
.pre_dispatch(&2, &STAKING_CALL, &Default::default(), Default::default())
.is_err());
// controller can't
assert!(PreventStakingOpsIfUnbonding::<T>::new()
.pre_dispatch(&1, &STAKING_CALL, &Default::default(), Default::default())
.is_err());
})
}
}
+8 -75
View File
@@ -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<T> = <<T as pallet_staking::Config>::Currency as Currency<
#[derive(
Encode, Decode, EqNoBound, PartialEqNoBound, Clone, TypeInfo, RuntimeDebugNoBound, MaxEncodedLen,
)]
pub struct UnstakeRequest<AccountId: Eq + PartialEq + Debug, MaxChecked: Get<u32>> {
pub struct UnstakeRequest<
AccountId: Eq + PartialEq + Debug,
MaxChecked: Get<u32>,
Balance: PartialEq + Debug,
> {
/// Their stash account.
pub(crate) stash: AccountId,
/// The list of eras for which they have been checked.
pub(crate) checked: BoundedVec<EraIndex, MaxChecked>,
}
#[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo, RuntimeDebugNoBound)]
#[scale_info(skip_type_params(T))]
pub struct PreventStakingOpsIfUnbonding<T: Config + Send + Sync>(sp_std::marker::PhantomData<T>);
impl<T: Config + Send + Sync> PreventStakingOpsIfUnbonding<T> {
pub fn new() -> Self {
Self(Default::default())
}
}
impl<T: Config + Send + Sync> sp_runtime::traits::SignedExtension
for PreventStakingOpsIfUnbonding<T>
where
<T as frame_system::Config>::RuntimeCall: IsSubType<pallet_staking::Call<T>>,
{
type AccountId = T::AccountId;
type Call = <T as frame_system::Config>::RuntimeCall;
type AdditionalSigned = ();
type Pre = ();
const IDENTIFIER: &'static str = "PreventStakingOpsIfUnbonding";
fn additional_signed(&self) -> Result<Self::AdditionalSigned, TransactionValidityError> {
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<Self::Call>,
_len: usize,
) -> Result<Self::Pre, TransactionValidityError> {
// we don't check this in the tx-pool as it requires a storage read.
if <Self::Call as IsSubType<pallet_staking::Call<T>>>::is_sub_type(call).is_some() {
let check_stash = |stash: &T::AccountId| {
if Queue::<T>::contains_key(&stash) ||
Head::<T>::get().map_or(false, |u| &u.stash == stash)
{
Err(TransactionValidityError::Invalid(InvalidTransaction::Call))
} else {
Ok(())
}
};
match (
// mapped from controller.
pallet_staking::Ledger::<T>::get(&stash_or_controller),
// mapped from stash.
pallet_staking::Bonded::<T>::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,
}