use super::*;
use sp_staking::StakingUnchecked;
#[test]
fn nominators_are_not_slashed() {
ExtBuilder::default()
.validator_count(4)
.set_status(41, StakerStatus::Validator)
.set_nominators_slashable(false)
.build_and_execute(|| {
let initial_exposure = Staking::eras_stakers(active_era(), &11);
assert_eq!(
initial_exposure,
Exposure {
total: 1250,
own: 1000,
others: vec![IndividualExposure { who: 101, value: 250 }]
}
);
let nominator_stake = Staking::ledger(101.into()).unwrap().active;
let nominator_balance = asset::stakeable_balance::<Test>(&101);
let validator_stake = Staking::ledger(11.into()).unwrap().active;
let validator_balance = asset::stakeable_balance::<Test>(&11);
add_slash(11);
assert_eq!(
staking_events_since_last_call(),
vec![Event::OffenceReported {
offence_era: 1,
validator: 11,
fraction: Perbill::from_percent(10)
}]
);
assert_eq!(SlashDeferDuration::get(), 0);
Session::roll_next();
assert_eq!(
staking_events_since_last_call(),
vec![
Event::SlashComputed { offence_era: 1, slash_era: 1, offender: 11, page: 0 },
Event::Slashed { staker: 11, amount: 100 },
]
);
assert_eq!(Staking::ledger(101.into()).unwrap().active, nominator_stake);
assert_eq!(asset::stakeable_balance::<Test>(&101), nominator_balance);
assert!(Staking::ledger(11.into()).unwrap().active < validator_stake);
assert!(asset::stakeable_balance::<Test>(&11) < validator_balance);
assert_eq!(Staking::ledger(11.into()).unwrap().active, validator_stake - 100);
assert_eq!(asset::stakeable_balance::<Test>(&11), validator_balance - 100);
});
}
#[test]
fn nominators_can_unbond_in_next_era() {
ExtBuilder::default().set_nominators_slashable(false).build_and_execute(|| {
assert_eq!(
Staking::ledger(101.into()).unwrap(),
StakingLedgerInspect {
stash: 101,
total: 500,
active: 500,
unlocking: Default::default(),
}
);
let era = active_era();
assert_eq!(era, 1);
assert_ok!(Staking::unbond(RuntimeOrigin::signed(101), 200));
assert_eq!(
staking_events_since_last_call(),
vec![Event::Unbonded { stash: 101, amount: 200 }]
);
let fast_unbond_era = era + NominatorFastUnbondDuration::get();
assert_eq!(
Staking::ledger(101.into()).unwrap(),
StakingLedgerInspect {
stash: 101,
total: 500,
active: 300,
unlocking: bounded_vec![UnlockChunk { value: 200, era: fast_unbond_era }],
}
);
assert_ok!(Staking::withdraw_unbonded(RuntimeOrigin::signed(101), 0));
assert_eq!(Staking::ledger(101.into()).unwrap().total, 500);
Session::roll_until_active_era(fast_unbond_era);
assert_eq!(active_era(), fast_unbond_era);
let _ = staking_events_since_last_call(); assert_ok!(Staking::withdraw_unbonded(RuntimeOrigin::signed(101), 0));
assert_eq!(
staking_events_since_last_call(),
vec![Event::Withdrawn { stash: 101, amount: 200 }]
);
assert_eq!(
Staking::ledger(101.into()).unwrap(),
StakingLedgerInspect {
stash: 101,
total: 300,
active: 300,
unlocking: Default::default(),
}
);
});
}
#[test]
fn validators_still_have_full_bonding_duration() {
ExtBuilder::default().set_nominators_slashable(false).build_and_execute(|| {
assert_eq!(
Staking::ledger(11.into()).unwrap(),
StakingLedgerInspect {
stash: 11,
total: 1000,
active: 1000,
unlocking: Default::default(),
}
);
let era = active_era();
assert_eq!(era, 1);
let bonding_duration = BondingDuration::get();
assert_eq!(bonding_duration, 3);
assert_ok!(Staking::unbond(RuntimeOrigin::signed(11), 200));
assert_eq!(
staking_events_since_last_call(),
vec![Event::Unbonded { stash: 11, amount: 200 }]
);
assert_eq!(
Staking::ledger(11.into()).unwrap(),
StakingLedgerInspect {
stash: 11,
total: 1000,
active: 800,
unlocking: bounded_vec![UnlockChunk { value: 200, era: era + bonding_duration }],
}
);
Session::roll_until_active_era(era + 1);
assert_eq!(active_era(), 2);
assert_ok!(Staking::withdraw_unbonded(RuntimeOrigin::signed(11), 0));
assert_eq!(Staking::ledger(11.into()).unwrap().total, 1000);
Session::roll_until_active_era(era + 2);
assert_eq!(active_era(), 3);
assert_ok!(Staking::withdraw_unbonded(RuntimeOrigin::signed(11), 0));
assert_eq!(Staking::ledger(11.into()).unwrap().total, 1000);
Session::roll_until_active_era(era + bonding_duration);
assert_eq!(active_era(), 4);
assert_ok!(Staking::withdraw_unbonded(RuntimeOrigin::signed(11), 0));
assert_eq!(
Staking::ledger(11.into()).unwrap(),
StakingLedgerInspect {
stash: 11,
total: 800,
active: 800,
unlocking: Default::default(),
}
);
});
}
#[test]
fn nominator_not_slashed_with_deferred_slash() {
ExtBuilder::default()
.validator_count(4)
.set_status(41, StakerStatus::Validator)
.set_nominators_slashable(false)
.slash_defer_duration(2)
.build_and_execute(|| {
let initial_exposure = Staking::eras_stakers(active_era(), &11);
assert_eq!(
initial_exposure,
Exposure {
total: 1250,
own: 1000,
others: vec![IndividualExposure { who: 101, value: 250 }]
}
);
let nominator_stake = Staking::ledger(101.into()).unwrap().active;
let nominator_balance = asset::stakeable_balance::<Test>(&101);
let validator_stake = Staking::ledger(11.into()).unwrap().active;
add_slash(11);
Session::roll_next();
assert_eq!(
staking_events_since_last_call(),
vec![
Event::OffenceReported {
offence_era: 1,
validator: 11,
fraction: Perbill::from_percent(10)
},
Event::SlashComputed { offence_era: 1, slash_era: 3, offender: 11, page: 0 },
]
);
assert_eq!(Staking::ledger(101.into()).unwrap().active, nominator_stake);
assert_eq!(Staking::ledger(11.into()).unwrap().active, validator_stake);
Session::roll_until_active_era(3);
Session::roll_next();
assert_eq!(Staking::ledger(11.into()).unwrap().active, validator_stake - 100);
assert_eq!(Staking::ledger(101.into()).unwrap().active, nominator_stake);
assert_eq!(asset::stakeable_balance::<Test>(&101), nominator_balance);
});
}
#[test]
fn virtual_staker_not_slashed() {
ExtBuilder::default()
.validator_count(4)
.set_status(41, StakerStatus::Validator)
.set_nominators_slashable(false)
.build_and_execute(|| {
let pool_account = 200;
let payee = 201;
let pool_stake = 500;
bond_virtual_nominator(pool_account, payee, pool_stake, vec![11]);
Session::roll_until_active_era(2);
let exposure = Staking::eras_stakers(active_era(), &11);
assert_eq!(
exposure,
Exposure {
total: 1000 + pool_stake,
own: 1000,
others: vec![IndividualExposure { who: pool_account, value: pool_stake }]
}
);
let virtual_staker_stake = Staking::ledger(pool_account.into()).unwrap().active;
let validator_stake = Staking::ledger(11.into()).unwrap().active;
add_slash(11);
assert_eq!(SlashDeferDuration::get(), 0);
Session::roll_next();
assert_eq!(Staking::ledger(pool_account.into()).unwrap().active, virtual_staker_stake);
assert_eq!(Staking::ledger(11.into()).unwrap().active, validator_stake - 100);
});
}
#[test]
fn virtual_staker_unbonds_in_one_era() {
ExtBuilder::default().set_nominators_slashable(false).build_and_execute(|| {
let pool_account = 200;
let payee = 201;
let pool_stake = 500;
assert_ok!(<Staking as StakingUnchecked>::virtual_bond(&pool_account, pool_stake, &payee));
assert_ok!(Staking::nominate(RuntimeOrigin::signed(pool_account), vec![11]));
let era = active_era();
assert_eq!(era, 1);
assert_ok!(<Staking as StakingInterface>::unbond(&pool_account, 200));
let fast_unbond_era = era + NominatorFastUnbondDuration::get();
assert_eq!(
Staking::ledger(pool_account.into()).unwrap(),
StakingLedgerInspect {
stash: pool_account,
total: pool_stake,
active: pool_stake - 200,
unlocking: bounded_vec![UnlockChunk { value: 200, era: fast_unbond_era }],
}
);
assert_ok!(<Staking as StakingInterface>::withdraw_unbonded(pool_account, 0));
assert_eq!(Staking::ledger(pool_account.into()).unwrap().total, pool_stake);
Session::roll_until_active_era(fast_unbond_era);
assert_eq!(active_era(), fast_unbond_era);
assert_ok!(<Staking as StakingInterface>::withdraw_unbonded(pool_account, 0));
assert_eq!(
Staking::ledger(pool_account.into()).unwrap(),
StakingLedgerInspect {
stash: pool_account,
total: pool_stake - 200,
active: pool_stake - 200,
unlocking: Default::default(),
}
);
});
}
#[test]
fn nominator_bonding_duration_returns_one_when_not_slashable() {
ExtBuilder::default().set_nominators_slashable(false).build_and_execute(|| {
assert_eq!(
<Staking as StakingInterface>::nominator_bonding_duration(),
NominatorFastUnbondDuration::get(),
"nominator_bonding_duration should be NominatorFastUnbondDuration when nominators are not slashable"
);
assert_eq!(
<Staking as StakingInterface>::bonding_duration(),
BondingDuration::get(),
"bonding_duration should still be the full duration"
);
assert!(
BondingDuration::get() > NominatorFastUnbondDuration::get(),
"BondingDuration should be > NominatorFastUnbondDuration for this test"
);
});
}
#[test]
fn nominator_bonding_duration_returns_full_when_slashable() {
ExtBuilder::default().build_and_execute(|| {
assert_eq!(
<Staking as StakingInterface>::nominator_bonding_duration(),
<Staking as StakingInterface>::bonding_duration(),
"nominator_bonding_duration should equal bonding_duration when nominators are slashable"
);
});
}
#[test]
fn offence_from_slashable_era_slashes_nominators_even_after_setting_changes() {
ExtBuilder::default()
.validator_count(4)
.set_status(41, StakerStatus::Validator)
.build_and_execute(|| {
assert!(AreNominatorsSlashable::<Test>::get());
assert_eq!(active_era(), 1);
Session::roll_until_active_era(2);
assert!(
ErasNominatorsSlashable::<Test>::get(1).unwrap_or(true),
"Era 1 should have nominators slashable"
);
let nominator_stake_before = Staking::ledger(101.into()).unwrap().active;
let validator_stake_before = Staking::ledger(11.into()).unwrap().active;
AreNominatorsSlashable::<Test>::put(false);
add_slash_in_era(11, 1, Perbill::from_percent(10));
Session::roll_next();
let validator_stake_after = Staking::ledger(11.into()).unwrap().active;
let nominator_stake_after = Staking::ledger(101.into()).unwrap().active;
assert!(validator_stake_after < validator_stake_before, "Validator should be slashed");
assert!(
nominator_stake_after < nominator_stake_before,
"Nominator should be slashed because the offence was in a slashable era"
);
});
}
#[test]
fn offence_from_non_slashable_era_does_not_slash_nominators_even_after_setting_changes() {
ExtBuilder::default()
.validator_count(4)
.set_status(41, StakerStatus::Validator)
.set_nominators_slashable(false)
.build_and_execute(|| {
assert!(!AreNominatorsSlashable::<Test>::get());
assert_eq!(active_era(), 1);
Session::roll_until_active_era(2);
assert!(
!ErasNominatorsSlashable::<Test>::get(1).unwrap_or(true),
"Era 1 should have nominators NOT slashable"
);
let nominator_stake_before = Staking::ledger(101.into()).unwrap().active;
let validator_stake_before = Staking::ledger(11.into()).unwrap().active;
AreNominatorsSlashable::<Test>::put(true);
add_slash_in_era(11, 1, Perbill::from_percent(10));
Session::roll_next();
let validator_stake_after = Staking::ledger(11.into()).unwrap().active;
let nominator_stake_after = Staking::ledger(101.into()).unwrap().active;
assert!(validator_stake_after < validator_stake_before, "Validator should be slashed");
assert_eq!(
nominator_stake_after, nominator_stake_before,
"Nominator should NOT be slashed because the offence was in a non-slashable era"
);
});
}
#[test]
fn mixed_era_offences_processed_based_on_era_specific_setting() {
ExtBuilder::default()
.validator_count(4)
.set_status(41, StakerStatus::Validator)
.build_and_execute(|| {
assert!(AreNominatorsSlashable::<Test>::get());
assert_eq!(active_era(), 1);
Session::roll_until_active_era(2);
AreNominatorsSlashable::<Test>::put(false);
Session::roll_until_active_era(3);
assert!(
ErasNominatorsSlashable::<Test>::get(1).unwrap_or(true),
"Era 1 should be slashable"
);
assert!(
ErasNominatorsSlashable::<Test>::get(2).unwrap_or(true),
"Era 2 should be slashable"
);
assert!(
!ErasNominatorsSlashable::<Test>::get(3).unwrap_or(true),
"Era 3 should NOT be slashable"
);
let nominator_stake_before = Staking::ledger(101.into()).unwrap().active;
add_slash_in_era(11, 2, Perbill::from_percent(5));
Session::roll_next();
let nominator_stake_after_era2_slash = Staking::ledger(101.into()).unwrap().active;
assert!(
nominator_stake_after_era2_slash < nominator_stake_before,
"Nominator should be slashed for era 2 offence"
);
add_slash_in_era(11, 3, Perbill::from_percent(5));
Session::roll_next();
let nominator_stake_after_era3_slash = Staking::ledger(101.into()).unwrap().active;
assert_eq!(
nominator_stake_after_era3_slash, nominator_stake_after_era2_slash,
"Nominator should NOT be slashed for era 3 offence"
);
});
}
#[test]
fn validator_cannot_switch_to_nominator_to_avoid_slashing() {
ExtBuilder::default()
.set_nominators_slashable(false)
.slash_defer_duration(2) .build_and_execute(|| {
let alice = 11;
assert!(Validators::<Test>::contains_key(&alice));
assert!(!Nominators::<Test>::contains_key(&alice));
assert_eq!(active_era(), 1);
let validator_stake_before = Staking::ledger(alice.into()).unwrap().active;
assert_eq!(validator_stake_before, 1000);
add_slash(alice);
assert_eq!(
staking_events_since_last_call(),
vec![Event::OffenceReported {
offence_era: 1,
validator: alice,
fraction: Perbill::from_percent(10)
}]
);
Session::roll_next();
assert_eq!(
staking_events_since_last_call(),
vec![Event::SlashComputed {
offence_era: 1,
slash_era: 3,
offender: alice,
page: 0
}]
);
assert_ok!(Staking::nominate(RuntimeOrigin::signed(alice), vec![21]));
assert!(!Validators::<Test>::contains_key(&alice));
assert!(Nominators::<Test>::contains_key(&alice));
assert_ok!(Staking::unbond(RuntimeOrigin::signed(alice), 999));
assert_eq!(
staking_events_since_last_call(),
[Event::Unbonded { stash: alice, amount: 999 }]
);
assert!(Nominators::<Test>::contains_key(&alice));
let fast_unbond_era = 1 + NominatorFastUnbondDuration::get();
let validator_unbond_era = 1 + BondingDuration::get();
assert_eq!(NominatorFastUnbondDuration::get(), 2);
assert_eq!(BondingDuration::get(), 3);
assert_eq!(fast_unbond_era, 3);
assert_eq!(validator_unbond_era, 4);
assert_eq!(
Staking::ledger(alice.into()).unwrap(),
StakingLedgerInspect {
stash: alice,
total: 1000,
active: 1,
unlocking: bounded_vec![UnlockChunk { value: 999, era: validator_unbond_era }],
}
);
Session::roll_until_active_era(3);
let _ = staking_events_since_last_call();
assert_ok!(Staking::withdraw_unbonded(RuntimeOrigin::signed(alice), 0));
assert_eq!(staking_events_since_last_call(), []);
assert_eq!(
Staking::ledger(alice.into()).unwrap().total,
1000 );
Session::roll_next();
assert_eq!(
staking_events_since_last_call(),
[Event::Slashed { staker: alice, amount: 100 }]
);
let ledger_after_slash = Staking::ledger(alice.into()).unwrap();
assert_eq_error_rate!(
ledger_after_slash.total,
900, 1 );
Session::roll_until_active_era(4);
let _ = staking_events_since_last_call();
assert_ok!(Staking::withdraw_unbonded(RuntimeOrigin::signed(alice), 0));
assert_eq!(
staking_events_since_last_call(),
[
Event::StakerRemoved { stash: alice },
Event::Withdrawn { stash: alice, amount: 900 }
]
);
assert!(Staking::ledger(alice.into()).is_err(), "Ledger should be reaped");
});
}