Allow privileged virtual bond in Staking pallet (#3889)

This is the first PR in preparation for
https://github.com/paritytech/polkadot-sdk/issues/454.

## Follow ups:
- https://github.com/paritytech/polkadot-sdk/pull/3904.
- https://github.com/paritytech/polkadot-sdk/pull/3905.

Overall changes are documented here (lot more visual 😍):
https://hackmd.io/@ak0n/454-np-governance

[Maybe followup](https://github.com/paritytech/polkadot-sdk/issues/4217)
with migration of storage item `VirtualStakers` as a bool or enum in
`Ledger`.

## Context
We want to achieve a way for a user (`Delegator`) to delegate their
funds to another account (`Agent`). Delegate implies the funds are
locked in delegator account itself. Agent can act on behalf of delegator
to stake directly on Staking pallet.

The delegation feature is added to Staking via another pallet
`delegated-staking` worked on
[here](https://github.com/paritytech/polkadot-sdk/pull/3904).

## Introduces:
### StakingUnchecked Trait
As the name implies, this trait allows unchecked (non-locked) mutation
of staking ledger. These apis are only meant to be used by other pallets
in the runtime and should not be exposed directly to user code path.
Also related: https://github.com/paritytech/polkadot-sdk/issues/3888.

### Virtual Bond
Allows other pallets to stake via staking pallet while managing the
locks on these accounts themselves. Introduces another storage
`VirtualStakers` that whitelist these accounts.

We also restrict virtual stakers to set reward account as themselves.
Since the account has no locks, we cannot support compounding of
rewards. Conservatively, we require them to set a separate account
different from the staker. Since these are code managed, it should be
easy for another pallet to redistribute reward and rebond them.

### Slashes
Since there is no actual lock maintained by staking-pallet for virtual
stakers, this pallet does not apply any slashes. It is then important
for pallets managing virtual stakers to listen to slashing events and
apply necessary slashes.
This commit is contained in:
Ankan
2024-04-20 02:05:34 +02:00
committed by GitHub
parent 4eabe5e0dd
commit e504c41a5a
9 changed files with 514 additions and 73 deletions
+29 -14
View File
@@ -33,13 +33,14 @@
use frame_support::{
defensive, ensure,
traits::{Defensive, LockableCurrency, WithdrawReasons},
traits::{Defensive, LockableCurrency},
};
use sp_staking::StakingAccount;
use sp_std::prelude::*;
use crate::{
BalanceOf, Bonded, Config, Error, Ledger, Payee, RewardDestination, StakingLedger, STAKING_ID,
BalanceOf, Bonded, Config, Error, Ledger, Pallet, Payee, RewardDestination, StakingLedger,
VirtualStakers, STAKING_ID,
};
#[cfg(any(feature = "runtime-benchmarks", test))]
@@ -187,7 +188,17 @@ impl<T: Config> StakingLedger<T> {
return Err(Error::<T>::NotStash)
}
T::Currency::set_lock(STAKING_ID, &self.stash, self.total, WithdrawReasons::all());
// We skip locking virtual stakers.
if !Pallet::<T>::is_virtual_staker(&self.stash) {
// for direct stakers, update lock on stash based on ledger.
T::Currency::set_lock(
STAKING_ID,
&self.stash,
self.total,
frame_support::traits::WithdrawReasons::all(),
);
}
Ledger::<T>::insert(
&self.controller().ok_or_else(|| {
defensive!("update called on a ledger that is not bonded.");
@@ -204,22 +215,22 @@ impl<T: Config> StakingLedger<T> {
/// It sets the reward preferences for the bonded stash.
pub(crate) fn bond(self, payee: RewardDestination<T::AccountId>) -> Result<(), Error<T>> {
if <Bonded<T>>::contains_key(&self.stash) {
Err(Error::<T>::AlreadyBonded)
} else {
<Payee<T>>::insert(&self.stash, payee);
<Bonded<T>>::insert(&self.stash, &self.stash);
self.update()
return Err(Error::<T>::AlreadyBonded)
}
<Payee<T>>::insert(&self.stash, payee);
<Bonded<T>>::insert(&self.stash, &self.stash);
self.update()
}
/// Sets the ledger Payee.
pub(crate) fn set_payee(self, payee: RewardDestination<T::AccountId>) -> Result<(), Error<T>> {
if !<Bonded<T>>::contains_key(&self.stash) {
Err(Error::<T>::NotStash)
} else {
<Payee<T>>::insert(&self.stash, payee);
Ok(())
return Err(Error::<T>::NotStash)
}
<Payee<T>>::insert(&self.stash, payee);
Ok(())
}
/// Sets the ledger controller to its stash.
@@ -252,12 +263,16 @@ impl<T: Config> StakingLedger<T> {
let controller = <Bonded<T>>::get(stash).ok_or(Error::<T>::NotStash)?;
<Ledger<T>>::get(&controller).ok_or(Error::<T>::NotController).map(|ledger| {
T::Currency::remove_lock(STAKING_ID, &ledger.stash);
Ledger::<T>::remove(controller);
<Bonded<T>>::remove(&stash);
<Payee<T>>::remove(&stash);
// kill virtual staker if it exists.
if <VirtualStakers<T>>::take(&stash).is_none() {
// if not virtual staker, clear locks.
T::Currency::remove_lock(STAKING_ID, &ledger.stash);
}
Ok(())
})?
}
+21 -2
View File
@@ -249,17 +249,21 @@ parameter_types! {
pub static LedgerSlashPerEra:
(BalanceOf<Test>, BTreeMap<EraIndex, BalanceOf<Test>>) =
(Zero::zero(), BTreeMap::new());
pub static SlashObserver: BTreeMap<AccountId, BalanceOf<Test>> = BTreeMap::new();
}
pub struct EventListenerMock;
impl OnStakingUpdate<AccountId, Balance> for EventListenerMock {
fn on_slash(
_pool_account: &AccountId,
pool_account: &AccountId,
slashed_bonded: Balance,
slashed_chunks: &BTreeMap<EraIndex, Balance>,
_total_slashed: Balance,
total_slashed: Balance,
) {
LedgerSlashPerEra::set((slashed_bonded, slashed_chunks.clone()));
SlashObserver::mutate(|map| {
map.insert(*pool_account, map.get(pool_account).unwrap_or(&0) + total_slashed)
});
}
}
@@ -598,6 +602,21 @@ pub(crate) fn bond_nominator(who: AccountId, val: Balance, target: Vec<AccountId
assert_ok!(Staking::nominate(RuntimeOrigin::signed(who), target));
}
pub(crate) fn bond_virtual_nominator(
who: AccountId,
payee: AccountId,
val: Balance,
target: Vec<AccountId>,
) {
// In a real scenario, `who` is a keyless account managed by another pallet which provides for
// it.
System::inc_providers(&who);
// Bond who virtually.
assert_ok!(<Staking as sp_staking::StakingUnchecked>::virtual_bond(&who, val, &payee));
assert_ok!(Staking::nominate(RuntimeOrigin::signed(who), target));
}
/// Progress to the given block, triggering session and era changes as we progress.
///
/// This will finalize the previous block, initialize up to the given block, essentially simulating
+147 -8
View File
@@ -28,15 +28,18 @@ use frame_support::{
pallet_prelude::*,
traits::{
Currency, Defensive, DefensiveSaturating, EstimateNextNewSession, Get, Imbalance,
InspectLockableCurrency, Len, OnUnbalanced, TryCollect, UnixTime,
InspectLockableCurrency, Len, LockableCurrency, OnUnbalanced, TryCollect, UnixTime,
},
weights::Weight,
};
use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin};
use pallet_session::historical;
use sp_runtime::{
traits::{Bounded, Convert, One, SaturatedConversion, Saturating, StaticLookup, Zero},
Perbill, Percent,
traits::{
Bounded, CheckedAdd, CheckedSub, Convert, One, SaturatedConversion, Saturating,
StaticLookup, Zero,
},
ArithmeticError, Perbill, Percent,
};
use sp_staking::{
currency_to_vote::CurrencyToVote,
@@ -149,6 +152,39 @@ impl<T: Config> Pallet<T> {
Self::slashable_balance_of_vote_weight(who, issuance)
}
pub(super) fn do_bond_extra(stash: &T::AccountId, additional: BalanceOf<T>) -> DispatchResult {
let mut ledger = Self::ledger(StakingAccount::Stash(stash.clone()))?;
// for virtual stakers, we don't need to check the balance. Since they are only accessed
// via low level apis, we can assume that the caller has done the due diligence.
let extra = if Self::is_virtual_staker(stash) {
additional
} else {
// additional amount or actual balance of stash whichever is lower.
additional.min(
T::Currency::free_balance(stash)
.checked_sub(&ledger.total)
.ok_or(ArithmeticError::Overflow)?,
)
};
ledger.total = ledger.total.checked_add(&extra).ok_or(ArithmeticError::Overflow)?;
ledger.active = ledger.active.checked_add(&extra).ok_or(ArithmeticError::Overflow)?;
// last check: the new active amount of ledger must be more than ED.
ensure!(ledger.active >= T::Currency::minimum_balance(), Error::<T>::InsufficientBond);
// NOTE: ledger must be updated prior to calling `Self::weight_of`.
ledger.update()?;
// update this staker in the sorted list, if they exist in it.
if T::VoterList::contains(stash) {
let _ = T::VoterList::on_update(&stash, Self::weight_of(stash)).defensive();
}
Self::deposit_event(Event::<T>::Bonded { stash: stash.clone(), amount: extra });
Ok(())
}
pub(super) fn do_withdraw_unbonded(
controller: &T::AccountId,
num_slashing_spans: u32,
@@ -1132,6 +1168,11 @@ impl<T: Config> Pallet<T> {
) -> Exposure<T::AccountId, BalanceOf<T>> {
EraInfo::<T>::get_full_exposure(era, account)
}
/// Whether `who` is a virtual staker whose funds are managed by another pallet.
pub(crate) fn is_virtual_staker(who: &T::AccountId) -> bool {
VirtualStakers::<T>::contains_key(who)
}
}
impl<T: Config> Pallet<T> {
@@ -1748,6 +1789,23 @@ impl<T: Config> StakingInterface for Pallet<T> {
.map(|_| ())
}
fn update_payee(stash: &Self::AccountId, reward_acc: &Self::AccountId) -> DispatchResult {
// Since virtual stakers are not allowed to compound their rewards as this pallet does not
// manage their locks, we do not allow reward account to be set same as stash. For
// external pallets that manage the virtual bond, they can claim rewards and re-bond them.
ensure!(
!Self::is_virtual_staker(stash) || stash != reward_acc,
Error::<T>::RewardDestinationRestricted
);
// since controller is deprecated and this function is never used for old ledgers with
// distinct controllers, we can safely assume that stash is the controller.
Self::set_payee(
RawOrigin::Signed(stash.clone()).into(),
RewardDestination::Account(reward_acc.clone()),
)
}
fn chill(who: &Self::AccountId) -> DispatchResult {
// defensive-only: any account bonded via this interface has the stash set as the
// controller, but we have to be sure. Same comment anywhere else that we read this.
@@ -1832,6 +1890,10 @@ impl<T: Config> StakingInterface for Pallet<T> {
}
}
fn slash_reward_fraction() -> Perbill {
SlashRewardFraction::<T>::get()
}
sp_staking::runtime_benchmarks_enabled! {
fn nominations(who: &Self::AccountId) -> Option<Vec<T::AccountId>> {
Nominators::<T>::get(who).map(|n| n.targets.into_inner())
@@ -1860,6 +1922,55 @@ impl<T: Config> StakingInterface for Pallet<T> {
}
}
impl<T: Config> sp_staking::StakingUnchecked for Pallet<T> {
fn migrate_to_virtual_staker(who: &Self::AccountId) {
T::Currency::remove_lock(crate::STAKING_ID, who);
VirtualStakers::<T>::insert(who, ());
}
/// Virtually bonds `keyless_who` to `payee` with `value`.
///
/// The payee must not be the same as the `keyless_who`.
fn virtual_bond(
keyless_who: &Self::AccountId,
value: Self::Balance,
payee: &Self::AccountId,
) -> DispatchResult {
if StakingLedger::<T>::is_bonded(StakingAccount::Stash(keyless_who.clone())) {
return Err(Error::<T>::AlreadyBonded.into())
}
// check if payee not same as who.
ensure!(keyless_who != payee, Error::<T>::RewardDestinationRestricted);
// mark this pallet as consumer of `who`.
frame_system::Pallet::<T>::inc_consumers(&keyless_who).map_err(|_| Error::<T>::BadState)?;
// mark who as a virtual staker.
VirtualStakers::<T>::insert(keyless_who, ());
Self::deposit_event(Event::<T>::Bonded { stash: keyless_who.clone(), amount: value });
let ledger = StakingLedger::<T>::new(keyless_who.clone(), value);
ledger.bond(RewardDestination::Account(payee.clone()))?;
Ok(())
}
#[cfg(feature = "runtime-benchmarks")]
fn migrate_to_direct_staker(who: &Self::AccountId) {
assert!(VirtualStakers::<T>::contains_key(who));
let ledger = StakingLedger::<T>::get(Stash(who.clone())).unwrap();
T::Currency::set_lock(
crate::STAKING_ID,
who,
ledger.total,
frame_support::traits::WithdrawReasons::all(),
);
VirtualStakers::<T>::remove(who);
}
}
#[cfg(any(test, feature = "try-runtime"))]
impl<T: Config> Pallet<T> {
pub(crate) fn do_try_state(_: BlockNumberFor<T>) -> Result<(), TryRuntimeError> {
@@ -1980,16 +2091,44 @@ impl<T: Config> Pallet<T> {
/// Invariants:
/// * Stake consistency: ledger.total == ledger.active + sum(ledger.unlocking).
/// * The ledger's controller and stash matches the associated `Bonded` tuple.
/// * Staking locked funds for every bonded stash should be the same as its ledger's total.
/// * Staking locked funds for every bonded stash (non virtual stakers) should be the same as
/// its ledger's total.
/// * For virtual stakers, locked funds should be zero and payee should be non-stash account.
/// * Staking ledger and bond are not corrupted.
fn check_ledgers() -> Result<(), TryRuntimeError> {
Bonded::<T>::iter()
.map(|(stash, ctrl)| {
// ensure locks consistency.
ensure!(
Self::inspect_bond_state(&stash) == Ok(LedgerIntegrityState::Ok),
"bond, ledger and/or staking lock inconsistent for a bonded stash."
);
if VirtualStakers::<T>::contains_key(stash.clone()) {
ensure!(
T::Currency::balance_locked(crate::STAKING_ID, &stash) == Zero::zero(),
"virtual stakers should not have any locked balance"
);
ensure!(
<Bonded<T>>::get(stash.clone()).unwrap() == stash.clone(),
"stash and controller should be same"
);
ensure!(
Ledger::<T>::get(stash.clone()).unwrap().stash == stash,
"ledger corrupted for virtual staker"
);
let reward_destination = <Payee<T>>::get(stash.clone()).unwrap();
if let RewardDestination::Account(payee) = reward_destination {
ensure!(
payee != stash.clone(),
"reward destination should not be same as stash for virtual staker"
);
} else {
return Err(DispatchError::Other(
"reward destination must be of account variant for virtual staker",
));
}
} else {
ensure!(
Self::inspect_bond_state(&stash) == Ok(LedgerIntegrityState::Ok),
"bond, ledger and/or staking lock inconsistent for a bonded stash."
);
}
// ensure ledger consistency.
Self::ensure_ledger_consistent(ctrl)
+15 -24
View File
@@ -32,7 +32,7 @@ use frame_support::{
};
use frame_system::{ensure_root, ensure_signed, pallet_prelude::*};
use sp_runtime::{
traits::{CheckedSub, SaturatedConversion, StaticLookup, Zero},
traits::{SaturatedConversion, StaticLookup, Zero},
ArithmeticError, Perbill, Percent,
};
@@ -379,6 +379,15 @@ pub mod pallet {
pub type Nominators<T: Config> =
CountedStorageMap<_, Twox64Concat, T::AccountId, Nominations<T>>;
/// Stakers whose funds are managed by other pallets.
///
/// This pallet does not apply any locks on them, therefore they are only virtually bonded. They
/// are expected to be keyless accounts and hence should not be allowed to mutate their ledger
/// directly via this pallet. Instead, these accounts are managed by other pallets and accessed
/// via low level apis. We keep track of them to do minimal integrity checks.
#[pallet::storage]
pub type VirtualStakers<T: Config> = CountedStorageMap<_, Twox64Concat, T::AccountId, ()>;
/// The maximum nominator count before we stop allowing new validators to join.
///
/// When this value is not set, no limits are enforced.
@@ -858,6 +867,10 @@ pub mod pallet {
ControllerDeprecated,
/// Cannot reset a ledger.
CannotRestoreLedger,
/// Provided reward destination is not allowed.
RewardDestinationRestricted,
/// Not enough funds available to withdraw.
NotEnoughFunds,
}
#[pallet::hooks]
@@ -985,29 +998,7 @@ pub mod pallet {
#[pallet::compact] max_additional: BalanceOf<T>,
) -> DispatchResult {
let stash = ensure_signed(origin)?;
let mut ledger = Self::ledger(StakingAccount::Stash(stash.clone()))?;
let stash_balance = T::Currency::free_balance(&stash);
if let Some(extra) = stash_balance.checked_sub(&ledger.total) {
let extra = extra.min(max_additional);
ledger.total += extra;
ledger.active += extra;
// Last check: the new active amount of ledger must be more than ED.
ensure!(
ledger.active >= T::Currency::minimum_balance(),
Error::<T>::InsufficientBond
);
// NOTE: ledger must be updated prior to calling `Self::weight_of`.
ledger.update()?;
// update this staker in the sorted list, if they exist in it.
if T::VoterList::contains(&stash) {
let _ = T::VoterList::on_update(&stash, Self::weight_of(&stash)).defensive();
}
Self::deposit_event(Event::<T>::Bonded { stash, amount: extra });
}
Ok(())
Self::do_bond_extra(&stash, max_additional)
}
/// Schedule a portion of the stash to be unlocked ready for transfer out after the bond
+13 -11
View File
@@ -609,8 +609,13 @@ pub fn do_slash<T: Config>(
};
let value = ledger.slash(value, T::Currency::minimum_balance(), slash_era);
if value.is_zero() {
// nothing to do
return
}
if !value.is_zero() {
// Skip slashing for virtual stakers. The pallets managing them should handle the slashing.
if !Pallet::<T>::is_virtual_staker(stash) {
let (imbalance, missing) = T::Currency::slash(stash, value);
slashed_imbalance.subsume(imbalance);
@@ -618,17 +623,14 @@ pub fn do_slash<T: Config>(
// deduct overslash from the reward payout
*reward_payout = reward_payout.saturating_sub(missing);
}
let _ = ledger
.update()
.defensive_proof("ledger fetched from storage so it exists in storage; qed.");
// trigger the event
<Pallet<T>>::deposit_event(super::Event::<T>::Slashed {
staker: stash.clone(),
amount: value,
});
}
let _ = ledger
.update()
.defensive_proof("ledger fetched from storage so it exists in storage; qed.");
// trigger the event
<Pallet<T>>::deposit_event(super::Event::<T>::Slashed { staker: stash.clone(), amount: value });
}
/// Apply a previously-unapplied slash.
+231 -12
View File
@@ -27,7 +27,7 @@ use frame_support::{
assert_noop, assert_ok, assert_storage_noop,
dispatch::{extract_actual_weight, GetDispatchInfo, WithPostDispatchInfo},
pallet_prelude::*,
traits::{Currency, Get, ReservableCurrency},
traits::{Currency, Get, InspectLockableCurrency, ReservableCurrency},
};
use mock::*;
@@ -623,12 +623,8 @@ fn nominating_and_rewards_should_work() {
));
assert_ok!(Staking::nominate(RuntimeOrigin::signed(1), vec![11, 21, 31]));
assert_ok!(Staking::bond(
RuntimeOrigin::signed(3),
1000,
RewardDestination::Account(3)
));
assert_ok!(Staking::nominate(RuntimeOrigin::signed(3), vec![11, 21, 41]));
// the second nominator is virtual.
bond_virtual_nominator(3, 333, 1000, vec![11, 21, 41]);
// the total reward for era 0
let total_payout_0 = current_total_payout_for_duration(reward_time_per_era());
@@ -694,10 +690,12 @@ fn nominating_and_rewards_should_work() {
);
// Nominator 3: has [400/1800 ~ 2/9 from 10] + [600/2200 ~ 3/11 from 21]'s reward. ==>
// 2/9 + 3/11
assert_eq!(Balances::total_balance(&3), initial_balance);
// 333 is the reward destination for 3.
assert_eq_error_rate!(
Balances::total_balance(&3),
initial_balance + (2 * payout_for_11 / 9 + 3 * payout_for_21 / 11),
2,
Balances::total_balance(&333),
2 * payout_for_11 / 9 + 3 * payout_for_21 / 11,
2
);
// Validator 11: got 800 / 1800 external stake => 8/18 =? 4/9 => Validator's share = 5/9
@@ -1893,7 +1891,7 @@ fn reap_stash_works() {
.balance_factor(10)
.build_and_execute(|| {
// given
assert_eq!(Balances::free_balance(11), 10 * 1000);
assert_eq!(Balances::balance_locked(STAKING_ID, &11), 10 * 1000);
assert_eq!(Staking::bonded(&11), Some(11));
assert!(<Ledger<Test>>::contains_key(&11));
@@ -1919,6 +1917,8 @@ fn reap_stash_works() {
assert!(!<Bonded<Test>>::contains_key(&11));
assert!(!<Validators<Test>>::contains_key(&11));
assert!(!<Payee<Test>>::contains_key(&11));
// lock is removed.
assert_eq!(Balances::balance_locked(STAKING_ID, &11), 0);
});
}
@@ -6849,6 +6849,226 @@ mod staking_interface {
}
}
mod staking_unchecked {
use sp_staking::{Stake, StakingInterface, StakingUnchecked};
use super::*;
#[test]
fn virtual_bond_does_not_lock() {
ExtBuilder::default().build_and_execute(|| {
mock::start_active_era(1);
assert_eq!(Balances::free_balance(10), 1);
// 10 can bond more than its balance amount since we do not require lock for virtual
// bonding.
assert_ok!(<Staking as StakingUnchecked>::virtual_bond(&10, 100, &15));
// nothing is locked on 10.
assert_eq!(Balances::balance_locked(STAKING_ID, &10), 0);
// adding more balance does not lock anything as well.
assert_ok!(<Staking as StakingInterface>::bond_extra(&10, 1000));
// but ledger is updated correctly.
assert_eq!(
<Staking as StakingInterface>::stake(&10),
Ok(Stake { total: 1100, active: 1100 })
);
// lets try unbonding some amount.
assert_ok!(<Staking as StakingInterface>::unbond(&10, 200));
assert_eq!(
Staking::ledger(10.into()).unwrap(),
StakingLedgerInspect {
stash: 10,
total: 1100,
active: 1100 - 200,
unlocking: bounded_vec![UnlockChunk { value: 200, era: 1 + 3 }],
legacy_claimed_rewards: bounded_vec![],
}
);
assert_eq!(
<Staking as StakingInterface>::stake(&10),
Ok(Stake { total: 1100, active: 900 })
);
// still no locks.
assert_eq!(Balances::balance_locked(STAKING_ID, &10), 0);
mock::start_active_era(2);
// cannot withdraw without waiting for unbonding period.
assert_ok!(<Staking as StakingInterface>::withdraw_unbonded(10, 0));
assert_eq!(
<Staking as StakingInterface>::stake(&10),
Ok(Stake { total: 1100, active: 900 })
);
// in era 4, 10 can withdraw unlocking amount.
mock::start_active_era(4);
assert_ok!(<Staking as StakingInterface>::withdraw_unbonded(10, 0));
assert_eq!(
<Staking as StakingInterface>::stake(&10),
Ok(Stake { total: 900, active: 900 })
);
// unbond all.
assert_ok!(<Staking as StakingInterface>::unbond(&10, 900));
assert_eq!(
<Staking as StakingInterface>::stake(&10),
Ok(Stake { total: 900, active: 0 })
);
mock::start_active_era(7);
assert_ok!(<Staking as StakingInterface>::withdraw_unbonded(10, 0));
// ensure withdrawing all amount cleans up storage.
assert_eq!(Staking::ledger(10.into()), Err(Error::<Test>::NotStash));
assert_eq!(VirtualStakers::<Test>::contains_key(10), false);
})
}
#[test]
fn virtual_staker_cannot_pay_reward_to_self_account() {
ExtBuilder::default().build_and_execute(|| {
// cannot set payee to self
assert_noop!(
<Staking as StakingUnchecked>::virtual_bond(&10, 100, &10),
Error::<Test>::RewardDestinationRestricted
);
// to another account works
assert_ok!(<Staking as StakingUnchecked>::virtual_bond(&10, 100, &11));
// cannot set via set_payee as well.
assert_noop!(
<Staking as StakingInterface>::update_payee(&10, &10),
Error::<Test>::RewardDestinationRestricted
);
});
}
#[test]
fn virtual_staker_cannot_bond_again() {
ExtBuilder::default().build_and_execute(|| {
// 200 virtual bonds
bond_virtual_nominator(200, 201, 500, vec![11, 21]);
// Tries bonding again
assert_noop!(
<Staking as StakingUnchecked>::virtual_bond(&200, 200, &201),
Error::<Test>::AlreadyBonded
);
// And again with a different reward destination.
assert_noop!(
<Staking as StakingUnchecked>::virtual_bond(&200, 200, &202),
Error::<Test>::AlreadyBonded
);
// Direct bond is not allowed as well.
assert_noop!(
<Staking as StakingInterface>::bond(&200, 200, &202),
Error::<Test>::AlreadyBonded
);
});
}
#[test]
fn normal_staker_cannot_virtual_bond() {
ExtBuilder::default().build_and_execute(|| {
// 101 is a nominator trying to virtual bond
assert_noop!(
<Staking as StakingUnchecked>::virtual_bond(&101, 200, &102),
Error::<Test>::AlreadyBonded
);
// validator 21 tries to virtual bond
assert_noop!(
<Staking as StakingUnchecked>::virtual_bond(&21, 200, &22),
Error::<Test>::AlreadyBonded
);
});
}
#[test]
fn migrate_virtual_staker() {
ExtBuilder::default().build_and_execute(|| {
// give some balance to 200
Balances::make_free_balance_be(&200, 2000);
// stake
assert_ok!(Staking::bond(RuntimeOrigin::signed(200), 1000, RewardDestination::Staked));
assert_eq!(Balances::balance_locked(crate::STAKING_ID, &200), 1000);
// migrate them to virtual staker
<Staking as StakingUnchecked>::migrate_to_virtual_staker(&200);
// payee needs to be updated to a non-stash account.
assert_ok!(<Staking as StakingInterface>::update_payee(&200, &201));
// ensure the balance is not locked anymore
assert_eq!(Balances::balance_locked(crate::STAKING_ID, &200), 0);
// and they are marked as virtual stakers
assert_eq!(Pallet::<Test>::is_virtual_staker(&200), true);
});
}
#[test]
fn virtual_nominators_are_lazily_slashed() {
ExtBuilder::default().build_and_execute(|| {
mock::start_active_era(1);
let slash_percent = Perbill::from_percent(5);
let initial_exposure = Staking::eras_stakers(active_era(), &11);
// 101 is a nominator for 11
assert_eq!(initial_exposure.others.first().unwrap().who, 101);
// make 101 a virtual nominator
<Staking as StakingUnchecked>::migrate_to_virtual_staker(&101);
// set payee different to self.
assert_ok!(<Staking as StakingInterface>::update_payee(&101, &102));
// cache values
let nominator_stake = Staking::ledger(101.into()).unwrap().active;
let nominator_balance = balances(&101).0;
let validator_stake = Staking::ledger(11.into()).unwrap().active;
let validator_balance = balances(&11).0;
let exposed_stake = initial_exposure.total;
let exposed_validator = initial_exposure.own;
let exposed_nominator = initial_exposure.others.first().unwrap().value;
// 11 goes offline
on_offence_now(
&[OffenceDetails { offender: (11, initial_exposure.clone()), reporters: vec![] }],
&[slash_percent],
);
let slash_amount = slash_percent * exposed_stake;
let validator_share =
Perbill::from_rational(exposed_validator, exposed_stake) * slash_amount;
let nominator_share =
Perbill::from_rational(exposed_nominator, exposed_stake) * slash_amount;
// both slash amounts need to be positive for the test to make sense.
assert!(validator_share > 0);
assert!(nominator_share > 0);
// both stakes must have been decreased pro-rata.
assert_eq!(
Staking::ledger(101.into()).unwrap().active,
nominator_stake - nominator_share
);
assert_eq!(
Staking::ledger(11.into()).unwrap().active,
validator_stake - validator_share
);
// validator balance is slashed as usual
assert_eq!(balances(&11).0, validator_balance - validator_share);
// Because slashing happened.
assert!(is_disabled(11));
// but virtual nominator's balance is not slashed.
assert_eq!(Balances::free_balance(&101), nominator_balance);
// but slash is broadcasted to slash observers.
assert_eq!(SlashObserver::get().get(&101).unwrap(), &nominator_share);
})
}
}
mod ledger {
use super::*;
@@ -7327,7 +7547,6 @@ mod ledger {
mod ledger_recovery {
use super::*;
use frame_support::traits::InspectLockableCurrency;
#[test]
fn inspect_recovery_ledger_simple_works() {