mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-07-21 14:25:41 +00:00
staking: Proportional ledger slashing (#10982)
* staking: Proportional ledger slashing * Some comment cleanup * Update frame/staking/src/pallet/mod.rs Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> * Fix benchmarks * FMT * Try fill in all staking configs * round of feedback and imp from kian * demonstrate per_thing usage * Update some tests * FMT * Test that era offset works correctly * Update mocks * Remove unnescary docs * Remove unlock_era * Update frame/staking/src/lib.rs * Adjust tests to account for only remove when < ED * Remove stale TODOs * Remove dupe test Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> Co-authored-by: kianenigma <kian@parity.io>
This commit is contained in:
@@ -537,6 +537,7 @@ impl pallet_staking::BenchmarkingConfig for StakingBenchmarkingConfig {
|
|||||||
impl pallet_staking::Config for Runtime {
|
impl pallet_staking::Config for Runtime {
|
||||||
type MaxNominations = MaxNominations;
|
type MaxNominations = MaxNominations;
|
||||||
type Currency = Balances;
|
type Currency = Balances;
|
||||||
|
type CurrencyBalance = Balance;
|
||||||
type UnixTime = Timestamp;
|
type UnixTime = Timestamp;
|
||||||
type CurrencyToVote = U128CurrencyToVote;
|
type CurrencyToVote = U128CurrencyToVote;
|
||||||
type RewardRemainder = Treasury;
|
type RewardRemainder = Treasury;
|
||||||
@@ -560,6 +561,7 @@ impl pallet_staking::Config for Runtime {
|
|||||||
type GenesisElectionProvider = onchain::UnboundedExecution<OnChainSeqPhragmen>;
|
type GenesisElectionProvider = onchain::UnboundedExecution<OnChainSeqPhragmen>;
|
||||||
type VoterList = BagsList;
|
type VoterList = BagsList;
|
||||||
type MaxUnlockingChunks = ConstU32<32>;
|
type MaxUnlockingChunks = ConstU32<32>;
|
||||||
|
type OnStakerSlash = ();
|
||||||
type WeightInfo = pallet_staking::weights::SubstrateWeight<Runtime>;
|
type WeightInfo = pallet_staking::weights::SubstrateWeight<Runtime>;
|
||||||
type BenchmarkingConfig = StakingBenchmarkingConfig;
|
type BenchmarkingConfig = StakingBenchmarkingConfig;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ impl pallet_staking::Config for Test {
|
|||||||
type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
|
type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
|
||||||
type Event = Event;
|
type Event = Event;
|
||||||
type Currency = Balances;
|
type Currency = Balances;
|
||||||
|
type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
|
||||||
type Slash = ();
|
type Slash = ();
|
||||||
type Reward = ();
|
type Reward = ();
|
||||||
type SessionsPerEra = SessionsPerEra;
|
type SessionsPerEra = SessionsPerEra;
|
||||||
@@ -202,6 +203,7 @@ impl pallet_staking::Config for Test {
|
|||||||
type GenesisElectionProvider = Self::ElectionProvider;
|
type GenesisElectionProvider = Self::ElectionProvider;
|
||||||
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
|
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
|
||||||
type MaxUnlockingChunks = ConstU32<32>;
|
type MaxUnlockingChunks = ConstU32<32>;
|
||||||
|
type OnStakerSlash = ();
|
||||||
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
|
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
|
||||||
type WeightInfo = ();
|
type WeightInfo = ();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ impl pallet_staking::Config for Test {
|
|||||||
type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
|
type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
|
||||||
type Event = Event;
|
type Event = Event;
|
||||||
type Currency = Balances;
|
type Currency = Balances;
|
||||||
|
type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
|
||||||
type Slash = ();
|
type Slash = ();
|
||||||
type Reward = ();
|
type Reward = ();
|
||||||
type SessionsPerEra = SessionsPerEra;
|
type SessionsPerEra = SessionsPerEra;
|
||||||
@@ -210,6 +211,7 @@ impl pallet_staking::Config for Test {
|
|||||||
type GenesisElectionProvider = Self::ElectionProvider;
|
type GenesisElectionProvider = Self::ElectionProvider;
|
||||||
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
|
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
|
||||||
type MaxUnlockingChunks = ConstU32<32>;
|
type MaxUnlockingChunks = ConstU32<32>;
|
||||||
|
type OnStakerSlash = ();
|
||||||
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
|
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
|
||||||
type WeightInfo = ();
|
type WeightInfo = ();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ impl onchain::Config for OnChainSeqPhragmen {
|
|||||||
impl pallet_staking::Config for Test {
|
impl pallet_staking::Config for Test {
|
||||||
type MaxNominations = ConstU32<16>;
|
type MaxNominations = ConstU32<16>;
|
||||||
type Currency = Balances;
|
type Currency = Balances;
|
||||||
|
type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
|
||||||
type UnixTime = pallet_timestamp::Pallet<Self>;
|
type UnixTime = pallet_timestamp::Pallet<Self>;
|
||||||
type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
|
type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
|
||||||
type RewardRemainder = ();
|
type RewardRemainder = ();
|
||||||
@@ -180,6 +181,7 @@ impl pallet_staking::Config for Test {
|
|||||||
type GenesisElectionProvider = Self::ElectionProvider;
|
type GenesisElectionProvider = Self::ElectionProvider;
|
||||||
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
|
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
|
||||||
type MaxUnlockingChunks = ConstU32<32>;
|
type MaxUnlockingChunks = ConstU32<32>;
|
||||||
|
type OnStakerSlash = ();
|
||||||
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
|
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
|
||||||
type WeightInfo = ();
|
type WeightInfo = ();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ impl onchain::Config for OnChainSeqPhragmen {
|
|||||||
impl pallet_staking::Config for Test {
|
impl pallet_staking::Config for Test {
|
||||||
type MaxNominations = ConstU32<16>;
|
type MaxNominations = ConstU32<16>;
|
||||||
type Currency = Balances;
|
type Currency = Balances;
|
||||||
|
type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
|
||||||
type UnixTime = pallet_timestamp::Pallet<Self>;
|
type UnixTime = pallet_timestamp::Pallet<Self>;
|
||||||
type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
|
type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
|
||||||
type RewardRemainder = ();
|
type RewardRemainder = ();
|
||||||
@@ -186,6 +187,7 @@ impl pallet_staking::Config for Test {
|
|||||||
type GenesisElectionProvider = Self::ElectionProvider;
|
type GenesisElectionProvider = Self::ElectionProvider;
|
||||||
type MaxUnlockingChunks = ConstU32<32>;
|
type MaxUnlockingChunks = ConstU32<32>;
|
||||||
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
|
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Self>;
|
||||||
|
type OnStakerSlash = ();
|
||||||
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
|
type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig;
|
||||||
type WeightInfo = ();
|
type WeightInfo = ();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -802,7 +802,8 @@ benchmarks! {
|
|||||||
&stash,
|
&stash,
|
||||||
slash_amount,
|
slash_amount,
|
||||||
&mut BalanceOf::<T>::zero(),
|
&mut BalanceOf::<T>::zero(),
|
||||||
&mut NegativeImbalanceOf::<T>::zero()
|
&mut NegativeImbalanceOf::<T>::zero(),
|
||||||
|
EraIndex::zero()
|
||||||
);
|
);
|
||||||
} verify {
|
} verify {
|
||||||
let balance_after = T::Currency::free_balance(&stash);
|
let balance_after = T::Currency::free_balance(&stash);
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ mod pallet;
|
|||||||
use codec::{Decode, Encode, HasCompact};
|
use codec::{Decode, Encode, HasCompact};
|
||||||
use frame_support::{
|
use frame_support::{
|
||||||
parameter_types,
|
parameter_types,
|
||||||
traits::{Currency, Get},
|
traits::{Currency, Defensive, Get},
|
||||||
weights::Weight,
|
weights::Weight,
|
||||||
BoundedVec, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
|
BoundedVec, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
|
||||||
};
|
};
|
||||||
@@ -310,7 +310,7 @@ use scale_info::TypeInfo;
|
|||||||
use sp_runtime::{
|
use sp_runtime::{
|
||||||
curve::PiecewiseLinear,
|
curve::PiecewiseLinear,
|
||||||
traits::{AtLeast32BitUnsigned, Convert, Saturating, Zero},
|
traits::{AtLeast32BitUnsigned, Convert, Saturating, Zero},
|
||||||
Perbill, RuntimeDebug,
|
Perbill, Perquintill, RuntimeDebug,
|
||||||
};
|
};
|
||||||
use sp_staking::{
|
use sp_staking::{
|
||||||
offence::{Offence, OffenceError, ReportOffence},
|
offence::{Offence, OffenceError, ReportOffence},
|
||||||
@@ -338,8 +338,7 @@ macro_rules! log {
|
|||||||
pub type RewardPoint = u32;
|
pub type RewardPoint = u32;
|
||||||
|
|
||||||
/// The balance type of this pallet.
|
/// The balance type of this pallet.
|
||||||
pub type BalanceOf<T> =
|
pub type BalanceOf<T> = <T as Config>::CurrencyBalance;
|
||||||
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
|
|
||||||
|
|
||||||
type PositiveImbalanceOf<T> = <<T as Config>::Currency as Currency<
|
type PositiveImbalanceOf<T> = <<T as Config>::Currency as Currency<
|
||||||
<T as frame_system::Config>::AccountId,
|
<T as frame_system::Config>::AccountId,
|
||||||
@@ -440,31 +439,30 @@ pub struct UnlockChunk<Balance: HasCompact> {
|
|||||||
|
|
||||||
/// The ledger of a (bonded) stash.
|
/// The ledger of a (bonded) stash.
|
||||||
#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
|
#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
|
||||||
pub struct StakingLedger<AccountId, Balance: HasCompact> {
|
#[scale_info(skip_type_params(T))]
|
||||||
|
pub struct StakingLedger<T: Config> {
|
||||||
/// The stash account whose balance is actually locked and at stake.
|
/// The stash account whose balance is actually locked and at stake.
|
||||||
pub stash: AccountId,
|
pub stash: T::AccountId,
|
||||||
/// The total amount of the stash's balance that we are currently accounting for.
|
/// The total amount of the stash's balance that we are currently accounting for.
|
||||||
/// It's just `active` plus all the `unlocking` balances.
|
/// It's just `active` plus all the `unlocking` balances.
|
||||||
#[codec(compact)]
|
#[codec(compact)]
|
||||||
pub total: Balance,
|
pub total: BalanceOf<T>,
|
||||||
/// The total amount of the stash's balance that will be at stake in any forthcoming
|
/// The total amount of the stash's balance that will be at stake in any forthcoming
|
||||||
/// rounds.
|
/// rounds.
|
||||||
#[codec(compact)]
|
#[codec(compact)]
|
||||||
pub active: Balance,
|
pub active: BalanceOf<T>,
|
||||||
/// Any balance that is becoming free, which may eventually be transferred out of the stash
|
/// Any balance that is becoming free, which may eventually be transferred out of the stash
|
||||||
/// (assuming it doesn't get slashed first). It is assumed that this will be treated as a first
|
/// (assuming it doesn't get slashed first). It is assumed that this will be treated as a first
|
||||||
/// in, first out queue where the new (higher value) eras get pushed on the back.
|
/// in, first out queue where the new (higher value) eras get pushed on the back.
|
||||||
pub unlocking: BoundedVec<UnlockChunk<Balance>, MaxUnlockingChunks>,
|
pub unlocking: BoundedVec<UnlockChunk<BalanceOf<T>>, MaxUnlockingChunks>,
|
||||||
/// List of eras for which the stakers behind a validator have claimed rewards. Only updated
|
/// List of eras for which the stakers behind a validator have claimed rewards. Only updated
|
||||||
/// for validators.
|
/// for validators.
|
||||||
pub claimed_rewards: Vec<EraIndex>,
|
pub claimed_rewards: Vec<EraIndex>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<AccountId, Balance: HasCompact + Copy + Saturating + AtLeast32BitUnsigned + Zero>
|
impl<T: Config> StakingLedger<T> {
|
||||||
StakingLedger<AccountId, Balance>
|
|
||||||
{
|
|
||||||
/// Initializes the default object using the given `validator`.
|
/// Initializes the default object using the given `validator`.
|
||||||
pub fn default_from(stash: AccountId) -> Self {
|
pub fn default_from(stash: T::AccountId) -> Self {
|
||||||
Self {
|
Self {
|
||||||
stash,
|
stash,
|
||||||
total: Zero::zero(),
|
total: Zero::zero(),
|
||||||
@@ -507,8 +505,8 @@ impl<AccountId, Balance: HasCompact + Copy + Saturating + AtLeast32BitUnsigned +
|
|||||||
/// Re-bond funds that were scheduled for unlocking.
|
/// Re-bond funds that were scheduled for unlocking.
|
||||||
///
|
///
|
||||||
/// Returns the updated ledger, and the amount actually rebonded.
|
/// Returns the updated ledger, and the amount actually rebonded.
|
||||||
fn rebond(mut self, value: Balance) -> (Self, Balance) {
|
fn rebond(mut self, value: BalanceOf<T>) -> (Self, BalanceOf<T>) {
|
||||||
let mut unlocking_balance: Balance = Zero::zero();
|
let mut unlocking_balance = BalanceOf::<T>::zero();
|
||||||
|
|
||||||
while let Some(last) = self.unlocking.last_mut() {
|
while let Some(last) = self.unlocking.last_mut() {
|
||||||
if unlocking_balance + last.value <= value {
|
if unlocking_balance + last.value <= value {
|
||||||
@@ -530,57 +528,96 @@ impl<AccountId, Balance: HasCompact + Copy + Saturating + AtLeast32BitUnsigned +
|
|||||||
|
|
||||||
(self, unlocking_balance)
|
(self, unlocking_balance)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl<AccountId, Balance> StakingLedger<AccountId, Balance>
|
/// Slash the staker for a given amount of balance. This can grow the value of the slash in the
|
||||||
where
|
/// case that either the active bonded or some unlocking chunks become dust after slashing.
|
||||||
Balance: AtLeast32BitUnsigned + Saturating + Copy,
|
/// Returns the amount of funds actually slashed.
|
||||||
{
|
|
||||||
/// Slash the validator for a given amount of balance. This can grow the value
|
|
||||||
/// of the slash in the case that the validator has less than `minimum_balance`
|
|
||||||
/// active funds. Returns the amount of funds actually slashed.
|
|
||||||
///
|
///
|
||||||
/// Slashes from `active` funds first, and then `unlocking`, starting with the
|
/// # Note
|
||||||
/// chunks that are closest to unlocking.
|
///
|
||||||
fn slash(&mut self, mut value: Balance, minimum_balance: Balance) -> Balance {
|
/// This calls `Config::OnStakerSlash::on_slash` with information as to how the slash
|
||||||
let pre_total = self.total;
|
/// was applied.
|
||||||
let total = &mut self.total;
|
fn slash(
|
||||||
let active = &mut self.active;
|
&mut self,
|
||||||
|
slash_amount: BalanceOf<T>,
|
||||||
|
minimum_balance: BalanceOf<T>,
|
||||||
|
slash_era: EraIndex,
|
||||||
|
) -> BalanceOf<T> {
|
||||||
|
use sp_staking::OnStakerSlash as _;
|
||||||
|
|
||||||
let slash_out_of =
|
if slash_amount.is_zero() {
|
||||||
|total_remaining: &mut Balance, target: &mut Balance, value: &mut Balance| {
|
return Zero::zero()
|
||||||
let mut slash_from_target = (*value).min(*target);
|
}
|
||||||
|
|
||||||
if !slash_from_target.is_zero() {
|
let mut remaining_slash = slash_amount;
|
||||||
*target -= slash_from_target;
|
let pre_slash_total = self.total;
|
||||||
|
|
||||||
// Don't leave a dust balance in the staking system.
|
let era_after_slash = slash_era + 1;
|
||||||
if *target <= minimum_balance {
|
let chunk_unlock_era_after_slash = era_after_slash + T::BondingDuration::get();
|
||||||
slash_from_target += *target;
|
|
||||||
*value += sp_std::mem::replace(target, Zero::zero());
|
|
||||||
}
|
|
||||||
|
|
||||||
*total_remaining = total_remaining.saturating_sub(slash_from_target);
|
// Calculate the total balance of active funds and unlocking funds in the affected range.
|
||||||
*value -= slash_from_target;
|
let (affected_balance, slash_chunks_priority): (_, Box<dyn Iterator<Item = usize>>) = {
|
||||||
|
if let Some(start_index) =
|
||||||
|
self.unlocking.iter().position(|c| c.era >= chunk_unlock_era_after_slash)
|
||||||
|
{
|
||||||
|
// The indices of the first chunk after the slash up through the most recent chunk.
|
||||||
|
// (The most recent chunk is at greatest from this era)
|
||||||
|
let affected_indices = start_index..self.unlocking.len();
|
||||||
|
let unbonding_affected_balance =
|
||||||
|
affected_indices.clone().fold(BalanceOf::<T>::zero(), |sum, i| {
|
||||||
|
if let Some(chunk) = self.unlocking.get_mut(i).defensive() {
|
||||||
|
sum.saturating_add(chunk.value)
|
||||||
|
} else {
|
||||||
|
sum
|
||||||
|
}
|
||||||
|
});
|
||||||
|
(
|
||||||
|
self.active.saturating_add(unbonding_affected_balance),
|
||||||
|
Box::new(affected_indices.chain((0..start_index).rev())),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(self.active, Box::new((0..self.unlocking.len()).rev()))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to update `target` and the ledgers total after accounting for slashing `target`.
|
||||||
|
let ratio = Perquintill::from_rational(slash_amount, affected_balance);
|
||||||
|
let mut slash_out_of = |target: &mut BalanceOf<T>, slash_remaining: &mut BalanceOf<T>| {
|
||||||
|
let mut slash_from_target =
|
||||||
|
if slash_amount < affected_balance { ratio * (*target) } else { *slash_remaining }
|
||||||
|
.min(*target);
|
||||||
|
|
||||||
|
// slash out from *target exactly `slash_from_target`.
|
||||||
|
*target = *target - slash_from_target;
|
||||||
|
if *target < minimum_balance {
|
||||||
|
// Slash the rest of the target if its dust
|
||||||
|
slash_from_target =
|
||||||
|
sp_std::mem::replace(target, Zero::zero()).saturating_add(slash_from_target)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.total = self.total.saturating_sub(slash_from_target);
|
||||||
|
*slash_remaining = slash_remaining.saturating_sub(slash_from_target);
|
||||||
|
};
|
||||||
|
|
||||||
|
// If this is *not* a proportional slash, the active will always wiped to 0.
|
||||||
|
slash_out_of(&mut self.active, &mut remaining_slash);
|
||||||
|
|
||||||
|
let mut slashed_unlocking = BTreeMap::<_, _>::new();
|
||||||
|
for i in slash_chunks_priority {
|
||||||
|
if let Some(chunk) = self.unlocking.get_mut(i).defensive() {
|
||||||
|
slash_out_of(&mut chunk.value, &mut remaining_slash);
|
||||||
|
// write the new slashed value of this chunk to the map.
|
||||||
|
slashed_unlocking.insert(chunk.era, chunk.value);
|
||||||
|
if remaining_slash.is_zero() {
|
||||||
|
break
|
||||||
}
|
}
|
||||||
};
|
} else {
|
||||||
|
break
|
||||||
slash_out_of(total, active, &mut value);
|
}
|
||||||
|
}
|
||||||
let i = self
|
self.unlocking.retain(|c| !c.value.is_zero());
|
||||||
.unlocking
|
T::OnStakerSlash::on_slash(&self.stash, self.active, &slashed_unlocking);
|
||||||
.iter_mut()
|
pre_slash_total.saturating_sub(self.total)
|
||||||
.map(|chunk| {
|
|
||||||
slash_out_of(total, &mut chunk.value, &mut value);
|
|
||||||
chunk.value
|
|
||||||
})
|
|
||||||
.take_while(|value| value.is_zero()) // Take all fully-consumed chunks out.
|
|
||||||
.count();
|
|
||||||
|
|
||||||
// Kill all drained chunks.
|
|
||||||
let _ = self.unlocking.drain(..i);
|
|
||||||
|
|
||||||
pre_total.saturating_sub(*total)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -238,6 +238,7 @@ parameter_types! {
|
|||||||
pub static BagThresholds: &'static [sp_npos_elections::VoteWeight] = &THRESHOLDS;
|
pub static BagThresholds: &'static [sp_npos_elections::VoteWeight] = &THRESHOLDS;
|
||||||
pub static MaxNominations: u32 = 16;
|
pub static MaxNominations: u32 = 16;
|
||||||
pub static RewardOnUnbalanceWasCalled: bool = false;
|
pub static RewardOnUnbalanceWasCalled: bool = false;
|
||||||
|
pub static LedgerSlashPerEra: (BalanceOf<Test>, BTreeMap<EraIndex, BalanceOf<Test>>) = (Zero::zero(), BTreeMap::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
impl pallet_bags_list::Config for Test {
|
impl pallet_bags_list::Config for Test {
|
||||||
@@ -263,9 +264,21 @@ impl OnUnbalanced<PositiveImbalanceOf<Test>> for MockReward {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct OnStakerSlashMock<T: Config>(core::marker::PhantomData<T>);
|
||||||
|
impl<T: Config> sp_staking::OnStakerSlash<AccountId, Balance> for OnStakerSlashMock<T> {
|
||||||
|
fn on_slash(
|
||||||
|
_pool_account: &AccountId,
|
||||||
|
slashed_bonded: Balance,
|
||||||
|
slashed_chunks: &BTreeMap<EraIndex, Balance>,
|
||||||
|
) {
|
||||||
|
LedgerSlashPerEra::set((slashed_bonded, slashed_chunks.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl crate::pallet::pallet::Config for Test {
|
impl crate::pallet::pallet::Config for Test {
|
||||||
type MaxNominations = MaxNominations;
|
type MaxNominations = MaxNominations;
|
||||||
type Currency = Balances;
|
type Currency = Balances;
|
||||||
|
type CurrencyBalance = <Self as pallet_balances::Config>::Balance;
|
||||||
type UnixTime = Timestamp;
|
type UnixTime = Timestamp;
|
||||||
type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
|
type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote;
|
||||||
type RewardRemainder = RewardRemainderMock;
|
type RewardRemainder = RewardRemainderMock;
|
||||||
@@ -286,6 +299,7 @@ impl crate::pallet::pallet::Config for Test {
|
|||||||
// NOTE: consider a macro and use `UseNominatorsAndValidatorsMap<Self>` as well.
|
// NOTE: consider a macro and use `UseNominatorsAndValidatorsMap<Self>` as well.
|
||||||
type VoterList = BagsList;
|
type VoterList = BagsList;
|
||||||
type MaxUnlockingChunks = ConstU32<32>;
|
type MaxUnlockingChunks = ConstU32<32>;
|
||||||
|
type OnStakerSlash = OnStakerSlashMock<Test>;
|
||||||
type BenchmarkingConfig = TestBenchmarkingConfig;
|
type BenchmarkingConfig = TestBenchmarkingConfig;
|
||||||
type WeightInfo = ();
|
type WeightInfo = ();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,10 +213,7 @@ impl<T: Config> Pallet<T> {
|
|||||||
/// Update the ledger for a controller.
|
/// Update the ledger for a controller.
|
||||||
///
|
///
|
||||||
/// This will also update the stash lock.
|
/// This will also update the stash lock.
|
||||||
pub(crate) fn update_ledger(
|
pub(crate) fn update_ledger(controller: &T::AccountId, ledger: &StakingLedger<T>) {
|
||||||
controller: &T::AccountId,
|
|
||||||
ledger: &StakingLedger<T::AccountId, BalanceOf<T>>,
|
|
||||||
) {
|
|
||||||
T::Currency::set_lock(STAKING_ID, &ledger.stash, ledger.total, WithdrawReasons::all());
|
T::Currency::set_lock(STAKING_ID, &ledger.stash, ledger.total, WithdrawReasons::all());
|
||||||
<Ledger<T>>::insert(controller, ledger);
|
<Ledger<T>>::insert(controller, ledger);
|
||||||
}
|
}
|
||||||
@@ -606,7 +603,7 @@ impl<T: Config> Pallet<T> {
|
|||||||
for era in (*earliest)..keep_from {
|
for era in (*earliest)..keep_from {
|
||||||
let era_slashes = <Self as Store>::UnappliedSlashes::take(&era);
|
let era_slashes = <Self as Store>::UnappliedSlashes::take(&era);
|
||||||
for slash in era_slashes {
|
for slash in era_slashes {
|
||||||
slashing::apply_slash::<T>(slash);
|
slashing::apply_slash::<T>(slash, era);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1248,7 +1245,7 @@ where
|
|||||||
unapplied.reporters = details.reporters.clone();
|
unapplied.reporters = details.reporters.clone();
|
||||||
if slash_defer_duration == 0 {
|
if slash_defer_duration == 0 {
|
||||||
// Apply right away.
|
// Apply right away.
|
||||||
slashing::apply_slash::<T>(unapplied);
|
slashing::apply_slash::<T>(unapplied, slash_era);
|
||||||
{
|
{
|
||||||
let slash_cost = (6, 5);
|
let slash_cost = (6, 5);
|
||||||
let reward_cost = (2, 2);
|
let reward_cost = (2, 2);
|
||||||
|
|||||||
@@ -75,8 +75,22 @@ pub mod pallet {
|
|||||||
#[pallet::config]
|
#[pallet::config]
|
||||||
pub trait Config: frame_system::Config + SendTransactionTypes<Call<Self>> {
|
pub trait Config: frame_system::Config + SendTransactionTypes<Call<Self>> {
|
||||||
/// The staking balance.
|
/// The staking balance.
|
||||||
type Currency: LockableCurrency<Self::AccountId, Moment = Self::BlockNumber>;
|
type Currency: LockableCurrency<
|
||||||
|
Self::AccountId,
|
||||||
|
Moment = Self::BlockNumber,
|
||||||
|
Balance = Self::CurrencyBalance,
|
||||||
|
>;
|
||||||
|
/// Just the `Currency::Balance` type; we have this item to allow us to constrain it to
|
||||||
|
/// `From<u64>`.
|
||||||
|
type CurrencyBalance: sp_runtime::traits::AtLeast32BitUnsigned
|
||||||
|
+ codec::FullCodec
|
||||||
|
+ Copy
|
||||||
|
+ MaybeSerializeDeserialize
|
||||||
|
+ sp_std::fmt::Debug
|
||||||
|
+ Default
|
||||||
|
+ From<u64>
|
||||||
|
+ TypeInfo
|
||||||
|
+ MaxEncodedLen;
|
||||||
/// Time used for computing era duration.
|
/// Time used for computing era duration.
|
||||||
///
|
///
|
||||||
/// It is guaranteed to start being called from the first `on_finalize`. Thus value at
|
/// It is guaranteed to start being called from the first `on_finalize`. Thus value at
|
||||||
@@ -177,6 +191,10 @@ pub mod pallet {
|
|||||||
#[pallet::constant]
|
#[pallet::constant]
|
||||||
type MaxUnlockingChunks: Get<u32>;
|
type MaxUnlockingChunks: Get<u32>;
|
||||||
|
|
||||||
|
/// A hook called when any staker is slashed. Mostly likely this can be a no-op unless
|
||||||
|
/// other pallets exist that are affected by slashing per-staker.
|
||||||
|
type OnStakerSlash: sp_staking::OnStakerSlash<Self::AccountId, BalanceOf<Self>>;
|
||||||
|
|
||||||
/// Some parameters of the benchmarking.
|
/// Some parameters of the benchmarking.
|
||||||
type BenchmarkingConfig: BenchmarkingConfig;
|
type BenchmarkingConfig: BenchmarkingConfig;
|
||||||
|
|
||||||
@@ -239,8 +257,7 @@ pub mod pallet {
|
|||||||
/// Map from all (unlocked) "controller" accounts to the info regarding the staking.
|
/// Map from all (unlocked) "controller" accounts to the info regarding the staking.
|
||||||
#[pallet::storage]
|
#[pallet::storage]
|
||||||
#[pallet::getter(fn ledger)]
|
#[pallet::getter(fn ledger)]
|
||||||
pub type Ledger<T: Config> =
|
pub type Ledger<T: Config> = StorageMap<_, Blake2_128Concat, T::AccountId, StakingLedger<T>>;
|
||||||
StorageMap<_, Blake2_128Concat, T::AccountId, StakingLedger<T::AccountId, BalanceOf<T>>>;
|
|
||||||
|
|
||||||
/// Where the reward payment should be made. Keyed by stash.
|
/// Where the reward payment should be made. Keyed by stash.
|
||||||
#[pallet::storage]
|
#[pallet::storage]
|
||||||
|
|||||||
@@ -598,6 +598,7 @@ pub fn do_slash<T: Config>(
|
|||||||
value: BalanceOf<T>,
|
value: BalanceOf<T>,
|
||||||
reward_payout: &mut BalanceOf<T>,
|
reward_payout: &mut BalanceOf<T>,
|
||||||
slashed_imbalance: &mut NegativeImbalanceOf<T>,
|
slashed_imbalance: &mut NegativeImbalanceOf<T>,
|
||||||
|
slash_era: EraIndex,
|
||||||
) {
|
) {
|
||||||
let controller = match <Pallet<T>>::bonded(stash) {
|
let controller = match <Pallet<T>>::bonded(stash) {
|
||||||
None => return, // defensive: should always exist.
|
None => return, // defensive: should always exist.
|
||||||
@@ -609,7 +610,7 @@ pub fn do_slash<T: Config>(
|
|||||||
None => return, // nothing to do.
|
None => return, // nothing to do.
|
||||||
};
|
};
|
||||||
|
|
||||||
let value = ledger.slash(value, T::Currency::minimum_balance());
|
let value = ledger.slash(value, T::Currency::minimum_balance(), slash_era);
|
||||||
|
|
||||||
if !value.is_zero() {
|
if !value.is_zero() {
|
||||||
let (imbalance, missing) = T::Currency::slash(stash, value);
|
let (imbalance, missing) = T::Currency::slash(stash, value);
|
||||||
@@ -628,7 +629,10 @@ pub fn do_slash<T: Config>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Apply a previously-unapplied slash.
|
/// Apply a previously-unapplied slash.
|
||||||
pub(crate) fn apply_slash<T: Config>(unapplied_slash: UnappliedSlash<T::AccountId, BalanceOf<T>>) {
|
pub(crate) fn apply_slash<T: Config>(
|
||||||
|
unapplied_slash: UnappliedSlash<T::AccountId, BalanceOf<T>>,
|
||||||
|
slash_era: EraIndex,
|
||||||
|
) {
|
||||||
let mut slashed_imbalance = NegativeImbalanceOf::<T>::zero();
|
let mut slashed_imbalance = NegativeImbalanceOf::<T>::zero();
|
||||||
let mut reward_payout = unapplied_slash.payout;
|
let mut reward_payout = unapplied_slash.payout;
|
||||||
|
|
||||||
@@ -637,10 +641,17 @@ pub(crate) fn apply_slash<T: Config>(unapplied_slash: UnappliedSlash<T::AccountI
|
|||||||
unapplied_slash.own,
|
unapplied_slash.own,
|
||||||
&mut reward_payout,
|
&mut reward_payout,
|
||||||
&mut slashed_imbalance,
|
&mut slashed_imbalance,
|
||||||
|
slash_era,
|
||||||
);
|
);
|
||||||
|
|
||||||
for &(ref nominator, nominator_slash) in &unapplied_slash.others {
|
for &(ref nominator, nominator_slash) in &unapplied_slash.others {
|
||||||
do_slash::<T>(&nominator, nominator_slash, &mut reward_payout, &mut slashed_imbalance);
|
do_slash::<T>(
|
||||||
|
&nominator,
|
||||||
|
nominator_slash,
|
||||||
|
&mut reward_payout,
|
||||||
|
&mut slashed_imbalance,
|
||||||
|
slash_era,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pay_reporters::<T>(reward_payout, slashed_imbalance, &unapplied_slash.reporters);
|
pay_reporters::<T>(reward_payout, slashed_imbalance, &unapplied_slash.reporters);
|
||||||
|
|||||||
@@ -2104,7 +2104,7 @@ fn reward_validator_slashing_validator_does_not_overflow() {
|
|||||||
&[Perbill::from_percent(100)],
|
&[Perbill::from_percent(100)],
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(Balances::total_balance(&11), stake - 1);
|
assert_eq!(Balances::total_balance(&11), stake);
|
||||||
assert_eq!(Balances::total_balance(&2), 1);
|
assert_eq!(Balances::total_balance(&2), 1);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -4854,3 +4854,216 @@ fn force_apply_min_commission_works() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ledger_slash_works() {
|
||||||
|
let c = |era, value| UnlockChunk::<Balance> { era, value };
|
||||||
|
// Given
|
||||||
|
let mut ledger = StakingLedger::<Test> {
|
||||||
|
stash: 123,
|
||||||
|
total: 10,
|
||||||
|
active: 10,
|
||||||
|
unlocking: bounded_vec![],
|
||||||
|
claimed_rewards: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(BondingDuration::get(), 3);
|
||||||
|
|
||||||
|
// When we slash a ledger with no unlocking chunks
|
||||||
|
assert_eq!(ledger.slash(5, 1, 0), 5);
|
||||||
|
// Then
|
||||||
|
assert_eq!(ledger.total, 5);
|
||||||
|
assert_eq!(ledger.active, 5);
|
||||||
|
assert_eq!(LedgerSlashPerEra::get().0, 5);
|
||||||
|
assert_eq!(LedgerSlashPerEra::get().1, Default::default());
|
||||||
|
|
||||||
|
// When we slash a ledger with no unlocking chunks and the slash amount is greater then the
|
||||||
|
// total
|
||||||
|
assert_eq!(ledger.slash(11, 1, 0), 5);
|
||||||
|
// Then
|
||||||
|
assert_eq!(ledger.total, 0);
|
||||||
|
assert_eq!(ledger.active, 0);
|
||||||
|
assert_eq!(LedgerSlashPerEra::get().0, 0);
|
||||||
|
assert_eq!(LedgerSlashPerEra::get().1, Default::default());
|
||||||
|
|
||||||
|
// Given
|
||||||
|
ledger.unlocking = bounded_vec![c(4, 10), c(5, 10)];
|
||||||
|
ledger.total = 2 * 10;
|
||||||
|
ledger.active = 0;
|
||||||
|
// When all the chunks overlap with the slash eras
|
||||||
|
assert_eq!(ledger.slash(20, 0, 0), 20);
|
||||||
|
// Then
|
||||||
|
assert_eq!(ledger.unlocking, vec![]);
|
||||||
|
assert_eq!(ledger.total, 0);
|
||||||
|
assert_eq!(LedgerSlashPerEra::get().0, 0);
|
||||||
|
assert_eq!(LedgerSlashPerEra::get().1, BTreeMap::from([(4, 0), (5, 0)]));
|
||||||
|
|
||||||
|
// Given
|
||||||
|
ledger.unlocking = bounded_vec![c(4, 100), c(5, 100), c(6, 100), c(7, 100)];
|
||||||
|
ledger.total = 4 * 100;
|
||||||
|
ledger.active = 0;
|
||||||
|
// When the first 2 chunks don't overlap with the affected range of unlock eras.
|
||||||
|
assert_eq!(ledger.slash(140, 0, 2), 140);
|
||||||
|
// Then
|
||||||
|
assert_eq!(ledger.unlocking, vec![c(4, 100), c(5, 100), c(6, 30), c(7, 30)]);
|
||||||
|
assert_eq!(ledger.total, 4 * 100 - 140);
|
||||||
|
assert_eq!(LedgerSlashPerEra::get().0, 0);
|
||||||
|
assert_eq!(LedgerSlashPerEra::get().1, BTreeMap::from([(6, 30), (7, 30)]));
|
||||||
|
|
||||||
|
// Given
|
||||||
|
ledger.unlocking = bounded_vec![c(4, 40), c(5, 100), c(6, 10), c(7, 250)];
|
||||||
|
ledger.active = 500;
|
||||||
|
// 900
|
||||||
|
ledger.total = 40 + 10 + 100 + 250 + 500;
|
||||||
|
// When we have a partial slash that touches all chunks
|
||||||
|
assert_eq!(ledger.slash(900 / 2, 0, 0), 450);
|
||||||
|
// Then
|
||||||
|
assert_eq!(ledger.active, 500 / 2);
|
||||||
|
assert_eq!(ledger.unlocking, vec![c(4, 40 / 2), c(5, 100 / 2), c(6, 10 / 2), c(7, 250 / 2)]);
|
||||||
|
assert_eq!(ledger.total, 900 / 2);
|
||||||
|
assert_eq!(LedgerSlashPerEra::get().0, 500 / 2);
|
||||||
|
assert_eq!(
|
||||||
|
LedgerSlashPerEra::get().1,
|
||||||
|
BTreeMap::from([(4, 40 / 2), (5, 100 / 2), (6, 10 / 2), (7, 250 / 2)])
|
||||||
|
);
|
||||||
|
|
||||||
|
// slash 1/4th with not chunk.
|
||||||
|
ledger.unlocking = bounded_vec![];
|
||||||
|
ledger.active = 500;
|
||||||
|
ledger.total = 500;
|
||||||
|
// When we have a partial slash that touches all chunks
|
||||||
|
assert_eq!(ledger.slash(500 / 4, 0, 0), 500 / 4);
|
||||||
|
// Then
|
||||||
|
assert_eq!(ledger.active, 3 * 500 / 4);
|
||||||
|
assert_eq!(ledger.unlocking, vec![]);
|
||||||
|
assert_eq!(ledger.total, ledger.active);
|
||||||
|
assert_eq!(LedgerSlashPerEra::get().0, 3 * 500 / 4);
|
||||||
|
assert_eq!(LedgerSlashPerEra::get().1, Default::default());
|
||||||
|
|
||||||
|
// Given we have the same as above,
|
||||||
|
ledger.unlocking = bounded_vec![c(4, 40), c(5, 100), c(6, 10), c(7, 250)];
|
||||||
|
ledger.active = 500;
|
||||||
|
ledger.total = 40 + 10 + 100 + 250 + 500; // 900
|
||||||
|
assert_eq!(ledger.total, 900);
|
||||||
|
// When we have a higher min balance
|
||||||
|
assert_eq!(
|
||||||
|
ledger.slash(
|
||||||
|
900 / 2,
|
||||||
|
25, /* min balance - chunks with era 0 & 2 will be slashed to <=25, causing it to
|
||||||
|
* get swept */
|
||||||
|
0
|
||||||
|
),
|
||||||
|
475
|
||||||
|
);
|
||||||
|
let dust = (10 / 2) + (40 / 2);
|
||||||
|
assert_eq!(ledger.active, 500 / 2);
|
||||||
|
assert_eq!(ledger.unlocking, vec![c(5, 100 / 2), c(7, 250 / 2)]);
|
||||||
|
assert_eq!(ledger.total, 900 / 2 - dust);
|
||||||
|
assert_eq!(LedgerSlashPerEra::get().0, 500 / 2);
|
||||||
|
assert_eq!(
|
||||||
|
LedgerSlashPerEra::get().1,
|
||||||
|
BTreeMap::from([(4, 0), (5, 100 / 2), (6, 0), (7, 250 / 2)])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Given
|
||||||
|
// slash order --------------------NA--------2----------0----------1----
|
||||||
|
ledger.unlocking = bounded_vec![c(4, 40), c(5, 100), c(6, 10), c(7, 250)];
|
||||||
|
ledger.active = 500;
|
||||||
|
ledger.total = 40 + 10 + 100 + 250 + 500; // 900
|
||||||
|
assert_eq!(
|
||||||
|
ledger.slash(
|
||||||
|
500 + 10 + 250 + 100 / 2, // active + era 6 + era 7 + era 5 / 2
|
||||||
|
0,
|
||||||
|
2 /* slash era 2+4 first, so the affected parts are era 2+4, era 3+4 and
|
||||||
|
* ledge.active. This will cause the affected to go to zero, and then we will
|
||||||
|
* start slashing older chunks */
|
||||||
|
),
|
||||||
|
500 + 250 + 10 + 100 / 2
|
||||||
|
);
|
||||||
|
// Then
|
||||||
|
assert_eq!(ledger.active, 0);
|
||||||
|
assert_eq!(ledger.unlocking, vec![c(4, 40), c(5, 100 / 2)]);
|
||||||
|
assert_eq!(ledger.total, 90);
|
||||||
|
assert_eq!(LedgerSlashPerEra::get().0, 0);
|
||||||
|
assert_eq!(LedgerSlashPerEra::get().1, BTreeMap::from([(5, 100 / 2), (6, 0), (7, 0)]));
|
||||||
|
|
||||||
|
// Given
|
||||||
|
// iteration order------------------NA---------2----------0----------1----
|
||||||
|
ledger.unlocking = bounded_vec![c(4, 100), c(5, 100), c(6, 100), c(7, 100)];
|
||||||
|
ledger.active = 100;
|
||||||
|
ledger.total = 5 * 100;
|
||||||
|
// When
|
||||||
|
assert_eq!(
|
||||||
|
ledger.slash(
|
||||||
|
351, // active + era 6 + era 7 + era 5 / 2 + 1
|
||||||
|
50, // min balance - everything slashed below 50 will get dusted
|
||||||
|
2 /* slash era 2+4 first, so the affected parts are era 2+4, era 3+4 and
|
||||||
|
* ledge.active. This will cause the affected to go to zero, and then we will
|
||||||
|
* start slashing older chunks */
|
||||||
|
),
|
||||||
|
400
|
||||||
|
);
|
||||||
|
// Then
|
||||||
|
assert_eq!(ledger.active, 0);
|
||||||
|
assert_eq!(ledger.unlocking, vec![c(4, 100)]);
|
||||||
|
assert_eq!(ledger.total, 100);
|
||||||
|
assert_eq!(LedgerSlashPerEra::get().0, 0);
|
||||||
|
assert_eq!(LedgerSlashPerEra::get().1, BTreeMap::from([(5, 0), (6, 0), (7, 0)]));
|
||||||
|
|
||||||
|
// Tests for saturating arithmetic
|
||||||
|
|
||||||
|
// Given
|
||||||
|
let slash = u64::MAX as Balance * 2;
|
||||||
|
let value = slash
|
||||||
|
- (9 * 4) // The value of the other parts of ledger that will get slashed
|
||||||
|
+ 1;
|
||||||
|
|
||||||
|
ledger.active = 10;
|
||||||
|
ledger.unlocking = bounded_vec![c(4, 10), c(5, 10), c(6, 10), c(7, value)];
|
||||||
|
ledger.total = value + 40;
|
||||||
|
// When
|
||||||
|
let slash_amount = ledger.slash(slash, 0, 0);
|
||||||
|
assert_eq_error_rate!(slash_amount, slash, 5);
|
||||||
|
// Then
|
||||||
|
assert_eq!(ledger.active, 0); // slash of 9
|
||||||
|
assert_eq!(ledger.unlocking, vec![]);
|
||||||
|
assert_eq!(ledger.total, 0);
|
||||||
|
assert_eq!(LedgerSlashPerEra::get().0, 0);
|
||||||
|
assert_eq!(LedgerSlashPerEra::get().1, BTreeMap::from([(4, 0), (5, 0), (6, 0), (7, 0)]));
|
||||||
|
|
||||||
|
// Given
|
||||||
|
let slash = u64::MAX as Balance * 2;
|
||||||
|
let value = u64::MAX as Balance * 2;
|
||||||
|
let unit = 100;
|
||||||
|
// slash * value that will saturate
|
||||||
|
assert!(slash.checked_mul(value).is_none());
|
||||||
|
// but slash * unit won't.
|
||||||
|
assert!(slash.checked_mul(unit).is_some());
|
||||||
|
ledger.unlocking = bounded_vec![c(4, unit), c(5, value), c(6, unit), c(7, unit)];
|
||||||
|
//--------------------------------------note value^^^
|
||||||
|
ledger.active = unit;
|
||||||
|
ledger.total = unit * 4 + value;
|
||||||
|
// When
|
||||||
|
assert_eq!(ledger.slash(slash, 0, 0), slash - 43);
|
||||||
|
// Then
|
||||||
|
// The amount slashed out of `unit`
|
||||||
|
let affected_balance = value + unit * 4;
|
||||||
|
let ratio = Perquintill::from_rational(slash, affected_balance);
|
||||||
|
// `unit` after the slash is applied
|
||||||
|
let unit_slashed = {
|
||||||
|
let unit_slash = ratio * unit;
|
||||||
|
unit - unit_slash
|
||||||
|
};
|
||||||
|
let value_slashed = {
|
||||||
|
let value_slash = ratio * value;
|
||||||
|
value - value_slash
|
||||||
|
};
|
||||||
|
assert_eq!(ledger.active, unit_slashed);
|
||||||
|
assert_eq!(ledger.unlocking, vec![c(5, value_slashed)]);
|
||||||
|
assert_eq!(ledger.total, value_slashed);
|
||||||
|
assert_eq!(LedgerSlashPerEra::get().0, 0);
|
||||||
|
assert_eq!(
|
||||||
|
LedgerSlashPerEra::get().1,
|
||||||
|
BTreeMap::from([(4, 0), (5, value_slashed), (6, 0), (7, 0)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
//! A crate which contains primitives that are useful for implementation that uses staking
|
//! A crate which contains primitives that are useful for implementation that uses staking
|
||||||
//! approaches in general. Definitions related to sessions, slashing, etc go here.
|
//! approaches in general. Definitions related to sessions, slashing, etc go here.
|
||||||
|
use sp_std::collections::btree_map::BTreeMap;
|
||||||
|
|
||||||
pub mod offence;
|
pub mod offence;
|
||||||
|
|
||||||
@@ -27,3 +28,27 @@ pub type SessionIndex = u32;
|
|||||||
|
|
||||||
/// Counter for the number of eras that have passed.
|
/// Counter for the number of eras that have passed.
|
||||||
pub type EraIndex = u32;
|
pub type EraIndex = u32;
|
||||||
|
|
||||||
|
/// Trait describing something that implements a hook for any operations to perform when a staker is
|
||||||
|
/// slashed.
|
||||||
|
pub trait OnStakerSlash<AccountId, Balance> {
|
||||||
|
/// A hook for any operations to perform when a staker is slashed.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `stash` - The stash of the staker whom the slash was applied to.
|
||||||
|
/// * `slashed_active` - The new bonded balance of the staker after the slash was applied.
|
||||||
|
/// * `slashed_unlocking` - a map of slashed eras, and the balance of that unlocking chunk after
|
||||||
|
/// the slash is applied. Any era not present in the map is not affected at all.
|
||||||
|
fn on_slash(
|
||||||
|
stash: &AccountId,
|
||||||
|
slashed_active: Balance,
|
||||||
|
slashed_unlocking: &BTreeMap<EraIndex, Balance>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<AccountId, Balance> OnStakerSlash<AccountId, Balance> for () {
|
||||||
|
fn on_slash(_: &AccountId, _: Balance, _: &BTreeMap<EraIndex, Balance>) {
|
||||||
|
// Nothing to do here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user