#![cfg(test)]
mod mock;
pub(crate) const LOG_TARGET: &str = "tests::e2e-epm";
use frame_support::{assert_err, assert_noop, assert_ok};
use mock::*;
use pallet_timestamp::Now;
use sp_core::Get;
use sp_runtime::Perbill;
use crate::mock::RuntimeOrigin;
use pallet_election_provider_multi_phase::CurrentPhase;
#[macro_export]
macro_rules! log {
($level:tt, $patter:expr $(, $values:expr)* $(,)?) => {
log::$level!(
target: crate::LOG_TARGET,
concat!("🛠️ ", $patter) $(, $values)*
)
};
}
fn log_current_time() {
log!(
trace,
"block: {:?}, session: {:?}, era: {:?}, EPM phase: {:?} ts: {:?}",
System::block_number(),
Session::current_index(),
pallet_staking::CurrentEra::<Runtime>::get(),
CurrentPhase::<Runtime>::get(),
Now::<Runtime>::get()
);
}
#[test]
fn block_progression_works() {
let (ext, pool_state, _) = ExtBuilder::default().build_offchainify();
execute_with(ext, || {
assert_eq!(active_era(), 0);
assert_eq!(Session::current_index(), 0);
assert!(CurrentPhase::<Runtime>::get().is_off());
assert!(start_next_active_era(pool_state.clone()).is_ok());
assert_eq!(active_era(), 1);
assert_eq!(Session::current_index(), <SessionsPerEra as Get<u32>>::get());
assert!(CurrentPhase::<Runtime>::get().is_off());
roll_to_epm_signed();
assert!(CurrentPhase::<Runtime>::get().is_signed());
});
let (ext, pool_state, _) = ExtBuilder::default().build_offchainify();
execute_with(ext, || {
assert_eq!(active_era(), 0);
assert_eq!(Session::current_index(), 0);
assert!(CurrentPhase::<Runtime>::get().is_off());
assert!(start_next_active_era_delayed_solution(pool_state).is_ok());
assert!(CurrentPhase::<Runtime>::get().is_emergency());
assert_eq!(active_era(), 0);
assert_eq!(Session::current_index(), 2);
})
}
#[test]
fn offchainify_works() {
use pallet_election_provider_multi_phase::QueuedSolution;
let staking_builder = StakingExtBuilder::default();
let epm_builder = EpmExtBuilder::default();
let (ext, pool_state, _) = ExtBuilder::default()
.epm(epm_builder)
.staking(staking_builder)
.build_offchainify();
execute_with(ext, || {
for _ in 0..100 {
roll_one(pool_state.clone(), false);
let current_phase = CurrentPhase::<Runtime>::get();
assert!(
match QueuedSolution::<Runtime>::get() {
Some(_) => current_phase.is_unsigned(),
None => !current_phase.is_unsigned(),
},
"solution must be queued *only* in unsigned phase"
);
}
for _ in 0..100 {
roll_one(pool_state.clone(), true);
assert_eq!(
QueuedSolution::<Runtime>::get(),
None,
"solution must never be submitted and stored since it is delayed"
);
}
})
}
#[test]
fn mass_slash_doesnt_enter_emergency_phase() {
let epm_builder = EpmExtBuilder::default().disable_emergency_throttling();
let staking_builder = StakingExtBuilder::default().validator_count(7);
let (mut ext, _, _) = ExtBuilder::default()
.epm(epm_builder)
.staking(staking_builder)
.build_offchainify();
ext.execute_with(|| {
assert_eq!(pallet_staking::ForceEra::<Runtime>::get(), pallet_staking::Forcing::NotForcing);
let active_set_size_before_slash = Session::validators().len();
let slashed = slash_half_the_active_set();
let active_set_size_after_slash = Session::validators().len();
assert_eq!(active_set_size_before_slash, active_set_size_after_slash);
let active_set = Session::validators();
let potentially_disabled = slashed
.into_iter()
.map(|d| active_set.iter().position(|a| *a == d).unwrap() as u32)
.collect::<Vec<_>>();
let disabled = Session::disabled_validators();
for d in disabled.iter() {
assert!(potentially_disabled.contains(d));
}
let disabling_limit = pallet_staking::UpToLimitWithReEnablingDisablingStrategy::<
SLASHING_DISABLING_FACTOR,
>::disable_limit(active_set_size_before_slash);
assert!(disabled.len() == disabling_limit);
assert_eq!(pallet_staking::ForceEra::<Runtime>::get(), pallet_staking::Forcing::NotForcing);
});
}
#[test]
fn continuous_slashes_below_offending_threshold() {
let staking_builder = StakingExtBuilder::default().validator_count(10);
let epm_builder = EpmExtBuilder::default().disable_emergency_throttling();
let (ext, pool_state, _) = ExtBuilder::default()
.epm(epm_builder)
.staking(staking_builder)
.build_offchainify();
execute_with(ext, || {
assert_eq!(Session::validators().len(), 10);
let mut active_validator_set = Session::validators();
roll_to_epm_signed();
assert!(set_minimum_election_score(500, 1000, 500).is_ok());
while active_validator_set.len() > 0 {
let slashed = slash_percentage(Perbill::from_percent(10));
assert_eq!(slashed.len(), 1);
if start_next_active_era(pool_state.clone()).is_err() {
assert!(CurrentPhase::<Runtime>::get().is_emergency());
break;
}
active_validator_set = Session::validators();
log!(
trace,
"slashed 10% of active validators ({:?}). After slash: {:?}",
slashed,
active_validator_set
);
}
});
}
#[test]
fn ledger_consistency_active_balance_below_ed() {
use pallet_staking::{Error, Event};
let (ext, pool_state, _) =
ExtBuilder::default().staking(StakingExtBuilder::default()).build_offchainify();
execute_with(ext, || {
assert_eq!(Staking::ledger(11.into()).unwrap().active, 1000);
assert_noop!(
Staking::unbond(RuntimeOrigin::signed(11), 1000),
Error::<Runtime>::InsufficientBond
);
assert_ok!(Staking::chill(RuntimeOrigin::signed(11)));
assert_ok!(Staking::unbond(RuntimeOrigin::signed(11), 1000));
assert_eq!(Staking::ledger(11.into()).unwrap().active, 0);
assert_eq!(Staking::ledger(11.into()).unwrap().total, 1000);
assert_ok!(Staking::withdraw_unbonded(RuntimeOrigin::signed(11), 0));
assert_eq!(Staking::ledger(11.into()).unwrap().total, 1000);
assert_err!(
Staking::reap_stash(RuntimeOrigin::signed(11), 21, 0),
Error::<Runtime>::FundedTarget,
);
assert_eq!(
staking_events(),
[Event::Chilled { stash: 11 }, Event::Unbonded { stash: 11, amount: 1000 }]
);
advance_eras(
<Runtime as pallet_staking::Config>::BondingDuration::get() as usize,
pool_state,
);
assert_ok!(Staking::withdraw_unbonded(RuntimeOrigin::signed(11), 0));
assert!(Staking::ledger(11.into()).is_err());
});
}
#[test]
fn automatic_unbonding_pools() {
use pallet_nomination_pools::TotalValueLocked;
let unlocking_chunks_of = |account: AccountId| -> usize {
Staking::ledger(sp_staking::StakingAccount::Controller(account))
.unwrap()
.unlocking
.len()
};
let (ext, pool_state, _) = ExtBuilder::default()
.pools(PoolsExtBuilder::default().max_unbonding(1))
.staking(StakingExtBuilder::default().max_unlocking(1).bonding_duration(2))
.build_offchainify();
execute_with(ext, || {
assert_eq!(<Runtime as pallet_staking::Config>::MaxUnlockingChunks::get(), 1);
assert_eq!(<Runtime as pallet_staking::Config>::BondingDuration::get(), 2);
assert_eq!(<Runtime as pallet_nomination_pools::Config>::MaxUnbonding::get(), 1);
let init_stakeable_balance_2 = pallet_staking::asset::stakeable_balance::<Runtime>(&2);
let init_stakeable_balance_3 = pallet_staking::asset::stakeable_balance::<Runtime>(&3);
let pool_bonded_account = Pools::generate_bonded_account(1);
assert_ok!(Pools::create(RuntimeOrigin::signed(1), 5, 1, 1, 1));
assert_eq!(staked_amount_for(pool_bonded_account), 5);
let init_tvl = TotalValueLocked::<Runtime>::get();
assert_ok!(Pools::join(RuntimeOrigin::signed(2), 10, 1));
assert_eq!(staked_amount_for(pool_bonded_account), 15);
assert_ok!(Pools::join(RuntimeOrigin::signed(3), 10, 1));
assert_eq!(staked_amount_for(pool_bonded_account), 25);
assert_eq!(TotalValueLocked::<Runtime>::get(), 25);
assert_eq!(unlocking_chunks_of(pool_bonded_account), 0);
assert_ok!(Pools::unbond(RuntimeOrigin::signed(2), 2, 10));
assert_eq!(staked_amount_for(pool_bonded_account), 25);
assert_eq!(unlocking_chunks_of(pool_bonded_account), 1);
assert_err!(
Pools::unbond(RuntimeOrigin::signed(3), 3, 10),
pallet_staking::Error::<Runtime>::NoMoreChunks
);
assert_eq!(current_era(), 0);
for _ in 0..=<Runtime as pallet_staking::Config>::BondingDuration::get() {
start_next_active_era(pool_state.clone()).unwrap();
}
assert_eq!(current_era(), 3);
System::reset_events();
let staked_before_withdraw_pool = staked_amount_for(pool_bonded_account);
assert_eq!(pallet_staking::asset::stakeable_balance::<Runtime>(&pool_bonded_account), 26);
assert_ok!(Pools::unbond(RuntimeOrigin::signed(3), 3, 10));
assert_eq!(unlocking_chunks_of(pool_bonded_account), 1);
assert_eq!(
staking_events(),
[
pallet_staking::Event::Withdrawn { stash: 7939698191839293293, amount: 10 },
pallet_staking::Event::Unbonded { stash: 7939698191839293293, amount: 10 }
]
);
assert_eq!(pallet_staking::asset::stakeable_balance::<Runtime>(&pool_bonded_account), 26);
assert_eq!(staked_before_withdraw_pool - 10, staked_amount_for(pool_bonded_account));
assert_eq!(TotalValueLocked::<Runtime>::get(), 25 - 10);
assert_eq!(pallet_staking::asset::stakeable_balance::<Runtime>(&pool_bonded_account), 26);
assert_eq!(pallet_staking::asset::staked::<Runtime>(&pool_bonded_account), 15);
assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(2), 2, 10));
assert_eq!(pallet_staking::asset::stakeable_balance::<Runtime>(&pool_bonded_account), 16);
assert_eq!(pallet_staking::asset::stakeable_balance::<Runtime>(&2), 20);
assert_eq!(TotalValueLocked::<Runtime>::get(), 15);
assert_err!(
Pools::withdraw_unbonded(RuntimeOrigin::signed(3), 3, 10),
pallet_nomination_pools::Error::<Runtime>::CannotWithdrawAny
);
for _ in 0..=<Runtime as pallet_staking::Config>::BondingDuration::get() {
start_next_active_era(pool_state.clone()).unwrap();
}
assert_eq!(current_era(), 6);
System::reset_events();
assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(3), 3, 10));
assert_eq!(pallet_staking::asset::stakeable_balance::<Runtime>(&pool_bonded_account), 6); assert_eq!(
pallet_staking::asset::stakeable_balance::<Runtime>(&2),
init_stakeable_balance_2
);
assert_eq!(
pallet_staking::asset::stakeable_balance::<Runtime>(&3),
init_stakeable_balance_3
);
assert_eq!(TotalValueLocked::<Runtime>::get(), init_tvl);
});
}