From c1536fe57e31f62d13fdb4ab43d339a594526c23 Mon Sep 17 00:00:00 2001 From: Amar Singh Date: Wed, 9 Jun 2021 11:55:26 -0400 Subject: [PATCH] Staking Early Optimizations and Sudo Dispatchable Tests (#489) * impl changes * root dispatchable tests and NoWritingSameValue error type * add some log tracing for common error paths and order dependencies in alphabetical order * min balance is 0 on moonbeam so rm unnecessary min balance gets * fix docs and rename Underflow error to CannotBondLessGEQTotalBond * bump versions --- Cargo.lock | 3 +- pallets/parachain-staking/Cargo.toml | 13 +- pallets/parachain-staking/src/lib.rs | 276 +++++++------- pallets/parachain-staking/src/mock.rs | 85 +++++ pallets/parachain-staking/src/tests.rs | 491 ++++++++++++++----------- 5 files changed, 501 insertions(+), 367 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d21b5ab575..5d39545ac0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6575,11 +6575,12 @@ dependencies = [ [[package]] name = "parachain-staking" -version = "1.0.0" +version = "1.0.1" dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "log", "nimbus-primitives", "pallet-balances", "parity-scale-codec", diff --git a/pallets/parachain-staking/Cargo.toml b/pallets/parachain-staking/Cargo.toml index ad2983f056..5f85869ae5 100644 --- a/pallets/parachain-staking/Cargo.toml +++ b/pallets/parachain-staking/Cargo.toml @@ -1,37 +1,38 @@ [package] name = "parachain-staking" -version = "1.0.0" +version = "1.0.1" authors = ["PureStake"] edition = "2018" description = "parachain staking pallet for collator selection and reward distribution" [dependencies] -nimbus-primitives = { git = "https://github.com/purestake/cumulus", branch = "nimbus-polkadot-v9.3", default-features = false } +frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.3", default-features = false, optional = true } frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.3", default-features = false } frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.3", default-features = false } +log = "0.4" +nimbus-primitives = { git = "https://github.com/purestake/cumulus", branch = "nimbus-polkadot-v9.3", default-features = false } pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.3", default-features = false } parity-scale-codec = { version = "2.0.0", default-features = false, features = ["derive"] } serde = { version = "1.0.101", optional = true } sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.3", default-features = false } sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.3", default-features = false } substrate-fixed = { default-features = false, git = "https://github.com/encointer/substrate-fixed" } -frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.3", default-features = false, optional = true } [dev-dependencies] -sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.3", default-features = false } sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.3", default-features = false } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.3", default-features = false } [features] default = ["std"] std = [ - "nimbus-primitives/std", "frame-support/std", "frame-system/std", "frame-benchmarking/std", + "nimbus-primitives/std", "pallet-balances/std", "parity-scale-codec/std", "serde", - "sp-std/std", "sp-runtime/std", + "sp-std/std", ] runtime-benchmarks = ["frame-benchmarking"] diff --git a/pallets/parachain-staking/src/lib.rs b/pallets/parachain-staking/src/lib.rs index ab9c59d3de..a1f212371a 100644 --- a/pallets/parachain-staking/src/lib.rs +++ b/pallets/parachain-staking/src/lib.rs @@ -177,7 +177,7 @@ pub mod pallet { self.bond += more; self.total += more; } - // Returns None if underflow or less == self.bond (in which case collator should leave) + // Return None if less >= self.bond => collator must leave instead of bond less pub fn bond_less(&mut self, less: B) -> Option { if self.bond > less { self.bond -= less; @@ -259,8 +259,8 @@ pub mod pallet { false } } - // Returns Some(remaining balance), must be more than MinNominatorStk - // Returns None if nomination not found + // Return Some(remaining balance), must be more than MinNominatorStk + // Return None if nomination not found pub fn rm_nomination(&mut self, collator: AccountId) -> Option { let mut amt: Option = None; let nominations = self @@ -284,20 +284,20 @@ pub mod pallet { None } } - // Returns None if nomination not found - pub fn inc_nomination(&mut self, collator: AccountId, more: Balance) -> Option { + // Return false if nomination not found + pub fn inc_nomination(&mut self, collator: AccountId, more: Balance) -> bool { for x in &mut self.nominations.0 { if x.owner == collator { x.amount += more; self.total += more; - return Some(x.amount); + return true; } } - None + false } - // Returns Some(Some(balance)) if successful - // None if nomination not found - // Some(None) if underflow + // Return Some(Some(balance)) if successful + // Return None if nomination not found + // Return Some(None) if less >= nomination_total pub fn dec_nomination( &mut self, collator: AccountId, @@ -310,7 +310,7 @@ pub mod pallet { self.total -= less; return Some(Some(x.amount)); } else { - // underflow error; should rm entire nomination if x.amount == collator + // must rm entire nomination if x.amount <= less return Some(None); } } @@ -363,7 +363,7 @@ pub mod pallet { > Default for RoundInfo { fn default() -> RoundInfo { - RoundInfo::new(1u32, 1u32.into(), 20u32.into()) + RoundInfo::new(1u32, 1u32.into(), 20u32) } } @@ -442,9 +442,10 @@ pub mod pallet { ExceedMaxCollatorsPerNom, AlreadyNominatedCollator, NominationDNE, - Underflow, + CannotBondLessGEQTotalBond, InvalidSchedule, CannotSetBelowMin, + NoWritingSameValue, } #[pallet::event] @@ -665,10 +666,16 @@ pub mod pallet { T::Currency::free_balance(&candidate) >= balance, "Account does not have enough balance to bond as a cadidate." ); - let _ = >::join_candidates( + if let Err(error) = >::join_candidates( T::Origin::from(Some(candidate.clone()).into()), balance, - ); + ) { + log::trace!( + target: "staking", + "Join candidates failed in genesis with error {:?}", + error + ); + } } // Initialize the nominations for &(ref nominator, ref target, balance) in &self.nominations { @@ -676,11 +683,17 @@ pub mod pallet { T::Currency::free_balance(&nominator) >= balance, "Account does not have enough balance to place nomination." ); - let _ = >::nominate( + if let Err(error) = >::nominate( T::Origin::from(Some(nominator.clone()).into()), target.clone(), balance, - ); + ) { + log::trace!( + target: "staking", + "Join nominators failed in genesis with error {:?}", + error + ); + } } // Set collator commission to default config >::put(T::DefaultCollatorCommission::get()); @@ -721,6 +734,10 @@ pub mod pallet { frame_system::ensure_root(origin)?; ensure!(expectations.is_valid(), Error::::InvalidSchedule); let mut config = >::get(); + ensure!( + config.expect != expectations, + Error::::NoWritingSameValue + ); config.set_expectations(expectations); Self::deposit_event(Event::StakeExpectationsSet( config.expect.min, @@ -739,6 +756,7 @@ pub mod pallet { frame_system::ensure_root(origin)?; ensure!(schedule.is_valid(), Error::::InvalidSchedule); let mut config = >::get(); + ensure!(config.annual != schedule, Error::::NoWritingSameValue); config.annual = schedule; config.set_round_from_annual::(schedule); Self::deposit_event(Event::InflationSet( @@ -761,11 +779,12 @@ pub mod pallet { frame_system::ensure_root(origin)?; let ParachainBondConfig { account: old, - percent: pct, + percent, } = >::get(); + ensure!(old != new, Error::::NoWritingSameValue); >::put(ParachainBondConfig { account: new.clone(), - percent: pct, + percent, }); Self::deposit_event(Event::ParachainBondAccountSet(old, new)); Ok(().into()) @@ -778,11 +797,12 @@ pub mod pallet { ) -> DispatchResultWithPostInfo { frame_system::ensure_root(origin)?; let ParachainBondConfig { - account: acc, + account, percent: old, } = >::get(); + ensure!(old != new, Error::::NoWritingSameValue); >::put(ParachainBondConfig { - account: acc, + account, percent: new, }); Self::deposit_event(Event::ParachainBondReservePercentSet(old, new)); @@ -798,6 +818,7 @@ pub mod pallet { Error::::CannotSetBelowMin ); let old = >::get(); + ensure!(old != new, Error::::NoWritingSameValue); >::put(new); Self::deposit_event(Event::TotalSelectedSet(old, new)); Ok(().into()) @@ -806,12 +827,13 @@ pub mod pallet { /// Set the commission for all collators pub fn set_collator_commission( origin: OriginFor, - pct: Perbill, + new: Perbill, ) -> DispatchResultWithPostInfo { frame_system::ensure_root(origin)?; let old = >::get(); - >::put(pct); - Self::deposit_event(Event::CollatorCommissionSet(old, pct)); + ensure!(old != new, Error::::NoWritingSameValue); + >::put(new); + Self::deposit_event(Event::CollatorCommissionSet(old, new)); Ok(().into()) } #[pallet::weight(0)] @@ -827,6 +849,7 @@ pub mod pallet { ); let mut round = >::get(); let (now, first, old) = (round.current, round.first, round.length); + ensure!(old != new, Error::::NoWritingSameValue); round.length = new; // update per-round inflation given new rounds per year let mut inflation_config = >::get(); @@ -975,7 +998,9 @@ pub mod pallet { let mut state = >::get(&collator).ok_or(Error::::CandidateDNE)?; ensure!(!state.is_leaving(), Error::::CannotActivateIfLeaving); let before = state.bond; - let after = state.bond_less(less).ok_or(Error::::Underflow)?; + let after = state + .bond_less(less) + .ok_or(Error::::CannotBondLessGEQTotalBond)?; ensure!( after >= T::MinCollatorCandidateStk::get(), Error::::ValBondBelowMin @@ -997,82 +1022,56 @@ pub mod pallet { amount: BalanceOf, ) -> DispatchResultWithPostInfo { let acc = ensure_signed(origin)?; - if let Some(mut nominator) = >::get(&acc) { + let nominator = if let Some(mut nom) = >::get(&acc) { // nomination after first ensure!( amount >= T::MinNomination::get(), Error::::NominationBelowMin ); ensure!( - (nominator.nominations.0.len() as u32) < T::MaxCollatorsPerNominator::get(), + (nom.nominations.0.len() as u32) < T::MaxCollatorsPerNominator::get(), Error::::ExceedMaxCollatorsPerNom ); - let mut state = - >::get(&collator).ok_or(Error::::CandidateDNE)?; ensure!( - nominator.add_nomination(Bond { + nom.add_nomination(Bond { owner: collator.clone(), amount }), Error::::AlreadyNominatedCollator ); - let nomination = Bond { - owner: acc.clone(), - amount, - }; - ensure!( - (state.nominators.0.len() as u32) < T::MaxNominatorsPerCollator::get(), - Error::::TooManyNominators - ); - ensure!( - state.nominators.insert(nomination), - Error::::NominatorExists - ); - T::Currency::reserve(&acc, amount)?; - let new_total = state.total + amount; - if state.is_active() { - Self::update_active(collator.clone(), new_total); - } - let new_total_locked = >::get() + amount; - >::put(new_total_locked); - state.total = new_total; - >::insert(&collator, state); - >::insert(&acc, nominator); - Self::deposit_event(Event::Nomination(acc, amount, collator, new_total)); + nom } else { // first nomination ensure!( amount >= T::MinNominatorStk::get(), Error::::NomBondBelowMin ); - // cannot be a collator candidate and nominator with same AccountId ensure!(!Self::is_candidate(&acc), Error::::CandidateExists); - let mut state = - >::get(&collator).ok_or(Error::::CandidateDNE)?; - let nomination = Bond { + Nominator::new(collator.clone(), amount) + }; + let mut state = >::get(&collator).ok_or(Error::::CandidateDNE)?; + ensure!( + (state.nominators.0.len() as u32) < T::MaxNominatorsPerCollator::get(), + Error::::TooManyNominators + ); + ensure!( + state.nominators.insert(Bond { owner: acc.clone(), amount, - }; - ensure!( - state.nominators.insert(nomination), - Error::::NominatorExists - ); - ensure!( - (state.nominators.0.len() as u32) <= T::MaxNominatorsPerCollator::get(), - Error::::TooManyNominators - ); - T::Currency::reserve(&acc, amount)?; - let new_total = state.total + amount; - if state.is_active() { - Self::update_active(collator.clone(), new_total); - } - let new_total_locked = >::get() + amount; - >::put(new_total_locked); - state.total = new_total; - >::insert(&collator, state); - >::insert(&acc, Nominator::new(collator.clone(), amount)); - Self::deposit_event(Event::Nomination(acc, amount, collator, new_total)); + }), + Error::::NominatorExists + ); + T::Currency::reserve(&acc, amount)?; + let new_total = state.total + amount; + if state.is_active() { + Self::update_active(collator.clone(), new_total); } + let new_total_locked = >::get() + amount; + >::put(new_total_locked); + state.total = new_total; + >::insert(&collator, state); + >::insert(&acc, nominator); + Self::deposit_event(Event::Nomination(acc, amount, collator, new_total)); Ok(().into()) } /// Leave the set of nominators and, by implication, revoke all ongoing nominations @@ -1107,9 +1106,10 @@ pub mod pallet { >::get(&nominator).ok_or(Error::::NominatorDNE)?; let mut collator = >::get(&candidate).ok_or(Error::::CandidateDNE)?; - let _ = nominations - .inc_nomination(candidate.clone(), more) - .ok_or(Error::::NominationDNE)?; + ensure!( + nominations.inc_nomination(candidate.clone(), more), + Error::::NominationDNE + ); T::Currency::reserve(&nominator, more)?; let before = collator.total; collator.inc_nominator(nominator.clone(), more); @@ -1139,7 +1139,7 @@ pub mod pallet { let remaining = nominations .dec_nomination(candidate.clone(), less) .ok_or(Error::::NominationDNE)? - .ok_or(Error::::Underflow)?; + .ok_or(Error::::CannotBondLessGEQTotalBond)?; ensure!( remaining >= T::MinNomination::get(), Error::::NominationBelowMin @@ -1188,16 +1188,13 @@ pub mod pallet { fn compute_issuance(staked: BalanceOf) -> BalanceOf { let config = >::get(); let round_issuance = crate::inflation::round_issuance_range::(config.round); + // TODO: consider interpolation instead of bounded range if staked < config.expect.min { - return round_issuance.min; + round_issuance.min } else if staked > config.expect.max { - return round_issuance.max; + round_issuance.max } else { - // TODO: split up into 3 branches - // 1. min < staked < ideal - // 2. ideal < staked < max - // 3. staked == ideal - return round_issuance.ideal; + round_issuance.ideal } } fn nominator_revokes_collator( @@ -1265,66 +1262,57 @@ pub mod pallet { Ok(().into()) } fn pay_stakers(next: RoundIndex) { + // payout is next - duration rounds ago => next - duration > 0 else return early + let duration = T::BondDuration::get(); + if next <= duration { + return; + } + let round_to_payout = next - duration; + let total = >::get(round_to_payout); + if total.is_zero() { + return; + } + let total_staked = >::get(round_to_payout); + let mut issuance = Self::compute_issuance(total_staked); + // reserve portion of issuance for parachain bond account + let bond_config = >::get(); + let parachain_bond_reserve = bond_config.percent * issuance; + if let Ok(imb) = + T::Currency::deposit_into_existing(&bond_config.account, parachain_bond_reserve) + { + // update round issuance iff transfer succeeds + issuance -= imb.peek(); + Self::deposit_event(Event::ReservedForParachainBond( + bond_config.account, + imb.peek(), + )); + } let mint = |amt: BalanceOf, to: T::AccountId| { - if amt > T::Currency::minimum_balance() { - if let Ok(imb) = T::Currency::deposit_into_existing(&to, amt) { - Self::deposit_event(Event::Rewarded(to.clone(), imb.peek())); - } + if let Ok(imb) = T::Currency::deposit_into_existing(&to, amt) { + Self::deposit_event(Event::Rewarded(to.clone(), imb.peek())); } }; - let duration = T::BondDuration::get(); let collator_fee = >::get(); - if next > duration { - let round_to_payout = next - duration; - let total = >::get(round_to_payout); - if total.is_zero() { - // return early, no issuance - return; - } - let total_staked = >::get(round_to_payout); - let mut issuance = Self::compute_issuance(total_staked); - // reserve portion of issuance for parachain bond account - let bond_config = >::get(); - let parachain_bond_reserve = bond_config.percent * issuance; - if let Ok(imb) = - T::Currency::deposit_into_existing(&bond_config.account, parachain_bond_reserve) - { - // update round issuance iff transfer succeeds - issuance -= imb.peek(); - Self::deposit_event(Event::ReservedForParachainBond( - bond_config.account, - imb.peek(), - )); - } - for (val, pts) in >::drain_prefix(round_to_payout) { - let pct_due = Perbill::from_rational(pts, total); - let mut amt_due = pct_due * issuance; - if amt_due <= T::Currency::minimum_balance() { - continue; - } - // Take the snapshot of block author and nominations - let state = >::take(round_to_payout, &val); - if state.nominators.is_empty() { - // solo collator with no nominators - mint(amt_due, val.clone()); - } else { - // pay collator first; commission + due_portion - let val_pct = Perbill::from_rational(state.bond, state.total); - let commission = collator_fee * amt_due; - let val_due = if commission > T::Currency::minimum_balance() { - amt_due -= commission; - (val_pct * amt_due) + commission - } else { - // commission is negligible so not applied - val_pct * amt_due - }; - mint(val_due, val.clone()); - // pay nominators due portion - for Bond { owner, amount } in state.nominators { - let percent = Perbill::from_rational(amount, state.total); - let due = percent * amt_due; - mint(due, owner); - } + for (val, pts) in >::drain_prefix(round_to_payout) { + let pct_due = Perbill::from_rational(pts, total); + let mut amt_due = pct_due * issuance; + // Take the snapshot of block author and nominations + let state = >::take(round_to_payout, &val); + if state.nominators.is_empty() { + // solo collator with no nominators + mint(amt_due, val.clone()); + } else { + // pay collator first; commission + due_portion + let val_pct = Perbill::from_rational(state.bond, state.total); + let commission = collator_fee * amt_due; + amt_due -= commission; + let val_due = (val_pct * amt_due) + commission; + mint(val_due, val.clone()); + // pay nominators due portion + for Bond { owner, amount } in state.nominators { + let percent = Perbill::from_rational(amount, state.total); + let due = percent * amt_due; + mint(due, owner); } } } diff --git a/pallets/parachain-staking/src/mock.rs b/pallets/parachain-staking/src/mock.rs index 9be39da055..2889bca4c2 100644 --- a/pallets/parachain-staking/src/mock.rs +++ b/pallets/parachain-staking/src/mock.rs @@ -249,3 +249,88 @@ pub(crate) fn set_author(round: u32, acc: u64, pts: u32) { >::mutate(round, |p| *p += pts); >::mutate(round, acc, |p| *p += pts); } + +#[test] +fn geneses() { + ExtBuilder::default() + .with_balances(vec![ + (1, 1000), + (2, 300), + (3, 100), + (4, 100), + (5, 100), + (6, 100), + (7, 100), + (8, 9), + (9, 4), + ]) + .with_collators(vec![(1, 500), (2, 200)]) + .with_nominations(vec![(3, 1, 100), (4, 1, 100), (5, 2, 100), (6, 2, 100)]) + .build() + .execute_with(|| { + assert!(System::events().is_empty()); + // collators + assert_eq!(Balances::reserved_balance(&1), 500); + assert_eq!(Balances::free_balance(&1), 500); + assert!(Stake::is_candidate(&1)); + assert_eq!(Balances::reserved_balance(&2), 200); + assert_eq!(Balances::free_balance(&2), 100); + assert!(Stake::is_candidate(&2)); + // nominators + for x in 3..7 { + assert!(Stake::is_nominator(&x)); + assert_eq!(Balances::free_balance(&x), 0); + assert_eq!(Balances::reserved_balance(&x), 100); + } + // uninvolved + for x in 7..10 { + assert!(!Stake::is_nominator(&x)); + } + assert_eq!(Balances::free_balance(&7), 100); + assert_eq!(Balances::reserved_balance(&7), 0); + assert_eq!(Balances::free_balance(&8), 9); + assert_eq!(Balances::reserved_balance(&8), 0); + assert_eq!(Balances::free_balance(&9), 4); + assert_eq!(Balances::reserved_balance(&9), 0); + }); + ExtBuilder::default() + .with_balances(vec![ + (1, 100), + (2, 100), + (3, 100), + (4, 100), + (5, 100), + (6, 100), + (7, 100), + (8, 100), + (9, 100), + (10, 100), + ]) + .with_collators(vec![(1, 20), (2, 20), (3, 20), (4, 20), (5, 10)]) + .with_nominations(vec![ + (6, 1, 10), + (7, 1, 10), + (8, 2, 10), + (9, 2, 10), + (10, 1, 10), + ]) + .build() + .execute_with(|| { + assert!(System::events().is_empty()); + // collators + for x in 1..5 { + assert!(Stake::is_candidate(&x)); + assert_eq!(Balances::free_balance(&x), 80); + assert_eq!(Balances::reserved_balance(&x), 20); + } + assert!(Stake::is_candidate(&5)); + assert_eq!(Balances::free_balance(&5), 90); + assert_eq!(Balances::reserved_balance(&5), 10); + // nominators + for x in 6..11 { + assert!(Stake::is_nominator(&x)); + assert_eq!(Balances::free_balance(&x), 90); + assert_eq!(Balances::reserved_balance(&x), 10); + } + }); +} diff --git a/pallets/parachain-staking/src/tests.rs b/pallets/parachain-staking/src/tests.rs index 1155ff56dd..b6e1387dae 100644 --- a/pallets/parachain-staking/src/tests.rs +++ b/pallets/parachain-staking/src/tests.rs @@ -19,94 +19,11 @@ use crate::mock::{ events, last_event, roll_to, set_author, Balances, Event as MetaEvent, ExtBuilder, Origin, Stake, System, Test, }; -use crate::{CollatorStatus, Error, Event}; +use crate::{CollatorStatus, Error, Event, Range}; use frame_support::{assert_noop, assert_ok}; use sp_runtime::{traits::Zero, DispatchError, Perbill, Percent}; -#[test] -fn geneses() { - ExtBuilder::default() - .with_balances(vec![ - (1, 1000), - (2, 300), - (3, 100), - (4, 100), - (5, 100), - (6, 100), - (7, 100), - (8, 9), - (9, 4), - ]) - .with_collators(vec![(1, 500), (2, 200)]) - .with_nominations(vec![(3, 1, 100), (4, 1, 100), (5, 2, 100), (6, 2, 100)]) - .build() - .execute_with(|| { - assert!(System::events().is_empty()); - // collators - assert_eq!(Balances::reserved_balance(&1), 500); - assert_eq!(Balances::free_balance(&1), 500); - assert!(Stake::is_candidate(&1)); - assert_eq!(Balances::reserved_balance(&2), 200); - assert_eq!(Balances::free_balance(&2), 100); - assert!(Stake::is_candidate(&2)); - // nominators - for x in 3..7 { - assert!(Stake::is_nominator(&x)); - assert_eq!(Balances::free_balance(&x), 0); - assert_eq!(Balances::reserved_balance(&x), 100); - } - // uninvolved - for x in 7..10 { - assert!(!Stake::is_nominator(&x)); - } - assert_eq!(Balances::free_balance(&7), 100); - assert_eq!(Balances::reserved_balance(&7), 0); - assert_eq!(Balances::free_balance(&8), 9); - assert_eq!(Balances::reserved_balance(&8), 0); - assert_eq!(Balances::free_balance(&9), 4); - assert_eq!(Balances::reserved_balance(&9), 0); - }); - ExtBuilder::default() - .with_balances(vec![ - (1, 100), - (2, 100), - (3, 100), - (4, 100), - (5, 100), - (6, 100), - (7, 100), - (8, 100), - (9, 100), - (10, 100), - ]) - .with_collators(vec![(1, 20), (2, 20), (3, 20), (4, 20), (5, 10)]) - .with_nominations(vec![ - (6, 1, 10), - (7, 1, 10), - (8, 2, 10), - (9, 2, 10), - (10, 1, 10), - ]) - .build() - .execute_with(|| { - assert!(System::events().is_empty()); - // collators - for x in 1..5 { - assert!(Stake::is_candidate(&x)); - assert_eq!(Balances::free_balance(&x), 80); - assert_eq!(Balances::reserved_balance(&x), 20); - } - assert!(Stake::is_candidate(&5)); - assert_eq!(Balances::free_balance(&5), 90); - assert_eq!(Balances::reserved_balance(&5), 10); - // nominators - for x in 6..11 { - assert!(Stake::is_nominator(&x)); - assert_eq!(Balances::free_balance(&x), 90); - assert_eq!(Balances::reserved_balance(&x), 10); - } - }); -} +// ~~ PUBLIC DISPATCHABLES ~~ #[test] fn online_offline_works() { @@ -820,7 +737,7 @@ fn collators_bond() { assert_ok!(Stake::candidate_bond_less(Origin::signed(3), 10)); assert_noop!( Stake::candidate_bond_less(Origin::signed(2), 11), - Error::::Underflow + Error::::CannotBondLessGEQTotalBond ); assert_noop!( Stake::candidate_bond_less(Origin::signed(2), 1), @@ -878,7 +795,7 @@ fn nominators_bond() { ); assert_noop!( Stake::nominator_bond_less(Origin::signed(6), 1, 11), - Error::::Underflow + Error::::CannotBondLessGEQTotalBond ); assert_noop!( Stake::nominator_bond_less(Origin::signed(6), 1, 8), @@ -1143,135 +1060,6 @@ fn payouts_follow_nomination_changes() { }); } -#[test] -fn round_transitions() { - // round_immediately_jumps_if_current_duration_exceeds_new_blocks_per_round - ExtBuilder::default() - .with_balances(vec![ - (1, 100), - (2, 100), - (3, 100), - (4, 100), - (5, 100), - (6, 100), - ]) - .with_collators(vec![(1, 20)]) - .with_nominations(vec![(2, 1, 10), (3, 1, 10)]) - .build() - .execute_with(|| { - // Default round every 5 blocks, but MinBlocksPerRound is 3 and we set it to min 3 blocks - roll_to(8); - // chooses top TotalSelectedCandidates (5), in order - let init = vec![ - Event::CollatorChosen(2, 1, 40), - Event::NewRound(5, 2, 1, 40), - ]; - assert_eq!(events(), init); - assert_ok!(Stake::set_blocks_per_round(Origin::root(), 3u32)); - assert_eq!( - last_event(), - MetaEvent::stake(Event::BlocksPerRoundSet( - 2, - 5, - 5, - 3, - Perbill::from_parts(463), - Perbill::from_parts(463), - Perbill::from_parts(463) - )) - ); - roll_to(9); - assert_eq!(last_event(), MetaEvent::stake(Event::NewRound(8, 3, 1, 40))); - }); - // round_immediately_jumps_if_current_duration_exceeds_new_blocks_per_round - ExtBuilder::default() - .with_balances(vec![ - (1, 100), - (2, 100), - (3, 100), - (4, 100), - (5, 100), - (6, 100), - ]) - .with_collators(vec![(1, 20)]) - .with_nominations(vec![(2, 1, 10), (3, 1, 10)]) - .build() - .execute_with(|| { - roll_to(9); - let init = vec![ - Event::CollatorChosen(2, 1, 40), - Event::NewRound(5, 2, 1, 40), - ]; - assert_eq!(events(), init); - assert_ok!(Stake::set_blocks_per_round(Origin::root(), 3u32)); - assert_eq!( - last_event(), - MetaEvent::stake(Event::BlocksPerRoundSet( - 2, - 5, - 5, - 3, - Perbill::from_parts(463), - Perbill::from_parts(463), - Perbill::from_parts(463) - )) - ); - roll_to(10); - assert_eq!(last_event(), MetaEvent::stake(Event::NewRound(9, 3, 1, 40))); - }); - // if current duration less than new blocks per round (bpr), round waits until new bpr passes - ExtBuilder::default() - .with_balances(vec![ - (1, 100), - (2, 100), - (3, 100), - (4, 100), - (5, 100), - (6, 100), - ]) - .with_collators(vec![(1, 20)]) - .with_nominations(vec![(2, 1, 10), (3, 1, 10)]) - .build() - .execute_with(|| { - // Default round every 5 blocks, but MinBlocksPerRound is 3 and we set it to min 3 blocks - roll_to(6); - // chooses top TotalSelectedCandidates (5), in order - let init = vec![ - Event::CollatorChosen(2, 1, 40), - Event::NewRound(5, 2, 1, 40), - ]; - assert_eq!(events(), init); - assert_ok!(Stake::set_blocks_per_round(Origin::root(), 3u32)); - assert_eq!( - last_event(), - MetaEvent::stake(Event::BlocksPerRoundSet( - 2, - 5, - 5, - 3, - Perbill::from_parts(463), - Perbill::from_parts(463), - Perbill::from_parts(463) - )) - ); - roll_to(8); - assert_eq!( - last_event(), - MetaEvent::stake(Event::BlocksPerRoundSet( - 2, - 5, - 5, - 3, - Perbill::from_parts(463), - Perbill::from_parts(463), - Perbill::from_parts(463) - )) - ); - roll_to(9); - assert_eq!(last_event(), MetaEvent::stake(Event::NewRound(8, 3, 1, 40))); - }); -} - #[test] fn parachain_bond_reserve_works() { ExtBuilder::default() @@ -1474,3 +1262,274 @@ fn parachain_bond_reserve_works() { assert_eq!(Balances::free_balance(&11), 183); }); } + +// ~~ ROOT DISPATCHABLES ~~ + +#[test] +fn set_staking_expectations_works() { + ExtBuilder::default().build().execute_with(|| { + // invalid call fails + assert_noop!( + Stake::set_staking_expectations( + Origin::root(), + Range { + min: 5u32.into(), + ideal: 4u32.into(), + max: 3u32.into() + } + ), + Error::::InvalidSchedule + ); + let (min, ideal, max): (u128, u128, u128) = (3u32.into(), 4u32.into(), 5u32.into()); + // valid call succeeds + assert_ok!(Stake::set_staking_expectations( + Origin::root(), + Range { min, ideal, max } + ),); + // verify event emission + assert_eq!( + last_event(), + MetaEvent::stake(Event::StakeExpectationsSet(min, ideal, max)) + ); + // verify storage change + let config = Stake::inflation_config(); + assert_eq!(config.expect, Range { min, ideal, max }); + }); +} + +#[test] +fn set_inflation_works() { + ExtBuilder::default().build().execute_with(|| { + // invalid call fails + assert_noop!( + Stake::set_inflation( + Origin::root(), + Range { + min: Perbill::from_percent(5), + ideal: Perbill::from_percent(4), + max: Perbill::from_percent(3) + } + ), + Error::::InvalidSchedule + ); + let (min, ideal, max): (Perbill, Perbill, Perbill) = ( + Perbill::from_percent(3), + Perbill::from_percent(4), + Perbill::from_percent(5), + ); + // valid call succeeds + assert_ok!(Stake::set_inflation( + Origin::root(), + Range { min, ideal, max } + ),); + // verify event emission + assert_eq!( + last_event(), + MetaEvent::stake(Event::InflationSet( + Perbill::from_parts(30000000), + Perbill::from_parts(40000000), + Perbill::from_parts(50000000), + Perbill::from_parts(57), + Perbill::from_parts(75), + Perbill::from_parts(93) + )) + ); + // verify storage change + let config = Stake::inflation_config(); + assert_eq!(config.annual, Range { min, ideal, max }); + assert_eq!( + config.round, + Range { + min: Perbill::from_parts(57), + ideal: Perbill::from_parts(75), + max: Perbill::from_parts(93) + } + ); + // invalid call fails + assert_noop!( + Stake::set_inflation(Origin::root(), Range { min, ideal, max }), + Error::::NoWritingSameValue + ); + }); +} + +#[test] +fn set_total_selected_works() { + ExtBuilder::default().build().execute_with(|| { + // invalid call fails + assert_noop!( + Stake::set_total_selected(Origin::root(), 4u32), + Error::::CannotSetBelowMin + ); + // valid call succeeds + assert_ok!(Stake::set_total_selected(Origin::root(), 6u32)); + // verify event emission + assert_eq!( + last_event(), + MetaEvent::stake(Event::TotalSelectedSet(5u32, 6u32,)) + ); + // verify storage change + assert_eq!(Stake::total_selected(), 6u32); + // invalid call fails + assert_noop!( + Stake::set_total_selected(Origin::root(), 6u32), + Error::::NoWritingSameValue + ); + }); +} + +#[test] +fn set_collator_commission_works() { + ExtBuilder::default().build().execute_with(|| { + // valid call succeeds + assert_ok!(Stake::set_collator_commission( + Origin::root(), + Perbill::from_percent(5) + )); + // verify event emission + assert_eq!( + last_event(), + MetaEvent::stake(Event::CollatorCommissionSet( + Perbill::from_percent(20), + Perbill::from_percent(5), + )) + ); + // verify storage change + assert_eq!(Stake::collator_commission(), Perbill::from_percent(5)); + // invalid call fails + assert_noop!( + Stake::set_collator_commission(Origin::root(), Perbill::from_percent(5)), + Error::::NoWritingSameValue + ); + }); +} + +#[test] +fn mutable_blocks_per_round() { + // round_immediately_jumps_if_current_duration_exceeds_new_blocks_per_round + ExtBuilder::default() + .with_balances(vec![ + (1, 100), + (2, 100), + (3, 100), + (4, 100), + (5, 100), + (6, 100), + ]) + .with_collators(vec![(1, 20)]) + .with_nominations(vec![(2, 1, 10), (3, 1, 10)]) + .build() + .execute_with(|| { + assert_noop!( + Stake::set_blocks_per_round(Origin::root(), 2u32), + Error::::CannotSetBelowMin + ); + assert_noop!( + Stake::set_blocks_per_round(Origin::root(), 5u32), + Error::::NoWritingSameValue + ); + // Default round every 5 blocks, but MinBlocksPerRound is 3 and we set it to min 3 blocks + roll_to(8); + // chooses top TotalSelectedCandidates (5), in order + let init = vec![ + Event::CollatorChosen(2, 1, 40), + Event::NewRound(5, 2, 1, 40), + ]; + assert_eq!(events(), init); + assert_ok!(Stake::set_blocks_per_round(Origin::root(), 3u32)); + assert_eq!( + last_event(), + MetaEvent::stake(Event::BlocksPerRoundSet( + 2, + 5, + 5, + 3, + Perbill::from_parts(463), + Perbill::from_parts(463), + Perbill::from_parts(463) + )) + ); + roll_to(12); + assert_eq!( + last_event(), + MetaEvent::stake(Event::NewRound(11, 4, 1, 40)) + ); + }); + // round_immediately_jumps_if_current_duration_exceeds_new_blocks_per_round + ExtBuilder::default() + .with_balances(vec![ + (1, 100), + (2, 100), + (3, 100), + (4, 100), + (5, 100), + (6, 100), + ]) + .with_collators(vec![(1, 20)]) + .with_nominations(vec![(2, 1, 10), (3, 1, 10)]) + .build() + .execute_with(|| { + roll_to(9); + let init = vec![ + Event::CollatorChosen(2, 1, 40), + Event::NewRound(5, 2, 1, 40), + ]; + assert_eq!(events(), init); + assert_ok!(Stake::set_blocks_per_round(Origin::root(), 3u32)); + assert_eq!( + last_event(), + MetaEvent::stake(Event::BlocksPerRoundSet( + 2, + 5, + 5, + 3, + Perbill::from_parts(463), + Perbill::from_parts(463), + Perbill::from_parts(463) + )) + ); + roll_to(13); + assert_eq!( + last_event(), + MetaEvent::stake(Event::NewRound(12, 4, 1, 40)) + ); + }); + // if current duration less than new blocks per round (bpr), round waits until new bpr passes + ExtBuilder::default() + .with_balances(vec![ + (1, 100), + (2, 100), + (3, 100), + (4, 100), + (5, 100), + (6, 100), + ]) + .with_collators(vec![(1, 20)]) + .with_nominations(vec![(2, 1, 10), (3, 1, 10)]) + .build() + .execute_with(|| { + // Default round every 5 blocks, but MinBlocksPerRound is 3 and we set it to min 3 blocks + roll_to(6); + // chooses top TotalSelectedCandidates (5), in order + let init = vec![ + Event::CollatorChosen(2, 1, 40), + Event::NewRound(5, 2, 1, 40), + ]; + assert_eq!(events(), init); + assert_ok!(Stake::set_blocks_per_round(Origin::root(), 3u32)); + assert_eq!( + last_event(), + MetaEvent::stake(Event::BlocksPerRoundSet( + 2, + 5, + 5, + 3, + Perbill::from_parts(463), + Perbill::from_parts(463), + Perbill::from_parts(463) + )) + ); + roll_to(9); + assert_eq!(last_event(), MetaEvent::stake(Event::NewRound(8, 3, 1, 40))); + }); +}